refactor(webapp): vue 3 migration - ds-form (#9407)

This commit is contained in:
Ulf Gebhardt 2026-03-20 18:14:59 +01:00 committed by GitHub
parent 16c4f03d3f
commit 906ac801be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 521 additions and 415 deletions

View File

@ -14,7 +14,8 @@ Phase 3: OsButton ██████████ 100% (133/133 Buttons) ✅
Phase 4: Tier 1 ██████████ 100% (OsButton, OsIcon, OsSpinner, OsCard) ✅ Phase 4: Tier 1 ██████████ 100% (OsButton, OsIcon, OsSpinner, OsCard) ✅
Phase 4: Tier A → HTML ██████████ 100% (10 ds-* Wrapper → Plain HTML) ✅ Phase 4: Tier A → HTML ██████████ 100% (10 ds-* Wrapper → Plain HTML) ✅
Phase 4: Tier B ██████████ 100% (ds-chip→OsBadge✅, ds-tag→OsBadge✅, ds-grid✅, ds-number→OsNumber✅, ds-radio→HTML✅) Phase 4: Tier B ██████████ 100% (ds-chip→OsBadge✅, ds-tag→OsBadge✅, ds-grid✅, ds-number→OsNumber✅, ds-radio→HTML✅)
Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅) | Tier 2 begonnen (OsModal✅) | Rest ausstehend (OsInput, OsMenu, OsSelect) Phase 4: Tier B ██████████ 100% (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Radio→HTML, Table→HTML) ✅
Phase 4: Tier 2+ ██████░░░░ 50% (OsModal✅, ds-form entkoppelt✅) | Rest ausstehend (OsInput, OsMenu, OsSelect, OsDropdown, OsAvatar)
``` ```
### Statistiken ### Statistiken
@ -29,7 +30,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
| ✅ → OsNumber | Number (5 Nutzungen → OsNumber, CountTo.vue gelöscht, vue-count-to entfernt) | | ✅ → OsNumber | Number (5 Nutzungen → OsNumber, CountTo.vue gelöscht, vue-count-to entfernt) |
| ✅ → Plain HTML | Radio (1 Datei → native `<input type="radio">` in ReportModal) | | ✅ → Plain HTML | Radio (1 Datei → native `<input type="radio">` in ReportModal) |
| ⬜ → UI-Library | Modal, Input, Menu, MenuItem, Select (5) — Tier 2-3 | | ⬜ → UI-Library | Modal, Input, Menu, MenuItem, Select (5) — Tier 2-3 |
| ⬜ Offen | Form (18 Dateien — HTML oder OsForm?) | | ✅ ds-form entkoppelt | Form-Validierung → formValidation Mixin (async-validator), ds-input/ds-select bleiben als UI-Komponenten |
| ⬜ Nicht in Webapp | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) | | ⬜ Nicht in Webapp | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
### OsButton Migration (Phase 3) ✅ ### OsButton Migration (Phase 3) ✅
@ -38,7 +39,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
**Erkenntnisse aus der Migration:** **Erkenntnisse aus der Migration:**
- `type="submit"` muss explizit gesetzt werden (OsButton Default: `type="button"`) - `type="submit"` muss explizit gesetzt werden (OsButton Default: `type="button"`)
- DsForm `errors` ist ein Objekt → `!!errors` für Boolean-Cast bei `:disabled` - `formErrors` ist ein Objekt → `!!formErrors` für Boolean-Cast bei `:disabled`
- CSS `.base-button` Selektoren → `> button` oder `button` - CSS `.base-button` Selektoren → `> button` oder `button`
- Filter-Buttons nutzen `:appearance="condition ? 'filled' : 'outline'"` Pattern - Filter-Buttons nutzen `:appearance="condition ? 'filled' : 'outline'"` Pattern
- Circle-Buttons mit Icon: `<template #icon><os-icon :icon="..." /></template>` - Circle-Buttons mit Icon: `<template #icon><os-icon :icon="..." /></template>`
@ -438,19 +439,19 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
14. [x] ds-chip (5 Dateien, 20 Nutzungen) → OsBadge (UI-Library) 14. [x] ds-chip (5 Dateien, 20 Nutzungen) → OsBadge (UI-Library)
15. [x] ds-tag (3 Dateien) → OsBadge shape="square" (UI-Library) 15. [x] ds-tag (3 Dateien) → OsBadge shape="square" (UI-Library)
16. [x] ds-grid / ds-grid-item (10 Dateien) → CSS Grid (Plain HTML) 16. [x] ds-grid / ds-grid-item (10 Dateien) → CSS Grid (Plain HTML)
17. [ ] ds-number (5 Dateien) → `<div class="ds-number">` 17. [x] ds-number (5 Dateien) → OsNumber (UI-Library) ✅
18. [x] ds-radio (1 Datei) → native `<input type="radio">` + `<fieldset>` (ReportModal) ✅ 18. [x] ds-radio (1 Datei) → native `<input type="radio">` + `<fieldset>` (ReportModal) ✅
### Phase 4: Tier 2-4 — UI-Library ### Phase 4: Tier 2-4 — UI-Library
18. [x] OsModal (h() Render, Focus-Trap, Scroll-Lock, A11y; ConfirmModal + ReportModal nutzen OsModal; DeleteUserModal/DisableModal/ReleaseModal gelöscht) ✅ 19. [x] OsModal (h() Render, Focus-Trap, Scroll-Lock, A11y; ConfirmModal + ReportModal nutzen OsModal; DeleteUserModal/DisableModal/ReleaseModal gelöscht) ✅
19. [ ] OsInput (23 Dateien, gekoppelt mit ds-form) 20. [x] ds-form → formValidation Mixin (async-validator), 18 Dateien migriert, vuelidate entfernt ✅
20. [ ] OsMenu / OsMenuItem (17 Dateien) 21. [ ] OsInput (23 Dateien)
21. [ ] OsSelect (3 Dateien), OsTable (7 Dateien) 22. [ ] OsMenu / OsMenuItem (17 Dateien)
22. [ ] ds-form → HTML `<form>` oder OsForm (18 Dateien) 23. [ ] OsSelect (3 Dateien), OsTable (7 Dateien)
--- ---
**✅ Phase 0-3 abgeschlossen. Phase 4: Tier 1 + Tier A ✅, Tier B 80% (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Table→HTML), Tier 2: OsModal ✅, Rest ausstehend.** **✅ Phase 0-3 abgeschlossen. Phase 4: Tier 1 + Tier A ✅, Tier B (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Radio→HTML, Table→HTML), Tier 2: OsModal ✅, ds-form entkoppelt ✅, Rest ausstehend (OsInput, OsMenu, OsSelect).**
--- ---
@ -948,14 +949,14 @@ interface OsDropdownProps {
| — | ds-space | ✅ → div + Margin-Utility-Klassen | | — | ds-space | ✅ → div + Margin-Utility-Klassen |
| — | ds-flex, ds-flex-item | ✅ → HTML + CSS @media Queries | | — | ds-flex, ds-flex-item | ✅ → HTML + CSS @media Queries |
### Tier B: Einfache ds-* Migration (60%) ### Tier B: Einfache ds-* Migration
| # | Komponente | Dateien | Ziel | Status | | # | Komponente | Dateien | Ziel | Status |
|---|------------|---------|------|--------| |---|------------|---------|------|--------|
| 5 | **OsBadge** | — | ds-chip (20 Nutzungen, 5 Dateien) + ds-tag (3 Dateien) | ✅ | | 5 | **OsBadge** | — | ds-chip (20 Nutzungen, 5 Dateien) + ds-tag (3 Dateien) | ✅ |
| — | ds-grid / ds-grid-item | 10 | CSS Grid (Plain HTML) | ✅ | | — | ds-grid / ds-grid-item | 10 | CSS Grid (Plain HTML) | ✅ |
| — | ds-number | 5 | `<div class="ds-number">` | ⬜ | | — | ds-number | 5 | OsNumber (UI-Library) | ✅ |
| — | ds-radio | 1 | native `<input type="radio">` | | | — | ds-radio | 1 | native `<input type="radio">` | |
### Tier 2: Layout & Feedback ### Tier 2: Layout & Feedback
@ -964,7 +965,7 @@ interface OsDropdownProps {
| 5 | **OsModal** | 7 | OsButton | ✅ | | 5 | **OsModal** | 7 | OsButton | ✅ |
| 6 | **OsDropdown** | — | OsButton | ⬜ | | 6 | **OsDropdown** | — | OsButton | ⬜ |
| 7 | **OsAvatar** | — | - | ⬜ | | 7 | **OsAvatar** | — | - | ⬜ |
| 8 | **OsInput** | 23 | gekoppelt mit ds-form (18 Dateien) | ⬜ | | 8 | **OsInput** | 23 | ds-form Kopplung aufgelöst (formValidation Mixin) | ⬜ |
### Tier 3: Navigation (ausstehend) ### Tier 3: Navigation (ausstehend)
@ -979,7 +980,7 @@ interface OsDropdownProps {
|---|------------|---------| |---|------------|---------|
| 11 | OsSelect | 3 | | 11 | OsSelect | 3 |
| 12 | OsTable | 7 | | 12 | OsTable | 7 |
| 13 | ds-form → HTML `<form>` oder OsForm | 18 | | 13 | ~~ds-form~~ | — | ✅ entkoppelt via formValidation Mixin (async-validator) |
> **Hinweis:** OsHeading, OsText, OsTag sind nicht mehr geplant — wurden zu Plain HTML migriert (Tier A). > **Hinweis:** OsHeading, OsText, OsTag sind nicht mehr geplant — wurden zu Plain HTML migriert (Tier A).
@ -1014,17 +1015,17 @@ ds-flex, ds-flex-item ✅ → HTML + CSS @media Q
ds-chip → OsBadge (UI-Library) ✅ ds-chip → OsBadge (UI-Library) ✅
ds-tag → OsBadge shape="square" (UI-Library) ✅ ds-tag → OsBadge shape="square" (UI-Library) ✅
ds-grid / ds-grid-item → CSS Grid (HTML) ✅ ds-grid / ds-grid-item → CSS Grid (HTML) ✅
ds-number → Plain HTML ⬜ (5 Dateien) ds-number → OsNumber (UI-Library) ✅
ds-radio → native <input type="radio"> (1 Datei) ds-radio → native <input type="radio"> (1 Datei)
``` ```
### Tier 2-4: UI-Library (ausstehend) ### Tier 2-4: UI-Library
``` ```
5. OsModal → Basis: DsModal, Feature-Modals bleiben in Webapp 5. OsModal → Basis: DsModal, Feature-Modals bleiben in Webapp
6. OsDropdown → Basis: Dropdown (Webapp) — wichtiger als gedacht! 6. OsDropdown → Basis: Dropdown (Webapp) — wichtiger als gedacht!
7. OsAvatar → Vereint: DsAvatar + ProfileAvatar 7. OsAvatar → Vereint: DsAvatar + ProfileAvatar
8. OsInput → Basis: DsInput, gekoppelt mit ds-form 8. OsInput → Basis: DsInput (ds-form Kopplung aufgelöst via formValidation Mixin)
9. OsMenu → Basis: DsMenu/DsMenuItem 9. OsMenu → Basis: DsMenu/DsMenuItem
10. OsSelect → Basis: DsSelect 10. OsSelect → Basis: DsSelect
11. OsTable → Basis: DsTable 11. OsTable → Basis: DsTable
@ -1047,8 +1048,8 @@ ds-radio → native <input type="radio"> ⬜ (1 Datei)
### Noch offen: ### Noch offen:
1. **Logo** - Existiert doppelt (Webapp + Styleguide) 1. **Logo** - Existiert doppelt (Webapp + Styleguide)
2. **Modal** - Existiert doppelt (Webapp Modal.vue ist Modal-Router, DsModal ist UI) 2. ~~**Modal** - Existiert doppelt~~ → OsModal migriert ✅
3. **ds-form Kopplung** - ds-input und ds-form sind stark gekoppelt (Schema-Validation) 3. ~~**ds-form Kopplung**~~ → aufgelöst via formValidation Mixin (async-validator), vuelidate entfernt ✅
--- ---

View File

@ -289,7 +289,8 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
- [x] 0 Tier-A `ds-*` Komponenten-Tags verbleibend - [x] 0 Tier-A `ds-*` Komponenten-Tags verbleibend
**Verbleibende ds-* Komponenten (6 Typen):** **Verbleibende ds-* Komponenten (6 Typen):**
- Tier C (→ UI-Library): ds-input (23), ds-form (18), ds-modal (7), ds-menu/ds-menu-item (17), ds-select (3) - Tier C (→ UI-Library): ds-input (23), ds-modal (7→✅ OsModal), ds-menu/ds-menu-item (17), ds-select (3)
- ✅ ds-form (18 Dateien) → formValidation Mixin (async-validator), vuelidate entfernt
**Zuvor abgeschlossen (Session 26 - CodeRabbit Review Fixes):** **Zuvor abgeschlossen (Session 26 - CodeRabbit Review Fixes):**
- [x] Cypress: `.os-card .title``.os-card > .title` (Kind-Kombinator statt Nachfahren) - [x] Cypress: `.os-card .title``.os-card > .title` (Kind-Kombinator statt Nachfahren)
@ -415,7 +416,8 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
- [ ] Tier B (Rest): ds-radio → Plain HTML - [ ] Tier B (Rest): ds-radio → Plain HTML
- [x] OsModal Komponente + DsModal/ConfirmModal/ReportModal → OsModal Webapp-Integration ✅ - [x] OsModal Komponente + DsModal/ConfirmModal/ReportModal → OsModal Webapp-Integration ✅
- [ ] Weitere Tier 2 Komponenten (OsDropdown, OsAvatar, OsInput) - [ ] Weitere Tier 2 Komponenten (OsDropdown, OsAvatar, OsInput)
- [ ] ds-form + ds-input → OsForm + OsInput (stark gekoppelt, 18+23 Dateien) - [x] ds-form → formValidation Mixin (async-validator), 18 Dateien migriert, vuelidate entfernt ✅
- [ ] ds-input → OsInput (23 Dateien, ds-form Kopplung aufgelöst)
- [ ] ds-menu / ds-menu-item → OsMenu / OsMenuItem - [ ] ds-menu / ds-menu-item → OsMenu / OsMenuItem
- [ ] ds-select → OsSelect - [ ] ds-select → OsSelect
- [ ] Browser-Fehler untersuchen: `TypeError: Cannot read properties of undefined (reading 'heartO')` (ocelotIcons undefined im Browser trotz korrekter Webpack-Aliase) - [ ] Browser-Fehler untersuchen: `TypeError: Cannot read properties of undefined (reading 'heartO')` (ocelotIcons undefined im Browser trotz korrekter Webpack-Aliase)
@ -686,13 +688,13 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
- [x] ds-tag (3 Dateien) → OsBadge shape="square" (UI-Library) ✅ - [x] ds-tag (3 Dateien) → OsBadge shape="square" (UI-Library) ✅
- [x] ds-number (5 Dateien) → OsNumber (UI-Library) ✅ + CountTo.vue gelöscht, vue-count-to entfernt - [x] ds-number (5 Dateien) → OsNumber (UI-Library) ✅ + CountTo.vue gelöscht, vue-count-to entfernt
- [x] ds-grid / ds-grid-item (10 Dateien) → CSS Grid ✅ - [x] ds-grid / ds-grid-item (10 Dateien) → CSS Grid ✅
- [ ] ds-radio (1 Datei) → native `<input type="radio">` - [x] ds-radio (1 Datei) → native `<input type="radio">`
**Tier 2: Layout & Feedback (UI-Library)** **Tier 2: Layout & Feedback (UI-Library)**
- [x] OsModal (Basis: DsModal → h() Render-Function, Vue 2/3 Compat, Focus-Trap, Scroll-Lock, A11y) ✅ - [x] OsModal (Basis: DsModal → h() Render-Function, Vue 2/3 Compat, Focus-Trap, Scroll-Lock, A11y) ✅
- [ ] OsDropdown (Basis: Webapp Dropdown) - [ ] OsDropdown (Basis: Webapp Dropdown)
- [ ] OsAvatar (vereint DsAvatar + ProfileAvatar) - [ ] OsAvatar (vereint DsAvatar + ProfileAvatar)
- [ ] OsInput (Basis: DsInput, 23 Dateien — gekoppelt mit ds-form) - [ ] OsInput (Basis: DsInput, 23 Dateien — ds-form Kopplung aufgelöst via formValidation Mixin)
**Tier 3: Navigation (UI-Library)** **Tier 3: Navigation (UI-Library)**
- [ ] OsMenu (Basis: DsMenu, 11 Dateien) - [ ] OsMenu (Basis: DsMenu, 11 Dateien)
@ -701,7 +703,7 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
**Tier 4: Spezial-Komponenten** **Tier 4: Spezial-Komponenten**
- [ ] OsSelect (3 Dateien) - [ ] OsSelect (3 Dateien)
- [x] ds-table (7 Dateien) → Plain HTML `<table>` + CSS-Klassen ✅ (kein OsTable nötig) - [x] ds-table (7 Dateien) → Plain HTML `<table>` + CSS-Klassen ✅ (kein OsTable nötig)
- [ ] ds-form → Plain HTML `<form>` oder OsForm (18 Dateien) - [x] ds-form → formValidation Mixin (async-validator), vuelidate entfernt ✅
**Infrastruktur** **Infrastruktur**
- [x] System-Icons einrichten ✅ vite-svg-icon Plugin, 3 System-Icons, Ocelot-Icons Entry-Point - [x] System-Icons einrichten ✅ vite-svg-icon Plugin, 3 System-Icons, Ocelot-Icons Entry-Point
@ -1703,11 +1705,11 @@ Bei der Migration werden:
| 2026-02-11 | **Milestone 4b abgeschlossen** | icon ✅, circle ✅, loading ✅ — alle OsButton-Props implementiert | | 2026-02-11 | **Milestone 4b abgeschlossen** | icon ✅, circle ✅, loading ✅ — alle OsButton-Props implementiert |
| 2026-02-11 | **Milestone 4c: 59 Buttons** | Chat (2), AddChatRoomByUserSearch (1), CommentCard (1), CommentForm (2), ComponentSlider (2), ContributionForm (1), DeleteData (1), EmbedComponent (1), FilterMenu (1), HeaderButton (2), CategoriesFilter (2), OrderByFilter (2), EventsByFilter (2), FollowingFilter (3), GroupButton (1), ConfirmModal (2), ReportModal (2), Password/Change (1), PasswordReset/Request (1), PasswordReset/ChangePassword (1), Registration/Signup (1), ReleaseModal (1), ImageUploader (2), CreateInvitation (1), Invitation (2), ProfileList (1), ReportRow (1), MySomethingList (3), ActionButton (1), pages/index (2), profile/add-post (1), post/blur-toggle (1), groups/slug (3), settings/index (1), admin/users (2), blocked-users (1), data-download (1), muted-users (1), groups/index (1), enter-nonce (1) | | 2026-02-11 | **Milestone 4c: 59 Buttons** | Chat (2), AddChatRoomByUserSearch (1), CommentCard (1), CommentForm (2), ComponentSlider (2), ContributionForm (1), DeleteData (1), EmbedComponent (1), FilterMenu (1), HeaderButton (2), CategoriesFilter (2), OrderByFilter (2), EventsByFilter (2), FollowingFilter (3), GroupButton (1), ConfirmModal (2), ReportModal (2), Password/Change (1), PasswordReset/Request (1), PasswordReset/ChangePassword (1), Registration/Signup (1), ReleaseModal (1), ImageUploader (2), CreateInvitation (1), Invitation (2), ProfileList (1), ReportRow (1), MySomethingList (3), ActionButton (1), pages/index (2), profile/add-post (1), post/blur-toggle (1), groups/slug (3), settings/index (1), admin/users (2), blocked-users (1), data-download (1), muted-users (1), groups/index (1), enter-nonce (1) |
| 2026-02-11 | **type="submit" Pattern** | OsButton hat `type="button"` als Default; alle Form-Submit-Buttons brauchen explizit `type="submit"` | | 2026-02-11 | **type="submit" Pattern** | OsButton hat `type="button"` als Default; alle Form-Submit-Buttons brauchen explizit `type="submit"` |
| 2026-02-11 | **!!errors Pattern** | DsForm `errors` ist ein Objekt, nicht Boolean; OsButton `disabled` Prop erwartet Boolean → `!!errors` nötig | | 2026-02-11 | **!!errors Pattern** | `formErrors` ist ein Objekt, nicht Boolean; OsButton `disabled` Prop erwartet Boolean → `!!formErrors` nötig |
| 2026-02-11 | **CSS-Selector Pattern** | `.base-button``> button` oder `button`; Position/Dimensions brauchen `!important` für Tailwind-Override | | 2026-02-11 | **CSS-Selector Pattern** | `.base-button``> button` oder `button`; Position/Dimensions brauchen `!important` für Tailwind-Override |
| 2026-02-11 | **Disabled border-color** | Outline disabled border von `var(--color-disabled)` auf `var(--color-disabled-border,#e5e3e8)` mit Fallback | | 2026-02-11 | **Disabled border-color** | Outline disabled border von `var(--color-disabled)` auf `var(--color-disabled-border,#e5e3e8)` mit Fallback |
| 2026-02-11 | **Phase 3 abgeschlossen** | 132 `<os-button>` Tags in 78 Dateien, 0 `<base-button>` in Templates verbleibend | | 2026-02-11 | **Phase 3 abgeschlossen** | 132 `<os-button>` Tags in 78 Dateien, 0 `<base-button>` in Templates verbleibend |
| 2026-02-11 | **Password/Change.vue Fix** | `!!errors` für disabled-Prop (DsForm errors ist Objekt) | | 2026-02-11 | **Password/Change.vue Fix** | `!!errors` für disabled-Prop (formErrors ist Objekt) |
| 2026-02-11 | **CommentForm.vue Fix** | `type="submit"` fehlte + `!!errors` für disabled-Prop | | 2026-02-11 | **CommentForm.vue Fix** | `type="submit"` fehlte + `!!errors` für disabled-Prop |
| 2026-02-11 | **GroupForm.vue ds-button** | Letzter `<ds-button>` in Webapp → `<os-button>` mit `#icon` Slot migriert | | 2026-02-11 | **GroupForm.vue ds-button** | Letzter `<ds-button>` in Webapp → `<os-button>` mit `#icon` Slot migriert |
| 2026-02-11 | **OsButton.spec.ts TS-Fix** | `size` aus `Object.entries` als Union Type gecastet (`as 'sm' | 'md' | 'lg' | 'xl'`) | | 2026-02-11 | **OsButton.spec.ts TS-Fix** | `size` aus `Object.entries` als Union Type gecastet (`as 'sm' | 'md' | 'lg' | 'xl'`) |
@ -1847,6 +1849,9 @@ Bei der Migration werden:
| 2026-03-13 | **Modal Webapp-Integration** | ConfirmModal + ReportModal nutzen OsModal; Vuex Modal Store entfernt; Modals inline gerendert | | 2026-03-13 | **Modal Webapp-Integration** | ConfirmModal + ReportModal nutzen OsModal; Vuex Modal Store entfernt; Modals inline gerendert |
| 2026-03-13 | **Modal Bugfixes** | z-index Stacking Context Fix (PostTeaser/GroupTeaser), Callback-Promise Propagation (ReportList, MySomethingList), Group Leave Authorization Fix ($nextTick), Cypress .ds-modal → .os-modal | | 2026-03-13 | **Modal Bugfixes** | z-index Stacking Context Fix (PostTeaser/GroupTeaser), Callback-Promise Propagation (ReportList, MySomethingList), Group Leave Authorization Fix ($nextTick), Cypress .ds-modal → .os-modal |
| 2026-03-13 | **Modal A11y** | scrollable-region-focusable Fix (tabindex=0), aria-label Fallback wenn kein Title, body overflow save/restore | | 2026-03-13 | **Modal A11y** | scrollable-region-focusable Fix (tabindex=0), aria-label Fallback wenn kein Title, body overflow save/restore |
| 2026-03-14 | **ds-form entkoppelt** | Neues `formValidation` Mixin (async-validator): provide/subscribe Pattern, formData/formSchema/formErrors, handleInput/handleInputValid Callbacks; vuelidate komplett entfernt |
| 2026-03-14 | **18 Formulare migriert** | CommentForm, ContributionForm, EnterNonce, GroupForm, Password/Change, PasswordReset (2), Registration (5), Signup, MySomethingList, donations, admin/users, settings (3) |
| 2026-03-20 | **formValidation Fix** | `handleInput()` vor `$validateForm()` aufrufen (Reihenfolge-Bug: handleInput überschrieb handleInputValid bei synchronem async-validator Callback) |
--- ---
@ -1865,12 +1870,11 @@ Bei der Migration werden:
| Status | Komponenten | | Status | Komponenten |
|--------|------------| |--------|------------|
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge, OsNumber, OsModal (7) | | ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge, OsNumber, OsModal (7) |
| ✅ → Plain HTML | Section, Placeholder, List, ListItem, Container, Heading, Text, Space, Flex, FlexItem, Grid, GridItem, Table (13) — Tier A/B | | ✅ → Plain HTML | Section, Placeholder, List, ListItem, Container, Heading, Text, Space, Flex, FlexItem, Grid, GridItem, Table, Radio (14) — Tier A/B |
| ✅ → UI-Library | Chip, Tag → OsBadge (2), Number → OsNumber (1) — Tier B | | ✅ → UI-Library | Chip, Tag → OsBadge (2), Number → OsNumber (1) — Tier B |
| ⬜ → Plain HTML | Radio (1) — Tier B | | ✅ ds-form entkoppelt | Form-Validierung → formValidation Mixin (async-validator), vuelidate entfernt, ds-input/ds-select bleiben als UI-Komponenten |
| ⬜ → UI-Library | Input, Menu, MenuItem, Select (4) — Tier 2-3 | | ⬜ → UI-Library | Input, Menu, MenuItem, Select (4) — Tier 2-3 |
| ⬜ Nicht genutzt | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) | | ⬜ Nicht genutzt | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
| ⬜ Offen | Form (18 Dateien — HTML `<form>` oder OsForm?) |
--- ---

View File

@ -1,34 +1,32 @@
<template> <template>
<ds-form v-model="form" @submit="handleSubmit" class="comment-form"> <form @submit.prevent="handleSubmit" class="comment-form" novalidate>
<template #default="{ errors }"> <os-card>
<os-card> <hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" /> <div class="buttons">
<div class="buttons"> <os-button
<os-button variant="primary"
variant="primary" appearance="outline"
appearance="outline" :disabled="disabled && !update"
:disabled="disabled && !update" @click="handleCancel"
@click="handleCancel" data-test="cancel-button"
data-test="cancel-button" >
> {{ $t('actions.cancel') }}
{{ $t('actions.cancel') }} </os-button>
</os-button> <os-button
<os-button variant="primary"
variant="primary" appearance="filled"
appearance="filled" type="submit"
type="submit" :loading="loading"
:loading="loading" :disabled="disabled"
:disabled="disabled || !!errors" >
> <template #icon>
<template #icon> <os-icon :icon="icons.comment" />
<os-icon :icon="icons.comment" /> </template>
</template> {{ $t('post.comment.submit') }}
{{ $t('post.comment.submit') }} </os-button>
</os-button> </div>
</div> </os-card>
</os-card> </form>
</template>
</ds-form>
</template> </template>
<script> <script>

View File

@ -1,13 +1,7 @@
<template> <template>
<div> <div>
<ds-form <form class="contribution-form" @submit.prevent="onSubmit" novalidate>
class="contribution-form" <template>
ref="contributionForm"
v-model="formData"
:schema="formSchema"
@submit="submit"
>
<template #default="{ errors }">
<os-card> <os-card>
<template #heroImage> <template #heroImage>
<img <img
@ -42,10 +36,10 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.title ? 'danger' : undefined" :variant="formErrors && formErrors.title ? 'danger' : undefined"
> >
{{ formData.title.length }}/{{ formSchema.title.max }} {{ formData.title.length }}/{{ formSchema.title.max }}
<os-icon v-if="errors && errors.title" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.title" :icon="icons.warning" />
</os-badge> </os-badge>
<editor <editor
:users="users" :users="users"
@ -56,10 +50,10 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.content ? 'danger' : undefined" :variant="formErrors && formErrors.content ? 'danger' : undefined"
> >
{{ contentLength }} {{ contentLength }}
<os-icon v-if="errors && errors.content" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.content" :icon="icons.warning" />
</os-badge> </os-badge>
<!-- Eventdata --> <!-- Eventdata -->
@ -86,13 +80,13 @@
></date-picker> ></date-picker>
</div> </div>
<div <div
v-if="errors && errors.eventStart" v-if="formErrors && formErrors.eventStart"
class="chipbox event-grid-item-margin-helper" class="chipbox event-grid-item-margin-helper"
> >
<os-badge <os-badge
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
:variant="errors && errors.eventStart ? 'danger' : undefined" :variant="formErrors && formErrors.eventStart ? 'danger' : undefined"
> >
<os-icon :icon="icons.warning" /> <os-icon :icon="icons.warning" />
</os-badge> </os-badge>
@ -129,10 +123,10 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.eventVenue ? 'danger' : undefined" :variant="formErrors && formErrors.eventVenue ? 'danger' : undefined"
> >
{{ formData.eventVenue.length }}/{{ formSchema.eventVenue.max }} {{ formData.eventVenue.length }}/{{ formSchema.eventVenue.max }}
<os-icon v-if="errors && errors.eventVenue" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.eventVenue" :icon="icons.warning" />
</os-badge> </os-badge>
</div> </div>
</div> </div>
@ -146,10 +140,13 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.eventLocationName ? 'danger' : undefined" :variant="formErrors && formErrors.eventLocationName ? 'danger' : undefined"
> >
{{ formData.eventLocationName.length }}/{{ formSchema.eventLocationName.max }} {{ formData.eventLocationName.length }}/{{ formSchema.eventLocationName.max }}
<os-icon v-if="errors && errors.eventLocationName" :icon="icons.warning" /> <os-icon
v-if="formErrors && formErrors.eventLocationName"
:icon="icons.warning"
/>
</os-badge> </os-badge>
</div> </div>
</div> </div>
@ -177,10 +174,10 @@
v-if="categoriesActive" v-if="categoriesActive"
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.categoryIds ? 'danger' : undefined" :variant="formErrors && formErrors.categoryIds ? 'danger' : undefined"
> >
{{ formData.categoryIds.length }} / 3 {{ formData.categoryIds.length }} / 3
<os-icon v-if="errors && errors.categoryIds" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.categoryIds" :icon="icons.warning" />
</os-badge> </os-badge>
<div class="ds-flex ds-flex-gap-xxx-small buttons-footer"> <div class="ds-flex ds-flex-gap-xxx-small buttons-footer">
<div style="flex: 3.5 0 0" class="buttons-footer-helper"> <div style="flex: 3.5 0 0" class="buttons-footer-helper">
@ -207,7 +204,7 @@
appearance="filled" appearance="filled"
type="submit" type="submit"
:loading="loading" :loading="loading"
:disabled="!!errors" :disabled="!!formErrors"
> >
<template #icon> <template #icon>
<os-icon :icon="icons.check" /> <os-icon :icon="icons.check" />
@ -218,7 +215,7 @@
</div> </div>
</os-card> </os-card>
</template> </template>
</ds-form> </form>
</div> </div>
</template> </template>
<script> <script>
@ -235,9 +232,10 @@ import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParams
import DatePicker from 'vue2-datepicker' import DatePicker from 'vue2-datepicker'
import 'vue2-datepicker/scss/index.scss' import 'vue2-datepicker/scss/index.scss'
import GetCategories from '~/mixins/getCategoriesMixin.js' import GetCategories from '~/mixins/getCategoriesMixin.js'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [GetCategories], mixins: [GetCategories, formValidation],
components: { components: {
CategoriesSelect, CategoriesSelect,
DatePicker, DatePicker,
@ -424,6 +422,9 @@ export default {
notBeforeEvent(date) { notBeforeEvent(date) {
return date <= new Date(this.formData.eventStart) return date <= new Date(this.formData.eventStart)
}, },
onSubmit() {
this.formSubmit(this.submit)
},
submit() { submit() {
let image = null let image = null
@ -470,16 +471,16 @@ export default {
}) })
}, },
updateEditorContent(value) { updateEditorContent(value) {
this.$refs.contributionForm.update('content', value) this.updateFormField('content', value)
}, },
changeEventIsOnline(event) { changeEventIsOnline(event) {
this.$refs.contributionForm.update('eventIsOnline', this.formData.eventIsOnline) this.updateFormField('eventIsOnline', this.formData.eventIsOnline)
}, },
changeEventEnd(event) { changeEventEnd(event) {
this.$refs.contributionForm.update('eventEnd', event) this.updateFormField('eventEnd', event)
}, },
changeEventStart(event) { changeEventStart(event) {
this.$refs.contributionForm.update('eventStart', event) this.updateFormField('eventStart', event)
}, },
addHeroImage(file) { addHeroImage(file) {
this.formData.image = null this.formData.image = null

View File

@ -1,12 +1,5 @@
<template> <template>
<ds-form <form class="enter-nonce" @submit.prevent="onSubmit" novalidate>
class="enter-nonce"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitVerify"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-input <ds-input
:placeholder="$t('components.registration.email-nonce.form.nonce')" :placeholder="$t('components.registration.email-nonce.form.nonce')"
model="nonce" model="nonce"
@ -30,15 +23,17 @@
{{ $t('components.registration.email-nonce.form.next') }} {{ $t('components.registration.email-nonce.form.next') }}
</os-button> </os-button>
<slot></slot> <slot></slot>
</ds-form> </form>
</template> </template>
<script> <script>
import { OsButton } from '@ocelot-social/ui' import { OsButton } from '@ocelot-social/ui'
import registrationConstants from '~/constants/registration' import registrationConstants from '~/constants/registration'
import formValidation from '~/mixins/formValidation'
export default { export default {
name: 'EnterNonce', name: 'EnterNonce',
mixins: [formValidation],
components: { OsButton }, components: { OsButton },
props: { props: {
email: { type: String, required: true }, email: { type: String, required: true },
@ -69,6 +64,9 @@ export default {
async handleInputValid() { async handleInputValid() {
this.disabled = false this.disabled = false
}, },
onSubmit() {
this.formSubmit(this.handleSubmitVerify)
},
handleSubmitVerify() { handleSubmitVerify() {
const { nonce } = this.formData const { nonce } = this.formData
const email = this.email const email = this.email

View File

@ -1,14 +1,7 @@
<template> <template>
<div> <div>
<ds-form <form class="group-form" @submit.prevent="onSubmit" novalidate>
class="group-form" <template>
ref="groupForm"
v-model="formData"
:schema="formSchema"
@submit="submit"
>
<!-- "errors" is only working if you use a submit event on the form -->
<template #default="{ errors }">
<!-- group Name --> <!-- group Name -->
<ds-input <ds-input
name="name" name="name"
@ -20,10 +13,10 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.name ? 'danger' : undefined" :variant="formErrors && formErrors.name ? 'danger' : undefined"
> >
{{ `${formData.name.length} / ${formSchema.name.min}${formSchema.name.max}` }} {{ `${formData.name.length} / ${formSchema.name.min}${formSchema.name.max}` }}
<os-icon v-if="errors && errors.name" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.name" :icon="icons.warning" />
</os-badge> </os-badge>
<!-- group Slug --> <!-- group Slug -->
@ -56,11 +49,13 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.groupType && formData.groupType === '' ? 'danger' : undefined" :variant="
formErrors && formErrors.groupType && formData.groupType === '' ? 'danger' : undefined
"
> >
{{ `${formData.groupType === '' ? 0 : 1} / 1` }} {{ `${formData.groupType === '' ? 0 : 1} / 1` }}
<os-icon <os-icon
v-if="errors && errors.groupType && formData.groupType === ''" v-if="formErrors && formErrors.groupType && formData.groupType === ''"
:icon="icons.warning" :icon="icons.warning"
/> />
</os-badge> </os-badge>
@ -91,10 +86,10 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.description ? 'danger' : undefined" :variant="formErrors && formErrors.description ? 'danger' : undefined"
> >
{{ `${descriptionLength} / ${formSchema.description.min}` }} {{ `${descriptionLength} / ${formSchema.description.min}` }}
<os-icon v-if="errors && errors.description" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.description" :icon="icons.warning" />
</os-badge> </os-badge>
<!-- actionRadius --> <!-- actionRadius -->
@ -109,12 +104,14 @@
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant=" :variant="
errors && errors.actionRadius && formData.actionRadius === '' ? 'danger' : undefined formErrors && formErrors.actionRadius && formData.actionRadius === ''
? 'danger'
: undefined
" "
> >
{{ `${formData.actionRadius === '' ? 0 : 1} / 1` }} {{ `${formData.actionRadius === '' ? 0 : 1} / 1` }}
<os-icon <os-icon
v-if="errors && errors.actionRadius && formData.actionRadius === ''" v-if="formErrors && formErrors.actionRadius && formData.actionRadius === ''"
:icon="icons.warning" :icon="icons.warning"
/> />
</os-badge> </os-badge>
@ -138,10 +135,10 @@
<os-badge <os-badge
role="status" role="status"
aria-live="polite" aria-live="polite"
:variant="errors && errors.categoryIds ? 'danger' : undefined" :variant="formErrors && formErrors.categoryIds ? 'danger' : undefined"
> >
{{ formData.categoryIds.length }} / 3 {{ formData.categoryIds.length }} / 3
<os-icon v-if="errors && errors.categoryIds" :icon="icons.warning" /> <os-icon v-if="formErrors && formErrors.categoryIds" :icon="icons.warning" />
</os-badge> </os-badge>
</div> </div>
<!-- submit --> <!-- submit -->
@ -153,14 +150,14 @@
variant="primary" variant="primary"
appearance="filled" appearance="filled"
type="submit" type="submit"
:disabled="checkFormError(errors)" :disabled="checkFormError(formErrors)"
> >
<template #icon><os-icon :icon="icons.save" /></template> <template #icon><os-icon :icon="icons.save" /></template>
{{ update ? $t('group.update') : $t('group.save') }} {{ update ? $t('group.update') : $t('group.save') }}
</os-button> </os-button>
</div> </div>
</template> </template>
</ds-form> </form>
</div> </div>
</template> </template>
@ -178,10 +175,11 @@ import Editor from '~/components/Editor/Editor'
import ActionRadiusSelect from '~/components/Select/ActionRadiusSelect' import ActionRadiusSelect from '~/components/Select/ActionRadiusSelect'
import LocationSelect from '~/components/Select/LocationSelect' import LocationSelect from '~/components/Select/LocationSelect'
import GetCategories from '~/mixins/getCategoriesMixin.js' import GetCategories from '~/mixins/getCategoriesMixin.js'
import formValidation from '~/mixins/formValidation'
export default { export default {
name: 'GroupForm', name: 'GroupForm',
mixins: [GetCategories], mixins: [GetCategories, formValidation],
components: { components: {
CategoriesSelect, CategoriesSelect,
Editor, Editor,
@ -308,16 +306,19 @@ export default {
return false return false
}, },
changeGroupType(event) { changeGroupType(event) {
this.$refs.groupForm.update('groupType', event.target.value) this.updateFormField('groupType', event.target.value)
}, },
changeActionRadius(event) { changeActionRadius(event) {
this.$refs.groupForm.update('actionRadius', event.target.value) this.updateFormField('actionRadius', event.target.value)
}, },
changeLocation(event) { changeLocation(event) {
this.formData.locationName = event.target.value this.formData.locationName = event.target.value
}, },
updateEditorDescription(value) { updateEditorDescription(value) {
this.$refs.groupForm.update('description', value) this.updateFormField('description', value)
},
onSubmit() {
this.formSubmit(this.submit)
}, },
submit() { submit() {
const { name, slug, about, description, groupType, actionRadius, categoryIds } = this.formData const { name, slug, about, description, groupType, actionRadius, categoryIds } = this.formData

View File

@ -65,8 +65,7 @@ describe('ChangePassword.vue', () => {
it('displays a validation error', async () => { it('displays a validation error', async () => {
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
const dsForm = wrapper.findComponent({ name: 'DsForm' }) expect(wrapper.vm.formErrors).toHaveProperty('passwordConfirmation')
expect(dsForm.vm.errors).toHaveProperty('passwordConfirmation')
}) })
}) })

View File

@ -1,44 +1,42 @@
<template> <template>
<ds-form v-model="formData" :schema="formSchema" @submit="handleSubmit"> <form @submit.prevent="onSubmit" novalidate>
<template #default="{ errors }"> <ds-input
<ds-input id="oldPassword"
id="oldPassword" model="oldPassword"
model="oldPassword" type="password"
type="password" autocomplete="off"
autocomplete="off" :label="$t('settings.security.change-password.label-old-password')"
:label="$t('settings.security.change-password.label-old-password')" />
/> <ds-input
<ds-input id="password"
id="password" model="password"
model="password" type="password"
type="password" autocomplete="off"
autocomplete="off" :label="$t('settings.security.change-password.label-new-password')"
:label="$t('settings.security.change-password.label-new-password')" />
/> <ds-input
<ds-input id="passwordConfirmation"
id="passwordConfirmation" model="passwordConfirmation"
model="passwordConfirmation" type="password"
type="password" autocomplete="off"
autocomplete="off" :label="$t('settings.security.change-password.label-new-password-confirm')"
:label="$t('settings.security.change-password.label-new-password-confirm')" />
/> <password-strength :password="formData.password" />
<password-strength :password="formData.password" /> <div class="ds-mt-base ds-mb-large">
<div class="ds-mt-base ds-mb-large"> <os-button
<os-button variant="primary"
variant="primary" appearance="filled"
appearance="filled" :loading="loading"
:loading="loading" :disabled="!!formErrors"
:disabled="!!errors" type="submit"
type="submit" >
> <template #icon>
<template #icon> <os-icon :icon="icons.lock" />
<os-icon :icon="icons.lock" /> </template>
</template> {{ $t('settings.security.change-password.button') }}
{{ $t('settings.security.change-password.button') }} </os-button>
</os-button> </div>
</div> </form>
</template>
</ds-form>
</template> </template>
<script> <script>
@ -47,9 +45,11 @@ import { iconRegistry } from '~/utils/iconRegistry'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import PasswordStrength from './Strength' import PasswordStrength from './Strength'
import PasswordForm from '~/components/utils/PasswordFormHelper' import PasswordForm from '~/components/utils/PasswordFormHelper'
import formValidation from '~/mixins/formValidation'
export default { export default {
name: 'ChangePassword', name: 'ChangePassword',
mixins: [formValidation],
components: { components: {
OsButton, OsButton,
OsIcon, OsIcon,
@ -77,6 +77,9 @@ export default {
} }
}, },
methods: { methods: {
onSubmit() {
this.formSubmit(this.handleSubmit)
},
async handleSubmit(data) { async handleSubmit(data) {
this.loading = true this.loading = true
const mutation = gql` const mutation = gql`

View File

@ -1,44 +1,41 @@
<template> <template>
<div class="ds-mt-base ds-mb-xxx-small"> <div class="ds-mt-base ds-mb-xxx-small">
<ds-form <form
v-if="!changePasswordResult" v-if="!changePasswordResult"
v-model="formData" @submit.prevent="onSubmit"
:schema="formSchema"
@submit="handleSubmitPassword"
class="change-password" class="change-password"
novalidate
> >
<template #default="{ errors }"> <ds-input
<ds-input id="password"
id="password" model="password"
model="password" type="password"
type="password" autocomplete="off"
autocomplete="off" :label="$t('settings.security.change-password.label-new-password')"
:label="$t('settings.security.change-password.label-new-password')" />
/> <ds-input
<ds-input id="passwordConfirmation"
id="passwordConfirmation" model="passwordConfirmation"
model="passwordConfirmation" type="password"
type="password" autocomplete="off"
autocomplete="off" :label="$t('settings.security.change-password.label-new-password-confirm')"
:label="$t('settings.security.change-password.label-new-password-confirm')" />
/> <password-strength :password="formData.password" />
<password-strength :password="formData.password" /> <div class="ds-mt-base ds-mb-xxx-small">
<div class="ds-mt-base ds-mb-xxx-small"> <os-button
<os-button variant="primary"
variant="primary" appearance="filled"
appearance="filled" :loading="$apollo.loading"
:loading="$apollo.loading" :disabled="!!formErrors"
:disabled="!!errors" type="submit"
type="submit" >
> <template #icon>
<template #icon> <os-icon :icon="icons.lock" />
<os-icon :icon="icons.lock" /> </template>
</template> {{ $t('settings.security.change-password.button') }}
{{ $t('settings.security.change-password.button') }} </os-button>
</os-button> </div>
</div> </form>
</template>
</ds-form>
<div v-else class="ds-mb-large"> <div v-else class="ds-mb-large">
<template v-if="changePasswordResult === 'success'"> <template v-if="changePasswordResult === 'success'">
<transition name="ds-transition-fade"> <transition name="ds-transition-fade">
@ -75,8 +72,10 @@ import PasswordStrength from '../Password/Strength'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons' import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper' import PasswordForm from '~/components/utils/PasswordFormHelper'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [formValidation],
components: { components: {
OsButton, OsButton,
OsIcon, OsIcon,
@ -105,6 +104,9 @@ export default {
} }
}, },
methods: { methods: {
onSubmit() {
this.formSubmit(this.handleSubmitPassword)
},
async handleSubmitPassword() { async handleSubmitPassword() {
const mutation = gql` const mutation = gql`
mutation ($nonce: String!, $email: String!, $password: String!) { mutation ($nonce: String!, $email: String!, $password: String!) {

View File

@ -1,12 +1,5 @@
<template> <template>
<ds-form <form v-if="!submitted" @submit.prevent="onSubmit" novalidate>
v-if="!submitted"
@input="handleInput"
@input-valid="handleInputValid"
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
>
<div class="ds-my-small"> <div class="ds-my-small">
<ds-input <ds-input
:placeholder="$t('login.email')" :placeholder="$t('login.email')"
@ -32,7 +25,7 @@
{{ $t('components.password-reset.request.form.submit') }} {{ $t('components.password-reset.request.form.submit') }}
</os-button> </os-button>
<slot></slot> <slot></slot>
</ds-form> </form>
<div v-else> <div v-else>
<transition name="ds-transition-fade"> <transition name="ds-transition-fade">
<div class="ds-flex ds-flex-centered"> <div class="ds-flex ds-flex-centered">
@ -47,8 +40,10 @@
import { OsButton } from '@ocelot-social/ui' import { OsButton } from '@ocelot-social/ui'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { SweetalertIcon } from 'vue-sweetalert-icons' import { SweetalertIcon } from 'vue-sweetalert-icons'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [formValidation],
components: { components: {
OsButton, OsButton,
SweetalertIcon, SweetalertIcon,
@ -82,6 +77,9 @@ export default {
handleInputValid() { handleInputValid() {
this.disabled = false this.disabled = false
}, },
onSubmit() {
this.formSubmit(this.handleSubmit)
},
async handleSubmit() { async handleSubmit() {
const mutation = gql` const mutation = gql`
mutation ($email: String!, $locale: String!) { mutation ($email: String!, $locale: String!) {

View File

@ -25,15 +25,7 @@
<div class="ds-my-xxx-small"></div> <div class="ds-my-xxx-small"></div>
</div> </div>
<div v-else class="create-account-card"> <div v-else class="create-account-card">
<ds-form <form class="create-user-account" novalidate>
class="create-user-account"
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<!-- leave this here in case the scoped variable is needed in the future nobody would remember this -->
<!-- <template v-slot="{ errors }"> -->
<template> <template>
<email-display-and-verify :email="sliderData.collectedInputData.email" /> <email-display-and-verify :email="sliderData.collectedInputData.email" />
@ -151,7 +143,7 @@
</div> </div>
</template> </template>
<div class="ds-my-xxx-small"></div> <div class="ds-my-xxx-small"></div>
</ds-form> </form>
</div> </div>
</template> </template>
@ -168,11 +160,13 @@ import PasswordForm from '~/components/utils/PasswordFormHelper'
import { iconRegistry } from '~/utils/iconRegistry' import { iconRegistry } from '~/utils/iconRegistry'
import ShowPassword from '../ShowPassword/ShowPassword.vue' import ShowPassword from '../ShowPassword/ShowPassword.vue'
import LocationSelect from '~/components/Select/LocationSelect' import LocationSelect from '~/components/Select/LocationSelect'
import formValidation from '~/mixins/formValidation'
const threePerEmSpace = '' // unicode u+2004; const threePerEmSpace = '' // unicode u+2004;
export default { export default {
name: 'RegistrationSlideCreate', name: 'RegistrationSlideCreate',
mixins: [formValidation],
components: { components: {
EmailDisplayAndVerify, EmailDisplayAndVerify,
PageParamsLink, PageParamsLink,

View File

@ -1,11 +1,5 @@
<template> <template>
<ds-form <form class="enter-email" @submit.prevent novalidate>
class="enter-email"
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<p class="ds-text"> <p class="ds-text">
{{ $t('components.registration.signup.form.description') }} {{ $t('components.registration.signup.form.description') }}
</p> </p>
@ -23,7 +17,7 @@
</label> </label>
</p> </p>
<div class="ds-my-xxx-small"></div> <div class="ds-my-xxx-small"></div>
</ds-form> </form>
</template> </template>
<script> <script>
@ -31,6 +25,7 @@ import gql from 'graphql-tag'
import metadata from '~/constants/metadata' import metadata from '~/constants/metadata'
import { isEmail } from 'validator' import { isEmail } from 'validator'
import translateErrorMessage from '~/components/utils/TranslateErrorMessage' import translateErrorMessage from '~/components/utils/TranslateErrorMessage'
import formValidation from '~/mixins/formValidation'
export const SignupMutation = gql` export const SignupMutation = gql`
mutation ($email: String!, $locale: String!, $inviteCode: String) { mutation ($email: String!, $locale: String!, $inviteCode: String) {
@ -41,6 +36,7 @@ export const SignupMutation = gql`
` `
export default { export default {
name: 'RegistrationSlideEmail', name: 'RegistrationSlideEmail',
mixins: [formValidation],
props: { props: {
sliderData: { type: Object, required: true }, sliderData: { type: Object, required: true },
}, },

View File

@ -1,11 +1,5 @@
<template> <template>
<ds-form <form class="enter-invite" @submit.prevent novalidate>
class="enter-invite"
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-input <ds-input
:placeholder="formSchema.inviteCode.placeholder" :placeholder="formSchema.inviteCode.placeholder"
:minlength="formSchema.inviteCode.minLength" :minlength="formSchema.inviteCode.minLength"
@ -40,16 +34,18 @@
</div> </div>
<slot></slot> <slot></slot>
<div class="ds-my-xxx-small"></div> <div class="ds-my-xxx-small"></div>
</ds-form> </form>
</template> </template>
<script> <script>
import registrationConstants from '~/constants/registrationBranded.js' import registrationConstants from '~/constants/registrationBranded.js'
import { validateInviteCode } from '~/graphql/InviteCode' import { validateInviteCode } from '~/graphql/InviteCode'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar' import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
import formValidation from '~/mixins/formValidation'
export default { export default {
name: 'RegistrationSlideInvite', name: 'RegistrationSlideInvite',
mixins: [formValidation],
props: { props: {
sliderData: { type: Object, required: true }, sliderData: { type: Object, required: true },
}, },

View File

@ -1,12 +1,5 @@
<template> <template>
<ds-form <form class="enter-nonce" @submit.prevent="onSubmit" novalidate>
class="enter-nonce"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitVerify"
@input="handleInput"
@input-valid="handleInputValid"
>
<email-display-and-verify :email="sliderData.collectedInputData.email" /> <email-display-and-verify :email="sliderData.collectedInputData.email" />
<ds-input <ds-input
:placeholder="$t('components.registration.email-nonce.form.nonce')" :placeholder="$t('components.registration.email-nonce.form.nonce')"
@ -19,13 +12,14 @@
</p> </p>
<slot></slot> <slot></slot>
<div class="ds-my-xxx-small"></div> <div class="ds-my-xxx-small"></div>
</ds-form> </form>
</template> </template>
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { isEmail } from 'validator' import { isEmail } from 'validator'
import registrationConstants from '~/constants/registration' import registrationConstants from '~/constants/registration'
import formValidation from '~/mixins/formValidation'
import EmailDisplayAndVerify from './EmailDisplayAndVerify' import EmailDisplayAndVerify from './EmailDisplayAndVerify'
@ -36,6 +30,7 @@ export const verifyNonceQuery = gql`
` `
export default { export default {
name: 'RegistrationSlideNonce', name: 'RegistrationSlideNonce',
mixins: [formValidation],
components: { components: {
EmailDisplayAndVerify, EmailDisplayAndVerify,
}, },
@ -101,6 +96,9 @@ export default {
async handleInputValid() { async handleInputValid() {
this.sendValidation() this.sendValidation()
}, },
onSubmit() {
this.formSubmit(this.handleSubmitVerify)
},
async handleSubmitVerify() { async handleSubmitVerify() {
const { email, nonce } = this.sliderData.collectedInputData const { email, nonce } = this.sliderData.collectedInputData
const variables = { email, nonce } const variables = { email, nonce }

View File

@ -1,12 +1,6 @@
<template> <template>
<div v-if="!data && !error" class="ds-my-large"> <div v-if="!data && !error" class="ds-my-large">
<ds-form <form @submit.prevent="onSubmit" novalidate>
@input="handleInput"
@input-valid="handleInputValid"
v-model="formData"
:schema="formSchema"
@submit="handleSubmit"
>
<h1> <h1>
{{ {{
invitation invitation
@ -40,7 +34,7 @@
{{ $t('components.registration.signup.form.submit') }} {{ $t('components.registration.signup.form.submit') }}
</os-button> </os-button>
<slot></slot> <slot></slot>
</ds-form> </form>
</div> </div>
<div v-else class="ds-my-large"> <div v-else class="ds-my-large">
<template v-if="!error"> <template v-if="!error">
@ -67,6 +61,7 @@ import gql from 'graphql-tag'
import metadata from '~/constants/metadata' import metadata from '~/constants/metadata'
import { SweetalertIcon } from 'vue-sweetalert-icons' import { SweetalertIcon } from 'vue-sweetalert-icons'
import translateErrorMessage from '~/components/utils/TranslateErrorMessage' import translateErrorMessage from '~/components/utils/TranslateErrorMessage'
import formValidation from '~/mixins/formValidation'
export const SignupMutation = gql` export const SignupMutation = gql`
mutation ($email: String!, $locale: String!, $inviteCode: String) { mutation ($email: String!, $locale: String!, $inviteCode: String) {
@ -77,6 +72,7 @@ export const SignupMutation = gql`
` `
export default { export default {
name: 'Signup', name: 'Signup',
mixins: [formValidation],
components: { components: {
OsButton, OsButton,
SweetalertIcon, SweetalertIcon,
@ -115,6 +111,9 @@ export default {
handleInputValid() { handleInputValid() {
this.disabled = false this.disabled = false
}, },
onSubmit() {
this.formSubmit(this.handleSubmit)
},
async handleSubmit() { async handleSubmit() {
const { email } = this.formData const { email } = this.formData

View File

@ -1,11 +1,5 @@
<template> <template>
<ds-form <form @submit.prevent="onSubmit" novalidate>
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
@submit="handleSubmitItem"
>
<div v-if="isEditing"> <div v-if="isEditing">
<div class="ds-my-base"> <div class="ds-my-base">
<h5 class="ds-heading ds-heading-h5"> <h5 class="ds-heading ds-heading-h5">
@ -77,16 +71,18 @@
</os-button> </os-button>
</div> </div>
<confirm-modal v-if="showConfirmModal" :modalData="currentModalData" @close="closeModal" /> <confirm-modal v-if="showConfirmModal" :modalData="currentModalData" @close="closeModal" />
</ds-form> </form>
</template> </template>
<script> <script>
import { OsButton, OsIcon } from '@ocelot-social/ui' import { OsButton, OsIcon } from '@ocelot-social/ui'
import { iconRegistry } from '~/utils/iconRegistry' import { iconRegistry } from '~/utils/iconRegistry'
import ConfirmModal from '~/components/Modal/ConfirmModal' import ConfirmModal from '~/components/Modal/ConfirmModal'
import formValidation from '~/mixins/formValidation'
export default { export default {
name: 'MySomethingList', name: 'MySomethingList',
mixins: [formValidation],
components: { ConfirmModal, OsButton, OsIcon }, components: { ConfirmModal, OsButton, OsIcon },
props: { props: {
useFormData: { type: Object, default: () => ({}) }, useFormData: { type: Object, default: () => ({}) },
@ -139,6 +135,9 @@ export default {
this.icons = iconRegistry this.icons = iconRegistry
}, },
methods: { methods: {
onSubmit() {
this.formSubmit(this.handleSubmitItem)
},
handleInput(data) { handleInput(data) {
this.callbacks.handleInput(this, data) this.callbacks.handleInput(this, data)
this.disabled = true this.disabled = true

View File

@ -2,6 +2,29 @@ import { debounce } from 'lodash'
import { checkSlugAvailableQuery } from '~/graphql/User.js' import { checkSlugAvailableQuery } from '~/graphql/User.js'
export default function UniqueSlugForm({ translate, apollo, currentUser }) { export default function UniqueSlugForm({ translate, apollo, currentUser }) {
let pendingCallback = null
const debouncedSlugCheck = debounce((value, callback) => {
const variables = { slug: value }
apollo
.query({ query: checkSlugAvailableQuery, variables })
.then((response) => {
const {
data: { User },
} = response
const existingSlug = User && User[0] && User[0].slug
const available = !existingSlug || existingSlug === currentUser.slug
if (!available) {
callback(new Error(translate('settings.validation.slug.alreadyTaken')))
} else {
callback()
}
})
.catch(() => {
callback()
})
}, 500)
return { return {
formSchema: { formSchema: {
slug: [ slug: [
@ -13,21 +36,20 @@ export default function UniqueSlugForm({ translate, apollo, currentUser }) {
}, },
{ {
asyncValidator(rule, value, callback) { asyncValidator(rule, value, callback) {
debounce(() => { // Resolve any pending callback from a previous debounced call
const variables = { slug: value } // that was cancelled, so async-validator doesn't hang
apollo.query({ query: checkSlugAvailableQuery, variables }).then((response) => { if (pendingCallback) {
const { pendingCallback()
data: { User }, }
} = response pendingCallback = callback
const existingSlug = User && User[0] && User[0].slug debouncedSlugCheck(value, (error) => {
const available = !existingSlug || existingSlug === currentUser.slug pendingCallback = null
if (!available) { if (error) {
callback(new Error(translate('settings.validation.slug.alreadyTaken'))) callback(error)
} else { } else {
callback() callback()
} }
}) })
}, 500)()
}, },
}, },
], ],

View File

@ -0,0 +1,102 @@
import Schema from 'async-validator'
Schema.warning = function () {}
function cloneDeep(obj) {
return JSON.parse(JSON.stringify(obj))
}
export default {
provide() {
return {
$parentForm: this.$formProxy,
}
},
data() {
return {
formErrors: null,
}
},
beforeCreate() {
const vm = this
const subscribers = []
this.$formProxy = {
subscribe(cb) {
if (cb && typeof cb === 'function') {
cb(cloneDeep(vm.formData))
subscribers.push(cb)
}
},
unsubscribe(cb) {
const index = subscribers.indexOf(cb)
if (index > -1) {
subscribers.splice(index, 1)
}
},
update(model, value) {
vm.updateFormField(model, value)
},
}
this.$formSubscribers = subscribers
},
watch: {
formData: {
handler(value) {
this.$notifyFormSubscribers(value, this.formErrors)
},
deep: true,
},
},
methods: {
updateFormField(model, value) {
this.$set(this.formData, model, value)
if (typeof this.handleInput === 'function') {
this.handleInput(cloneDeep(this.formData))
}
this.$validateForm(() => {
if (typeof this.handleInputValid === 'function') {
this.handleInputValid(cloneDeep(this.formData))
}
})
},
formSubmit(callback) {
this.$validateForm(() => {
if (callback && typeof callback === 'function') {
callback(cloneDeep(this.formData))
}
})
},
$validateForm(cb) {
const schema = this.formSchema
if (!schema || Object.keys(schema).length === 0) {
this.formErrors = null
this.$notifyFormSubscribers(this.formData, null)
if (cb && typeof cb === 'function') {
cb()
}
return
}
const validator = new Schema(schema)
validator.validate(this.formData, (errors) => {
if (errors) {
this.formErrors = errors.reduce((errorObj, error) => {
const result = { ...errorObj }
result[error.field] = error.message
return result
}, {})
} else {
this.formErrors = null
}
this.$notifyFormSubscribers(this.formData, this.formErrors)
if (!errors && cb && typeof cb === 'function') {
cb()
}
})
},
$notifyFormSubscribers(data, errors) {
this.$formSubscribers.forEach((cb) => {
cb(cloneDeep(data), errors)
})
},
},
}

View File

@ -1,7 +1,7 @@
<template> <template>
<os-card> <os-card>
<h2 class="title">{{ $t('admin.donations.name') }}</h2> <h2 class="title">{{ $t('admin.donations.name') }}</h2>
<ds-form v-model="formData" @submit="submit"> <form @submit.prevent="submit" novalidate>
<p class="ds-text show-donations-checkbox"> <p class="ds-text show-donations-checkbox">
<input id="showDonations" type="checkbox" v-model="showDonations" /> <input id="showDonations" type="checkbox" v-model="showDonations" />
<label for="showDonations"> <label for="showDonations">
@ -31,15 +31,17 @@
<os-button class="donations-info-button" variant="primary" appearance="filled" type="submit"> <os-button class="donations-info-button" variant="primary" appearance="filled" type="submit">
{{ $t('actions.save') }} {{ $t('actions.save') }}
</os-button> </os-button>
</ds-form> </form>
</os-card> </os-card>
</template> </template>
<script> <script>
import { OsButton, OsCard } from '@ocelot-social/ui' import { OsButton, OsCard } from '@ocelot-social/ui'
import { DonationsQuery, UpdateDonations } from '~/graphql/Donations' import { DonationsQuery, UpdateDonations } from '~/graphql/Donations'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [formValidation],
components: { OsButton, OsCard }, components: { OsButton, OsCard },
data() { data() {
return { return {

View File

@ -14,8 +14,6 @@ exports[`Users given badges are disabled renders 1`] = `
</h2> </h2>
<form <form
autocomplete="off"
class="ds-form"
novalidate="novalidate" novalidate="novalidate"
> >
<div <div
@ -465,8 +463,6 @@ exports[`Users given badges are enabled renders 1`] = `
</h2> </h2>
<form <form
autocomplete="off"
class="ds-form"
novalidate="novalidate" novalidate="novalidate"
> >
<div <div

View File

@ -2,7 +2,7 @@
<div class="admin-users"> <div class="admin-users">
<os-card> <os-card>
<h2 class="title">{{ $t('admin.users.name') }}</h2> <h2 class="title">{{ $t('admin.users.name') }}</h2>
<ds-form v-model="form" @submit="submit"> <form @submit.prevent="onSubmit" novalidate>
<div class="ds-flex ds-flex-gap-small"> <div class="ds-flex ds-flex-gap-small">
<div style="flex: 0 0 90%; width: 90%"> <div style="flex: 0 0 90%; width: 90%">
<ds-input <ds-input
@ -24,7 +24,7 @@
</os-button> </os-button>
</div> </div>
</div> </div>
</ds-form> </form>
</os-card> </os-card>
<os-card v-if="User && User.length"> <os-card v-if="User && User.length">
<div class="ds-table-wrap"> <div class="ds-table-wrap">
@ -162,8 +162,10 @@ import { isEmail } from 'validator'
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons' import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
import { adminUserQuery } from '~/graphql/User' import { adminUserQuery } from '~/graphql/User'
import { FetchAllRoles, updateUserRole } from '~/graphql/admin/Roles' import { FetchAllRoles, updateUserRole } from '~/graphql/admin/Roles'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [formValidation],
components: { components: {
OsButton, OsButton,
OsCard, OsCard,
@ -184,10 +186,8 @@ export default {
email: null, email: null,
filter: null, filter: null,
userRoles: [], userRoles: [],
form: { formData: {
formData: { query: '',
query: '',
},
}, },
} }
}, },
@ -234,9 +234,9 @@ export default {
next() { next() {
this.offset += this.pageSize this.offset += this.pageSize
}, },
submit(formData) { onSubmit() {
this.offset = 0 this.offset = 0
const { query } = formData const { query } = this.formData
if (isEmail(query)) { if (isEmail(query)) {
this.email = query this.email = query
this.filter = null this.filter = null

View File

@ -29,7 +29,7 @@ describe('change-password', () => {
}) })
it('renders', () => { it('renders', () => {
expect(wrapper.findAll('.ds-form')).toHaveLength(1) expect(wrapper.findAll('form')).toHaveLength(1)
}) })
}) })
}) })

View File

@ -30,7 +30,7 @@ describe('enter-nonce.vue', () => {
}) })
it('renders', () => { it('renders', () => {
expect(wrapper.findAll('.ds-form')).toHaveLength(1) expect(wrapper.findAll('form')).toHaveLength(1)
}) })
}) })
}) })

View File

@ -33,7 +33,7 @@ describe('request.vue', () => {
}) })
it('renders', () => { it('renders', () => {
expect(wrapper.findAll('.ds-form')).toHaveLength(1) expect(wrapper.findAll('form')).toHaveLength(1)
}) })
it('navigates to enter-nonce on handlePasswordResetRequested', () => { it('navigates to enter-nonce on handlePasswordResetRequested', () => {

View File

@ -114,7 +114,7 @@ describe('index.vue', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
wrapper.find('#name').setValue('Peter') wrapper.find('#name').setValue('Peter')
wrapper.find('.ds-form').trigger('submit') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled() expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
}) })
@ -130,7 +130,7 @@ describe('index.vue', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
wrapper.find('#name').setValue('Peter') wrapper.find('#name').setValue('Peter')
wrapper.find('.ds-form').trigger('submit') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -147,7 +147,7 @@ describe('index.vue', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
wrapper.find('#slug').setValue('peter-der-lustige') wrapper.find('#slug').setValue('peter-der-lustige')
wrapper.find('.ds-form').trigger('submit') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -163,7 +163,7 @@ describe('index.vue', () => {
it('calls updateUser mutation', async () => { it('calls updateUser mutation', async () => {
const wrapper = Wrapper() const wrapper = Wrapper()
wrapper.findComponent(LocationSelect).vm.$emit('input', 'Berlin, Germany') wrapper.findComponent(LocationSelect).vm.$emit('input', 'Berlin, Germany')
wrapper.find('.ds-form').trigger('submit') wrapper.find('form').trigger('submit')
await expect(mocks.$apollo.mutate).toHaveBeenCalledWith( await expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -180,7 +180,7 @@ describe('index.vue', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
wrapper.find('#about').setValue('I am Peter!111elf') wrapper.find('#about').setValue('I am Peter!111elf')
wrapper.find('.ds-form').trigger('submit') wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -199,7 +199,7 @@ describe('index.vue', () => {
wrapper.find('#slug').setValue('peter-der-lustige') wrapper.find('#slug').setValue('peter-der-lustige')
await wrapper.findComponent(LocationSelect).vm.$emit('input', 'Hamburg, Germany') await wrapper.findComponent(LocationSelect).vm.$emit('input', 'Hamburg, Germany')
wrapper.find('#about').setValue('I am Peter!111elf') wrapper.find('#about').setValue('I am Peter!111elf')
wrapper.find('.ds-form').trigger('submit') wrapper.find('form').trigger('submit')
await expect(mocks.$apollo.mutate).toHaveBeenCalledWith( await expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({

View File

@ -1,47 +1,45 @@
<template> <template>
<ds-form class="settings-form" v-model="formData" :schema="formSchema" @submit="submit"> <form class="settings-form" @submit.prevent="onSubmit" novalidate>
<template #default="{ errors }"> <os-card>
<os-card> <h2 class="title">{{ $t('settings.data.name') }}</h2>
<h2 class="title">{{ $t('settings.data.name') }}</h2> <ds-input
<ds-input id="name"
id="name" model="name"
model="name" icon="user"
icon="user" :label="
:label=" $env.ASK_FOR_REAL_NAME
$env.ASK_FOR_REAL_NAME ? $t('settings.data.realNamePlease')
? $t('settings.data.realNamePlease') : $t('settings.data.labelName')
: $t('settings.data.labelName') "
" :placeholder="$t('settings.data.namePlaceholder')"
:placeholder="$t('settings.data.namePlaceholder')" />
/> <ds-input id="slug" model="slug" icon="at" :label="$t('settings.data.labelSlug')" />
<ds-input id="slug" model="slug" icon="at" :label="$t('settings.data.labelSlug')" /> <location-select
<location-select class="location-selet"
class="location-selet" v-model="formData.locationName"
v-model="formData.locationName" :canBeCleared="!$env.REQUIRE_LOCATION"
:canBeCleared="!$env.REQUIRE_LOCATION" />
/> <!-- eslint-enable vue/use-v-on-exact -->
<!-- eslint-enable vue/use-v-on-exact --> <ds-input
<ds-input id="about"
id="about" model="about"
model="about" type="textarea"
type="textarea" rows="3"
rows="3" :label="$t('settings.data.labelBio')"
:label="$t('settings.data.labelBio')" :placeholder="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')" />
/> <os-button
<os-button variant="primary"
variant="primary" appearance="filled"
appearance="filled" type="submit"
type="submit" :disabled="!!formErrors"
:disabled="!!errors" :loading="loadingData"
:loading="loadingData" >
> <template #icon><os-icon :icon="icons.check" /></template>
<template #icon><os-icon :icon="icons.check" /></template> {{ $t('actions.save') }}
{{ $t('actions.save') }} </os-button>
</os-button> </os-card>
</os-card> </form>
</template>
</ds-form>
</template> </template>
<script> <script>
@ -52,9 +50,10 @@ import UniqueSlugForm from '~/components/utils/UniqueSlugForm'
import LocationSelect from '~/components/Select/LocationSelect' import LocationSelect from '~/components/Select/LocationSelect'
import { updateUserMutation } from '~/graphql/User' import { updateUserMutation } from '~/graphql/User'
import scrollToContent from './scroll-to-content.js' import scrollToContent from './scroll-to-content.js'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [scrollToContent], mixins: [scrollToContent, formValidation],
name: 'Settings', name: 'Settings',
components: { components: {
OsButton, OsButton,
@ -105,6 +104,9 @@ export default {
...mapMutations({ ...mapMutations({
setCurrentUser: 'auth/SET_USER', setCurrentUser: 'auth/SET_USER',
}), }),
onSubmit() {
this.formSubmit(this.submit)
},
async submit() { async submit() {
this.loadingData = true this.loadingData = true
const { name, slug, about } = this.formData const { name, slug, about } = this.formData

View File

@ -1,58 +1,59 @@
<template> <template>
<ds-form v-model="form" :schema="formSchema" @submit="submit"> <form @submit.prevent="onSubmit" novalidate>
<template #default="{ errors }"> <os-card>
<os-card> <h2 class="title">{{ $t('settings.email.name') }}</h2>
<h2 class="title">{{ $t('settings.email.name') }}</h2> <ds-input
<ds-input id="email"
id="email" model="email"
model="email" icon="envelope"
icon="envelope" disabled
disabled :label="$t('settings.email.labelNewEmail')"
:label="$t('settings.email.labelNewEmail')" />
/> <ds-input
<ds-input id="nonce"
id="nonce" model="nonce"
model="nonce" icon="question-circle"
icon="question-circle" :label="$t('settings.email.labelNonce')"
:label="$t('settings.email.labelNonce')" />
/> <os-button variant="primary" appearance="filled" type="submit" :disabled="!!formErrors">
<os-button variant="primary" appearance="filled" type="submit" :disabled="!!errors"> <template #icon><os-icon :icon="icons.check" /></template>
<template #icon><os-icon :icon="icons.check" /></template> {{ $t('actions.save') }}
{{ $t('actions.save') }} </os-button>
</os-button> </os-card>
</os-card> </form>
</template>
</ds-form>
</template> </template>
<script> <script>
import { OsButton, OsCard, OsIcon } from '@ocelot-social/ui' import { OsButton, OsCard, OsIcon } from '@ocelot-social/ui'
import { iconRegistry } from '~/utils/iconRegistry' import { iconRegistry } from '~/utils/iconRegistry'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [formValidation],
components: { OsButton, OsCard, OsIcon }, components: { OsButton, OsCard, OsIcon },
data() { data() {
return { return {
formData: {
email: '',
nonce: '',
},
formSchema: { formSchema: {
nonce: { type: 'string', required: true }, nonce: { type: 'string', required: true },
}, },
} }
}, },
computed: { mounted() {
form: { const { email = '', nonce = '' } = this.$route.query
get: function () { this.formData.email = email
const { email = '', nonce = '' } = this.$route.query this.formData.nonce = nonce
return { email, nonce }
},
set: function (formData) {
this.formData = formData
},
},
}, },
created() { created() {
this.icons = iconRegistry this.icons = iconRegistry
}, },
methods: { methods: {
onSubmit() {
this.formSubmit(this.submit)
},
async submit() { async submit() {
const { email, nonce } = this.formData const { email, nonce } = this.formData
this.$router.replace({ this.$router.replace({

View File

@ -5,34 +5,27 @@
</transition> </transition>
<p class="ds-text" v-html="submitMessage" /> <p class="ds-text" v-html="submitMessage" />
</os-card> </os-card>
<ds-form v-else v-model="form" :schema="formSchema" @submit="submit"> <form v-else @submit.prevent="onSubmit" novalidate>
<template #default="{ errors }"> <os-card>
<os-card> <h2 class="title">{{ $t('settings.email.name') }}</h2>
<h2 class="title">{{ $t('settings.email.name') }}</h2> <ds-input id="email" model="email" icon="envelope" :label="$t('settings.email.labelEmail')" />
<ds-input <div class="ds-mb-large backendErrors" v-if="backendErrors">
id="email" <p class="ds-text ds-text-center ds-text-bold ds-text-danger">
model="email" {{ backendErrors.message }}
icon="envelope" </p>
:label="$t('settings.email.labelEmail')" </div>
/> <os-button
<div class="ds-mb-large backendErrors" v-if="backendErrors"> :disabled="!!formErrors"
<p class="ds-text ds-text-center ds-text-bold ds-text-danger"> :loading="loadingData"
{{ backendErrors.message }} type="submit"
</p> variant="primary"
</div> appearance="filled"
<os-button >
:disabled="!!errors" <template #icon><os-icon :icon="icons.check" /></template>
:loading="loadingData" {{ $t('actions.save') }}
type="submit" </os-button>
variant="primary" </os-card>
appearance="filled" </form>
>
<template #icon><os-icon :icon="icons.check" /></template>
{{ $t('actions.save') }}
</os-button>
</os-card>
</template>
</ds-form>
</template> </template>
<script> <script>
@ -42,9 +35,10 @@ import { iconRegistry } from '~/utils/iconRegistry'
import { AddEmailAddressMutation } from '~/graphql/EmailAddress.js' import { AddEmailAddressMutation } from '~/graphql/EmailAddress.js'
import { SweetalertIcon } from 'vue-sweetalert-icons' import { SweetalertIcon } from 'vue-sweetalert-icons'
import scrollToContent from '../scroll-to-content.js' import scrollToContent from '../scroll-to-content.js'
import formValidation from '~/mixins/formValidation'
export default { export default {
mixins: [scrollToContent], mixins: [scrollToContent, formValidation],
components: { components: {
OsButton, OsButton,
OsCard, OsCard,
@ -59,8 +53,14 @@ export default {
backendErrors: null, backendErrors: null,
data: null, data: null,
loadingData: false, loadingData: false,
formData: {
email: '',
},
} }
}, },
mounted() {
this.formData.email = this.currentUser.email || ''
},
computed: { computed: {
submitMessage() { submitMessage() {
const { email } = this.data.AddEmailAddress const { email } = this.data.AddEmailAddress
@ -69,15 +69,6 @@ export default {
...mapGetters({ ...mapGetters({
currentUser: 'auth/user', currentUser: 'auth/user',
}), }),
form: {
get: function () {
const { email } = this.currentUser
return { email }
},
set: function (formData) {
this.formData = formData
},
},
formSchema() { formSchema() {
const currentEmail = this.currentUser.email const currentEmail = this.currentUser.email
const sameEmailValidationError = this.$t('settings.email.validation.same-email') const sameEmailValidationError = this.$t('settings.email.validation.same-email')
@ -98,6 +89,9 @@ export default {
}, },
}, },
methods: { methods: {
onSubmit() {
this.formSubmit(this.submit)
},
async submit() { async submit() {
this.loadingData = true this.loadingData = true
const { email } = this.formData const { email } = this.formData