Power-Toolkit

Power-Toolkit for Power Apps & Dynamics 365

License: MIT

Power-Toolkit is a comprehensive, client-side developer tool designed to accelerate the development and debugging of Power Apps Model-Driven Apps. Built as a browser extension, it provides a suite of powerful features to inspect, debug, and manipulate form data, metadata, and server-side processes in real-time, directly within your browser.


✨ Key Features

The toolkit is organized into a clear, tab-based interface, with each tab providing a distinct and powerful capability:

πŸ” Live Form Inspection & Editing

βš™οΈ Automation & Logic Debugging

πŸ“Š Data & API Interaction

πŸ” Security & Configuration

πŸ“ˆ Performance & Development


πŸš€ Installation

Install directly from your browser’s extension store:

Microsoft Edge

Install from Edge Add-ons

Google Chrome

Install from Chrome Web Store

Option 2: Load Unpacked (Development)

  1. Download or clone this repository
  2. Run npm install and npm run build
  3. Open your browser’s extension management page:
    • Edge: edge://extensions/
    • Chrome: chrome://extensions/
  4. Enable β€œDeveloper mode”
  5. Click β€œLoad unpacked” and select the extension/ folder

Quick Tips


πŸ’» Development & Contribution

Contributions are welcome! Whether you want to fix a bug, add a new feature, or create a new tab, this guide will help you get started.

Prerequisites

Setup

  1. Clone the Repository:
    git clone https://github.com/khawatme/Power-Toolkit.git
    cd Power-Toolkit
    
  2. Install Dependencies:
    npm install
    
  3. Development Mode: Watch for changes and auto-rebuild:
    npm run dev
    
  4. Build for Production: Create optimized bundle:
    npm run build
    

Project Structure

Power-Toolkit/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ components/        # Tab components (e.g., InspectorTab.js)
β”‚   β”œβ”€β”€ services/          # Business logic services
β”‚   β”œβ”€β”€ helpers/           # Utility functions (modular)
β”‚   β”œβ”€β”€ ui/                # UI factories and controls
β”‚   β”œβ”€β”€ utils/             # Utilities (builders, parsers, validators)
β”‚   β”œβ”€β”€ constants/         # Configuration and constants
β”‚   β”œβ”€β”€ core/              # Base classes and core infrastructure
β”‚   β”œβ”€β”€ data/              # Static data (code snippets, etc.)
β”‚   β”œβ”€β”€ assets/            # Styles, icons, and static assets
β”‚   β”œβ”€β”€ App.js             # Main application entry point
β”‚   └── Main.js            # Bootstrap and initialization
β”œβ”€β”€ extension/
β”‚   β”œβ”€β”€ manifest.json      # Extension manifest
β”‚   β”œβ”€β”€ background.js      # Service worker
β”‚   └── icons/             # Extension icons
β”œβ”€β”€ webpack.config.js      # Webpack configuration
└── package.json           # Dependencies and scripts

πŸ› οΈ How to Implement a New Tab

Adding a new tab to Power-Toolkit is straightforward thanks to the modular architecture. Follow these steps:

Step 1: Create Your Tab Component

Create a new file in src/components/ (e.g., MyCustomTab.js):

/**
 * @file MyCustomTab - Description of what your tab does
 * @module components/MyCustomTab
 */

import { BaseComponent } from '../core/BaseComponent.js';
import { Config } from '../constants/index.js';
import { DataService } from '../services/DataService.js';
import { NotificationService } from '../services/NotificationService.js';
import { UIFactory } from '../ui/UIFactory.js';

export class MyCustomTab extends BaseComponent {
    constructor() {
        super('mycustomtab'); // Unique tab ID

        // Handler references for cleanup (IMPORTANT: Prevents memory leaks)
        /** @private {Function|null} Handler for execute button */ this._executeHandler = null;
        /** @private {Function|null} Handler for input keydown */ this._inputKeydownHandler = null;
    }

    /**
     * Render the tab's UI
     * @returns {string} HTML content for the tab
     */
    render() {
        return `
            <div class="pdt-section">
                <div class="pdt-section-header">
                    <h3>My Custom Feature</h3>
                    <p class="pdt-note">Description of what this tab does</p>
                </div>
                
                <div class="pdt-card">
                    <div class="pdt-input-group">
                        <label for="my-input">Input Label:</label>
                        <input type="text" id="my-input" placeholder="Enter something...">
                    </div>
                    
                    <div class="pdt-button-group">
                        <button id="my-execute-btn" class="pdt-button primary">
                            Execute
                        </button>
                    </div>
                </div>
                
                <div id="my-result-container"></div>
            </div>
        `;
    }

