real http methods support in form edit

This commit is contained in:
grey-cat-1908 2024-09-12 13:15:12 +03:00
parent 08e1934238
commit 915307b232
11 changed files with 230 additions and 102 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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" />

View file

@ -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), [
'варианта', 'варианта',

View file

@ -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" />

View file

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

View file

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

View file

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