diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php index 97c2b923767..9f86d50e92f 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php @@ -163,7 +163,7 @@ public function saveWidgetsAction() $dashboard = json_encode($this->request->getPost()); if (strlen($dashboard) > (1024 * 1024)) { // prevent saving large blobs of data - $result['message'] = 'dashboard size limit reached'; + $result['message'] = 'Dashboard size limit reached'; } elseif (($node = $this->usermdl->getUserByName($this->getUserName())) !== null) { $node->dashboard = base64_encode($dashboard); if ($this->usermdl->serializeToConfig(false, true)) { diff --git a/src/opnsense/www/js/opnsense_widget_manager.js b/src/opnsense/www/js/opnsense_widget_manager.js index ea65f7ca96f..9650c40c75e 100644 --- a/src/opnsense/www/js/opnsense_widget_manager.js +++ b/src/opnsense/www/js/opnsense_widget_manager.js @@ -414,6 +414,7 @@ class WidgetManager { $('.link-handle').hide(); $('.close-handle').show(); $('.edit-handle').show(); + $('.title-invisible').css('display', ''); } changed = true; @@ -808,6 +809,19 @@ class WidgetManager { $option.append($(`
${value.title}
`)); $option.append($select); break; + case 'textarea': + let $textarea = $(``); + $option.append($textarea); + break; default: console.error('Unknown option type', value.type); continue; @@ -816,28 +830,33 @@ class WidgetManager { $content.append($option); } + const hasTextarea = Object.values(options).some(v => v.type === 'textarea'); + const modalTitle = widget.dialogTitle || this.gettext.options; // present widget options BootstrapDialog.show({ - title: this.gettext.options, + title: modalTitle, draggable: true, animate: false, message: $content, buttons: [{ label: this.gettext.ok, - hotkey: 13, + hotkey: hasTextarea ? undefined : 13, action: async (dialog) => { let values = {}; for (const [key, value] of Object.entries(options)) { switch (value.type) { case 'select': values[key] = $(`#${value.id}`).val() ?? value.default; - break; + break; case 'select_multiple': values[key] = $(`#${value.id}`).val(); if (values[key].count === 0) { values[key] = value.default; } break; + case 'textarea': + values[key] = $(`#${value.id}`).val() ?? value.default; + break; default: console.error('Unknown option type', value.type); } diff --git a/src/opnsense/www/js/widgets/BaseWidget.js b/src/opnsense/www/js/widgets/BaseWidget.js index 7987dbdf4ce..525c898eff3 100644 --- a/src/opnsense/www/js/widgets/BaseWidget.js +++ b/src/opnsense/www/js/widgets/BaseWidget.js @@ -106,6 +106,10 @@ class BaseWidget { return {}; } + get dialogTitle() { + return null; + } + getMarkup() { return $(""); } diff --git a/src/opnsense/www/js/widgets/Metadata/Core.xml b/src/opnsense/www/js/widgets/Metadata/Core.xml index d3864af5317..262b7abee36 100644 --- a/src/opnsense/www/js/widgets/Metadata/Core.xml +++ b/src/opnsense/www/js/widgets/Metadata/Core.xml @@ -385,4 +385,11 @@ N/A + + Notes.js + + Notes + Note editor + + diff --git a/src/opnsense/www/js/widgets/Notes.js b/src/opnsense/www/js/widgets/Notes.js new file mode 100644 index 00000000000..a5077742b26 --- /dev/null +++ b/src/opnsense/www/js/widgets/Notes.js @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2026 Konstantinos Spartalis (cspartalis@potatonetworks.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +export default class Notes extends BaseWidget { + constructor(config) { + super(config); + this.configurable = true; + } + + getGridOptions() { + return { + minH: 82, + } + } + + get dialogTitle() { + return this.translations.titleedit; + } + + getMarkup() { + return $(` +
+
+
+ `); + } + + async getWidgetOptions() { + return { + note: { + title: this.translations.title, + type: 'textarea', + id: `notes-option-${this.id}`, + default: '', + }, + }; + } + + async onWidgetOptionsChanged(options) { + $(`#notes-text-${this.id}`).text(options.note || ''); + this.config.callbacks.updateGrid(); + } + + async onMarkupRendered() { + const config = await this.getWidgetConfig(); + const note = config.note || ''; + + $(`#notes-text-${this.id}`).text(note); + } +}