From 9bb1bf2cdbd76c8ea18d3f3c8aac59d4dbfac297 Mon Sep 17 00:00:00 2001 From: Heiko Joerg Schick Date: Sat, 22 Mar 2025 19:33:46 +0100 Subject: [PATCH] Optimize DraftButtons component with improved documentation, code structure, and performance --- src/taskpane/components/DraftButtons.tsx | 436 ++++++++++++++--------- 1 file changed, 268 insertions(+), 168 deletions(-) diff --git a/src/taskpane/components/DraftButtons.tsx b/src/taskpane/components/DraftButtons.tsx index 7d0020f7..a631be34 100644 --- a/src/taskpane/components/DraftButtons.tsx +++ b/src/taskpane/components/DraftButtons.tsx @@ -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 => { 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 => { + 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 => { 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 => { + 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 (
@@ -260,4 +360,4 @@ export const DraftButtons: React.FC = () => { ); }; -export default DraftButtons; \ No newline at end of file +export default DraftButtons;