mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-06-21 06:39:52 -04:00
feat(workflows): Enhance device management with query capabilities and conflict resolution
- Added `queryByConditions` method to `DeviceInstance` for flexible device querying based on dynamic conditions. - Introduced `interpolate_tokens` function to replace placeholders in action values with actual device data. - Updated `UpdateFieldAction` to handle cross-device updates and archive conflicting MAC addresses. - Implemented cascade prevention in `WorkflowManager` to avoid processing events for devices modified in the same batch. - Added unit tests for new functionalities, including token interpolation, condition querying, and action execution. - Created constants for device column validation to enhance security and maintainability. - Established a structured research skill specification to guide development practices.
This commit is contained in:
@@ -295,8 +295,86 @@ function generateWorkflowUI(wf, wfIndex) {
|
||||
});
|
||||
|
||||
|
||||
// Dropdown for action.type
|
||||
let $actionDropdown= createEditableDropdown(
|
||||
// how big should the background icon be — computed after all content decisions
|
||||
let numberOfLines = 1
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Target selector — shown first so user picks the target before the action
|
||||
// Applies to update_field and delete_device actions
|
||||
// ------------------------------------------------------------------
|
||||
if (action.type == "update_field" || action.type == "delete_device") {
|
||||
let currentStrategy = (action.target && action.target.strategy) ? action.target.strategy : "triggering_device";
|
||||
|
||||
let $targetDropdown = createEditableDropdown(
|
||||
`[${wfIndex}].actions[${actionIndex}].target.strategy`,
|
||||
getString("WF_Action_target"),
|
||||
["triggering_device", "query"],
|
||||
currentStrategy,
|
||||
`wf-${wfIndex}-actionIndex-${actionIndex}-target-strategy`
|
||||
);
|
||||
|
||||
$actionEl.append($targetDropdown);
|
||||
|
||||
// Conditional query conditions sub-form
|
||||
let $targetConditionsWrap = $("<div>", {
|
||||
class: `action-target-conditions panel col-sm-12 col-sx-12 ${currentStrategy === "query" ? "" : "hidden"}`,
|
||||
id: `wf-${wfIndex}-actionIndex-${actionIndex}-target-conditions-wrap`
|
||||
});
|
||||
|
||||
let $targetConditionsTitle = $("<div>", { class: "section-title" })
|
||||
.append($("<i>", { class: "fa-solid fa-crosshairs" }))
|
||||
.append(` ${getString("WF_Action_target_conditions")}:`);
|
||||
|
||||
let $tokenHint = $("<div>", { class: "text-muted small col-sm-12 col-xs-12" })
|
||||
.text(getString("WF_Action_token_hint"));
|
||||
|
||||
$targetConditionsWrap.append($targetConditionsTitle);
|
||||
$targetConditionsWrap.append($tokenHint);
|
||||
|
||||
let targetConditions = (action.target && action.target.conditions) ? action.target.conditions : [];
|
||||
let targetBasePath = `[${wfIndex}].actions[${actionIndex}].target`;
|
||||
|
||||
$.each(targetConditions, function(tcIdx, tc) {
|
||||
let $tcRow = createTargetConditionRow(wfIndex, actionIndex, tcIdx, tc, targetBasePath);
|
||||
$targetConditionsWrap.append($tcRow);
|
||||
});
|
||||
|
||||
let $addTargetCondBtn = $("<div>", {
|
||||
class: "pointer add-target-condition green-hover-text col-sm-12",
|
||||
wfIndex: wfIndex,
|
||||
actionIndex: actionIndex
|
||||
}).append($("<i>", { class: "fa-solid fa-plus" })).append(` ${getString("WF_Add_Condition")}`);
|
||||
|
||||
$targetConditionsWrap.append($addTargetCondBtn);
|
||||
$actionEl.append($targetConditionsWrap);
|
||||
|
||||
// Show/hide conditions sub-form when strategy dropdown changes
|
||||
$targetDropdown.find("select").on("change", function() {
|
||||
let val = $(this).val();
|
||||
let $wrap = $(`#wf-${wfIndex}-actionIndex-${actionIndex}-target-conditions-wrap`);
|
||||
if (val === "query") {
|
||||
$wrap.removeClass("hidden");
|
||||
} else {
|
||||
$wrap.addClass("hidden");
|
||||
// Strip target.conditions from the in-memory object when switching away from query
|
||||
let wfs = getWorkflowsJson();
|
||||
if (wfs[wfIndex] && wfs[wfIndex].actions[actionIndex] && wfs[wfIndex].actions[actionIndex].target) {
|
||||
delete wfs[wfIndex].actions[actionIndex].target.conditions;
|
||||
}
|
||||
updateWorkflowsJson(wfs);
|
||||
}
|
||||
});
|
||||
|
||||
// numberOfLines: 1 (target dropdown) = 1 base for both action types
|
||||
// query mode adds: 1 (section title+hint) + N×3 (each condition: field/op/value) + 1 (add btn)
|
||||
let conditionLines = currentStrategy === "query"
|
||||
? 2 + (targetConditions.length * 3)
|
||||
: 0;
|
||||
numberOfLines = 1 + conditionLines;
|
||||
}
|
||||
|
||||
// Dropdown for action.type — rendered after target so user reads: who → what
|
||||
let $actionDropdown = createEditableDropdown(
|
||||
`[${wfIndex}].actions[${actionIndex}].type`,
|
||||
getString("WF_Action_type"),
|
||||
actionTypes,
|
||||
@@ -304,15 +382,13 @@ function generateWorkflowUI(wf, wfIndex) {
|
||||
`wf-${wfIndex}-actionIndex-${actionIndex}-type`
|
||||
);
|
||||
|
||||
|
||||
$actionEl.append($actionDropdown);
|
||||
|
||||
// how big should the background icon be
|
||||
let numberOfLines = 1
|
||||
numberOfLines += 1;
|
||||
|
||||
if(action.type == "update_field")
|
||||
{
|
||||
numberOfLines = 3
|
||||
// +2 for field dropdown and value input rows
|
||||
numberOfLines += 2;
|
||||
|
||||
// Dropdown for action.field
|
||||
let $fieldDropdown = createEditableDropdown(
|
||||
@@ -356,7 +432,8 @@ function generateWorkflowUI(wf, wfIndex) {
|
||||
$actionRemoveButtonWrap.append($actionRemoveButton);
|
||||
|
||||
let $actionIcon = $("<i>", {
|
||||
class: `fa-solid fa-person-running fa-flip-horizontal bckg-icon-${numberOfLines}-line `
|
||||
class: `fa-solid fa-person-running fa-flip-horizontal bckg-icon-base`,
|
||||
style: `font-size: ${numberOfLines * 3}em`
|
||||
});
|
||||
|
||||
$actionEl.prepend($actionIcon)
|
||||
@@ -721,6 +798,57 @@ function createEditableInput(jsonPath, labelText, value, id, className = "") {
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// Render a single row in a target-conditions sub-form (cross-device query targeting v2)
|
||||
function createTargetConditionRow(wfIndex, actionIndex, tcIdx, tc, targetBasePath) {
|
||||
let basePath = `${targetBasePath}.conditions[${tcIdx}]`;
|
||||
|
||||
let $row = $("<div>", { class: "panel col-sm-12 col-sx-12 target-condition-row" });
|
||||
|
||||
let $icon = $("<i>", { class: "fa-solid fa-crosshairs bckg-icon-3-line" });
|
||||
$row.append($icon);
|
||||
|
||||
let $inner = $("<div>", { class: "col-sm-11 col-sx-12" });
|
||||
|
||||
let $fieldDropdown = createEditableDropdown(
|
||||
`${basePath}.field`,
|
||||
getString("WF_Condition_field"),
|
||||
fieldOptions,
|
||||
tc.field || "",
|
||||
`wf-${wfIndex}-act-${actionIndex}-tc-${tcIdx}-field`
|
||||
);
|
||||
|
||||
let $operatorDropdown = createEditableDropdown(
|
||||
`${basePath}.operator`,
|
||||
getString("WF_Condition_operator"),
|
||||
operatorTypes,
|
||||
tc.operator || "equals",
|
||||
`wf-${wfIndex}-act-${actionIndex}-tc-${tcIdx}-operator`
|
||||
);
|
||||
|
||||
let $valueInput = createEditableInput(
|
||||
`${basePath}.value`,
|
||||
getString("WF_Condition_value"),
|
||||
tc.value || "",
|
||||
`wf-${wfIndex}-act-${actionIndex}-tc-${tcIdx}-value`,
|
||||
"condition-value-input"
|
||||
);
|
||||
|
||||
$inner.append($fieldDropdown).append($operatorDropdown).append($valueInput);
|
||||
|
||||
let $removeWrap = $("<div>", { class: "button-container col-sm-1 col-sx-12" });
|
||||
let $removeBtn = $("<div>", {
|
||||
class: "pointer red-hover-text remove-target-condition",
|
||||
wfIndex: wfIndex,
|
||||
actionIndex: actionIndex,
|
||||
tcIdx: tcIdx
|
||||
}).append($("<i>", { class: "fa-solid fa-trash" }));
|
||||
$removeWrap.append($removeBtn);
|
||||
|
||||
$row.append($inner).append($removeWrap);
|
||||
return $row;
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// Updating the in-memory workflow object
|
||||
function updateWorkflowObject(newValue, jsonPath) {
|
||||
@@ -1097,6 +1225,7 @@ function getEmptyWorkflowJson()
|
||||
// Save workflows JSON
|
||||
function saveWorkflows()
|
||||
{
|
||||
showSpinner();
|
||||
// encode for import
|
||||
appConfBase64 = btoa(JSON.stringify(getWorkflowsJson()))
|
||||
|
||||
@@ -1104,6 +1233,7 @@ function saveWorkflows()
|
||||
$.post('php/server/query_replace_config.php', { base64data: appConfBase64, fileName: "workflows.json" }, function(msg) {
|
||||
console.log(msg);
|
||||
// showMessage(msg);
|
||||
hideSpinner();
|
||||
write_notification(`[WF]: ${msg}`, 'interrupt');
|
||||
});
|
||||
}
|
||||
@@ -1168,6 +1298,42 @@ $(document).on("click", ".remove-action", function () {
|
||||
removeAction(getWorkflowsJson(), wfIndex, actionIndex);
|
||||
});
|
||||
|
||||
// Event Listeners for target condition rows (v2 cross-device targeting)
|
||||
$(document).on("click", ".add-target-condition", function () {
|
||||
let wfIndex = parseInt($(this).attr("wfIndex"), 10);
|
||||
let actionIndex = parseInt($(this).attr("actionIndex"), 10);
|
||||
let wfs = getWorkflowsJson();
|
||||
|
||||
if (!wfs[wfIndex].actions[actionIndex].target) {
|
||||
wfs[wfIndex].actions[actionIndex].target = { strategy: "query", conditions: [] };
|
||||
}
|
||||
if (!wfs[wfIndex].actions[actionIndex].target.conditions) {
|
||||
wfs[wfIndex].actions[actionIndex].target.conditions = [];
|
||||
}
|
||||
|
||||
wfs[wfIndex].actions[actionIndex].target.conditions.push({
|
||||
field: fieldOptions[0],
|
||||
operator: "equals",
|
||||
value: ""
|
||||
});
|
||||
|
||||
updateWorkflowsJson(wfs);
|
||||
renderWorkflows();
|
||||
});
|
||||
|
||||
$(document).on("click", ".remove-target-condition", function () {
|
||||
let wfIndex = parseInt($(this).attr("wfIndex"), 10);
|
||||
let actionIndex = parseInt($(this).attr("actionIndex"), 10);
|
||||
let tcIdx = parseInt($(this).attr("tcIdx"), 10);
|
||||
let wfs = getWorkflowsJson();
|
||||
|
||||
if (wfs[wfIndex].actions[actionIndex].target && wfs[wfIndex].actions[actionIndex].target.conditions) {
|
||||
wfs[wfIndex].actions[actionIndex].target.conditions.splice(tcIdx, 1);
|
||||
updateWorkflowsJson(wfs);
|
||||
renderWorkflows();
|
||||
}
|
||||
});
|
||||
|
||||
// Event Listeners for Removing Condition Groups
|
||||
$(document).on("click", ".remove-condition-group", function () {
|
||||
let wfIndex = $(this).attr("wfindex");
|
||||
|
||||
Reference in New Issue
Block a user