Refactor InsertTitles component for better maintainability and performance
This commit is contained in:
@@ -1,172 +1,275 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import { TextBulletListRegular } from "@fluentui/react-icons";
|
||||||
Button
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import {
|
|
||||||
TextBulletListRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
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 = () => {
|
export const InsertTitles: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
const {
|
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||||
statusMessage, setStatusMessage,
|
|
||||||
statusType, setStatusType,
|
/**
|
||||||
isProcessing, setIsProcessing
|
* Validates that a text box is selected
|
||||||
} = useStatusContext();
|
* @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 {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
// Process each slide
|
||||||
try {
|
for (let i = 0; i < slides.items.length; i++) {
|
||||||
// Get the selected shape
|
const slide = slides.items[i];
|
||||||
const selectedShapes = context.presentation.getSelectedShapes();
|
|
||||||
selectedShapes.load("items");
|
// Load shapes collection
|
||||||
await context.sync();
|
slide.load("shapes");
|
||||||
|
await context.sync();
|
||||||
// Check if any shape is selected
|
|
||||||
if (selectedShapes.items.length === 0) {
|
const shapes = slide.shapes;
|
||||||
setStatusMessage("Please select a text box to insert slide titles.");
|
shapes.load("items");
|
||||||
setStatusType("warning");
|
await context.sync();
|
||||||
return;
|
|
||||||
}
|
// Find the title shape on this slide
|
||||||
|
const titleShape = await findTitleShape(shapes, context, i + 1);
|
||||||
// Get the first selected shape
|
|
||||||
const selectedShape = selectedShapes.items[0];
|
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
|
// Add to our collection
|
||||||
selectedShape.load("textFrame");
|
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();
|
await context.sync();
|
||||||
|
|
||||||
if (!selectedShape.textFrame) {
|
if (shape.textFrame.textRange) {
|
||||||
setStatusMessage("Please select a text box to insert slide titles.");
|
shape.textFrame.textRange.load("text");
|
||||||
setStatusType("warning");
|
await context.sync();
|
||||||
return;
|
|
||||||
}
|
const shapeText = shape.textFrame.textRange.text;
|
||||||
|
|
||||||
// Get all slides in the presentation
|
// Check if the shape has text
|
||||||
const slides = context.presentation.slides;
|
if (shapeText && shapeText.trim() !== "") {
|
||||||
slides.load("items");
|
// Load position to check if it's at the top
|
||||||
await context.sync();
|
shape.load("top");
|
||||||
|
|
||||||
// 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");
|
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Get title text using a simplified approach
|
// Use position as a heuristic for identifying titles
|
||||||
// This gets all the shapes and looks for one with text content positioned at the top
|
if (shape.top < TITLE_POSITION_THRESHOLD) {
|
||||||
const shapes = slide.shapes;
|
return { text: shapeText };
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} 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) {
|
} catch (error) {
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
console.error("Error in collectAndInsertTitles:", getErrorMessage(error));
|
||||||
|
setStatusMessage(`Error: ${getErrorMessage(error)}`);
|
||||||
setStatusType("error");
|
setStatusType("error");
|
||||||
console.error("Collect titles error:", error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button
|
<ActionButton
|
||||||
appearance="primary"
|
title="Insert Titles"
|
||||||
className={styles.actionButton}
|
|
||||||
onClick={collectAndInsertTitles}
|
|
||||||
icon={<TextBulletListRegular />}
|
icon={<TextBulletListRegular />}
|
||||||
disabled={isProcessing}
|
onClick={collectAndInsertTitles}
|
||||||
>
|
/>
|
||||||
Insert Titles
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InsertTitles;
|
export default InsertTitles;
|
||||||
|
|||||||
Reference in New Issue
Block a user