mirror of
https://github.com/pretix/pretix.git
synced 2026-09-15 16:34:42 +00:00
wallet wip
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
|||||||
- 'src/pretix/static/pretixpresale/widget/**'
|
- 'src/pretix/static/pretixpresale/widget/**'
|
||||||
- 'src/pretix/static/pretixcontrol/js/ui/checkinrules/**'
|
- 'src/pretix/static/pretixcontrol/js/ui/checkinrules/**'
|
||||||
- 'src/pretix/plugins/webcheckin/**'
|
- 'src/pretix/plugins/webcheckin/**'
|
||||||
|
- 'src/pretix/plugins/wallet/**'
|
||||||
- 'eslint.config.mjs'
|
- 'eslint.config.mjs'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'package-lock.json'
|
- 'package-lock.json'
|
||||||
@@ -16,6 +17,7 @@ on:
|
|||||||
- 'src/pretix/static/pretixpresale/widget/**'
|
- 'src/pretix/static/pretixpresale/widget/**'
|
||||||
- 'src/pretix/static/pretixcontrol/js/ui/checkinrules/**'
|
- 'src/pretix/static/pretixcontrol/js/ui/checkinrules/**'
|
||||||
- 'src/pretix/plugins/webcheckin/**'
|
- 'src/pretix/plugins/webcheckin/**'
|
||||||
|
- 'src/pretix/plugins/wallet/**'
|
||||||
- 'eslint.config.mjs'
|
- 'eslint.config.mjs'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'package-lock.json'
|
- 'package-lock.json'
|
||||||
|
|||||||
@@ -1,67 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, inject, ref, watchEffect } from "vue";
|
||||||
import StyleSettings from "./style-settings.vue";
|
import StyleSettings from "./style-settings.vue";
|
||||||
import Select from "./input/select.vue";
|
import Select from "./input/select.vue";
|
||||||
import Input from "./input/input.vue";
|
import Input from "./input/input.vue";
|
||||||
|
import { StoreKey } from "../walletStore";
|
||||||
|
|
||||||
const gettext = (window as any).gettext;
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
const isLoading = ref<boolean>(true);
|
const store = inject(StoreKey)!
|
||||||
const wallet_layout = ref<Layout | null>(null);
|
|
||||||
|
|
||||||
const PLATFORMS: Platforms = JSON.parse(
|
|
||||||
document.querySelector("#platforms")?.textContent ?? "{}",
|
|
||||||
);
|
|
||||||
const VARIABLES: VariableConfig = JSON.parse(
|
|
||||||
document.querySelector("#variables")?.textContent ?? "{}",
|
|
||||||
);
|
|
||||||
const LOCALES: Record<string, string> = JSON.parse(
|
|
||||||
document.querySelector("#locales")?.textContent ?? "{}",
|
|
||||||
);
|
|
||||||
const CSRF_TOKEN =
|
|
||||||
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
|
|
||||||
?.value ?? "";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const platformChoices = computed(() => {
|
||||||
layoutId: string;
|
return [[null, "Do not generate pass"], ...Object.values(store.currentPlatformStyles).map(x => [x.identifier, x.name])]
|
||||||
}>();
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
// TODO: error handling / proper api client
|
|
||||||
isLoading.value = true;
|
|
||||||
fetch(
|
|
||||||
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
|
||||||
)
|
|
||||||
.then((x) => x.json())
|
|
||||||
.then((x) => {
|
|
||||||
wallet_layout.value = x;
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function saveLayout(e: SubmitEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
isLoading.value = true;
|
|
||||||
// TODO: error handling / proper api client
|
|
||||||
fetch(
|
|
||||||
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
"X-CSRFToken": CSRF_TOKEN,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(wallet_layout.value),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((x) => x.json())
|
|
||||||
.catch((x) => alert(x))
|
|
||||||
.then((x) => {
|
|
||||||
wallet_layout.value = x;
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openForm(url: string, data: Record<string, string>) {
|
function openForm(url: string, data: Record<string, string>) {
|
||||||
|
|
||||||
let form = document.createElement("form");
|
let form = document.createElement("form");
|
||||||
@@ -82,33 +34,9 @@ function openForm(url: string, data: Record<string, string>) {
|
|||||||
document.body.removeChild(form);
|
document.body.removeChild(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const currentPlatform = ref(PLATFORMS[0].identifier);
|
|
||||||
const currentLayout = computed(() => ({}));
|
|
||||||
const platformStyles = computed(() => {
|
|
||||||
for (const platform of PLATFORMS) {
|
|
||||||
if (platform.identifier === currentPlatform.value) {
|
|
||||||
return platform.styles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const platformLayout = computed(() => {
|
|
||||||
for (const layout of wallet_layout.value.platform_layouts) {
|
|
||||||
if (layout.platform === currentPlatform.value) {
|
|
||||||
return layout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newLayout = {platform: currentPlatform, style: null, layout: {}};
|
|
||||||
wallet_layout.value.platform_layouts.push(newLayout);
|
|
||||||
return newLayout
|
|
||||||
});
|
|
||||||
const platformChoices = computed(() => {
|
|
||||||
return [[null, "Do not generate pass"], ...Object.values(platformStyles.value).map(x => [x.identifier, x.name])]
|
|
||||||
});
|
|
||||||
|
|
||||||
function openPreview(e: SubmitEvent) {
|
function openPreview(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openForm("../../preview/", {"csrfmiddlewaretoken": CSRF_TOKEN, "platform": currentPlatform.value, "style": platformLayout.value.style, "layout": JSON.stringify(platformLayout.value.layout)})
|
openForm("../../preview/", {"csrfmiddlewaretoken": store.csrfToken, "platform": store.currentPlatform, "style": store.currentPlatformLayout.style, "layout": JSON.stringify(store.currentPlatformLayout.layout)})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -116,31 +44,41 @@ function openPreview(e: SubmitEvent) {
|
|||||||
// TODO: add :key for all `v-for`s
|
// TODO: add :key for all `v-for`s
|
||||||
// TODO: i18n textfields
|
// TODO: i18n textfields
|
||||||
// TODO: proper spinner
|
// TODO: proper spinner
|
||||||
template(v-if="isLoading") {{ gettext("Loading...") }}
|
|
||||||
form(v-else @submit="saveLayout")
|
template(v-if="!store.loaded") {{ gettext("Loading...") }}
|
||||||
|
form(v-else @submit.prevent="store.saveLayout")
|
||||||
.form-group
|
.form-group
|
||||||
Input(label="Name" v-model="wallet_layout.name")
|
Input(label="Name" v-model="store.walletLayout.name")
|
||||||
nav
|
nav
|
||||||
ul.nav.nav-tabs
|
ul.nav.nav-tabs
|
||||||
li(v-for="platform in PLATFORMS" :class="{'active': currentPlatform === platform.identifier}")
|
li(v-for="platform in store.platforms" :class="{'active': store.currentPlatform === platform.identifier}")
|
||||||
a(role="tab" @click="currentPlatform = platform.identifier") {{ platform.name }}
|
a(role="tab" @click="store.currentPlatform = platform.identifier") {{ platform.name }}
|
||||||
.tabbed-form.tab-content
|
.tabbed-form.tab-content
|
||||||
.tab-pane.active.row
|
.tab-pane.active.row
|
||||||
.col-md-8
|
.col-md-8
|
||||||
Select.form-group(label="Style" v-model="platformLayout.style" :choices="platformChoices")
|
Select.form-group(label="Style" :modelValue="store.currentPlatformLayout.style" @update:modelValue="store.setCurrentPlatformStyle" :choices="platformChoices")
|
||||||
|
|
||||||
StyleSettings(v-if="platformLayout.style" v-model="platformLayout.layout" :style="platformStyles[platformLayout.style]" :variables="VARIABLES" :locales="LOCALES")
|
StyleSettings(v-if="store.currentPlatformLayout.style" v-model="store.currentPlatformLayout.layout" :style="store.currentPlatformStyles[store.currentPlatformLayout.style]")
|
||||||
.col-md-4
|
.col-md-4
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading Preview
|
.panel-heading Preview
|
||||||
.panel-body
|
.panel-body
|
||||||
|
span.text-muted The preview below is only a rough representation of what the pass might look like. Please check the generated pass.
|
||||||
|
div(style="margin-top: 1em; width: 10cm; height: 15cm; position: relative; border: solid 1px gray; border-radius: 1em;")
|
||||||
|
div(style="position: absolute; background-color: red; top: 0.5cm; left: 0cm; height: 1cm; width: 2cm;") Logo
|
||||||
|
div(style="position: absolute; background-color: red; top: 0.5cm; left: 2.25cm; height: 1cm; width: 5.5cm;") Logo Text
|
||||||
|
div(style="position: absolute; background-color: red; top: 0.5cm; left: 8cm; height: 1cm; width: 2cm;") Header
|
||||||
|
div(style="position: absolute; background-color: red; top: 2.5cm; left: 0cm; height: 2cm; width: 10cm;") Primary
|
||||||
|
div(style="position: absolute; background-color: red; top: 4.75cm; left: 0cm; height: 1cm; width: 10cm;") Secondary
|
||||||
|
div(style="position: absolute; background-color: red; top: 7cm; left: 0cm; height: 1cm; width: 10cm;") Auxiliary
|
||||||
|
div(style="position: absolute; background-color: red; top: 10cm; left: 3cm; height: 4cm; width: 4cm;") Barcode
|
||||||
// TODO: Preview
|
// TODO: Preview
|
||||||
pre
|
pre
|
||||||
code {{ platformLayout }}
|
code {{ store.currentPlatformLayout }}
|
||||||
pre(v-if="wallet_layout.style")
|
pre(v-if="store.currentPlatformLayout.style")
|
||||||
code {{ platformStyles[wallet_layout.style] }}
|
code {{ store.currentPlatformStyles[store.currentPlatformLayout.style] }}
|
||||||
pre
|
pre
|
||||||
code {{ wallet_layout }}
|
code {{ store.walletLayout }}
|
||||||
.form-group.submit-group
|
.form-group.submit-group
|
||||||
button.btn.btn-lg.btn-default(type="button" @click="openPreview") Preview
|
button.btn.btn-lg.btn-default(type="button" @click="openPreview") Preview
|
||||||
button.btn.btn-primary.btn-save(type="submit") Submit
|
button.btn.btn-primary.btn-save(type="submit") Submit
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
import StyleSettings from "./style-settings.vue";
|
||||||
|
import Select from "./input/select.vue";
|
||||||
|
import Input from "./input/input.vue";
|
||||||
|
|
||||||
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
|
const isLoading = ref<boolean>(true);
|
||||||
|
const wallet_layout = ref<PlatformLayout | null>(null);
|
||||||
|
|
||||||
|
const PLATFORMS: Platforms = JSON.parse(
|
||||||
|
document.querySelector("#platforms")?.textContent ?? "{}",
|
||||||
|
);
|
||||||
|
const VARIABLES: VariableConfig = JSON.parse(
|
||||||
|
document.querySelector("#variables")?.textContent ?? "{}",
|
||||||
|
);
|
||||||
|
const LOCALES: Record<string, string> = JSON.parse(
|
||||||
|
document.querySelector("#locales")?.textContent ?? "{}",
|
||||||
|
);
|
||||||
|
const CSRF_TOKEN =
|
||||||
|
document.querySelector<HTMLInputElement>("input[name=csrfmiddlewaretoken]")
|
||||||
|
?.value ?? "";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
layoutId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
// TODO: error handling / proper api client
|
||||||
|
isLoading.value = true;
|
||||||
|
fetch(
|
||||||
|
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.then((x) => {
|
||||||
|
wallet_layout.value = x;
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveLayout(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
isLoading.value = true;
|
||||||
|
// TODO: error handling / proper api client
|
||||||
|
fetch(
|
||||||
|
`/api/v1/organizers/demo/events/wallet/walletlayouts/${props.layoutId}/`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"X-CSRFToken": CSRF_TOKEN,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(wallet_layout.value),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.catch((x) => alert(x))
|
||||||
|
.then((x) => {
|
||||||
|
wallet_layout.value = x;
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openForm(url: string, data: Record<string, string>) {
|
||||||
|
|
||||||
|
let form = document.createElement("form");
|
||||||
|
form.target = "_blank";
|
||||||
|
form.method = "POST";
|
||||||
|
form.action = url;
|
||||||
|
form.style.display = "none";
|
||||||
|
|
||||||
|
for (var key in data) {
|
||||||
|
var input = document.createElement("input");
|
||||||
|
input.type = "hidden";
|
||||||
|
input.name = key;
|
||||||
|
input.value = data[key];
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
document.body.removeChild(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const currentPlatform = ref(PLATFORMS[0].identifier);
|
||||||
|
const currentLayout = computed(() => ({}));
|
||||||
|
const platformStyles = computed(() => {
|
||||||
|
for (const platform of PLATFORMS) {
|
||||||
|
if (platform.identifier === currentPlatform.value) {
|
||||||
|
return platform.styles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const platformLayout = computed(() => {
|
||||||
|
for (const layout of wallet_layout.value.platform_layouts) {
|
||||||
|
if (layout.platform === currentPlatform.value) {
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newLayout = {platform: currentPlatform, style: null, layout: {}};
|
||||||
|
wallet_layout.value.platform_layouts.push(newLayout);
|
||||||
|
return newLayout
|
||||||
|
});
|
||||||
|
const platformChoices = computed(() => {
|
||||||
|
return [[null, "Do not generate pass"], ...Object.values(platformStyles.value).map(x => [x.identifier, x.name])]
|
||||||
|
});
|
||||||
|
|
||||||
|
function openPreview(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
openForm("../../preview/", {"csrfmiddlewaretoken": CSRF_TOKEN, "platform": currentPlatform.value, "style": platformLayout.value.style, "layout": JSON.stringify(platformLayout.value.layout)})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
// TODO: add :key for all `v-for`s
|
||||||
|
// TODO: i18n textfields
|
||||||
|
// TODO: proper spinner
|
||||||
|
template(v-if="isLoading") {{ gettext("Loading...") }}
|
||||||
|
form(v-else @submit="saveLayout")
|
||||||
|
.form-group
|
||||||
|
Input(label="Name" v-model="wallet_layout.name")
|
||||||
|
nav
|
||||||
|
ul.nav.nav-tabs
|
||||||
|
li(v-for="platform in PLATFORMS" :class="{'active': currentPlatform === platform.identifier}")
|
||||||
|
a(role="tab" @click="currentPlatform = platform.identifier") {{ platform.name }}
|
||||||
|
.tabbed-form.tab-content
|
||||||
|
.tab-pane.active.row
|
||||||
|
.col-md-8
|
||||||
|
Select.form-group(label="Style" v-model="platformLayout.style" :choices="platformChoices")
|
||||||
|
|
||||||
|
StyleSettings(v-if="platformLayout.style" v-model="platformLayout.layout" :style="platformStyles[platformLayout.style]" :variables="VARIABLES" :locales="LOCALES")
|
||||||
|
.col-md-4
|
||||||
|
.panel.panel-default
|
||||||
|
.panel-heading Preview
|
||||||
|
.panel-body
|
||||||
|
// TODO: Preview
|
||||||
|
pre
|
||||||
|
code {{ platformLayout }}
|
||||||
|
pre(v-if="wallet_layout.style")
|
||||||
|
code {{ platformStyles[wallet_layout.style] }}
|
||||||
|
pre
|
||||||
|
code {{ wallet_layout }}
|
||||||
|
.form-group.submit-group
|
||||||
|
button.btn.btn-lg.btn-default(type="button" @click="openPreview") Preview
|
||||||
|
button.btn.btn-primary.btn-save(type="submit") Submit
|
||||||
|
|
||||||
|
</template>
|
||||||
+6
-4
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watchEffect } from 'vue'
|
import { inject, watchEffect } from 'vue'
|
||||||
|
import { StoreKey } from "../../walletStore";
|
||||||
|
|
||||||
|
const store = inject(StoreKey)!
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false
|
inheritAttrs: false
|
||||||
@@ -7,19 +10,18 @@ defineOptions({
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
errors?: string[],
|
errors?: string[],
|
||||||
locales: Record<string, string>
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modelValue = defineModel<Record<string, string> | string>();
|
const modelValue = defineModel<Record<string, string> | string>();
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (typeof modelValue.value === "string") {
|
if (typeof modelValue.value === "string") {
|
||||||
const oldVal = modelValue.value;
|
const oldVal = modelValue.value;
|
||||||
modelValue.value = Object.fromEntries(Object.keys(props.locales).map((x): [string, string] => [x, oldVal]))
|
modelValue.value = Object.fromEntries(Object.keys(store.locales).map((x): [string, string] => [x, oldVal]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
input.form-control(v-for="(human_readable, locale) in locales" v-model="modelValue[locale]" v-bind="$attrs" :lang="locale" :title="human_readable" :placeholder="human_readable")
|
input.form-control(v-for="(human_readable, locale) in store.locales" v-model="modelValue[locale]" v-bind="$attrs" :lang="locale" :title="human_readable" :placeholder="human_readable")
|
||||||
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
|
.help-block(v-if="props.errors" v-for="error in props.errors") {{ error }}
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
|||||||
label?: string
|
label?: string
|
||||||
choices: Array<[string, string]>
|
choices: Array<[string, string]>
|
||||||
errors?: string[],
|
errors?: string[],
|
||||||
class: string
|
class?: string
|
||||||
}>()
|
}>()
|
||||||
const modelValue = defineModel<string|null>();
|
const modelValue = defineModel<string|null>();
|
||||||
const id = useId()
|
const id = useId()
|
||||||
|
|||||||
+7
-8
@@ -1,17 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watchEffect } from "vue";
|
import { computed, inject, reactive, watchEffect } from "vue";
|
||||||
import Select from "./input/select.vue";
|
import Select from "./input/select.vue";
|
||||||
import Input from "./input/input.vue";
|
import Input from "./input/input.vue";
|
||||||
import I18nInput from "./input/i18ninput.vue";
|
import I18nInput from "./input/i18ninput.vue";
|
||||||
import TextContent from "./text-content.vue";
|
import TextContent from "./text-content.vue";
|
||||||
|
import { StoreKey } from "../walletStore";
|
||||||
|
|
||||||
|
const store = inject(StoreKey)!
|
||||||
|
|
||||||
const gettext = (window as any).gettext;
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fieldgroup: PlaceholderFieldGroupDefinition;
|
fieldgroup: PlaceholderFieldGroupDefinition;
|
||||||
overflows: FieldGroupDefinition[];
|
overflows: FieldGroupDefinition[];
|
||||||
variables: Variables;
|
|
||||||
locales: Record<string, string>;
|
|
||||||
}>();
|
}>();
|
||||||
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({ required: true });
|
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({ required: true });
|
||||||
|
|
||||||
@@ -58,15 +59,13 @@ watchEffect(() => {
|
|||||||
tr(v-for="n,i in fieldConfig.entries.length" :key="i")
|
tr(v-for="n,i in fieldConfig.entries.length" :key="i")
|
||||||
td(v-if="fieldgroup.labels")
|
td(v-if="fieldgroup.labels")
|
||||||
.i18n-form-group
|
.i18n-form-group
|
||||||
I18nInput(v-model="fieldConfig.entries[n-1].label" :locales="locales")
|
I18nInput(v-model="fieldConfig.entries[n-1].label")
|
||||||
td
|
td
|
||||||
TextContent(v-if='fieldgroup.content_type == "text"'
|
TextContent(v-if='fieldgroup.content_type == "text"'
|
||||||
v-model="fieldConfig.entries[n-1]"
|
v-model="fieldConfig.entries[n-1]")
|
||||||
:variables="props.variables"
|
|
||||||
:locales="locales")
|
|
||||||
Select(v-else-if='fieldgroup.content_type == "image"'
|
Select(v-else-if='fieldgroup.content_type == "image"'
|
||||||
v-model="fieldConfig.entries[n-1].content"
|
v-model="fieldConfig.entries[n-1].content"
|
||||||
:choices="Object.entries(props.variables).map(([k,v]) => [k, v.label])"
|
:choices="Object.entries(store.variables.image).map(([k,v]) => [k, v.label])"
|
||||||
)
|
)
|
||||||
td.text-right
|
td.text-right
|
||||||
button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watchEffect } from "vue";
|
import { computed, inject, watchEffect } from "vue";
|
||||||
import PlaceholderFieldSettings from "./placeholder-field-settings.vue";
|
import PlaceholderFieldSettings from "./placeholder-field-settings.vue";
|
||||||
import PredefinedFieldSettings from "./predefined-field-settings.vue";
|
import PredefinedFieldSettings from "./predefined-field-settings.vue";
|
||||||
|
|
||||||
const gettext = (window as any).gettext;
|
const gettext = (window as any).gettext;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
variables: VariableConfig
|
|
||||||
style?: Style;
|
style?: Style;
|
||||||
locales: Record<string, string>;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const layout = defineModel<LayoutData>();
|
const layout = defineModel<LayoutData>();
|
||||||
@@ -32,8 +30,6 @@ watchEffect(() => {
|
|||||||
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
||||||
:fieldgroup="fieldgroup"
|
:fieldgroup="fieldgroup"
|
||||||
:overflows="props.style.fieldgroups.slice(fieldgroupId + 1).filter(x => x.type == 'placeholder' && x.content_type === fieldgroup.content_type)"
|
:overflows="props.style.fieldgroups.slice(fieldgroupId + 1).filter(x => x.type == 'placeholder' && x.content_type === fieldgroup.content_type)"
|
||||||
:variables="variables[fieldgroup.content_type]"
|
|
||||||
:locales="locales"
|
|
||||||
)
|
)
|
||||||
PredefinedFieldSettings(v-else-if="fieldgroup.type == 'predefined'"
|
PredefinedFieldSettings(v-else-if="fieldgroup.type == 'predefined'"
|
||||||
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue'
|
import { computed, inject, reactive } from 'vue'
|
||||||
import Select from './input/select.vue'
|
import Select from './input/select.vue'
|
||||||
import I18nInput from './input/i18ninput.vue'
|
import I18nInput from './input/i18ninput.vue'
|
||||||
|
import { StoreKey } from "../walletStore";
|
||||||
|
|
||||||
|
const store = inject(StoreKey)!
|
||||||
const gettext = (window as any).gettext
|
const gettext = (window as any).gettext
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
variables: Variables
|
|
||||||
locales: Record<string, string>;
|
|
||||||
}>()
|
|
||||||
const entry = defineModel<FieldEntry>({ required: true })
|
const entry = defineModel<FieldEntry>({ required: true })
|
||||||
|
|
||||||
const selectChoices = computed(() =>{
|
const selectChoices = computed(() =>{
|
||||||
const choices = Object.entries(props.variables).map(([k,v]): [string, string] => [k, v.label])
|
const choices = Object.entries(store.variables.text).map(([k,v]): [string, string] => [k, v.label])
|
||||||
choices.push(["other", gettext("Other…")])
|
choices.push(["other", gettext("Other…")])
|
||||||
return choices
|
return choices
|
||||||
});
|
});
|
||||||
@@ -23,8 +21,6 @@ const selection = computed({
|
|||||||
return entry.value.content
|
return entry.value.content
|
||||||
} else if (entry.value.type === 'custom') {
|
} else if (entry.value.type === 'custom') {
|
||||||
return "other"
|
return "other"
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown entry type "${entry.value.type}"`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set(newValue) {
|
set(newValue) {
|
||||||
@@ -44,8 +40,6 @@ const textContent = computed({
|
|||||||
return ""
|
return ""
|
||||||
} else if (entry.value.type === 'custom') {
|
} else if (entry.value.type === 'custom') {
|
||||||
return entry.value.content
|
return entry.value.content
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown entry type "${entry.value.type}"`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set(newValue) {
|
set(newValue) {
|
||||||
@@ -61,5 +55,5 @@ const textContent = computed({
|
|||||||
v-model="selection"
|
v-model="selection"
|
||||||
:choices="selectChoices"
|
:choices="selectChoices"
|
||||||
)
|
)
|
||||||
I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="locales")
|
I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="store.locales")
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ type PlaceholderFieldEntry = {
|
|||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentFieldEntry = {
|
type CustomFieldEntry = {
|
||||||
type: FieldContentType;
|
type: 'custom';
|
||||||
label?: I18nString;
|
label?: I18nString;
|
||||||
content?: I18nString;
|
content?: I18nString;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FieldEntry = PlaceholderFieldEntry | ContentFieldEntry;
|
type FieldEntry = PlaceholderFieldEntry | CustomFieldEntry;
|
||||||
|
|
||||||
type Style = {
|
type Style = {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
@@ -57,7 +57,7 @@ type Platform = {
|
|||||||
type Styles = Record<string, Style>;
|
type Styles = Record<string, Style>;
|
||||||
type Variables = Record<string, Variable>;
|
type Variables = Record<string, Variable>;
|
||||||
type VariableConfig = Record<string, Variables>;
|
type VariableConfig = Record<string, Variables>;
|
||||||
type Platforms = Record<string, Platform>;
|
type Platforms = Platform[];
|
||||||
|
|
||||||
|
|
||||||
type PlaceholderFieldGroupConfig = {
|
type PlaceholderFieldGroupConfig = {
|
||||||
@@ -73,9 +73,20 @@ type LayoutData = {
|
|||||||
fieldgroups: Record<string, FieldGroupConfig>;
|
fieldgroups: Record<string, FieldGroupConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Layout = {
|
type PlatformLayout = {
|
||||||
name?: string;
|
platform: string;
|
||||||
style?: string;
|
style: string | null;
|
||||||
layout?: LayoutData;
|
layout: LayoutData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WalletLayout = {
|
||||||
|
name?: string;
|
||||||
|
platform_layouts: PlatformLayout[];
|
||||||
|
}
|
||||||
|
type WalletStore = {
|
||||||
|
platforms: Platforms,
|
||||||
|
variables: VariableConfig,
|
||||||
|
locales: Record<string, string>,
|
||||||
|
csrfToken: String,
|
||||||
|
walletLayout: WalletLayout | null
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../../pretix/static/pretixpresale/widget/src/lib/store.ts
|
||||||
@@ -1,13 +1,28 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from "vue";
|
||||||
import App from './components/app.vue'
|
import App from "./components/app.vue";
|
||||||
|
import { createWalletStore, StoreKey } from "./walletStore";
|
||||||
const mountEl = document.querySelector<HTMLElement>('#editor')!
|
const mountEl = document.querySelector<HTMLElement>("#editor")!;
|
||||||
const app = createApp(App, mountEl.dataset)
|
const store = createWalletStore({
|
||||||
app.mount(mountEl)
|
platforms: JSON.parse(
|
||||||
|
document.querySelector("#platforms")?.textContent ?? "{}",
|
||||||
|
),
|
||||||
|
variables: JSON.parse(
|
||||||
|
document.querySelector("#variables")?.textContent ?? "{}",
|
||||||
|
),
|
||||||
|
locales: JSON.parse(document.querySelector("#locales")?.textContent ?? "{}"),
|
||||||
|
csrfToken: document.querySelector<HTMLInputElement>(
|
||||||
|
"input[name=csrfmiddlewaretoken]",
|
||||||
|
)?.value!!,
|
||||||
|
layoutId: mountEl.dataset.layoutId!
|
||||||
|
});
|
||||||
|
store.load();
|
||||||
|
const app = createApp(App);
|
||||||
|
app.provide(StoreKey, store);
|
||||||
|
app.mount(mountEl);
|
||||||
|
|
||||||
app.config.errorHandler = (error, _vm, info) => {
|
app.config.errorHandler = (error, _vm, info) => {
|
||||||
// vue fatals on errors by default, which is a weird choice
|
// vue fatals on errors by default, which is a weird choice
|
||||||
// https://github.com/vuejs/core/issues/3525
|
// https://github.com/vuejs/core/issues/3525
|
||||||
// https://github.com/vuejs/router/discussions/2435
|
// https://github.com/vuejs/router/discussions/2435
|
||||||
console.error('[VUE]', info, error)
|
console.error("[VUE]", info, error);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { createStore } from "./lib/store.ts";
|
||||||
|
import { nextTick, type InjectionKey } from "vue";
|
||||||
|
|
||||||
|
export type WidgetStore = ReturnType<typeof createWalletStore>;
|
||||||
|
export const StoreKey: InjectionKey<WidgetStore> = Symbol("WidgetStore");
|
||||||
|
|
||||||
|
export function createWalletStore(config: {
|
||||||
|
platforms: Platforms;
|
||||||
|
variables: VariableConfig;
|
||||||
|
locales: Record<string, string>;
|
||||||
|
csrfToken: string;
|
||||||
|
layoutId: string;
|
||||||
|
// platform_layouts: Record<string, PlatformLayout>;
|
||||||
|
}) {
|
||||||
|
return createStore({
|
||||||
|
state: () => ({
|
||||||
|
...config,
|
||||||
|
walletLayout: null as WalletLayout | null,
|
||||||
|
currentPlatform: config.platforms[0].identifier,
|
||||||
|
loaded: false,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
currentPlatformStyles() {
|
||||||
|
for (const platform of this.platforms) {
|
||||||
|
if (platform.identifier === this.currentPlatform) {
|
||||||
|
return platform.styles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw "Unknown platform";
|
||||||
|
},
|
||||||
|
currentPlatformLayout() {
|
||||||
|
if (!this.walletLayout) {
|
||||||
|
throw "currentPlatformLayout access before store was loaded";
|
||||||
|
}
|
||||||
|
for (const layout of this.walletLayout.platform_layouts) {
|
||||||
|
if (layout.platform === this.currentPlatform) {
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newLayout = {
|
||||||
|
platform: this.currentPlatform,
|
||||||
|
style: null,
|
||||||
|
layout: { fieldgroups: {} },
|
||||||
|
};
|
||||||
|
this.walletLayout.platform_layouts.push(newLayout);
|
||||||
|
return newLayout;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
load() {
|
||||||
|
// TODO: error handling / proper api client
|
||||||
|
fetch(
|
||||||
|
`/api/v1/organizers/demo/events/wallet/walletlayouts/${this.layoutId}/`,
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.then((x) => {
|
||||||
|
this.walletLayout = x;
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveLayout() {
|
||||||
|
// TODO: error handling / proper api client
|
||||||
|
fetch(
|
||||||
|
`/api/v1/organizers/demo/events/wallet/walletlayouts/${this.layoutId}/`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"X-CSRFToken": this.csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.walletLayout),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.catch((x) => alert(x))
|
||||||
|
.then((x) => {
|
||||||
|
this.walletLayout = x;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCurrentPlatformStyle(style: string | null) {
|
||||||
|
if (style === null) {
|
||||||
|
this.currentPlatformLayout.style = null;
|
||||||
|
this.currentPlatformLayout.layout.fieldgroups = {};
|
||||||
|
} else if (Object.keys(this.currentPlatformStyles).includes(style)) {
|
||||||
|
const oldStyle =
|
||||||
|
this.currentPlatformLayout.style !== null
|
||||||
|
? this.currentPlatformStyles[this.currentPlatformLayout.style]
|
||||||
|
: { fieldgroups: [] };
|
||||||
|
const newStyle = this.currentPlatformStyles[style];
|
||||||
|
|
||||||
|
const oldFieldGroups = Object.fromEntries(
|
||||||
|
oldStyle.fieldgroups.map((x) => [x.identifier, x]),
|
||||||
|
);
|
||||||
|
const newFieldGroups = Object.fromEntries(
|
||||||
|
newStyle.fieldgroups.map((x) => [x.identifier, x]),
|
||||||
|
);
|
||||||
|
const keysToKeep = new Set(
|
||||||
|
Object.keys(this.currentPlatformLayout.layout.fieldgroups).filter(
|
||||||
|
(x) =>
|
||||||
|
oldFieldGroups[x]?.type === "placeholder" &&
|
||||||
|
newFieldGroups[x]?.type === "placeholder" &&
|
||||||
|
oldFieldGroups[x]?.content_type ===
|
||||||
|
newFieldGroups[x]?.content_type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const keysToDefault = new Set(Object.keys(newFieldGroups)).difference(
|
||||||
|
keysToKeep,
|
||||||
|
);
|
||||||
|
const keysToRemove = new Set(
|
||||||
|
Object.keys(this.currentPlatformLayout.layout.fieldgroups),
|
||||||
|
)
|
||||||
|
.difference(keysToKeep)
|
||||||
|
.difference(keysToDefault);
|
||||||
|
|
||||||
|
for (const key of keysToRemove) {
|
||||||
|
delete this.currentPlatformLayout.layout.fieldgroups[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keysToDefault) {
|
||||||
|
if (newFieldGroups[key].type == "placeholder") {
|
||||||
|
this.currentPlatformLayout.layout.fieldgroups[key] = {overflow: null, entries: JSON.parse(JSON.stringify(newFieldGroups[key].default_entries))};
|
||||||
|
} else {
|
||||||
|
this.currentPlatformLayout.layout.fieldgroups[key] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentPlatformLayout.style = style;
|
||||||
|
}
|
||||||
|
// else if (this.currentPlatformLayout.style === null && Object.keys(this.currentPlatformStyles).includes(style)) {
|
||||||
|
// this.currentPlatformLayout.style = style;
|
||||||
|
// this.currentPlatformLayout.layout.fieldgroups = {}
|
||||||
|
// } else if (this.currentPlatformLayout.style !== null && Object.keys(this.currentPlatformStyles).includes(style)) {
|
||||||
|
// const oldStyle = this.currentPlatformStyles[this.currentPlatformLayout.style];
|
||||||
|
// const newStyle = this.currentPlatformStyles[style];
|
||||||
|
// this.currentPlatformLayout.style = style;
|
||||||
|
// console.log(oldStyle, newStyle)
|
||||||
|
// }
|
||||||
|
// this.currentPlatformLayout.style = style;
|
||||||
|
// }
|
||||||
|
// if (style == null) { }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,13 +15,16 @@ AVAILABLE_STYLES_DICT = {
|
|||||||
plat: {s.identifier: s for s in styls} for plat, styls in AVAILABLE_STYLES.items()
|
plat: {s.identifier: s for s in styls} for plat, styls in AVAILABLE_STYLES.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO? move to models?
|
# TODO? move to models?
|
||||||
def get_platform(identifier: str):
|
def get_platform(identifier: str):
|
||||||
for platform in AVAILABLE_PLATFORMS:
|
for platform in AVAILABLE_PLATFORMS:
|
||||||
if platform.identifier == identifier:
|
if platform.identifier == identifier:
|
||||||
return platform
|
return platform
|
||||||
|
|
||||||
|
|
||||||
def get_style(platform: str, identifier: str):
|
def get_style(platform: str, identifier: str):
|
||||||
return AVAILABLE_STYLES_DICT.get(platform, {}).get(identifier)
|
return AVAILABLE_STYLES_DICT.get(platform, {}).get(identifier)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AVAILABLE_PLATFORMS", "AVAILABLE_STYLES", "PassLayout"]
|
__all__ = ["AVAILABLE_PLATFORMS", "AVAILABLE_STYLES", "PassLayout"]
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ApplePlatform(WalletPlatform):
|
|||||||
order.code,
|
order.code,
|
||||||
op.pk,
|
op.pk,
|
||||||
)
|
)
|
||||||
breakpoint()
|
|
||||||
context = {
|
context = {
|
||||||
"placeholders": get_layout_variables(op.order.event),
|
"placeholders": get_layout_variables(op.order.event),
|
||||||
"evaluation_context": [op, order, order.event],
|
"evaluation_context": [op, order, order.event],
|
||||||
@@ -255,8 +255,9 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
|||||||
TextFieldGroup(
|
TextFieldGroup(
|
||||||
identifier="headers", name=_("Header"), max_entries=3
|
identifier="headers", name=_("Header"), max_entries=3
|
||||||
),
|
),
|
||||||
TextFieldGroup(identifier="auxillary", name=_("Auxillary"), max_entries=4),
|
TextFieldGroup(identifier="auxiliary", name=_("Auxiliary"), max_entries=4),
|
||||||
TextFieldGroup(identifier="back", name=_("Back")),
|
TextFieldGroup(identifier="back", name=_("Back")),
|
||||||
|
TextFieldGroup(identifier="code", name=_("QR-Code"), max_entries=1),
|
||||||
]
|
]
|
||||||
# preview_image = "apple/event_ticket.svg"
|
# preview_image = "apple/event_ticket.svg"
|
||||||
|
|
||||||
@@ -285,9 +286,10 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
|||||||
"secondaryFields": self.convert_fields(
|
"secondaryFields": self.convert_fields(
|
||||||
strings, fields["secondary"], "secondary"
|
strings, fields["secondary"], "secondary"
|
||||||
),
|
),
|
||||||
"auxillaryFields": self.convert_fields(
|
"auxiliaryFields": self.convert_fields(
|
||||||
strings, fields["auxillary"], "auxillary"
|
strings, fields["auxiliary"], "auxiliary"
|
||||||
),
|
),
|
||||||
"backFields": self.convert_fields(strings, fields["back"], "back"),
|
"backFields": self.convert_fields(strings, fields["back"], "back"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -3,7 +3,9 @@
|
|||||||
"src/pretix/static/**/*",
|
"src/pretix/static/**/*",
|
||||||
"src/pretix/static/**/*.vue",
|
"src/pretix/static/**/*.vue",
|
||||||
"src/pretix/plugins/webcheckin/**/*",
|
"src/pretix/plugins/webcheckin/**/*",
|
||||||
"src/pretix/plugins/webcheckin/**/*.vue"
|
"src/pretix/plugins/webcheckin/**/*.vue",
|
||||||
|
"src/pretix/plugins/wallet/**/*",
|
||||||
|
"src/pretix/plugins/wallet/**/*.vue"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
@@ -11,7 +13,7 @@
|
|||||||
"strict": false,
|
"strict": false,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"target": "esnext",
|
"target": "es2025",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user