diff --git a/package.json b/package.json index c20749c4eb..e58a65afae 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "build": "npm run build:control -s && npm run build:widget -s", "build:control": "vite build", "build:widget": "vite build src/pretix/static/pretixpresale/widget", - "lint:eslint": "eslint src/pretix/static/pretixpresale/widget src/pretix/static/pretixcontrol/js/ui/checkinrules src/pretix/plugins/webcheckin", + "lint:eslint": "eslint src/pretix/static/pretixpresale/widget src/pretix/static/pretixcontrol/js/ui/checkinrules src/pretix/plugins/webcheckin src/pretix/plugins/wallet", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue new file mode 100644 index 0000000000..cb59ad70d3 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue @@ -0,0 +1,42 @@ + + + + // TODO: add :key for all `v-for`s + //- pre + //- code {{ STYLES }} + .row + .col-md-8 + .form-group(:class='"name" in FORM_ERRORS ? "has-error" : ""') + label.control-label(for="layout-info-name") Name + input#layout-info-name.form-control(v-model="name" name="name") + + .form-group(:class='"style" in FORM_ERRORS ? "has-error" : ""') + label.control-label(for="layout-info-style") Style + select#layout-info-style.form-control(v-model="style" name="style") + option(v-for="styleconfig in STYLES" :key="styleconfig.identifier" :value="styleconfig.identifier") {{ styleconfig.name }} + + StyleSettings(v-model="layout" :style="style" :styles="STYLES" :variables="VARIABLES") + .col-md-4 + .panel.panel-default + .panel-heading Preview + // TODO: i18n + .panel-body + // TODO: Preview + pre + code {{ layout }} + input(type="hidden" name="layout" :value="JSON.stringify(layout)") + .form-group.submit-group + button.btn.btn-primary.btn-save(type="submit") Submit + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/field-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/field-settings.vue new file mode 100644 index 0000000000..0ba28ad736 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/field-settings.vue @@ -0,0 +1,55 @@ + + + + //- pre + //- code {{ props }} + .panel.panel-default + .panel-heading + h3.panel-title {{ field.name }} + .panel-body + .form-group(v-if="props.variables[field.entry_type]") + span.text-muted These fields appear somewhere and are visible too. + // TODO: for="..." / labeledby? + h4 Fields + .row.form-group(v-for="n in fieldConfig.entries.length") + .col-md-5 + // TODO: i18n + label.control-label Label + input.form-control(v-model="fieldConfig.entries[n-1].label") + .col-md-6 + label.control-label Value + select.form-control(v-model="fieldConfig.entries[n-1].value") + option(v-for="(config, id) in props.variables[field.entry_type]" :key="id" :value="id") {{ config.label }} + .col-md-1 + label.control-label + span.sr-only "Delete" + button.btn.btn-danger(type="button" @click="fieldConfig.entries.splice(n-1, 1)") + i.fa.fa-trash + span.sr-only "Delete" + button.btn.btn-default(type="button" @click="addVariable") + i.fa.fa-plus + span.sr-only Add field + OptionalSelect(label="Overflow to ..." :choices="overflowOptions" v-model="fieldConfig.overflow") + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/optional-select.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/optional-select.vue new file mode 100644 index 0000000000..ac738b0564 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/optional-select.vue @@ -0,0 +1,18 @@ + + + + template(v-if="choices.length >= 1") + label(:for="id") {{ props.label }} + select.form-control(:id="id" v-model="modelValue") + // TODO: persist/v-model + option(v-for="choice in props.choices" :key="choice[0]" :value="choice[0]") {{ choice[1] }} + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue new file mode 100644 index 0000000000..b8910f8f04 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/style-settings.vue @@ -0,0 +1,45 @@ + + + + h2.h3 Form Fields + FieldSettings(v-if="styleData" + v-for="(field, fieldId) in styleData.fields" + v-model="layout.fields[field.identifier]" + :field="field" + :overflows="styleData.fields.slice(fieldId+1).filter(x => x.entry_type === field.entry_type)" + :variables="variables" + ) + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts new file mode 100644 index 0000000000..ec20a3924f --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/main.ts @@ -0,0 +1,12 @@ +import { createApp } from 'vue' +import App from './components/app.vue' + +const app = createApp(App) +app.mount('#editor') + +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) +} diff --git a/src/pretix/plugins/wallet/styles.py b/src/pretix/plugins/wallet/styles.py index 6813264662..c851281cba 100644 --- a/src/pretix/plugins/wallet/styles.py +++ b/src/pretix/plugins/wallet/styles.py @@ -6,18 +6,22 @@ from django.core.exceptions import ValidationError from i18nfield.strings import LazyI18nString from .models import WalletLayout + class WalletPlatform: identifier: str name: str + class ApplePlatform(WalletPlatform): identifier = "apple" name = _("Apple") + class GooglePlatform(WalletPlatform): identifier = "google" name = _("Google") - + + class PlaceholderFieldType(enum.Enum): TEXT = "text" CODE = "qr" @@ -30,10 +34,11 @@ class PlaceholderFieldType(enum.Enum): class PlaceholderField: type: PlaceholderFieldType label: LazyI18nString - value: LazyI18nString + value: str def asdict(self): - return {'type': self.type.value, 'label': self.label, 'value': self.value} + return {"type": self.type.value, "label": self.label.data, "value": self.value} + @dataclass class FieldGroupDefinition: @@ -44,7 +49,13 @@ class FieldGroupDefinition: max_entries: int | None = None def asdict(self): - return {"identifier": self.identifier, "name": self.name, "min_entries": self.min_entries, "max_entries": self.max_entries} + return { + "identifier": self.identifier, + "name": self.name, + "entry_type": self.entry_type.value, + "min_entries": self.min_entries, + "max_entries": self.max_entries, + } @dataclass @@ -54,7 +65,7 @@ class PlaceholderFieldGroup(FieldGroupDefinition): def asdict(self): asdict = super().asdict() - asdict['default_entries'] = [x.asdict() for x in self.default_entries] + asdict["default_entries"] = [x.asdict() for x in self.default_entries] return asdict @@ -67,7 +78,7 @@ class PredefinedFieldGroup(FieldGroupDefinition): class PassStyle: identifier: str # unique within platform - name: str + name: str platform: Literal["apple"] | Literal["google"] fields: list[FieldGroupDefinition] # preview_image: str # TODO: preview @@ -94,18 +105,28 @@ class AppleWalletEventTicket(PassStyle): min_entries=1, max_entries=1, default_entries=[ - PlaceholderField(PlaceholderFieldType.IMAGE, "logo", "event:image") + PlaceholderField(PlaceholderFieldType.IMAGE, LazyI18nString("logo"), "event:image") ], entry_type=PlaceholderFieldType.IMAGE, ), - PlaceholderFieldGroup(identifier="primary", name=_("Primary"), min_entries=1, max_entries=1), + PlaceholderFieldGroup( + identifier="primary", + name=_("Primary"), + min_entries=1, + max_entries=1, + default_entries=[ + PlaceholderField(PlaceholderFieldType.TEXT, LazyI18nString("Ticket type"), "item") + ], + ), PlaceholderFieldGroup( identifier="secondary", name=_("Secondary"), max_entries=4 ), # TODO: validation of max field count if combined "Coupons, store cards, and generic passes with a square barcode can have a total of up to four secondary and auxiliary fields, combined." PlaceholderFieldGroup( identifier="headers", name=_("Header"), max_entries=3 ), # TODO: header image - PlaceholderFieldGroup(identifier="auxillary", name=_("Auxillary"), max_entries=4), + PlaceholderFieldGroup( + identifier="auxillary", name=_("Auxillary"), max_entries=4 + ), PlaceholderFieldGroup(identifier="back", name=_("Back")), ] # preview_image = "apple/event_ticket.svg" @@ -117,13 +138,16 @@ class GoogleWalletEventTicket(PassStyle): platform = "google" fields = [ PredefinedFieldGroup(identifier="seating", name=_("Seating")), - PlaceholderFieldGroup(identifier="qrcode", name=_("QR-Code"), entry_type=PlaceholderFieldType.CODE), + PlaceholderFieldGroup( + identifier="qrcode", name=_("QR-Code"), entry_type=PlaceholderFieldType.CODE + ), ] AVAILABLE_PLATFORMS = {"apple": ApplePlatform, "google": GooglePlatform} AVAILABLE_STYLES = [AppleWalletEventTicket(), GoogleWalletEventTicket()] + def get_platforms_with_styles(): platforms_with_styles = {} for style in AVAILABLE_STYLES: @@ -133,6 +157,7 @@ def get_platforms_with_styles(): platforms_with_styles[platform][style.identifier] = style return platforms_with_styles + def get_platform_styles(platform): platform_styles = {} for style in AVAILABLE_STYLES: @@ -140,9 +165,11 @@ def get_platform_styles(platform): platform_styles[style.identifier] = style return platform_styles + def get_platforms(): return AVAILABLE_PLATFORMS + class PassLayout: style: PassStyle layout: dict @@ -156,15 +183,19 @@ class PassLayout: def validate_fields(self): style_fields = self.style.fields - if 'fields' not in self.layout: + if "fields" not in self.layout: raise ValidationError(_("Layout did not contain any fields")) - layout_fields = self.layout['fields'] + layout_fields = self.layout["fields"] if not isinstance(layout_fields, dict): raise ValidationError(_("'fields' must be dict")) for fieldgroup in style_fields: - layout_field_data = layout_fields.get(fieldgroup.identifier, []) - if fieldgroup.min_entries and fieldgroup.min_entries < len(layout_field_data): - raise ValidationError(_("At least {min_entries} must be specified for {name}").format(min_entries=fieldgroup.min_entries, name=fieldgroup.name)) - - \ No newline at end of file + layout_field_data = layout_fields.get(fieldgroup.identifier, {}) + if fieldgroup.min_entries and fieldgroup.min_entries < len( + layout_field_data.get('entries') + ): + raise ValidationError( + _("At least {min_entries} must be specified for {name}").format( + min_entries=fieldgroup.min_entries, name=fieldgroup.name + ) + ) diff --git a/src/pretix/plugins/wallet/templates/pretixplugins/wallet/edit.html b/src/pretix/plugins/wallet/templates/pretixplugins/wallet/edit.html index 4bd6c08e46..e0d869cbb0 100644 --- a/src/pretix/plugins/wallet/templates/pretixplugins/wallet/edit.html +++ b/src/pretix/plugins/wallet/templates/pretixplugins/wallet/edit.html @@ -2,18 +2,23 @@ {% load i18n %} {% load money %} {% load bootstrap3 %} +{% load vite %} +{% load static %} +{% load compress %} + {% block title %}{% trans "Wallet layouts" %}{% endblock %} + {% block content %} -
{{ styles }}
- {{ variables }}
+ + +
+