From a25ba1052a4a0a235ba8110f2af9e19113c2c2f5 Mon Sep 17 00:00:00 2001 From: Brendan Bond Date: Tue, 10 Sep 2024 09:01:32 -0500 Subject: [PATCH] FIO-8796: Add DynamicWizard component (#47) * remove dist and lib from committed files; update webpack config to include @formio/js as an external dependency; add skipLibCheck for now...; update deps; 5.x updates for resource component * add DynamicWizard component * add DynamicWizard component * remove lockfile * add dynamic wizard css --- .mocharc.json | 4 + package.json | 7 +- register.js | 12 + src/Wizard.ts | 124 +++ .../DynamicWizard/DynamicWizard.form.ts | 34 + .../DynamicWizard/DynamicWizard.spec.ts | 569 ++++++++++ src/components/DynamicWizard/DynamicWizard.ts | 973 ++++++++++++++++++ .../editForm/DynamicWizard.edit.data.ts | 18 + .../editForm/DynamicWizard.edit.display.ts | 76 ++ .../editForm/DynamicWizard.edit.templates.ts | 57 + .../editForm/DynamicWizard.edit.validation.ts | 32 + .../fixtures/dynamicWizardInsideWizard.ts | 61 ++ .../dynamicWizardWithMinMaxValidation.ts | 148 +++ .../DynamicWizard/fixtures/nestedWizard.ts | 149 +++ .../fixtures/simpleDynamicWizardChild.ts | 152 +++ .../fixtures/webformWithNestedWizard.ts | 136 +++ .../fixtures/wizardSecondPageNestedDw.ts | 145 +++ .../wizardWithDisabledDynamicWizard.ts | 187 ++++ .../fixtures/wizardWithDynamicWizard.ts | 171 +++ .../fixtures/wizardWithDynamicWizardLogic.ts | 177 ++++ .../fixtures/wizardWithDynamicWizardOnInit.ts | 71 ++ .../wizardWithDynamicWizardUnderCondition.ts | 178 ++++ .../wizardWithDynamicWizardValidation.ts | 74 ++ .../fixtures/wizardWithEmptyDynamicWizard.ts | 28 + .../fixtures/wizardWithNestedWizard.ts | 136 +++ .../fixtures/wizardWithRequiredDW.ts | 193 ++++ .../DynamicWizard/templates/header.ejs | 5 + .../DynamicWizard/templates/index.ts | 3 + .../DynamicWizard/templates/row.ejs | 31 + src/sass/contrib.scss | 38 + .../bootstrap/dynamicWizard/form.ejs | 86 ++ .../bootstrap/dynamicWizard/html.ejs | 63 ++ .../bootstrap/dynamicWizard/index.ts | 4 + tsconfig.json | 4 +- tsconfig.spec.json | 21 + 35 files changed, 4164 insertions(+), 3 deletions(-) create mode 100644 .mocharc.json create mode 100644 register.js create mode 100644 src/Wizard.ts create mode 100644 src/components/DynamicWizard/DynamicWizard.form.ts create mode 100644 src/components/DynamicWizard/DynamicWizard.spec.ts create mode 100644 src/components/DynamicWizard/DynamicWizard.ts create mode 100644 src/components/DynamicWizard/editForm/DynamicWizard.edit.data.ts create mode 100644 src/components/DynamicWizard/editForm/DynamicWizard.edit.display.ts create mode 100644 src/components/DynamicWizard/editForm/DynamicWizard.edit.templates.ts create mode 100644 src/components/DynamicWizard/editForm/DynamicWizard.edit.validation.ts create mode 100644 src/components/DynamicWizard/fixtures/dynamicWizardInsideWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/dynamicWizardWithMinMaxValidation.ts create mode 100644 src/components/DynamicWizard/fixtures/nestedWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/simpleDynamicWizardChild.ts create mode 100644 src/components/DynamicWizard/fixtures/webformWithNestedWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardSecondPageNestedDw.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithDisabledDynamicWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithDynamicWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithDynamicWizardLogic.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithDynamicWizardOnInit.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithDynamicWizardUnderCondition.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithDynamicWizardValidation.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithEmptyDynamicWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithNestedWizard.ts create mode 100644 src/components/DynamicWizard/fixtures/wizardWithRequiredDW.ts create mode 100644 src/components/DynamicWizard/templates/header.ejs create mode 100644 src/components/DynamicWizard/templates/index.ts create mode 100644 src/components/DynamicWizard/templates/row.ejs create mode 100644 src/templates/bootstrap/dynamicWizard/form.ejs create mode 100644 src/templates/bootstrap/dynamicWizard/html.ejs create mode 100644 src/templates/bootstrap/dynamicWizard/index.ts create mode 100644 tsconfig.spec.json diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..ac4be81 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,4 @@ +{ + "require": "./register.js", + "reporter": "dot" +} \ No newline at end of file diff --git a/package.json b/package.json index 90b6c31..e0dfe2f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "module": "node", "scripts": { "test:coverage": "nyc --reporter=text mocha --reporter spec './{,!(node_modules)/**/}*.spec.js'", - "test": "mocha --require ts-node/register --reporter spec './{,!(node_modules)/**/}*.spec.ts'", + "test": "mocha --require ejsify --require jsdom-global/register --reporter spec './{,!(node_modules)/**/}*.spec.ts'", "watch": "tsc -w", "webpack:dev": "webpack", "webpack:prod": "webpack --config webpack.prod.js", @@ -48,11 +48,16 @@ "@types/node": "^18.15.5", "@types/sinon": "^10.0.13", "chai": "^4.2.0", + "ejs": "^3.1.10", + "ejsify": "^1.0.0", + "esm": "^3.2.25", "eventemitter3": "^5.0.1", "gulp": "^4.0.2", "gulp-insert": "^0.5.0", "gulp-rename": "^2.0.0", "gulp-template": "^5.0.0", + "jsdom": "^25.0.0", + "jsdom-global": "^3.0.2", "lodash": "^4.17.21", "mocha": "^10.2.0", "native-promise-only": "^0.8.1", diff --git a/register.js b/register.js new file mode 100644 index 0000000..a0bcf09 --- /dev/null +++ b/register.js @@ -0,0 +1,12 @@ +/** + * Overrides the tsconfig used for the app. + * In the test environment we need some tweaks. + */ + + const tsNode = require('ts-node'); + + tsNode.register({ + files: true, + transpileOnly: true, + project: './tsconfig.spec.json' + }); diff --git a/src/Wizard.ts b/src/Wizard.ts new file mode 100644 index 0000000..649dae1 --- /dev/null +++ b/src/Wizard.ts @@ -0,0 +1,124 @@ +import { Displays, Utils } from '@formio/js'; +const { _ } = Utils; +const { get } = _; +const OriginalWizard = Displays.displays.wizard; + +export default class Wizard extends OriginalWizard { + [x: string]: any; + + emitWizardPageSelected(index) { + this.setChangingMode(); + this.emit('wizardPageSelected', this.pages[index], index); + } + + emitNextPage() { + this.setChangingMode(); + this.emit('nextPage', { page: this.page, submission: this.submission }); + } + + emitPrevPage() { + this.setChangingMode(); + this.emit('prevPage', { page: this.page, submission: this.submission }); + } + + focusOnComponent(key) { + const superFocusOnComponent = super.focusOnComponent(key); + const dynamicWizardComponent = this.hasDynamicWizard(); + + if (dynamicWizardComponent) { + const component = this.getComponent(key); + dynamicWizardComponent.switchToStep(component); + } + else if (this.element && this.element.classList.contains('dynamicWizard-changingMode')) { + this.element.classList.remove('dynamicWizard-changingMode'); + } + return superFocusOnComponent; + } + + hasButton(name, nextPage = this.getNextPage()) { + // get page options with global options as default values + const { + previous = this.options.buttonSettings.showPrevious, + cancel = this.options.buttonSettings.showCancel, + submit = this.options.buttonSettings.showSubmit, + next = this.options.buttonSettings.showNext + } = get(this.currentPage, 'component.buttonSettings', {}); + const dynamicWizardComponent = this.hasDynamicWizard(); + const isDynamicWizardNotActive = !dynamicWizardComponent?.isChangingMode || !dynamicWizardComponent.hasComponents; + const showNavButtons = isDynamicWizardNotActive || this.options.readOnly; + + switch (name) { + case 'previous': + return previous && (this.getPreviousPage() > -1) && showNavButtons; + case 'next': + return next && (nextPage !== null) && (nextPage !== -1) && showNavButtons; + case 'cancel': + return cancel && !this.options.readOnly && showNavButtons; + case 'submit': + return submit && !this.options.readOnly && ((nextPage === null) || (this.page === (this.pages.length - 1))) && isDynamicWizardNotActive; + default: + return true; + } + } + + setEditMode(submission) { + if (!this.editMode && submission._id && !this.options.readOnly) { + this.editMode = true; + const dynamicWizard = this.hasDynamicWizard(); + if (dynamicWizard) { + dynamicWizard.shouldUpdate = false; + dynamicWizard.isChangingMode = false; + } + this.redraw(); + } + } + + setChangingMode() { + if (this.options.readOnly) { + return; + } + + const dynamicWizard = this.hasDynamicWizard(); + // If the current page contains dynamicWizard component + if (dynamicWizard && !this.editMode && dynamicWizard.hasComponents) { + if (dynamicWizard.shouldUpdate) { + dynamicWizard.addRow(); + dynamicWizard.shouldUpdate = false; + dynamicWizard.secondRender = true; + } + + if (this.element && !this.element.classList.contains('dynamicWizard-changingMode')) { + // helps to return nearby components, if validation.maxLength is occurred + if ( + dynamicWizard?.editRows.length === get(dynamicWizard?.component, 'validate.maxLength') + && !dynamicWizard.isOpen(dynamicWizard.editRows[dynamicWizard.changingRowIndex])) { + return; + } + this.element.classList.add('dynamicWizard-changingMode'); + this.redraw(); + } + } + else if (this.element) { + if (dynamicWizard?.isChangingMode && dynamicWizard.hasComponents) { + dynamicWizard.isChangingMode = false; + this.redraw(); + } + if (this.element.classList.contains('dynamicWizard-changingMode')) { + this.element.classList.remove('dynamicWizard-changingMode'); + } + } + } + + hasDynamicWizard() { + return this.currentPage?.components.find((comp) => comp.component.type === 'dynamicWizard' && comp._parentVisible && comp._visible && !comp._disabled); + } + + updatePages() { + this.pages = this.allPages; + const dynamicWizardComponent = this.hasDynamicWizard(); + + if (this.element && this.element.classList.contains('dynamicWizard-changingMode') && !dynamicWizardComponent) { + this.element.classList.remove('dynamicWizard-changingMode'); + } + } +} diff --git a/src/components/DynamicWizard/DynamicWizard.form.ts b/src/components/DynamicWizard/DynamicWizard.form.ts new file mode 100644 index 0000000..93da429 --- /dev/null +++ b/src/components/DynamicWizard/DynamicWizard.form.ts @@ -0,0 +1,34 @@ +import { Components } from '@formio/js'; +const baseEditForm = (Components.components as any).component.editForm; + +import DynamicWizardEditData from './editForm/DynamicWizard.edit.data'; +import DynamicWizardEditDisplay from './editForm/DynamicWizard.edit.display'; +import DynamicWizardEditTemplates from './editForm/DynamicWizard.edit.templates'; +import DynamicWizardEditValidation from './editForm/DynamicWizard.edit.validation'; + +export default function(...extend) { + return baseEditForm([ + { + label: 'Templates', + key: 'templates', + weight: 5, + components: DynamicWizardEditTemplates + }, + { + key: 'display', + components: DynamicWizardEditDisplay, + }, + { + key: 'data', + components: DynamicWizardEditData, + }, + { + key: 'validation', + components: DynamicWizardEditValidation + }, + { + key: 'addons', + ignore: true + }, + ], ...extend); +} diff --git a/src/components/DynamicWizard/DynamicWizard.spec.ts b/src/components/DynamicWizard/DynamicWizard.spec.ts new file mode 100644 index 0000000..50a4db2 --- /dev/null +++ b/src/components/DynamicWizard/DynamicWizard.spec.ts @@ -0,0 +1,569 @@ +import { expect } from 'chai'; +import { Utils, Components, Displays } from '@formio/js'; +const { cloneDeep, get } = require('lodash'); +const sinon = require('sinon'); +const sandbox = sinon.createSandbox(); + +import DynamicWizard from './DynamicWizard'; +import Wizard from '../../Wizard'; +import wizardWithNestedWizard from './fixtures/wizardWithNestedWizard'; +import webformWithNestedWizard from './fixtures/webformWithNestedWizard'; +import nestedWizardForm from './fixtures/nestedWizard'; +import wizardWithDynamicWizardUnderCondition from './fixtures/wizardWithDynamicWizardUnderCondition'; +import wizardWithDisabledDynamicWizard from './fixtures/wizardWithDisabledDynamicWizard'; +import wizardWithDynamicWizard from './fixtures/wizardWithDynamicWizard'; +import wizardWithDynamicWizardOnInit from './fixtures/wizardWithDynamicWizardOnInit'; +import wizardWithDynamicWizardValidation from './fixtures/wizardWithDynamicWizardValidation'; +import wizardWithDynamicWizardLogic from './fixtures/wizardWithDynamicWizardLogic'; +import simpleDynamicWizardChild from './fixtures/simpleDynamicWizardChild'; +import wizardSecondPageNestedDynamicWizard from './fixtures/wizardSecondPageNestedDw'; +import wizardWithRequiredDW from './fixtures/wizardWithRequiredDW'; +import dynamicWizardWithMinMaxValidation from './fixtures/dynamicWizardWithMinMaxValidation'; +import dynamicWizardInsideWizard from './fixtures/dynamicWizardInsideWizard'; +import wizardWithEmptyDynamicWizard from './fixtures/wizardWithEmptyDynamicWizard'; + +describe('DynamicWizard', () => { + before(() => { + Components.setComponent('dynamicWizard', DynamicWizard); + }); + it('rootWizard should be correct for the DynamicWizard inside the wizard nested in the wizard', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const nestedWizard = cloneDeep(nestedWizardForm); + + wizard.setForm(wizardWithNestedWizard)?.then(() => { + const nestedWizardComp = wizard.getComponent('wizardNested'); + + nestedWizardComp.loadSubForm = () => { + nestedWizardComp.formObj = nestedWizard; + nestedWizardComp.subFormLoading = false; + + return new Promise((resolve) => resolve(nestedWizard)); + }; + + nestedWizardComp.createSubForm(); + + setTimeout(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + + expect(wizard.id).to.equal(dynamicWizard.rootWizard.id); + + done(); + }, 200); + }).catch((err) => done(err));; + }); + + it('rootWizard should be correct for the DynamicWizard inside the wizard nested in the webform', (done) => { + const formElement = document.createElement('div'); + const Webform = Displays.displays.webform; + const webform = new Webform(formElement); + const nestedWizard = cloneDeep(nestedWizardForm); + + webform.setForm(webformWithNestedWizard).then(() => { + const nestedWizardComp = webform.getComponent('wizardNested'); + + nestedWizardComp.loadSubForm = () => { + nestedWizardComp.formObj = nestedWizard; + nestedWizardComp.subFormLoading = false; + + return new Promise((resolve) => resolve(nestedWizard)); + }; + + nestedWizardComp.createSubForm(); + + setTimeout(() => { + const dynamicWizard = webform.getComponent('dynamicWizard'); + expect(nestedWizardComp.subForm.id).to.equal(dynamicWizard.rootWizard.id); + + done(); + }, 200); + }).catch((err) => done(err));; + }); + + it('Should change prevVisibility value properly when conditions are satisfied', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardUnderCondition); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const textFieldnotDW = wizard.getComponent('textFieldnotDW'); + const conditionalValue = 'up'; + const nonConditionalValue = 'wrong value'; + + textFieldnotDW.setValue(conditionalValue) + + setTimeout(() => { + expect(dynamicWizard.prevVisibility).to.be.true; + + textFieldnotDW.setValue(nonConditionalValue) + + setTimeout(() => { + expect(dynamicWizard.prevVisibility).to.be.false; + + done(); + }, 300); + }, 300); + }); + }); + + it('Should call addRow method when conditions are satisfied', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardUnderCondition); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const textFieldnotDW = wizard.getComponent('textFieldnotDW'); + const conditionalValue = 'up'; + const spy = sandbox.spy(dynamicWizard, 'addRow'); + + textFieldnotDW.setValue(conditionalValue) + + setTimeout(() => { + expect(spy.calledOnce).to.be.true; + + done(); + }, 500); + }); + }); + + it('Should change prevBlocking and isChangingMode property to "false" when component is no longer disabled', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDisabledDynamicWizard); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const numberField = wizard.getComponent('number'); + const valueToTriggerAction = 2; + + numberField.setValue(valueToTriggerAction); + + setTimeout(() => { + expect(dynamicWizard.prevBlocking).to.be.false; + expect(dynamicWizard.isChangingMode).to.be.false; + + done(); + }, 500); + }); + }); + + it('Should call redraw method on rootWizard when component is no longer disabled', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDisabledDynamicWizard); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const numberField = wizard.getComponent('number'); + const valueToTriggerAction = 2; + const spy = sandbox.spy(dynamicWizard.rootWizard, 'redraw'); + + numberField.setValue(valueToTriggerAction); + + setTimeout(() => { + expect(spy.calledOnce).to.be.true; + + done(); + }, 500); + }); + }); + + it('Should render dynamicWizard in changingMode after init', () => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardOnInit); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const dynamicWizardInput = dynamicWizard.components[0]; + const editRowsInput = dynamicWizard.editRows[0].components[0]; + + expect(wizard.element.className).to.equal('dynamicWizard-changingMode'); + expect(dynamicWizard.isChangingMode).to.be.true; + expect(dynamicWizardInput).to.equal(editRowsInput); + }); + }); + + it('Should render dynamicWizard in changingMode after set new page', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizard); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + + expect(dynamicWizard.editRows).to.have.lengthOf(0); + + const secondPageBtn = wizard.refs[`${wizard.wizardKey}-link`][1]; + const clickEvent = new Event('click'); + + secondPageBtn.dispatchEvent(clickEvent); + + setTimeout(() => { + const dynamicWizardInput = dynamicWizard.components[0]; + const editRowsInput = dynamicWizard.editRows[0].components[0]; + + expect(dynamicWizard.isChangingMode).to.be.true; + expect(wizard.element.className).to.equal('dynamicWizard-changingMode'); + expect(dynamicWizardInput).to.equal(editRowsInput); + + done() + }, 200); + }); + }); + + it('Should cancel a row and clear the data correctly', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardOnInit); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + + expect(dynamicWizard.editRows.length).to.equal(1); + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [] }); + + wizard.getComponent('dynamicWizard[0].textField').setValue('text'); + + setTimeout(() => { + dynamicWizard.nextPage(); + + setTimeout(() => { + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'text', textField1: '' }] }); + dynamicWizard.cancelRow(); + + setTimeout(() => { + expect(dynamicWizard.editRows.length).to.equal(0); + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [] }); + expect(dynamicWizard.isChangingMode).to.be.false; + expect(wizard.element.className).not.equal('dynamicWizard-changingMode'); + done() + }, 200); + }, 200); + }, 200); + }); + }); + + it('Should edit/save a row correctly', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardOnInit); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + + expect(dynamicWizard.editRows.length).to.equal(1); + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [] }); + + wizard.getComponent('dynamicWizard[0].textField').setValue('text'); + + setTimeout(() => { + dynamicWizard.nextPage(); + + setTimeout(() => { + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'text', textField1: '' }] }); + + wizard.getComponent('dynamicWizard[0].textField1').setValue('text1'); + + setTimeout(() => { + dynamicWizard.saveRow(); + + setTimeout(() => { + expect(dynamicWizard.editRows.length).to.equal(1); + expect(dynamicWizard.editRows[0].state).to.equal('saved'); + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'text', textField1: 'text1' }] }); + expect(dynamicWizard.isChangingMode).to.be.false; + expect(wizard.element.className).not.equal('dynamicWizard-changingMode'); + + setTimeout(() => { + dynamicWizard.editRow(0); + + setTimeout(() => { + expect(dynamicWizard.editRows.length).to.equal(1); + expect(dynamicWizard.editRows[0].state).to.equal('editing'); + expect(dynamicWizard.isChangingMode).to.be.true; + expect(wizard.element.className).to.equal('dynamicWizard-changingMode'); + + wizard.getComponent('dynamicWizard[0].textField').setValue('changedText'); + + setTimeout(() => { + dynamicWizard.nextPage(); + + setTimeout(() => { + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'changedText', textField1: 'text1' }] }); + + dynamicWizard.cancelRow(); + + setTimeout(() => { + expect(dynamicWizard.editRows.length).to.equal(1); + expect(dynamicWizard.editRows[0].state).to.equal('saved'); + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'text', textField1: 'text1' }] }); + expect(dynamicWizard.isChangingMode).to.be.false; + expect(wizard.element.className).not.equal('dynamicWizard-changingMode'); + + done() + }, 100); + }, 100); + }, 100); + }, 100); + }, 100); + }, 100); + }, 100); + }, 100); + }, 100); + }); + }); + it('Should cancel a row and clear the data correctly', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardValidation); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + expect(dynamicWizard.step).to.equal(0); + expect(dynamicWizard.errors.length).to.equal(0); + + dynamicWizard.nextPage(); + setTimeout(() => { + expect(dynamicWizard.step).to.equal(0); + expect(dynamicWizard.errors[0].message).to.equal('Text Field is required'); + + wizard.getComponent('dynamicWizard[0].textField').setValue('text'); + setTimeout(() => { + dynamicWizard.nextPage(); + + setTimeout(() => { + expect(dynamicWizard.step).to.equal(1); + expect(dynamicWizard.errors.length).to.equal(0); + + done() + }, 200); + }, 200); + }, 200); + }); + }) + it('Should not cause infinite redraw method on changing the value set by logic', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardLogic); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const textField = wizard.getComponent('textField'); + const textArea = wizard.getComponent('textArea'); + + textField.setValue('yes'); + + setTimeout(() => { + dynamicWizard.nextPage(); + const renderSpy = sandbox.spy(dynamicWizard, 'render'); + const beforeFocusSpy = sandbox.spy(dynamicWizard, 'beforeFocus'); + + setTimeout(() => { + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'yes', textArea: 'definitely' }] }); + textArea.focus(); + textArea.setValue('random'); + + setTimeout(() => { + expect(dynamicWizard.data).to.deep.equal({ dynamicWizard: [{ textField: 'yes', textArea: 'definitely' }] }); + expect(renderSpy.notCalled).to.be.equal(true); + expect(beforeFocusSpy.calledOnce).to.be.equal(true); + done() + }, 200); + }, 200); + }, 200); + }); + }); + + it('Should have components in nested Dynamic Wizard', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardSecondPageNestedDynamicWizard); + const nestedForm = cloneDeep(simpleDynamicWizardChild); + + wizard.setForm(form)?.then(() => { + + const nestedWizardComp = wizard.getComponent('wizardNested'); + nestedWizardComp.loadSubForm = () => { + nestedWizardComp.formObj = nestedForm; + nestedWizardComp.subFormLoading = false; + + return new Promise((resolve) => resolve(nestedForm)); + }; + nestedWizardComp.createSubForm() + + setTimeout(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + + const clickWizardBtn = (pathPart, clickError) => { + const btn = get(wizard.refs, clickError ? pathPart : `${wizard.wizardKey}-${pathPart}`); + const clickEvent = new Event('click'); + btn.dispatchEvent(clickEvent); + }; + + clickWizardBtn('link[1]', null); + + setTimeout(() => { + expect(dynamicWizard.components).to.have.lengthOf(1); + expect(dynamicWizard.editRows).to.have.lengthOf(1); + expect(dynamicWizard.changingRowIndex).to.equal(0); + done() + }, 200) + }, 200) + }) + }); + + it('Should call triggerChange on proper component', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(simpleDynamicWizardChild); + + wizard.setForm(form)?.then(() => { + const checkbox = wizard.getComponent('checkbox'); + const onChangeSpy = sandbox.spy(checkbox, 'onChange'); + const triggerChangeSpy = sandbox.spy(checkbox, 'triggerRootChange'); + checkbox.setValue(true); + wizard.render(); + + setTimeout(() => { + expect(onChangeSpy.calledOnce).to.be.equal(true); + expect(triggerChangeSpy.calledOnce).to.be.equal(true); + expect(checkbox.root.id).to.be.equal(wizard.id); + done(); + }, 200) + }); + }); + + it('Should work in edit mode', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithDynamicWizardOnInit); + const submission = { + "_id": "608fb38c4b356614a4d73db2", + "data": { + "dynamicWizard": [ + { + "textField": "text1", + "textField1": "text2" + } + ] + }, + }; + + wizard.setForm(form)?.then(() => { + wizard.setValue(submission, {}, false); + const dynamicWizard = wizard.getComponent('dynamicWizard'); + dynamicWizard.redraw().then(() => { + expect(dynamicWizard.editRows.length).to.equal(1); + expect(dynamicWizard.editRows[0].state).to.equal('saved'); + expect(dynamicWizard.isChangingMode).to.be.false; + expect(wizard.element.className).not.equal('dynamicWizard-changingMode'); + + done(); + }).catch((err) => done(err)); + }) + }).timeout(10000) + + it('Should display components near DW in edit mode while clicking on the error in the Error list', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithRequiredDW); + const clickEvent = new Event('click'); + + wizard.setForm(form)?.then(() => { + const clickWizardBtn = (pathPart, clickError) => { + const btn = get(wizard.refs, clickError ? pathPart : `${wizard.wizardKey}-${pathPart}`); + btn.dispatchEvent(clickEvent); + }; + + clickWizardBtn('link[2]', null); + + setTimeout(() => { + clickWizardBtn('submit', null); + + setTimeout(() => { + expect(wizard.errors.length).to.equal(4); + expect((wizard.refs as any).errorRef.length).to.equal(4); + + const secondListErrorBtn = (wizard.refs as any).errorRef[1]; + secondListErrorBtn.dispatchEvent(clickEvent); + + setTimeout(() => { + expect(wizard.page).to.equal(1); + expect(wizard.element.className).to.equal('dynamicWizard-changingMode'); + + const firstListErrorBtn = (wizard.refs as any).errorRef[0]; + firstListErrorBtn.dispatchEvent(clickEvent); + + setTimeout(() => { + expect(wizard.page).to.equal(0); + expect(wizard.element.className).not.equal('dynamicWizard-changingMode'); + done() + }, 200); + }, 200); + }, 200); + }, 200); + }); + }); + + it('Should hide "Delete"/"Yes" buttons when there are min-max number of data entries display', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(dynamicWizardWithMinMaxValidation); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + expect(dynamicWizard.isChangingMode).to.equal(true); + dynamicWizard.nextPage(); + setTimeout(() => { + expect(dynamicWizard.isChangingMode).to.equal(false); + dynamicWizard.removeRow(0); + setTimeout(() => { + dynamicWizard.addRow(); + setTimeout(() => { + expect(dynamicWizard.isChangingMode).to.equal(true); + done(); + }, 200); + }, 200) + }, 200); + }); + }); + + it('getComponentPath should return correct path', (done) => { + const formElement = document.createElement('div'); + const Webform = Displays.displays.webform; + const webform = new Webform(formElement); + + webform.setForm(dynamicWizardInsideWizard).then(() => { + const wizardComp = webform.getComponent('dynamicWizard'); + const nestedWizardComp = webform.getComponent('nestedDynamicWizard'); + + expect(Utils.getComponentPath(wizardComp)).to.equal('dynamicWizard'); + expect(Utils.getComponentPath(nestedWizardComp)).to.equal('dynamicWizard.nestedDynamicWizard'); + + done(); + + }).catch((err) => done(err));; + }); + + it('Navigation buttons should render correctly if there are no components inside', (done) => { + const formElement = document.createElement('div'); + const wizard = new Wizard(formElement); + const form = cloneDeep(wizardWithEmptyDynamicWizard); + + wizard.setForm(form)?.then(() => { + const dynamicWizard = wizard.getComponent('dynamicWizard'); + const buttons = { + cancel: { name: 'cancel', method: 'cancel' }, + submit: { name: 'submit', method: 'submit' } + } + expect(dynamicWizard.editRows).to.have.lengthOf(0); + expect(dynamicWizard.buttons).to.deep.equal({}); + expect(wizard.buttons).to.deep.equal(buttons); + + done(); + }).catch((err) => done(err)); + }); +}); diff --git a/src/components/DynamicWizard/DynamicWizard.ts b/src/components/DynamicWizard/DynamicWizard.ts new file mode 100644 index 0000000..d602139 --- /dev/null +++ b/src/components/DynamicWizard/DynamicWizard.ts @@ -0,0 +1,973 @@ +import { Components, Utils } from '@formio/js'; +const EditGrid = (Components as any).components.editgrid; +const NestedComponent = (Components as any).components.nested; +import editForm from './DynamicWizard.form'; +import templates from './templates'; +const { _ } = Utils; +const { get, set, each, isNil, clone, findIndex, isFinite, cloneDeep, isArray } = _; + +const FormState = { + New: 'new', + Editing: 'editing', + Saved: 'saved', + Removed: 'removed', + Draft: 'draft', +}; + +export default class DynamicWizard extends (EditGrid as any) { + static schema(...extend) { + return EditGrid.schema({ + type: 'dynamicWizard', + label: 'Dynamic Wizard', + key: 'dynamicWizard', + templates: { + header: DynamicWizard.defaultHeaderTemplate, + row: DynamicWizard.defaultRowTemplate, + footer: '', + }, + }, ...extend); + } + + static get builderInfo(): object { + return { + title: 'Dynamic Wizard', + group: 'premium', + documentation: '/userguide/form-building/premium-components#dynamic-wizard', + showPreview: false, + icon: 'tasks', + ignoreForForm: true, + disableSiblings: true, + weight: 20, + schema: DynamicWizard.schema(), + }; + } + + static savedValueTypes(schema) { + return Utils.getComponentSavedTypes(schema) || [Utils.componentValueTypes.array]; + } + + public static editForm = editForm; + + static get defaultHeaderTemplate() { + return `
+
+ Users +
+
`; + } + + static get defaultRowTemplate() { + return `
+
+
+
+ User {{ rowIndex + 1 }} +
+ {% if (!ctx.self.options.readOnly && !component.disabled) { %} +
+
+ + {% if (!instance.hasRemoveButtons || instance.hasRemoveButtons()) { %} + + {% } %} +
+
+ {% } %} +
+
+ {% ctx.util.eachComponent(ctx.components, function(component) { %} + {% if ((!component.hasOwnProperty('tableView') || component.tableView) && isVisibleInRow(component)) { %} +
+
+ {{ component.key }} +
+
+
+
+ {{ ctx.getView(component, ctx.row[component.key]) }} +
+
+ {% } %} + {% }) %} +
`; + } + + get dynamicWizardKey() { + return `dynamicWizard-${this.key}`; + } + + get rowRef() { + return `${this.dynamicWizardKey}-row`; + } + + get rowElements() { + return this.refs[this.rowRef]; + } + + get rowRefs() { + return this.refs[`dynamicWizard-${this.component.key}-row`]; + } + + get agreeButtonRef() { + return `${this.dynamicWizardKey}-agreeButton`; + } + + get denyButtonRef() { + return `${this.dynamicWizardKey}-denyButton`; + } + + get agreeButtonElements() { + return this.refs[this.agreeButtonRef]; + } + + get cancelRowRef() { + return `${this.dynamicWizardKey}-cancelRow`; + } + + get cancelRowElements() { + return this.refs[this.cancelRowRef]; + } + + get inlineEditMode() { + return this.component.inlineEdit; + } + + get saveEditMode() { + return !this.inlineEditMode; + } + + get rootWizard() { + return this.findRootWizard(this.parent?.parent) || {}; + } + + get hasComponents() { + return this.component.components.length; + } + + get buttons() { + const buttons = {}; + [ + { name: 'cancel', method: 'cancelRow' }, + { name: 'previous', method: 'prevPage' }, + { name: 'next', method: 'nextPage' }, + ].forEach((button) => { + if (this.hasButton(button.name)) { + buttons[button.name] = button; + } + }); + return buttons; + } + + get defaultSchema() { + return DynamicWizard.schema() + } + + constructor(...args) { + super(...args); + this.type = 'dynamicWizard'; + this.isChangingMode = true; + this.isSavingInProgress = false; + this.editRows = []; + this.changingRowIndex = null; + this.step = 0; + this.buttonSettings = { + showPrevious: true, + showNext: true, + showSubmit: true, + showCancel: !this.options.readOnly + } + this.shouldUpdate = true; + } + + init(secondRender, prevVisibility = this._visible) { + this.prevVisibility = prevVisibility; + this.prevBlocking = this.disabled; + this.secondRender = !!secondRender; + this.shouldUpdate = true; + this.changingRowIndex = null; + if (this.builderMode) { + this.editRows = []; + } + + return super.init(); + } + + public render(children) { + if (this.builderMode) { + return super.render(); + } + + const dataValue = this.dataValue || []; + const headerTemplate = Utils.Evaluator.noeval ? templates.header : get(this.component, 'templates.header'); + const editRow = this.editRows[this.changingRowIndex]; + + if (this.step === 0 && editRow && !editRow.components[this.step]._visible) { + this.nextPage(); + } + + const reset = !this.editRows.length && this.step === 0 && this.changingRowIndex === 0; + if (((!this.secondRender && this.shouldUpdate) || reset) && !this.options.readOnly) { + this.secondRender = true; + + if (!this.rootWizard.editMode) { + const onFirstPage = this.root.page === 0 && this.page === this.root.page; + if (onFirstPage && this._parentVisible && this._visible && !this._disabled && this.hasComponents) { + this.shouldUpdate = false; + this.isInitRowExists = true; + this.addRow(true, reset); + } + } + } + + if (this.root?.editMode && this.rootWizard.editMode && this.isInitRowExists) { + this.isInitRowExists = false; + this.cancelRow(); + } + + return super.render(children || this.renderTemplate('dynamicWizard', { + ref: { + row: this.rowRef, + agreeButton: this.agreeButtonRef, + denyButton: this.denyButtonRef, + }, + header: this.renderString(headerTemplate), + footer: this.renderString(get(this.component, 'templates.footer'), { + components: this.component.components, + value: dataValue, + }), + rows: this.editRows.map(this.renderRow.bind(this)), + currentComponent: this.step === -1 ? null : this.editRows[this.changingRowIndex]?.components[this.step]?.render(), + errors: this.editRows.map((row) => row.error), + buttons: this.buttons, + hasAddButton: this.hasAddButton(), + hasRemoveButtons: this.hasRemoveButtons(), + isChangingMode: this.isChangingMode, + isDisabled: this._disabled, + isBlocking: !this._visible || this._disabled || !this.hasComponents, + dynamicWizardKey: this.dynamicWizardKey, + readOnly: this.options.readOnly, + })); + } + + attach(element: HTMLElement) { + const superAttach = super.attach(element); + if (this.builderMode) { + return super.attach(element); + } + + this.visibilityCheck(); + this.blockingCheck(); + + this.loadRefs(element, { + [this.agreeButtonRef]: 'multiple', + [this.denyButtonRef]: 'multiple', + [this.rowRef]: 'multiple', + [`${this.dynamicWizardKey}-cancel`]: 'single', + [`${this.dynamicWizardKey}-previous`]: 'single', + [`${this.dynamicWizardKey}-next`]: 'single', + }); + this.agreeButtonElements.forEach((agreeButton) => { + this.addEventListener(agreeButton, 'click', () => { + this.addRow(); + + if (this.rootWizard.currentNextPage === -1) { + this.rootWizard.redraw(); + } + } + ); + }); + + this.attachNav(); + + if (!this.isChangingMode) { + this.rowElements.forEach((row, rowIndex) => { + // Attach edit and remove button events. + [ + { + className: 'removeCard', + event: 'click', + action: () => this.removeRow(rowIndex), + }, + { + className: 'editCard', + event: 'click', + action: () => { + this.editRow(rowIndex); + }, + }, + ].forEach(({ + className, + event, + action, + }) => { + const elements = row.getElementsByClassName(className); + Array.prototype.forEach.call(elements, (element) => { + this.addEventListener(element, event, action); + }); + }); + }); + } else { + const editRow = this.editRows[this.changingRowIndex]; + if (editRow) { + this.attachComponents(this.rowElements[0], editRow.components, this.component.components, true); + } + } + + return superAttach; + } + + attachNav() { + each(this.buttons, (button) => { + const buttonElement = this.refs[`${this.dynamicWizardKey}-${button.name}`]; + this.addEventListener(buttonElement, 'click', (event) => { + event.preventDefault(); + const cancelRedraw = this[button.method](); + + if (cancelRedraw && button.method === 'nextPage') { + return; + } + this.redraw().then(() => { + this.afterAttachNav(button.method); + }); + + const editRow = this.editRows[this.changingRowIndex]; + + if (editRow) { + this.validateRow(editRow, false); + } + }); + }); + } + + afterAttachNav(method) { + const editRow = this.editRows[this.changingRowIndex]; + switch (method) { + case 'prevPage': + case 'nextPage': + if (this.step === -1) { + this.agreeButtonElements[0].focus(); + } else { + const input = editRow.components[this.step].refs?.input; + input ? input[0].focus() : this.focusOnNewRowElement(editRow.components, this.step); + } + } + } + + attachComponents(element, components, container = this.component.components, isDynamicWizard) { + if (this.builderMode) { + return super.attachComponents(element, components, container); + } + if (!isDynamicWizard) { + return; + } + components = components || this.component.components; + + element = this.hook('attachComponents', element, components, container, this); + if (!element) { + // Return a non-resolving promise. + return (new Promise(() => { + // empty + })); + } + + let index = 0; + const promises = []; + if (!this.isChangingMode) { + Array.prototype.slice.call(element.children).forEach(child => { + if (!child.getAttribute('data-noattach') && components[index]) { + promises.push(components[index].attach(child)); + index++; + } + }); + } else if (!isNil(this.step) && element.children[0] && !element.children[0]?.getAttribute('data-noattach') && components[this.step]) { + promises.push(components[this.step].attach(element.children[0])); + } + + return Promise.all(promises); + } + + visibilityCheck() { // start changingMode when the component becomes visible by condition + if (this.page === this.root.page) { + if (this._visible && !this.prevVisibility) { + this.prevVisibility = true; + if (!this.disabled && this.hasComponents) { + this.addRow(); + } + } + if (this.prevVisibility && !this._visible) { + this.prevVisibility = false; + } + } + } + blockingCheck() { // show agreeButton when component is no longer disabled + if (this.page === this.root.page && this.prevBlocking && !this.disabled) { + this.prevBlocking = false; + this.isChangingMode = false; + + this.rootWizard.redraw(); + } + } + + isOpen(editRow) { + return [FormState.New, FormState.Editing].includes(editRow?.state); + } + + resetValue() { + NestedComponent.prototype.resetValue.call(this); + this.emptyRows(); + this.step = 0; + this.addRow(true, true); + } + + addRow(firstPage = false, reset = false) { + if (this.options.readOnly) { + return; + } + + if (!this.hasAddButton() && !this.isOpen(this.editRows[this.changingRowIndex])) { + if (this.isChangingMode) { + this.isChangingMode = false; + } + this.rootWizard.element.classList.remove('dynamicWizard-changingMode'); + this.rootWizard.redraw(); + return; + } + + const dataObj = {}; + const rowIndex = this.editRows.length; + let editRow; + + if (isNil(this.changingRowIndex) || reset) { + this.changingRowIndex = this.editRows.length; + editRow = { + components: this.createRowComponents(dataObj, rowIndex), + data: dataObj, + state: FormState.New, + backup: null, + error: null, + }; + this.editRows.push(editRow); + + if (this.inlineEditMode) { + this.dataValue.push(dataObj); + this.triggerChange(); + } + this.checkRow('checkData', null, {}, editRow.data, editRow.components); + if (this.component.modal) { + this.addRowModal(rowIndex); + } + } + + this.isChangingMode = true; + this.step = this.step !== -1 ? this.step : 0; + + this.addChangingMode(firstPage); + + return editRow || this.editRows[this.changingRowIndex]; + } + + cancelRow() { + const rowIndex = clone(this.changingRowIndex); + if (this.options.readOnly) { + return; + } + + const editRow = this.editRows[rowIndex]; + if (editRow?.state === FormState.New) { + if (this.isSavingInProgress) { + this.removeRow(rowIndex); + } + else { + editRow.state = FormState.Removed; + + this.clearErrors(rowIndex); + this.destroyComponents(rowIndex); + if (this.inlineEditMode) { + this.splice(rowIndex); + } + this.editRows.splice(rowIndex, 1); + } + } + else if (editRow?.state === FormState.Editing) { + editRow.state = editRow.prevState; + + if (this.inlineEditMode) { + this.dataValue[rowIndex] = editRow.backup; + } + editRow.data = editRow.backup; + editRow.backup = null; + + if (this.isSavingInProgress) { + this.setValues(editRow); + this.rebuild(true); + } + + this.restoreRowContext(editRow); + + if (!this.component.rowDrafts) { + this.clearErrors(rowIndex); + } + } + + this.returnPrevPageState(); + + this.shouldUpdate = true; + + this.emit('cancelRow'); + + this.checkValidity(null, true); + this.redraw().then(() => { + this.afterCancelRow(); + }); + + if (this.component.rowDrafts) { + this.checkValidity(this.data, false); + } + } + + afterCancelRow() { + this.agreeButtonElements[0].focus(); + } + + rebuild(secondRender = false) { + this.destroy(); + this.init(secondRender, this.prevVisibility); + this.visible = this.conditionallyVisible(null, null); + return this.redraw(); + } + + returnPrevPageState() { + this.isChangingMode = false; + this.isSavingInProgress = false; + this.step = -1; + this.changingRowIndex = null; + this.removeChangingMode(); + } + + setValues(editRow) { + if (this.options.readOnly) { + return; + } + + if (this.saveEditMode) { + const dataValue = this.dataValue || []; + if (editRow?.state === FormState.New && !this.isSavingInProgress) { + const newIndex = dataValue.length; + dataValue.push(editRow?.data); + if (this.changingRowIndex !== newIndex) { + this.editRows.splice(this.changingRowIndex, 1); + this.editRows.splice(this.changingRowIndex, 0, editRow); + } + } + else if (editRow?.state === FormState.Editing || this.isSavingInProgress) { + dataValue[this.changingRowIndex] = editRow?.data; + } + } + } + + saveRow(isRowValid) { + const editRow = this.editRows[this.changingRowIndex]; + + if (editRow) { + this.setValues(editRow); + if (this.isSavingInProgress) { + this.isSavingInProgress = false; + } + + editRow.state = this.component.rowDrafts && !isRowValid ? FormState.Draft : FormState.Saved; + editRow.backup = null; + + this.updateValue(); + this.triggerChange(); + if (this.component.rowDrafts) { + editRow.components.forEach(comp => comp.setPristine(this.pristine)); + } + this.checkValidity(null, true); + this.redraw().then(() => { + this.emit('saveRow'); + if (this.afterSaveRow) { + this.afterSaveRow(); + } + }); + + if (editRow.alerts) { + editRow.alerts = false; + } + + this.isChangingMode = false; + this.changingRowIndex = null; + this.shouldUpdate = true; + + this.removeChangingMode(); + } + return true; + } + + saveCurrentPageData(editRow) { + if (editRow?.state === FormState.New || editRow?.state === FormState.Editing) { + this.setValues(editRow); + this.checkData(this.data); + if (!this.isSavingInProgress) { + this.isSavingInProgress = true; + } + } + } + + isRowEditing(editRow) { + return editRow?.state === FormState.Editing || editRow?.state === FormState.New; + } + + switchToStep(component) { + const relativePath = this.getRelativePath(component.path); + const arrayPath = (Utils as any).getArrayFromComponentPath(relativePath); + const step = findIndex(this.components, (comp) => comp.key === component.component.key); + const editRow = this.editRows[arrayPath[0]]; + const isAlreadyEditing = this.isRowEditing(editRow); + + if (!editRow) { + this.addRow(); + } + else if (!isAlreadyEditing) { + this.editRow(arrayPath[0], step); + } + else { + this.step = isFinite(step) ? step : 0; + this.addChangingMode(); + this.validateRow(editRow, false); + } + } + + editRow(rowIndex, step?) { + const editRow = this.editRows[rowIndex]; + const isAlreadyEditing = this.isRowEditing(editRow); + + if (!editRow || isAlreadyEditing) { + return; + } + editRow.prevState = editRow.state; + editRow.state = FormState.Editing; + + const dataSnapshot = cloneDeep(editRow.data); + + if (this.inlineEditMode) { + editRow.backup = dataSnapshot; + } + else { + editRow.backup = editRow.data; + editRow.data = dataSnapshot; + this.restoreRowContext(editRow); + } + + if (this.component.modal) { + return this.addRowModal(rowIndex); + } + + this.isChangingMode = true; + this.changingRowIndex = rowIndex; + this.step = isFinite(step) ? step : 0; + + this.addChangingMode(); + this.validateRow(editRow, false); + } + + removeRow(rowIndex) { + if (this.options.readOnly) { + return; + } + + this.baseRemoveRow(rowIndex); + this.splice(rowIndex); + this.editRows.splice(rowIndex, 1); + this.updateRowsComponents(rowIndex); + this.updateValue(); + this.triggerChange(); + this.checkValidity(null, true); + this.checkData(); + this.redraw().then(() => { + this.afterRemoveRow(); + }); + this.shouldUpdate = true; + } + + afterRemoveRow() { + this.agreeButtonElements[0].focus(); + } + + renderRow(row, rowIndex) { + const dataValue = this.dataValue || []; + const flattenedComponents = this.flattenComponents(rowIndex); + const rowTemplate = Utils.Evaluator.noeval ? templates.row : get(this.component, 'templates.row', DynamicWizard.defaultRowTemplate); + + return this.renderString( + rowTemplate, + { + row: dataValue[rowIndex] || {}, + data: this.data, + rowIndex, + components: this.component.components, + flattenedComponents, + isVisibleInRow: (component) => this.isComponentVisibleInRow(component, flattenedComponents), + getView: (component, data) => { + const instance = flattenedComponents[component.key]; + let view = instance ? instance.getView(data || instance.dataValue) : ''; + + if (instance && instance.widget && (view !== '--- PROTECTED ---')) { + if (isArray(view)) { + view = view.map((value) => instance.widget.getValueAsString(value)); + } + else { + view = instance.widget.getValueAsString(view); + } + } + + return view; + }, + state: this.editRows[rowIndex].state, + }, + ); + } + + hasButton(name, nextPage = this.getNextPage()) { + // get page options with global options as default values + const { + previous = this.buttonSettings.showPrevious, + cancel = this.buttonSettings.showCancel, + next = this.buttonSettings.showNext + } = this.buttonSettings; + + switch (name) { + case 'previous': + return previous && this.step !== 0 && this.hasComponents; + case 'next': + return next && nextPage && this.hasComponents; + case 'cancel': + return cancel && this.hasComponents; + default: + return true; + } + } + + getNextPage() { + return this.step < this.component?.components.length; + } + + prevPage() { + const editRow = this.editRows[this.changingRowIndex]; + + if (this.step === 0) { + this.rootWizard.prevPage(); + return; + } + else { + const currStep = clone(this.step); + this.step = currStep - 1; + if (!editRow?.components?.[this.step]?._visible) { + this.prevPage(); + return; + } + + this.saveCurrentPageData(editRow); + } + } + + findRootWizard(component) { + let root = component?.parent?.parent; + + if (component?.parent?.subForm) { + root = root?.parent; + } + + return root && root?._form?.display === 'wizard' ? this.findRootWizard(root) : component; + } + + nextPage() { + const editRow = this.editRows[this.changingRowIndex]; + const isRowValid = this.validateRow(editRow, true).length === 0; + + if (!this.component.rowDrafts) { + if (!isRowValid) { + return true; + } + } + + if (this.step === this.component?.components?.length - 1) { + this.step = -1; + this.saveRow(isRowValid); + + if (this.rootWizard.currentNextPage === -1) { + this.rootWizard.redraw(); + } + + return false; + } + const prevStep = clone(this.step); + this.step = prevStep + 1; + + if (!editRow?.components?.[this.step]?._visible) { + this.nextPage(); + return; + } + + this.saveCurrentPageData(editRow); + } + + validateStep(component, valid, dirty, editRow) { + let isValid = valid; + const hasServerError = !!(component.serverErrors && component.serverErrors.length); + + if (!this.component.rowDrafts && !hasServerError) { + component.setPristine(!dirty); + } + + const rootValue = JSON.parse(JSON.stringify(this.rootValue)); + const editGridValue = get(rootValue, this.path, []); + editGridValue[this.step] = editRow.data; + set(rootValue, this.path, editGridValue); + isValid = isValid && component.checkValidity(rootValue, dirty, editRow.data); + return isValid; + }; + + validateRow(editRow, dirty) { + let valid = true as any; + + if (!editRow) { + return []; + } + + const errorsSnapshot = [...this.errors]; + + if (editRow.state === FormState.Editing || dirty || (editRow.state === FormState.Draft && !this.pristine && !this.root.pristine)) { + const component = editRow.components[this.step]; + + if (component) { + valid = this.validateStep(component, valid, dirty, editRow); + } + else if (this.step === -1) { + editRow.components.forEach(comp => { + valid = this.validateStep(comp, valid, dirty, editRow); + }); + } + } + + if (this.component.validate && this.component.validate.row) { + valid = this.evaluate(this.component.validate.row, { + valid, + row: editRow.data + }, 'valid', true); + if (valid.toString() !== 'true') { + editRow.error = valid; + valid = false; + } + else { + editRow.error = null; + } + if (valid === null) { + valid = `Invalid row validation for ${this.key}`; + } + } + + editRow.errors = !valid ? this.errors.filter((err) => !errorsSnapshot.includes(err)) : []; + this.showRowErrorAlerts(editRow, !!valid); + return editRow.errors; + } + + findSourceRoot(root) { + return root?.parent ? this.findSourceRoot(root?.root) : root; + } + + changeState(changed) { + const sourceRoot = this.root ? this.findSourceRoot(this.root) : {}; + if (changed && !sourceRoot.submissionInProcess) { + !this.prevVisibility && this._visible ? this.rebuild(this.secondRender) : this.rebuild(); + } + else { + this.redraw(); + } + } + + addChangingMode(firstPage = false) { + if (this.rootWizard.element) { + if (!this.rootWizard.element.classList.contains('dynamicWizard-changingMode')) { + this.rootWizard.element.classList.add('dynamicWizard-changingMode'); + } + + if (!firstPage) { + this.redraw().then(() => { + this.afterAddChangingMode(); + }); + this.rootWizard.redraw(); + } + + this.triggerChange(); + } + } + + afterAddChangingMode() { + const editRow = this.editRows[this.changingRowIndex]; + if (editRow.components[this.step].refs?.input) { + editRow.components[this.step].refs?.input[0].focus(); + } else { + this.focusOnNewRowElement(editRow.components, this.step); + } + } + + removeChangingMode() { + if (this.rootWizard?.element) { + this.rootWizard.element.classList.remove('dynamicWizard-changingMode'); + this.redraw().then(() => { + if (this.afterRemoveChangingMode) { + this.afterRemoveChangingMode(); + } + }); + this.rootWizard.redraw(); + + this.triggerChange(); + } + } + + getValueAsString(value, options) { + if (options?.email) { + let result = (` + + + + `); + + this.component.components?.forEach((component) => { + const label = component.label || component.key; + result += ``; + }); + + result += (` + + + + `); + + this.iteratableRows.forEach(({ components }) => { + result += ''; + each(components, (component) => { + result += ''; + }); + result += ''; + }); + + result += (` + +
${label}
'; + if (component.isInputComponent && component.visible && !component.skipInEmail) { + result += component.getView(component.dataValue, options); + } + result += '
+ `); + + return result; + } + + if (!value || !value.length) { + return ''; + } + + return super.getValueAsString(value, options); + } + } diff --git a/src/components/DynamicWizard/editForm/DynamicWizard.edit.data.ts b/src/components/DynamicWizard/editForm/DynamicWizard.edit.data.ts new file mode 100644 index 0000000..0d29afb --- /dev/null +++ b/src/components/DynamicWizard/editForm/DynamicWizard.edit.data.ts @@ -0,0 +1,18 @@ +export default [ + { + type: 'checkbox', + input: true, + weight: 105, + key: 'inlineEdit', + label: 'Inline Editing', + tooltip: 'Check this if you would like your changes within \'edit\' mode to be committed directly to the submission object as that row is being changed', + }, + { + key: 'defaultValue', + ignore: true, + }, + { + key: 'multiple', + ignore: true + }, +]; diff --git a/src/components/DynamicWizard/editForm/DynamicWizard.edit.display.ts b/src/components/DynamicWizard/editForm/DynamicWizard.edit.display.ts new file mode 100644 index 0000000..3b05d93 --- /dev/null +++ b/src/components/DynamicWizard/editForm/DynamicWizard.edit.display.ts @@ -0,0 +1,76 @@ +function showHideColumnByDefault(context: any) { + return context?.instance.options?.flags?.isInDataTable && + !['hidden', 'columns', 'panel', 'table', 'tabs', 'well', 'fieldset'] + .includes(context.data.type); +}; + +function dataTableConfigFields() { + return [ + { + key: 'display', + components: [ + { + weight: 442, + type: 'number', + input: true, + key: 'columnWeight', + label: 'Column weight in DataTable', + placeholder: '0', + defaultValue: 0, + tooltip: 'Make columns appear in a different order in the table.', + customConditional(context) { + return showHideColumnByDefault(context) + } + }, + { + weight: 1370, + type: 'checkbox', + label: 'Hide column in Data Table by default', + tooltip: 'Hide column in Data Table by default.', + key: 'hideColumnByDefault', + input: true, + customConditional(context) { + return showHideColumnByDefault(context); + } + }, + { + label: 'Disable sorting and filtering in Data Table', + tooltip: 'if enabled, sorting and filtering is disabled for this column.', + tableView: false, + key: 'disableSortingAndFiltering', + type: 'checkbox', + input: true, + weight: 1390, + defaultValue: false, + customConditional(context) { + return showHideColumnByDefault(context); + } + }, + { + label: 'Column Query Property', + weight: 445, + tooltip:'The component property to use it in API driven Data Table queries for filtering and sorting. If the property is not defined, the component key will be used instead.', + tableView: true, + key: 'columnQueryProperty', + type: 'textfield', + input: true, + customConditional(context) { + return showHideColumnByDefault(context); + }, + }, + ] + } + ] +} + +export default [ + { + key: 'placeholder', + ignore: true, + }, + { + key: 'modalEdit', + ignore: true, + }, + ...dataTableConfigFields() +]; diff --git a/src/components/DynamicWizard/editForm/DynamicWizard.edit.templates.ts b/src/components/DynamicWizard/editForm/DynamicWizard.edit.templates.ts new file mode 100644 index 0000000..86fdfbd --- /dev/null +++ b/src/components/DynamicWizard/editForm/DynamicWizard.edit.templates.ts @@ -0,0 +1,57 @@ +import { Utils } from '@formio/js'; + +export default [ + { + type: 'textarea', + label: 'Header Template', + key: 'templates.header', + rows: 5, + editor: 'ace', + as: 'handlebars', + input: true, + placeholder: '/*** Lodash Template Code ***/', + tooltip: 'This is the Lodash Template used to render the header of the Dynamic Wizard.', + customConditional() { + return !Utils.Evaluator.noeval; + } + }, + { + type: 'textarea', + label: 'Row Template', + key: 'templates.row', + rows: 5, + editor: 'ace', + as: 'handlebars', + input: true, + placeholder: '/*** Lodash Template Code ***/', + description: 'Three available variables. "row" is an object of one row\'s data, "components"' + + ' is the array of components in the grid and "state" is current row\'s state (can be "draft" or "saved").', + tooltip: 'This is the Lodash Template used to render each row of the Dynamic Wizard.', + customConditional() { + return !Utils.Evaluator.noeval; + } + }, + { + type: 'textarea', + label: 'Footer Template', + key: 'templates.footer', + rows: 5, + editor: 'ace', + as: 'handlebars', + input: true, + placeholder: '/*** Lodash Template Code ***/', + description: 'Two available variables. "value" is the array of row data and "components" is the array of components in the grid.', + tooltip: 'This is the Lodash Template used to render the footer of the Dynamic Wizard.', + customConditional() { + return !Utils.Evaluator.noeval; + } + }, + { + type: 'textfield', + input: true, + key: 'rowClass', + label: 'Row CSS Class', + placeholder: 'Row CSS Class', + tooltip: 'CSS class to add to the edit row wrapper.' + }, +]; diff --git a/src/components/DynamicWizard/editForm/DynamicWizard.edit.validation.ts b/src/components/DynamicWizard/editForm/DynamicWizard.edit.validation.ts new file mode 100644 index 0000000..ec0c5e6 --- /dev/null +++ b/src/components/DynamicWizard/editForm/DynamicWizard.edit.validation.ts @@ -0,0 +1,32 @@ +export default [ + { + ignore: true, + key: 'unique', + }, + { + weight: 110, + key: 'validate.minLength', + label: 'Minimum Length', + placeholder: 'Minimum Length', + type: 'number', + tooltip: 'The minimum length requirement this field must meet.', + input: true + }, + { + weight: 120, + key: 'validate.maxLength', + label: 'Maximum Length', + placeholder: 'Maximum Length', + type: 'number', + tooltip: 'The maximum length requirement this field must meet.', + input: true + }, + { + type: 'checkbox', + input: true, + weight: 105, + key: 'rowDrafts', + label: 'Enable Row Drafts', + tooltip: 'Allow save rows even if their data is invalid. Errors will occur when try to submit with invalid rows.', + } +]; diff --git a/src/components/DynamicWizard/fixtures/dynamicWizardInsideWizard.ts b/src/components/DynamicWizard/fixtures/dynamicWizardInsideWizard.ts new file mode 100644 index 0000000..9b9140f --- /dev/null +++ b/src/components/DynamicWizard/fixtures/dynamicWizardInsideWizard.ts @@ -0,0 +1,61 @@ +export default { + type: 'form', + owner: null, + components: [ + { + label: 'Wizard Form', + tableView: true, + display: 'wizard', + components: [ + { + label: 'Dynamic Wizard', + tableView: true, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Dynamic Wizard', + tableView: true, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'nestedDynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + }, + ], + }, + ], + key: 'wizard', + type: 'form', + input: true, + }, + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false, + }, + ], +}; diff --git a/src/components/DynamicWizard/fixtures/dynamicWizardWithMinMaxValidation.ts b/src/components/DynamicWizard/fixtures/dynamicWizardWithMinMaxValidation.ts new file mode 100644 index 0000000..a2a591f --- /dev/null +++ b/src/components/DynamicWizard/fixtures/dynamicWizardWithMinMaxValidation.ts @@ -0,0 +1,148 @@ +export default { + _id: '60b64fdb93d8df225cfd44fd', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + validate: { + minLength: 2, + maxLength: 3, + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW with min max validation', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'dwWithMinMaxValidation', + path: 'dwwithminmaxvalidation', + project: '6038c83637595d104cfc3593', + created: '2021-06-01T15:18:51.827Z', + modified: '2021-06-01T15:20:23.629Z', + machineName: 'dqroghuntybetsh:dwWithMinMaxValidation', +}; diff --git a/src/components/DynamicWizard/fixtures/nestedWizard.ts b/src/components/DynamicWizard/fixtures/nestedWizard.ts new file mode 100644 index 0000000..e286f07 --- /dev/null +++ b/src/components/DynamicWizard/fixtures/nestedWizard.ts @@ -0,0 +1,149 @@ +export default { + _id: '60479f9f2c328733ce0965db', + type: 'form', + tags: [], + owner: '5f8c868776930a501b799009', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Dynamic Wizard', + tableView: true, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'Example DW', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '5f8d5c8c3b869f1794056e8b', + '5f8d5c8c3b869f1794056e8c', + '5f8d5c8c3b869f1794056e8d', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'exampleDw', + path: 'exampledw', + project: '5f8d5c8c3b869f1794056e8a', + created: '2021-03-09T16:17:35.996Z', + modified: '2021-03-11T17:03:00.017Z', + machineName: 'ddafsaalcenlmfj:exampleDw', +}; diff --git a/src/components/DynamicWizard/fixtures/simpleDynamicWizardChild.ts b/src/components/DynamicWizard/fixtures/simpleDynamicWizardChild.ts new file mode 100644 index 0000000..385aedd --- /dev/null +++ b/src/components/DynamicWizard/fixtures/simpleDynamicWizardChild.ts @@ -0,0 +1,152 @@ +export default { + _id: '6081383bfc88e7048cbe50ee', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Inner Page 1', + breadcrumbClickable: true, + buttonSettings: { + previous: true, + cancel: true, + next: true, + }, + scrollToTop: false, + collapsible: false, + key: 'innerPage1', + type: 'panel', + label: 'Page 1', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Checkbox', + tableView: false, + key: 'checkbox', + type: 'checkbox', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'Simple DW Child', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'simpleDwChild', + path: 'simpledwchild', + project: '6038c83637595d104cfc3593', + created: '2021-04-22T08:47:55.458Z', + modified: '2021-04-22T16:11:56.581Z', + machineName: 'dqroghuntybetsh:dwChild', +}; diff --git a/src/components/DynamicWizard/fixtures/webformWithNestedWizard.ts b/src/components/DynamicWizard/fixtures/webformWithNestedWizard.ts new file mode 100644 index 0000000..5a322b9 --- /dev/null +++ b/src/components/DynamicWizard/fixtures/webformWithNestedWizard.ts @@ -0,0 +1,136 @@ +export default { + _id: '60659857e484d324a81806c0', + type: 'form', + tags: [], + owner: '5f8c868776930a501b799009', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Form', + tableView: true, + // "form": "6065984ce484d324a81806b9", + useOriginalRevision: false, + key: 'wizardNested', + type: 'form', + input: true, + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'Parent Webform', + display: 'form', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '5f8d5c8c3b869f1794056e8b', + '5f8d5c8c3b869f1794056e8c', + '5f8d5c8c3b869f1794056e8d', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'parentWebform', + path: 'parentwebform', + project: '5f8d5c8c3b869f1794056e8a', + created: '2021-04-01T09:54:31.581Z', + modified: '2021-04-01T14:40:39.135Z', + machineName: 'ddafsaalcenlmfj:parentWebform', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardSecondPageNestedDw.ts b/src/components/DynamicWizard/fixtures/wizardSecondPageNestedDw.ts new file mode 100644 index 0000000..2cdfa5b --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardSecondPageNestedDw.ts @@ -0,0 +1,145 @@ +export default { + _id: '60813cf2fc88e7048cbe5124', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + input: false, + tableView: false, + components: [], + }, + { + title: 'Page 2', + label: 'Page 2', + type: 'panel', + key: 'page2', + components: [ + { + label: 'Form', + tableView: true, + // "form": "6081383bfc88e7048cbe50ee", + useOriginalRevision: false, + key: 'wizardNested', + type: 'form', + input: true, + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'Wizard Second Page Nested DW', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'wizardSecondPageNestedDw', + path: 'wizardsecondpagenesteddw', + project: '6038c83637595d104cfc3593', + created: '2021-04-22T09:08:02.903Z', + modified: '2021-04-22T09:18:32.151Z', + machineName: 'dqroghuntybetsh:00', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithDisabledDynamicWizard.ts b/src/components/DynamicWizard/fixtures/wizardWithDisabledDynamicWizard.ts new file mode 100644 index 0000000..bffce9b --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithDisabledDynamicWizard.ts @@ -0,0 +1,187 @@ +export default { + _id: '6050c1426174062bd847d3e8', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Number', + mask: false, + spellcheck: true, + tableView: false, + delimiter: false, + requireDecimal: false, + inputFormat: 'plain', + key: 'number', + type: 'number', + input: true, + }, + { + label: 'Dynamic Wizard', + disabled: true, + tableView: false, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + logic: [ + { + name: 'logic1', + trigger: { + type: 'simple', + simple: { + show: true, + when: 'number', + eq: '2', + }, + }, + actions: [ + { + name: 'action1', + type: 'property', + property: { + label: 'Disabled', + value: 'disabled', + type: 'boolean', + }, + state: false, + }, + ], + }, + ], + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'with disabled DW', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'withDisabledDw', + path: 'withdisableddw', + project: '6038c83637595d104cfc3593', + created: '2021-03-16T14:31:30.417Z', + modified: '2021-03-16T15:07:20.466Z', + machineName: 'dqroghuntybetsh:withDisabledDw', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithDynamicWizard.ts b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizard.ts new file mode 100644 index 0000000..1fa11e8 --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizard.ts @@ -0,0 +1,171 @@ +export default { + _id: '6051c38125d64e0afc2bdff3', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + input: false, + tableView: false, + }, + { + title: 'Page 2', + label: 'Page 2', + type: 'panel', + key: 'page2', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Number', + mask: false, + spellcheck: true, + tableView: false, + delimiter: false, + requireDecimal: false, + inputFormat: 'plain', + key: 'number', + type: 'number', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW-addRow', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'dwAddRow', + path: 'dwaddrow', + project: '6038c83637595d104cfc3593', + created: '2021-03-17T08:53:21.786Z', + modified: '2021-03-17T10:02:30.476Z', + machineName: 'dqroghuntybetsh:dwAddRow', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardLogic.ts b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardLogic.ts new file mode 100644 index 0000000..443211d --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardLogic.ts @@ -0,0 +1,177 @@ +export default { + _id: '606b1de9595f721860ee2be1', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly && !component.disabled) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if ((!component.hasOwnProperty(\'tableView\') || component.tableView) && isVisibleInRow(component)) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + { + label: 'Text Area', + autoExpand: false, + tableView: true, + key: 'textArea', + logic: [ + { + name: 'depending on textFild', + trigger: { + type: 'simple', + simple: { + show: true, + when: 'dynamicWizard.textField', + eq: 'yes', + }, + }, + actions: [ + { + name: 'set value', + type: 'value', + value: 'value = "definitely"', + }, + ], + }, + ], + type: 'textarea', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW simple logic', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'dwSimpleLogic', + path: 'dwsimplelogic', + project: '6038c83637595d104cfc3593', + created: '2021-04-05T14:25:45.850Z', + modified: '2021-04-06T13:21:31.624Z', + machineName: 'dqroghuntybetsh:dwSimpleLogic', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardOnInit.ts b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardOnInit.ts new file mode 100644 index 0000000..9e665e1 --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardOnInit.ts @@ -0,0 +1,71 @@ +export default { + _id: '6051f70025d64e0afc2be04f', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + { + label: 'Text Field 1', + tableView: true, + key: 'textField1', + type: 'textfield', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW-init', + display: 'wizard', + access: [ + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + ], + submissionAccess: [], + controller: '', + properties: {}, + settings: {}, + name: 'dwInit', + path: 'dwinit', + project: '6038c83637595d104cfc3593', + created: '2021-03-17T12:33:04.386Z', + modified: '2021-03-17T12:33:04.394Z', + machineName: 'dqroghuntybetsh:dwInit', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardUnderCondition.ts b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardUnderCondition.ts new file mode 100644 index 0000000..b464acc --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardUnderCondition.ts @@ -0,0 +1,178 @@ +export default { + _id: '60507bb26174062bd847d3aa', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Text Field - not in DW', + tableView: true, + key: 'textFieldnotDW', + type: 'textfield', + input: true, + }, + { + label: 'Number', + mask: false, + spellcheck: true, + tableView: false, + delimiter: false, + requireDecimal: false, + inputFormat: 'plain', + key: 'number', + conditional: { + show: true, + when: 'textFieldnotDW', + eq: 'up', + }, + type: 'number', + input: true, + }, + { + label: 'Dynamic Wizard', + tableView: false, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + conditional: { + show: true, + when: 'textFieldnotDW', + eq: 'up', + }, + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW-blocking-visibility-testx', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'dwBlockingVisibilityTestx', + path: 'dwblockingvisibilitytestx', + project: '6038c83637595d104cfc3593', + created: '2021-03-16T09:34:42.912Z', + modified: '2021-03-16T12:33:06.270Z', + machineName: 'dqroghuntybetsh:dwBlockingVisibilityTest', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardValidation.ts b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardValidation.ts new file mode 100644 index 0000000..aa37fd8 --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithDynamicWizardValidation.ts @@ -0,0 +1,74 @@ +export default { + _id: '6051f70025d64e0afc2be04f', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + templates: { + header: + '
\n
\n Users\n
\n
', + row: '
\n
\n
\n
\n User {{ rowIndex + 1 }}\n
\n {% if (!ctx.self.options.readOnly) { %}\n
\n
\n \n \n
\n
\n {% } %}\n
\n
\n {% ctx.util.eachComponent(ctx.components, function(component) { %}\n {% if (!component.hasOwnProperty(\'tableView\') || component.tableView) { %}\n
\n
\n {{ component.key }}\n
\n
\n
\n
\n {{ ctx.getView(component, ctx.row[component.key]) }}\n
\n
\n {% } %}\n {% }) %}\n
', + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Text Field', + tableView: true, + validate: { + required: true, + }, + key: 'textField', + type: 'textfield', + input: true, + }, + { + label: 'Text Field 1', + tableView: true, + key: 'textField1', + type: 'textfield', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW-init', + display: 'wizard', + access: [ + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + ], + submissionAccess: [], + controller: '', + properties: {}, + settings: {}, + name: 'dwInit', + path: 'dwinit', + project: '6038c83637595d104cfc3593', + created: '2021-03-17T12:33:04.386Z', + modified: '2021-03-17T12:33:04.394Z', + machineName: 'dqroghuntybetsh:dwInit', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithEmptyDynamicWizard.ts b/src/components/DynamicWizard/fixtures/wizardWithEmptyDynamicWizard.ts new file mode 100644 index 0000000..7ff28fa --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithEmptyDynamicWizard.ts @@ -0,0 +1,28 @@ +export default { + type: 'form', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [], + }, + ], + input: false, + tableView: false, + }, + ], + title: 'DW Empty', + display: 'wizard', + name: 'dwEmpty', + path: 'dwempty', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithNestedWizard.ts b/src/components/DynamicWizard/fixtures/wizardWithNestedWizard.ts new file mode 100644 index 0000000..6632b4c --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithNestedWizard.ts @@ -0,0 +1,136 @@ +export default { + _id: '604f188cf6d0b5297bea2109', + type: 'form', + tags: [], + owner: '5f8c868776930a501b799009', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Form', + tableView: true, + // "form": "60479f9f2c328733ce0965db", + useOriginalRevision: false, + key: 'wizardNested', + type: 'form', + input: true, + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'Simple Wizard With Nested Wizard1', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '5f8d5c8c3b869f1794056e8b', + '5f8d5c8c3b869f1794056e8c', + '5f8d5c8c3b869f1794056e8d', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'simpleWizardWithNestedWizard1', + path: 'simplewizardwithnestedwizard1', + project: '5f8d5c8c3b869f1794056e8a', + created: '2021-03-15T08:19:24.722Z', + modified: '2021-03-15T08:19:37.108Z', + machineName: 'ddafsaalcenlmfj:simpleWizardWithNestedWizard1', +}; diff --git a/src/components/DynamicWizard/fixtures/wizardWithRequiredDW.ts b/src/components/DynamicWizard/fixtures/wizardWithRequiredDW.ts new file mode 100644 index 0000000..ee85366 --- /dev/null +++ b/src/components/DynamicWizard/fixtures/wizardWithRequiredDW.ts @@ -0,0 +1,193 @@ +export default { + _id: '60b5ff4593d8df225cfd4316', + type: 'form', + tags: [], + owner: '6038bed737595d104cfc358a', + components: [ + { + title: 'Page 1', + label: 'Page 1', + type: 'panel', + key: 'page1', + components: [ + { + label: 'Text Field', + tableView: true, + validate: { + required: true, + }, + key: 'textField', + type: 'textfield', + input: true, + }, + ], + input: false, + tableView: false, + }, + { + title: 'Page 2', + label: 'Page 2', + type: 'panel', + key: 'page2', + components: [ + { + label: 'Dynamic Wizard', + tableView: false, + validate: { + required: true, + }, + rowDrafts: false, + key: 'dynamicWizard', + type: 'dynamicWizard', + input: true, + components: [ + { + label: 'Number', + mask: false, + spellcheck: true, + tableView: false, + delimiter: false, + requireDecimal: false, + inputFormat: 'plain', + key: 'number', + type: 'number', + input: true, + }, + ], + }, + ], + input: false, + tableView: false, + }, + { + title: 'Page 3', + label: 'Page 3', + type: 'panel', + key: 'page3', + components: [ + { + label: 'Text Area', + autoExpand: false, + tableView: true, + validate: { + required: true, + }, + key: 'textArea', + type: 'textarea', + input: true, + }, + ], + input: false, + tableView: false, + }, + ], + revisions: '', + _vid: 0, + title: 'DW test no component', + display: 'wizard', + access: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [ + '6038c83637595d104cfc3594', + '6038c83637595d104cfc3595', + '6038c83637595d104cfc3596', + ], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + submissionAccess: [ + { + roles: [], + type: 'create_own', + }, + { + roles: [], + type: 'create_all', + }, + { + roles: [], + type: 'read_own', + }, + { + roles: [], + type: 'read_all', + }, + { + roles: [], + type: 'update_own', + }, + { + roles: [], + type: 'update_all', + }, + { + roles: [], + type: 'delete_own', + }, + { + roles: [], + type: 'delete_all', + }, + { + roles: [], + type: 'team_read', + }, + { + roles: [], + type: 'team_write', + }, + { + roles: [], + type: 'team_admin', + }, + ], + controller: '', + properties: {}, + settings: {}, + name: 'dwTestNoComponent', + path: 'dwtestnocomponent', + project: '6038c83637595d104cfc3593', + created: '2021-06-01T09:35:01.444Z', + modified: '2021-06-01T09:36:11.563Z', + machineName: 'dqroghuntybetsh:dwTestNoComponent', +}; diff --git a/src/components/DynamicWizard/templates/header.ejs b/src/components/DynamicWizard/templates/header.ejs new file mode 100644 index 0000000..e098369 --- /dev/null +++ b/src/components/DynamicWizard/templates/header.ejs @@ -0,0 +1,5 @@ +
+
+ Users +
+
\ No newline at end of file diff --git a/src/components/DynamicWizard/templates/index.ts b/src/components/DynamicWizard/templates/index.ts new file mode 100644 index 0000000..c30a3f0 --- /dev/null +++ b/src/components/DynamicWizard/templates/index.ts @@ -0,0 +1,3 @@ +import row from './row.ejs'; +import header from './header.ejs'; +export default { row, header }; diff --git a/src/components/DynamicWizard/templates/row.ejs b/src/components/DynamicWizard/templates/row.ejs new file mode 100644 index 0000000..71c6a0f --- /dev/null +++ b/src/components/DynamicWizard/templates/row.ejs @@ -0,0 +1,31 @@ +
+
+
+
+ User {{ rowIndex + 1 }} +
+ {% if (!ctx.self.options.readOnly) { %} +
+
+ + +
+
+ {% } %} +
+
+ {% ctx.util.eachComponent(ctx.components, function(component) { %} + {% if ((!component.hasOwnProperty('tableView') || component.tableView) && ctx.isVisibleInRow(component)) { %} +
+
+ {{ component.key }} +
+
+
+
+ {{ ctx.getView(component, ctx.row[component.key]) }} +
+
+ {% } %} + {% }) %} +
\ No newline at end of file diff --git a/src/sass/contrib.scss b/src/sass/contrib.scss index 7267b98..7835cb2 100644 --- a/src/sass/contrib.scss +++ b/src/sass/contrib.scss @@ -18,3 +18,41 @@ .tree-listgroup { flex-direction: row; } + +.dynamicWizard-listgroup { + + .list-group-card { + padding: 0; + + list-style-type: none; + + @media (max-width: 568px) { + .list-group-item.list-group-subheader { + .row { + justify-content: space-between; + + .col-sm-2 { + width: auto; + } + } + } + } + } + + .list-group-field { + list-style-type: none; + } +} + +.dynamicWizard-changingMode { + .formio-component { + display: none; + } + .formio-component.formio-component-dynamicWizard { + display: block; + + .formio-component { + display: block; + } + } +} diff --git a/src/templates/bootstrap/dynamicWizard/form.ejs b/src/templates/bootstrap/dynamicWizard/form.ejs new file mode 100644 index 0000000..5ec1bd7 --- /dev/null +++ b/src/templates/bootstrap/dynamicWizard/form.ejs @@ -0,0 +1,86 @@ +
+ {% if (ctx.readOnly || !ctx.isChangingMode || ctx.isDisabled) { %} + {% if (ctx.header) { %} + + {% } %} + {% ctx.rows.forEach(function(row, rowIndex) { %} + + {% }) %} + {% if (ctx.footer) { %} + + {% } %} + {% } else { %} +
+ {{ ctx.currentComponent }} +
+ {% } %} +
+{% if (!ctx.readOnly && !ctx.isBlocking) { %} +{% if (!ctx.isChangingMode) { %} +{% if (ctx.hasAddButton) { %} +

Would you like to add another?

+ +{% } %} +{% } else { %} +
+ {% if (ctx.buttons.cancel) { %} +
+ +
+ {% } %} + {% if (ctx.buttons.previous) { %} +
+ +
+ {% } %} + {% if (ctx.buttons.next) { %} +
+ +
+ {% } %} +
+{% } %} +{% } %} +{% if (ctx.options.vpat) { %} + +{% } %} \ No newline at end of file diff --git a/src/templates/bootstrap/dynamicWizard/html.ejs b/src/templates/bootstrap/dynamicWizard/html.ejs new file mode 100644 index 0000000..ed8ce1d --- /dev/null +++ b/src/templates/bootstrap/dynamicWizard/html.ejs @@ -0,0 +1,63 @@ + +{% if (!ctx.readOnly && !ctx.isBlocking) { %} +{% if (!ctx.isChangingMode) { %} +{% if (ctx.hasAddButton) { %} +

Would you like to add another?

+ +{% } %} +{% } else { %} + +{% } %} +{% } %} \ No newline at end of file diff --git a/src/templates/bootstrap/dynamicWizard/index.ts b/src/templates/bootstrap/dynamicWizard/index.ts new file mode 100644 index 0000000..7f65740 --- /dev/null +++ b/src/templates/bootstrap/dynamicWizard/index.ts @@ -0,0 +1,4 @@ +import form from './form.ejs'; +import html from './html.ejs'; + +export default { form, html }; diff --git a/tsconfig.json b/tsconfig.json index 1d8ef71..8eef83e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "lib": [ "es2020", "dom" - ] + ], }, "include": [ "src/*.ts", @@ -23,6 +23,6 @@ ], "exclude": [ "node_modules", - "**/*.spec.ts" + "**/*.spec.ts", ] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..46513b7 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "module": "commonjs", + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "isolatedModules": false, + "strict": false, + "noImplicitAny": false, + "typeRoots" : [ + "../node_modules/@types" + ] + }, + "exclude": [ + "../node_modules" + ], + "include": [ + "./**/*.spec.ts" + ] +}