From 29fada375da62be353c069835a56f6c98d8e81bb Mon Sep 17 00:00:00 2001 From: PrayagCodes Date: Mon, 3 Nov 2025 01:16:06 -0600 Subject: [PATCH] feat: Enhanced alert system with multiple types and flexible configuration Implement comprehensive alert system supporting multiple alert types (info, success, warning, error, question) with type-specific icons and buttons, string array button syntax, and custom UI content injection. Features Added: - Multiple alert types with appropriate icons * Info: reminder icon * Success: checkmark icon * Warning: warning sign icon * Error: danger icon * Question: reminder icon - Type-specific default buttons * Question alerts default to Yes/No * Warning alerts default to OK/Cancel * Other types default to OK only - String array button support * Simple syntax: buttons: ['Save', 'Cancel'] * Auto-converts to button objects * First button gets primary styling * Returns clicked button text as value - Modern SDK API with backward compatibility * Object-style parameters: puter.ui.alert({ type, message, buttons }) * Legacy string/array parameters still supported - Custom UI content injection * Inject custom HTML between message and buttons * HTML sanitization with safe tag allowlist (,

,
) * Prevents XSS attacks while allowing basic formatting Files modified: - src/gui/src/UI/UIAlert.js: Icon mapping, button conversion, custom UI - src/puter-js/src/modules/UI.js: Object-based API - src/gui/src/IPC.js: CustomUI parameter forwarding Breaking Changes: for exisitng alert usage Example Usage: // Typed alert with string array buttons const choice = await puter.ui.alert({ type: 'question', message: 'Save changes?', buttons: ['Save', "Don't Save", 'Cancel'] }); // Custom UI content puter.ui.alert({ type: 'info', message: 'Upload Progress', customUI: '

File: doc.pdf

75% complete

' }); --- src/gui/src/IPC.js | 24 +++++++--- src/gui/src/UI/UIAlert.js | 84 ++++++++++++++++++++++++++++------ src/puter-js/src/modules/UI.js | 33 ++++++++++++- 3 files changed, 119 insertions(+), 22 deletions(-) diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index bd8132dad5..9ae04c206f 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -160,14 +160,24 @@ const ipc_listener = async (event, handled) => { // ALERT //-------------------------------------------------------- else if(event.data.msg === 'ALERT' && event.data.message !== undefined){ + if (event.data.message === undefined || event.data.message === null) { + console.error('Alert message is undefined or null', event.data); + event.data.message = 'Alert'; // Provide a default message + } + // Normalize message format - handle both string and object + const msgData = typeof event.data.message === 'string' + ? { message: event.data.message } + : event.data.message; + const alert_resp = await UIAlert({ - message: event.data.message, - buttons: event.data.buttons, - type: event.data.options?.type, - window_options: { - parent_uuid: event.data.appInstanceID, - disable_parent_window: true, - } + message: msgData.message, + buttons: msgData.buttons, + type: msgData.type, + customUI: msgData.customUI, + window_options: { + parent_uuid: event.data.appInstanceID, + disable_parent_window: true, + } }) target_iframe.contentWindow.postMessage({ diff --git a/src/gui/src/UI/UIAlert.js b/src/gui/src/UI/UIAlert.js index 82f2a7b0af..9254872fc6 100644 --- a/src/gui/src/UI/UIAlert.js +++ b/src/gui/src/UI/UIAlert.js @@ -34,33 +34,89 @@ function UIAlert(options){ } return new Promise(async (resolve) => { - // provide an 'OK' button if no buttons are provided + // Provide type-specific default buttons if no buttons are provided if(!options.buttons || options.buttons.length === 0){ - options.buttons = [ - {label: i18n('ok'), value: true, type: 'primary'} - ] + switch (options.type) { + case 'question': + options.buttons = [ + { label: i18n('yes'), value: 'yes', type: 'primary' }, + { label: i18n('no'), value: 'no', type: 'default' } + ]; + break; + case 'warning': + options.buttons = [ + { label: i18n('ok'), value: true, type: 'primary' }, + { label: i18n('cancel'), value: false, type: 'default' } + ]; + break; + case 'error': + case 'info': + case 'success': + default: + options.buttons = [ + { label: i18n('ok'), value: true, type: 'primary' } + ]; + break; + } + } + + // Convert string array to button objects + if (options.buttons && options.buttons.length > 0 && + typeof options.buttons[0] === 'string') { + options.buttons = options.buttons.map((label, index) => ({ + label: label, + value: label, + type: index === 0 ? 'primary' : 'default' + })); } - // set body icon - options.body_icon = options.body_icon ?? window.icons['warning-sign.svg']; - if(options.type === 'success') - options.body_icon = window.icons['c-check.svg']; + // Icon mapping for all alert types + const iconMap = { + 'warning': window.icons['warning-sign.svg'], + 'success': window.icons['c-check.svg'], + 'error': window.icons['danger.svg'], + 'info': window.icons['reminder.svg'], + 'question': window.icons['reminder.svg'], + }; - let santized_message = html_encode(options.message); + // Set body icon based on type, or use custom override + options.body_icon = options.body_icon ?? + (options.type ? iconMap[options.type] : iconMap['warning']); + let message = options.message; + if (typeof message !== 'string') { + message = message != null ? String(message) : ''; + } + + let sanitized_message = html_encode(message); // replace sanitized with - santized_message = santized_message.replace(/<strong>/g, ''); - santized_message = santized_message.replace(/<\/strong>/g, ''); + sanitized_message = sanitized_message.replace(/<strong>/g, ''); + sanitized_message = sanitized_message.replace(/<\/strong>/g, ''); // replace sanitized

