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.
The toolkit is organized into a clear, tab-based interface, with each tab providing a distinct and powerful capability:
OnLoad, OnSave, and OnChange events as they happenOnLoad/OnSave event handlersTarget, PreEntityImage, and PostEntityImage sent to server-side plugins. Includes a C# unit test generator for FakeXrmEasyInstall directly from your browserβs extension store:
npm install and npm run buildedge://extensions/chrome://extensions/extension/ folderContributions 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.
git clone https://github.com/khawatme/Power-Toolkit.git
cd Power-Toolkit
npm install
npm run dev
npm run build
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
Adding a new tab to Power-Toolkit is straightforward thanks to the modular architecture. Follow these steps:
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);
}
}
}
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
}
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
];
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'
}
};
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 */
}
npm run dev to start development modeAlways 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:
destroy() methoddestroy()BaseComponent for automatic lifecycle managementrender() for HTML generationattachEventListeners() for event bindingdestroy() for cleanup (memory leak prevention)getElement(selector) helper for DOM queriesnull with JSDocNotificationService for user-facing errorsfinally blocksstyle.css:
.pdt-section for main containers.pdt-card for content cards.pdt-button for buttons (add primary for primary actions).pdt-input-group for form inputs.pdt-note for helper textUIFactory for common UI elements (code blocks, info grids, etc.)PerformanceHelpers.debounce())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);
Power-Toolkit follows strict memory management patterns. Here are common patterns:
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);
}
}
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);
}
}
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);
}
}
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);
}
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);
}
Before submitting your tab:
destroy() method implemented with all event listener cleanupgit checkout -b feature/my-new-feature
git commit -m "Add: MyCustomTab for feature X"
git push origin feature/my-new-feature
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
Found a bug? Have a feature request? Please open an issue with:
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ for Power Platform Developers