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">
-
+
@@ -82,7 +82,7 @@
:name="t('forms', 'Form expired')"
:description="t('forms', 'This form has expired and is no longer taking answers')">
-
+
@@ -112,6 +112,12 @@
:disabled="loading"
:aria-label="t('forms', 'Submit form')">
+
+
+
@@ -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