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#goto_form', 'url' => '/{hash}', 'verb' => 'GET'],
|
||||||
['name' => 'page#insert_submission', 'url' => '/insert/submission', 'verb' => 'POST'],
|
['name' => 'page#insert_submission', 'url' => '/insert/submission', 'verb' => 'POST'],
|
||||||
|
|
||||||
['name' => 'api#getForms', 'url' => 'api/v1/forms', 'verb' => 'GET'],
|
['name' => 'api#getForms', 'url' => '/api/v1/forms', 'verb' => 'GET'],
|
||||||
['name' => 'api#newForm', 'url' => 'api/v1/form', 'verb' => 'POST'],
|
['name' => 'api#newForm', 'url' => '/api/v1/form', 'verb' => 'POST'],
|
||||||
['name' => 'api#getForm', 'url' => 'api/v1/form/{id}', 'verb' => 'GET'],
|
['name' => 'api#getForm', 'url' => '/api/v1/form/{id}', 'verb' => 'GET'],
|
||||||
['name' => 'api#updateForm', 'url' => 'api/v1/form/update/', 'verb' => 'POST'],
|
['name' => 'api#updateForm', 'url' => '/api/v1/form/update/', 'verb' => 'POST'],
|
||||||
['name' => 'api#deleteForm', 'url' => 'api/v1/form/{id}', 'verb' => 'DELETE'],
|
['name' => 'api#deleteForm', 'url' => '/api/v1/form/{id}', 'verb' => 'DELETE'],
|
||||||
['name' => 'api#updateQuestion', 'url' => 'api/v1/question/update', 'verb' => 'POST'],
|
['name' => 'api#updateQuestion', 'url' => '/api/v1/question/update', 'verb' => 'POST'],
|
||||||
['name' => 'api#reorderQuestions', 'url' => 'api/v1/question/reorder', 'verb' => 'POST'],
|
['name' => 'api#reorderQuestions', 'url' => '/api/v1/question/reorder', 'verb' => 'POST'],
|
||||||
['name' => 'api#newQuestion', 'url' => 'api/v1/question', 'verb' => 'POST'],
|
['name' => 'api#newQuestion', 'url' => '/api/v1/question', 'verb' => 'POST'],
|
||||||
['name' => 'api#deleteQuestion', 'url' => 'api/v1/question/{id}', 'verb' => 'DELETE'],
|
['name' => 'api#deleteQuestion', 'url' => '/api/v1/question/{id}', 'verb' => 'DELETE'],
|
||||||
['name' => 'api#newOption', 'url' => 'api/v1/option', 'verb' => 'POST'],
|
['name' => 'api#newOption', 'url' => '/api/v1/option', 'verb' => 'POST'],
|
||||||
['name' => 'api#deleteOption', 'url' => 'api/v1/option/{id}', 'verb' => 'DELETE'],
|
['name' => 'api#deleteOption', 'url' => '/api/v1/option/{id}', 'verb' => 'DELETE'],
|
||||||
['name' => 'api#getSubmissions', 'url' => 'api/v1/submissions/{hash}', 'verb' => 'GET'],
|
['name' => 'api#getSubmissions', 'url' => '/api/v1/submissions/{hash}', 'verb' => 'GET'],
|
||||||
|
|
||||||
['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'],
|
['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'],
|
||||||
]
|
]
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<Content app-name="forms">
|
<Content app-name="forms">
|
||||||
<AppNavigation>
|
<AppNavigation>
|
||||||
<AppNavigationNew button-class="icon-add" :text="t('forms', 'New form')" @click="onNewForm" />
|
<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"
|
:key="form.id"
|
||||||
:form="form"
|
:form="form"
|
||||||
@delete="onDeleteForm" />
|
@delete="onDeleteForm" />
|
||||||
|
@ -71,8 +71,6 @@ import Content from '@nextcloud/vue/dist/Components/Content'
|
||||||
import AppNavigationForm from './components/AppNavigationForm'
|
import AppNavigationForm from './components/AppNavigationForm'
|
||||||
import EmptyContent from './components/EmptyContent'
|
import EmptyContent from './components/EmptyContent'
|
||||||
|
|
||||||
import { formatForm } from './utils/FormsUtils'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Forms',
|
name: 'Forms',
|
||||||
|
|
||||||
|
@ -97,10 +95,6 @@ export default {
|
||||||
return this.forms && this.forms.length === 0
|
return this.forms && this.forms.length === 0
|
||||||
},
|
},
|
||||||
|
|
||||||
formattedForms() {
|
|
||||||
return this.forms.map(formatForm)
|
|
||||||
},
|
|
||||||
|
|
||||||
hash() {
|
hash() {
|
||||||
return this.$route.params.hash
|
return this.$route.params.hash
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,5 +94,19 @@ export default {
|
||||||
onDelete() {
|
onDelete() {
|
||||||
this.$emit('delete')
|
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 {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
hash: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
form: {
|
form: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default new Router({
|
||||||
{
|
{
|
||||||
path: '/:hash',
|
path: '/:hash',
|
||||||
name: 'fill',
|
name: 'fill',
|
||||||
props: true,
|
props: { default: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:hash/edit',
|
path: '/:hash/edit',
|
||||||
|
@ -65,13 +65,13 @@ export default new Router({
|
||||||
sidebar: Sidebar,
|
sidebar: Sidebar,
|
||||||
},
|
},
|
||||||
name: 'edit',
|
name: 'edit',
|
||||||
props: true,
|
props: { default: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:hash/results',
|
path: '/:hash/results',
|
||||||
component: Results,
|
component: Results,
|
||||||
name: 'results',
|
name: 'results',
|
||||||
props: true,
|
props: { default: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:hash/clone',
|
path: '/:hash/clone',
|
||||||
|
@ -80,7 +80,7 @@ export default new Router({
|
||||||
sidebar: Sidebar,
|
sidebar: Sidebar,
|
||||||
},
|
},
|
||||||
name: 'clone',
|
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>
|
<template>
|
||||||
<AppContent v-if="loadingForm">
|
<AppContent v-if="isLoadingForm">
|
||||||
<EmptyContent icon="icon-loading">
|
<EmptyContent icon="icon-loading">
|
||||||
{{ t('forms', 'Loading form “{title}”', { title: form.title }) }}
|
{{ t('forms', 'Loading form “{title}”', { title: form.title }) }}
|
||||||
</EmptyContent>
|
</EmptyContent>
|
||||||
|
@ -76,10 +76,11 @@
|
||||||
v-tooltip="t('forms', 'Add a question to this form')"
|
v-tooltip="t('forms', 'Add a question to this form')"
|
||||||
:aria-label="t('forms', 'Add a question to this form')"
|
:aria-label="t('forms', 'Add a question to this form')"
|
||||||
:open.sync="questionMenuOpened"
|
: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"
|
<ActionButton v-for="(answer, type) in answerTypes"
|
||||||
:key="answer.label"
|
:key="answer.label"
|
||||||
:disabled="loadingQuestions"
|
:close-after-click="true"
|
||||||
|
:disabled="isLoadingQuestions"
|
||||||
:icon="answer.icon"
|
:icon="answer.icon"
|
||||||
class="question-toolbar__question"
|
class="question-toolbar__question"
|
||||||
@click="addQuestion(type)">
|
@click="addQuestion(type)">
|
||||||
|
@ -104,9 +105,10 @@
|
||||||
<Draggable v-model="form.questions"
|
<Draggable v-model="form.questions"
|
||||||
:animation="200"
|
:animation="200"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
@start="dragging = true"
|
@start="isDragging = true"
|
||||||
@end="dragging = false">
|
@end="isDragging = false">
|
||||||
<Questions :is="answerTypes[question.type].component"
|
<Questions ref="questions"
|
||||||
|
:is="answerTypes[question.type].component"
|
||||||
v-for="(question, index) in form.questions"
|
v-for="(question, index) in form.questions"
|
||||||
:key="question.id"
|
:key="question.id"
|
||||||
:model="answerTypes[question.type]"
|
:model="answerTypes[question.type]"
|
||||||
|
@ -165,10 +167,13 @@ export default {
|
||||||
return {
|
return {
|
||||||
questionMenuOpened: false,
|
questionMenuOpened: false,
|
||||||
answerTypes,
|
answerTypes,
|
||||||
loadingForm: true,
|
|
||||||
loadingQuestions: false,
|
// Various states
|
||||||
|
isLoadingForm: true,
|
||||||
|
isLoadingQuestions: false,
|
||||||
errorForm: false,
|
errorForm: false,
|
||||||
dragging: false,
|
|
||||||
|
isDragging: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -186,15 +191,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
form: {
|
// Fetch full form on change
|
||||||
deep: true,
|
hash() {
|
||||||
handler: function(newForm, oldForm) {
|
// TODO: cancel previous request if not done
|
||||||
if (newForm.hash === oldForm.hash) {
|
this.fetchFullForm(this.form.id)
|
||||||
this.debounceSaveForm()
|
|
||||||
} else {
|
|
||||||
this.fetchFullForm(newForm.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ export default {
|
||||||
* @param {number} id the unique form hash
|
* @param {number} id the unique form hash
|
||||||
*/
|
*/
|
||||||
async fetchFullForm(id) {
|
async fetchFullForm(id) {
|
||||||
this.loadingForm = true
|
this.isLoadingForm = true
|
||||||
console.debug('Loading form', id)
|
console.debug('Loading form', id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -223,14 +223,10 @@ export default {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
this.errorForm = true
|
this.errorForm = true
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingForm = false
|
this.isLoadingForm = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit(e) {
|
|
||||||
this.saveForm()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new question to the current form
|
* Add a new question to the current form
|
||||||
*
|
*
|
||||||
|
@ -238,7 +234,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
async addQuestion(type) {
|
async addQuestion(type) {
|
||||||
const text = t('forms', 'New question')
|
const text = t('forms', 'New question')
|
||||||
this.loadingQuestions = true
|
this.isLoadingQuestions = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(generateUrl('/apps/forms/api/v1/question'), {
|
const response = await axios.post(generateUrl('/apps/forms/api/v1/question'), {
|
||||||
|
@ -255,23 +251,28 @@ export default {
|
||||||
answers: [],
|
answers: [],
|
||||||
}, question))
|
}, question))
|
||||||
|
|
||||||
|
// Focus newly added question
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const lastQuestion = this.$refs.questions[this.$refs.questions.length - 1]
|
||||||
|
lastQuestion.focus()
|
||||||
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showError(t('forms', 'There was an error while adding the new question'))
|
showError(t('forms', 'There was an error while adding the new question'))
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingQuestions = false
|
this.isLoadingQuestions = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a question
|
* Delete a question
|
||||||
|
*
|
||||||
* @param {Object} question the question to delete
|
* @param {Object} question the question to delete
|
||||||
* @param {number} question.id the question id to delete
|
* @param {number} question.id the question id to delete
|
||||||
*/
|
*/
|
||||||
async deleteQuestion(question) {
|
async deleteQuestion({ id }) {
|
||||||
console.info(question)
|
this.isLoadingQuestions = true
|
||||||
const id = question.id
|
|
||||||
this.loadingQuestions = true
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id }))
|
await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id }))
|
||||||
|
@ -281,7 +282,7 @@ export default {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
showError(t('forms', 'There was an error while removing the question'))
|
showError(t('forms', 'There was an error while removing the question'))
|
||||||
} finally {
|
} 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()
|
this.saveForm()
|
||||||
}, 200),
|
}, 200),
|
||||||
|
|
||||||
async saveForm() {
|
async saveForm() {
|
||||||
try {
|
try {
|
||||||
// TODO: add loading status feedback ?
|
// 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) {
|
} catch (error) {
|
||||||
showError(t('forms', 'Error on saving form, see console'))
|
showError(t('forms', 'Error on saving form, see console'))
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
Loading…
Reference in a new issue