@mcp-consultant-tools/powerplatform-data
Version:
MCP server for PowerPlatform data CRUD operations (operational use)
342 lines • 13.7 kB
JavaScript
/**
* Icon Management Module
*
* Integrates with Fluent UI System Icons for entity icon management.
* Fetches SVG icons from GitHub and uploads them as web resources.
*/
/**
* Fluent UI System Icons configuration
*/
const FLUENT_ICONS_CONFIG = {
baseUrl: 'https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/assets',
defaultSize: 24,
defaultStyle: 'filled'
};
/**
* Icon suggestions based on entity type/name
*/
const ICON_SUGGESTIONS = {
// People & Organizations
contact: [
{ name: 'Person', fileName: 'person_24_filled.svg', url: '', category: 'people' },
{ name: 'People', fileName: 'people_24_filled.svg', url: '', category: 'people' }
],
account: [
{ name: 'Building', fileName: 'building_24_filled.svg', url: '', category: 'places' },
{ name: 'Organization', fileName: 'organization_24_filled.svg', url: '', category: 'people' }
],
customer: [
{ name: 'Person Circle', fileName: 'person_circle_24_filled.svg', url: '', category: 'people' }
],
// Business
opportunity: [
{ name: 'Money', fileName: 'money_24_filled.svg', url: '', category: 'commerce' },
{ name: 'Target', fileName: 'target_24_filled.svg', url: '', category: 'general' }
],
quote: [
{ name: 'Document', fileName: 'document_24_filled.svg', url: '', category: 'document' },
{ name: 'Document Text', fileName: 'document_text_24_filled.svg', url: '', category: 'document' }
],
order: [
{ name: 'Cart', fileName: 'cart_24_filled.svg', url: '', category: 'commerce' },
{ name: 'Receipt', fileName: 'receipt_24_filled.svg', url: '', category: 'commerce' }
],
invoice: [
{ name: 'Receipt Money', fileName: 'receipt_money_24_filled.svg', url: '', category: 'commerce' }
],
product: [
{ name: 'Box', fileName: 'box_24_filled.svg', url: '', category: 'commerce' },
{ name: 'Cube', fileName: 'cube_24_filled.svg', url: '', category: 'general' }
],
// Cases & Service
case: [
{ name: 'Question Circle', fileName: 'question_circle_24_filled.svg', url: '', category: 'general' },
{ name: 'Chat Help', fileName: 'chat_help_24_filled.svg', url: '', category: 'communication' }
],
incident: [
{ name: 'Alert', fileName: 'alert_24_filled.svg', url: '', category: 'general' },
{ name: 'Warning', fileName: 'warning_24_filled.svg', url: '', category: 'general' }
],
ticket: [
{ name: 'Ticket', fileName: 'ticket_24_filled.svg', url: '', category: 'general' }
],
// Tasks & Activities
task: [
{ name: 'Task List', fileName: 'task_list_24_filled.svg', url: '', category: 'productivity' },
{ name: 'Checkmark', fileName: 'checkmark_24_filled.svg', url: '', category: 'general' }
],
appointment: [
{ name: 'Calendar', fileName: 'calendar_24_filled.svg', url: '', category: 'productivity' },
{ name: 'Calendar Clock', fileName: 'calendar_clock_24_filled.svg', url: '', category: 'productivity' }
],
email: [
{ name: 'Mail', fileName: 'mail_24_filled.svg', url: '', category: 'communication' },
{ name: 'Mail Inbox', fileName: 'mail_inbox_24_filled.svg', url: '', category: 'communication' }
],
phonecall: [
{ name: 'Call', fileName: 'call_24_filled.svg', url: '', category: 'communication' },
{ name: 'Phone', fileName: 'phone_24_filled.svg', url: '', category: 'communication' }
],
// Reference Data
category: [
{ name: 'Tag', fileName: 'tag_24_filled.svg', url: '', category: 'general' },
{ name: 'Grid', fileName: 'grid_24_filled.svg', url: '', category: 'general' }
],
status: [
{ name: 'Status', fileName: 'status_24_filled.svg', url: '', category: 'general' },
{ name: 'Circle', fileName: 'circle_24_filled.svg', url: '', category: 'shapes' }
],
type: [
{ name: 'Options', fileName: 'options_24_filled.svg', url: '', category: 'general' }
],
reason: [
{ name: 'Info', fileName: 'info_24_filled.svg', url: '', category: 'general' }
],
// Documents & Files
document: [
{ name: 'Document', fileName: 'document_24_filled.svg', url: '', category: 'document' },
{ name: 'Document Text', fileName: 'document_text_24_filled.svg', url: '', category: 'document' }
],
file: [
{ name: 'Document', fileName: 'document_24_filled.svg', url: '', category: 'document' }
],
attachment: [
{ name: 'Attach', fileName: 'attach_24_filled.svg', url: '', category: 'document' }
],
note: [
{ name: 'Note', fileName: 'note_24_filled.svg', url: '', category: 'document' },
{ name: 'Document Edit', fileName: 'document_edit_24_filled.svg', url: '', category: 'document' }
],
// Locations
location: [
{ name: 'Location', fileName: 'location_24_filled.svg', url: '', category: 'places' },
{ name: 'Pin', fileName: 'pin_24_filled.svg', url: '', category: 'places' }
],
address: [
{ name: 'Location', fileName: 'location_24_filled.svg', url: '', category: 'places' },
{ name: 'Home', fileName: 'home_24_filled.svg', url: '', category: 'places' }
],
// Projects & Work
project: [
{ name: 'Briefcase', fileName: 'briefcase_24_filled.svg', url: '', category: 'productivity' },
{ name: 'Folder', fileName: 'folder_24_filled.svg', url: '', category: 'document' }
],
application: [
{ name: 'Apps', fileName: 'apps_24_filled.svg', url: '', category: 'general' },
{ name: 'Window', fileName: 'window_24_filled.svg', url: '', category: 'general' }
],
// Default fallback
default: [
{ name: 'Circle', fileName: 'circle_24_filled.svg', url: '', category: 'shapes' },
{ name: 'Square', fileName: 'square_24_filled.svg', url: '', category: 'shapes' },
{ name: 'Star', fileName: 'star_24_filled.svg', url: '', category: 'shapes' }
]
};
/**
* Icon Manager Class
*/
export class IconManager {
baseUrl;
cache = new Map();
constructor() {
this.baseUrl = FLUENT_ICONS_CONFIG.baseUrl;
}
/**
* Suggest icons based on entity name or type
*/
suggestIcons(entityName) {
const lowerName = entityName.toLowerCase()
.replace(/^sic_/, '') // Remove publisher prefix
.replace(/^ref_/, ''); // Remove ref data infix
// Try exact match first
if (ICON_SUGGESTIONS[lowerName]) {
return this.populateUrls(ICON_SUGGESTIONS[lowerName]);
}
// Try partial match
for (const [key, suggestions] of Object.entries(ICON_SUGGESTIONS)) {
if (lowerName.includes(key) || key.includes(lowerName)) {
return this.populateUrls(suggestions);
}
}
// Return default suggestions
return this.populateUrls(ICON_SUGGESTIONS.default);
}
/**
* Populate full URLs for icon suggestions
*/
populateUrls(suggestions) {
return suggestions.map(s => {
try {
const iconPath = this.constructFluentIconPath(s.fileName);
return {
...s,
url: `${this.baseUrl}/${iconPath}`
};
}
catch (error) {
// If path construction fails, return suggestion with empty URL
return {
...s,
url: ''
};
}
});
}
/**
* Parse icon filename and construct GitHub path
* Converts: people_community_24_filled.svg
* To: assets/People Community/SVG/ic_fluent_people_community_24_filled.svg
*/
constructFluentIconPath(fileName) {
// Remove .svg extension
const base = fileName.replace('.svg', '');
// Split into parts
const parts = base.split('_');
// Extract size (e.g., '24') and style (e.g., 'filled' or 'regular')
const size = parts[parts.length - 2];
const style = parts[parts.length - 1];
// Validate size and style
if (!size || !style || isNaN(parseInt(size))) {
throw new Error(`Invalid icon filename format: ${fileName}. Expected format: {icon_name}_{size}_{style}.svg (e.g., people_community_24_filled.svg)`);
}
// Icon name is everything before size and style
const iconNameParts = parts.slice(0, -2);
if (iconNameParts.length === 0) {
throw new Error(`Invalid icon filename format: ${fileName}. Missing icon name.`);
}
// Folder name: capitalize each word, join with space
const folderName = iconNameParts
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
// Full filename with ic_fluent_ prefix
const fullFileName = `ic_fluent_${base}.svg`;
// Construct the full path
const path = `${folderName}/SVG/${fullFileName}`;
return path;
}
/**
* Fetch SVG icon from Fluent UI GitHub repository
*/
async fetchIcon(fileName) {
// Check cache first
if (this.cache.has(fileName)) {
return this.cache.get(fileName);
}
try {
// Construct the correct GitHub path
const iconPath = this.constructFluentIconPath(fileName);
const url = `${this.baseUrl}/${iconPath}`;
// Use global fetch if available (Node 18+)
const response = await global.fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch icon from GitHub: ${response.status} ${response.statusText}. URL: ${url}`);
}
const svg = await response.text();
// Validate it's an SVG
if (!svg.includes('<svg')) {
throw new Error('Fetched content is not a valid SVG');
}
// Cache the result
this.cache.set(fileName, svg);
return svg;
}
catch (error) {
throw new Error(`Failed to fetch icon '${fileName}': ${error instanceof Error ? error.message : String(error)}\n\nExpected format: {icon_name}_{size}_{style}.svg (e.g., people_community_24_filled.svg)\nBrowse icons at: https://github.com/microsoft/fluentui-system-icons`);
}
}
/**
* Generate web resource name for icon
*/
generateWebResourceName(entitySchemaName, iconName) {
const prefix = 'sic_';
const cleanEntityName = entitySchemaName.toLowerCase().replace(/^sic_/, '');
const cleanIconName = iconName.toLowerCase().replace(/\s+/g, '_');
return `${prefix}${cleanEntityName}_icon_${cleanIconName}`;
}
/**
* Generate icon vector name for EntityMetadata
* Uses $webresource: directive which is the correct syntax for Dynamics 365
* This creates a solution dependency and tells the system to look up the web resource by name
*/
generateIconVectorName(webResourceName) {
return `$webresource:${webResourceName}`;
}
/**
* Validate icon SVG content
*/
validateIconSvg(svg) {
if (!svg.includes('<svg')) {
return { valid: false, error: 'Content is not a valid SVG' };
}
if (svg.length > 100000) {
return { valid: false, error: 'SVG file is too large (max 100KB)' };
}
// Check for script tags (security)
if (svg.includes('<script')) {
return { valid: false, error: 'SVG contains script tags (security risk)' };
}
return { valid: true };
}
/**
* Get all available icon categories
*/
getCategories() {
const categories = new Set();
for (const suggestions of Object.values(ICON_SUGGESTIONS)) {
suggestions.forEach(s => categories.add(s.category));
}
return Array.from(categories).sort();
}
/**
* Search icons by name
*/
searchIcons(searchTerm) {
const results = [];
const lowerSearch = searchTerm.toLowerCase();
for (const suggestions of Object.values(ICON_SUGGESTIONS)) {
for (const suggestion of suggestions) {
if (suggestion.name.toLowerCase().includes(lowerSearch) ||
suggestion.fileName.toLowerCase().includes(lowerSearch)) {
results.push(this.populateUrls([suggestion])[0]);
}
}
}
return results;
}
/**
* Get icons by category
*/
getIconsByCategory(category) {
const results = [];
for (const suggestions of Object.values(ICON_SUGGESTIONS)) {
for (const suggestion of suggestions) {
if (suggestion.category === category) {
results.push(this.populateUrls([suggestion])[0]);
}
}
}
return results;
}
/**
* Build custom icon URL for specific size/style
*/
buildIconUrl(iconName, size = 24, style = 'filled') {
const fileName = `${iconName}_${size}_${style}.svg`;
try {
const iconPath = this.constructFluentIconPath(fileName);
return `${this.baseUrl}/${iconPath}`;
}
catch (error) {
throw new Error(`Failed to build icon URL for '${iconName}': ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Clear icon cache
*/
clearCache() {
this.cache.clear();
}
}
// Export singleton instance
export const iconManager = new IconManager();
//# sourceMappingURL=iconManager.js.map