Optimize DraftButtons component with improved documentation, code structure, and performance

This commit is contained in:
2025-03-22 19:33:46 +01:00
parent 54328baaec
commit 9bb1bf2cdb
+268 -168
View File
@@ -1,3 +1,10 @@
/**
* @file DraftButtons.tsx
* @description Component that provides functionality to add and remove draft watermarks
* on PowerPoint master slides. The watermarks appear as large, light-colored text
* across the slide background.
*/
import * as React from "react";
import {
Button,
@@ -10,6 +17,42 @@ import {
import { useStatusContext } from "./App";
import { useCommonStyles } from "./commonStyles";
/**
* Configuration constants for draft watermarks
*/
const DRAFT_CONFIG = {
// Shape name used to identify draft watermarks
SHAPE_NAME: "DraftWatermark",
// Font configuration
FONT: {
NAME: "Inter",
SIZE: 54,
COLOR: "#FFE9E8" // Light pink RGB(255, 233, 232)
},
// Text box configuration
TEXT_BOX: {
LEFT: -330,
TOP: 32,
WIDTH: 2400,
HEIGHT: 540
},
// Watermark text (repeated pattern)
TEXT: [
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT",
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT"
].join("\n")
};
/**
* Component-specific styles
*/
const useStyles = makeStyles({
buttonGrid: {
display: "grid",
@@ -27,16 +70,78 @@ const useStyles = makeStyles({
}
});
/**
* Interface for master slide processing results
*/
interface ProcessingResult {
processedMasters: number;
errorMasters: number;
affectedCount: number; // Either added or removed watermarks
}
/**
* DraftButtons component provides UI and functionality to add or remove
* draft watermarks on PowerPoint master slides.
*
* @returns React component
*/
export const DraftButtons: React.FC = () => {
const styles = useStyles();
const commonStyles = useCommonStyles();
const {
statusMessage, setStatusMessage,
statusType, setStatusType,
isProcessing, setIsProcessing
setStatusMessage,
setStatusType,
isProcessing,
setIsProcessing
} = useStatusContext();
const addDraftWatermark = async () => {
/**
* Handles errors and updates the status message
*
* @param error - The error object
* @param operation - The operation being performed (for logging)
*/
const handleError = (error: any, operation: string): void => {
setStatusMessage(`Error: ${error.message || "Unknown error occurred"}`);
setStatusType("error");
console.error(`${operation} error:`, error);
setIsProcessing(false);
};
/**
* Updates the status message based on processing results
*
* @param result - The processing result object
* @param isAddOperation - Whether this was an add operation (true) or remove operation (false)
*/
const updateStatusFromResult = (result: ProcessingResult, isAddOperation: boolean): void => {
const { processedMasters, errorMasters, affectedCount } = result;
if (affectedCount > 0) {
const action = isAddOperation ? "Added" : "Removed";
const pluralWatermark = affectedCount > 1 ? 's' : '';
const pluralMasters = processedMasters > 1 ? 's' : '';
setStatusMessage(`${action} draft watermark${pluralWatermark} to ${processedMasters} master slide${pluralMasters}.`);
setStatusType("success");
} else if (errorMasters > 0) {
const pluralMasters = errorMasters > 1 ? 's' : '';
setStatusMessage(`Failed to ${isAddOperation ? "add" : "remove"} watermarks. Errors on ${errorMasters} master slide${pluralMasters}.`);
setStatusType("error");
} else {
if (isAddOperation) {
setStatusMessage("No master slides found to add draft watermark.");
} else {
setStatusMessage("No draft watermark found to remove.");
}
setStatusType(isAddOperation ? "warning" : "info");
}
};
/**
* Adds draft watermarks to all master slides in the presentation
*/
const addDraftWatermark = async (): Promise<void> => {
setIsProcessing(true);
try {
await PowerPoint.run(async (context) => {
@@ -51,187 +156,182 @@ export const DraftButtons: React.FC = () => {
return;
}
// Process counter
let processedMasters = 0;
let errorMasters = 0;
// Process each slide master (usually there's just one)
for (let i = 0; i < masters.items.length; i++) {
try {
const master = masters.items[i];
// Create the textbox on the master slide
const textBox = master.shapes.addTextBox("");
// textBox.left = 0; // Center it horizontally
textBox.left = -330
textBox.top = 32;
// textBox.width = 960;
textBox.width = 2400;
textBox.height = 540;
await context.sync();
// Load textFrame to set text properties
textBox.load("textFrame");
await context.sync();
if (textBox.textFrame) {
// Load textRange to set text and properties
textBox.textFrame.load("textRange");
await context.sync();
// Need to load font and paragraphFormat
textBox.textFrame.textRange.load("font,paragraphFormat");
await context.sync();
// Set font properties
try {
// Ensure the font is loaded properly before setting properties
textBox.textFrame.textRange.font.load();
await context.sync();
// Set font properties exactly as in the VBA code
textBox.textFrame.textRange.font.name = "Inter";
// textBox.textFrame.textRange.font.size = 256;
textBox.textFrame.textRange.font.size = 54;
textBox.textFrame.textRange.font.bold = true;
textBox.textFrame.verticalAlignment = "MiddleCentered"
// Set the color to RGB(255, 233, 232)
// Different APIs may need different color formats
textBox.textFrame.textRange.font.color = "#FFE9E8";
// Set the text
textBox.textFrame.textRange.text =
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n" +
"DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT\n";
} catch (fontError) {
console.error("Error setting font properties:", fontError);
// Even if we can't set the font properties exactly, continue with default font
}
// Add a name/tag to the shape for identification
textBox.name = "DraftWatermark";
await context.sync();
processedMasters++;
}
} catch (masterError) {
console.error(`Error processing master ${i+1}:`, masterError);
errorMasters++;
// Continue to the next master
continue;
}
}
// Report results
if (processedMasters > 0) {
setStatusMessage(`Added draft watermark to ${processedMasters} master slide${processedMasters > 1 ? 's' : ''}.`);
setStatusType("success");
} else if (errorMasters > 0) {
setStatusMessage(`Failed to add markings. Errors on ${errorMasters} master slide${errorMasters > 1 ? 's' : ''}.`);
setStatusType("error");
} else {
setStatusMessage("No master slides found to add draft watermark.");
setStatusType("warning");
}
const result = await processAddWatermarks(context, masters.items);
updateStatusFromResult(result, true);
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
setStatusType("error");
console.error("Add draft watermark error:", error);
handleError(error, "Add draft watermark");
} finally {
setIsProcessing(false);
}
};
const removeDraftWatermark = async () => {
/**
* Processes all master slides to add draft watermarks
*
* @param context - The PowerPoint API context
* @param masters - Array of master slides to process
* @returns Processing result with counts of processed and error slides
*/
const processAddWatermarks = async (
context: PowerPoint.RequestContext,
masters: PowerPoint.SlideMaster[]
): Promise<ProcessingResult> => {
const result: ProcessingResult = {
processedMasters: 0,
errorMasters: 0,
affectedCount: 0
};
// Process each slide master (usually there's just one)
for (let i = 0; i < masters.length; i++) {
try {
const master = masters[i];
// Create the textbox on the master slide
const textBox = master.shapes.addTextBox("");
// Set position and size in a single batch
textBox.left = DRAFT_CONFIG.TEXT_BOX.LEFT;
textBox.top = DRAFT_CONFIG.TEXT_BOX.TOP;
textBox.width = DRAFT_CONFIG.TEXT_BOX.WIDTH;
textBox.height = DRAFT_CONFIG.TEXT_BOX.HEIGHT;
textBox.name = DRAFT_CONFIG.SHAPE_NAME;
// Load textFrame to set text properties
textBox.load("textFrame");
await context.sync();
if (textBox.textFrame) {
// Load textRange to set text and properties
textBox.textFrame.load("textRange");
await context.sync();
// Set text and load font properties in a single batch
textBox.textFrame.textRange.text = DRAFT_CONFIG.TEXT;
textBox.textFrame.textRange.load("font,paragraphFormat");
await context.sync();
try {
// Apply all font properties in a single batch
const font = textBox.textFrame.textRange.font;
font.name = DRAFT_CONFIG.FONT.NAME;
font.size = DRAFT_CONFIG.FONT.SIZE;
font.bold = true;
font.color = DRAFT_CONFIG.FONT.COLOR;
// Set alignment
textBox.textFrame.verticalAlignment = "MiddleCentered";
await context.sync();
result.affectedCount++;
} catch (fontError) {
console.error("Error setting font properties:", fontError);
// Continue with default font if custom font fails
}
}
result.processedMasters++;
} catch (masterError) {
console.error(`Error processing master ${i+1}:`, masterError);
result.errorMasters++;
// Continue to the next master
}
}
return result;
};
/**
* Removes draft watermarks from all master slides in the presentation
*/
const removeDraftWatermark = async (): Promise<void> => {
setIsProcessing(true);
try {
await PowerPoint.run(async (context) => {
try {
// Get the slide masters collection
const masters = context.presentation.slideMasters;
masters.load("items");
await context.sync();
if (masters.items.length === 0) {
setStatusMessage("Could not access slide masters.");
setStatusType("error");
return;
}
// Process counter
let processedMasters = 0;
let errorMasters = 0;
let removedCount = 0;
// Process each master slide
for (let i = 0; i < masters.items.length; i++) {
try {
const master = masters.items[i];
// Load all shapes on the master slide
master.shapes.load("items");
await context.sync();
// Find shapes with name "DraftWatermark"
for (let j = 0; j < master.shapes.items.length; j++) {
const shape = master.shapes.items[j];
shape.load("name");
await context.sync();
if (shape.name === "DraftWatermark") {
// Delete the draft watermark shape
shape.delete();
removedCount++;
}
}
await context.sync();
processedMasters++;
} catch (masterError) {
console.error(`Error processing master slide ${i+1}:`, masterError);
errorMasters++;
// Continue to the next master slide
continue;
}
}
// Report results
if (removedCount > 0) {
setStatusMessage(`Removed ${removedCount} draft watermark${removedCount > 1 ? 's' : ''} from ${processedMasters} master slide${processedMasters > 1 ? 's' : ''}.`);
setStatusType("success");
} else if (errorMasters > 0) {
setStatusMessage(`Failed to remove draft watermarks. Errors on ${errorMasters} master slide${errorMasters > 1 ? 's' : ''}.`);
setStatusType("error");
} else {
setStatusMessage("No draft watermark found to remove.");
setStatusType("info");
}
} catch (innerError) {
console.error("Inner error:", innerError);
throw innerError; // Re-throw to outer catch
// Get the slide masters collection
const masters = context.presentation.slideMasters;
masters.load("items");
await context.sync();
if (masters.items.length === 0) {
setStatusMessage("Could not access slide masters.");
setStatusType("error");
return;
}
const result = await processRemoveWatermarks(context, masters.items);
updateStatusFromResult(result, false);
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
setStatusType("error");
console.error("Remove draft watermark error:", error);
handleError(error, "Remove draft watermark");
} finally {
setIsProcessing(false);
}
};
/**
* Processes all master slides to remove draft watermarks
*
* @param context - The PowerPoint API context
* @param masters - Array of master slides to process
* @returns Processing result with counts of processed and affected slides
*/
const processRemoveWatermarks = async (
context: PowerPoint.RequestContext,
masters: PowerPoint.SlideMaster[]
): Promise<ProcessingResult> => {
const result: ProcessingResult = {
processedMasters: 0,
errorMasters: 0,
affectedCount: 0
};
// Process each master slide
for (let i = 0; i < masters.length; i++) {
try {
const master = masters[i];
// Load all shapes on the master slide
master.shapes.load("items");
await context.sync();
// Collect shapes to delete
const shapesToDelete: PowerPoint.Shape[] = [];
// Load all shape names in a single batch
for (let j = 0; j < master.shapes.items.length; j++) {
const shape = master.shapes.items[j];
shape.load("name");
}
// Wait for all shape names to load
await context.sync();
// Now check names and collect shapes to delete
for (let j = 0; j < master.shapes.items.length; j++) {
const shape = master.shapes.items[j];
if (shape.name === DRAFT_CONFIG.SHAPE_NAME) {
shapesToDelete.push(shape);
}
}
// Delete all matching shapes in one batch
shapesToDelete.forEach(shape => shape.delete());
result.affectedCount += shapesToDelete.length;
await context.sync();
result.processedMasters++;
} catch (masterError) {
console.error(`Error processing master slide ${i+1}:`, masterError);
result.errorMasters++;
// Continue to the next master slide
}
}
return result;
};
return (
<div className={commonStyles.container}>
<div className={styles.buttonGrid}>
@@ -260,4 +360,4 @@ export const DraftButtons: React.FC = () => {
);
};
export default DraftButtons;
export default DraftButtons;