6 Commits

Author SHA1 Message Date
schihei 3dc9afea50 Make version numbers consistent across all files
- Updated manifest.xml from 1.0.0.0 to 1.1.0.0
- Updated README.md version history with v1.1.0 entry
- Synchronized package-lock.json version with package.json

All application files now consistently refer to version 1.1.0

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-15 00:19:11 +01:00
schihei 338810ef60 Update version to 1.1.0 and enhance documentation
- Bumped version from 1.0.1 to 1.1.0 in package.json and UI
- Updated USER_GUIDE.md with new features in v1.1.0
- Enhanced DEVELOPERS.md with:
  - Information about reusable components (ActionButton, Section)
  - Documentation for type-safe utilities
  - Code examples for performance optimizations
  - Updated best practices
  - Added information about the improved project structure

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-15 00:16:42 +01:00
schihei 50966867b2 Improve TypeScript types and error handling
- Created proper TypeScript types for Office API operations
- Replaced 'any' casts with appropriate types
- Added type-safe utility functions in office-types.ts
- Improved error handling with proper unknown type
- Added helper functions for PowerPoint shape operations
- Created proper TypeScript interface for webpack HMR

These changes improve code quality, maintainability, and help catch potential bugs at compile time rather than runtime.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-15 00:02:35 +01:00
schihei 07b0232726 Optimize performance by batching context.sync() calls
- GridGuidelineManager: Load all shape names upfront and apply operations in batches
- AlignmentButtons: Replaced for loops with forEach and reduced sync calls
- MatchProperties: Reorganized code to batch load operations and property assignments