    /**
     * Attach event listeners after render
     * IMPORTANT: Store handlers as instance properties for cleanup in destroy()
     */
    attachEventListeners() {
        const executeBtn = this.getElement('#my-execute-btn');
        const inputField = this.getElement('#my-input');
        
        // Store handlers for cleanup
        this._executeHandler = () => this._handleExecute();
        this._inputKeydownHandler = (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                this._handleExecute();
            }
        };
        
        // Attach listeners
        executeBtn?.addEventListener('click', this._executeHandler);
        inputField?.addEventListener('keydown', this._inputKeydownHandler);
    }

    /**
     * Handle execute button click
     * @private
     */
    async _handleExecute() {
        const input = this.getElement('#my-input')?.value?.trim();
        
        if (!input) {
            NotificationService.show('Please enter a value', 'error');
            return;
        }

        const executeBtn = this.getElement('#my-execute-btn');
        const resultContainer = this.getElement('#my-result-container');

        try {
            // Show loading state
            executeBtn.disabled = true;
            executeBtn.textContent = 'Processing...';
            
            // Your business logic here
            const result = await DataService.retrieveRecord('account', input);
            
            // Display results
            resultContainer.innerHTML = '';
            resultContainer.appendChild(
                UIFactory.createCopyableCodeBlock(
                    JSON.stringify(result, null, 2),
                    'json'
                )
            );
            
            NotificationService.show('Operation completed successfully', 'success');
            
        } catch (error) {
            NotificationService.show(`Error: ${error.message}`, 'error');
            resultContainer.innerHTML = `
                <div class="pdt-card error">
                    <p><strong>Error:</strong> ${error.message}</p>
                </div>
            `;
        } finally {
            // Reset button state
            executeBtn.disabled = false;
            executeBtn.textContent = 'Execute';
        }
    }

    /**
     * Lifecycle hook for cleaning up event listeners
     * CRITICAL: Always implement this to prevent memory leaks
     */
    destroy() {
        const executeBtn = this.getElement('#my-execute-btn');
        const inputField = this.getElement('#my-input');
        
        if (executeBtn) {
            executeBtn.removeEventListener('click', this._executeHandler);
        }
        if (inputField) {
            inputField.removeEventListener('keydown', this._inputKeydownHandler);
        }
    }
}

Step 2: Register Your Tab

Open src/core/ComponentRegistry.js and import your component:

import { MyCustomTab } from '../components/MyCustomTab.js';

Then add it to the registry:

export function registerAllComponents() {
    ComponentRegistry.register('mycustomtab', MyCustomTab);
    // ... other components
}

Step 3: Add Tab to UI

Open src/App.js and add your tab to the tabs array:

this.tabs = [
    { id: 'inspector', label: 'Inspector', icon: 'πŸ”' },
    // ... other tabs
    { id: 'mycustomtab', label: 'My Custom', icon: '🎯' },
    // ... remaining tabs
];

Step 4: Update Constants (Optional)

If your tab needs custom messages or configuration, add them to src/constants/:

messages.js:

export const MESSAGES = {
    // ... existing messages
    MY_CUSTOM_TAB: {
        successMessage: 'Operation completed successfully',
        errorMessage: 'Failed to process request',
        validationError: 'Invalid input provided'
    }
};

Step 5: Add Styles (Optional)

If you need custom styles, add them to src/assets/style.css:

/* My Custom Tab Styles */
#my-result-container {
    margin-top: 1rem;
    max-height: 400px;
    overflow-y: auto;
}

.my-custom-class {
    /* Your custom styles */
}

Step 6: Test Your Tab

  1. Run npm run dev to start development mode
  2. Reload the extension in your browser
  3. Open Power-Toolkit and navigate to your new tab
  4. Test all functionality and error handling

Best Practices for Tab Development

1. Memory Leak Prevention (CRITICAL)

Always implement proper cleanup to prevent memory leaks!

// βœ… CORRECT Pattern
constructor() {
    super('mytab');
    // Initialize ALL handler properties to null
    /** @private {Function|null} */ this._myHandler = null;
}

attachEventListeners() {
    // Store handler as instance property
    this._myHandler = () => { /* handler code */ };
    element.addEventListener('click', this._myHandler);
}

destroy() {
    // Remove listener using stored reference
    if (element) {
        element.removeEventListener('click', this._myHandler);
    }
}

// ❌ WRONG - Memory leak!
attachEventListeners() {
    // Anonymous function - can't be removed later
    element.addEventListener('click', () => { /* handler code */ });
}

Key Rules:

2. Follow the BaseComponent Pattern

3. Use Existing Services

4. Lifecycle Management

5. Error Handling

6. UI Consistency

7. Accessibility

8. Performance

Example: Using Helper Functions

Power-Toolkit provides many helper utilities in src/helpers/:

