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 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+ ████████░░ 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
@ -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) |
| ✅ → Plain HTML | Radio (1 Datei → native `<input type="radio">` in ReportModal) |
| ⬜ → 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) |
### OsButton Migration (Phase 3) ✅
@ -38,7 +39,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
**Erkenntnisse aus der Migration:**
- `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`
- Filter-Buttons nutzen `:appearance="condition ? 'filled' : 'outline'"` Pattern
- 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)
15. [x] ds-tag (3 Dateien) → OsBadge shape="square" (UI-Library)
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) ✅
### 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. [ ] OsInput (23 Dateien, gekoppelt mit ds-form)
20. [ ] OsMenu / OsMenuItem (17 Dateien)
21. [ ] OsSelect (3 Dateien), OsTable (7 Dateien)
22. [ ] ds-form → HTML `<form>` oder OsForm (18 Dateien)
19. [x] OsModal (h() Render, Focus-Trap, Scroll-Lock, A11y; ConfirmModal + ReportModal nutzen OsModal; DeleteUserModal/DisableModal/ReleaseModal gelöscht) ✅
20. [x] ds-form → formValidation Mixin (async-validator), 18 Dateien migriert, vuelidate entfernt ✅
21. [ ] OsInput (23 Dateien)
22. [ ] OsMenu / OsMenuItem (17 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-flex, ds-flex-item | ✅ → HTML + CSS @media Queries |
### Tier B: Einfache ds-* Migration (60%)
### Tier B: Einfache ds-* Migration
| # | Komponente | Dateien | Ziel | Status |
|---|------------|---------|------|--------|
| 5 | **OsBadge** | — | ds-chip (20 Nutzungen, 5 Dateien) + ds-tag (3 Dateien) | ✅ |
| — | ds-grid / ds-grid-item | 10 | CSS Grid (Plain HTML) | ✅ |
| — | ds-number | 5 | `<div class="ds-number">` | ⬜ |
| — | ds-radio | 1 | native `<input type="radio">` | |
| — | ds-number | 5 | OsNumber (UI-Library) | ✅ |
| — | ds-radio | 1 | native `<input type="radio">` | |
### Tier 2: Layout & Feedback
@ -964,7 +965,7 @@ interface OsDropdownProps {
| 5 | **OsModal** | 7 | OsButton | ✅ |
| 6 | **OsDropdown** | — | OsButton | ⬜ |
| 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)
@ -979,7 +980,7 @@ interface OsDropdownProps {
|---|------------|---------|
| 11 | OsSelect | 3 |
| 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).
@ -1014,17 +1015,17 @@ ds-flex, ds-flex-item ✅ → HTML + CSS @media Q
ds-chip → OsBadge (UI-Library) ✅
ds-tag → OsBadge shape="square" (UI-Library) ✅
ds-grid / ds-grid-item → CSS Grid (HTML) ✅
ds-number → Plain HTML ⬜ (5 Dateien)
ds-radio → native <input type="radio"> (1 Datei)
ds-number → OsNumber (UI-Library) ✅
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!
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
10. OsSelect → Basis: DsSelect
11. OsTable → Basis: DsTable
@ -1047,8 +1048,8 @@ ds-radio → native <input type="radio"> ⬜ (1 Datei)
### Noch offen:
1. **Logo** - Existiert doppelt (Webapp + Styleguide)
2. **Modal** - Existiert doppelt (Webapp Modal.vue ist Modal-Router, DsModal ist UI)
3. **ds-form Kopplung** - ds-input und ds-form sind stark gekoppelt (Schema-Validation)
2. ~~**Modal** - Existiert doppelt~~ → OsModal migriert ✅
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
**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):**
- [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
- [x] OsModal Komponente + DsModal/ConfirmModal/ReportModal → OsModal Webapp-Integration ✅
- [ ] 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-select → OsSelect
- [ ] 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-number (5 Dateien) → OsNumber (UI-Library) ✅ + CountTo.vue gelöscht, vue-count-to entfernt
- [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)**
- [x] OsModal (Basis: DsModal → h() Render-Function, Vue 2/3 Compat, Focus-Trap, Scroll-Lock, A11y) ✅
- [ ] OsDropdown (Basis: Webapp Dropdown)
- [ ] 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)**
- [ ] 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**
- [ ] OsSelect (3 Dateien)
- [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**
- [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 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 | **!!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 | **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 | **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 | **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'`) |
@ -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 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-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 |
|--------|------------|
| ✅ 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 |
| ⬜ → 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 |
| ⬜ 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>
<ds-form v-model="form" @submit="handleSubmit" class="comment-form">
<template #default="{ errors }">
<os-card>
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
<div class="buttons">
<os-button
variant="primary"
appearance="outline"
:disabled="disabled && !update"
@click="handleCancel"
data-test="cancel-button"
>
{{ $t('actions.cancel') }}
</os-button>
<os-button
variant="primary"
appearance="filled"
type="submit"
:loading="loading"
:disabled="disabled || !!errors"
>
<template #icon>
<os-icon :icon="icons.comment" />
</template>
{{ $t('post.comment.submit') }}
</os-button>
</div>
</os-card>
</template>
</ds-form>
<form @submit.prevent="handleSubmit" class="comment-form" novalidate>
<os-card>
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
<div class="buttons">
<os-button
variant="primary"
appearance="outline"
:disabled="disabled && !update"
@click="handleCancel"
data-test="cancel-button"
>
{{ $t('actions.cancel') }}
</os-button>
<os-button
variant="primary"
appearance="filled"
type="submit"
:loading="loading"
:disabled="disabled"
>
<template #icon>
<os-icon :icon="icons.comment" />
</template>
{{ $t('post.comment.submit') }}
</os-button>
</div>
</os-card>
</form>
</template>
<script>

View File

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

View File

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

View File

@ -1,14 +1,7 @@
<template>
<div>
<ds-form
class="group-form"
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 }">
<form class="group-form" @submit.prevent="onSubmit" novalidate>
<template>
<!-- group Name -->
<ds-input
name="name"
@ -20,10 +13,10 @@
<os-badge
role="status"
aria-live="polite"
:variant="errors && errors.name ? 'danger' : undefined"
:variant="formErrors && formErrors.name ? 'danger' : undefined"
>
{{ `${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>
<!-- group Slug -->
@ -56,11 +49,13 @@
<os-badge
role="status"
aria-live="polite"
:variant="errors && errors.groupType && formData.groupType === '' ? 'danger' : undefined"
:variant="
formErrors && formErrors.groupType && formData.groupType === '' ? 'danger' : undefined
"
>
{{ `${formData.groupType === '' ? 0 : 1} / 1` }}
<os-icon
v-if="errors && errors.groupType && formData.groupType === ''"
v-if="formErrors && formErrors.groupType && formData.groupType === ''"
:icon="icons.warning"
/>
</os-badge>
@ -91,10 +86,10 @@
<os-badge
role="status"
aria-live="polite"
:variant="errors && errors.description ? 'danger' : undefined"
:variant="formErrors && formErrors.description ? 'danger' : undefined"
>
{{ `${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>
<!-- actionRadius -->
@ -109,12 +104,14 @@
role="status"
aria-live="polite"
:variant="
errors && errors.actionRadius && formData.actionRadius === '' ? 'danger' : undefined
formErrors && formErrors.actionRadius && formData.actionRadius === ''
? 'danger'
: undefined
"
>
{{ `${formData.actionRadius === '' ? 0 : 1} / 1` }}
<os-icon
v-if="errors && errors.actionRadius && formData.actionRadius === ''"
v-if="formErrors && formErrors.actionRadius && formData.actionRadius === ''"
:icon="icons.warning"
/>
</os-badge>
@ -138,10 +135,10 @@
<os-badge
role="status"
aria-live="polite"
:variant="errors && errors.categoryIds ? 'danger' : undefined"
:variant="formErrors && formErrors.categoryIds ? 'danger' : undefined"
>
{{ 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>
</div>
<!-- submit -->
@ -153,14 +150,14 @@
variant="primary"
appearance="filled"
type="submit"
:disabled="checkFormError(errors)"
:disabled="checkFormError(formErrors)"
>
<template #icon><os-icon :icon="icons.save" /></template>
{{ update ? $t('group.update') : $t('group.save') }}
</os-button>
</div>
</template>
</ds-form>
</form>
</div>
</template>
@ -178,10 +175,11 @@ import Editor from '~/components/Editor/Editor'
import ActionRadiusSelect from '~/components/Select/ActionRadiusSelect'
import LocationSelect from '~/components/Select/LocationSelect'
import GetCategories from '~/mixins/getCategoriesMixin.js'
import formValidation from '~/mixins/formValidation'
export default {
name: 'GroupForm',
mixins: [GetCategories],
mixins: [GetCategories, formValidation],
components: {
CategoriesSelect,
Editor,
@ -308,16 +306,19 @@ export default {
return false
},
changeGroupType(event) {
this.$refs.groupForm.update('groupType', event.target.value)
this.updateFormField('groupType', event.target.value)
},
changeActionRadius(event) {
this.$refs.groupForm.update('actionRadius', event.target.value)
this.updateFormField('actionRadius', event.target.value)
},
changeLocation(event) {
this.formData.locationName = event.target.value
},
updateEditorDescription(value) {
this.$refs.groupForm.update('description', value)
this.updateFormField('description', value)
},
onSubmit() {
this.formSubmit(this.submit)
},
submit() {
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 () => {
await wrapper.find('form').trigger('submit')
await wrapper.vm.$nextTick()
const dsForm = wrapper.findComponent({ name: 'DsForm' })
expect(dsForm.vm.errors).toHaveProperty('passwordConfirmation')
expect(wrapper.vm.formErrors).toHaveProperty('passwordConfirmation')
})
})

View File

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

View File

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

View File

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

View File

@ -25,15 +25,7 @@
<div class="ds-my-xxx-small"></div>
</div>
<div v-else class="create-account-card">
<ds-form
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 }"> -->
<form class="create-user-account" novalidate>
<template>
<email-display-and-verify :email="sliderData.collectedInputData.email" />
@ -151,7 +143,7 @@
</div>
</template>
<div class="ds-my-xxx-small"></div>
</ds-form>
</form>
</div>
</template>
@ -168,11 +160,13 @@ import PasswordForm from '~/components/utils/PasswordFormHelper'
import { iconRegistry } from '~/utils/iconRegistry'
import ShowPassword from '../ShowPassword/ShowPassword.vue'
import LocationSelect from '~/components/Select/LocationSelect'
import formValidation from '~/mixins/formValidation'
const threePerEmSpace = '' // unicode u+2004;
export default {
name: 'RegistrationSlideCreate',
mixins: [formValidation],
components: {
EmailDisplayAndVerify,
PageParamsLink,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,29 @@ import { debounce } from 'lodash'
import { checkSlugAvailableQuery } from '~/graphql/User.js'
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 {
formSchema: {
slug: [
@ -13,21 +36,20 @@ export default function UniqueSlugForm({ translate, apollo, currentUser }) {
},
{
asyncValidator(rule, value, callback) {
debounce(() => {
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()
}
})
}, 500)()
// Resolve any pending callback from a previous debounced call
// that was cancelled, so async-validator doesn't hang
if (pendingCallback) {
pendingCallback()
}
pendingCallback = callback
debouncedSlugCheck(value, (error) => {
pendingCallback = null
if (error) {
callback(error)
} else {
callback()
}
})
},
},
],

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ describe('change-password', () => {
})
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', () => {
expect(wrapper.findAll('.ds-form')).toHaveLength(1)
expect(wrapper.findAll('form')).toHaveLength(1)
})
})
})

View File

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

View File

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

View File

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

View File

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

View File

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