UNPKG

reportbro-designer

Version:

Designer to create pdf and excel report layouts. The reports can be generated with reportbro-lib (a Python package) on the server.

704 lines (655 loc) 28.6 kB
import AddDeleteParameterCmd from '../commands/AddDeleteParameterCmd'; import Command from '../commands/Command'; import SetValueCmd from '../commands/SetValueCmd'; import MainPanelItem from '../menu/MainPanelItem'; /** * Parameter data object. Contains all parameter settings including test data. * @class */ export default class Parameter { static dateRegex = /^(\d{4})-(\d{1,2})-(\d{1,2})( (\d{1,2}):(\d{2})(:(\d{2}))?)?$/; constructor(id, initialData, rb) { this.rb = rb; this.id = id; this.name = rb.getLabel('parameter'); this.panelItem = null; this.errors = []; this.type = Parameter.type.string; this.arrayItemType = Parameter.type.string; this.eval = !rb.getProperty('adminMode'); // if false value comes from database this.nullable = false; this.pattern = ''; this.expression = ''; this.testData = ''; this.testDataBoolean = false; this.testDataImage = ''; this.testDataImageFilename = ''; this.testDataRichText = ''; this.children = []; this.editable = rb.getProperty('adminMode'); this.showOnlyNameType = false; this.setInitialData(initialData); } setInitialData(initialData) { for (let key in initialData) { if (initialData.hasOwnProperty(key) && this.hasOwnProperty(key)) { this[key] = initialData[key]; } } if ('showOnlyNameType' in initialData && initialData['showOnlyNameType']) { this.editable = false; } } setHighlightUnused(highlightUnused) { if (highlightUnused) { document.getElementById(`rbro_menu_item${this.panelItem.getId()}`).classList.add('rbroUnusedParameter'); } else { document.getElementById(`rbro_menu_item${this.panelItem.getId()}`).classList.remove('rbroUnusedParameter'); } } /** * Called after initialization is finished. */ setup() { if (this.type === Parameter.type.array || this.type === Parameter.type.map) { const adminMode = this.rb.getProperty('adminMode'); for (let child of this.children) { let parameter = new Parameter(child.id || this.rb.getUniqueId(), child, this.rb); this.rb.addParameter(parameter); const showOnlyNameType = parameter.getValue('showOnlyNameType'); const showAddDelete = adminMode && !showOnlyNameType; // in case children and add/delete buttons exist: the visibility depends on parameter // type which can be modified (e.g. map and list have children and add button) and // is updated dynamically let panelItem = new MainPanelItem( 'parameter', this.panelItem, parameter, { hasChildren: !showOnlyNameType, showAdd: showAddDelete, showDelete: showAddDelete, draggable: true }, this.rb); parameter.setPanelItem(panelItem); this.panelItem.appendChild(panelItem); parameter.setup(); this.rb.notifyEvent(parameter, Command.operation.add); } } this.updateMenuItemDisplay(); } /** * Return true if parameter is a function with range input. */ isRangeFunction() { return this.type === Parameter.type.average || this.type === Parameter.type.sum; } /** * Returns all data fields of this object. The fields are used when serializing the object. * @returns {String[]} */ getFields() { const fields = this.getProperties(); fields.splice(0, 0, 'id'); return fields; } /** * Returns all fields of this object that can be modified in the properties panel. * @returns {String[]} */ getProperties() { return [ 'name', 'type', 'arrayItemType', 'eval', 'nullable', 'pattern', 'expression', 'showOnlyNameType', 'testData', 'testDataBoolean', 'testDataImage', 'testDataImageFilename', 'testDataRichText', ]; } getId() { return this.id; } /** * Returns highest id of this component including all its child components. * @returns {Number} */ getMaxId() { let maxId = this.id; if (this.type === Parameter.type.array || this.type === Parameter.type.map) { const children = this.children.slice(); let i = 0; while ( i < children.length) { const child = children[i]; if (child.id > maxId) { maxId = child.id; } if (child.type === Parameter.type.array || child.type === Parameter.type.map) { children.push(...child.children); } i++; } } return maxId; } getName() { return this.name; } getPanelItem() { return this.panelItem; } setPanelItem(panelItem) { this.panelItem = panelItem; } getValue(field) { return this[field]; } setValue(field, value) { this[field] = value; if (field === 'type') { this.updateMenuItemDisplay(); } } /** * Returns value to use for updating input control. * Can be overridden in case update value can be different from internal value, e.g. * width for table cells with colspan > 1. * @param {String} field - field name. * @param {String} value - value for update. */ getUpdateValue(field, value) { return value; } /** * Updates visibility of menu panel item (buttons, children) for this parameter. * * Must be called initially and when parameter type changes. */ updateMenuItemDisplay() { // for parameters where only name and type are shown (showOnlyNameType == true) // there are no buttons for add / delete and toggle children (e.g. page_count, page_number) if (this.rb.getProperty('adminMode') && !this.showOnlyNameType) { if (this.type === Parameter.type.array || this.type === Parameter.type.map) { document.getElementById(`rbro_menu_item_add${this.getId()}`).removeAttribute('style'); document.getElementById(`rbro_menu_item_children${this.getId()}`).style.display = 'block'; document.getElementById(`rbro_menu_item_children_toggle${this.getId()}`).removeAttribute('style'); } else { document.getElementById(`rbro_menu_item_add${this.getId()}`).style.display = 'none'; document.getElementById(`rbro_menu_item_children${this.getId()}`).style.display = 'none'; document.getElementById(`rbro_menu_item_children_toggle${this.getId()}`).style.display = 'none'; } } } /** * Returns parent in case parameter is child of a map/array parameter. * @returns {?Parameter} parent parameter if available, null otherwise. */ getParent() { if (this.panelItem !== null && this.panelItem.getParent().getData() instanceof Parameter) { return this.panelItem.getParent().getData(); } return null; } addError(error) { this.errors.push(error); } clearErrors() { this.errors = []; } getErrors() { return this.errors; } remove() { } select() { } deselect() { } /** * Adds SetValue commands to command group parameter in case the specified parameter is used in any of * the object fields. * @param {Parameter} parameter - parameter which will be renamed. * @param {String} newParameterName - new name of the parameter. * @param {CommandGroupCmd} cmdGroup - possible SetValue commands will be added to this command group. */ addCommandsForChangedParameterName(parameter, newParameterName, cmdGroup) { this.addCommandForChangedParameterName(parameter, newParameterName, 'expression', cmdGroup); for (let child of this.getChildren()) { child.addCommandsForChangedParameterName(parameter, newParameterName, cmdGroup); } } /** * Adds SetValue command to command group parameter in case the specified parameter is used in the * specified object field. * @param {Parameter} parameter - parameter which will be renamed. * @param {String} newParameterName - new name of the parameter. * @param {String} field * @param {CommandGroupCmd} cmdGroup - possible SetValue command will be added to this command group. */ addCommandForChangedParameterName(parameter, newParameterName, field, cmdGroup) { let paramParent = parameter.getParent(); let paramRef; let newParamRef; if (paramParent !== null && paramParent.getValue('type') === Parameter.type.map) { paramRef = '${' + paramParent.getName() + '.' + parameter.getName() + '}'; newParamRef = '${' + paramParent.getName() + '.' + newParameterName + '}'; } else if (parameter.getValue('type') === Parameter.type.map) { paramRef = '${' + parameter.getName() + '.'; newParamRef = '${' + newParameterName + '.'; } else { paramRef = '${' + parameter.getName() + '}'; newParamRef = '${' + newParameterName + '}'; } if (this.getValue(field).indexOf(paramRef) !== -1) { let cmd = new SetValueCmd( this.id, field, this.getValue(field).replaceAll(paramRef, newParamRef), SetValueCmd.type.text, this.rb); cmdGroup.addCommand(cmd); } } /** * Update test data for arrays and maps. Adapt field names of list items so test data is still valid when a * parameter of a list item is renamed. * @param {String} newParameterName * @param {Parameter[]} parents * @param {CommandGroupCmd} cmdGroup - possible SetValue command will be added to this command group. */ addUpdateTestDataCmdForChangedParameterName(newParameterName, parents, cmdGroup) { const rootParent = (parents.length > 0) ? parents[0] : null; if (rootParent !== null && (rootParent.type === Parameter.type.array || rootParent.type === Parameter.type.map)) { // update test data of root parameter because test data is only set for root parameters try { const testData = rootParent.getTestData(true); this.renameTestDataParameter(testData, this.getName(), newParameterName, parents, 0); let updatedTestData = JSON.stringify(testData); if (this.testData !== updatedTestData) { let cmd = new SetValueCmd( rootParent.getId(), 'testData', updatedTestData, SetValueCmd.type.text, this.rb); cmdGroup.addCommand(cmd); } } catch (e) { } } } renameTestDataParameter(testData, oldParameterName, newParameterName, parents, parentLevel) { const nextParentLevel = parentLevel + 1; const hasNextParent = (nextParentLevel < parents.length); const compareName = hasNextParent ? parents[nextParentLevel].getName() : oldParameterName; for (const testDataRow of testData) { if (compareName in testDataRow) { if (hasNextParent) { this.renameTestDataParameter( testDataRow[compareName], oldParameterName, newParameterName, parents, nextParentLevel); } else { testDataRow[newParameterName] = testDataRow[compareName]; delete testDataRow[compareName]; } } } } /** * Adds AddDeleteParameterCmd to command group parameter in case the * parameter type was changed from/to array. The command will add/delete the internal * 'row_number' parameter which is available for array parameters. * @param {String} newParameterType - new type of the parameter. * @param {CommandGroupCmd} cmdGroup - possible AddDeleteParameterCmd command will * be added to this command group. */ addCommandsForChangedParameterType(newParameterType, cmdGroup) { if (this.type === Parameter.type.array || this.type === Parameter.type.simpleArray || this.type === Parameter.type.map || newParameterType === Parameter.type.array || newParameterType === Parameter.type.simpleArray || newParameterType === Parameter.type.map) { // clear test data if parameter type is changed from or to array / simpleArray / map, the test data // is saved in the same field but the test data format is different depending on the parameter type const cmd = new SetValueCmd(this.getId(), 'testData', '', SetValueCmd.type.text, this.rb); cmdGroup.addCommand(cmd); } if (this.type !== Parameter.type.array && newParameterType === Parameter.type.array) { let initialData = { name: 'row_number', type: Parameter.type.number, eval: false, editable: false, showOnlyNameType: true }; let cmd = new AddDeleteParameterCmd(true, initialData, this.rb.getUniqueId(), this.getId(), 0, this.rb); cmdGroup.addCommand(cmd); } else if (this.type === Parameter.type.array && newParameterType !== Parameter.type.array) { let children = this.getChildren(); for (let child of children) { if (child.getValue('name') === 'row_number' && !child.getValue('editable')) { let cmd = new AddDeleteParameterCmd( false, child.toJS(), child.getId(), this.getId(), child.getPanelItem().getSiblingPosition(), this.rb); cmdGroup.addCommand(cmd); break; } } } } toJS() { let ret = {}; for (let field of this.getFields()) { ret[field] = this.getValue(field); } if (this.type === Parameter.type.array || this.type === Parameter.type.map) { let children = []; for (let child of this.panelItem.getChildren()) { children.push(child.getData().toJS()); } ret.children = children; } return ret; } getChildren() { let children = []; if (this.type === Parameter.type.array || this.type === Parameter.type.map) { for (let child of this.panelItem.getChildren()) { children.push(child.getData()); } } return children; } /** * In case of map parameter all child parameters are appended, * for other parameter types the parameter itself is appended. * Parameters with type array are only added if explicitly specified * in allowedTypes parameter. Nested map parameters (map parameter inside map) are * also possible. * * Used for parameter popup window. * * @param {Object[]} parameters - list where parameter items will be appended to. * @param {?String[]} allowedTypes - specify allowed parameter types which will be * added to the parameter list. If not set all parameter types are allowed. * @param {?String} dataSourceName - data source name which will be set as prefix for inserted parameter name. */ appendParameterItems(parameters, allowedTypes, dataSourceName) { this.appendParameterItemsWithPrefix(parameters, allowedTypes, dataSourceName, ''); } appendParameterItemsWithPrefix(parameters, allowedTypes, dataSourceName, parameterPrefix) { if (this.type === Parameter.type.map) { const parametersToAppend = []; const nestedMapParameters = []; for (const child of this.getChildren()) { if (child.type === Parameter.type.map) { nestedMapParameters.push(child); } else if (child.isAllowed(allowedTypes)) { parametersToAppend.push(child); } } if (parametersToAppend.length > 0) { parameters.push({ separator: true, id: this.id, separatorClass: 'rbroParameterGroup', name: parameterPrefix + this.name }); for (const parameter of parametersToAppend) { const paramName = parameterPrefix + this.name + '.' + parameter.getName(); parameters.push({ name: paramName, nameLowerCase: paramName.toLowerCase(), id: parameter.getId(), description: '', dataSourceName: dataSourceName }); } } // append nested map parameters after other parameters of the map for (const nestedMapParameter of nestedMapParameters) { nestedMapParameter.appendParameterItemsWithPrefix( parameters, allowedTypes, dataSourceName, parameterPrefix + this.name + '.'); } } else if (this.isAllowed(allowedTypes)) { parameters.push({ name: parameterPrefix + this.name, nameLowerCase: this.name.toLowerCase(), id: this.id, description: '', dataSourceName: dataSourceName }); } } /** * Return true if parameter fulfills requirement of allowed types. * If parameter is an array it is only allowed if array type is contained in allowedTypes, * otherwise it is also allowed if allowedTypes is undefined/null. * @param {?String[]} allowedTypes - can be undefined/null or an array containing allowed parameter types. * @return {Boolean} */ isAllowed(allowedTypes) { if (this.type !== Parameter.type.array) { return !Array.isArray(allowedTypes) || allowedTypes.indexOf(this.type) !== -1; } else { return Array.isArray(allowedTypes) && allowedTypes.indexOf(this.type) !== -1; } } /** * Appends field parameters of array parameter. * * Used in parameter popup window for parameter expression. * * @param {Object[]} parameters - list where parameter items will be appended to. * @param {String[]} allowedTypes - specify allowed parameter types which will be * added to the parameter list. If not set all parameter types are allowed. * @param {Boolean} relative - if true then added parameters are relative * to this one. This means that only the parameter name itself will * be set for the added parameters and parent parameters will also be searched. * If false then the full name including name of parent parameter will be set. * This is used when a parameter is selected for a function, e.g. sum or average * of a list field. * @param {?String} dataSourceName - data source name which will be set as prefix for inserted parameter name. */ appendFieldParameterItems(parameters, allowedTypes, relative, dataSourceName) { if (this.type === Parameter.type.array) { let firstRowParam = true; for (let child of this.panelItem.getChildren()) { let parameter = child.getData(); if (!Array.isArray(allowedTypes) || allowedTypes.indexOf(parameter.getValue('type')) !== -1) { if (relative) { if (firstRowParam) { parameters.push({ separator: true, id: this.id, separatorClass: 'rbroParameterRowGroup', name: this.rb.getLabel('parameterRowParams') }); } let paramName = parameter.getName(); parameters.push({ name: paramName, nameLowerCase: paramName.toLowerCase(), id: parameter.getId(), description: '', dataSourceName: dataSourceName }); } else { let paramName = this.name + '.' + parameter.getName(); parameters.push({ name: paramName, nameLowerCase: paramName.toLowerCase(), id: parameter.getId(), description: '', dataSourceName: dataSourceName }); } firstRowParam = false; } } } if (relative) { let parent = this.getParent(); while (parent !== null) { if (parent.type === Parameter.type.array) { parent.appendFieldParameterItems(parameters, allowedTypes, relative, parent.name); break; } parent = parent.getParent(); } } } getParameterFields() { const fields = []; if (this.type === Parameter.type.array || this.type === Parameter.type.simpleArray || this.type === Parameter.type.map) { if (this.type === Parameter.type.simpleArray) { fields.push({ name: 'data', type: this.arrayItemType, parameter: this }); } else { for (let child of this.getChildren()) { if (!child.showOnlyNameType && !child.eval && !child.isRangeFunction()) { fields.push({ name: child.getName(), type: child.getValue('type'), parameter: child }); } } } } return fields; } /** * Returns test data of parameter as array or map. * The test data is sanitized, i.e. the data value types match the corresponding parameter types. * @param {Boolean} editFormat - if true the data will be returned in edit format (containing additional info), * i.e. the data is used in the popup window to edit test data. * @returns {?Object|Object[]} test data. Null in case parameter is not an array, simple array or map. */ getTestData(editFormat) { let testData = null; try { testData = JSON.parse(this.testData); } catch (e) { } if (this.type === Parameter.type.array || this.type === Parameter.type.simpleArray || this.type === Parameter.type.map) { if (testData) { return Parameter.getSanitizedTestData(this, testData, editFormat); } } return null; } /** * Returns test data of parameter as array or map. * The test data is sanitized, i.e. the data value types match the corresponding parameter types. * @returns {Object|Object[]} sanitized test data */ static getSanitizedTestData(parameter, testData, editFormat) { let rv; if (parameter.type === Parameter.type.map) { if (!testData || Object.getPrototypeOf(testData) !== Object.prototype) { testData = {}; } rv = Parameter.getSanitizedTestDataMap(parameter, testData, editFormat); } else if (parameter.type === Parameter.type.simpleArray) { rv = Parameter.getSanitizedTestDataSimpleArray(parameter, testData, editFormat); } else if (parameter.type === Parameter.type.array) { if (!Array.isArray(testData)) { testData = []; } rv = []; for (let testDataRow of testData) { if (!testDataRow || Object.getPrototypeOf(testDataRow) !== Object.prototype) { testDataRow = {}; } rv.push(Parameter.getSanitizedTestDataMap(parameter, testDataRow, editFormat)); } } return rv; } static getSanitizedTestDataMap(parameter, testData, editFormat) { const rv = {}; for (const field of parameter.getChildren()) { if (field.showOnlyNameType) { continue; } const value = (field.name in testData) ? testData[field.name] : null; if (field.type === Parameter.type.array || field.type === Parameter.type.map) { rv[field.name] = Parameter.getSanitizedTestData(field, value, editFormat); } else if (field.type === Parameter.type.simpleArray) { rv[field.name] = Parameter.getSanitizedTestDataSimpleArray(field, value, editFormat); } else { rv[field.name] = Parameter.getSanitizedTestDataValue(field.type, value, editFormat); } } return rv; } static getSanitizedTestDataSimpleArray(parameter, testData, editFormat) { let testDataRows = testData; if (!Array.isArray(testDataRows)) { testDataRows = []; } const arrayValues = []; for (let testDataRow of testDataRows) { if (Object.getPrototypeOf(testDataRow) === Object.prototype) { const val = Parameter.getSanitizedTestDataValue( parameter.arrayItemType, testDataRow['data'], editFormat); if (editFormat) { arrayValues.push({ data: val }); } else { arrayValues.push(val); } } } return arrayValues; } static getSanitizedTestDataValue(fieldType, testData, editFormat) { let rv = null; if (fieldType === Parameter.type.string) { if (typeof testData === 'string') { rv = testData; } } else if (fieldType === Parameter.type.number) { if (typeof testData === 'number') { rv = testData; } else if (typeof testData === 'string') { const num = Number(testData.replaceAll(',', '.')); if (!isNaN(num)) { rv = num; } } } else if (fieldType === Parameter.type.boolean) { if (typeof testData === 'boolean') { rv = testData; } else { rv = Boolean(testData); } } else if (fieldType === Parameter.type.date) { if (typeof testData === 'string') { // we allow dates in format "YYYY-MM-DD", "YYYY-MM-DD HH:MM" and "YYYY-MM-DD HH:MM:SS" for test data if (Parameter.dateRegex.test(testData)) { rv = testData; } } } else if (fieldType === Parameter.type.image) { if (!testData || Object.getPrototypeOf(testData) !== Object.prototype || !('data' in testData) || !('filename' in testData)) { if (editFormat) { rv = { data: '', filename: '' }; } else { rv = ''; } } else { if (editFormat) { rv = testData; } else { rv = testData.data; } } } else if (fieldType === Parameter.type.richText) { if (typeof testData === 'string') { rv = testData; } } return rv; } /** * Removes ids of possible child elements. * @param {Object} data - map containing parameter data. */ static removeIds(data) { if (data.children) { for (let child of data.children) { if ('id' in child) { delete child.id; } } } } /** * Returns class name. * This can be useful for introspection when the class names are mangled * due to the webpack uglification process. * @returns {string} */ getClassName() { return 'Parameter'; } } Parameter.type = { 'none': 'none', 'string': 'string', 'number': 'number', 'boolean': 'boolean', 'date': 'date', 'image': 'image', 'richText': 'rich_text', 'array': 'array', 'simpleArray': 'simple_array', 'map': 'map', 'sum': 'sum', 'average': 'average' };