Fix forms change saving & add question scroll

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2020-04-21 18:03:43 +02:00 committed by Jonas Rittershofer
parent 2cd445d201
commit ebf66d7e82
7 changed files with 70 additions and 94 deletions

View file

@ -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'],
]

View file

@ -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
},

View file

@ -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()
}
})
},
},
}

View file

@ -21,6 +21,10 @@
export default {
props: {
hash: {
type: String,
default: '',
},
form: {
type: Object,
required: true,

View file

@ -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 },
},
],
})

View file

@ -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 }

View file

@ -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)