Merge pull request #347 from nextcloud/enh/mandatory

Mandatory option on questions
This commit is contained in:
Jan-Christoph Borchardt 2020-05-05 23:40:35 +02:00 committed by GitHub
commit 0904899811
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 150 additions and 61 deletions

View file

@ -43,10 +43,13 @@
minlength="1"
:maxlength="maxQuestionLength"
required
@input="onInput"
@keyup="onTitleChange">
<h3 v-else class="question__header-title" v-text="text" />
@input="onTitleChange">
<h3 v-else class="question__header-title" v-text="computedText" />
<Actions v-if="!readOnly" class="question__header-menu" :force-menu="true">
<ActionCheckbox :checked="mandatory"
@update:checked="onMandatoryChange">
{{ t('forms', 'Mandatory') }}
</ActionCheckbox>
<ActionButton icon="icon-delete" @click="onDelete">
{{ t('forms', 'Delete question') }}
</ActionButton>
@ -59,14 +62,10 @@
</template>
<script>
import axios from '@nextcloud/axios'
import debounce from 'debounce'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import { directive as ClickOutside } from 'v-click-outside'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import ActionCheckbox from '@nextcloud/vue/dist/Components/ActionCheckbox'
export default {
name: 'Question',
@ -78,13 +77,10 @@ export default {
components: {
Actions,
ActionButton,
ActionCheckbox,
},
props: {
id: {
type: Number,
required: true,
},
index: {
type: Number,
required: true,
@ -93,6 +89,10 @@ export default {
type: String,
required: true,
},
mandatory: {
type: Boolean,
required: true,
},
edit: {
type: Boolean,
required: true,
@ -107,11 +107,28 @@ export default {
},
},
computed: {
/**
* Extend text with asterisk if question is mandatory
* @returns {Boolean}
*/
computedText() {
if (this.mandatory) {
return this.text + ' *'
}
return this.text
},
},
methods: {
onInput({ target }) {
onTitleChange({ target }) {
this.$emit('update:text', target.value)
},
onMandatoryChange(mandatory) {
this.$emit('update:mandatory', mandatory)
},
/**
* Enable the edit mode
*/
@ -136,25 +153,6 @@ export default {
onDelete() {
this.$emit('delete')
},
onTitleChange: debounce(function() {
this.saveQuestionProperty('text')
}, 200),
async saveQuestionProperty(key) {
try {
// TODO: add loading status feedback ?
await axios.post(generateUrl('/apps/forms/api/v1/question/update'), {
id: this.id,
keyValuePairs: {
[key]: this[key],
},
})
} catch (error) {
showError(t('forms', 'Error while saving question'))
console.error(error)
}
},
},
}
</script>

View file

@ -24,16 +24,18 @@
<Question
v-bind.sync="$attrs"
:text="text"
:mandatory="mandatory"
:edit.sync="edit"
:max-question-length="maxStringLengths.questionText"
@delete="onDelete"
@update:text="onTitleChange">
@update:text="onTitleChange"
@update:mandatory="onMandatoryChange"
@delete="onDelete">
<div class="question__content">
<!-- TODO: properly choose max length -->
<textarea ref="textarea"
:aria-label="t('forms', 'A long answer for the question “{text}”', { text })"
:placeholder="t('forms', 'Long answer text')"
:required="true /* TODO: implement required option */"
:required="mandatory"
:value="values[0]"
class="question__text"
:maxlength="maxStringLengths.answerText"

View file

@ -22,13 +22,14 @@
<template>
<Question
:id="id"
v-bind.sync="$attrs"
:text="text"
:mandatory="mandatory"
:edit.sync="edit"
:max-question-length="maxStringLengths.questionText"
@delete="onDelete"
@update:text="onTitleChange">
@update:text="onTitleChange"
@update:mandatory="onMandatoryChange"
@delete="onDelete">
<ul class="question__content">
<template v-for="(answer, index) in options">
<li v-if="!edit" :key="answer.id" class="question__item">
@ -96,13 +97,6 @@ export default {
mixins: [QuestionMixin],
props: {
id: {
type: Number,
required: true,
},
},
computed: {
isLastEmpty() {
const value = this.options[this.options.length - 1]
@ -172,10 +166,18 @@ export default {
* @returns {boolean}
*/
isRequired(id) {
// false, if question not mandatory
if (!this.mandatory) {
return false
}
// true for Radiobuttons
if (this.isUnique) {
return true
}
return this.areNoneChecked || this.isChecked(id)
// For checkboxes, only required if no other is checked
return this.areNoneChecked
},
/**

View file

@ -24,16 +24,18 @@
<Question
v-bind.sync="$attrs"
:text="text"
:mandatory="mandatory"
:edit.sync="edit"
:max-question-length="maxStringLengths.questionText"
@delete="onDelete"
@update:text="onTitleChange">
@update:text="onTitleChange"
@update:mandatory="onMandatoryChange"
@delete="onDelete">
<div class="question__content">
<!-- TODO: properly choose max length -->
<input ref="input"
:aria-label="t('forms', 'A short answer for the question “{text}”', { text })"
:placeholder="t('forms', 'Short answer text')"
:required="true /* TODO: implement required option */"
:required="mandatory"
:value="values[0]"
class="question__input"
:maxlength="maxStringLengths.answerText"

View file

@ -18,12 +18,25 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { debounce } from 'debounce'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import Question from '../components/Questions/Question'
export default {
inheritAttrs: false,
props: {
/**
* Question-Id
*/
id: {
type: Number,
required: true,
},
/**
* The question title
*/
@ -32,6 +45,14 @@ export default {
required: true,
},
/**
* Mandatory-Setting
*/
mandatory: {
type: Boolean,
required: true,
},
/**
* The user answers
*/
@ -80,13 +101,24 @@ export default {
methods: {
/**
* Forward the title change to the parent
* Forward the title change to the parent and store to db
*
* @param {string} text the title
*/
onTitleChange(text) {
onTitleChange: debounce(function(text) {
this.$emit('update:text', text)
},
this.saveQuestionProperty('text', text)
}, 200),
/**
* Forward the mandatory change to the parent and store to db
*
* @param {Boolean} mandatoryValue new mandatory Value
*/
onMandatoryChange: debounce(function(mandatoryValue) {
this.$emit('update:mandatory', mandatoryValue)
this.saveQuestionProperty('mandatory', mandatoryValue)
}, 200),
/**
* Forward the answer(s) change to the parent
@ -117,5 +149,20 @@ export default {
}
})
},
async saveQuestionProperty(key, value) {
try {
// TODO: add loading status feedback ?
await axios.post(generateUrl('/apps/forms/api/v1/question/update'), {
id: this.id,
keyValuePairs: {
[key]: value,
},
})
} catch (error) {
showError(t('forms', 'Error while saving question'))
console.error(error)
}
},
},
}

View file

@ -60,14 +60,18 @@
@keyup="onTitleChange">
<label class="hidden-visually" for="form-desc">{{ t('forms', 'Description') }}</label>
<textarea
id="form-desc"
ref="description"
v-model="form.description"
class="form-desc"
:maxlength="maxStringLengths.formDescription"
:placeholder="t('forms', 'Description')"
@change="autoSizeDescription"
@keydown="autoSizeDescription"
@keyup="onDescChange" />
<!-- Only visible if at least one question is marked as mandatory-->
<p v-if="mandatoryUsed" class="form-desc info-mandatory">
* {{ t('forms', 'Mandatory questions') }}
</p>
</header>
<section>
@ -181,6 +185,16 @@ export default {
hasQuestions() {
return this.form.questions && this.form.questions.length === 0
},
/**
* Check if at least one question is mandatory
* @returns {Boolean}
*/
mandatoryUsed() {
return this.form.questions.reduce(
(isUsed, question) => isUsed || question.mandatory
, false)
},
},
watch: {
@ -378,10 +392,11 @@ export default {
header {
display: flex;
flex-direction: column;
margin: 44px;
margin-top: 44px;
margin-bottom: 24px;
#form-title,
#form-desc {
.form-desc {
width: 100%;
padding: 0 16px;
border: none;
@ -395,10 +410,14 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
}
#form-desc {
min-height: 60px;
.form-desc {
padding-bottom: 20px;
resize: none;
}
.info-mandatory {
color: var(--color-text-maxcontrast);
}
}
.empty-content__button {

View file

@ -28,9 +28,13 @@
<h2 id="form-title">
{{ form.title }}
</h2>
<p v-if="!loading && !success" id="form-desc">
<p v-if="!loading && !success" class="form-desc">
{{ form.description }}
</p>
<!-- Only visible if at least one question is marked as mandatory-->
<p v-if="mandatoryUsed && !loading && !success" class="form-desc info-mandatory">
* {{ t('forms', 'Mandatory questions') }}
</p>
</header>
<!-- Questions list -->
@ -121,6 +125,16 @@ export default {
return true
})
},
/**
* Check if at least one question is mandatory
* @returns {Boolean}
*/
mandatoryUsed() {
return this.form.questions.reduce(
(isUsed, question) => isUsed || question.mandatory
, false)
},
},
methods: {
@ -170,9 +184,10 @@ export default {
// Title & description header
header {
margin-top: 44px;
margin-bottom: 24px;
#form-title,
#form-desc {
.form-desc {
width: 100%;
padding: 0 16px;
border: none;
@ -186,10 +201,14 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
}
#form-desc {
min-height: 60px;
.form-desc {
padding-bottom: 20px;
resize: none;
}
.info-mandatory {
color: var(--color-text-maxcontrast);
}
}
form {