Fix forms change saving & add question scroll
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
2cd445d201
commit
ebf66d7e82
|
@ -34,18 +34,18 @@ return [
|
|||
['name' => 'page#goto_form', 'url' => '/{hash}', 'verb' => 'GET'],
|
||||
['name' => 'page#insert_submission', 'url' => '/insert/submission', 'verb' => 'POST'],
|
||||
|
||||
['name' => 'api#getForms', 'url' => 'api/v1/forms', 'verb' => 'GET'],
|
||||
['name' => 'api#newForm', 'url' => 'api/v1/form', 'verb' => 'POST'],
|
||||
['name' => 'api#getForm', 'url' => 'api/v1/form/{id}', 'verb' => 'GET'],
|
||||
['name' => 'api#updateForm', 'url' => 'api/v1/form/update/', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteForm', 'url' => 'api/v1/form/{id}', 'verb' => 'DELETE'],
|
||||
['name' => 'api#updateQuestion', 'url' => 'api/v1/question/update', 'verb' => 'POST'],
|
||||
['name' => 'api#reorderQuestions', 'url' => 'api/v1/question/reorder', 'verb' => 'POST'],
|
||||
['name' => 'api#newQuestion', 'url' => 'api/v1/question', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteQuestion', 'url' => 'api/v1/question/{id}', 'verb' => 'DELETE'],
|
||||
['name' => 'api#newOption', 'url' => 'api/v1/option', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteOption', 'url' => 'api/v1/option/{id}', 'verb' => 'DELETE'],
|
||||
['name' => 'api#getSubmissions', 'url' => 'api/v1/submissions/{hash}', 'verb' => 'GET'],
|
||||
['name' => 'api#getForms', 'url' => '/api/v1/forms', 'verb' => 'GET'],
|
||||
['name' => 'api#newForm', 'url' => '/api/v1/form', 'verb' => 'POST'],
|
||||
['name' => 'api#getForm', 'url' => '/api/v1/form/{id}', 'verb' => 'GET'],
|
||||
['name' => 'api#updateForm', 'url' => '/api/v1/form/update/', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteForm', 'url' => '/api/v1/form/{id}', 'verb' => 'DELETE'],
|
||||
['name' => 'api#updateQuestion', 'url' => '/api/v1/question/update', 'verb' => 'POST'],
|
||||
['name' => 'api#reorderQuestions', 'url' => '/api/v1/question/reorder', 'verb' => 'POST'],
|
||||
['name' => 'api#newQuestion', 'url' => '/api/v1/question', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteQuestion', 'url' => '/api/v1/question/{id}', 'verb' => 'DELETE'],
|
||||
['name' => 'api#newOption', 'url' => '/api/v1/option', 'verb' => 'POST'],
|
||||
['name' => 'api#deleteOption', 'url' => '/api/v1/option/{id}', 'verb' => 'DELETE'],
|
||||
['name' => 'api#getSubmissions', 'url' => '/api/v1/submissions/{hash}', 'verb' => 'GET'],
|
||||
|
||||
['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'],
|
||||
]
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<Content app-name="forms">
|
||||
<AppNavigation>
|
||||
<AppNavigationNew button-class="icon-add" :text="t('forms', 'New form')" @click="onNewForm" />
|
||||
<AppNavigationForm v-for="form in formattedForms"
|
||||
<AppNavigationForm v-for="form in forms"
|
||||
:key="form.id"
|
||||
:form="form"
|
||||
@delete="onDeleteForm" />
|
||||
|
@ -71,8 +71,6 @@ import Content from '@nextcloud/vue/dist/Components/Content'
|
|||
import AppNavigationForm from './components/AppNavigationForm'
|
||||
import EmptyContent from './components/EmptyContent'
|
||||
|
||||
import { formatForm } from './utils/FormsUtils'
|
||||
|
||||
export default {
|
||||
name: 'Forms',
|
||||
|
||||
|
@ -97,10 +95,6 @@ export default {
|
|||
return this.forms && this.forms.length === 0
|
||||
},
|
||||
|
||||
formattedForms() {
|
||||
return this.forms.map(formatForm)
|
||||
},
|
||||
|
||||
hash() {
|
||||
return this.$route.params.hash
|
||||
},
|
||||
|
|
|
@ -94,5 +94,19 @@ export default {
|
|||
onDelete() {
|
||||
this.$emit('delete')
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus the first focusable element
|
||||
*/
|
||||
focus() {
|
||||
this.edit = true
|
||||
this.$el.scrollIntoView({ behavior: 'smooth' })
|
||||
this.$nextTick(() => {
|
||||
const title = this.$el.querySelector('.question__header-title')
|
||||
if (title) {
|
||||
title.select()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
|
||||
export default {
|
||||
props: {
|
||||
hash: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
|
@ -56,7 +56,7 @@ export default new Router({
|
|||
{
|
||||
path: '/:hash',
|
||||
name: 'fill',
|
||||
props: true,
|
||||
props: { default: true },
|
||||
},
|
||||
{
|
||||
path: '/:hash/edit',
|
||||
|
@ -65,13 +65,13 @@ export default new Router({
|
|||
sidebar: Sidebar,
|
||||
},
|
||||
name: 'edit',
|
||||
props: true,
|
||||
props: { default: true },
|
||||
},
|
||||
{
|
||||
path: '/:hash/results',
|
||||
component: Results,
|
||||
name: 'results',
|
||||
props: true,
|
||||
props: { default: true },
|
||||
},
|
||||
{
|
||||
path: '/:hash/clone',
|
||||
|
@ -80,7 +80,7 @@ export default new Router({
|
|||
sidebar: Sidebar,
|
||||
},
|
||||
name: 'clone',
|
||||
props: true,
|
||||
props: { default: true },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a form object prior to forms v2.0
|
||||
*
|
||||
* @param {Object} form the form raw object
|
||||
* @returns {Object} properly formatted form object
|
||||
*/
|
||||
const formatForm = function(form) {
|
||||
// clone form
|
||||
const newForm = Object.assign({}, form)
|
||||
|
||||
// cleanup
|
||||
delete newForm.event
|
||||
return newForm
|
||||
}
|
||||
|
||||
export { formatForm }
|
|
@ -27,7 +27,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<AppContent v-if="loadingForm">
|
||||
<AppContent v-if="isLoadingForm">
|
||||
<EmptyContent icon="icon-loading">
|
||||
{{ t('forms', 'Loading form “{title}”', { title: form.title }) }}
|
||||
</EmptyContent>
|
||||
|
@ -76,10 +76,11 @@
|
|||
v-tooltip="t('forms', 'Add a question to this form')"
|
||||
:aria-label="t('forms', 'Add a question to this form')"
|
||||
:open.sync="questionMenuOpened"
|
||||
:default-icon="loadingQuestions ? 'icon-loading-small' : 'icon-add-white'">
|
||||
:default-icon="isLoadingQuestions ? 'icon-loading-small' : 'icon-add-white'">
|
||||
<ActionButton v-for="(answer, type) in answerTypes"
|
||||
:key="answer.label"
|
||||
:disabled="loadingQuestions"
|
||||
:close-after-click="true"
|
||||
:disabled="isLoadingQuestions"
|
||||
:icon="answer.icon"
|
||||
class="question-toolbar__question"
|
||||
@click="addQuestion(type)">
|
||||
|
@ -104,9 +105,10 @@
|
|||
<Draggable v-model="form.questions"
|
||||
:animation="200"
|
||||
tag="ul"
|
||||
@start="dragging = true"
|
||||
@end="dragging = false">
|
||||
<Questions :is="answerTypes[question.type].component"
|
||||
@start="isDragging = true"
|
||||
@end="isDragging = false">
|
||||
<Questions ref="questions"
|
||||
:is="answerTypes[question.type].component"
|
||||
v-for="(question, index) in form.questions"
|
||||
:key="question.id"
|
||||
:model="answerTypes[question.type]"
|
||||
|
@ -165,10 +167,13 @@ export default {
|
|||
return {
|
||||
questionMenuOpened: false,
|
||||
answerTypes,
|
||||
loadingForm: true,
|
||||
loadingQuestions: false,
|
||||
|
||||
// Various states
|
||||
isLoadingForm: true,
|
||||
isLoadingQuestions: false,
|
||||
errorForm: false,
|
||||
dragging: false,
|
||||
|
||||
isDragging: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -186,15 +191,10 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
form: {
|
||||
deep: true,
|
||||
handler: function(newForm, oldForm) {
|
||||
if (newForm.hash === oldForm.hash) {
|
||||
this.debounceSaveForm()
|
||||
} else {
|
||||
this.fetchFullForm(newForm.id)
|
||||
}
|
||||
},
|
||||
// Fetch full form on change
|
||||
hash() {
|
||||
// TODO: cancel previous request if not done
|
||||
this.fetchFullForm(this.form.id)
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -213,7 +213,7 @@ export default {
|
|||
* @param {number} id the unique form hash
|
||||
*/
|
||||
async fetchFullForm(id) {
|
||||
this.loadingForm = true
|
||||
this.isLoadingForm = true
|
||||
console.debug('Loading form', id)
|
||||
|
||||
try {
|
||||
|
@ -223,14 +223,10 @@ export default {
|
|||
console.error(error)
|
||||
this.errorForm = true
|
||||
} finally {
|
||||
this.loadingForm = false
|
||||
this.isLoadingForm = false
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit(e) {
|
||||
this.saveForm()
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new question to the current form
|
||||
*
|
||||
|
@ -238,7 +234,7 @@ export default {
|
|||
*/
|
||||
async addQuestion(type) {
|
||||
const text = t('forms', 'New question')
|
||||
this.loadingQuestions = true
|
||||
this.isLoadingQuestions = true
|
||||
|
||||
try {
|
||||
const response = await axios.post(generateUrl('/apps/forms/api/v1/question'), {
|
||||
|
@ -255,23 +251,28 @@ export default {
|
|||
answers: [],
|
||||
}, question))
|
||||
|
||||
// Focus newly added question
|
||||
this.$nextTick(() => {
|
||||
const lastQuestion = this.$refs.questions[this.$refs.questions.length - 1]
|
||||
lastQuestion.focus()
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
showError(t('forms', 'There was an error while adding the new question'))
|
||||
} finally {
|
||||
this.loadingQuestions = false
|
||||
this.isLoadingQuestions = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a question
|
||||
*
|
||||
* @param {Object} question the question to delete
|
||||
* @param {number} question.id the question id to delete
|
||||
*/
|
||||
async deleteQuestion(question) {
|
||||
console.info(question)
|
||||
const id = question.id
|
||||
this.loadingQuestions = true
|
||||
async deleteQuestion({ id }) {
|
||||
this.isLoadingQuestions = true
|
||||
|
||||
try {
|
||||
await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id }))
|
||||
|
@ -281,7 +282,7 @@ export default {
|
|||
console.error(error)
|
||||
showError(t('forms', 'There was an error while removing the question'))
|
||||
} finally {
|
||||
this.loadingQuestions = false
|
||||
this.isLoadingQuestions = false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -313,16 +314,16 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Forms saving handlers
|
||||
* Save form on submit
|
||||
*/
|
||||
debounceSaveForm: debounce(function() {
|
||||
onSubmit: debounce(function() {
|
||||
this.saveForm()
|
||||
}, 200),
|
||||
|
||||
async saveForm() {
|
||||
try {
|
||||
// TODO: add loading status feedback ?
|
||||
await axios.post(OC.generateUrl('/apps/forms/write/form'), this.form)
|
||||
await axios.post(OC.generateUrl('/apps/forms/update/form'), this.form)
|
||||
} catch (error) {
|
||||
showError(t('forms', 'Error on saving form, see console'))
|
||||
console.error(error)
|
||||
|
|
Loading…
Reference in a new issue