This optimization significantly reduces round-trips between JavaScript and the Office application,
improving performance and responsiveness of the add-in.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-14 23:56:54 +01:00
schihei d09dec4706 Refactor UI components for better reusability
- Created reusable ActionButton component to handle common button behavior
- Created reusable Section component for consistent section styling
- Refactored App, MatchSizes, and SwapPositions to use new components
- Fixed scrolling issues in the main container
- Improved code organization and reduced duplication

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-14 23:50:19 +01:00
schihei 0cbb9c948e Minor changes 2025-03-14 23:40:29 +01:00
17 changed files with 481 additions and 277 deletions
+1
View File
@@ -159,5 +159,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail
## Version History
- v1.1.0 - Performance improvements, code quality, and UI enhancements
- v1.0.0 - First stable release
- v0.1.0 - Initial release with core functionality
+92 -6
View File
@@ -24,18 +24,24 @@ The application follows a component-based architecture where each tool is encaps
│ │ └── commands.ts # Command implementations
│ └── taskpane/ # Main add-in UI
│ ├── components/ # React components
│ │ ├── ActionButton.tsx # Reusable button with error handling
│ │ ├── AlignmentButtons.tsx
│ │ ├── App.tsx # Main application component
│ │ ├── App.tsx # Main application component
│ │ ├── ConfidentialButtons.tsx
│ │ ├── DraftButtons.tsx
│ │ ├── GridGuidelineManager.tsx
│ │ ├── Header.tsx
│ │ ├── InsertTitles.tsx
│ │ ├── MatchProperties.tsx
│ │ ├── MatchSizes.tsx
│ │ ├── ProgressBarButtons.tsx
│ │ ├── RoundImage.tsx
│ │ ├── Section.tsx # Reusable section component
│ │ ├── SmartTextFormatter.tsx
│ │ ├── SwapPositions.tsx
│ │ └── commonStyles.tsx
│ ├── types/ # TypeScript type definitions
│ │ └── office-types.ts # Office API type utilities
│ ├── index.tsx # Entry point
│ └── taskpane.html # Main HTML container
├── babel.config.json # Babel configuration
@@ -92,6 +98,34 @@ npx office-addin-dev-certs install
## Component Development Guidelines
### Reusable Components
#### ActionButton
A reusable button component that handles error states, loading states, and consistent styling:
```tsx
import ActionButton from "./ActionButton";
// In your component:
<ActionButton
title="Match Sizes"
icon={<ArrowFitInRegular />}
onClick={myAsyncFunction}
/>
```
#### Section
A consistent container for organizing the UI:
```tsx
import Section from "./Section";
// In your component:
<Section title="My Feature Group">
<MyFeatureComponent />
</Section>
```
### Creating a New Tool
1. Create a new component file in `src/taskpane/components/`
@@ -142,6 +176,25 @@ export default MyNewTool;
### Common PowerPoint.js Operations
#### Type-Safe Utilities
The application provides type-safe utility functions in `src/taskpane/types/office-types.ts`:
```typescript
// Get error message safely from any error type
const errorMsg = getErrorMessage(error);
// Check if a shape is an image
if (isPictureShape(shape)) {
// It's a picture shape
}
// Get the first selected slide
const slide = getFirstSelectedSlide(context);
// Select shapes by ID
selectShapesById(slide, [shape1.id, shape2.id]);
```
#### Selecting Shapes
```typescript
const shapes = context.presentation.getSelectedShapes();
@@ -217,10 +270,40 @@ Recommended testing approach:
## Performance Considerations
- Minimize `context.sync()` calls, especially in loops
- Batch operations when possible
### Optimizing context.sync() calls
A key performance optimization is to batch context.sync() calls:
```typescript
// BAD: Calling context.sync() in a loop
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].load("name");
await context.sync(); // Slow! One network roundtrip per shape
if (shapes.items[i].name === "Target") {
// Do something
}
}
// GOOD: Batching context.sync() calls
// Load all properties at once
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].load("name");
}
await context.sync(); // Single network roundtrip for all shapes
// Process the data locally
for (let i = 0; i < shapes.items.length; i++) {
if (shapes.items[i].name === "Target") {
// Do something
}
}
```
Additional performance tips:
- Use memoization for expensive calculations
- Ensure proper cleanup of event listeners
- Use the ActionButton component for consistent loading states
- Consider using Web Workers for heavy computations
## Common Issues and Solutions
@@ -236,11 +319,14 @@ Recommended testing approach:
## Best Practices
1. **Error Handling**: Always use try/catch blocks with PowerPoint API calls
2. **User Feedback**: Provide clear status messages for all operations
3. **Accessibility**: Ensure all UI elements have proper labels and keyboard access
1. **Error Handling**: Always use try/catch blocks with PowerPoint API calls and use proper types (see `office-types.ts`)
2. **User Feedback**: Provide clear status messages for all operations using the StatusContext
3. **Accessibility**: Ensure all UI elements have proper ARIA attributes and support keyboard navigation
4. **Theme Support**: Use Fluent UI theme tokens for consistent theming
5. **Code Organization**: Keep component files focused on a single responsibility
6. **Performance**: Batch context.sync() calls and avoid calling them inside loops
7. **Reusability**: Use custom hooks for common operations and shared components like ActionButton and Section
8. **Type Safety**: Avoid using "any" type and leverage TypeScript's static typing
## Contributing
+9 -1
View File
@@ -212,4 +212,12 @@ If you encounter issues not covered in this guide, contact your IT department or
## Version Information
This user guide is for Edison PowerPoint Add-in v1.0.0.
This user guide is for Edison PowerPoint Add-in v1.1.0.
### What's New in v1.1.0
* **Performance Improvements** - Faster operations especially when working with multiple shapes
* **Enhanced Stability** - Improved error handling and recovery
* **Better UI Experience** - More responsive interface with improved scrolling
* **Accessibility Enhancements** - Better support for screen readers and keyboard navigation
* **Code Quality** - Improved TypeScript typing for increased reliability
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
<Id>be5c6f61-4bbf-4bd2-a8da-5031abf096a5</Id>
<Version>1.0.0.0</Version>
<Version>1.1.0.0</Version>
<ProviderName>Contoso</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Edison"/>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "edison-powerpoint-addin",
"version": "0.0.1",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "edison-powerpoint-addin",
"version": "0.0.1",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"@fluentui/react-components": "^9.55.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "edison-powerpoint-addin",
"version": "1.0.0",
"version": "1.1.0",
"repository": {
"type": "git",
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.git"
+64
View File
@@ -0,0 +1,64 @@
import * as React from "react";
import { Button, ButtonProps } from "@fluentui/react-components";
import { useStatusContext } from "./App";
import { useCommonStyles } from "./commonStyles";
import { getErrorMessage } from "../types/office-types";
export interface ActionButtonProps {
icon: ButtonProps["icon"];
onClick: () => Promise<void>;
disabled?: boolean;
title: string;
appearance?: "primary" | "secondary" | "outline" | "subtle";
className?: string;
}
/**
* Reusable action button component with consistent styling and error handling
*/
export const ActionButton: React.FC<ActionButtonProps> = ({
icon,
onClick,
disabled = false,
title,
appearance = "primary",
className = "",
}) => {
const styles = useCommonStyles();
const {
setStatusMessage,
setStatusType,
isProcessing,
setIsProcessing
} = useStatusContext();
const handleClick = async () => {
if (isProcessing) return;
setIsProcessing(true);
try {
await onClick();
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
setStatusMessage(`Error: ${errorMessage}`);
setStatusType("error");
console.error(`Error in ${title} action:`, error);
} finally {
setIsProcessing(false);
}
};
return (
<Button
appearance={appearance}
className={`${styles.actionButton} ${className}`}
onClick={handleClick}
icon={icon}
disabled={disabled || isProcessing}
>
{title}
</Button>
);
};
export default ActionButton;
+30 -24
View File
@@ -56,10 +56,11 @@ export const AlignmentButtons: React.FC = () => {
return;
}
// Loop through all select shapes and align them
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].left = 81.1;
}
// Set alignment for all shapes at once
shapes.items.forEach(shape => {
shape.left = 81.1;
});
// Single sync after all updates
await context.sync();
setStatusMessage("Objects aligned to left.");
@@ -90,10 +91,11 @@ export const AlignmentButtons: React.FC = () => {
return;
}
// Loop through all select shapes and align them
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].left = 480 - (shapes.items[i].width / 2);
}
// Set alignment for all shapes at once
shapes.items.forEach(shape => {
shape.left = 480 - (shape.width / 2);
});
// Single sync after all updates
await context.sync();
setStatusMessage("Objects aligned to center.");
@@ -124,10 +126,11 @@ export const AlignmentButtons: React.FC = () => {
return;
}
// Loop through all select shapes and align them
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].left = 879.75 - shapes.items[i].width;
}
// Set alignment for all shapes at once
shapes.items.forEach(shape => {
shape.left = 879.75 - shape.width;
});
// Single sync after all updates
await context.sync();
setStatusMessage("Objects aligned to right.");
@@ -158,10 +161,11 @@ export const AlignmentButtons: React.FC = () => {
return;
}
// Loop through all select shapes and align them
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].top = 136.75;
}
// Set alignment for all shapes at once
shapes.items.forEach(shape => {
shape.top = 136.75;
});
// Single sync after all updates
await context.sync();
setStatusMessage("Objects aligned to top.");
@@ -192,10 +196,11 @@ export const AlignmentButtons: React.FC = () => {
return;
}
// Loop through all select shapes and align them
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].top = 321 - (shapes.items[i].height / 2);
}
// Set alignment for all shapes at once
shapes.items.forEach(shape => {
shape.top = 321 - (shape.height / 2);
});
// Single sync after all updates
await context.sync();
setStatusMessage("Objects aligned to middle.");
@@ -226,10 +231,11 @@ export const AlignmentButtons: React.FC = () => {
return;
}
// Loop through all select shapes and align them
for (let i = 0; i < shapes.items.length; i++) {
shapes.items[i].top = 505.25 - shapes.items[i].height;
}
// Set alignment for all shapes at once
shapes.items.forEach(shape => {
shape.top = 505.25 - shape.height;
});
// Single sync after all updates
await context.sync();
setStatusMessage("Objects aligned to bottom.");
+20 -50
View File
@@ -10,7 +10,8 @@ import DraftButtons from "./DraftButtons";
import ProgressBarButtons from "./ProgressBarButtons";
import AlignmentButtons from "./AlignmentButtons";
import GridGuidelineManager from "./GridGuidelineManager";
import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components";
import Section from "./Section";
import { makeStyles, Text, tokens, Theme, Spinner } from "@fluentui/react-components";
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
interface AppProps {
@@ -42,12 +43,15 @@ const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
height: "100%",
padding: "16px",
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
backgroundColor: tokens.colorNeutralBackground1,
overflow: "auto",
maxHeight: "100vh",
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
},
statusContainer: {
marginTop: "4px",
@@ -76,22 +80,6 @@ const useStyles = makeStyles({
display: "flex",
alignItems: "center",
},
section: {
marginBottom: "6px",
padding: "12px",
borderRadius: "8px",
backgroundColor: tokens.colorNeutralBackground2,
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)",
transition: "all 0.2s ease",
":hover": {
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
},
},
sectionTitle: {
marginBottom: "12px",
fontWeight: tokens.fontWeightSemibold,
color: tokens.colorBrandForeground1,
},
footer: {
fontSize: "12px",
color: tokens.colorNeutralForeground3,
@@ -153,10 +141,7 @@ const App: React.FC<AppProps> = () => {
setIsProcessing
}}>
<div className={styles.root} data-theme={theme}>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Content
</Subtitle1>
<Section title="Content">
<MatchProperties />
<div style={{ marginTop: "8px" }}></div>
<MatchSizes />
@@ -166,42 +151,27 @@ const App: React.FC<AppProps> = () => {
<InsertTitles />
<div style={{ marginTop: "8px" }}></div>
<RoundImage />
</div>
</Section>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Progress Bar
</Subtitle1>
<Section title="Progress Bar">
<ProgressBarButtons />
</div>
</Section>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Confidential Marking
</Subtitle1>
<Section title="Confidential Marking">
<ConfidentialButtons />
</div>
</Section>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Draft Watermark
</Subtitle1>
<Section title="Draft Watermark">
<DraftButtons />
</div>
</Section>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Alignment
</Subtitle1>
<Section title="Alignment">
<AlignmentButtons />
</div>
</Section>
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
Layout
</Subtitle1>
<Section title="Layout">
<GridGuidelineManager />
</div>
</Section>
{/* Status message area at the bottom */}
{isProcessing && (
@@ -223,7 +193,7 @@ const App: React.FC<AppProps> = () => {
)}
<div className={styles.footer}>
<Text size={100}>Edison v1.0.0</Text>
<Text size={100}>Edison v1.1.0</Text>
</div>
</div>
</StatusContext.Provider>
@@ -241,13 +241,17 @@ export const GridGuidelineManager: React.FC = () => {
shapes.load("items");
await context.sync();
// Find shapes that are part of our grid
// Load all shape names at once
const allShapes = shapes.items;
for (let i = 0; i < allShapes.length; i++) {
allShapes[i].load("name");
}
await context.sync();
// Find shapes that are part of our grid and mark them for deletion
let removedCount = 0;
for (let i = 0; i < shapes.items.length; i++) {
const shape = shapes.items[i];
shape.load("name");
await context.sync();
for (let i = 0; i < allShapes.length; i++) {
const shape = allShapes[i];
// Check if this shape is part of our grid
if (shape.name && shape.name.startsWith(GRID_PREFIX)) {
shape.delete();
@@ -255,6 +259,7 @@ export const GridGuidelineManager: React.FC = () => {
}
}
// Single sync after all deletions
await context.sync();
if (showStatus) {
+50 -44
View File
@@ -74,10 +74,27 @@ export const MatchProperties: React.FC = () => {
}
}
// First, load all text frames in a batch
if (hasText) {
// Pre-load textFrame for all target shapes
for (let i = 1; i < shapes.items.length; i++) {
shapes.items[i].load("textFrame");
}
await context.sync();
// For shapes that have textFrames, load their textRanges
for (let i = 1; i < shapes.items.length; i++) {
if (shapes.items[i].textFrame) {
shapes.items[i].textFrame.load("textRange");
}
}
await context.sync();
}
// Now apply properties to all shapes
let successCount = 0;
let textStyleCount = 0;
// Loop through remaining shapes and apply properties
for (let i = 1; i < shapes.items.length; i++) {
try {
const targetShape = shapes.items[i];
@@ -100,7 +117,6 @@ export const MatchProperties: React.FC = () => {
targetShape.lineFormat.color = firstShape.lineFormat.color;
}
await context.sync();
propertiesApplied = true;
} catch (err) {
console.error(`Error applying line format to shape ${i}:`, err);
@@ -117,56 +133,43 @@ export const MatchProperties: React.FC = () => {
targetShape.fill.foregroundColor = firstShape.fill.foregroundColor;
}
await context.sync();
propertiesApplied = true;
} catch (err) {
console.error(`Error applying fill format to shape ${i}:`, err);
}
// Apply text properties if the source has text
if (hasText) {
if (hasText && targetShape.textFrame && targetShape.textFrame.textRange) {
try {
// First check if target shape has text
targetShape.load("textFrame");
await context.sync();
const sourceFont = firstShape.textFrame.textRange.font;
const targetFont = targetShape.textFrame.textRange.font;
if (targetShape.textFrame) {
targetShape.textFrame.load("textRange");
await context.sync();
if (targetShape.textFrame.textRange) {
const sourceFont = firstShape.textFrame.textRange.font;
const targetFont = targetShape.textFrame.textRange.font;
// Apply font properties
if (sourceFont.name !== undefined) {
targetFont.name = sourceFont.name;
}
if (sourceFont.size !== undefined) {
targetFont.size = sourceFont.size;
}
if (sourceFont.bold !== undefined) {
targetFont.bold = sourceFont.bold;
}
if (sourceFont.italic !== undefined) {
targetFont.italic = sourceFont.italic;
}
if (sourceFont.underline !== undefined) {
targetFont.underline = sourceFont.underline;
}
if (sourceFont.color !== undefined) {
targetFont.color = sourceFont.color;
}
await context.sync();
textStyleCount++;
}
// Apply font properties in batch
if (sourceFont.name !== undefined) {
targetFont.name = sourceFont.name;
}
if (sourceFont.size !== undefined) {
targetFont.size = sourceFont.size;
}
if (sourceFont.bold !== undefined) {
targetFont.bold = sourceFont.bold;
}
if (sourceFont.italic !== undefined) {
targetFont.italic = sourceFont.italic;
}
if (sourceFont.underline !== undefined) {
targetFont.underline = sourceFont.underline;
}
if (sourceFont.color !== undefined) {
targetFont.color = sourceFont.color;
}
textStyleCount++;
} catch (err) {
console.error(`Error applying text format to shape ${i}:`, err);
}
@@ -180,6 +183,9 @@ export const MatchProperties: React.FC = () => {
console.error(`Error updating shape ${i}:`, err);
}
}
// Single sync after all property changes
await context.sync();
// Final status message based on what was applied
if (successCount > 0) {
+38 -68
View File
@@ -1,89 +1,59 @@
import * as React from "react";
import {
Button,
Text,
Tooltip,
InfoLabel,
Card
} from "@fluentui/react-components";
import {
ArrowFitInRegular,
ArrowSortDownRegular,
ArrowSortUpRegular
} from "@fluentui/react-icons";
import { ArrowFitInRegular } from "@fluentui/react-icons";
import { useStatusContext } from "./App";
import { useCommonStyles } from "./commonStyles";
import ActionButton from "./ActionButton";
export const MatchSizes: React.FC = () => {
const styles = useCommonStyles();
const {
statusMessage, setStatusMessage,
statusType, setStatusType,
isProcessing, setIsProcessing
} = useStatusContext();
const { setStatusMessage, setStatusType } = useStatusContext();
const matchSizeToFirstSelected = async () => {
setIsProcessing(true);
try {
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items");
await context.sync();
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items");
await context.sync();
// Check if shapes are selected
if (shapes.items.length === 0) {
setStatusMessage("No shapes are selected. Please select shapes first.");
setStatusType("warning");
return;
}
// Check if shapes are selected
if (shapes.items.length === 0) {
setStatusMessage("No shapes are selected. Please select shapes first.");
setStatusType("warning");
return;
}
// Check if there is more than one shape selected
if (shapes.items.length === 1) {
setStatusMessage("Please select multiple shapes to resize.");
setStatusType("warning");
return;
}
// Check if there is more than one shape selected
if (shapes.items.length === 1) {
setStatusMessage("Please select multiple shapes to resize.");
setStatusType("warning");
return;
}
// Get the first shape's dimensions
const firstShape = shapes.items[0];
firstShape.load("width,height");
await context.sync();
// Get the first shape's dimensions
const firstShape = shapes.items[0];
firstShape.load("width,height");
await context.sync();
// Loop through the remaining shapes and resize them
for (let i = 1; i < shapes.items.length; i++) {
shapes.items[i].width = firstShape.width;
shapes.items[i].height = firstShape.height;
}
// Loop through the remaining shapes and resize them
for (let i = 1; i < shapes.items.length; i++) {
shapes.items[i].width = firstShape.width;
shapes.items[i].height = firstShape.height;
}
await context.sync();
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
setStatusType("success");
// Timeout is handled in App.tsx now
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
setStatusType("error");
console.error(error);
} finally {
setIsProcessing(false);
}
await context.sync();
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
setStatusType("success");
});
};
return (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Button
appearance="primary"
className={styles.actionButton}
onClick={matchSizeToFirstSelected}
icon={<ArrowFitInRegular />}
disabled={isProcessing}
>
Match Sizes
</Button>
<ActionButton
title="Match Sizes"
icon={<ArrowFitInRegular />}
onClick={matchSizeToFirstSelected}
/>
</div>
</div>
);
+13 -8
View File
@@ -7,6 +7,7 @@ import {
} from "@fluentui/react-icons";
import { useStatusContext } from "./App";
import { useCommonStyles } from "./commonStyles";
import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types";
export const RoundImage: React.FC = () => {
const styles = useCommonStyles();
@@ -38,8 +39,8 @@ export const RoundImage: React.FC = () => {
shape.load(["type"]);
await context.sync();
// Ensure the shape is a picture
if (shape.type !== PowerPoint.ShapeType.image) {
// Ensure the shape is a picture using our type-safe utility
if (!isPictureShape(shape)) {
setStatusMessage("Please select an image.");
setStatusType("warning");
return;
@@ -53,8 +54,10 @@ export const RoundImage: React.FC = () => {
const width = shape.width;
const height = shape.height;
const slide = context.presentation.getSelectedSlides().getItemAt(0);
const maskShape = slide.shapes.addGeometricShape("Ellipse" as any);
// Get the current slide using our type-safe utility
const slide = getFirstSelectedSlide(context);
// Create elliptical mask with proper type
const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse);
maskShape.load(["width", "height", "left", "top", "id"]);
await context.sync();
@@ -73,17 +76,19 @@ export const RoundImage: React.FC = () => {
setStatusMessage("Created mask shape. Please select both the image and the oval, then use the 'Shape Forma > Merge Shapes > Intersect' command in PowerPoint.");
setStatusType("warning");
slide.setSelectedShapes([shape.id, maskShape.id]);
// Use our type-safe utility for selecting shapes
selectShapesById(slide, [shape.id, maskShape.id]);
// Ensure we maintain the same size
shape.width = width;
shape.height = height;
await context.sync();
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
} catch (error: unknown) {
const errorMessage = getErrorMessage(error);
setStatusMessage(`Error: ${errorMessage}`);
setStatusType("error");
console.error("Main error:", error);
console.error("Round image error:", error);
} finally {
setIsProcessing(false);
}
+44
View File
@@ -0,0 +1,44 @@
import * as React from "react";
import { Subtitle1, makeStyles, tokens } from "@fluentui/react-components";
interface SectionProps {
title: string;
children: React.ReactNode;
}
const useStyles = makeStyles({
section: {
marginBottom: "6px",
padding: "12px",
borderRadius: "8px",
backgroundColor: tokens.colorNeutralBackground2,
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)",
transition: "all 0.2s ease",
":hover": {
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
},
},
sectionTitle: {
marginBottom: "12px",
fontWeight: tokens.fontWeightSemibold,
color: tokens.colorBrandForeground1,
},
});
/**
* Reusable section component with consistent styling
*/
export const Section: React.FC<SectionProps> = ({ title, children }) => {
const styles = useStyles();
return (
<div className={styles.section}>
<Subtitle1 block className={styles.sectionTitle}>
{title}
</Subtitle1>
{children}
</div>
);
};
export default Section;
+42 -63
View File
@@ -1,82 +1,61 @@
import * as React from "react";
import {
Button
} from "@fluentui/react-components";
import {
ArrowSwapRegular
} from "@fluentui/react-icons";
import { ArrowSwapRegular } from "@fluentui/react-icons";
import { useStatusContext } from "./App";
import { useCommonStyles } from "./commonStyles";
import ActionButton from "./ActionButton";
export const SwapPositions: React.FC = () => {
const styles = useCommonStyles();
const {
statusMessage, setStatusMessage,
statusType, setStatusType,
isProcessing, setIsProcessing
} = useStatusContext();
const { setStatusMessage, setStatusType } = useStatusContext();
const swapPositionsOfTwoSelectedObjects = async () => {
setIsProcessing(true);
try {
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items/count");
await context.sync();
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items/count");
await context.sync();
// Check if exactly two shapes are selected
if (shapes.items.length !== 2) {
setStatusMessage("Please select exactly two shapes to swap their positions.");
setStatusType("warning");
return;
}
// Check if exactly two shapes are selected
if (shapes.items.length !== 2) {
setStatusMessage("Please select exactly two shapes to swap their positions.");
setStatusType("warning");
return;
}
// Get the two shapes
const shapeObj1 = shapes.items[0];
const shapeObj2 = shapes.items[1];
// Load position properties
shapeObj1.load("left,top");
shapeObj2.load("left,top");
await context.sync();
// Store the position of the first shape
const tempLeft = shapeObj1.left;
const tempTop = shapeObj1.top;
// Swap positions
shapeObj1.left = shapeObj2.left;
shapeObj1.top = shapeObj2.top;
shapeObj2.left = tempLeft;
shapeObj2.top = tempTop;
await context.sync();
setStatusMessage("Positions of the two shapes have been swapped successfully.");
setStatusType("success");
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
setStatusType("error");
console.error("Swap positions error:", error);
} finally {
setIsProcessing(false);
}
// Get the two shapes
const shapeObj1 = shapes.items[0];
const shapeObj2 = shapes.items[1];
// Load position properties
shapeObj1.load("left,top");
shapeObj2.load("left,top");
await context.sync();
// Store the position of the first shape
const tempLeft = shapeObj1.left;
const tempTop = shapeObj1.top;
// Swap positions
shapeObj1.left = shapeObj2.left;
shapeObj1.top = shapeObj2.top;
shapeObj2.left = tempLeft;
shapeObj2.top = tempTop;
await context.sync();
setStatusMessage("Positions of the two shapes have been swapped successfully.");
setStatusType("success");
});
};
return (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Button
appearance="primary"
className={styles.actionButton}
onClick={swapPositionsOfTwoSelectedObjects}
<ActionButton
title="Swap Positions"
icon={<ArrowSwapRegular />}
disabled={isProcessing}
>
Swap Positions
</Button>
onClick={swapPositionsOfTwoSelectedObjects}
/>
</div>
</div>
);
+15 -3
View File
@@ -19,9 +19,21 @@ Office.onReady(() => {
);
});
if ((module as any).hot) {
(module as any).hot.accept("./components/App", () => {
// Define proper module hot interface for webpack hot module replacement
interface HotModule extends NodeModule {
hot?: {
accept(path: string, callback: () => void): void;
};
}
// Use the proper type for module with HMR
if ((module as HotModule).hot) {
(module as HotModule).hot?.accept("./components/App", () => {
const NextApp = require("./components/App").default;
root?.render(NextApp);
root?.render(
<FluentProvider theme={webLightTheme}>
<NextApp title={title} />
</FluentProvider>
);
});
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Type definitions for the Office JS API
*/
/**
* Extended Error interface for Office/PowerPoint API errors
*/
export interface OfficeApiError extends Error {
code?: string;
debugInfo?: {
code?: string;
message?: string;
errorLocation?: string;
statement?: string;
innerError?: any;
};
}
/**
* Helper to safely extract error message from any error object
*/
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
/**
* Type-safe wrapper for PowerPoint API operations on shapes
*/
export function isPictureShape(shape: PowerPoint.Shape): boolean {
return shape.type === PowerPoint.ShapeType.image;
}
/**
* Type-safe wrapper for working with PowerPoint slides
*/
export function getFirstSelectedSlide(context: PowerPoint.RequestContext): PowerPoint.Slide {
return context.presentation.getSelectedSlides().getItemAt(0);
}
/**
* Type-safe utility for selecting shapes
*/
export function selectShapesById(slide: PowerPoint.Slide, shapeIds: string[]): void {
slide.setSelectedShapes(shapeIds);
}