mirror of https://github.com/MISP/MISP
chg: [workflow] Added an `id` in all module instead of relying on the label
parent
5b6f051749
commit
e265057d24
|
@ -70,7 +70,8 @@ class Module_misp_module extends WorkflowBaseActionModule
|
|||
$postData['filteredItems'] = !empty($filteredItems) ? $filteredItems : $rData;
|
||||
}
|
||||
|
||||
$postData['params'] = $this->getIndexedParamsWithValues($node);
|
||||
$indexedParams = $this->getParamsWithValues($node);
|
||||
$postData['params'] = Hash::combine($indexedParams, '{s}.id', '{s}.value');
|
||||
$params = $this->getParamsWithValues($node);
|
||||
$matchingData = [];
|
||||
foreach ($params as $param) {
|
||||
|
@ -93,9 +94,11 @@ class Module_misp_module extends WorkflowBaseActionModule
|
|||
// FIXME: We might want to align the module config with what's currently supported
|
||||
protected function translateParams($paramName, $moduleParam): array
|
||||
{
|
||||
$param = [];
|
||||
$param['label'] = $paramName;
|
||||
$param['placeholder'] = $moduleParam['value'] ?? '';
|
||||
$param = [
|
||||
'id' => Inflector::underscore($paramName),
|
||||
'label' => Inflector::humanize($paramName),
|
||||
'placeholder' => $moduleParam['value'] ?? '',
|
||||
];
|
||||
if ($moduleParam['type'] == 'hash_path') {
|
||||
$param['type'] = 'input';
|
||||
$param['_isHashPath'] = true;
|
||||
|
|
|
@ -32,35 +32,29 @@ class WorkflowBaseModule
|
|||
{
|
||||
}
|
||||
|
||||
protected function getParams($node): array
|
||||
protected function mergeNodeConfigIntoParameters($node): array
|
||||
{
|
||||
$indexedParam = [];
|
||||
$nodeParam = [];
|
||||
$nodeParamByID = [];
|
||||
foreach ($node['data']['params'] as $param) {
|
||||
$nodeParam[$param['label']] = $param;
|
||||
$nodeParamByID[$param['id']] = $param;
|
||||
}
|
||||
foreach ($this->params as $param) {
|
||||
$param['value'] = $nodeParam[$param['label']]['value'] ?? null;
|
||||
$indexedParam[$param['label']] = $param;
|
||||
$param['value'] = $nodeParamByID[$param['id']]['value'] ?? null;
|
||||
$indexedParam[$param['id']] = $param;
|
||||
}
|
||||
return $indexedParam;
|
||||
}
|
||||
|
||||
protected function getParamsWithValues($node): array
|
||||
{
|
||||
$indexedParams = $this->getParams($node);
|
||||
foreach ($indexedParams as $label => $param) {
|
||||
$indexedParams[$label]['value'] = $param['value'] ?? ($param['default'] ?? '');
|
||||
$indexedParams = $this->mergeNodeConfigIntoParameters($node);
|
||||
foreach ($indexedParams as $id => $param) {
|
||||
$indexedParams[$id]['value'] = $param['value'] ?? ($param['default'] ?? '');
|
||||
}
|
||||
return $indexedParams;
|
||||
}
|
||||
|
||||
protected function getIndexedParamsWithValues($node): array
|
||||
{
|
||||
$indexedParams = $this->getParamsWithValues($node);
|
||||
return Hash::combine($indexedParams, '{s}.label', '{s}.value');
|
||||
}
|
||||
|
||||
protected function getFilters($node): array
|
||||
{
|
||||
$indexedFilters = [];
|
||||
|
|
|
@ -29,8 +29,9 @@ class Module_enrich_event extends WorkflowBaseActionModule
|
|||
}
|
||||
$this->params = [
|
||||
[
|
||||
'type' => 'select',
|
||||
'id' => 'modules',
|
||||
'label' => 'Modules',
|
||||
'type' => 'select',
|
||||
'options' => $moduleOptions,
|
||||
],
|
||||
];
|
||||
|
@ -40,7 +41,7 @@ class Module_enrich_event extends WorkflowBaseActionModule
|
|||
{
|
||||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
if (empty($params['Modules']['value'])) {
|
||||
if (empty($params['modules']['value'])) {
|
||||
$errors[] = __('No enrichmnent module selected');
|
||||
return false;
|
||||
}
|
||||
|
@ -49,7 +50,7 @@ class Module_enrich_event extends WorkflowBaseActionModule
|
|||
$options = [
|
||||
'user' => $roamingData->getUser(),
|
||||
'event_id' => $event_id,
|
||||
'modules' => [$params['Modules']['value']]
|
||||
'modules' => [$params['modules']['value']]
|
||||
];
|
||||
$filters = $this->getFilters($node);
|
||||
$extracted = $this->extractData($rData, $filters['selector']);
|
||||
|
|
|
@ -17,20 +17,9 @@ class Module_push_zmq extends WorkflowBaseActionModule
|
|||
parent::__construct();
|
||||
$this->params = [
|
||||
[
|
||||
'type' => 'input',
|
||||
'label' => 'Namespace',
|
||||
'default' => '',
|
||||
'placeholder' => __('A namespace in the ZMQ topic')
|
||||
],
|
||||
[
|
||||
'type' => 'input',
|
||||
'label' => 'Content',
|
||||
'default' => '',
|
||||
'placeholder' => __('Whatever text to be published')
|
||||
],
|
||||
[
|
||||
'type' => 'input',
|
||||
'id' => 'match_condition',
|
||||
'label' => 'Match Condition',
|
||||
'type' => 'input',
|
||||
'default' => '',
|
||||
'placeholder' => 'Attribute.{n}.AttributeTag.{n}.Tag.name',
|
||||
],
|
||||
|
@ -41,7 +30,7 @@ class Module_push_zmq extends WorkflowBaseActionModule
|
|||
{
|
||||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
$path = $params['Match Condition']['value'];
|
||||
$path = $params['match_condition']['value'];
|
||||
$data = $roamingData->getData();
|
||||
$extracted = $this->extractData($data, $path);
|
||||
if ($extracted === false) {
|
||||
|
|
|
@ -23,22 +23,25 @@ class Module_webhook extends WorkflowBaseActionModule
|
|||
parent::__construct();
|
||||
$this->params = [
|
||||
[
|
||||
'type' => 'input',
|
||||
'id' => 'url',
|
||||
'label' => 'Payload URL',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'https://example.com/test',
|
||||
],
|
||||
[
|
||||
'type' => 'select',
|
||||
'id' => 'content_type',
|
||||
'label' => 'Content type',
|
||||
'default' => 'form',
|
||||
'type' => 'select',
|
||||
'default' => 'json',
|
||||
'options' => [
|
||||
'json' => 'application/json',
|
||||
'form' => 'application/x-www-form-urlencoded',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'input',
|
||||
'id' => 'data_extraction_path',
|
||||
'label' => 'Data extraction path',
|
||||
'type' => 'input',
|
||||
'default' => '',
|
||||
'placeholder' => 'Attribute.{n}.AttributeTag.{n}.Tag.name',
|
||||
],
|
||||
|
@ -49,16 +52,16 @@ class Module_webhook extends WorkflowBaseActionModule
|
|||
{
|
||||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
if (empty($params['Payload URL']['value'])) {
|
||||
if (empty($params['url']['value'])) {
|
||||
$errors[] = __('URL not provided.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$rData = $roamingData->getData();
|
||||
$path = $params['Data extraction path']['value'];
|
||||
$extracted = !empty($params['Data extraction path']['value']) ? $this->extractData($rData, $path) : $rData;
|
||||
$path = $params['data_extraction_path']['value'];
|
||||
$extracted = !empty($params['data_extraction_path']['value']) ? $this->extractData($rData, $path) : $rData;
|
||||
try {
|
||||
$response = $this->doRequest($params['Payload URL']['value'], $params['Content type']['value'], $extracted);
|
||||
$response = $this->doRequest($params['url']['value'], $params['content_type']['value'], $extracted);
|
||||
if ($response->isOk()) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class Module_concurrent_task extends WorkflowBaseLogicModule
|
|||
Job::WORKER_PRIO,
|
||||
'workflowParallelTask',
|
||||
sprintf('Workflow ID: %s', $roamingData->getWorkflow()['Workflow']['id']),
|
||||
'Running workflow parallel tasks.'
|
||||
__('Running workflow parallel tasks.')
|
||||
);
|
||||
$this->Job->getBackgroundJobsTool()->enqueue(
|
||||
BackgroundJobsTool::PRIO_QUEUE,
|
||||
|
@ -48,7 +48,7 @@ class Module_concurrent_task extends WorkflowBaseLogicModule
|
|||
$roamingData->getWorkflow()['Workflow']['id'],
|
||||
$node_id_to_exec,
|
||||
JsonTool::encode($roamingData->getData()),
|
||||
Workflow::NON_BLOCKING_PATH,
|
||||
$this->Workflow::NON_BLOCKING_PATH,
|
||||
$jobId
|
||||
],
|
||||
true,
|
||||
|
|
|
@ -34,6 +34,7 @@ class Module_distribution_if extends WorkflowBaseLogicModule
|
|||
}
|
||||
$this->params = [
|
||||
[
|
||||
'id' => 'scope',
|
||||
'label' => 'Scope',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
|
@ -43,12 +44,14 @@ class Module_distribution_if extends WorkflowBaseLogicModule
|
|||
'default' => 'attribute',
|
||||
],
|
||||
[
|
||||
'id' => 'condition',
|
||||
'label' => 'Condition',
|
||||
'type' => 'select',
|
||||
'default' => 'equals',
|
||||
'options' => $this->operators,
|
||||
],
|
||||
[
|
||||
'id' => 'distribution',
|
||||
'label' => 'Distribution',
|
||||
'type' => 'select',
|
||||
'default' => '0',
|
||||
|
@ -63,9 +66,9 @@ class Module_distribution_if extends WorkflowBaseLogicModule
|
|||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
|
||||
$scope = $params['Scope']['value'];
|
||||
$operator = $params['Condition']['value'];
|
||||
$value = $params['Distribution']['value'];
|
||||
$scope = $params['scope']['value'];
|
||||
$operator = $params['condition']['value'];
|
||||
$value = $params['distribution']['value'];
|
||||
$data = $roamingData->getData();
|
||||
$final_distribution = $this->__getPropagatedDistribution($data['Event']);
|
||||
if ($scope == 'attribute') {
|
||||
|
|
|
@ -24,19 +24,22 @@ class Module_generic_if extends WorkflowBaseLogicModule
|
|||
parent::__construct();
|
||||
$this->params = [
|
||||
[
|
||||
'type' => 'input',
|
||||
'id' => 'value',
|
||||
'label' => 'Value',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'tlp:red',
|
||||
],
|
||||
[
|
||||
'type' => 'select',
|
||||
'id' => 'operator',
|
||||
'label' => 'Operator',
|
||||
'type' => 'select',
|
||||
'default' => 'in',
|
||||
'options' => $this->operators,
|
||||
],
|
||||
[
|
||||
'type' => 'input',
|
||||
'id' => 'hash_path',
|
||||
'label' => 'Hash path',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'Attribute.{n}.Tag',
|
||||
],
|
||||
];
|
||||
|
@ -46,9 +49,9 @@ class Module_generic_if extends WorkflowBaseLogicModule
|
|||
{
|
||||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
$path = $params['Hash path']['value'];
|
||||
$operator = $params['Operator']['value'];
|
||||
$value = $params['Value']['value'];
|
||||
$path = $params['hash_path']['value'];
|
||||
$operator = $params['operator']['value'];
|
||||
$value = $params['value']['value'];
|
||||
$data = $roamingData->getData();
|
||||
$extracted = [];
|
||||
if ($operator == 'equals' || $operator == 'not_equals') {
|
||||
|
|
|
@ -29,24 +29,26 @@ class Module_organisation_if extends WorkflowBaseLogicModule
|
|||
]);
|
||||
$this->params = [
|
||||
[
|
||||
'id' => 'org_type',
|
||||
'label' => 'Organisation Type',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'org' => __('Owner Organisation'),
|
||||
'orgc' => __('Creator Organisation'),
|
||||
],
|
||||
'default' => 'orgc',
|
||||
'label' => 'Organisation Type',
|
||||
],
|
||||
[
|
||||
'type' => 'select',
|
||||
'id' => 'condition',
|
||||
'label' => 'Condition',
|
||||
'type' => 'select',
|
||||
'default' => 'equals',
|
||||
'options' => $this->operators,
|
||||
],
|
||||
[
|
||||
'id' => 'org_id',
|
||||
'type' => 'picker',
|
||||
'multiple' => false,
|
||||
'label' => 'Organisation',
|
||||
'options' => $orgs,
|
||||
'default' => 1,
|
||||
'placeholder' => __('Pick an organisation'),
|
||||
|
@ -59,9 +61,9 @@ class Module_organisation_if extends WorkflowBaseLogicModule
|
|||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
|
||||
$org_type = $params['Organisation Type']['value'];
|
||||
$operator = $params['Condition']['value'];
|
||||
$org_id = $params['Organisation']['value'];
|
||||
$org_type = $params['org_type']['value'];
|
||||
$operator = $params['condition']['value'];
|
||||
$org_id = $params['org_id']['value'];
|
||||
$data = $roamingData->getData();
|
||||
$path = 'Event.org_id';
|
||||
if ($org_type == 'orgc') {
|
||||
|
|
|
@ -23,8 +23,9 @@ class Module_published_if extends WorkflowBaseLogicModule
|
|||
parent::__construct();
|
||||
$this->params = [
|
||||
[
|
||||
'type' => 'select',
|
||||
'id' => 'condition',
|
||||
'label' => 'Condition',
|
||||
'type' => 'select',
|
||||
'default' => 'equals',
|
||||
'options' => $this->operators,
|
||||
],
|
||||
|
@ -36,7 +37,7 @@ class Module_published_if extends WorkflowBaseLogicModule
|
|||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
|
||||
$operator = $params['Condition']['value'];
|
||||
$operator = $params['condition']['value'];
|
||||
$data = $roamingData->getData();
|
||||
$path = 'Event.published';
|
||||
$is_published = !empty(Hash::get($data, $path));
|
||||
|
|
|
@ -37,6 +37,8 @@ class Module_tag_if extends WorkflowBaseLogicModule
|
|||
$tags = array_column(array_column($tags, 'Tag'), 'name', 'id');
|
||||
$this->params = [
|
||||
[
|
||||
'id' => 'scope',
|
||||
'label' => 'Scope',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'event' => __('Event'),
|
||||
|
@ -44,18 +46,19 @@ class Module_tag_if extends WorkflowBaseLogicModule
|
|||
'event_attribute' => __('Inherited Attribute'),
|
||||
],
|
||||
'default' => 'event',
|
||||
'label' => 'Scope',
|
||||
],
|
||||
[
|
||||
'type' => 'select',
|
||||
'id' => 'condition',
|
||||
'label' => 'Condition',
|
||||
'type' => 'select',
|
||||
'default' => 'in_or',
|
||||
'options' => $this->operators,
|
||||
],
|
||||
[
|
||||
'id' => 'tags',
|
||||
'label' => 'Tags',
|
||||
'type' => 'picker',
|
||||
'multiple' => true,
|
||||
'label' => 'Tags',
|
||||
'options' => $tags,
|
||||
'placeholder' => __('Pick a tag'),
|
||||
],
|
||||
|
@ -67,9 +70,9 @@ class Module_tag_if extends WorkflowBaseLogicModule
|
|||
parent::exec($node, $roamingData, $errors);
|
||||
$params = $this->getParamsWithValues($node);
|
||||
|
||||
$value = $params['Tags']['value'];
|
||||
$operator = $params['Condition']['value'];
|
||||
$scope = $params['Scope']['value'];
|
||||
$value = $params['tags']['value'];
|
||||
$operator = $params['condition']['value'];
|
||||
$scope = $params['scope']['value'];
|
||||
$data = $roamingData->getData();
|
||||
$extracted = $this->__getTagFromScope($scope, $data);
|
||||
$eval = $this->evaluateCondition($extracted, $operator, $value);
|
||||
|
|
|
@ -690,7 +690,7 @@ function loadWorkflow(workflow) {
|
|||
console.error('Tried to add node for unknown module ' + node.data.id + ' (' + node.id + ')')
|
||||
var userFriendlyParams = {}
|
||||
node.data.params.forEach(function (param) {
|
||||
userFriendlyParams[param.label] = (param.value ?? param.default)
|
||||
userFriendlyParams[param.id] = (param.value ?? param.default)
|
||||
})
|
||||
var html = window['dotBlock_error']({
|
||||
error: 'Invalid module id`' + node.data.id + '` (' + node.id + ')',
|
||||
|
@ -840,7 +840,7 @@ function addNodesFromWorkflowBlueprint(workflowBlueprint, cursorPosition) {
|
|||
if (all_modules_by_id[node.data.id] === undefined) {
|
||||
var userFriendlyParams = {}
|
||||
node.data.params.forEach(function (param) {
|
||||
userFriendlyParams[param.label] = (param.value ?? param.default)
|
||||
userFriendlyParams[param.id] = (param.value ?? param.default)
|
||||
})
|
||||
var errorMessage = 'Invalid ' + node.data.module_type + ' module id `' + node.data.id + '` (' + node.id + ')'
|
||||
var html = window['dotBlock_error']({
|
||||
|
@ -911,29 +911,35 @@ function getCanvasCentroid() {
|
|||
}
|
||||
|
||||
function mergeNodeAndModuleParams(node, moduleParams) {
|
||||
var moduleParamsByFormattedName = {}
|
||||
var nodeParamsByFormattedName = {}
|
||||
moduleParams.forEach(function (param) {
|
||||
moduleParamsByFormattedName[param.label.toLowerCase().replace(' ', '-')] = param
|
||||
var moduleParamsById = {}
|
||||
var nodeParamsById = {}
|
||||
moduleParams.forEach(function (param, i) {
|
||||
if (param.id === undefined) { // Param id is not set in the module definition.
|
||||
param.id = 'param-' + i
|
||||
param.no_id = true
|
||||
}
|
||||
moduleParamsById[param.id] = param
|
||||
})
|
||||
node.data.params.forEach(function (param) {
|
||||
nodeParamsByFormattedName[param.label.toLowerCase().replace(' ', '-')] = param
|
||||
node.data.params.forEach(function (param, i) {
|
||||
if (param.id === undefined) { // Param id is not set in the module definition.
|
||||
param.id = 'param-' + i
|
||||
}
|
||||
nodeParamsById[param.id] = param
|
||||
})
|
||||
var finalParams = {}
|
||||
var nodeAndModuleParams = node.data.params.concat(moduleParams)
|
||||
nodeAndModuleParams.forEach(function (param) {
|
||||
var formattedName = param.label.toLowerCase().replace(' ', '-')
|
||||
if (finalParams[formattedName]) { // param has already been processed
|
||||
if (finalParams[param.id]) { // param has already been processed
|
||||
return;
|
||||
}
|
||||
if (moduleParamsByFormattedName[formattedName] === undefined) { // Param do not exist in the module (anymore or never did)
|
||||
if (moduleParamsById[param.id] === undefined) { // Param do not exist in the module (anymore or never did)
|
||||
param.is_invalid = true
|
||||
}
|
||||
if (!param['param_id']) {
|
||||
param['param_id'] = getIDForNodeParameter(node, param)
|
||||
}
|
||||
finalParam = Object.assign({}, nodeParamsByFormattedName[formattedName], moduleParamsByFormattedName[formattedName])
|
||||
finalParams[formattedName] = finalParam
|
||||
finalParam = Object.assign({}, nodeParamsById[param.id], moduleParamsById[param.id])
|
||||
finalParams[param.id] = finalParam
|
||||
})
|
||||
return Object.values(finalParams)
|
||||
}
|
||||
|
@ -1212,14 +1218,31 @@ function afterModalShowCallback() {
|
|||
}
|
||||
|
||||
function genParameterWarning(options) {
|
||||
return options.is_invalid ?
|
||||
$('<span>').addClass('text-error').css('margin-left', '5px')
|
||||
// return options.is_invalid ?
|
||||
// $('<span>').addClass('text-error').css('margin-left', '5px')
|
||||
// .append(
|
||||
// $('<i>').addClass('fas fa-exclamation-triangle'),
|
||||
// $('<span>').text('Invalid parameter')
|
||||
// )
|
||||
// .attr('title', 'This parameter does not exist in the associated module and thus will be removed upon saving. Make sure you have the latest version of this module.') :
|
||||
// ''
|
||||
var text = '', text_short = ''
|
||||
if (options.is_invalid) {
|
||||
text = 'This parameter does not exist in the associated module and thus will be removed upon saving. Make sure you have the latest version of this module.'
|
||||
text_short = 'Invalid parameter'
|
||||
} else if (options.no_id) {
|
||||
text = 'This parameter does not have an ID in the associated module and thus will be ignored. Make sure you have the latest version of this module.'
|
||||
text_short = 'parameter has no ID'
|
||||
}
|
||||
if (text || text_short) {
|
||||
return $('<span>').addClass('text-error').css('margin-left', '5px')
|
||||
.append(
|
||||
$('<i>').addClass('fas fa-exclamation-triangle'),
|
||||
$('<span>').text('Invalid parameter')
|
||||
$('<span>').text(text_short)
|
||||
)
|
||||
.attr('title', 'This parameter does not exist in the associated module and thus will be removed upon saving. Make sure you have the latest version of this module.') :
|
||||
''
|
||||
.attr('title', text)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function genSelect(options, forNode = true) {
|
||||
|
@ -1464,7 +1487,7 @@ function getIDForNodeParameter(node, param) {
|
|||
if (param.id !== undefined) {
|
||||
return param.id + '-' + node.node_uid
|
||||
}
|
||||
return param.label.toLowerCase().replace(' ', '-') + '-' + node.node_uid
|
||||
return param.id + '-' + node.node_uid
|
||||
}
|
||||
|
||||
function getNodeFromNodeInput($input) {
|
||||
|
@ -1621,10 +1644,10 @@ function genGenericBlockFilter(block) {
|
|||
]
|
||||
var filters = getFiltersFromNode(block)
|
||||
var $div = $('<div></div>').append($('<form></form>').append(
|
||||
genGenericInput({ id: 'filtering-selector', label: 'Element selector', type: 'text', placeholder: 'Attribute.{n}', required: false, value: filters.selector}),
|
||||
genGenericInput({ id: 'filtering-value', label: 'Value', type: 'text', placeholder: 'tlp:white', required: false, value: filters.value}),
|
||||
genGenericSelect({ id: 'filtering-operator', label: 'Operator', options: operatorOptions, value: filters.operator}),
|
||||
genGenericInput({ id: 'filtering-path', label: 'Hash Path', type: 'text', placeholder: 'AttributeTag.{n}.Tag.name', required: false, value: filters.path}),
|
||||
genGenericInput({ id: 'filtering-selector', id: 'element_selector', label: 'Element selector', type: 'text', placeholder: 'Attribute.{n}', required: false, value: filters.selector}),
|
||||
genGenericInput({ id: 'filtering-value', id: 'value', label: 'Value', type: 'text', placeholder: 'tlp:white', required: false, value: filters.value}),
|
||||
genGenericSelect({ id: 'filtering-operator', id: 'operator', label: 'Operator', options: operatorOptions, value: filters.operator}),
|
||||
genGenericInput({ id: 'filtering-path', id: 'hash_path', label: 'Hash Path', type: 'text', placeholder: 'AttributeTag.{n}.Tag.name', required: false, value: filters.path}),
|
||||
))
|
||||
return $div[0].outerHTML
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue