aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Hackl <hackl@data-quest.de>2026-01-22 12:46:10 +0100
committerThomas Hackl <hackl@data-quest.de>2026-03-17 09:01:06 +0100
commit529259be12d09ce9171169a1801bb8a6772c36a3 (patch)
treed2a7ddda4f30956a851c233156b352dfb280c879
parent6b9e76775e2dd805037e6ee0835dc35c3df33e08 (diff)
introduce store for forms
-rw-r--r--app/controllers/wizard.php237
-rw-r--r--lib/classes/WizardPart.php23
-rw-r--r--lib/classes/forms/Form.php23
-rw-r--r--lib/navigation/StartNavigation.php3
-rw-r--r--resources/assets/javascripts/lib/forms.js19
-rw-r--r--resources/vue/apps/StudipWizard.vue38
-rw-r--r--resources/vue/store/pinia/formsStore.js25
-rw-r--r--templates/forms/form.php5
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"