import { 
    escapeHtml,           // Safe HTML escaping
    isValidGuid,          // GUID validation
    formatDisplayValue,   // Format attribute values
    normalizeGuid,        // Clean GUID formatting
    copyToClipboard      // Clipboard operations
} from '../helpers/index.js';

// Usage
const safeText = escapeHtml(userInput);
const isValid = isValidGuid(recordId);
const displayText = formatDisplayValue(attribute.getValue(), attribute);

Memory Leak Prevention Patterns

Power-Toolkit follows strict memory management patterns. Here are common patterns:

Pattern 1: Simple Event Handlers

constructor() {
    super('mytab');
    /** @private {Function|null} */ this._clickHandler = null;
}

attachEventListeners() {
    this._clickHandler = () => { /* code */ };
    button.addEventListener('click', this._clickHandler);
}

destroy() {
    if (button) {
        button.removeEventListener('click', this._clickHandler);
    }
}

Pattern 2: Delegated Event Handlers

constructor() {
    super('mytab');
    /** @private {Function|null} */ this._delegatedHandler = null;
    /** @private {HTMLElement|null} */ this._container = null;
}

attachEventListeners() {
    this._container = this.getElement('#container');
    this._delegatedHandler = (e) => {
        const target = e.target.closest('.my-item');
        if (target) { /* handle */ }
    };
    this._container.addEventListener('click', this._delegatedHandler);
}

destroy() {
    if (this._container) {
        this._container.removeEventListener('click', this._delegatedHandler);
    }
}

Pattern 3: Debounced Handlers

import { debounce } from '../helpers/index.js';

constructor() {
    super('mytab');
    /** @private {Function|null} */ this._debouncedSearch = null;
}

attachEventListeners() {
    this._debouncedSearch = debounce(() => {
        // Search logic
    }, 250);
    searchInput.addEventListener('input', this._debouncedSearch);
}

destroy() {
    if (searchInput) {
        searchInput.removeEventListener('input', this._debouncedSearch);
    }
}

Pattern 4: Document-Level Events

constructor() {
    super('mytab');
    /** @private {Function|null} */ this._globalHandler = null;
}

attachEventListeners() {
    this._globalHandler = (e) => { /* code */ };
    document.addEventListener('pdt:refresh', this._globalHandler);
}

destroy() {
    // IMPORTANT: Remove document-level listeners!
    document.removeEventListener('pdt:refresh', this._globalHandler);
}

Pattern 5: Multiple Inputs with Same Handler

constructor() {
    super('mytab');
    /** @private {Function|null} */ this._inputHandler = null;
}

attachEventListeners() {
    this._inputHandler = () => this._updatePreview();
    
    [input1, input2, input3].forEach(input => {
        input?.addEventListener('input', this._inputHandler);
    });
}

destroy() {
    [input1, input2, input3].forEach(input => {
        if (input) {
            input.removeEventListener('input', this._inputHandler);
        }
    });
}

Common Mistakes to Avoid:

// ❌ WRONG: Closure captures scope - memory leak
postRender() {
    const data = this.getData();
    button.addEventListener('click', () => {
        console.log(data); // Captures 'data' and 'this'
    });
}

// βœ… CORRECT: Use instance property
constructor() {
    /** @private {Function|null} */ this._buttonHandler = null;
}
postRender() {
    this._buttonHandler = () => {
        console.log(this.getData()); // No closure leak
    };
    button.addEventListener('click', this._buttonHandler);
}

Testing Checklist

Before submitting your tab:


🀝 Contributing Guidelines

Submitting Changes

  1. Fork the Repository
  2. Create a Feature Branch:
    git checkout -b feature/my-new-feature
    
  3. Make Your Changes following the guidelines above
  4. Test Thoroughly using the checklist
  5. Commit with Clear Messages:
    git commit -m "Add: MyCustomTab for feature X"
    
  6. Push to Your Fork:
    git push origin feature/my-new-feature
    
  7. Open a Pull Request with a clear description of changes

Code Standards

Commit Message Format

Type: Short description (50 chars max)

Longer description if needed (wrap at 72 chars)

Fixes #issue-number (if applicable)

Types: Add, Fix, Update, Remove, Refactor, Docs, Style


πŸ› Reporting Issues

Found a bug? Have a feature request? Please open an issue with:

  1. Clear Title: Brief description of the issue
  2. Environment: Browser version, Power Apps version, extension version
  3. Steps to Reproduce: Detailed steps to recreate the issue
  4. Expected Behavior: What should happen
  5. Actual Behavior: What actually happens
  6. Screenshots: If applicable

οΏ½ License

This project is licensed under the MIT License - see the LICENSE file for details.


πŸ™ Acknowledgments


Made with ❀️ for Power Platform Developers