diff --git a/src/components/renderer/file-upload.vue b/src/components/renderer/file-upload.vue index 78211762a..0863436b1 100644 --- a/src/components/renderer/file-upload.vue +++ b/src/components/renderer/file-upload.vue @@ -94,8 +94,6 @@ export default { (loop, removed) => this.listenRemovedLoop(loop, removed)); this.removeDefaultClasses(); - - this.checkIfInRecordList(); this.setPrefix(); if (this.$refs['uploader']) { @@ -288,15 +286,6 @@ export default { this.prefix = parent.loopContext + '.'; } }, - setFileUploadNameForChildren(children, prefix) { - children.forEach(child => { - if (_.get(child, '$options.name') === 'FileUpload') { - child.prefix = prefix; - } else if (_.get(child, '$children', []).length > 0) { - this.setFileUploadNameForChildren(child.$children, prefix); - } - }); - }, addFile(file) { if (this.disabled) { file.ignored = true; @@ -420,14 +409,6 @@ export default { : null; } }, - checkIfInRecordList() { - const parent = this.parentRecordList(this); - if (parent !== null) { - const recordList = parent; - const prefix = recordList.name + '.'; - this.setFileUploadNameForChildren(recordList.$children, prefix); - } - }, }, }; diff --git a/src/components/renderer/form-record-list.vue b/src/components/renderer/form-record-list.vue index c9179eace..c78679fda 100644 --- a/src/components/renderer/form-record-list.vue +++ b/src/components/renderer/form-record-list.vue @@ -86,6 +86,7 @@ debug-context="Record List Add" :key="Array.isArray(value) ? value.length : 0" :_parent="validationData" + @update="updateRowDataNamePrefix" /> diff --git a/tests/e2e/fixtures/record_list_fileupload_loops.json b/tests/e2e/fixtures/record_list_fileupload_loops.json new file mode 100644 index 000000000..b8efef92a --- /dev/null +++ b/tests/e2e/fixtures/record_list_fileupload_loops.json @@ -0,0 +1,1003 @@ +{ + "type": "screen_package", + "version": "2", + "screens": [ + { + "id": 2, + "screen_category_id": "1", + "title": "26955-1", + "description": "test", + "type": "FORM", + "config": [ + { + "name": "record_list_page", + "items": [ + { + "label": "Record List", + "config": { + "form": "1", + "icon": "fas fa-th-list", + "name": "form_record_list_1", + "label": "New Record List", + "fields": { + "jsonData": "[]", + "editIndex": null, + "dataSource": "provideData", + "optionsList": [], + "removeIndex": null, + "showJsonEditor": false, + "showOptionCard": false, + "showRemoveWarning": false + }, + "editable": true + }, + "component": "FormRecordList", + "inspector": [ + { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:\/^([a-zA-Z]([a-zA-Z0-9_]?)+\\.?)+(? Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####", + "validation": null + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + } + ], + "editor-control": "FormText", + "editor-component": "FormText" + }, + { + "label": "Submit Button", + "config": { + "icon": "fas fa-share-square", + "name": null, + "event": "submit", + "label": "New Submit", + "variant": "primary", + "fieldValue": null, + "defaultSubmit": true + }, + "component": "FormButton", + "inspector": [ + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, + { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information." + } + }, + { + "type": "FormMultiselect", + "field": "event", + "config": { + "label": "Type", + "helper": "Choose whether the button should submit the form", + "options": [ + { + "value": "submit", + "content": "Submit Button" + }, + { + "value": "script", + "content": "Regular Button" + } + ] + } + }, + { + "type": "FormInput", + "field": "fieldValue", + "config": { + "label": "Value", + "helper": "The value being submitted" + } + }, + { + "type": "FormMultiselect", + "field": "variant", + "config": { + "label": "Button Variant Style", + "helper": "The variant determines the appearance of the button", + "options": [ + { + "value": "primary", + "content": "Primary" + }, + { + "value": "secondary", + "content": "Secondary" + }, + { + "value": "success", + "content": "Success" + }, + { + "value": "danger", + "content": "Danger" + }, + { + "value": "warning", + "content": "Warning" + }, + { + "value": "info", + "content": "Info" + }, + { + "value": "light", + "content": "Light" + }, + { + "value": "dark", + "content": "Dark" + }, + { + "value": "link", + "content": "Link" + } + ] + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####", + "validation": null + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + } + ], + "editor-control": "FormSubmit", + "editor-component": "FormButton" + } + ] + }, + { + "name": "input_page", + "items": [ + { + "items": [ + { + "label": "Line Input", + "config": { + "icon": "far fa-square", + "name": "form_input_4", + "type": "text", + "label": "New Input", + "helper": null, + "readonly": false, + "dataFormat": "string", + "validation": [], + "placeholder": null + }, + "component": "FormInput", + "inspector": [ + { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:\/^(?:[A-Za-z])(?:[0-9A-Z_a-z])*(?:\\.[0-9A-Z_a-z]+)*$\/|required|not_in:null,break,case,catch,continue,debugger,default,delete,do,else,finally,for,function,if,in,instanceof,new,return,switch,this,throw,try,typeof,var,void,while,with,class,const,enum,export,extends,import,super" + } + }, + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, + { + "type": "FormMultiselect", + "field": "dataFormat", + "config": { + "name": "Data Type", + "label": "Data Type", + "helper": "The data type specifies what kind of data is stored in the variable.", + "options": [ + { + "value": "string", + "content": "Text" + }, + { + "value": "int", + "content": "Integer" + }, + { + "value": "currency", + "content": "Currency" + }, + { + "value": "percentage", + "content": "Percentage" + }, + { + "value": "float", + "content": "Decimal" + }, + { + "value": "datetime", + "content": "Datetime" + }, + { + "value": "date", + "content": "Date" + }, + { + "value": "password", + "content": "Password" + } + ], + "validation": "required" + } + }, + { + "type": { + "_Ctor": [], + "extends": { + "_Ctor": [], + "props": { + "name": { + "type": null + }, + "error": { + "type": null + }, + "label": { + "type": null + }, + "value": { + "type": null + }, + "helper": { + "type": null + }, + "options": { + "type": null + }, + "selectedControl": { + "type": null + } + }, + "mixins": [ + { + "props": { + "validation": { + "type": null + }, + "validationData": { + "type": null + }, + "validationField": { + "type": null + }, + "validationMessages": { + "type": null + } + }, + "watch": { + "validationData": { + "deep": true, + "user": true + } + }, + "methods": [], + "computed": [] + } + ], + "methods": [], + "computed": [], + "_compiled": true, + "components": { + "Multiselect": { + "name": "vue-multiselect", + "_Ctor": [], + "props": { + "name": { + "default": null + }, + "limit": { + "default": 99999 + }, + "loading": { + "default": false + }, + "disabled": { + "default": false + }, + "tabindex": { + "default": 0 + }, + "limitText": [], + "maxHeight": { + "default": 300 + }, + "showLabels": { + "default": true + }, + "selectLabel": { + "default": "Press enter to select" + }, + "deselectLabel": { + "default": "Press enter to remove" + }, + "openDirection": { + "default": null + }, + "selectedLabel": { + "default": "Selected" + }, + "showNoOptions": { + "default": true + }, + "showNoResults": { + "default": true + }, + "selectGroupLabel": { + "default": "Press enter to select group" + }, + "deselectGroupLabel": { + "default": "Press enter to deselect group" + } + }, + "mixins": [ + { + "props": { + "id": { + "default": null + }, + "max": { + "type": [ + null, + null + ], + "default": false + }, + "label": [], + "value": { + "type": null + }, + "options": { + "required": true + }, + "trackBy": [], + "multiple": { + "default": false + }, + "taggable": { + "default": false + }, + "blockKeys": [], + "allowEmpty": { + "default": true + }, + "groupLabel": [], + "resetAfter": { + "default": false + }, + "searchable": { + "default": true + }, + "customLabel": [], + "groupSelect": { + "default": false + }, + "groupValues": [], + "placeholder": { + "default": "Select option" + }, + "tagPosition": { + "default": "top" + }, + "hideSelected": { + "default": false + }, + "optionsLimit": { + "default": 1000 + }, + "clearOnSelect": { + "default": true + }, + "closeOnSelect": { + "default": true + }, + "internalSearch": { + "default": true + }, + "preselectFirst": { + "default": false + }, + "preserveSearch": { + "default": false + }, + "tagPlaceholder": { + "default": "Press enter to create a tag" + } + }, + "watch": [], + "methods": [], + "computed": [] + }, + { + "props": { + "showPointer": { + "default": true + }, + "optionHeight": { + "default": 40 + } + }, + "watch": [], + "methods": [], + "computed": [] + } + ], + "computed": [], + "_compiled": true, + "beforeCreate": [ + null + ], + "staticRenderFns": [] + } + }, + "inheritAttrs": false, + "staticRenderFns": [] + }, + "computed": [], + "_compiled": true, + "staticRenderFns": [] + }, + "field": "dataMask", + "config": { + "name": "Data Format", + "label": "Data Format", + "helper": "The data format for the selected type." + } + }, + { + "type": "ValidationSelect", + "field": "validation", + "config": { + "label": "Validation Rules", + "helper": "The validation rules needed for this field" + } + }, + { + "type": "FormInput", + "field": "placeholder", + "config": { + "label": "Placeholder Text", + "helper": "The placeholder is what is shown in the field when no value is provided yet" + } + }, + { + "type": "FormInput", + "field": "helper", + "config": { + "label": "Helper Text", + "helper": "Help text is meant to provide additional guidance on the field's value" + } + }, + { + "type": "FormCheckbox", + "field": "readonly", + "config": { + "label": "Read Only", + "helper": null + } + }, + { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [ + { + "value": "text-primary", + "content": "primary" + }, + { + "value": "text-secondary", + "content": "secondary" + }, + { + "value": "text-success", + "content": "success" + }, + { + "value": "text-danger", + "content": "danger" + }, + { + "value": "text-warning", + "content": "warning" + }, + { + "value": "text-info", + "content": "info" + }, + { + "value": "text-light", + "content": "light" + }, + { + "value": "text-dark", + "content": "dark" + } + ] + } + }, + { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [ + { + "value": "alert alert-primary", + "content": "primary" + }, + { + "value": "alert alert-secondary", + "content": "secondary" + }, + { + "value": "alert alert-success", + "content": "success" + }, + { + "value": "alert alert-danger", + "content": "danger" + }, + { + "value": "alert alert-warning", + "content": "warning" + }, + { + "value": "alert alert-info", + "content": "info" + }, + { + "value": "alert alert-light", + "content": "light" + }, + { + "value": "alert alert-dark", + "content": "dark" + } + ] + } + }, + { + "type": { + "_Ctor": [], + "props": { + "value": { + "type": null + }, + "helper": { + "type": null + } + }, + "watch": { + "value": { + "user": true, + "immediate": true + } + }, + "methods": [], + "_scopeId": "data-v-7c18055b", + "computed": { + "effectiveValue": [] + }, + "_compiled": true, + "components": { + "MonacoEditor": { + "name": "monaco-editor", + "_Ctor": [], + "props": { + "amdRequire": [] + }, + "extends": { + "name": "MonacoEditor", + "model": { + "event": "change" + }, + "props": { + "theme": { + "default": "vs" + }, + "value": { + "required": true + }, + "options": [], + "language": [], + "original": [], + "amdRequire": [], + "diffEditor": { + "default": false + } + }, + "watch": { + "options": { + "deep": true, + "user": true + } + }, + "methods": [] + }, + "methods": [] + } + }, + "staticRenderFns": [] + }, + "field": "defaultValue", + "config": { + "label": "Default Value", + "helper": "The default value is pre populated using the existing request data. This feature will allow you to modify the value displayed on screen load if needed." + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####", + "validation": null + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + } + ], + "editor-control": "FormInput", + "editor-component": "FormInput" + }, + { + "label": "File Upload", + "config": { + "icon": "fas fa-file-upload", + "name": "file_upload_2", + "label": "New File Upload" + }, + "component": "FileUpload", + "inspector": [ + { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:\/^(?:[A-Za-z])(?:[0-9A-Z_a-z])*(?:\\.[0-9A-Z_a-z]+)*$\/|required|not_in:null,break,case,catch,continue,debugger,default,delete,do,else,finally,for,function,if,in,instanceof,new,return,switch,this,throw,try,typeof,var,void,while,with,class,const,enum,export,extends,import,super" + } + }, + { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, + { + "type": "FormInput", + "field": "accept", + "config": { + "label": "File Accepted", + "helper": "application\/msword, image\/gif, image\/jpeg, application\/pdf, application\/vnd.ms-powerpoint, application\/vnd.ms-excel, text\/plain" + } + }, + { + "type": "RequiredCheckbox", + "field": "validation", + "config": { + "label": "Required", + "helper": "Prevent form from being submitted unless a file is uploaded" + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####", + "validation": null + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + } + ], + "editor-control": "FileUpload", + "editor-component": "FileUpload" + } + ], + "label": "Loop", + "config": { + "icon": "fas fa-redo", + "name": "loop_1", + "label": null, + "settings": { + "add": true, + "type": "new", + "times": "1", + "varname": "loop_1" + }, + "conditionalHide": null + }, + "component": "FormLoop", + "container": true, + "inspector": [ + { + "type": "LoopInspector", + "field": "settings", + "config": { + "label": null, + "helper": null + } + }, + { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, + { + "type": "FormInput", + "field": "customFormatter", + "config": { + "label": "Custom Format String", + "helper": "Use the Mask Pattern format
Date ##\/##\/####
SSN ###-##-####
Phone (###) ###-####", + "validation": null + } + }, + { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + } + ], + "editor-control": "Loop", + "editor-component": "Loop" + } + ] + } + ], + "computed": [], + "custom_css": null, + "created_at": "2021-12-28T13:16:33-06:00", + "updated_at": "2022-03-16T12:02:22-06:00", + "status": "ACTIVE", + "key": null, + "watchers": [], + "categories": [ + { + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2022-03-15T12:12:34-06:00", + "updated_at": "2022-03-15T12:12:34-06:00", + "pivot": { + "assignable_id": 2, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScreenCategory" + } + } + ] + } + ], + "screen_categories": [], + "scripts": [] +} \ No newline at end of file diff --git a/tests/e2e/specs/RecordList.spec.js b/tests/e2e/specs/RecordList.spec.js index 58cc6fba5..3666b832a 100644 --- a/tests/e2e/specs/RecordList.spec.js +++ b/tests/e2e/specs/RecordList.spec.js @@ -144,4 +144,82 @@ describe('Record list', () => { }); }); }); + + it('FileUpload in record lists within loops', () => { + cy.loadFromJson('record_list_fileupload_loops.json', 0); + cy.get('[data-cy=mode-preview]').click(); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=add-row]').click(); + + // Upload the first file. + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_4]').eq(0).clear().type('First'); + cy.route('POST', '/api/1.0/requests/1/files', JSON.stringify({ + message: 'The file was uploaded.', + fileUploadId: 1, + })); + cy.uploadFile('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [data-cy="screen-field-file_upload_2"] input[type=file]', 'avatar.jpeg', 'image/jpg'); + + // Upload a second file. + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [data-cy=loop-loop_1-add]').click(); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [name=form_input_4]').eq(1).clear().type('Second'); + cy.route('POST', '/api/1.0/requests/1/files', JSON.stringify({ + message: 'The file was uploaded.', + fileUploadId: 2, + })); + cy.uploadFile('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [data-cy="screen-field-file_upload_2"] input[type=file]', 'avatar.jpeg', 'image/jpg', 1); + cy.wait(1000); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [data-cy="screen-field-file_upload_2"]').eq(0).should('contain.text', 'avatar.jpeg'); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] [data-cy="screen-field-file_upload_2"]').eq(1).should('contain.text', 'avatar.jpeg'); + + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-add] button:contains(Ok)').click(); + + // Edit record and check the uploaded files are displayed. + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=edit-row]').click(); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [data-cy="screen-field-file_upload_2"]').eq(0).should('contain.text', 'avatar.jpeg'); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [data-cy="screen-field-file_upload_2"]').eq(1).should('contain.text', 'avatar.jpeg'); + + // Add a third file in edit modal. + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [data-cy=loop-loop_1-add]').click(); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [name=form_input_4]').eq(2).clear().type('Third'); + cy.route('POST', '/api/1.0/requests/1/files', JSON.stringify({ + message: 'The file was uploaded.', + fileUploadId: 3, + })); + cy.uploadFile('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [data-cy="screen-field-file_upload_2"] input[type=file]', 'avatar.jpeg', 'image/jpg', 2); + cy.wait(1000); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [data-cy="screen-field-file_upload_2"]').eq(2).should('contain.text', 'avatar.jpeg'); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] button:contains(Save)').click(); + + // Delete third file in edit modal. + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=edit-row]').click(); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] [data-cy=loop-loop_1-remove]').click(); + cy.on('window:confirm', () => true); + cy.wait(500); + cy.get('[data-cy=preview-content] [data-cy=screen-field-form_record_list_1] [data-cy=modal-edit] button:contains(Save)').click(); + + // Verify the data structure. + cy.get('#screen-builder-container').then((div) => { + const data = div[0].__vue__.previewData; + const record_row_id = data.form_record_list_1[0].row_id; + expect(data).to.eql({ + 'form_record_list_1': [ + { + 'loop_1': [ + { + 'form_input_4': 'First', + 'file_upload_2': 1, + }, + { + 'form_input_4': 'Second', + 'file_upload_2': 2, + }, + ], + 'row_id': record_row_id, + }, + ], + 'loop_1': [ + {}, + ], + }); + }); + }); }); diff --git a/tests/e2e/support/commands.js b/tests/e2e/support/commands.js index 0f426488b..a11642f3a 100644 --- a/tests/e2e/support/commands.js +++ b/tests/e2e/support/commands.js @@ -59,8 +59,8 @@ function getFixtureBlob(fileUrl, type) { * @param {String} fileUrl - The file url to upload * @param {String} type - content type of the uploaded file */ -Cypress.Commands.add('uploadFile', (selector, fileUrl, type = '') => { - return cy.get(selector).then(subject => { +Cypress.Commands.add('uploadFile', (selector, fileUrl, type = '', index = 0) => { + return cy.get(selector).eq(index).then(subject => { return getFixtureBlob(fileUrl, type).then(blob => { return cy.window().then(win => { const el = subject[0];