diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index ab65e6faa4..d5e41a2085 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -1,60 +1,12 @@ -# name: Build & Test - -# on: push - -# env: -# NODE_VERSION: 18.x - -# jobs: -# test: -# runs-on: ubuntu-latest -# steps: -# - run: echo "Triggered by ${{ github.event_name }} event." - -# - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} -# uses: actions/checkout@v3 - -# - name: Set up Node.js ${{ env.NODE_VERSION }} -# uses: actions/setup-node@v3 -# with: -# node-version: ${{ env.NODE_VERSION }} -# cache: 'npm' - -# - name: Cache node modules -# uses: actions/cache@v3 -# with: -# path: node_modules -# key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} -# restore-keys: | -# ${{ runner.os }}-node- - -# - name: Installing dependencies -# if: steps.cache.outputs.cache-hit != 'true' -# uses: borales/actions-yarn@v4 -# with: -# cmd: install --frozen-lockfile - -# - name: Lint -# uses: borales/actions-yarn@v4 -# with: -# cmd: lint - -# - name: Build -# uses: borales/actions-yarn@v4 -# with: -# cmd: build - -# - name: Test -# uses: borales/actions-yarn@v4 -# with: -# cmd: test - name: Build & Test -on: push +on: + push: + branches: + - '*' # This will make sure all push events on any branch triggers this workflow. env: - NODE_VERSION: 18.x + NODE_VERSION: 20.x jobs: setup: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..ab69ccdd92 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release + +on: + push: + tags: + - '*' # This will make sure tag creations also trigger the workflow. + +env: + NODE_VERSION: 20.x + AWS_DEFAULT_REGION: us-west-2 + AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + +jobs: + deploy_to_test: + # if: false + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - run: echo "Triggered by ${{ github.event_name }} event." + - name: Check out repository code ${{ github.repository }} on ${{ github.ref }} + uses: actions/checkout@v3 + + - name: Setup Ruby and Install Jekyll + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + bundler-cache: true + + - name: Install Jekyll + run: gem install jekyll + + - name: Restore node modules from cache + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install awscli + run: | + sudo apt-get update + sudo apt install -y awscli + + - name: Release + uses: borales/actions-yarn@v4 + with: + cmd: release \ No newline at end of file diff --git a/Changelog.md b/Changelog.md index 4fbdb88540..a6aac712a6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -208,9 +208,23 @@ Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/e - FIO-4871: fixed calculated value issues - FIO 7603: fixed Edit Grid With Empty Rows Not Submitting Form - FIO-7445: fixed an issue where the interpolated data does not show up on PDF + - FIO-7774: added validateWhenHidden option - FIO-7421: Adds ReCaptcha error messages to the translations config - FIO-7804: Added PKCE method for OIDC - FIO-7675: Removed maps key from repo + - FIO-2453: Fixes an issue where custom disabled dates are not recalculated + - FIO-7395: Fixed the issue with loading nested form + - FIO-7996: refactor recaptcha validation + - FIO-7899: fixed an issue where saveDraft option does not work and added errors handling for the save draft and restore draft functionality + - FIO-7956: fixed an issue where simple condition based on stringified checkbox value is not executed correctly + - FIO-7933: added PDF Document Designer + - FIO-6632: update-formiojs-test-env-on-tag + - FIO-2453: Fixes an issue where custom disabled dates are bot recalculated after for valus is changed + - FIO-7395: Fixed the issue with loading nested form + - FIO-7807: added sanitizeConfig to global form settings + - FIO-7334: Fixes an issue where Radio values do not appear for Action Conditions settings + - FIO-8009: fixed display of the required asterisk + - FIO-8111: fixed saveDraft Trigger for nested forms ## 5.0.0-rc.37 ### Fixed diff --git a/src/Form.js b/src/Form.js index 9a4c695e94..1b92c55de6 100644 --- a/src/Form.js +++ b/src/Form.js @@ -161,6 +161,38 @@ export default class Form extends Element { }; } + /** + * Check Subdirectories path and provide correct options + * + * @param {string} url - The the URL of the form json. + * @param {form} object - The form json. + * @return {object} The initial options with base and project. + */ + getFormInitOptions(url, form) { + const options = {}; + const index = url.indexOf(form?.path); + // form url doesn't include form path + if (index === -1) { + return options; + } + const projectUrl = url.substring(0, index - 1); + const urlParts = Formio.getUrlParts(projectUrl); + // project url doesn't include subdirectories path + if (!urlParts || urlParts.filter(part => !!part).length < 4) { + return options; + } + const baseUrl = `${urlParts[1]}${urlParts[2]}`; + // Skip if baseUrl has already been set + if (baseUrl !== Formio.baseUrl) { + return { + base: baseUrl, + project: projectUrl, + }; + } + + return {}; + } + setForm(formParam) { let result; formParam = formParam || this.form; @@ -185,7 +217,8 @@ export default class Form extends Element { } this.loading = false; this.instance = this.instance || this.create(form.display); - this.instance.url = formParam; + const options = this.getFormInitOptions(formParam, form); + this.instance.setUrl(formParam, options); this.instance.nosubmit = false; this._form = this.instance.form = form; if (submission) { diff --git a/src/Formio.unit.js b/src/Formio.unit.js index 0dbbaf2ee3..6167635c05 100644 --- a/src/Formio.unit.js +++ b/src/Formio.unit.js @@ -2222,6 +2222,48 @@ describe('Formio.js Tests', () => { ]; } }, + { + name: 'Should return correct options for form url with Subdirectories path', + test() { + let form = new Formio.Form(); + let options = form.getFormInitOptions('http://localhost:3000/fakeproject/fakeform', { path: 'fakeform' }); + assert.deepEqual(options, { + base: 'http://localhost:3000', + project: 'http://localhost:3000/fakeproject', + }); + + form = new Formio.Form(); + options = form.getFormInitOptions(`${Formio.baseUrl}/fakeproject/fakeform`, { path: 'fakeform' }); + assert.deepEqual(options, {}); + } + }, + { + name: 'Should set correct formio base and project url for form with Subdirectories path', + test() { + const formElement = document.createElement('div'); + return Formio.createForm(formElement, 'http://localhost:3000/fakeproject/fakeform') + .then((form) => { + assert.equal(form.formio.base, 'http://localhost:3000'); + assert.equal(form.formio.projectUrl, 'http://localhost:3000/fakeproject'); + }); + }, + mock() { + return { + url: 'http://localhost:3000/fakeproject/fakeform', + method: 'GET', + response() { + return { + headers: { + 'Content-Type': 'application/json', + }, + body: { + path: 'fakeform', + } + }; + } + }; + }, + }, ]; tests.forEach(testCapability); diff --git a/src/WebformBuilder.js b/src/WebformBuilder.js index ccbc889e73..857223a819 100644 --- a/src/WebformBuilder.js +++ b/src/WebformBuilder.js @@ -134,6 +134,7 @@ export default class WebformBuilder extends Component { html, disableBuilderActions: self?.component?.disableBuilderActions, childComponent: component, + design: self?.options?.design }); }; @@ -560,6 +561,7 @@ export default class WebformBuilder extends Component { attach(element) { this.on('change', (form) => { this.populateRecaptchaSettings(form); + this.webform.setAlert(false); }); return super.attach(element).then(() => { this.loadRefs(element, { @@ -945,6 +947,21 @@ export default class WebformBuilder extends Component { } } + if (draggableComponent.uniqueComponent) { + let isCompAlreadyExists = false; + eachComponent(this.webform.components, (component) => { + if (component.key === draggableComponent.schema.key) { + isCompAlreadyExists = true; + return; + } + }, true); + if (isCompAlreadyExists) { + this.webform.redraw(); + this.webform.setAlert('danger', `You cannot add more than one ${draggableComponent.title} component to one page.`); + return; + } + } + if (target !== source) { // Ensure the key remains unique in its new container. BuilderUtils.uniquify(this.findNamespaceRoot(target.formioComponent), info); @@ -982,7 +999,7 @@ export default class WebformBuilder extends Component { const componentInDataGrid = parent.type === 'datagrid'; - if (isNew && !this.options.noNewEdit && !info.noNewEdit) { + if (isNew && !this.options.noNewEdit && !info.noNewEdit && !(this.options.design && info.type === 'reviewpage')) { this.editComponent(info, target, isNew, null, null, { inDataGrid: componentInDataGrid }); } diff --git a/src/components/datagrid/DataGrid.js b/src/components/datagrid/DataGrid.js index 564bc1c0ff..56e615de88 100644 --- a/src/components/datagrid/DataGrid.js +++ b/src/components/datagrid/DataGrid.js @@ -241,7 +241,7 @@ export default class DataGridComponent extends NestedArrayComponent { } get canAddColumn() { - return this.builderMode; + return this.builderMode && !this.options.design; } render() { diff --git a/src/components/datetime/DateTime.unit.js b/src/components/datetime/DateTime.unit.js index c55b8922dd..ec7eb9d455 100644 --- a/src/components/datetime/DateTime.unit.js +++ b/src/components/datetime/DateTime.unit.js @@ -4,6 +4,7 @@ import DateTimeComponent from './DateTime'; import { Formio } from './../../Formio'; import _ from 'lodash'; import 'flatpickr'; +import moment from 'moment'; import { comp1, comp2, @@ -15,7 +16,8 @@ import { // comp9, comp10, comp11, - comp12 + comp12, + comp13, } from './fixtures'; describe('DateTime Component', () => { @@ -702,6 +704,33 @@ describe('DateTime Component', () => { }).catch(done); }); + it('Should refresh disabled dates when other fields values change', (done) => { + const form = _.cloneDeep(comp13); + const element = document.createElement('div'); + + Formio.createForm(element, form).then(form => { + const minDate = form.getComponent('minDate'); + const maxDate = form.getComponent('maxDate'); + minDate.setValue(moment().startOf('month').toISOString()); + maxDate.setValue(moment().startOf('month').add(7, 'days').toISOString()); + + setTimeout(() => { + const inBetweenDate = form.getComponent('inBetweenDate'); + const calendar = inBetweenDate.element.querySelector('.flatpickr-input').widget.calendar; + assert.equal(calendar.days.querySelectorAll('.flatpickr-disabled').length, 36, 'Only dates between selected' + + ' min and max dates should be enabled'); + + maxDate.setValue(moment().startOf('month').add(10, 'days').toISOString(), { modified: true }); + setTimeout(() => { + assert.equal(calendar.days.querySelectorAll('.flatpickr-disabled').length, 33, 'Should recalculate' + + ' disabled dates after value change'); + + done(); + }, 400); + }, 400); + }).catch(done); + }); + // it('Should provide correct date in selected timezone after submission', (done) => { // const form = _.cloneDeep(comp9); // const element = document.createElement('div'); diff --git a/src/components/datetime/fixtures/comp13.js b/src/components/datetime/fixtures/comp13.js new file mode 100644 index 0000000000..1d5f555ce1 --- /dev/null +++ b/src/components/datetime/fixtures/comp13.js @@ -0,0 +1,116 @@ +export default { + type: 'form', + display: 'form', + components: [ + { + label: 'Min Date', + tableView: false, + datePicker: { + disableWeekends: false, + disableWeekdays: false + }, + enableMinDateInput: false, + enableMaxDateInput: false, + key: 'minDate', + type: 'datetime', + input: true, + widget: { + type: 'calendar', + displayInTimezone: 'viewer', + locale: 'en', + useLocaleSettings: false, + allowInput: true, + mode: 'single', + enableTime: true, + noCalendar: false, + format: 'yyyy-MM-dd hh:mm a', + hourIncrement: 1, + minuteIncrement: 1, + // eslint-disable-next-line camelcase + time_24hr: false, + minDate: null, + disableWeekends: false, + disableWeekdays: false, + maxDate: null + } + }, + { + label: 'Max Date', + tableView: false, + enableMinDateInput: false, + datePicker: { + disableWeekends: false, + disableWeekdays: false + }, + enableMaxDateInput: false, + validate: { + custom: "var minDate = moment(data.minDate);\nvar maxDate = moment(data.maxDate);\nvalid = maxDate.isAfter(minDate)? true : 'Max date must be after min date'" + }, + key: 'maxDate', + type: 'datetime', + input: true, + widget: { + type: 'calendar', + displayInTimezone: 'viewer', + locale: 'en', + useLocaleSettings: false, + allowInput: true, + mode: 'single', + enableTime: true, + noCalendar: false, + format: 'yyyy-MM-dd hh:mm a', + hourIncrement: 1, + minuteIncrement: 1, + // eslint-disable-next-line camelcase + time_24hr: false, + minDate: null, + disableWeekends: false, + disableWeekdays: false, + maxDate: null + } + }, + { + label: 'In Between Date', + tableView: false, + datePicker: { + disableFunction: '!moment(date).isBetween(moment(data.minDate), moment(data.maxDate))', + disableWeekends: false, + disableWeekdays: false + }, + enableMinDateInput: false, + enableMaxDateInput: false, + key: 'inBetweenDate', + customConditional: 'show = !!data.minDate && !!data.maxDate', + type: 'datetime', + input: true, + widget: { + type: 'calendar', + displayInTimezone: 'viewer', + locale: 'en', + useLocaleSettings: false, + allowInput: true, + mode: 'single', + enableTime: true, + noCalendar: false, + format: 'yyyy-MM-dd hh:mm a', + hourIncrement: 1, + minuteIncrement: 1, + // eslint-disable-next-line camelcase + time_24hr: false, + minDate: null, + disableWeekends: false, + disableWeekdays: false, + disableFunction: '!moment(date).isBetween(moment(data.minDate), moment(data.maxDate))', + maxDate: null + } + }, + { + type: 'button', + label: 'Submit', + key: 'submit', + disableOnInvalid: true, + input: true, + tableView: false + } + ], +}; diff --git a/src/components/datetime/fixtures/index.js b/src/components/datetime/fixtures/index.js index c8c6e0a000..797556add6 100644 --- a/src/components/datetime/fixtures/index.js +++ b/src/components/datetime/fixtures/index.js @@ -9,4 +9,5 @@ import comp9 from './comp9'; import comp10 from './comp10'; import comp11 from './comp11'; import comp12 from './comp12'; -export { comp1, comp10, comp11, comp12, comp2, comp3, comp5, comp6, comp7, comp8, comp9 }; +import comp13 from './comp13'; +export { comp1, comp10, comp11, comp12, comp13, comp2, comp3, comp5, comp6, comp7, comp8, comp9 }; diff --git a/src/components/form/Form.js b/src/components/form/Form.js index 0aa906ee6e..76e6c15440 100644 --- a/src/components/form/Form.js +++ b/src/components/form/Form.js @@ -1,3 +1,4 @@ +/* eslint-disable max-statements */ import _ from 'lodash'; import Component from '../_classes/component/Component'; import ComponentModal from '../_classes/componentModal/ComponentModal'; @@ -221,6 +222,12 @@ export default class FormComponent extends Component { if (this.options.inEditGrid) { options.inEditGrid = this.options.inEditGrid; } + if (this.options.saveDraft) { + options.saveDraft = this.options.saveDraft; + } + if (this.options.saveDraftThrottle) { + options.saveDraftThrottle = this.options.saveDraftThrottle; + } return options; } @@ -449,6 +456,10 @@ export default class FormComponent extends Component { this.subForm.nosubmit = true; this.subForm.root = this.root; this.subForm.localRoot = this.isNestedWizard ? this.localRoot : this.subForm; + if (this.parent) { + this.subForm.draftEnabled = this.parent.draftEnabled; + this.subForm.savingDraft = this.parent.savingDraft; + } this.restoreValue(); this.valueChanged = this.hasSetValue; this.onChange(); @@ -489,7 +500,13 @@ export default class FormComponent extends Component { } else if (this.formSrc) { this.subFormLoading = true; - return (new Formio(this.formSrc)).loadForm({ params: { live: 1 } }) + const options = this.root.formio?.base && this.root.formio?.projectUrl + ? { + base: this.root.formio.base, + project: this.root.formio.projectUrl, + } + : {}; + return (new Formio(this.formSrc, options)).loadForm({ params: { live: 1 } }) .then((formObj) => { this.formObj = formObj; if (this.options.pdf && this.component.useOriginalRevision) { @@ -686,7 +703,13 @@ export default class FormComponent extends Component { if (shouldLoadSubmissionById) { const formId = submission.form || this.formObj.form || this.component.form; const submissionUrl = `${this.subForm.formio.formsUrl}/${formId}/submission/${submission._id}`; - this.subForm.setUrl(submissionUrl, this.options); + const options = this.root.formio?.base && this.root.formio?.projectUrl + ? { + base: this.root.formio.base, + project: this.root.formio.projectUrl, + } + : {}; + this.subForm.setUrl(submissionUrl, { ...this.options, ...options }); this.subForm.loadSubmission().catch((err) => { console.error(`Unable to load subform submission ${submission._id}:`, err); }); diff --git a/src/components/radio/Radio.js b/src/components/radio/Radio.js index 1907460990..a2973b1172 100644 --- a/src/components/radio/Radio.js +++ b/src/components/radio/Radio.js @@ -48,6 +48,23 @@ export default class RadioComponent extends ListComponent { }; } + static get serverConditionSettings() { + return { + ...super.serverConditionSettings, + valueComponent(classComp) { + return { + type: 'select', + dataSrc: 'custom', + valueProperty: 'value', + dataType: classComp.dataType || '', + data: { + custom: `values = ${classComp && classComp.values ? JSON.stringify(classComp.values) : []}`, + }, + }; + }, + }; + } + static savedValueTypes(schema) { const { boolean, string, number, object, array } = componentValueTypes; const { dataType } = schema; diff --git a/src/sass/formio.form.scss b/src/sass/formio.form.scss index 6f5c232ea2..857e7925be 100644 --- a/src/sass/formio.form.scss +++ b/src/sass/formio.form.scss @@ -846,8 +846,8 @@ body.formio-dialog-open { color:#EB0000; } -.formio-component-radio.formio-component-label-hidden.required .label-position-right.form-check-label:before, -.formio-component-selectboxes.formio-component-label-hidden.required .label-position-right.form-check-label:before { +.formio-component-radio.formio-component-label-hidden.required .form-check .label-position-right.form-check-label:before, +.formio-component-selectboxes.formio-component-label-hidden.required .form-check .label-position-right.form-check-label:before { right: 20px; } diff --git a/src/widgets/CalendarWidget.js b/src/widgets/CalendarWidget.js index 417c500885..8dcc5a23ca 100644 --- a/src/widgets/CalendarWidget.js +++ b/src/widgets/CalendarWidget.js @@ -498,6 +498,15 @@ export default class CalendarWidget extends InputWidget { } }); + // If other fields are used to calculate disabled dates, we need to redraw calendar to refresh disabled dates + if (this.settings.disableFunction && this.componentInstance && this.componentInstance.root) { + this.componentInstance.root.on('change', (e) => { + if (e.changed && this.calendar) { + this.calendar.redraw(); + } + }); + } + // Restore the calendar value from the component value. this.setValue(this.componentValue); }