Optimize DraftButtons component with improved documentation, code structure, and performance
This commit is contained in:
@@ -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 * as React from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -10,6 +17,42 @@ import {
|
|||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
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({
|
const useStyles = makeStyles({
|
||||||
buttonGrid: {
|
buttonGrid: {
|
||||||
display: "grid",
|
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 = () => {
|
export const DraftButtons: React.FC = () => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const {
|
const {
|
||||||
statusMessage, setStatusMessage,
|
setStatusMessage,
|
||||||
statusType, setStatusType,
|
setStatusType,
|
||||||
isProcessing, setIsProcessing
|
isProcessing,
|
||||||
|
setIsProcessing
|
||||||
} = useStatusContext();
|
} = 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);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
await PowerPoint.run(async (context) => {
|
||||||
@@ -51,187 +156,182 @@ export const DraftButtons: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process counter
|
const result = await processAddWatermarks(context, masters.items);
|
||||||
let processedMasters = 0;
|
updateStatusFromResult(result, true);
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
handleError(error, "Add draft watermark");
|
||||||
setStatusType("error");
|
|
||||||
console.error("Add draft watermark error:", error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
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);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
await PowerPoint.run(async (context) => {
|
||||||
try {
|
// Get the slide masters collection
|
||||||
// Get the slide masters collection
|
const masters = context.presentation.slideMasters;
|
||||||
const masters = context.presentation.slideMasters;
|
masters.load("items");
|
||||||
masters.load("items");
|
await context.sync();
|
||||||
await context.sync();
|
|
||||||
|
if (masters.items.length === 0) {
|
||||||
if (masters.items.length === 0) {
|
setStatusMessage("Could not access slide masters.");
|
||||||
setStatusMessage("Could not access slide masters.");
|
setStatusType("error");
|
||||||
setStatusType("error");
|
return;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await processRemoveWatermarks(context, masters.items);
|
||||||
|
updateStatusFromResult(result, false);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
handleError(error, "Remove draft watermark");
|
||||||
setStatusType("error");
|
|
||||||
console.error("Remove draft watermark error:", error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
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 (
|
return (
|
||||||
<div className={commonStyles.container}>
|
<div className={commonStyles.container}>
|
||||||
<div className={styles.buttonGrid}>
|
<div className={styles.buttonGrid}>
|
||||||
@@ -260,4 +360,4 @@ export const DraftButtons: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DraftButtons;
|
export default DraftButtons;
|
||||||
|
|||||||
Reference in New Issue
Block a user