mirror of
https://github.com/grey-cat-1908/formaptix-web.git
synced 2024-09-22 19:21:59 +03:00
real http methods support in form edit
This commit is contained in:
parent
08e1934238
commit
915307b232
11 changed files with 230 additions and 102 deletions
|
@ -59,7 +59,7 @@ If you have any questions or want to get involved in the project in any way, you
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
Thanks to my friend Vyacheslav ([flyare](https://github.com/flyare1337)) with frontend and layout design.
|
Thanks to my friend Vyacheslav ([flyare](https://github.com/flyare1337)) for help with frontend and layout design.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,16 @@ import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
import { PhList } from '@phosphor-icons/vue'
|
import { PhList } from '@phosphor-icons/vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const isToggled = ref(false)
|
const isToggled = ref(false)
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
authStore.logout()
|
||||||
|
await router.push('/')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -25,7 +31,7 @@ const isToggled = ref(false)
|
||||||
<button class="header-nickname" type="button" @click="$router.push('/profile')">
|
<button class="header-nickname" type="button" @click="$router.push('/profile')">
|
||||||
{{ authStore.user.username }}
|
{{ authStore.user.username }}
|
||||||
</button>
|
</button>
|
||||||
<button class="default-button header-leave-btn" type="button" @click="authStore.logout">
|
<button class="default-button header-leave-btn" type="button" @click="logout">
|
||||||
Выйти
|
Выйти
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ const props = defineProps({
|
||||||
min_length: null,
|
min_length: null,
|
||||||
max_length: null,
|
max_length: null,
|
||||||
options: [],
|
options: [],
|
||||||
min_values: null,
|
min_values: 1,
|
||||||
max_values: null,
|
max_values: null,
|
||||||
min_value: 1,
|
min_value: 1,
|
||||||
max_value: 5,
|
max_value: 5,
|
||||||
|
|
|
@ -5,11 +5,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="selector">
|
<div class="selector">
|
||||||
<div class="selector-options">
|
<div class="selector-options">
|
||||||
<div
|
<div v-for="(option, index) in options" :key="index" class="selector-option default-button">
|
||||||
v-for="(option, index) in options"
|
|
||||||
:key="index"
|
|
||||||
class="selector-option default-button"
|
|
||||||
>
|
|
||||||
<span>{{ option.label }}</span>
|
<span>{{ option.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="default-card" :class="{ 'form-red': isEmpty }">
|
<div class="default-card" :class="{ 'form-red': isEmpty }">
|
||||||
<div class="view-form-q-title">
|
<div class="view-form-q-title">
|
||||||
<h3 class="form-q-title">{{ label }}</h3>
|
<h3 class="form-q-title">{{ label }} <span style="color: red" v-if="isRequired">*</span></h3>
|
||||||
<p class="form-q-description">{{ description }}</p>
|
<p class="form-q-description">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="imageUrl" :src="imageUrl" alt="image by user" />
|
<img v-if="imageUrl" :src="imageUrl" alt="image by user" />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="default-card" :class="{ 'form-red': error || isEmpty }">
|
<div class="default-card" :class="{ 'form-red': error || isEmpty }">
|
||||||
<div class="view-form-q-title">
|
<div class="view-form-q-title">
|
||||||
<h3 class="form-q-title">{{ label }}</h3>
|
<h3 class="form-q-title">{{ label }} <span style="color: red" v-if="isRequired">*</span></h3>
|
||||||
<p class="form-q-description">{{ description }}</p>
|
<p class="form-q-description">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="imageUrl" :src="imageUrl" alt="image by user" />
|
<img v-if="imageUrl" :src="imageUrl" alt="image by user" />
|
||||||
|
@ -10,7 +10,8 @@
|
||||||
<div class="selector-labels-info">
|
<div class="selector-labels-info">
|
||||||
<PhCaretCircleUpDown :size="23" class="selector-labels-info--sign" />
|
<PhCaretCircleUpDown :size="23" class="selector-labels-info--sign" />
|
||||||
<div class="selector-labels-info--text">
|
<div class="selector-labels-info--text">
|
||||||
Выберите от {{ minValues }} до {{ Math.min(maxValues, options.length) }}
|
Выберите от {{ minValues }} до
|
||||||
|
{{ Math.min(maxValues ?? options.length, options.length) }}
|
||||||
{{
|
{{
|
||||||
normalizeCountForm(Math.min(maxValues, options.length), [
|
normalizeCountForm(Math.min(maxValues, options.length), [
|
||||||
'варианта',
|
'варианта',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="default-card" :class="{ 'form-red': error || isEmpty }">
|
<div class="default-card" :class="{ 'form-red': error || isEmpty }">
|
||||||
<div class="view-form-q-title">
|
<div class="view-form-q-title">
|
||||||
<h3 class="form-q-title">{{ label }}</h3>
|
<h3 class="form-q-title">{{ label }} <span style="color: red" v-if="isRequired">*</span></h3>
|
||||||
<p class="form-q-description">{{ description }}</p>
|
<p class="form-q-description">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="imageUrl" :src="imageUrl" alt="image by user" />
|
<img v-if="imageUrl" :src="imageUrl" alt="image by user" />
|
||||||
|
|
|
@ -24,9 +24,9 @@ const router = createRouter({
|
||||||
component: () => import('@/views/form/Answers.vue')
|
component: () => import('@/views/form/Answers.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/form/create',
|
path: '/form/edit/:id',
|
||||||
name: 'Create Form',
|
name: 'Edit Form',
|
||||||
component: () => import('@/views/form/Create.vue')
|
component: () => import('@/views/form/Edit.vue')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
scrollBehavior(to) {
|
scrollBehavior(to) {
|
||||||
|
|
|
@ -28,12 +28,30 @@ async function deleteForm(index: Number) {
|
||||||
await makeAPIRequest('/form/delete', 'DELETE', { id: form.id }, {}, true)
|
await makeAPIRequest('/form/delete', 'DELETE', { id: form.id }, {}, true)
|
||||||
userForms.value.splice(index, 1)
|
userForms.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createForm() {
|
||||||
|
const formResponse = await makeAPIRequest(
|
||||||
|
'/form/create',
|
||||||
|
'POST',
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
name: 'Новая форма',
|
||||||
|
pages: [{ text: 'Пустая страница', questions: [] }]
|
||||||
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (!formResponse.json || formResponse.status !== 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userForms.value.push(formResponse.json)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="profile">
|
<div class="profile" v-if="authStore.isAuthorized">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="profile-title">Управление формами</h1>
|
<h1 class="profile-title">Управление формами</h1>
|
||||||
|
<button @click="createForm">Создать</button>
|
||||||
<div class="profile-cards">
|
<div class="profile-cards">
|
||||||
<div v-for="(form, index) in userForms" v-if="userForms.length > 0">
|
<div v-for="(form, index) in userForms" v-if="userForms.length > 0">
|
||||||
<div class="profile-card">
|
<div class="profile-card">
|
||||||
|
@ -47,7 +65,7 @@ async function deleteForm(index: Number) {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="profile-card-btn profile-card-btn--update"
|
class="profile-card-btn profile-card-btn--update"
|
||||||
@click="$router.push('/form/update/' + form.id)"
|
@click="$router.push('/form/edit/' + form.id)"
|
||||||
>
|
>
|
||||||
<div class="profile-card-btn-inner"><PhPencil :size="24" /></div>
|
<div class="profile-card-btn-inner"><PhPencil :size="24" /></div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import QuestionEdit from '@/components/edit/QuestionEdit.vue'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import draggable from 'vuedraggable'
|
|
||||||
import TextPreview from '@/components/edit/TextPreview.vue'
|
|
||||||
import SelectorPreview from '@/components/edit/SelectorPreview.vue'
|
|
||||||
import ScalePreview from '@/components/edit/ScalePreview.vue'
|
|
||||||
|
|
||||||
const pages = ref([
|
|
||||||
{ text: 'Тестовый текст 1', questions: [] },
|
|
||||||
{ text: 'лоре ипсум долор сит амет', questions: [] }
|
|
||||||
])
|
|
||||||
const showCreateDialog = ref(false);
|
|
||||||
const showEditDialog = ref(false);
|
|
||||||
|
|
||||||
const currentPage = ref(0);
|
|
||||||
const currentQuestion = ref(0);
|
|
||||||
|
|
||||||
function addNewQuestion(event: any) {
|
|
||||||
pages.value[pages.value.length - 1].questions.push(event)
|
|
||||||
showCreateDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function editQuestion(event: any) {
|
|
||||||
pages.value[currentPage.value].questions[currentQuestion.value] = event;
|
|
||||||
showEditDialog.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function queueQuestionEdit(pageIndex: number, questionIndex: number) {
|
|
||||||
currentPage.value = pageIndex;
|
|
||||||
currentQuestion.value = questionIndex;
|
|
||||||
showEditDialog.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePage(index: number) {
|
|
||||||
let moveTo = index - 1;
|
|
||||||
if (index === 0) {
|
|
||||||
moveTo = index + 1;
|
|
||||||
}
|
|
||||||
pages.value[moveTo].questions.concat(pages.value[index].questions);
|
|
||||||
pages.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteQuestion(pageIndex: number, questionIndex: number) {
|
|
||||||
pages.value[pageIndex].questions.splice(questionIndex, 1);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<QuestionEdit v-if="showCreateDialog" @input="addNewQuestion($event)" />
|
|
||||||
<QuestionEdit v-if="showEditDialog" @input="editQuestion($event)" :data="pages[currentPage].questions[currentQuestion]" />
|
|
||||||
<div class="">
|
|
||||||
<button @click="showCreateDialog = true">Создать вопрос</button>
|
|
||||||
<button @click="pages.push({text: null, questions: []})">Создать страницу</button>
|
|
||||||
<button>Сохранить</button>
|
|
||||||
</div>
|
|
||||||
<div style="user-select: none;">
|
|
||||||
<div v-for="(page, pageIndex) in pages">
|
|
||||||
<div>
|
|
||||||
<h3>Страница {{ pageIndex + 1 }}</h3>
|
|
||||||
<input type="text" placeholder="Описание" v-model="page.text" />
|
|
||||||
<button @click="deletePage(pageIndex)" v-if="pages.length > 1">X</button>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<draggable fallback-class="fallbackStyleClass" ghost-class="ghost" direction="vertical" :force-fallback="true" group="questions" :list="page.questions">
|
|
||||||
<template #item="{element, index}">
|
|
||||||
<div class="default-card" style="cursor: move!important;">
|
|
||||||
<TextPreview v-if="element.question_type === 1" :label="element.label" :description="element.description" :required="element.required" />
|
|
||||||
<SelectorPreview v-if="element.question_type === 2" :label="element.label" :description="element.description" :required="element.required" :options="element.options" />
|
|
||||||
<ScalePreview v-if="element.question_type === 3" :label="element.label" :description="element.description" :required="element.required" :min="element.min_value" :max="element.max_value" :minLabel="element.min_label" :maxLabel="element.max_label"/>
|
|
||||||
<button @click="deleteQuestion(pageIndex, index)">X</button>
|
|
||||||
<button @click="queueQuestionEdit(pageIndex, index)">Редактировать</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.fallbackStyleClass {
|
|
||||||
cursor: move!important;
|
|
||||||
user-select: none!important;
|
|
||||||
}
|
|
||||||
</style>
|
|
192
src/views/form/Edit.vue
Normal file
192
src/views/form/Edit.vue
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import QuestionEdit from '@/components/edit/QuestionEdit.vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
import TextPreview from '@/components/edit/TextPreview.vue'
|
||||||
|
import SelectorPreview from '@/components/edit/SelectorPreview.vue'
|
||||||
|
import ScalePreview from '@/components/edit/ScalePreview.vue'
|
||||||
|
import { makeAPIRequest } from '@/utils/http'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const pages = ref([])
|
||||||
|
const formName = ref()
|
||||||
|
const formId = ref(0)
|
||||||
|
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
|
||||||
|
const currentPage = ref(0)
|
||||||
|
const currentQuestion = ref(0)
|
||||||
|
|
||||||
|
const savedNotify = ref(false)
|
||||||
|
|
||||||
|
function addNewQuestion(event: any) {
|
||||||
|
pages.value[pages.value.length - 1].questions.push(event)
|
||||||
|
showCreateDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function editQuestion(event: any) {
|
||||||
|
pages.value[currentPage.value].questions[currentQuestion.value] = event
|
||||||
|
showEditDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueQuestionEdit(pageIndex: number, questionIndex: number) {
|
||||||
|
currentPage.value = pageIndex
|
||||||
|
currentQuestion.value = questionIndex
|
||||||
|
showEditDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePage(index: number) {
|
||||||
|
let moveTo = index - 1
|
||||||
|
if (index === 0) {
|
||||||
|
moveTo = index + 1
|
||||||
|
}
|
||||||
|
pages.value[moveTo].questions.concat(pages.value[index].questions)
|
||||||
|
pages.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteQuestion(pageIndex: number, questionIndex: number) {
|
||||||
|
pages.value[pageIndex].questions.splice(questionIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSave() {
|
||||||
|
const formResponse = await makeAPIRequest(
|
||||||
|
'/form/edit',
|
||||||
|
'PUT',
|
||||||
|
{ id: formId.value },
|
||||||
|
{ name: formName.value, pages: pages.value },
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (!formResponse.json || formResponse.status !== 200) {
|
||||||
|
await router.push('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
savedNotify.value = true
|
||||||
|
setTimeout(function () {
|
||||||
|
savedNotify.value = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const formResponse = await makeAPIRequest('/form/get', 'GET', { id: Number(route.params.id) })
|
||||||
|
if (!formResponse.json || formResponse.status !== 200 || !authStore.isAuthorized) {
|
||||||
|
await router.push('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pages.value = formResponse.json.data.pages
|
||||||
|
formName.value = formResponse.json.data.name
|
||||||
|
formId.value = Number(route.params.id)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition-group name="notification-appear">
|
||||||
|
<div class="notification" v-if="savedNotify">
|
||||||
|
<div class="save-success">Успешно сохранено!</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
<QuestionEdit v-if="showCreateDialog" @input="addNewQuestion($event)" />
|
||||||
|
<QuestionEdit
|
||||||
|
v-if="showEditDialog"
|
||||||
|
@input="editQuestion($event)"
|
||||||
|
:data="pages[currentPage].questions[currentQuestion]"
|
||||||
|
/>
|
||||||
|
<div class="">
|
||||||
|
<button @click="showCreateDialog = true">Создать вопрос</button>
|
||||||
|
<button @click="pages.push({ text: null, questions: [] })">Создать страницу</button>
|
||||||
|
<button @click="submitSave">Сохранить</button>
|
||||||
|
</div>
|
||||||
|
<div style="user-select: none">
|
||||||
|
<input type="text" v-model="formName" />
|
||||||
|
<div v-for="(page, pageIndex) in pages">
|
||||||
|
<div>
|
||||||
|
<h3>Страница {{ pageIndex + 1 }}</h3>
|
||||||
|
<textarea placeholder="Описание" v-model="page.text" />
|
||||||
|
<button @click="deletePage(pageIndex)" v-if="pages.length > 1">X</button>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<draggable
|
||||||
|
fallback-class="fallbackStyleClass"
|
||||||
|
ghost-class="ghost"
|
||||||
|
direction="vertical"
|
||||||
|
:force-fallback="true"
|
||||||
|
group="questions"
|
||||||
|
:list="page.questions"
|
||||||
|
>
|
||||||
|
<template #item="{ element, index }">
|
||||||
|
<div class="default-card" style="cursor: move !important">
|
||||||
|
<TextPreview
|
||||||
|
v-if="element.question_type === 1"
|
||||||
|
:label="element.label"
|
||||||
|
:description="element.description"
|
||||||
|
:required="element.required"
|
||||||
|
/>
|
||||||
|
<SelectorPreview
|
||||||
|
v-if="element.question_type === 2"
|
||||||
|
:label="element.label"
|
||||||
|
:description="element.description"
|
||||||
|
:required="element.required"
|
||||||
|
:options="element.options"
|
||||||
|
/>
|
||||||
|
<ScalePreview
|
||||||
|
v-if="element.question_type === 3"
|
||||||
|
:label="element.label"
|
||||||
|
:description="element.description"
|
||||||
|
:required="element.required"
|
||||||
|
:min="element.min_value"
|
||||||
|
:max="element.max_value"
|
||||||
|
:minLabel="element.min_label"
|
||||||
|
:maxLabel="element.max_label"
|
||||||
|
/>
|
||||||
|
<button @click="deleteQuestion(pageIndex, index)">X</button>
|
||||||
|
<button @click="queueQuestionEdit(pageIndex, index)">Редактировать</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.fallbackStyleClass {
|
||||||
|
cursor: move !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
z-index: 5678;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-success {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-appear-enter-from,
|
||||||
|
.notification-appear-leave-to {
|
||||||
|
transform: translateX(10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-appear-enter-active,
|
||||||
|
.notification-appear-leave-active {
|
||||||
|
transition-timing-function: ease;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue