Editor UX rework

This commit is contained in:
Mira Weller
2026-09-17 16:11:03 +02:00
parent e3a3768a6c
commit eb7936634b
10 changed files with 318 additions and 139 deletions
@@ -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>
&nbsp;
<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">&nbsp;</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,
}