with

- santized_message = santized_message.replace(/<p>/g, '

'); - santized_message = santized_message.replace(/<\/p>/g, '

'); + sanitized_message = sanitized_message.replace(/<p>/g, '

'); + sanitized_message = sanitized_message.replace(/<\/p>/g, '

'); let h = ''; // icon h += ``; // message - h += `
${santized_message}
`; + h += `
${sanitized_message}
`; + + // Custom UI content (if provided) + if (options.customUI) { + let sanitized_custom = html_encode(options.customUI); + // Allow safe tags + sanitized_custom = sanitized_custom.replace(/<strong>/g, ''); + sanitized_custom = sanitized_custom.replace(/<\/strong>/g, ''); + sanitized_custom = sanitized_custom.replace(/<p>/g, '

'); + sanitized_custom = sanitized_custom.replace(/<\/p>/g, '

'); + sanitized_custom = sanitized_custom.replace(/<br>/g, '
'); + sanitized_custom = sanitized_custom.replace(/<br\/>/g, '
'); + + h += `
${sanitized_custom}
`; + } + // buttons if(options.buttons && options.buttons.length > 0){ h += `
`; diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index f42111efeb..7960a08a50 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -622,7 +622,38 @@ class UI extends EventListener { alert = function(message, buttons, options, callback) { return new Promise((resolve) => { - this.#postMessageWithCallback('ALERT', resolve, { message, buttons, options }); + let messagePayload; + + // Support object-based API: alert({ type, message, buttons, ... }) + if (typeof message === 'object' && message !== null && !Array.isArray(message)) { + // New API - first argument is an options object + messagePayload = { + message: message.message, + type: message.type, + buttons: message.buttons, + customUI: message.customUI, + body_icon: message.body_icon, + backdrop: message.backdrop, + stay_on_top: message.stay_on_top, + draggable_body: message.draggable_body, + window_options: message.window_options + }; + } else { + // Legacy API: alert(message, buttons, options) + messagePayload = { + message: message, + buttons: buttons, + type: options?.type, + customUI: options?.customUI, + body_icon: options?.body_icon, + backdrop: options?.backdrop, + stay_on_top: options?.stay_on_top, + draggable_body: options?.draggable_body, + window_options: options?.window_options + }; + } + + this.#postMessageWithCallback('ALERT', resolve, { message: messagePayload }); }) }