mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-04 01:01:35 -07:00
The rename of the elementOptions key from "ordeable" to "orderable" (part of #1584) updated handleElementOptions() in settings_utils.js to return the property as isOrderable. However, multiEditCore.php still destructured the old name isOrdeable from that return value (line 139). Because JavaScript object destructuring resolves properties by name, isOrdeable would silently evaluate to undefined — no runtime error, just a broken binding. The bug was masked because isOrdeable is not referenced after destructuring in the current code of multiEditCore.php. The incorrect binding would become a functional regression as soon as that code path is extended to actually consume the orderable flag (e.g. to conditionally apply select2 sorting in the multi-edit form). Changes: - front/multiEditCore.php:139 — isOrdeable → isOrderable Aligns the destructured property name with the renamed return key of handleElementOptions() so the binding resolves to the correct boolean value instead of undefined. All 35 previously updated files already use the correct spelling; this was the single remaining inconsistency. After this commit, grep for "isOrdeable" and "ordeable" across front/ and server/ returns zero results.
550 lines
20 KiB
PHP
Executable File
550 lines
20 KiB
PHP
Executable File
<?php
|
|
//------------------------------------------------------------------------------
|
|
// check if authenticated
|
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
|
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
|
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
|
|
?>
|
|
|
|
<div class="col-md-12">
|
|
<div class="callout callout-warning">
|
|
<h4><?= lang('Gen_Warning');?></h4>
|
|
|
|
<p><?= lang('Device_MultiEdit_Backup');?></p>
|
|
</div>
|
|
<div class="box box-default">
|
|
|
|
<div class="box-header">
|
|
|
|
<h3 class="box-title"><?= lang('Gen_Selected_Devices');?></h3>
|
|
|
|
</div>
|
|
<div class="deviceSelector col-md-11 col-sm-11" style="z-index:5">
|
|
<div class="db_info_table_row col-sm-12" >
|
|
<div class="form-group" >
|
|
<div class="input-group col-sm-12 " >
|
|
<select class="form-control select2 select2-hidden-accessible" multiple="" style="width: 100%;" tabindex="-1" aria-hidden="true">
|
|
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-1 hoverHighlight">
|
|
<i class="fa-solid fa-circle-check hoverHighlight pointer" onclick="markAllSelected()" title="<?= lang('Gen_Add_All');?>"></i>
|
|
<i class="fa-solid fa-circle-xmark hoverHighlight pointer" onclick="markAllNotSelected()" title="<?= lang('Gen_Remove_All');?>"></i>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="col-md-12">
|
|
<div class="box box-default">
|
|
|
|
<div class="box-header">
|
|
<h3 class="box-title"><?= lang('Device_MultiEdit_Fields');?></h3>
|
|
</div>
|
|
<div class="box-body">
|
|
<form id="multi-edit-form">
|
|
<!-- Form fields will be appended here -->
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="col-md-12">
|
|
<div class="box box-default">
|
|
<div class="box-header ">
|
|
<h3 class="box-title"><?= lang('Device_MultiEdit_MassActions');?></h3>
|
|
</div>
|
|
<div class="box-body">
|
|
<div class="col-md-12">
|
|
<div class="col-md-4" style="">
|
|
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red col-md-12 col-sm-12 col-xs-12" id="btnDeleteMAC" onclick="askDeleteSelectedDevices()"><?= lang('Maintenance_Tool_del_selecteddev');?></button>
|
|
</div>
|
|
<div class="col-md-8"><?= lang('Maintenance_Tool_del_selecteddev_text');?></div>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<div class="col-md-4" style="">
|
|
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red col-md-12 col-sm-12 col-xs-12" id="btnUnlockFieldsSelected" onclick="askUnlockFieldsSelected()"><?= lang('Maintenance_Tool_unlockFields_selecteddev');?></button>
|
|
</div>
|
|
<div class="col-md-8"><?= lang('Maintenance_Tool_del_unlockFields_selecteddev_text');?></div>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<div class="col-md-4" style="">
|
|
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red col-md-12 col-sm-12 col-xs-12" id="btnClearSourceFields" onclick="askClearSourceFields()"><?= lang('Maintenance_Tool_clearSourceFields_selected');?></button>
|
|
</div>
|
|
<div class="col-md-8"><?= lang('Maintenance_Tool_clearSourceFields_selected_text');?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script defer>
|
|
|
|
// -------------------------------------------------------------------
|
|
// Get plugin and settings data from API endpoints
|
|
function getData(){
|
|
|
|
// some race condition, need to implement delay
|
|
setTimeout(() => {
|
|
$.get('php/server/query_json.php', { file: 'table_settings.json', nocache: Date.now() }, function(res) {
|
|
|
|
settingsData = res["data"];
|
|
|
|
excludedColumns = ["NEWDEV_devMac", "NEWDEV_devFirstConnection", "NEWDEV_devLastConnection", "NEWDEV_devLastNotification", "NEWDEV_devScan", "NEWDEV_devPresentLastScan", "NEWDEV_devCustomProps", "NEWDEV_devChildrenNicsDynamic", "NEWDEV_devChildrenDynamic" ]
|
|
|
|
const relevantColumns = settingsData.filter(set =>
|
|
set.setGroup === "NEWDEV" &&
|
|
set.setKey.includes("_dev") &&
|
|
!excludedColumns.includes(set.setKey) &&
|
|
!set.setKey.includes("__metadata")
|
|
);
|
|
|
|
const generateSimpleForm = multiEditColumns => {
|
|
const form = $('#multi-edit-form');
|
|
const numColumns = 2; // Number of columns
|
|
|
|
// Calculate number of elements per column
|
|
const elementsPerColumn = Math.ceil(multiEditColumns.length / numColumns);
|
|
|
|
// Divide columns equally
|
|
for (let i = 0; i < numColumns; i++) {
|
|
const column = $('<div>').addClass('col-md-6');
|
|
|
|
// Append form groups to the column
|
|
for (let j = i * elementsPerColumn; j < Math.min((i + 1) * elementsPerColumn, multiEditColumns.length); j++) {
|
|
|
|
const setTypeObject = JSON.parse(multiEditColumns[j].setType.replace(/'/g, '"'));
|
|
|
|
// get the element with the input value(s)
|
|
let elements = setTypeObject.elements.filter(element => element.elementHasInputValue === 1);
|
|
|
|
// if none found, take last
|
|
if(elements.length == 0)
|
|
{
|
|
elementWithInputValue = setTypeObject.elements[setTypeObject.elements.length - 1]
|
|
} else
|
|
{
|
|
elementWithInputValue = elements[0]
|
|
}
|
|
|
|
const { elementType, elementOptions = [], transformers = [] } = elementWithInputValue;
|
|
const {
|
|
inputType,
|
|
readOnly,
|
|
isMultiSelect,
|
|
isOrderable,
|
|
cssClasses,
|
|
placeholder,
|
|
suffix,
|
|
sourceIds,
|
|
separator,
|
|
editable,
|
|
valRes,
|
|
getStringKey,
|
|
onClick,
|
|
onChange,
|
|
customParams,
|
|
customId,
|
|
columns,
|
|
base64Regex,
|
|
elementOptionsBase64,
|
|
focusout
|
|
} = handleElementOptions('none', elementOptions, transformers, val = "");
|
|
|
|
// render based on element type
|
|
if (elementType === 'select') {
|
|
|
|
targetLocation = multiEditColumns[j].setKey + "_generateSetOptions"
|
|
|
|
generateOptionsOrSetOptions(multiEditColumns[j].setKey, [], targetLocation, generateOptions, null)
|
|
|
|
console.log(multiEditColumns[j].setKey)
|
|
// Handle Icons as they need a preview
|
|
if(multiEditColumns[j].setKey == 'NEWDEV_devIcon')
|
|
{
|
|
input = `
|
|
<span class="input-group-addon iconPreview" my-customid="NEWDEV_devIcon_preview"></span>
|
|
<select class="form-control"
|
|
onChange="updateIconPreview(this)"
|
|
my-customparams="NEWDEV_devIcon,NEWDEV_devIcon_preview"
|
|
id="${multiEditColumns[j].setKey}"
|
|
data-my-column="${multiEditColumns[j].setKey}"
|
|
data-my-targetColumns="${multiEditColumns[j].setKey.replace('NEWDEV_','')}" >
|
|
<option id="${targetLocation}"></option>
|
|
</select>`
|
|
|
|
} else{
|
|
|
|
input = `<select class="form-control"
|
|
id="${multiEditColumns[j].setKey}"
|
|
data-my-column="${multiEditColumns[j].setKey}"
|
|
data-my-targetColumns="${multiEditColumns[j].setKey.replace('NEWDEV_','')}" >
|
|
<option id="${targetLocation}"></option>
|
|
</select>`
|
|
}
|
|
|
|
|
|
} else if (elementType === 'input' || elementType === 'textarea'){
|
|
|
|
// Add classes specifically for checkboxes
|
|
inputType === 'checkbox' ? inputClass = 'checkbox' : inputClass = 'form-control';
|
|
|
|
|
|
input = `<input class="${inputClass}"
|
|
id="${multiEditColumns[j].setKey}"
|
|
my-customid="${multiEditColumns[j].setKey}"
|
|
data-my-column="${multiEditColumns[j].setKey}"
|
|
data-my-targetColumns="${multiEditColumns[j].setKey.replace('NEWDEV_','')}"
|
|
type="${inputType}">`
|
|
}
|
|
|
|
|
|
const inputEntry = `<div class="form-group col-sm-12" >
|
|
<label class="col-sm-3 control-label">${multiEditColumns[j].setName}</label>
|
|
<div class="col-sm-9">
|
|
<div class="input-group red-hover-border">
|
|
${input}
|
|
<span class="input-group-addon pointer red-hover-background" onclick="massUpdateField('${multiEditColumns[j].setKey}');" title="${getString('Device_MultiEdit_Tooltip')}">
|
|
<i class="fa fa-save"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>`
|
|
|
|
|
|
column.append(inputEntry);
|
|
}
|
|
|
|
form.append(column);
|
|
}
|
|
};
|
|
|
|
console.log(relevantColumns)
|
|
|
|
generateSimpleForm(relevantColumns);
|
|
|
|
initSelect2();
|
|
initDeviceSelectors();
|
|
|
|
|
|
})
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Initialize device selectors / pickers fields
|
|
function initDeviceSelectors() {
|
|
|
|
// Parse device list using the shared helper
|
|
devicesList = parseDeviceCache(getCache('devicesListAll_JSON'));
|
|
|
|
// Check if the device list exists and is an array
|
|
if (Array.isArray(devicesList)) {
|
|
const $select = $(".deviceSelector select");
|
|
|
|
$select.append(
|
|
devicesList
|
|
.filter(d => d.devMac && d.devName)
|
|
.map(d => `<option value="${d.devMac}">${d.devName}</option>`)
|
|
.join('')
|
|
).trigger('change');
|
|
}
|
|
|
|
|
|
|
|
// Initialize selected items after a delay so selected macs are available in the context
|
|
setTimeout(function(){
|
|
// Retrieve MAC addresses from query string or cache
|
|
var macs = getQueryString('macs') || getCache('selectedDevices');
|
|
|
|
if(macs)
|
|
{
|
|
// Split MAC addresses if they are comma-separated
|
|
macs = macs.split(',');
|
|
|
|
console.log(macs)
|
|
|
|
// Loop through macs to be selected list
|
|
macs.forEach(function(mac) {
|
|
|
|
// Create the option and append to Select2
|
|
var option = new Option($('.deviceSelector select option[value="' + mac + '"]').html(), mac, true, true);
|
|
|
|
$('.deviceSelector select').append(option).trigger('change');
|
|
});
|
|
|
|
}
|
|
|
|
}, 10);
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Get selected devices Macs
|
|
function selectorMacs () {
|
|
return $('.deviceSelector select').val().join(',');
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Select All
|
|
function markAllSelected() {
|
|
// Get the <select> element with the class 'deviceSelector'
|
|
var selectElement = $('.deviceSelector select');
|
|
|
|
// Iterate over each option within the select element
|
|
selectElement.find('option').each(function() {
|
|
// Mark each option as selected
|
|
$(this).prop('selected', true);
|
|
});
|
|
|
|
// Trigger the 'change' event to notify Bootstrap Select of the changes
|
|
selectElement.trigger('change');
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// UN-Select All
|
|
function markAllNotSelected() {
|
|
// Get the <select> element with the class 'deviceSelector'
|
|
var selectElement = $('.deviceSelector select');
|
|
|
|
// Iterate over each option within the select element
|
|
selectElement.find('option').each(function() {
|
|
// Unselect each option
|
|
$(this).prop('selected', false);
|
|
});
|
|
|
|
// Trigger the 'change' event to notify Bootstrap Select of the changes
|
|
selectElement.trigger('change');
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Update specified field over the specified DB column and selected entry(ies)
|
|
function massUpdateField(id) {
|
|
|
|
// Get the input element
|
|
var inputElement = $(`#${id}`);
|
|
|
|
console.log(inputElement);
|
|
console.log(id);
|
|
|
|
// Initialize columnValue variable
|
|
var columnValue;
|
|
|
|
// Check the type of the input element
|
|
if (inputElement.is(':checkbox')) {
|
|
// For checkboxes, set the value to 1 if checked, otherwise set it to 0
|
|
columnValue = inputElement.is(':checked') ? 1 : 0;
|
|
} else {
|
|
// For other input types (like textboxes), simply retrieve their values
|
|
// Don't encode icons (already base64) or other pre-encoded values
|
|
columnValue = inputElement.val();
|
|
}
|
|
|
|
var targetColumns = inputElement.attr('data-my-targetColumns');
|
|
|
|
|
|
console.log(targetColumns);
|
|
console.log(columnValue);
|
|
|
|
// update selected
|
|
if(selectorMacs() != "")
|
|
{
|
|
executeAction('update', 'devMac', selectorMacs(), targetColumns, columnValue )
|
|
}
|
|
else
|
|
{
|
|
showModalWarning(getString("Gen_Error"), getString('Device_MultiEdit_No_Devices'));
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// action: Represents the action to be performed, a CRUD operation like "update", "delete", etc.
|
|
// whereColumnName: Specifies the name of the column used in the WHERE or SELECT statement for filtering.
|
|
// key: Represents the unique identifier of the row or record to be acted upon.
|
|
// targetColumns: Indicates the columns to be updated or affected by the action.
|
|
// newTargetColumnValue: Specifies the new value to be assigned to the specified column(s).
|
|
function executeAction(action, whereColumnName, key, targetColumns, newTargetColumnValue )
|
|
{
|
|
const apiBase = getApiBase();
|
|
const apiToken = getSetting("API_TOKEN");
|
|
const url = `${apiBase}/dbquery/${action}`;
|
|
|
|
// Convert comma-separated string to array if needed
|
|
let idArray = key;
|
|
if (typeof key === 'string' && key.includes(',')) {
|
|
idArray = key.split(',');
|
|
} else if (!Array.isArray(key)) {
|
|
idArray = [key];
|
|
}
|
|
|
|
// Build request data based on action type
|
|
const requestData = {
|
|
dbtable: "Devices",
|
|
columnName: whereColumnName,
|
|
id: idArray
|
|
};
|
|
|
|
// Only include columns and values for update action
|
|
if (action === "update") {
|
|
// Ensure columns and values are arrays
|
|
requestData.columns = Array.isArray(targetColumns) ? targetColumns : [targetColumns];
|
|
requestData.values = Array.isArray(newTargetColumnValue) ? newTargetColumnValue : [newTargetColumnValue];
|
|
}
|
|
|
|
$.ajax({
|
|
url,
|
|
method: "POST",
|
|
headers: { "Authorization": `Bearer ${apiToken}` },
|
|
data: JSON.stringify(requestData),
|
|
contentType: "application/json",
|
|
success: function(response) {
|
|
if (response.success) {
|
|
showMessage(getString('Gen_DataUpdatedUITakesTime'));
|
|
// Remove navigation prompt "Are you sure you want to leave..."
|
|
window.onbeforeunload = null;
|
|
|
|
// update API endpoints to refresh the UI
|
|
updateApi("devices,appevents")
|
|
|
|
const columnsMsg = targetColumns ? ` on Columns "${targetColumns}"` : '';
|
|
write_notification(`[Multi edit] Executed "${action}"${columnsMsg} matching "${key}"`, 'info')
|
|
|
|
} else {
|
|
console.error(response.error || "Unknown error");
|
|
showMessage(response.error || getString('Gen_LockedDB'));
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error("Error executing action:", status, error, xhr.responseJSON);
|
|
showMessage("Error: " + (xhr.responseJSON?.error || error));
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Ask to unlock fields of selected devices
|
|
function askUnlockFieldsSelected () {
|
|
// Ask
|
|
showModalWarning(
|
|
getString('Maintenance_Tool_unlockFields_selecteddev_noti'),
|
|
getString('Gen_AreYouSure'),
|
|
getString('Gen_Cancel'),
|
|
getString('Gen_Okay'),
|
|
'unlockFieldsSelected');
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Ask to unlock fields of selected devices
|
|
function askClearSourceFields () {
|
|
// Ask
|
|
showModalWarning(
|
|
getString('Maintenance_Tool_clearSourceFields_selected_noti'),
|
|
getString('Gen_AreYouSure'),
|
|
getString('Gen_Cancel'),
|
|
getString('Gen_Okay'),
|
|
()=>unlockFieldsSelected(null, true));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Unlock fields for selected devices
|
|
function unlockFieldsSelected(fields = null, clearAll = false) {
|
|
// Get selected MACs
|
|
const macs_tmp = selectorMacs(); // returns array of MACs
|
|
|
|
console.log(macs_tmp);
|
|
console.log(clearAll);
|
|
|
|
|
|
if (!macs_tmp || macs_tmp == "" || macs_tmp.length === 0) {
|
|
showMessage(textMessage = "No devices selected", timeout = 3000, colorClass = "modal_red")
|
|
return;
|
|
}
|
|
|
|
// API setup
|
|
const apiBase = getApiBase();
|
|
const apiToken = getSetting("API_TOKEN");
|
|
const url = `${apiBase}/devices/fields/unlock`;
|
|
|
|
// Convert string to array
|
|
const macsArray = macs_tmp.split(",").map(m => m.trim()).filter(Boolean);
|
|
|
|
const payload = {
|
|
mac: macsArray, // array of MACs for backend
|
|
fields: fields, // null for all tracked fields
|
|
clearAll: clearAll // true to clear all sources, false to clear only LOCKED/USER
|
|
};
|
|
|
|
$.ajax({
|
|
url: url,
|
|
method: "POST",
|
|
headers: { "Authorization": `Bearer ${apiToken}` },
|
|
contentType: "application/json",
|
|
data: JSON.stringify(payload),
|
|
success: function(response) {
|
|
if (response.success) {
|
|
showMessage(getString('Gen_DataUpdatedUITakesTime'));
|
|
write_notification(
|
|
`[Multi edit] Successfully unlocked fields of devices with MACs: ${macs_tmp}`,
|
|
"info"
|
|
);
|
|
} else {
|
|
write_notification(
|
|
`[Multi edit] Failed to unlock fields: ${response.error || "Unknown error"}`,
|
|
"interrupt"
|
|
);
|
|
}
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error("Error unlocking fields:", status, error);
|
|
write_notification(
|
|
`[Multi edit] Error unlocking fields: ${xhr.responseJSON?.error || error}`,
|
|
"error"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Ask to delete selected devices
|
|
function askDeleteSelectedDevices () {
|
|
// Ask
|
|
showModalWarning(
|
|
getString('Maintenance_Tool_del_alldev_noti'),
|
|
getString('Gen_AreYouSure'),
|
|
getString('Gen_Cancel'),
|
|
getString('Gen_Delete'),
|
|
'deleteSelectedDevices');
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Delete selected devices
|
|
function deleteSelectedDevices()
|
|
{
|
|
macs_tmp = selectorMacs()
|
|
executeAction('delete', 'devMac', macs_tmp )
|
|
write_notification('[Multi edit] Manually deleted devices with MACs:' + macs_tmp, 'info')
|
|
}
|
|
|
|
|
|
getData();
|
|
|
|
|
|
</script>
|
|
|
|
<!-- ----------------------------------------------------------------------- -->
|
|
|
|
<!-- ----------------------------------------------------------------------- -->
|
|
|
|
|