mirror of
https://github.com/pretix/pretix.git
synced 2026-08-06 10:07:50 +00:00
wallet wip
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
||||
- 'src/pretix/static/pretixpresale/widget/**'
|
||||
- 'src/pretix/static/pretixcontrol/js/ui/checkinrules/**'
|
||||
- 'src/pretix/plugins/webcheckin/**'
|
||||
- 'src/pretix/plugins/wallet/**'
|
||||
- 'eslint.config.mjs'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
@@ -16,6 +17,7 @@ on:
|
||||
- 'src/pretix/static/pretixpresale/widget/**'
|
||||
- 'src/pretix/static/pretixcontrol/js/ui/checkinrules/**'
|
||||
- 'src/pretix/plugins/webcheckin/**'
|
||||
- 'src/pretix/plugins/wallet/**'
|
||||
- 'eslint.config.mjs'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
|
||||
@@ -1,67 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { computed, inject, ref, watchEffect } from "vue";
|
||||
import StyleSettings from "./style-settings.vue";
|
||||
import Select from "./input/select.vue";
|
||||
import Input from "./input/input.vue";
|
||||
import { StoreKey } from "../walletStore";
|
||||
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const isLoading = ref<boolean>(true);
|
||||
const wallet_layout = ref<Layout | null>(null);
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
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;
|
||||
});
|
||||
const platformChoices = computed(() => {
|
||||
return [[null, "Do not generate pass"], ...Object.values(store.currentPlatformStyles).map(x => [x.identifier, x.name])]
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -82,33 +34,9 @@ function openForm(url: string, data: Record<string, string>) {
|
||||
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)})
|
||||
openForm("../../preview/", {"csrfmiddlewaretoken": store.csrfToken, "platform": store.currentPlatform, "style": store.currentPlatformLayout.style, "layout": JSON.stringify(store.currentPlatformLayout.layout)})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -116,31 +44,41 @@ function openPreview(e: SubmitEvent) {
|
||||
// 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")
|
||||
|
||||
template(v-if="!store.loaded") {{ gettext("Loading...") }}
|
||||
form(v-else @submit.prevent="store.saveLayout")
|
||||
.form-group
|
||||
Input(label="Name" v-model="wallet_layout.name")
|
||||
Input(label="Name" v-model="store.walletLayout.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 }}
|
||||
li(v-for="platform in store.platforms" :class="{'active': store.currentPlatform === platform.identifier}")
|
||||
a(role="tab" @click="store.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")
|
||||
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
|
||||
.panel.panel-default
|
||||
.panel-heading Preview
|
||||
.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
|
||||
pre
|
||||
code {{ platformLayout }}
|
||||
pre(v-if="wallet_layout.style")
|
||||
code {{ platformStyles[wallet_layout.style] }}
|
||||
code {{ store.currentPlatformLayout }}
|
||||
pre(v-if="store.currentPlatformLayout.style")
|
||||
code {{ store.currentPlatformStyles[store.currentPlatformLayout.style] }}
|
||||
pre
|
||||
code {{ wallet_layout }}
|
||||
code {{ store.walletLayout }}
|
||||
.form-group.submit-group
|
||||
button.btn.btn-lg.btn-default(type="button" @click="openPreview") Preview
|
||||
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">
|
||||
import { watchEffect } from 'vue'
|
||||
import { inject, watchEffect } from 'vue'
|
||||
import { StoreKey } from "../../walletStore";
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
@@ -7,19 +10,18 @@ defineOptions({
|
||||
|
||||
const props = defineProps<{
|
||||
errors?: string[],
|
||||
locales: Record<string, string>
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<Record<string, string> | string>();
|
||||
watchEffect(() => {
|
||||
if (typeof modelValue.value === "string") {
|
||||
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>
|
||||
|
||||
<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 }}
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
label?: string
|
||||
choices: Array<[string, string]>
|
||||
errors?: string[],
|
||||
class: string
|
||||
class?: string
|
||||
}>()
|
||||
const modelValue = defineModel<string|null>();
|
||||
const id = useId()
|
||||
|
||||
+7
-8
@@ -1,17 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watchEffect } from "vue";
|
||||
import { computed, inject, reactive, watchEffect } from "vue";
|
||||
import Select from "./input/select.vue";
|
||||
import Input from "./input/input.vue";
|
||||
import I18nInput from "./input/i18ninput.vue";
|
||||
import TextContent from "./text-content.vue";
|
||||
import { StoreKey } from "../walletStore";
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const props = defineProps<{
|
||||
fieldgroup: PlaceholderFieldGroupDefinition;
|
||||
overflows: FieldGroupDefinition[];
|
||||
variables: Variables;
|
||||
locales: Record<string, string>;
|
||||
}>();
|
||||
const fieldConfig = defineModel<PlaceholderFieldGroupConfig>({ required: true });
|
||||
|
||||
@@ -58,15 +59,13 @@ watchEffect(() => {
|
||||
tr(v-for="n,i in fieldConfig.entries.length" :key="i")
|
||||
td(v-if="fieldgroup.labels")
|
||||
.i18n-form-group
|
||||
I18nInput(v-model="fieldConfig.entries[n-1].label" :locales="locales")
|
||||
I18nInput(v-model="fieldConfig.entries[n-1].label")
|
||||
td
|
||||
TextContent(v-if='fieldgroup.content_type == "text"'
|
||||
v-model="fieldConfig.entries[n-1]"
|
||||
:variables="props.variables"
|
||||
:locales="locales")
|
||||
v-model="fieldConfig.entries[n-1]")
|
||||
Select(v-else-if='fieldgroup.content_type == "image"'
|
||||
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
|
||||
button.btn.btn-danger.form-control-static(type="button" @click="fieldConfig.entries.splice(n-1, 1)")
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watchEffect } from "vue";
|
||||
import { computed, inject, watchEffect } from "vue";
|
||||
import PlaceholderFieldSettings from "./placeholder-field-settings.vue";
|
||||
import PredefinedFieldSettings from "./predefined-field-settings.vue";
|
||||
|
||||
const gettext = (window as any).gettext;
|
||||
|
||||
const props = defineProps<{
|
||||
variables: VariableConfig
|
||||
style?: Style;
|
||||
locales: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const layout = defineModel<LayoutData>();
|
||||
@@ -32,8 +30,6 @@ watchEffect(() => {
|
||||
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
||||
:fieldgroup="fieldgroup"
|
||||
: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'"
|
||||
v-model="layout.fieldgroups[fieldgroup.identifier]"
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
import { computed, inject, reactive } from 'vue'
|
||||
import Select from './input/select.vue'
|
||||
import I18nInput from './input/i18ninput.vue'
|
||||
import { StoreKey } from "../walletStore";
|
||||
|
||||
const store = inject(StoreKey)!
|
||||
const gettext = (window as any).gettext
|
||||
|
||||
const props = defineProps<{
|
||||
variables: Variables
|
||||
locales: Record<string, string>;
|
||||
}>()
|
||||
const entry = defineModel<FieldEntry>({ required: true })
|
||||
|
||||
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…")])
|
||||
return choices
|
||||
});
|
||||
@@ -23,11 +21,9 @@ const selection = computed({
|
||||
return entry.value.content
|
||||
} else if (entry.value.type === 'custom') {
|
||||
return "other"
|
||||
} else {
|
||||
throw new Error(`Unknown entry type "${entry.value.type}"`);
|
||||
}
|
||||
},
|
||||
set(newValue) {
|
||||
set(newValue) {
|
||||
if (newValue == "other") {
|
||||
entry.value.type = "custom"
|
||||
entry.value.content = {};
|
||||
@@ -44,11 +40,9 @@ const textContent = computed({
|
||||
return ""
|
||||
} else if (entry.value.type === 'custom') {
|
||||
return entry.value.content
|
||||
} else {
|
||||
throw new Error(`Unknown entry type "${entry.value.type}"`);
|
||||
}
|
||||
},
|
||||
set(newValue) {
|
||||
set(newValue) {
|
||||
entry.value.content = newValue
|
||||
}
|
||||
})
|
||||
@@ -61,5 +55,5 @@ const textContent = computed({
|
||||
v-model="selection"
|
||||
:choices="selectChoices"
|
||||
)
|
||||
I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="locales")
|
||||
I18nInput(v-model="textContent" v-if="selection === 'other'" :locales="store.locales")
|
||||
</template>
|
||||
|
||||
@@ -30,13 +30,13 @@ type PlaceholderFieldEntry = {
|
||||
content?: string;
|
||||
}
|
||||
|
||||
type ContentFieldEntry = {
|
||||
type: FieldContentType;
|
||||
type CustomFieldEntry = {
|
||||
type: 'custom';
|
||||
label?: I18nString;
|
||||
content?: I18nString;
|
||||
}
|
||||
|
||||
type FieldEntry = PlaceholderFieldEntry | ContentFieldEntry;
|
||||
type FieldEntry = PlaceholderFieldEntry | CustomFieldEntry;
|
||||
|
||||
type Style = {
|
||||
identifier: string;
|
||||
@@ -57,7 +57,7 @@ type Platform = {
|
||||
type Styles = Record<string, Style>;
|
||||
type Variables = Record<string, Variable>;
|
||||
type VariableConfig = Record<string, Variables>;
|
||||
type Platforms = Record<string, Platform>;
|
||||
type Platforms = Platform[];
|
||||
|
||||
|
||||
type PlaceholderFieldGroupConfig = {
|
||||
@@ -73,9 +73,20 @@ type LayoutData = {
|
||||
fieldgroups: Record<string, FieldGroupConfig>;
|
||||
};
|
||||
|
||||
type Layout = {
|
||||
name?: string;
|
||||
style?: string;
|
||||
layout?: LayoutData;
|
||||
type PlatformLayout = {
|
||||
platform: string;
|
||||
style: string | null;
|
||||
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 App from './components/app.vue'
|
||||
|
||||
const mountEl = document.querySelector<HTMLElement>('#editor')!
|
||||
const app = createApp(App, mountEl.dataset)
|
||||
app.mount(mountEl)
|
||||
import { createApp } from "vue";
|
||||
import App from "./components/app.vue";
|
||||
import { createWalletStore, StoreKey } from "./walletStore";
|
||||
const mountEl = document.querySelector<HTMLElement>("#editor")!;
|
||||
const store = createWalletStore({
|
||||
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) => {
|
||||
// vue fatals on errors by default, which is a weird choice
|
||||
// https://github.com/vuejs/core/issues/3525
|
||||
// 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()
|
||||
}
|
||||
|
||||
|
||||
# TODO? move to models?
|
||||
def get_platform(identifier: str):
|
||||
for platform in AVAILABLE_PLATFORMS:
|
||||
if platform.identifier == identifier:
|
||||
return platform
|
||||
|
||||
|
||||
|
||||
def get_style(platform: str, identifier: str):
|
||||
return AVAILABLE_STYLES_DICT.get(platform, {}).get(identifier)
|
||||
|
||||
|
||||
__all__ = ["AVAILABLE_PLATFORMS", "AVAILABLE_STYLES", "PassLayout"]
|
||||
|
||||
@@ -44,7 +44,7 @@ class ApplePlatform(WalletPlatform):
|
||||
order.code,
|
||||
op.pk,
|
||||
)
|
||||
breakpoint()
|
||||
|
||||
context = {
|
||||
"placeholders": get_layout_variables(op.order.event),
|
||||
"evaluation_context": [op, order, order.event],
|
||||
@@ -255,8 +255,9 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
||||
TextFieldGroup(
|
||||
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="code", name=_("QR-Code"), max_entries=1),
|
||||
]
|
||||
# preview_image = "apple/event_ticket.svg"
|
||||
|
||||
@@ -285,9 +286,10 @@ class AppleWalletEventTicket(AppleWalletStyle):
|
||||
"secondaryFields": self.convert_fields(
|
||||
strings, fields["secondary"], "secondary"
|
||||
),
|
||||
"auxillaryFields": self.convert_fields(
|
||||
strings, fields["auxillary"], "auxillary"
|
||||
"auxiliaryFields": self.convert_fields(
|
||||
strings, fields["auxiliary"], "auxiliary"
|
||||
),
|
||||
"backFields": self.convert_fields(strings, fields["back"], "back"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -3,7 +3,9 @@
|
||||
"src/pretix/static/**/*",
|
||||
"src/pretix/static/**/*.vue",
|
||||
"src/pretix/plugins/webcheckin/**/*",
|
||||
"src/pretix/plugins/webcheckin/**/*.vue"
|
||||
"src/pretix/plugins/webcheckin/**/*.vue",
|
||||
"src/pretix/plugins/wallet/**/*",
|
||||
"src/pretix/plugins/wallet/**/*.vue"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
@@ -11,7 +13,7 @@
|
||||
"strict": false,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"target": "esnext",
|
||||
"target": "es2025",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user