From 2d37ba226f2db31fbfd1a46a854b2a9d28bf7432 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 28 Nov 2023 21:30:01 +0100 Subject: [PATCH] enh: Show confirmation dialog before submitting an empty form Signed-off-by: Ferdinand Thiessen --- .eslintrc.js | 1 + package-lock.json | 70 ++++++++++++++++++++++++++++++++++++-------- package.json | 4 ++- src/views/Submit.vue | 65 +++++++++++++++++++++++++++++++++++----- webpack.js | 12 ++++++++ 5 files changed, 132 insertions(+), 20 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 761dc9c85..8440e5f10 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,5 +8,6 @@ module.exports = { rules: { // We are using the @nextcloud/logger 'no-console': ['error', { allow: undefined }], + 'import/no-unresolved': ['error', { ignore: ['\\?raw'] }], }, } diff --git a/package-lock.json b/package-lock.json index bcaa9c0f0..1f1315e92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,13 @@ "vuedraggable": "^2.24.3" }, "devDependencies": { + "@mdi/svg": "^7.3.67", "@nextcloud/babel-config": "^1.0.0", "@nextcloud/browserslist-config": "^3.0.0", "@nextcloud/eslint-config": "^8.3.0", "@nextcloud/stylelint-config": "^2.3.1", - "@nextcloud/webpack-vue-config": "^6.0.0" + "@nextcloud/webpack-vue-config": "^6.0.0", + "raw-loader": "^4.0.2" }, "engines": { "node": "^20.0.0", @@ -4534,8 +4536,7 @@ "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", - "peer": true + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -5529,7 +5530,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5587,7 +5587,6 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peer": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -6304,7 +6303,6 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, - "peer": true, "engines": { "node": "*" } @@ -7972,7 +7970,6 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, - "peer": true, "engines": { "node": ">= 4" } @@ -9158,8 +9155,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "peer": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.1", @@ -12741,8 +12737,7 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "peer": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -15087,6 +15082,58 @@ "node": ">= 0.8" } }, + "node_modules/raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/raw-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/raw-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -17860,7 +17907,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "peer": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index dcd607475..13ad979c4 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,12 @@ "npm": "^9.0.0" }, "devDependencies": { + "@mdi/svg": "^7.3.67", "@nextcloud/babel-config": "^1.0.0", "@nextcloud/browserslist-config": "^3.0.0", "@nextcloud/eslint-config": "^8.3.0", "@nextcloud/stylelint-config": "^2.3.1", - "@nextcloud/webpack-vue-config": "^6.0.0" + "@nextcloud/webpack-vue-config": "^6.0.0", + "raw-loader": "^4.0.2" } } diff --git a/src/views/Submit.vue b/src/views/Submit.vue index ddd8e178f..ba5b52833 100644 --- a/src/views/Submit.vue +++ b/src/views/Submit.vue @@ -70,7 +70,7 @@ :name="t('forms', 'Thank you for completing the form!')" :description="form.submissionMessage"> @@ -120,12 +126,17 @@ import { loadState } from '@nextcloud/initial-state' import { generateOcsUrl } from '@nextcloud/router' import { showError } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' + import axios from '@nextcloud/axios' import moment from '@nextcloud/moment' import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' +import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import IconCheck from 'vue-material-design-icons/Check.vue' +import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' + +import IconCancelSvg from '@mdi/svg/svg/cancel.svg?raw' +import IconCheckSvg from '@mdi/svg/svg/check.svg?raw' import answerTypes from '../models/AnswerTypes.js' import logger from '../utils/Logger.js' @@ -142,10 +153,11 @@ export default { name: 'Submit', components: { - IconCheck, NcAppContent, + NcDialog, NcEmptyContent, NcLoadingIcon, + NcIconSvgWrapper, Question, QuestionLong, QuestionShort, @@ -197,15 +209,24 @@ export default { return { maxStringLengths: loadState('forms', 'maxStringLengths'), answerTypes, + /** + * Mapping of questionId => answers + * @type {Record} + */ answers: {}, loading: false, success: false, /** Submit state of the form, true if changes are currently submitted */ submitForm: false, + showConfirmEmptyModal: false, } }, computed: { + IconCheckSvg() { + return IconCheckSvg + }, + validQuestions() { return this.form.questions.filter(question => { // All questions must have a valid title @@ -264,6 +285,22 @@ export default { } return t('forms', 'Expires {relativeDate}.', { relativeDate }) }, + + /** + * Buttons for the "confirm submit empty form" dialog + */ + confirmEmptyModalButtons() { + return [{ + label: t('forms', 'Abort'), + icon: IconCancelSvg, + callback: () => {}, + }, { + label: t('forms', 'Submit'), + icon: IconCheckSvg, + type: 'primary', + callback: () => this.onConfirmedSubmit(), + }] + }, }, watch: { @@ -420,11 +457,24 @@ export default { }, /** - * Submit the form after the browser validated it 🚀 + * Submit the form after the browser validated it 🚀 or show confirmation modal if empty + */ + onSubmit() { + // in case no answer is set or all are empty show the confirmation dialog + if (Object.keys(this.answers).length === 0 || Object.values(this.answers).every((answers) => answers.length === 0)) { + this.showConfirmEmptyModal = true + } else { + // otherwise do the real submit + this.onConfirmedSubmit() + } + }, + + /** + * Handle the real submit of the form, this is only called if the form is not empty or user confirmed to submit */ - async onSubmit() { + async onConfirmedSubmit() { + this.showConfirmEmptyModal = false this.loading = true - this.submitForm = true try { await axios.post(generateOcsUrl('apps/forms/api/v2.1/submission/insert'), { @@ -432,6 +482,7 @@ export default { answers: this.answers, shareHash: this.shareHash, }) + this.submitForm = true this.success = true this.deleteFormFieldFromLocalStorage() emit('forms:last-updated:set', this.form.id) diff --git a/webpack.js b/webpack.js index 08d54672e..925124650 100644 --- a/webpack.js +++ b/webpack.js @@ -1,8 +1,20 @@ const path = require('path') const webpackConfig = require('@nextcloud/webpack-vue-config') +const webpackRules = require('@nextcloud/webpack-vue-config/rules') webpackConfig.entry.emptyContent = path.resolve(path.join('src', 'emptyContent.js')) webpackConfig.entry.submit = path.resolve(path.join('src', 'submit.js')) webpackConfig.entry.settings = path.resolve(path.join('src', 'settings.js')) +delete webpackRules.RULE_ASSETS + +webpackConfig.module.rules = [ + { + test: /\.svg$/i, + use: 'raw-loader', + resourceQuery: /raw/, + }, + ...Object.values(webpackRules), +] + module.exports = webpackConfig