mirror of
https://github.com/pretix/pretix.git
synced 2026-09-30 19:04:43 +00:00
Editor UX rework
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import QuestionnaireElement from './QuestionnaireElement.vue';
|
||||
import * as api from './api';
|
||||
import { Questionnaire } from './model';
|
||||
import { i18n_any, sort, numericComp, groupBy } from './helper';
|
||||
import {i18n_any, sort, numericComp, groupBy, SYSTEM_DATAFIELDS} from './helper';
|
||||
import { gettext } from './gettextstub';
|
||||
import { ref } from 'vue';
|
||||
import { SlickList, SlickItem } from 'vue-slicksort';
|
||||
@@ -21,17 +21,31 @@ const all_questionnaires: (Omit<Questionnaire, 'id'> & { _new_id?: number, id?:
|
||||
const order_questionnaires = ref(all_questionnaires.filter(q => q.type.startsWith('O')));
|
||||
const position_questionnaires = ref(all_questionnaires.filter(q => q.type.startsWith('P')));
|
||||
const order_datafields = ref(await api.getDatafields('O'));
|
||||
const position_datafields = ref(await api.getDatafields('P'));
|
||||
const position_datafields = ref((await api.getDatafields('P')).concat(Object.values(SYSTEM_DATAFIELDS)));
|
||||
|
||||
function saveQuestionnaire(questionnaire) {
|
||||
let result;
|
||||
questionnaire._loading = true
|
||||
if (questionnaire.id) {
|
||||
return api.updateQuestionnaire(questionnaire.id, questionnaire);
|
||||
result = api.updateQuestionnaire(questionnaire.id, questionnaire);
|
||||
} else {
|
||||
return api.createQuestionnaire(questionnaire).then(d => {
|
||||
result = api.createQuestionnaire(questionnaire).then(d => {
|
||||
questionnaire.id = d.id;
|
||||
return d;
|
||||
});
|
||||
}
|
||||
result = result.then(d => {
|
||||
console.log(questionnaire, 'ok')
|
||||
questionnaire._err_mes = null
|
||||
questionnaire._loading = false
|
||||
return d;
|
||||
}, err => {
|
||||
console.log(questionnaire, 'err:', err)
|
||||
questionnaire._err_mes = err
|
||||
questionnaire._loading = false
|
||||
return err;
|
||||
})
|
||||
return result;
|
||||
}
|
||||
function addPositionQuestionnaire () {
|
||||
position_questionnaires.value.push({
|
||||
@@ -90,16 +104,24 @@ const preview_mode = ref(false)
|
||||
<style>
|
||||
.hidden-questionnaire { opacity: 0.3; }
|
||||
|
||||
.questionnaires-list { margin-right: 180px; }
|
||||
.questionnaires-list {
|
||||
padding: 20px 190px 20px 0;
|
||||
background: linear-gradient(to right, #fff 0, #fff calc(100% - 172px), #ddd calc(100% - 171px), #f6f2f8 calc(100% - 171px), #fff calc(100% - 100px), #fff 100%);
|
||||
}
|
||||
.question-edit-buttons { float:right; }
|
||||
.question-edit-buttons div { position: absolute; margin-left: 10px; min-width: 100px; }
|
||||
.question-edit-buttons div { position: absolute; margin-left: 10px; min-width: 100px; }
|
||||
.question-edit-buttons button { }
|
||||
.form-group { margin-bottom: 30px }
|
||||
|
||||
.questionnaires-editor.product-selected .panel .panel-heading {}
|
||||
.questionnaires-editor.product-selected .questionnaire-panel .questionnaire-panel-heading {}
|
||||
|
||||
.questionnaires-editor.product-selected .panel { margin-bottom: 0; border: 0 none; border-bottom: 1px solid #ddd; box-shadow: none; border-radius: 0; }
|
||||
.questionnaires-editor.product-selected .panel .panel-heading { font-style: italic; background: white; border: 0 none; }
|
||||
.questionnaires-editor:not(.preview-mode) .questionnaire-panel .questionnaire-panel-heading .editor-row { border-top: 1px solid rgb(175 175 175 / 0.3); }
|
||||
.questionnaires-editor:not(.preview-mode) .questionnaire-panel[open] .questionnaire-panel-heading .editor-row { border-bottom: 1px solid rgb(216 216 216 / 0.3); }
|
||||
|
||||
.questionnaires-editor.preview-mode .questionnaire-panel .questionnaire-panel-heading { color: #888; margin-top: 5px; border-top: 1px dashed rgb(175 175 175 / 0.3); border-bottom: 1px dashed rgb(216 216 216 / 0.3); font-style: italic; padding: 0; }
|
||||
|
||||
.questionnaires-editor .editor-row:not(:hover) .btn.btn-default,
|
||||
.questionnaires-editor .editor-action-row:not(:hover) .btn.btn-default{ box-shadow: 0 0 0 0 #eeeeee; background: transparent; color: #555; }
|
||||
|
||||
.filter-row { background: #f8e6ff; border: 1px solid #e3cbed; padding: 10px; }
|
||||
|
||||
@@ -108,32 +130,69 @@ const preview_mode = ref(false)
|
||||
.dependency-info > span { }
|
||||
|
||||
.category-header { margin: 8px 0 -5px 0; font-weight: bold; color: #737373; }
|
||||
|
||||
.editor-row { display: flex; width: calc(100% + 190px); background: transparent; }
|
||||
.editor-row:hover { background: rgba(255,233,244,0.3); }
|
||||
.editor-preview-area { flex: 1; padding-left: 10px; padding-right: 40px; }
|
||||
.editor-action-area { width: 160px; padding-left: 20px; }
|
||||
.questionnaire-panel-heading .editor-preview-area { padding-block: 11px; }
|
||||
.questionnaire-panel-heading .editor-action-area { padding-block: 5px; }
|
||||
.questionnaire-panel-body .editor-preview-area, .questionnaire-panel-body .editor-action-area { padding-block: 10px; }
|
||||
.preview-mode .editor-preview-area { padding-right: 50px; }
|
||||
|
||||
.questionnaires-editor.questionnaires-editor .form-group { margin-bottom: 0px; }
|
||||
|
||||
.questionnaire-panel .questionnaire-panel-heading .editor-preview-area::before {
|
||||
margin-top: -.5em;
|
||||
content: "";
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 150ms ease-in 0s;
|
||||
}
|
||||
.questionnaire-panel[open] .questionnaire-panel-heading .editor-preview-area::before {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.questionnaires-list > .editor-action-row { border-top: 1px solid rgb(175 175 175 / 0.3); }
|
||||
.editor-action-row { padding-top: 10px; width: calc(100% + 190px); padding-right: 170px }
|
||||
</style>
|
||||
<template>
|
||||
<div class="questionnaires-editor">
|
||||
<p class="filter-row">
|
||||
<div class="questionnaires-editor" v-if="!preview_mode">
|
||||
<div class="filter-row">
|
||||
Order questionnaires
|
||||
</p>
|
||||
</div>
|
||||
<div class="questionnaires-list">
|
||||
<SlickList axis="y" v-model:list="order_questionnaires" useDragHandle appendTo="#orderQuestionnaireListParent" id="orderQuestionnaireListParent">
|
||||
<SlickList axis="y" v-model:list="order_questionnaires" useDragHandle appendTo="#orderQuestionnaireListParent" id="orderQuestionnaireListParent" @update:list="saveData()">
|
||||
<SlickItem v-for="(questionnaire, index) in order_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
|
||||
<QuestionnaireElement
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="order_datafields"
|
||||
:grouped_items="null"
|
||||
:selected_product="null"
|
||||
:preview_mode="false" />
|
||||
:preview_mode="false"
|
||||
@update="saveQuestionnaire(questionnaire)" />
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
<div class="editor-action-row form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-push-3">
|
||||
<button class="btn btn-default" @click="addOrderQuestionnaire()"><i class="fa fa-plus"></i> {{ gettext('New questionnaire') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<button class="btn btn-default" @click="addOrderQuestionnaire()"><i class="fa fa-plus"></i> {{ gettext('New questionnaire') }}</button>
|
||||
<button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> {{ gettext('Save') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div :class="`questionnaires-editor ${selected_product ? 'product-selected':''}`">
|
||||
<p class="filter-row">
|
||||
<div :class="`questionnaires-editor ${selected_product ? 'product-selected':''} ${preview_mode ? 'preview-mode':''}`">
|
||||
<div class="filter-row" v-if="preview_mode">
|
||||
<button class="btn btn-default" @click="preview_mode = false"><span class="fa fa-chevron-left"></span> {{ gettext('Back') }}</button>
|
||||
Previewing questionnaires for product {{ selected_product }}
|
||||
</div>
|
||||
<div class="filter-row" v-else>
|
||||
Questionnaires for product:
|
||||
<select v-model="selected_product">
|
||||
<option value="">(all)</option>
|
||||
@@ -144,31 +203,46 @@ const preview_mode = ref(false)
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<label><input type="checkbox" v-model="preview_mode" :disabled="!selected_product"> {{ gettext('Preview') }}</label>
|
||||
</p>
|
||||
<div class="questionnaires-list" v-if="preview_mode && selected_product">
|
||||
<QuestionnaireElement v-for="(questionnaire, index) in position_questionnaires.filter(q => q.items.indexOf(selected_product as any) !== -1)"
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="position_datafields"
|
||||
:grouped_items="grouped_items"
|
||||
:selected_product="selected_product"
|
||||
:preview_mode="true"/>
|
||||
<button class="btn btn-default" @click="preview_mode = true" :disabled="!selected_product"> {{ gettext('Preview') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="questionnaires-list" v-if="preview_mode && selected_product">
|
||||
<details class="panel panel-default details-open" open>
|
||||
<summary class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<strong>Product {{ selected_product }}</strong>
|
||||
</h4>
|
||||
</summary>
|
||||
|
||||
<QuestionnaireElement v-for="(questionnaire, index) in position_questionnaires.filter(q => q.items.indexOf(selected_product as any) !== -1)"
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="position_datafields"
|
||||
:grouped_items="grouped_items"
|
||||
:selected_product="selected_product"
|
||||
:preview_mode="true"
|
||||
@update="saveQuestionnaire(questionnaire)"/>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="questionnaires-list" v-else>
|
||||
<SlickList axis="y" v-model:list="position_questionnaires" useDragHandle appendTo="#questionnaireListParent" id="questionnaireListParent">
|
||||
<SlickList axis="y" v-model:list="position_questionnaires" useDragHandle appendTo="#questionnaireListParent" id="questionnaireListParent" @update:list="saveData()">
|
||||
<SlickItem v-for="(questionnaire, index) in position_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
|
||||
<QuestionnaireElement
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="position_datafields"
|
||||
:grouped_items="grouped_items"
|
||||
:selected_product="selected_product"
|
||||
:preview_mode="false"/>
|
||||
:preview_mode="false"
|
||||
@update="saveQuestionnaire(questionnaire)"/>
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
<div v-if="!preview_mode" class="editor-action-row form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-push-3">
|
||||
<button class="btn btn-default" @click="addPositionQuestionnaire()"><i class="fa fa-plus"></i> {{ gettext('New questionnaire') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<button class="btn btn-default" @click="addPositionQuestionnaire()"><i class="fa fa-plus"></i> {{ gettext('New questionnaire') }}</button>
|
||||
<button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> {{ gettext('Save') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import {getEventLocales} from "./api"
|
||||
|
||||
const locales = getEventLocales();
|
||||
|
||||
const props = defineProps<{ value: any, id?: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-form-group" :id="id">
|
||||
<textarea v-for="locale in locales" cols="40" rows="2" :lang="locale" dir="ltr" class="form-control" title="Englisch" :id="`${id}_${locale}`" :placeholder="locale" v-model="value[locale]"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useId, defineProps } from 'vue';
|
||||
import {getEventLocales} from "./api";
|
||||
import {getEventLocales} from "./api"
|
||||
|
||||
const locales = getEventLocales();
|
||||
|
||||
const props = defineProps(['value', 'id']);
|
||||
|
||||
if (!props.value) props.value = {};
|
||||
const props = defineProps<{ value: any, id?: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="i18n-form-group" :id="id">
|
||||
<textarea v-for="locale in locales" cols="40" rows="2" :lang="locale" dir="ltr" class="form-control" title="Englisch" :id="`${id}_${locale}`" :placeholder="locale" v-model="value[locale]"></textarea>
|
||||
<input v-for="locale in locales" cols="40" rows="2" :lang="locale" dir="ltr" class="form-control" title="Englisch" :id="`${id}_${locale}`" :placeholder="locale" v-model="value[locale]">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,10 +5,11 @@ import I18nTextField from './I18nTextField.vue';
|
||||
import {useId, ref, computed} from 'vue'
|
||||
import { DragHandle } from 'vue-slicksort';
|
||||
import {getDatafieldEditUrl} from "./api";
|
||||
import I18nTextArea from "./I18nTextArea.vue";
|
||||
|
||||
const id = useId();
|
||||
const props = defineProps(['question', 'datafields', 'editable', 'possible_dependencies'])
|
||||
const emit = defineEmits(['removeSelf']);
|
||||
const emit = defineEmits(['removeSelf', 'update']);
|
||||
const gettext = (window as any).gettext;
|
||||
const question = ref(props.question);
|
||||
|
||||
@@ -28,55 +29,61 @@ const editor = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<div class="question-edit-buttons" v-if="editable"><div class="btn-group">
|
||||
<DragHandle tag="button" class="btn btn-default"><i class="fa fa-arrows"></i></DragHandle>
|
||||
<button class="btn btn-default" @click="editor.show()"><i class="fa fa-edit"></i></button>
|
||||
</div></div>
|
||||
<div class="editor-row">
|
||||
<div class="editor-preview-area">
|
||||
<div class="form-group">
|
||||
|
||||
<template v-if="df">
|
||||
<div v-if="question.dependency_question" class="dependency-info debuginfo">
|
||||
<span><span class="fa fa-link"></span> {{ question.dependency_question }} = {{ dependency_values_resolved }}</span>
|
||||
</div>
|
||||
<div class="col-md-3 control-label label-empty" v-if="df.type === QUESTION_TYPE.BOOLEAN"></div>
|
||||
<label class="col-md-3 control-label" :for="id" v-else>
|
||||
{{ i18n_any(question.label) }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<input :id="id" type="text" v-if="df.type === QUESTION_TYPE.STRING || df.type === QUESTION_TYPE.PHONENUMBER" class="form-control">
|
||||
<textarea :id="id" v-if="df.type === QUESTION_TYPE.TEXT" class="form-control"></textarea>
|
||||
<div class="checkbox" v-if="df.type === QUESTION_TYPE.BOOLEAN">
|
||||
<label :for="id">
|
||||
<input :id="id" type="checkbox"> {{ i18n_any(question.label) }}
|
||||
<template v-if="df">
|
||||
<div v-if="question.dependency_question" class="dependency-info debuginfo">
|
||||
<span><span class="fa fa-link"></span> {{ question.dependency_question }} = {{ dependency_values_resolved }}</span>
|
||||
</div>
|
||||
<div class="col-md-3 control-label label-empty" v-if="df.type === QUESTION_TYPE.BOOLEAN"></div>
|
||||
<label class="col-md-3 control-label" :for="id" v-else>
|
||||
{{ i18n_any(question.label) }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<input :id="id" type="text" v-if="df.type === QUESTION_TYPE.STRING || df.type === QUESTION_TYPE.PHONENUMBER" class="form-control">
|
||||
<textarea :id="id" v-if="df.type === QUESTION_TYPE.TEXT" class="form-control"></textarea>
|
||||
<div class="checkbox" v-if="df.type === QUESTION_TYPE.BOOLEAN">
|
||||
<label :for="id">
|
||||
<input :id="id" type="checkbox"> {{ i18n_any(question.label) }}
|
||||
</label>
|
||||
</div>
|
||||
<input :id="id" type="number" v-if="df.type === QUESTION_TYPE.NUMBER" class="form-control">
|
||||
<input :id="id" type="file" v-if="df.type === QUESTION_TYPE.FILE" class="form-control">
|
||||
<select :id="id" class="form-control"
|
||||
v-if="df.type === QUESTION_TYPE.CHOICE || df.type === QUESTION_TYPE.COUNTRYCODE">
|
||||
<option></option>
|
||||
<option v-for="opt in df.options">{{ i18n_any(opt.answer) }}</option>
|
||||
</select>
|
||||
<div class="checkbox" v-if="df.type === QUESTION_TYPE.CHOICE_MULTIPLE" v-for="(opt, index) in df.options">
|
||||
<label :for="`${id}-${index}`">
|
||||
<input :id="`${id}-${index}`" type="checkbox"> {{ i18n_any(opt.answer) }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block" v-html="question.rendered_help_text" v-if="question.rendered_help_text"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="col-md-9 col-md-push-3">
|
||||
<h4>{{ i18n_any(question.label) }}</h4>
|
||||
<p v-html="question.rendered_help_text" v-if="question.rendered_help_text"></p>
|
||||
</div>
|
||||
<input :id="id" type="number" v-if="df.type === QUESTION_TYPE.NUMBER" class="form-control">
|
||||
<input :id="id" type="file" v-if="df.type === QUESTION_TYPE.FILE" class="form-control">
|
||||
<select :id="id" class="form-control"
|
||||
v-if="df.type === QUESTION_TYPE.CHOICE || df.type === QUESTION_TYPE.COUNTRYCODE">
|
||||
<option></option>
|
||||
<option v-for="opt in df.options">{{ i18n_any(opt.answer) }}</option>
|
||||
</select>
|
||||
<div class="checkbox" v-if="df.type === QUESTION_TYPE.CHOICE_MULTIPLE" v-for="(opt, index) in df.options">
|
||||
<label :for="`${id}-${index}`">
|
||||
<input :id="`${id}-${index}`" type="checkbox"> {{ i18n_any(opt.answer) }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="help-block">{{ i18n_any(question.help_text) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="col-md-12">
|
||||
<h3>{{ i18n_any(question.label) }}</h3>
|
||||
<p>{{ i18n_any(question.help_text) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="editor-action-area">
|
||||
<div class="btn-group" v-if="editable">
|
||||
<DragHandle tag="button" class="btn btn-default"><i class="fa fa-arrows"></i></DragHandle>
|
||||
<button class="btn btn-default" @click="editor.show()"><i class="fa fa-edit"></i></button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<NativeDialog ref="editor" class="modal-card"
|
||||
title="Edit question">
|
||||
:title="df ? gettext('Edit question') : gettext('Edit text block')">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Question') }}
|
||||
{{ df ? gettext('Question') : gettext('Headline') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<I18nTextField :value="question.label"/>
|
||||
@@ -87,11 +94,11 @@ const editor = ref();
|
||||
{{ gettext('Help text') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<I18nTextField :value="question.help_text"/>
|
||||
<I18nTextArea :value="question.help_text"/>
|
||||
<div class="help-block">Wenn diese Frage noch weitere Erklärung braucht, können Sie sie hier eintragen.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" v-if="df">
|
||||
<label class="col-md-3 control-label">
|
||||
Data field
|
||||
</label>
|
||||
@@ -107,7 +114,7 @@ const editor = ref();
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" v-if="df">
|
||||
<label class="col-md-3 control-label">
|
||||
Data field type
|
||||
</label>
|
||||
@@ -117,7 +124,7 @@ const editor = ref();
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" v-if="df">
|
||||
<label class="col-md-3 control-label label-empty"> </label>
|
||||
<div class="col-md-9">
|
||||
<div class="checkbox">
|
||||
@@ -147,7 +154,7 @@ const editor = ref();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="editor.close()" class="btn btn-primary pull-right"><span class="fa fa-check"></span> Save and close</button>
|
||||
<button @click="editor.close(); emit('update')" class="btn btn-primary pull-right"><span class="fa fa-check"></span> Save and close</button>
|
||||
<button @click="emit('removeSelf')" class="btn btn-default">Remove from questionnaire</button>
|
||||
</NativeDialog>
|
||||
</Teleport>
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import {useId, ref, computed} from 'vue'
|
||||
import QuestionElement from "./QuestionElement.vue";
|
||||
import {i18n_any, QUESTION_TYPE, QUESTION_TYPE_LABEL} from "./helper";
|
||||
import {
|
||||
i18n_any,
|
||||
QUESTION_TYPE,
|
||||
QUESTION_TYPE_LABEL,
|
||||
QUESTIONNAIRE_TYPE,
|
||||
QUESTIONNAIRE_TYPE_LABEL,
|
||||
SYSTEM_DATAFIELDS
|
||||
} from "./helper";
|
||||
import { gettext } from './gettextstub';
|
||||
import I18nTextField from "./I18nTextField.vue";
|
||||
import NativeDialog from "./NativeDialog.vue";
|
||||
import { SlickList, SlickItem, DragHandle } from 'vue-slicksort';
|
||||
import {getDatafieldCreateUrl} from "./api";
|
||||
|
||||
const dlgEditor = ref();
|
||||
const dlgAddExisting = ref();
|
||||
const dlgAddTextblock = ref();
|
||||
const dlgEditor = ref()
|
||||
const dlgAddExisting = ref()
|
||||
const dlgAddTextblock = ref()
|
||||
|
||||
const newTextblockTitle = ref();
|
||||
const newTextblockText = ref();
|
||||
const newTextblockTitle = ref()
|
||||
const newTextblockText = ref()
|
||||
|
||||
const id = useId();
|
||||
const props = defineProps(['questionnaire', 'datafields', 'selected_product', 'grouped_items', 'preview_mode'])
|
||||
const props = defineProps(['questionnaire', 'datafields', 'selected_product', 'grouped_items', 'preview_mode', 'err_mes'])
|
||||
const emit = defineEmits(['update'])
|
||||
|
||||
for (let qc of props.questionnaire.children) {
|
||||
if (!qc._cid) qc._cid = useId();
|
||||
@@ -29,26 +39,27 @@ function setVisibleOnItem (checked, itemId) {
|
||||
}
|
||||
}
|
||||
|
||||
function addExistingDatafield(field) {
|
||||
function addExistingDatafield (field) {
|
||||
props.questionnaire.children.push({
|
||||
_cid: useId(),
|
||||
question: field.id,
|
||||
required: false,
|
||||
label: {},
|
||||
label: field.question,
|
||||
help_text: {},
|
||||
dependency_question: null,
|
||||
dependency_values: [],
|
||||
});
|
||||
dlgAddExisting.value.close();
|
||||
})
|
||||
dlgAddExisting.value.close()
|
||||
emit('update')
|
||||
}
|
||||
|
||||
function showAddTextblockDialog() {
|
||||
newTextblockTitle.value = {};
|
||||
newTextblockText.value = {};
|
||||
dlgAddTextblock.value.show();
|
||||
function showAddTextblockDialog () {
|
||||
newTextblockTitle.value = {}
|
||||
newTextblockText.value = {}
|
||||
dlgAddTextblock.value.show()
|
||||
}
|
||||
|
||||
function addTextblock() {
|
||||
function addTextblock () {
|
||||
props.questionnaire.children.push({
|
||||
_cid: useId(),
|
||||
question: null,
|
||||
@@ -57,46 +68,66 @@ function addTextblock() {
|
||||
help_text: newTextblockText.value,
|
||||
dependency_question: null,
|
||||
dependency_values: [],
|
||||
});
|
||||
dlgAddTextblock.value.close();
|
||||
})
|
||||
dlgAddTextblock.value.close()
|
||||
emit('update')
|
||||
}
|
||||
|
||||
const isHidden = computed(() => props.selected_product && props.questionnaire.items.indexOf(props.selected_product) === -1);
|
||||
const isEditable = computed(() => props.selected_product && props.questionnaire.items.indexOf(props.selected_product) !== -1);
|
||||
function newDatafield (container_type) {
|
||||
location.href = getDatafieldCreateUrl(container_type)
|
||||
}
|
||||
|
||||
const isHidden = computed(() => props.selected_product && props.questionnaire.items.indexOf(props.selected_product) === -1)
|
||||
const isEditable = computed(() => props.selected_product && props.questionnaire.items.indexOf(props.selected_product) !== -1)
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="question-edit-buttons"><div class="btn-group">
|
||||
<DragHandle tag="button" class="btn btn-default" v-if="!preview_mode"><i class="fa fa-arrows"></i></DragHandle>
|
||||
<button class="btn btn-default" @click="dlgEditor.show()"><i class="fa fa-edit"></i></button>
|
||||
</div></div>
|
||||
|
||||
<details class="panel panel-default " :open="!!isEditable"
|
||||
<details class="questionnaire-panel" :open="preview_mode"
|
||||
:class="{ 'hidden-questionnaire': isHidden }">
|
||||
<summary class="panel-heading">
|
||||
<input type="checkbox" @change="setVisibleOnItem(this.checked, selected_product)" v-if="selected_product && !preview_mode" :checked="!isHidden">
|
||||
{{ props.questionnaire.internal_name }}
|
||||
<summary class="questionnaire-panel-heading">
|
||||
<div class=" editor-row">
|
||||
<div class="editor-preview-area">
|
||||
<input type="checkbox" @change="setVisibleOnItem(this.checked, selected_product); emit('update')" v-if="selected_product && !preview_mode" :checked="!isHidden">
|
||||
{{ props.questionnaire.internal_name }}
|
||||
<span class="fa fa-warning" v-if="questionnaire._err_mes"></span>
|
||||
<span class="fa fa-cog fa-spin" v-if="questionnaire._loading"></span>
|
||||
</div>
|
||||
|
||||
<aside class="editor-action-area"><div class="btn-group">
|
||||
<DragHandle tag="button" class="btn btn-default" v-if="!preview_mode"><i class="fa fa-arrows"></i></DragHandle>
|
||||
<button class="btn btn-default" @click="dlgEditor.show()"><i class="fa fa-edit"></i></button>
|
||||
</div></aside>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="panel-body" v-if="!isHidden">
|
||||
<div class="questionnaire-panel-body" v-if="!isHidden">
|
||||
<div class="alert alert-warning" v-if="questionnaire._err_mes">{{ questionnaire._err_mes }}</div>
|
||||
<div class="form-horizontal" :id="`questionListParent${props.questionnaire.id}`">
|
||||
<SlickList axis="y" v-model:list="props.questionnaire.children" useDragHandle :appendTo="`#questionListParent${props.questionnaire.id}`">
|
||||
<SlickList axis="y" v-model:list="props.questionnaire.children" useDragHandle :appendTo="`#questionListParent${props.questionnaire.id}`" @update:list="emit('update')">
|
||||
<SlickItem v-for="(child, index) in props.questionnaire.children" :key="child._cid" :index="index">
|
||||
<QuestionElement
|
||||
:datafields="props.datafields"
|
||||
:question="child"
|
||||
:editable="true"
|
||||
:possible_dependencies="props.questionnaire.children.slice(0, index)"
|
||||
@remove-self="questionnaire.children.splice(index, 1)" />
|
||||
@remove-self="questionnaire.children.splice(index, 1); emit('update')"
|
||||
@update="emit('update')"/>
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
</div>
|
||||
<p v-if="true" class="btn-group" role="group">
|
||||
<button class="btn btn-default" @click="dlgAddExisting.show()"><i class="fa fa-plus"></i> {{ gettext('Existing data field') }}</button>
|
||||
<button class="btn btn-default" @click="newDatafield()"><i class="fa fa-plus"></i> {{ gettext('New data field') }}</button>
|
||||
<button class="btn btn-default" @click="showAddTextblockDialog()"><i class="fa fa-plus"></i> {{ gettext('Text') }}</button>
|
||||
</p>
|
||||
<div class="editor-action-row form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-push-3">
|
||||
<p class="btn-group" role="group">
|
||||
<button class="btn btn-default" @click="dlgAddExisting.show()"><i class="fa fa-plus"></i> {{ gettext('Existing data field') }}</button>
|
||||
<button class="btn btn-default" @click="newDatafield(questionnaire.type[0])"><i class="fa fa-plus"></i> {{ gettext('New data field') }}</button>
|
||||
<button class="btn btn-default" @click="showAddTextblockDialog()"><i class="fa fa-plus"></i> {{ gettext('Text') }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -111,6 +142,16 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
|
||||
<input type="text" class="form-control" v-model="questionnaire.internal_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Where to ask') }}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<select v-model="questionnaire.type" class="form-control">
|
||||
<option v-for="(label, type) in QUESTIONNAIRE_TYPE_LABEL" :value="QUESTIONNAIRE_TYPE[type]">{{ label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{{ gettext('Sales channels') }}
|
||||
@@ -138,7 +179,7 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="dlgEditor.close()" class="btn btn-primary pull-right"><span class="fa fa-check"></span> {{ gettext('Save and close') }}</button>
|
||||
<button @click="dlgEditor.close(); emit('update')" class="btn btn-primary pull-right"><span class="fa fa-check"></span> {{ gettext('Save and close') }}</button>
|
||||
<button class="btn btn-default">{{ gettext('Delete') }}</button>
|
||||
</NativeDialog>
|
||||
|
||||
@@ -146,7 +187,7 @@ const isEditable = computed(() => props.selected_product && props.questionnaire.
|
||||
:title="gettext('Add existing data field')">
|
||||
|
||||
<div class="list-group">
|
||||
<a href="javascript:" @click="addExistingDatafield(field)" v-for="field in datafields" class="list-group-item">{{ field.internal_name }}</a>
|
||||
<a href="javascript:" @click="addExistingDatafield(field)" v-for="field in datafields" class="list-group-item">{{ i18n_any(field.question) }}</a>
|
||||
</div>
|
||||
|
||||
<button @click="dlgAddExisting.close()" class="btn btn-default pull-right">{{ gettext('Cancel') }}</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiListResponse, Datafield, Questionnaire, Item, Category } from './model'
|
||||
import { ProgressBar } from "./ProgressBar";
|
||||
import {fromJsonScript} from "./helper";
|
||||
|
||||
const organizer_slug = document.body.getAttribute('data-organizer'),
|
||||
event_slug = document.body.getAttribute('data-event')
|
||||
@@ -20,15 +21,26 @@ async function api_get_all<T>(resource): Promise<T[]> {
|
||||
return result
|
||||
}
|
||||
|
||||
class APIError extends Error {
|
||||
api_error: string;
|
||||
constructor(json) {
|
||||
super('' + Object.values(json)[0]);
|
||||
this.api_error = json;
|
||||
}
|
||||
}
|
||||
async function api_json_request(resource, method, json_body) {
|
||||
return await (await fetch(`/api/v1/${resource}`, {
|
||||
const response = await fetch(`/api/v1/${resource}`, {
|
||||
body: JSON.stringify(json_body),
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val() as string,
|
||||
},
|
||||
})).json()
|
||||
});
|
||||
if (response.status >= 400)
|
||||
throw new APIError(await response.json());
|
||||
else
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export async function getDatafields(container_type) {
|
||||
@@ -59,14 +71,14 @@ export async function getCategories() {
|
||||
return await api_get_all<Category>(`organizers/${organizer_slug}/events/${event_slug}/categories/`);
|
||||
}
|
||||
|
||||
function get_json_script_value(id) {
|
||||
return JSON.parse(document.getElementById(id).innerText);
|
||||
}
|
||||
|
||||
export function getEventLocales() {
|
||||
return get_json_script_value('event_locales');
|
||||
return fromJsonScript('event_locales');
|
||||
}
|
||||
|
||||
export function getDatafieldEditUrl(datafield_id) {
|
||||
return get_json_script_value('datafield_edit_url').replace('/0/', `/${datafield_id}/`);
|
||||
return fromJsonScript('datafield_edit_url').replace('/0/', `/${datafield_id}/`);
|
||||
}
|
||||
|
||||
export function getDatafieldCreateUrl(container_type) {
|
||||
return fromJsonScript('datafield_create_url') + '?container_type=' + container_type;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// The actual gettext implementation is loaded asynchronously with the translation
|
||||
export function gettext (msgid) {
|
||||
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
|
||||
return django.gettext(msgid)
|
||||
}
|
||||
return msgid
|
||||
}
|
||||
|
||||
export function ngettext (singular, plural, count) {
|
||||
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
|
||||
return django.ngettext(singular, plural, count)
|
||||
}
|
||||
return plural
|
||||
}
|
||||
|
||||
export function pgettext (context, msgid) {
|
||||
if (typeof django !== 'undefined' && typeof django.pgettext !== 'undefined') {
|
||||
return django.pgettext(context, msgid)
|
||||
}
|
||||
return msgid
|
||||
}
|
||||
|
||||
export function interpolate (fmt, object, named) {
|
||||
if (named) {
|
||||
return fmt.replace(/%\(\w+\)s/g, function (match) { return String(object[match.slice(2, -2)]) })
|
||||
} else {
|
||||
return fmt.replace(/%s/g, function (match) { return String(object.shift()) })
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
export function i18n_any (data) {
|
||||
if (!data) return null
|
||||
if (data instanceof LazyI18nString) data = data.data;
|
||||
const preferred = document.body.getAttribute('data-pretixlocale')
|
||||
if (data[preferred]) return data[preferred]
|
||||
return Object.values(data)[0]
|
||||
@@ -56,7 +55,7 @@ export function *groupBy (array, key) {
|
||||
}
|
||||
}
|
||||
|
||||
function fromJsonScript (id) {
|
||||
export function fromJsonScript (id) {
|
||||
return JSON.parse(document.getElementById(id).textContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export type ApiListResponse<T> = {
|
||||
next: string | null,
|
||||
previous: string | null,
|
||||
results: T[],
|
||||
};
|
||||
}
|
||||
|
||||
// from webcheckin/i18n.ts
|
||||
export type I18nString = string | Record<string, string> | null | undefined;
|
||||
@@ -35,7 +35,7 @@ export type Datafield = {
|
||||
valid_string_length_max: null | number,
|
||||
valid_file_portrait: boolean,
|
||||
internal_name: string,
|
||||
};
|
||||
}
|
||||
|
||||
export type Questionnaire = {
|
||||
id: number,
|
||||
@@ -46,7 +46,7 @@ export type Questionnaire = {
|
||||
all_sales_channels: boolean,
|
||||
limit_sales_channels: string[],
|
||||
children: QuestionnaireChild[],
|
||||
};
|
||||
}
|
||||
|
||||
export type QuestionnaireChild = {
|
||||
question: string | number,
|
||||
@@ -55,11 +55,18 @@ export type QuestionnaireChild = {
|
||||
help_text: I18nString,
|
||||
dependency_question: number | null,
|
||||
dependency_values: null | string[],
|
||||
};
|
||||
}
|
||||
|
||||
export type Item = {
|
||||
id: number,
|
||||
category: number,
|
||||
name: I18nString,
|
||||
internal_name: string | null,
|
||||
};
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
id: number,
|
||||
position: number,
|
||||
name: I18nString,
|
||||
internal_name: string | null,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user