From 54328baaecae96ba4edffe58438c393168c38317 Mon Sep 17 00:00:00 2001 From: Heiko Joerg Schick Date: Sat, 22 Mar 2025 19:31:07 +0100 Subject: [PATCH] Optimize ConfidentialButtons component with improved documentation, code structure, and performance --- .../components/ConfidentialButtons.tsx | 453 ++++++++++-------- 1 file changed, 266 insertions(+), 187 deletions(-) diff --git a/src/taskpane/components/ConfidentialButtons.tsx b/src/taskpane/components/ConfidentialButtons.tsx index 7281f91b..5d7a7f07 100644 --- a/src/taskpane/components/ConfidentialButtons.tsx +++ b/src/taskpane/components/ConfidentialButtons.tsx @@ -1,3 +1,9 @@ +/** + * @file ConfidentialButtons.tsx + * @description Component that provides functionality to add and remove confidential markings + * to PowerPoint slides. The markings appear as text at the bottom of each slide. + */ + import * as React from "react"; import { Button, @@ -10,6 +16,35 @@ import { import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; +/** + * Configuration constants for confidential markings + */ +const CONFIDENTIAL_CONFIG = { + // Marking text that appears on slides + TEXT: "– Confidential –", + // Shape name used to identify confidential markings + SHAPE_NAME: "ConfidentialMarking", + // Font configuration + FONT: { + NAME: "Inter", + SIZE: 8, + COLOR: "#DA1335" // RGB(218, 19, 53) + }, + // Default slide dimensions in points (if we can't get actual dimensions) + SLIDE: { + WIDTH: 960, // 10 inches * 72 points + HEIGHT: 540, // 7.5 inches * 72 points + }, + // Text box configuration + TEXT_BOX: { + HEIGHT: 25, + POSITION_FROM_BOTTOM: 23 + } +}; + +/** + * Component-specific styles + */ const useStyles = makeStyles({ buttonGrid: { display: "grid", @@ -27,220 +62,264 @@ const useStyles = makeStyles({ } }); +/** + * Interface for slide processing results + */ +interface ProcessingResult { + processedSlides: number; + errorSlides: number; + affectedCount: number; // Either added or removed markings +} + +/** + * ConfidentialButtons component provides UI and functionality to add or remove + * confidential markings on PowerPoint slides. + * + * @returns React component + */ export const ConfidentialButtons: React.FC = () => { const styles = useStyles(); const commonStyles = useCommonStyles(); const { - statusMessage, setStatusMessage, - statusType, setStatusType, - isProcessing, setIsProcessing + setStatusMessage, + setStatusType, + isProcessing, + setIsProcessing } = useStatusContext(); - const addConfidentialMarking = 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 { processedSlides, errorSlides, affectedCount } = result; + + if (affectedCount > 0) { + const action = isAddOperation ? "Added" : "Removed"; + setStatusMessage(`${action} confidential marking to ${affectedCount} slides.`); + setStatusType("success"); + } else if (errorSlides > 0) { + setStatusMessage(`Failed to ${isAddOperation ? "add" : "remove"} markings. Errors on ${errorSlides} slides.`); + setStatusType("error"); + } else { + if (isAddOperation) { + setStatusMessage("No slides found to add confidential marking."); + } else { + setStatusMessage("No confidential markings found to remove."); + } + setStatusType(isAddOperation ? "warning" : "info"); + } + }; + + /** + * Adds confidential markings to all slides in the presentation + */ + const addConfidentialMarking = async (): Promise => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { - try { - // Get all slides in the presentation - const slides = context.presentation.slides; - slides.load("items"); - await context.sync(); - - // Get a reference slide to determine dimensions - // Since we can't access presentation width/height directly - if (slides.items.length === 0) { - setStatusMessage("No slides found in the presentation."); - setStatusType("warning"); - return; - } - - // Get dimensions from the first slide - const firstSlide = slides.items[0]; - - // Standard PowerPoint slide dimensions (in points) - // We'll use these as fallbacks and adjust if we can get actual dimensions - let slideWidth = 960; // Default slide width (10 inches * 72 points) - let slideHeight = 540; // Default slide height (7.5 inches * 72 points) - - // Process counter - let processedSlides = 0; - let errorSlides = 0; - - // Process each slide - for (let i = 0; i < slides.items.length; i++) { - try { - const slide = slides.items[i]; - - const positionFromTop = slideHeight - 23; - - // Create the textbox - make sure it spans the full width of the slide - // This is important for proper centering - const textBox = slide.shapes.addTextBox(""); - - // Make it span most of the width of the slide (90% centered) - // This ensures we have room for the text to be centered - const textBoxWidth = slideWidth * 1; // 0.9 - textBox.left = (slideWidth - textBoxWidth) / 2; // Center it horizontally - textBox.top = positionFromTop; - textBox.width = textBoxWidth; - textBox.height = 25; // Smaller height for footer text - - 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 = 8; - textBox.textFrame.textRange.paragraphFormat.horizontalAlignment = "Center" - - // Set the color to RGB(218, 19, 53) - // Different APIs may need different color formats - textBox.textFrame.textRange.font.color = "#DA1335"; - - // Set the text - textBox.textFrame.textRange.text = "– Confidential –"; - } 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 = "ConfidentialMarking"; - - await context.sync(); - processedSlides++; - } - } catch (slideError) { - console.error(`Error processing slide ${i+1}:`, slideError); - errorSlides++; - // Continue to the next slide - continue; - } - } - - // Report results - if (processedSlides > 0) { - setStatusMessage(`Added confidential marking to ${processedSlides} slides.`); - setStatusType("success"); - } else if (errorSlides > 0) { - setStatusMessage(`Failed to add markings. Errors on ${errorSlides} slides.`); - setStatusType("error"); - } else { - setStatusMessage("No slides found to add confidential marking."); - setStatusType("warning"); - } - - } catch (innerError) { - console.error("Inner error:", innerError); - throw innerError; // Re-throw to outer catch + // Get all slides in the presentation + const slides = context.presentation.slides; + slides.load("items"); + await context.sync(); + + if (slides.items.length === 0) { + setStatusMessage("No slides found in the presentation."); + setStatusType("warning"); + return; } + + const result = await processAddMarkings(context, slides.items); + updateStatusFromResult(result, true); }); } catch (error) { - setStatusMessage(`Error: ${error.message}`); - setStatusType("error"); - console.error("Add confidential error:", error); + handleError(error, "Add confidential"); } finally { setIsProcessing(false); } }; - const removeConfidentialMarking = async () => { + /** + * Processes all slides to add confidential markings + * + * @param context - The PowerPoint API context + * @param slides - Array of slides to process + * @returns Processing result with counts of processed and error slides + */ + const processAddMarkings = async ( + context: PowerPoint.RequestContext, + slides: PowerPoint.Slide[] + ): Promise => { + const result: ProcessingResult = { + processedSlides: 0, + errorSlides: 0, + affectedCount: 0 + }; + + const { WIDTH, HEIGHT } = CONFIDENTIAL_CONFIG.SLIDE; + const slideWidth = WIDTH; + const slideHeight = HEIGHT; + + // Process each slide + for (let i = 0; i < slides.length; i++) { + try { + const slide = slides[i]; + const positionFromTop = slideHeight - CONFIDENTIAL_CONFIG.TEXT_BOX.POSITION_FROM_BOTTOM; + + // Create the textbox spanning the full width of the slide + const textBox = slide.shapes.addTextBox(""); + textBox.left = 0; // Start from left edge + textBox.top = positionFromTop; + textBox.width = slideWidth; + textBox.height = CONFIDENTIAL_CONFIG.TEXT_BOX.HEIGHT; + textBox.name = CONFIDENTIAL_CONFIG.SHAPE_NAME; + + // Load textFrame and its properties in a single batch + textBox.load("textFrame"); + await context.sync(); + + if (textBox.textFrame) { + // Load textRange and its properties + textBox.textFrame.load("textRange"); + await context.sync(); + + // Set text and formatting in a single batch + textBox.textFrame.textRange.text = CONFIDENTIAL_CONFIG.TEXT; + textBox.textFrame.textRange.load("font,paragraphFormat"); + await context.sync(); + + try { + // Apply font properties in a single batch + const font = textBox.textFrame.textRange.font; + font.name = CONFIDENTIAL_CONFIG.FONT.NAME; + font.size = CONFIDENTIAL_CONFIG.FONT.SIZE; + font.color = CONFIDENTIAL_CONFIG.FONT.COLOR; + textBox.textFrame.textRange.paragraphFormat.horizontalAlignment = "Center"; + + await context.sync(); + result.affectedCount++; + } catch (fontError) { + console.error("Error setting font properties:", fontError); + // Continue with default font if custom font fails + } + } + + result.processedSlides++; + } catch (slideError) { + console.error(`Error processing slide ${i+1}:`, slideError); + result.errorSlides++; + // Continue to the next slide + } + } + + return result; + }; + + /** + * Removes confidential markings from all slides in the presentation + */ + const removeConfidentialMarking = async (): Promise => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { - try { - // Get all slides in the presentation - const slides = context.presentation.slides; - slides.load("items"); - await context.sync(); - - if (slides.items.length === 0) { - setStatusMessage("No slides found in the presentation."); - setStatusType("warning"); - return; - } - - // Process counter - let processedSlides = 0; - let errorSlides = 0; - let removedCount = 0; - - // Process each slide - for (let i = 0; i < slides.items.length; i++) { - try { - const slide = slides.items[i]; - - // Load all shapes on the slide - slide.shapes.load("items"); - await context.sync(); - - // Find shapes with name "ConfidentialMarking" - for (let j = 0; j < slide.shapes.items.length; j++) { - const shape = slide.shapes.items[j]; - shape.load("name"); - await context.sync(); - - if (shape.name === "ConfidentialMarking") { - // Delete the confidential marking shape - shape.delete(); - removedCount++; - } - } - - await context.sync(); - processedSlides++; - } catch (slideError) { - console.error(`Error processing slide ${i+1}:`, slideError); - errorSlides++; - // Continue to the next slide - continue; - } - } - - // Report results - if (removedCount > 0) { - setStatusMessage(`Removed ${removedCount} confidential markings from ${processedSlides} slides.`); - setStatusType("success"); - } else if (errorSlides > 0) { - setStatusMessage(`Failed to remove markings. Errors on ${errorSlides} slides.`); - setStatusType("error"); - } else { - setStatusMessage("No confidential markings found to remove."); - setStatusType("info"); - } - - } catch (innerError) { - console.error("Inner error:", innerError); - throw innerError; // Re-throw to outer catch + // Get all slides in the presentation + const slides = context.presentation.slides; + slides.load("items"); + await context.sync(); + + if (slides.items.length === 0) { + setStatusMessage("No slides found in the presentation."); + setStatusType("warning"); + return; } + + const result = await processRemoveMarkings(context, slides.items); + updateStatusFromResult(result, false); }); } catch (error) { - setStatusMessage(`Error: ${error.message}`); - setStatusType("error"); - console.error("Remove confidential error:", error); + handleError(error, "Remove confidential"); } finally { setIsProcessing(false); } }; + /** + * Processes all slides to remove confidential markings + * + * @param context - The PowerPoint API context + * @param slides - Array of slides to process + * @returns Processing result with counts of processed and affected slides + */ + const processRemoveMarkings = async ( + context: PowerPoint.RequestContext, + slides: PowerPoint.Slide[] + ): Promise => { + const result: ProcessingResult = { + processedSlides: 0, + errorSlides: 0, + affectedCount: 0 + }; + + // Process each slide + for (let i = 0; i < slides.length; i++) { + try { + const slide = slides[i]; + + // Load all shapes on the slide + slide.shapes.load("items"); + await context.sync(); + + // Collect shapes to delete + const shapesToDelete: PowerPoint.Shape[] = []; + + // Find shapes with the confidential marking name + for (let j = 0; j < slide.shapes.items.length; j++) { + const shape = slide.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 < slide.shapes.items.length; j++) { + const shape = slide.shapes.items[j]; + if (shape.name === CONFIDENTIAL_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.processedSlides++; + } catch (slideError) { + console.error(`Error processing slide ${i+1}:`, slideError); + result.errorSlides++; + // Continue to the next slide + } + } + + return result; + }; + return (
@@ -269,4 +348,4 @@ export const ConfidentialButtons: React.FC = () => { ); }; -export default ConfidentialButtons; \ No newline at end of file +export default ConfidentialButtons;