diff options
| author | Thomas Hackl <hackl@data-quest.de> | 2026-01-22 12:46:10 +0100 |
|---|---|---|
| committer | Thomas Hackl <hackl@data-quest.de> | 2026-03-17 09:01:06 +0100 |
| commit | 529259be12d09ce9171169a1801bb8a6772c36a3 (patch) | |
| tree | d2a7ddda4f30956a851c233156b352dfb280c879 | |
| parent | 6b9e76775e2dd805037e6ee0835dc35c3df33e08 (diff) | |
introduce store for forms
| -rw-r--r-- | app/controllers/wizard.php | 237 | ||||
| -rw-r--r-- | lib/classes/WizardPart.php | 23 | ||||
| -rw-r--r-- | lib/classes/forms/Form.php | 23 | ||||
| -rw-r--r-- | lib/navigation/StartNavigation.php | 3 | ||||
| -rw-r--r-- | resources/assets/javascripts/lib/forms.js | 19 | ||||
| -rw-r--r-- | resources/vue/apps/StudipWizard.vue | 38 | ||||
| -rw-r--r-- | resources/vue/store/pinia/formsStore.js | 25 | ||||
| -rw-r--r-- | templates/forms/form.php | 5 |
8 files changed, 105 insertions, 268 deletions
diff --git a/app/controllers/wizard.php b/app/controllers/wizard.php deleted file mode 100644 index 8278a92..0000000 --- a/app/controllers/wizard.php +++ /dev/null @@ -1,237 +0,0 @@ -<?php - -use Studip\WizardPart; - -class WizardController extends AuthenticatedController -{ - - public function before_filter(&$action, &$args) - { - $GLOBALS['perm']->check('root'); - - parent::before_filter($action, $args); - } - - public function index_action() - { - PageLayout::setTitle('Wizard'); - - $form = Studip\Forms\Form::fromSORM( - User::findCurrent(), - [ - 'legend' => _('Herzlich willkommen!'), - 'fields' => [ - 'username' => [ - 'label' => _('Benutzername'), - 'required' => true, - 'maxlength' => '63', - 'attributes' => ['autocomplete' => 'off'], - 'validate' => function ($value, $input) { - if (!preg_match(Config::get()->USERNAME_REGULAR_EXPRESSION, $value)) { - return Config::get()->getMetadata('USERNAME_REGULAR_EXPRESSION')['comment'] ?: - _('Benutzername muss mindestens 4 Zeichen lang sein und darf nur aus Buchstaben, ' - . 'Ziffern, Unterstrich, @, Punkt und Minus bestehen.'); - } - $user = User::findByUsername($value); - $context = $input->getContextObject(); - if ($user && ($user->id !== $context->getId())) { - return _('Benutzername ist schon vergeben.'); - } - return true; - } - ], - 'password' => [ - 'label' => _('Passwort'), - 'type' => 'password', - 'required' => true, - 'maxlength' => '31', - 'minlength' => '8', - 'attributes' => ['autocomplete' => 'new-password'], - 'mapper' => function($value) { - $hasher = UserManagement::getPwdHasher(); - return $hasher->HashPassword($value); - } - ], - 'confirm_password' => [ - 'label' => _('Passwortbestätigung'), - 'type' => 'password', - 'required' => true, - 'maxlength' => '31', - 'minlength' => '8', - 'attributes' => ['autocomplete' => 'new-password'], - ':pattern' => "password.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&')", //mask special chars - 'data-validation_requirement' => _('Die Passwörter stimmen nicht überein.'), - 'store' => function() {} - ], - 'title_front' => [ - 'label' => _('Titel'), - 'type' => 'datalist', - 'attributes' => ['autocomplete' => 'honorific-prefix'], - 'options' => $GLOBALS['TITLE_FRONT_TEMPLATE'] - ], - 'title_rear' => [ - 'label' => _('Titel nachgestellt'), - 'type' => 'datalist', - 'attributes' => ['autocomplete' => 'honorific-suffix'], - 'options' => $GLOBALS['TITLE_REAR_TEMPLATE'], - ], - 'vorname' => [ - 'label' => _('Vorname'), - 'attributes' => ['autocomplete' => 'given-name'], - 'required' => true - ], - 'nachname' => [ - 'label' => _('Nachname'), - 'attributes' => ['autocomplete' => 'family-name'], - 'required' => true - ], - 'geschlecht' => [ - 'name' => 'geschlecht', - 'value' => 0, - 'label' => _('Geschlecht'), - 'type' => 'radio', - 'orientation' => 'horizontal', - 'options' => [ - '0' => _('Keine Angabe'), - '1' => _('männlich'), - '2' => _('weiblich'), - '3' => _('divers'), - ], - ], - 'email' => [ - 'label' => _('E-Mail'), - 'required' => true, - 'attributes' => ['autocomplete' => 'email'], - 'validate' => function ($value, $input) { - $user = User::findOneByEmail($value); - $context = $input->getContextObject(); - if ($user && ($user->id !== $context->getId())) { - return _('Diese Emailadresse ist bereits registriert.'); - } - return true; - } - ], - ] - ] - )->noButtons(); - - $form2 = \Studip\Forms\Form::create()->noButtons(); - $details_part = new \Studip\Forms\Fieldset(_('Angaben zur gefundenen Barriere')); - $details_part->addInput( - new \Studip\Forms\SelectInput( - 'barrier_type', - _('Um welche Art von Barriere handelt es sich?'), - '', - [ - 'options' => [ - _('Inhalte auf dieser Seite (z.B. PDF, Bilder oder Lernmodule)') => _('Inhalte auf dieser Seite (z.B. PDF, Bilder oder Lernmodule)'), - _('Ein Problem mit der Seite selbst oder der Navigation') => _('Ein Problem mit der Seite selbst oder der Navigation'), - _('Sonstiges') => _('Sonstiges') - ] - ] - ) - )->setRequired(); - $details_part->addInput( - new \Studip\Forms\TextareaInput( - 'barrier_details', - _('Beschreiben Sie die Barriere'), - '' - ) - )->setRequired(); - $form2->addPart($details_part); - $personal_data_part = new \Studip\Forms\Fieldset(_('Ihre persönlichen Daten')); - $personal_data_part->addText(sprintf('<p>%s</p>', _('Freiwillige Angaben Ihrer Kontaktdaten für etwaige Rückfragen.'))); - $personal_data_part->addInput( - new \Studip\Forms\SelectInput( - 'salutation', - _('Anrede'), - 'Keine Angabe', - [ - 'options' => [ - _('Keine Angabe') => _('Keine Angabe'), - _('Frau') => _('Frau'), - _('Herr') => _('Herr'), - _('divers') => _('divers') - ] - ] - ) - ); - $personal_data_part->addInput( - new \Studip\Forms\TextInput( - 'name', - _('Vorname und Nachname'), - '' - ) - ); - $personal_data_part->addInput( - new \Studip\Forms\TextInput( - 'phone_number', - _('Telefonnummer'), - '' - ) - ); - $personal_data_part->addInput( - new \Studip\Forms\TextInput( - 'email_address', - _('E-Mail-Adresse'), - '' - ) - ); - $form2->addPart($personal_data_part); - - $steps = [ - WizardPart::create( - Studip\VueApp::create('massmail/MassMailMessagesList'), - 'Nachrichtenübersicht', - 'mail2' - ), - WizardPart::create( - Studip\VueApp::create('CacheAdministration') - ->withProps([ - 'enabled' => true, - 'currentCache' => StudipDbCache::class, - 'currentConfig' => StudipDbCache::getConfig(), - 'cacheTypes' => CacheType::findAndMapBySQL( - fn(CacheType $type) => $type->toArray(), - "1 ORDER BY `cache_id`" - ), - ]), - 'Cache', - 'admin' - ), - WizardPart::create( - Studip\VueApp::create('ColourSelector') - ->withProps([ - 'autofocus' => true, - 'colours' => collect($GLOBALS['PERS_TERMIN_KAT'])->map( - fn($data, $id) => ['id' => $id, 'colour' => $data['bgcolor']] - )->values() - ]), - 'Farbwähler', - 'colorpicker' - ), - WizardPart::create( - Studip\VueApp::create('ThemeSettings') - ->withVuexStore( - 'theme-settings.module.js', - 'theme-settings-module', - [ - 'setUserId' => User::findCurrent()->id, - ] - ), - 'Themes', - 'style' - ), - ]; - - /*$steps = [ - WizardPart::create($form, 'User form', 'person'), - WizardPart::create($form2, 'Report barrier', 'accessibility') - ];*/ - - Sidebar::Get()->addWidget(new VueWidget('wizard-sidebar')); - - $this->render_wizard($steps); - } - -} diff --git a/lib/classes/WizardPart.php b/lib/classes/WizardPart.php index a932ce9..abf3144 100644 --- a/lib/classes/WizardPart.php +++ b/lib/classes/WizardPart.php @@ -19,6 +19,7 @@ use Stringable, final class WizardPart implements Stringable, JsonSerializable { + private string $id; private string $type; private Form|VueApp $content; private string $title; @@ -27,19 +28,25 @@ final class WizardPart implements Stringable, JsonSerializable /** * Creates a vue app with the given relative path to the app component. */ - public static function create(Form|VueApp $content, string $title = '', string $iconShape = ''): WizardPart + public static function create(string $id, Form|VueApp $content, string $title = '', string $iconShape = ''): WizardPart { - return new static($content, $title, $iconShape); + return new static($id, $content, $title, $iconShape); } - public function __construct(Form|VueApp $content, string $title = '', string $iconShape = '') + public function __construct(string $id, Form|VueApp $content, string $title = '', string $iconShape = '') { + $this->id = $id; $this->type = get_class($content); $this->content = $content; $this->title = $title; $this->iconShape = $iconShape; } + public function getId(): string + { + return $this->id; + } + public function getType(): string { return $this->type; @@ -50,6 +57,16 @@ final class WizardPart implements Stringable, JsonSerializable return $this->title; } + public function getContent() { + return $this->content; + } + + public function setId(string $id): WizardPart + { + $this->id = $id; + return $this; + } + public function setTitle(string $title): WizardPart { $this->title = $title; diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php index f4208c4..f34b68d 100644 --- a/lib/classes/forms/Form.php +++ b/lib/classes/forms/Form.php @@ -24,7 +24,7 @@ class Form extends Part protected $cancel_button_name = ''; protected $autoStore = false; protected $debugmode = false; - protected $emitValues = false; + protected $useStore = false; protected $withButtons = true; protected $success_message = ''; @@ -347,22 +347,33 @@ class Form extends Part } /** - * This form doesn't submit to a URL but just emits its values on submitting via eventbus. + * Set whether this form uses a Pinia store instead of submitting to a URL. + * @param bool $value + * @return $this + */ + public function useStore(bool $value = true) + { + $this->useStore = $value; + return $this; + } + + /** + * This form doesn't call a URL but writes its values to the formStore on submit. * @return bool|mixed */ - public function justEmitsValues() + public function usesStore() { - return $this->emitValues; + return $this->useStore; } /** - * Don't provide buttons for submitting the form -> this form will just emit its values. + * Don't provide buttons for submitting the form -> this form will use the pinia store for submitting its values.. * @return Form $this */ public function noButtons() { $this->withButtons = false; - $this->emitValues = true; + $this->useStore = true; return $this; } diff --git a/lib/navigation/StartNavigation.php b/lib/navigation/StartNavigation.php index c1dd40e..9266eb5 100644 --- a/lib/navigation/StartNavigation.php +++ b/lib/navigation/StartNavigation.php @@ -109,9 +109,6 @@ class StartNavigation extends Navigation // my courses if ($perm->have_perm('root')) { - $navigation = new Navigation(_('Wizard'), 'dispatch.php/wizard'); - $this->addSubNavigation('wizard', $navigation); - $navigation = new Navigation(_('Veranstaltungsübersicht'), 'dispatch.php/admin/courses'); } else if ($perm->have_perm('admin')) { $navigation = new Navigation(_('Veranstaltungen an meinen Einrichtungen'), 'dispatch.php/my_courses'); diff --git a/resources/assets/javascripts/lib/forms.js b/resources/assets/javascripts/lib/forms.js index e4d54a2..7c66997 100644 --- a/resources/assets/javascripts/lib/forms.js +++ b/resources/assets/javascripts/lib/forms.js @@ -5,6 +5,7 @@ import Report from "./report"; import {$gettext} from "./gettext"; import Dialog from "./dialog"; +import {useFormsStore} from '@/vue/store/pinia/formsStore'; const Forms = { initialized: false, @@ -78,6 +79,8 @@ const Forms = { params.STUDIPFORM_INPUTS_ORDER = []; params.STUDIPFORM_SELECTEDLANGUAGES = {}; params.STUDIPFORM_EMIT_VALUES = f.dataset.emit; + params.STUDIPFORM_USE_STORE = f.dataset.useStore === 'true'; + params.STUDIPFORM_FORM_ID = f.dataset.formId; for (let i in JSON.parse(f.dataset.inputs)) { params.STUDIPFORM_INPUTS_ORDER.push(i); } @@ -94,10 +97,11 @@ const Forms = { // validation: this.validate() .then(() => { - if (this.STUDIPFORM_EMIT_VALUES) { - STUDIP.eventBus.emit('form.emitValues', this.getFormValues()); + if (this.STUDIPFORM_USE_STORE) { + const store = useFormsStore(); + store.initialize(); + store.setData(this.STUDIPFORM_FORM_ID, this.getFormValues()); } else { - console.log('After validating'); if (this.STUDIPFORM_AUTOSAVEURL) { let params = this.getFormValues(); params.STUDIPFORM_AUTOSTORE = 1; @@ -175,12 +179,13 @@ const Forms = { }); // Optional server validation - if (this.STUDIPFORM_SERVERVALIDATION) { + if (this.STUDIPFORM_SERVERVALIDATION && !this.STUDIPFORM_USE_STORE) { let params = this.getFormValues(); if (this.STUDIPFORM_AUTOSAVEURL) { params.STUDIPFORM_AUTOSTORE = 1; } params.STUDIPFORM_SERVERVALIDATION = 1; + params.STUDIPFORM_FORM_ID = this.STUDIPFORM_FORM_ID; const output = await fetch(this.STUDIPFORM_VALIDATION_URL, { method: 'POST', @@ -246,6 +251,12 @@ const Forms = { }) } } + + STUDIP.Vue.on('form.submit', id => { + if (this.$data.STUDIPFORM_FORM_ID === id) { + this.submit(new Event('submit')); + } + }); } }); const instance = app.mount(f); diff --git a/resources/vue/apps/StudipWizard.vue b/resources/vue/apps/StudipWizard.vue index 97dca49..1ee82c8 100644 --- a/resources/vue/apps/StudipWizard.vue +++ b/resources/vue/apps/StudipWizard.vue @@ -26,7 +26,8 @@ <h2> {{ visibleSteps[currentStep].title }} </h2> - <div ref="node" data-vue-app></div> + <div v-if="currentStepType === 'app'" ref="node" data-vue-app></div> + <div v-if="currentStepType === 'form'" ref="node" v-html="stepContent"></div> <footer class="wizard-buttons"> <button v-if="currentStep !== 0" class="button back-button" @@ -70,9 +71,10 @@ </template> <script setup> -import {onMounted, ref, provide} from 'vue'; +import {nextTick, onMounted, ref} from 'vue'; import {$gettext} from '@/assets/javascripts/lib/gettext'; import {useWizardStore} from '@/vue/store/pinia/wizardStore'; +import {useFormsStore} from '@/vue/store/pinia/formsStore'; import SidebarWidget from '@/vue/components/SidebarWidget'; const props = defineProps({ @@ -93,22 +95,27 @@ const currentStep = ref(0); // HTML content of current step let stepContent = ref(''); let mountedApp = null; +const currentStepType = ref(null); const visibleSteps = ref(props.showAllSteps ? props.steps : [props.steps[0]]); const store = useWizardStore(); - -provide('storedValues', store.getData()); +const formsStore = useFormsStore(); const jumpToStep = (number) => { - if (!visibleSteps.value.includes(props.steps[number])) { + + if (currentStepType.value === 'form') { + STUDIP.Vue.emit('form.submit', 'userdata'); + } + + /*if (!visibleSteps.value.includes(props.steps[number])) { visibleSteps.value[number] = props.steps[number]; } if (mountedApp !== null) { mountedApp.unmount(); } currentStep.value = number; - initializeContent(number); + initializeContent(number);*/ }; const finishWizard = () => { @@ -116,17 +123,23 @@ const finishWizard = () => { }; const initializeContent = async (stepNumber) => { - stepContent.value = JSON.parse(props.steps[stepNumber].content); if (props.steps[stepNumber].type === 'Studip\\Forms\\Form') { - STUDIP.Forms.create(node.value.childNodes); + currentStepType.value = 'form'; + stepContent.value = props.steps[stepNumber].content; + nextTick(() => { + STUDIP.Forms.create(node.value.childNodes); + }); } else if (props.steps[stepNumber].type === 'Studip\\VueApp') { - STUDIP.Vue.mountApp(node.value, props.steps[stepNumber].content); + currentStepType.value = 'app'; + stepContent.value = JSON.parse(props.steps[stepNumber].content); + nextTick(() => { + STUDIP.Vue.mountApp(node.value, props.steps[stepNumber].content); + }); } }; onMounted(() => { initializeContent(0); - store.initialize(); STUDIP.Vue.on('form.mounted', (mounted) => { mountedApp = mounted.app; @@ -136,9 +149,8 @@ onMounted(() => { mountedApp = mounted.app; } }); - STUDIP.Vue.on('form.emitValues', (values) => { - store.setValues(currentStep.value, values); - }); + STUDIP.Report.info('Info'); + STUDIP.Report.warning('Warning'); }); </script> diff --git a/resources/vue/store/pinia/formsStore.js b/resources/vue/store/pinia/formsStore.js new file mode 100644 index 0000000..64cd28b --- /dev/null +++ b/resources/vue/store/pinia/formsStore.js @@ -0,0 +1,25 @@ +import {defineStore} from 'pinia'; +import {ref} from 'vue'; + +export const useFormsStore = defineStore('forms', () => { + let data = ref({}); + + function initialize() { + data.value = {}; + } + + function getData(id) { + return data.value[id]; + } + + function setData(id, value) { + console.log('setData', id, value); + data.value[id] = value; + } + + return { + initialize, + getData, + setData + }; +}); diff --git a/templates/forms/form.php b/templates/forms/form.php index d1730f7..3197980 100644 --- a/templates/forms/form.php +++ b/templates/forms/form.php @@ -27,12 +27,13 @@ $form_id = md5(uniqid()); data-required="<?= htmlReady(json_encode($required_inputs)) ?>" data-server_validation="<?= $server_validation ? 1 : 0 ?>" data-validation_url="<?= htmlReady($_SERVER['REQUEST_URI']) ?>" + data-form-id="<?= htmlReady($form->getId()) ?>" <? if ($form->isAutoStoring()) : ?> data-autosave="<?= htmlReady($_SERVER['REQUEST_URI']) ?>" data-url="<?= htmlReady($form->getURL()) ?>" <? endif; ?> -<? if ($form->justEmitsValues()) : ?> - data-emit="true" + <? if ($form->usesStore()) : ?> + data-use-store="true" <? endif; ?> > <form method="post" |
