dashboard: add Notepad widget (#9936)

(cherry picked from commit 558bedda03)
(cherry picked from commit e54c919ea1)
This commit is contained in:
Konstantinos Spartalis 2026-03-31 17:04:44 +03:00 committed by Franco Fichtner
parent 3653be77f5
commit a38db0dbdf
6 changed files with 108 additions and 4 deletions

View file

@ -24,6 +24,7 @@ Copyright (c) 2004 Jim McBeath
Copyright (c) 2010-2012 Jim Pingle <jimp@pfsense.org>
Copyright (c) 2012 Jonas von Andrian
Copyright (c) 2015 Jos Schellevis <jos@opnsense.org>
Copyright (c) 2026 Konstantinos Spartalis <cspartalis@potatonetworks.com>
Copyright (c) 2021 Kyle Evans <kevans@FreeBSD.org>
Copyright (c) 2015 Manuel Faux <mfaux@conf.at>
Copyright (c) 2003-2006 Manuel Kasper <mk@neon1.net>

View file

@ -162,7 +162,7 @@ class DashboardController extends ApiControllerBase
$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)) {

View file

@ -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($(`<div><b>${value.title}</b></div>`));
$option.append($select);
break;
case 'textarea':
let $textarea = $(`<textarea
id="${value.id}"
maxlength="${value.maxlength || 8192}"
class="form-control"
style="
max-width: 100%;
resize: vertical;
min-height: ${value.minHeight || '150px'};
box-sizing: border-box;
">${config[key] || ''}</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);
}

View file

@ -106,6 +106,10 @@ class BaseWidget {
return {};
}
get dialogTitle() {
return null;
}
getMarkup() {
return $("");
}

View file

@ -385,4 +385,11 @@
<notavailable>N/A</notavailable>
</translations>
</dnsmasqleases>
<notes>
<filename>Notes.js</filename>
<translations>
<title>Notes</title>
<titleedit>Note editor</titleedit>
</translations>
</notes>
</metadata>

View file

@ -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 $(`
<div id="notes-container-${this.id}" class="widget-content">
<div id="notes-text-${this.id}" style="padding: 10px; white-space: pre-wrap; word-wrap: break-word;"></div>
</div>
`);
}
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);
}
}