/*
* Copyright 2025 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Dialog to modify a repeater in a form
*/
Ext.define('Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog', {
singleton: true,
/**
* Open and show a dialog containing all the repeater entries in a form from a content displayed in a content grid,
* or from a grid repeater dialog
*/
showRepeaterDialog: function (title, parentGridId, recordId, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback)
{
var parentRecord = null;
var parentGrid = null;
if (parentGridId != '' && recordId != '')
{
parentGrid = Ext.getCmp(parentGridId);
if (parentGrid != '')
{
parentRecord = parentGrid.getStore().getById(recordId);
}
}
if (parentRecord == null)
{
Ametys.log.ErrorDialog.display({
title: "{{i18n plugin.cms:UITOOL_SEARCH_ERROR_TITLE}}",
text: "{{i18n plugin.cms:UITOOL_SEARCH_ERROR_REPEATER}}",
category: 'Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog'
});
return;
}
title = title || "{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_TITLE}}";
return this._openDialog(title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback);
},
/**
* @private
* Create and open a new dialog
* @param {String} title The title of the dialog
* @param {Ext.data.Model} parentRecord The record containing the repeater
* @param {String} contentId The id of the main content.
* @param {Objet} subcolumns The sub columns to edit
* @param {Object} repeaterCfg The repeater info 'min-size', 'max-size', 'initial-size', 'add-label', 'del-label', 'header-label
* @param {String} dataIndex The name of the repeater metadata
* @param {String} metadataPath The path to the repeater metadata. Used to get the repeater columns definition
* @param {String} contentGridId The id of the main grid containing the content.
* @param {Function} callback a callback function to invoke after the dialog is closed, can be null
* @return the opened dialog
*/
_openDialog: function (title, parentRecord, contentId, subcolumns, repeaterCfg, dataIndex, metadataPath, contentGridId, callback)
{
function _containsPath(array, path)
{
if (!array)
{
return false;
}
if (array.includes(path))
{
return true;
}
let index = path.lastIndexOf('/');
if (index > 0)
{
return _containsPath(array, path.substring(0, index));
}
return false;
}
Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._isModified = false;
Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries = {};
let superParentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(parentRecord);
let data = superParentRecord.get("mccSession1_repeater")._educationalPath; // mccSession1 is hardcoded, because the value would be the same for mccSession2
let educationalPath = data ? data[superParentRecord.getId()] : null;
let rootInfo = Ametys.plugins.cms.search.SearchGridHelper._getRootRecord(parentRecord);
let form = this._createFormEditionPanel({contentId: contentId, educationalPath: educationalPath}, parentRecord, metadataPath);
let hintMessage = this._createTopText(rootInfo.record, parentRecord, metadataPath, Ametys.tool.ToolsManager.getFocusedTool().getInitialConfig()['odf-skills-enabled'] !== 'false');
let items = [
hintMessage,
form
];
// FIXME disable conditions qui remontent plus haut que la racine du form
let disableFirstLevel = false;
if (repeaterCfg.disabled || rootInfo.record.get('notEditableData') === true || _containsPath(rootInfo.record.get('notEditableDataIndex'), rootInfo.parentPath))
{
form.setDisabled(true);
}
else if (parentRecord.get('__notEditable'))
{
disableFirstLevel = true;
}
let externalValuesRequired = [];
let conf = {};
conf[dataIndex] = {
"name": dataIndex,
"label": repeaterCfg["title"],
"plugin": null,
"widget": null,
"widget-params": repeaterCfg["widget-params"],
"can-not-write": disableFirstLevel,
"type": "repeater",
"header-label": repeaterCfg["header-label"],
"add-label": repeaterCfg["add-label"],
"del-label": repeaterCfg["del-label"],
"min-size": repeaterCfg["min-size"],
"max-size": repeaterCfg["max-size"],
"initial-size": repeaterCfg["initial-size"],
"elements": Object.fromEntries(subcolumns.filter(sc => !hintMessage.hideSkills || sc.name != 'skills').map(sc => this._convertColumnToWidget(sc, externalValuesRequired, parentRecord, disableFirstLevel)).map(j => [j.name, j]))
};
if (Ametys.tool.ToolsManager.getFocusedTool().getInitialConfig()['odf-skills-enabled'] === 'false')
{
// Remove skills column if skills management is disabled
delete conf.notes.elements.skills;
delete conf.notes.elements.label.width;
delete conf.notes.elements.label['widget-params'].width;
}
form.configure(conf);
externalValuesRequired = [...new Set(externalValuesRequired)];
let entries = structuredClone(parentRecord.getData()[dataIndex]);
let values = {
values: {
__externalDisableConditionsValues: Object.fromEntries(externalValuesRequired.map(path => ["__external_NotesFormRepeaterDialog_" + path, Ametys.form.Widget.getRelativeValue(path, {record: rootInfo.record, dataPath: metadataPath + "/fake"})]))
},
repeaters: [],
};
// If parent is not limited to a path, the path in the skills repeater is worthy... so we need to hide the entries that are not mathing the educational path
if (parentRecord.get("common") !== false)
{
let superParentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(parentRecord);
let data = superParentRecord.get("mccSession1_repeater")._educationalPath; // mccSession1 is hardcoded, because the value would be the same for mccSession2
let educationalPath = data ? data[superParentRecord.getId()] : null;
if (educationalPath) // When no educational path we are supposed to be in readonly mode... so let's ignore it
{
// Modify the entries to remove the entries that are not matching the educational path and store them to reintroduce them during the save time
// 1) Find the entries that are not matching the educational path
let toRemoveTemporarily = [];
let previousPosition = 0;
for (let entry of Object.keys(entries || {}))
{
previousPosition++;
if (entry.endsWith("/path") && entries[entry] != educationalPath)
{
toRemoveTemporarily.push(entry.substring(0, entry.length - "/path".length));
}
else if (entry.endsWith("/skills"))
{
// Remove useless values
delete entries[entry];
}
}
// 2) Remove the entries (and keep them to reintroduce them later)
let handledSize = [];
let regex = new RegExp("^_?(\\[[0-9]*\\]/skills)\\[([0-9]*)\\](.*)");
for (let entry of Object.keys(entries || {}))
{
for (let toRemove of toRemoveTemporarily)
{
if (entry.startsWith(toRemove + "/") || entry.startsWith("_" + toRemove + "/"))
{
if (regex.test(entry))
{
// Save the entry to reintroduce it later (during the save time)
Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1] || {};
Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1][RegExp.$2] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1][RegExp.$2] || {};
Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[RegExp.$1][RegExp.$2][RegExp.$3.substring(1)] = entries[entry];
// Delete the entry to do not see it while editiing
delete entries[entry];
}
}
if (!handledSize.includes(toRemove))
{
handledSize.push(toRemove);
let sizeKey = "_" + toRemove.substring(0, toRemove.lastIndexOf("[")) + "/size";
entries[sizeKey] = entries[sizeKey] - 1;
}
}
}
// 3) Renumber the entries according to the removed entries
for (let i = toRemoveTemporarily.length - 1; i >= 0; i--)
{
if (regex.test(toRemoveTemporarily[i]))
{
let notesPrefix = RegExp.$1;
let skillsIndex = parseInt(RegExp.$2);
for (let entry of Object.keys(entries || {}))
{
if (regex.test(entry) && notesPrefix == RegExp.$1 &&parseInt(RegExp.$2) > skillsIndex)
{
entries[(entry.startsWith("_") ? "_" : "") + notesPrefix + "[" + (parseInt(RegExp.$2) - 1) + "]" + RegExp.$3] = entries[entry];
delete entries[entry];
}
}
}
}
}
}
for (let entry of Object.keys(entries || {}))
{
if (entry.startsWith("_") && entry.endsWith("size"))
{
let parts = entry.split("/");
values.repeaters.push({
"prefix": parts.length > 2 ? dataIndex + entry.substring(1, entry.length - (parts[parts.length - 2].length + "/size".length)) : "",
"name": parts.length > 2 ? parts[parts.length - 2] : dataIndex,
"count": entries[entry] || (entry == "_size" ? repeaterCfg["initial-size"] : 0/* should be this repeater initialsize */) // null repeater is converted to 0 sized... as we want it to have the initial size
});
}
else if (entry.startsWith("["))
{
values.values[dataIndex + entry] = entries[entry];
}
else if (entry.startsWith("_"))
{
values.values["_" + dataIndex + entry.substring(1)] = entries[entry];
}
}
form.setValues(values);
var dialog = this._showDialog(
title,
items,
{type: 'vbox', align: 'stretch'},
function () {
this._ok(dialog, parentRecord, form, dataIndex, contentId, callback);
},
callback
);
dialog.setReadOnly = function() {
form.disable()
}
return dialog;
},
_createTopText(rootRecord, parentRecord, metadataPath, withSkills)
{
let rootsLabels = "";
let roots = (rootRecord.get("mccSession1_repeater")._educationalPathRootLabels || {})[rootRecord.getId()];
if (withSkills && roots)
{
rootsLabels = "<br/><br/>{{i18n PLUGINS_ODF_PILOTAGE_RIGHTS_MCCCOURSE_NOTES_MCCSESSIONS_EDUCATIONAL_PATH_MULTIPLE_GLOBAL_INTRO}}<ul style='margin-top: 0;'>" + roots.map(x => "<li><strong>" + x + "</strong></li>").join("") + "</ul>{{i18n PLUGINS_ODF_PILOTAGE_RIGHTS_MCCCOURSE_NOTES_MCCSESSIONS_EDUCATIONAL_PATH_MULTIPLE_GLOBAL_WARN}}";
}
let educationalPathLabel = rootRecord.get("mccSession1_repeater")._educationalPathLabel[rootRecord.getId()];
let sessionLabel = metadataPath.indexOf("mccSession1") != -1 ? "{{i18n PLUGINS_ODF_PILOTAGE_COURSE_MCC_SESSION1_LABEL}}" : "{{i18n PLUGINS_ODF_PILOTAGE_COURSE_MCC_SESSION2_LABEL}}";
return {
xtype: "component",
ui: 'tool-hintmessage',
hideSkills: withSkills && roots,
html: (withSkills ? "{{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES2_LABEL}} " : "{{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES_LABEL}} ")
+ "<strong>" + sessionLabel + " > {{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES_EVAL}} " + (parentRecord.store.indexOf(parentRecord) + 1) + (parentRecord.get("label") ? " - " + parentRecord.get("label") : "") + "</strong> "
+ "{{i18n plugin.odf-pilotage:PLUGINS_ODF_PILOTAGE_DIALOG_MCCCOURSE_NOTES_IN}} "
+ "<strong>" + educationalPathLabel + "</strong>"
+ rootsLabels
}
},
/**
* @private
*/
_convertColumnToWidget: function(column, externalValuesRequired, parentRecord, disableFirstLevel, rootPath = "")
{
let widget = {...column};
if (disableFirstLevel === true && widget.type != 'repeater')
{
widget['can-not-write'] = true;
}
if (column.columns)
{
widget.elements = Object.fromEntries(column.columns.map(sc => this._convertColumnToWidget(sc, externalValuesRequired, parentRecord, false, rootPath + "/" + sc.name)).map(j => [j.name, j]));
}
// We need to clone the disable condition since we will modify when calling this._transformConditions
widget.disableCondition = window.structuredClone(column.disableCondition);
externalValuesRequired.push(...this._transformConditions(widget.disableCondition, rootPath));
return widget;
},
/**
* @private
*/
_transformConditions: function(conditions, rootPath)
{
function _normalize(path)
{
let paths = path.substring(1).split("/");
let toRemove;
do
{
toRemove = [];
for (let p = 1; p < paths.length; p++)
{
if (paths[p] == ".." && paths[p-1] != "..")
{
toRemove.push(p-1);
toRemove.push(p);
break;
}
}
for (let i = toRemove.length - 1; i >= 0; i--)
{
paths.splice(toRemove[i], 1);
}
}
while (toRemove.length > 0);
return paths.join("/");
}
let externalValues = [];
if (conditions)
{
if (conditions.conditions)
{
for (let c of conditions.conditions)
{
externalValues.push(..._transformConditions(c, rootPath));
}
}
if (conditions.condition)
{
for (let c of conditions.condition)
{
// If algo was already done once (since we modify existing object)
if (c.id.startsWith("__external_NotesFormRepeaterDialog_"))
{
externalValues.push(c.id.substring("__external_NotesFormRepeaterDialog_".length));
}
else if (!c.id.startsWith("__external"))
{
let path = _normalize(rootPath + "/" + c.id);
if (path.startsWith("../"))
{
// Found a relative path, we cannot handle it => replace it by a "pseudo" external condition
c.id = "__external_NotesFormRepeaterDialog_" + path;
externalValues.push(path);
}
}
}
}
}
return externalValues;
},
/**
* @private Get the panel used for edit content
* @param {Object} widgetInfo The widget info
* @return {Ext.Panel} The form panel
*/
_createFormEditionPanel: function (widgetInfo, parentRecord, metadataPath)
{
let cfp = Ext.create('Ametys.form.ConfigurableFormPanel', {
cls: 'content-form-inner',
flex: 1,
listeners: {
'fieldchange': Ext.bind(this._onChange, this)
},
additionalWidgetsConf: {
contentInfo: widgetInfo
},
additionalWidgetsConfFromParams: {
contentType: 'contentType', // some widgets require the contentType configuration
editableSource: 'editableSource' // for richtext widgets we want to check the right on content
},
fieldNamePrefix: 'content.input.',
displayGroupsDescriptions: false
});
cfp.getRelativeField = function(fieldPath, field, silently)
{
// Is fieldPath outside the form?
let fieldDeepness = field.getName().replace(/[^/]/g,"").length;
let cursor = fieldPath;
while (cursor.startsWith("../"))
{
cursor = cursor.substring("../".length);
fieldDeepness--;
}
if (fieldDeepness > 0)
{
return Ametys.form.ConfigurableFormPanel.prototype.getRelativeField.call(this, fieldPath, field, silently);
}
else
{
return {
getValue: function() { return this.value; },
value: Ametys.form.Widget.getRelativeValue("../" + cursor, {record: parentRecord, dataPath: metadataPath}),
doAddListener: function() { /* Ignore the onRelativeFieldChange... it can not happens */ }
};
}
}
return cfp;
},
/**
* @private
* Listener when at least one change was detected
*/
_onChange: function()
{
Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._isModified = true;
},
/**
* @protected
* Display the dialog
*/
_showDialog: function(title, items, layout, okHandler, callback, additionnalButtons)
{
var buttons = additionnalButtons || [];
buttons.push({
reference: 'okButton',
text :"{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_OK}}",
handler: okHandler,
scope: this
}, {
text :'{{i18n plugin.cms:PLUGINS_CMS_UITOOL_SEARCH_REPEATER_MODAL_CANCEL}}',
handler: function () {
dialog.close();
},
scope: this
});
let dialog = Ext.create("Ametys.window.DialogBox", {
title: title,
cls: ['a-formrepeater', 'a-notes-form-repeater-dialog'],
closeAction : 'destroy',
width : Math.max(500, window.innerWidth * 0.7),
height : Math.max(490, window.innerHeight * 0.75),
layout: layout,
items: items,
defaultButton: 'okButton',
referenceHolder: true,
// Buttons
buttons : buttons,
listeners: {
'beforeclose': function() {
if (this._closeForced)
{
this._closeForced = false;
if (callback && !dialog.doNotCallCallback)
{
callback(null);
}
return;
}
else
{
this._askToDiscard(dialog);
return false; // do not close yet
}
},
scope: this
}
});
dialog.show();
return dialog;
},
/**
* @private
* Test if there is any modification and ask if it is ok to discard them.
* If it is ok, or no modification call the callback
* @param {Ext.Component} dialog The dialog box
*/
_askToDiscard: function(dialog)
{
let me = this;
function finish()
{
me._closeForced = true;
dialog.close();
}
if (this._anyModificationToDiscard(dialog)) // any modif ?
{
Ametys.Msg.confirm("{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_LABEL}}",
"{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_SAVEBAR_UNSAVE_CONFIRM_DESC}}",
function(answer) {
if (answer == 'yes') {
finish();
}
}
);
}
else
{
finish();
}
},
/**
* @protected
* Test if there is any modification in the dialog
* @param {Ext.Component} dialog The dialog box
* @return {Boolean} true if any modification
*/
_anyModificationToDiscard: function(dialog)
{
return Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._isModified;
},
/**
* @private
* Callback when validating the dialog
* @param {Ametys.window.DialogBox} dialog The current dialog
* @param {Ext.data.Model} parentRecord The parent record
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @param {String} dataIndex The repeater metadata name
* @param {String} contentId The main content id
* @param {Function} callback a callback function to invoke after the dialog is closed, can be null
*/
_ok: function (dialog, parentRecord, form, dataIndex, contentId, callback)
{
let originalEntriesValues = this._formToValues(form, dataIndex);
if (originalEntriesValues == null)
{
Ametys.log.ErrorDialog.display({
title: "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_REPEATER_VALUE_ERROR_TITLE}}",
text: "{{i18n PLUGINS_ODF_PILOTAGE_GRIDREPEATER_FORM_ERROR}}",
details: "The repeater has some invalid values",
category: this.self.getName()
});
return;
}
let entriesValues = originalEntriesValues.entries.map(t => { return {...t.values, "previous-position": t['previous-position']}; });
if (Ametys.tool.ToolsManager.getFocusedTool().getInitialConfig()['odf-skills-enabled'] !== 'false')
{
// Set educational path on the entries
let superParentRecord = Ametys.plugins.cms.search.SearchGridHelper._getParentRecord(parentRecord);
let data = superParentRecord.get("mccSession1_repeater")._educationalPath; // mccSession1 is hardcoded, because the value would be the same for mccSession2
let educationalPath = data ? data[superParentRecord.getId()] : null;
if (educationalPath)
{
for (let entry of entriesValues)
{
// Data is store twice, modifying the first
for (let k of Object.keys(entry.skills))
{
if (k.endsWith("/path"))
{
entry.skills[k] = educationalPath;
}
}
// Modyfing the second
for (let subentry of entry.skills_repeater.entries)
{
subentry.values.path = educationalPath;
}
}
}
// Reinsert the entries that were removed because they were not matching the educational path
for (let entry of Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries))
{
let originalIndex = parseInt(entry.substring(1, entry.indexOf("]")));
let matchingEntries = originalEntriesValues.entries.filter(e => e['previous-position'] == originalIndex);
if (matchingEntries.length == 0)
{
// Discarding the skill values associateed to other paths since the note entry has been removed
break;
}
let entryValue = matchingEntries[0].values;
let cursor = entryValue.skills._size;
entryValue.skills._size += Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry]).length;
for (let subentry of Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry]))
{
cursor++;
let values = {};
let previousPosition;
for (let attribute of Object.keys(Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry]))
{
if (attribute == "previous-position")
{
previousPosition = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry][attribute];
}
else
{
entryValue.skills["[" + cursor + "]/" + attribute] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry][attribute];
values[attribute] = Ametys.plugins.odfpilotage.widget.NotesFormRepeaterDialog._removedEntries[entry][subentry][attribute];
}
}
entryValue.skills_repeater.entries.push({ position: cursor, values: values, "previous-position": previousPosition });
// FIXME we should sort the subentries to avoid generating changes
}
}
}
var repeaterValues = parentRecord.getData()[dataIndex + "_repeater"];
repeaterValues.entries = [];
for (var index = 0; index < entriesValues.length; index++)
{
let notEditable = entriesValues[index].__notEditable;
delete entriesValues[index].__notEditable;
repeaterValues.entries[index] = {
position: index + 1,
values: entriesValues[index],
"previous-position": entriesValues[index]['previous-position']
};
delete entriesValues[index]['previous-position'];
if (notEditable)
{
repeaterValues.entries[index].notEditable = true;
}
}
if (callback)
{
callback(repeaterValues);
dialog.doNotCallCallback = true;
}
this._closeForced = true;
dialog.close();
},
/**
* @private
* Get the form values at the converted format
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @parĂ¹m {String} dataIndex The repeater metadata name
*/
_formToValues: function(form, dataIndex)
{
if (!form.isValid())
{
return null;
}
return this._convertRepeaterToValues(form.getValues(), form.fieldNamePrefix + dataIndex);
},
/**
* @private
*/
_convertRepeaterToValues: function(values, prefix)
{
let me = this;
let entriesValues = [];
let size = values["_" + prefix + "/size"];
for (let pos = 1; pos <= size; pos++)
{
let entryValues = {};
let subPrefix = prefix + "[" + pos + "]/";
// Direct subvalues (including composite)
Object.keys(values).filter(v => v.startsWith(subPrefix)).forEach(v => {
let subv = v.substring(subPrefix.length);
if (!subv.includes("["))
{
entryValues[subv] = values[v];
}
})
// Direct subrepeaters
function _getDataIndex(v) { return v.substring(("_" + subPrefix).length, v.length - "/size".length); }
Object.keys(values).filter(v => v.startsWith("_" + subPrefix) && v.endsWith("/size") && !_getDataIndex(v).includes("[")).forEach(v => {
let subv = v.substring(1, subPrefix.length + 1);
let localDataIndex = _getDataIndex(v);
entryValues[localDataIndex + "_repeater"] = me._convertRepeaterToValues(values, subv + localDataIndex);
entryValues[localDataIndex] = Object.fromEntries(Object.keys(values).filter(v => v.startsWith(subv + localDataIndex) || v.startsWith("_" + subv + localDataIndex)).map(v => [v.replace(subv + localDataIndex, '').replace('_/', '_'), values[v]]));
});
let entry = {
"position": pos,
"previous-position": parseInt(values["_" + subPrefix + "previous-position"]),
"values": entryValues
};
entriesValues.push(entry);
}
return {
"entries": entriesValues,
"type": "repeater",
// "header-label": "", // how to fill it? is it needed?
// "label": "" // how to fill it? is it needed?
};
}
});