refactor(webapp): vue3 migration - button - icon + circle + loading (#9208)
@ -2,6 +2,6 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
|||||||
|
|
||||||
defineStep('I open the content menu of post {string}', (title) => {
|
defineStep('I open the content menu of post {string}', (title) => {
|
||||||
cy.contains('.post-teaser', title)
|
cy.contains('.post-teaser', title)
|
||||||
.find('.content-menu .base-button')
|
.find('[data-test="content-menu-button"]')
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
|||||||
|
|
||||||
defineStep('I click on "Report Post" from the content menu of the post', () => {
|
defineStep('I click on "Report Post" from the content menu of the post', () => {
|
||||||
cy.contains('.base-card', 'The Truth about the Holocaust')
|
cy.contains('.base-card', 'The Truth about the Holocaust')
|
||||||
.find('.content-menu .base-button')
|
.find('[data-test="content-menu-button"]')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.popover .ds-menu-item-link')
|
cy.get('.popover .ds-menu-item-link')
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
|
||||||
defineStep('I should see the {string} button', button => {
|
defineStep('I should see the {string} button', button => {
|
||||||
cy.get('.base-card .action-buttons .base-button')
|
cy.get('.base-card .action-buttons button')
|
||||||
.should('contain', button)
|
.should('contain', button)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
|
||||||
defineStep('I {string} see {string} from the content menu in the user info box', (condition, link) => {
|
defineStep('I {string} see {string} from the content menu in the user info box', (condition, link) => {
|
||||||
cy.get('.user-content-menu .base-button').click()
|
cy.get('.user-content-menu [data-test="content-menu-button"]').click()
|
||||||
cy.get('.popover .ds-menu-item-link')
|
cy.get('.popover .ds-menu-item-link')
|
||||||
.should(condition === 'should' ? 'contain' : 'not.contain', link)
|
.should(condition === 'should' ? 'contain' : 'not.contain', link)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||||
|
|
||||||
defineStep('I delete a social media link', () => {
|
defineStep('I delete a social media link', () => {
|
||||||
cy.get(".base-button[title='Delete']")
|
cy.get("button[title='Delete']")
|
||||||
.click()
|
.click()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
|||||||
|
|
||||||
defineStep('I click on {string} from the content menu in the user info box',
|
defineStep('I click on {string} from the content menu in the user info box',
|
||||||
button => {
|
button => {
|
||||||
cy.get('.user-content-menu .base-button').click()
|
cy.get('.user-content-menu [data-test="content-menu-button"]').click()
|
||||||
cy.get('.popover .ds-menu-item-link')
|
cy.get('.popover .ds-menu-item-link')
|
||||||
.contains(button)
|
.contains(button)
|
||||||
.click({
|
.click({
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "../src/styles/animations.css";
|
||||||
|
|
||||||
/* Watercolor theme for Storybook - vibrant colors inspired by aquarelle paints */
|
/* Watercolor theme for Storybook - vibrant colors inspired by aquarelle paints */
|
||||||
/* All colors meet WCAG AA contrast requirements (4.5:1 for normal text) */
|
/* All colors meet WCAG AA contrast requirements (4.5:1 for normal text) */
|
||||||
|
|||||||
@ -10,9 +10,9 @@
|
|||||||
### Übersicht
|
### Übersicht
|
||||||
```
|
```
|
||||||
Phase 0: Analyse ██████████ 100% (8/8 Schritte)
|
Phase 0: Analyse ██████████ 100% (8/8 Schritte)
|
||||||
Phase 3: Migration ████░░░░░░ 36% (32/90 Buttons)
|
Phase 3: Migration ██████████ 100% (132/132 Buttons) ✅
|
||||||
───────────────────────────────────────────
|
───────────────────────────────────────────
|
||||||
Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
Phase 3 ABGESCHLOSSEN: M4a ✅, M4b ✅, M4c ✅ — 0 <base-button> verbleibend
|
||||||
```
|
```
|
||||||
|
|
||||||
### Statistiken
|
### Statistiken
|
||||||
@ -25,108 +25,30 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| Duplikate gefunden | 5 direkte + 3 Familien |
|
| Duplikate gefunden | 5 direkte + 3 Familien |
|
||||||
| Zur Migration priorisiert | 15 Kern-Komponenten |
|
| Zur Migration priorisiert | 15 Kern-Komponenten |
|
||||||
|
|
||||||
### OsButton Migration (Phase 3)
|
### OsButton Migration (Phase 3) ✅
|
||||||
| Status | Anzahl | Details |
|
| Status | Anzahl | Details |
|
||||||
|--------|--------|---------|
|
|--------|--------|---------|
|
||||||
| ✅ Migriert | 32 | Erste Welle (16) + Milestone 4a (14) + NotificationMenu (2) |
|
| ✅ Migriert | 132 | 78 Dateien, alle `<base-button>` ersetzt |
|
||||||
| ⏳ Ausstehend (mit neuen Props) | ~60 | Milestone 4c (benötigen icon/circle/loading) |
|
| ⬜ Verbleibend | 0 | Nur BaseButton.vue Definition + Test/Snapshot-Dateien |
|
||||||
| **Gesamt** | **~90** | In ~50 Dateien |
|
| **Gesamt** | **132** | **100% erledigt** |
|
||||||
|
|
||||||
**Migrierte Komponenten (32):**
|
**Alle 132 Buttons migriert in 78 Dateien ✅**
|
||||||
|
|
||||||
*Erste Welle (16):*
|
Migration vollständig abgeschlossen. 0 `<base-button>` Tags verbleiben in Vue-Templates.
|
||||||
- UserTeaserPopover.vue (1 Button)
|
|
||||||
- GroupForm.vue (1 Button - Cancel)
|
|
||||||
- EmbedComponent.vue (2 Buttons - Cancel, Play Now)
|
|
||||||
- DonationInfo.vue (1 Button)
|
|
||||||
- CommentCard.vue (1 Button - Show More)
|
|
||||||
- MapStylesButtons.vue (1 Button)
|
|
||||||
- GroupMember.vue (1 Button)
|
|
||||||
- embeds.vue (2 Buttons)
|
|
||||||
- notifications.vue (3 Buttons)
|
|
||||||
- privacy.vue (1 Button)
|
|
||||||
- terms-and-conditions-confirm.vue (2 Buttons)
|
|
||||||
|
|
||||||
*Milestone 4a (14) ✅:*
|
**Erkenntnisse aus der Migration:**
|
||||||
- ✅ DisableModal.vue (1 Button - Cancel)
|
- `type="submit"` muss explizit gesetzt werden (OsButton Default: `type="button"`)
|
||||||
- ✅ DeleteUserModal.vue (1 Button - Cancel)
|
- DsForm `errors` ist ein Objekt → `!!errors` für Boolean-Cast bei `:disabled`
|
||||||
- ✅ ReleaseModal.vue (1 Button - Cancel)
|
- CSS `.base-button` Selektoren → `> button` oder `button`
|
||||||
- ✅ ContributionForm.vue (1 Button - Cancel)
|
- Position/Dimensions brauchen `!important` für Tailwind-Override
|
||||||
- ✅ EnterNonce.vue (1 Button - Submit)
|
- Filter-Buttons nutzen `:appearance="condition ? 'filled' : 'outline'"` Pattern
|
||||||
- ✅ MySomethingList.vue (1 Button - Cancel)
|
- Circle-Buttons mit Icon: `<template #icon><base-icon :name="..." /></template>`
|
||||||
- ✅ ImageUploader.vue (2 Buttons - Crop)
|
|
||||||
- ✅ admin/donations.vue (1 Button - Save)
|
|
||||||
- ✅ profile/_id/_slug.vue (2 Buttons - Unblock, Unmute)
|
|
||||||
- ✅ settings/badges.vue (1 Button - Remove)
|
|
||||||
- ✅ notifications/index.vue (1 Button - Mark All Read)
|
|
||||||
- ✅ ReportRow.vue (1 Button - More Details)
|
|
||||||
|
|
||||||
*Sonstige (2):*
|
**Verbleibende Cleanup-Aufgaben:**
|
||||||
- ✅ NotificationMenu.vue (2 Buttons - Mark All Read, Notification Page)
|
- [ ] Snapshot-Dateien aktualisieren (enthalten noch `base-button` Referenzen)
|
||||||
|
- [ ] Test-Dateien aktualisieren (Selektoren `.base-button` → `button` oder `os-button-stub`)
|
||||||
**Ausstehend - benötigen neue Props (~60):**
|
- [ ] BaseButton.vue Komponente ggf. entfernen (wenn nicht mehr referenziert)
|
||||||
|
- [ ] CSS-Selektor `.base-button` in ImageUploader.vue entfernen
|
||||||
*Button-Komponenten mit icon/circle/loading:*
|
|
||||||
- ActionButton.vue: 3 Buttons (icon, circle)
|
|
||||||
- LabeledButton.vue: 1 Button (icon)
|
|
||||||
- MenuBarButton.vue: 1 Button (icon)
|
|
||||||
- EmotionButton.vue: 1 Button (icon)
|
|
||||||
- ShoutButton.vue: 1 Button (icon)
|
|
||||||
- FollowButton.vue: 1 Button (icon, loading)
|
|
||||||
- JoinLeaveButton.vue: 1 Button (icon, loading)
|
|
||||||
- ObserveButton.vue: 1 Button (icon, loading)
|
|
||||||
- InviteButton.vue: 1 Button (icon, loading)
|
|
||||||
- MapButton.vue: 1 Button (icon)
|
|
||||||
- PaginationButtons.vue: 2 Buttons (icon, circle)
|
|
||||||
|
|
||||||
*Navigation mit icon:*
|
|
||||||
- LocaleSwitch.vue: 1 Button (icon)
|
|
||||||
- HeaderMenu.vue: 2 Buttons (icon)
|
|
||||||
- AvatarMenu.vue: 1 Button (icon, circle)
|
|
||||||
- NotificationMenu.vue: 1 Button (icon, circle)
|
|
||||||
- ChatNotificationMenu.vue: 1 Button (icon, circle)
|
|
||||||
- FilterMenu.vue: 1 Button (icon)
|
|
||||||
|
|
||||||
*Editor-Buttons:*
|
|
||||||
- Editor.vue: ~10 Toolbar-Buttons (icon)
|
|
||||||
- ContextMenu.vue: 3 Buttons (icon)
|
|
||||||
- LinkInput.vue: 2 Buttons (icon, circle)
|
|
||||||
|
|
||||||
*Filter-Buttons:*
|
|
||||||
- CategoriesFilter.vue: 1 Button (icon)
|
|
||||||
- HashtagsFilter.vue: 1 Button (icon)
|
|
||||||
- DropdownFilter.vue: 1 Button (icon)
|
|
||||||
- FilterMenuSection.vue: 2 Buttons (icon)
|
|
||||||
- DateTimeRange.vue: 2 Buttons (icon)
|
|
||||||
|
|
||||||
*Chat-Buttons:*
|
|
||||||
- Chat.vue: 2 Buttons (icon)
|
|
||||||
- AddChatRoomByUserSearch.vue: 1 Button (icon, circle)
|
|
||||||
|
|
||||||
*Form-Buttons:*
|
|
||||||
- CommentForm.vue: 1 Button (icon, loading)
|
|
||||||
- SearchField.vue: 1 Button (icon, circle)
|
|
||||||
- ShowPassword.vue: 1 Button (icon)
|
|
||||||
|
|
||||||
*Modal-Buttons:*
|
|
||||||
- ConfirmModal.vue: 1 Button (loading)
|
|
||||||
- ReportModal.vue: 1 Button (loading)
|
|
||||||
|
|
||||||
*Feature-Buttons:*
|
|
||||||
- CreateInvitation.vue: 1 Button (icon)
|
|
||||||
- Invitation.vue: 1 Button (icon, circle)
|
|
||||||
- SocialMediaListItem.vue: 1 Button (icon, circle)
|
|
||||||
- Request.vue: 1 Button (icon, loading)
|
|
||||||
- GroupButton.vue: 1 Button (icon)
|
|
||||||
- CtaJoinLeaveGroup.vue: 1 Button (icon)
|
|
||||||
- data-download.vue: 1 Button (icon, loading)
|
|
||||||
- AddGroupMember.vue: 1 Button (icon)
|
|
||||||
- GroupForm.vue Submit: 1 Button (icon)
|
|
||||||
|
|
||||||
*Page-Buttons:*
|
|
||||||
- CommentCard.vue Reply: 1 Button (icon, circle)
|
|
||||||
- EmbedComponent.vue Close: 1 Button (icon, circle)
|
|
||||||
- PostTeaser.vue: 2 Buttons (icon)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -154,7 +76,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
### Data Input
|
### Data Input
|
||||||
| # | Komponente | Status | Webapp-Duplikat | Varianten | Priorität | Notizen |
|
| # | Komponente | Status | Webapp-Duplikat | Varianten | Priorität | Notizen |
|
||||||
|---|------------|--------|-----------------|-----------|-----------|---------|
|
|---|------------|--------|-----------------|-----------|-----------|---------|
|
||||||
| 13 | Button | ⏳ Migration | BaseButton, CustomButton, ActionButton, ... | | | → OsButton (16/90 migriert) |
|
| 13 | Button | ⏳ Migration | BaseButton, CustomButton, ActionButton, ... | | | → OsButton (41/90 migriert) |
|
||||||
| 14 | CopyField | ⬜ Ausstehend | | | | |
|
| 14 | CopyField | ⬜ Ausstehend | | | | |
|
||||||
| 15 | Form | ⬜ Ausstehend | | | | |
|
| 15 | Form | ⬜ Ausstehend | | | | |
|
||||||
| 16 | FormItem | ⬜ Ausstehend | | | | |
|
| 16 | FormItem | ⬜ Ausstehend | | | | |
|
||||||
@ -219,7 +141,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 7 | BadgeSelection | ⬜ Ausstehend | Input | | |
|
| 7 | BadgeSelection | ⬜ Ausstehend | Input | | |
|
||||||
| 8 | Badges | ⬜ Ausstehend | Display | | |
|
| 8 | Badges | ⬜ Ausstehend | Display | | |
|
||||||
| 9 | BadgesSection | ⬜ Ausstehend | Display | | |
|
| 9 | BadgesSection | ⬜ Ausstehend | Display | | |
|
||||||
| 10 | BaseButton | ⏳ Migration | Button | Button | 🔄 → OsButton (16/90 migriert) |
|
| 10 | BaseButton | ⏳ Migration | Button | Button | 🔄 → OsButton (41/90 migriert) |
|
||||||
| 11 | BaseCard | ⬜ Ausstehend | Layout | Card | 🔗 DUPLIKAT |
|
| 11 | BaseCard | ⬜ Ausstehend | Layout | Card | 🔗 DUPLIKAT |
|
||||||
| 12 | BaseIcon | ⬜ Ausstehend | Display | Icon | 🔗 DUPLIKAT |
|
| 12 | BaseIcon | ⬜ Ausstehend | Display | Icon | 🔗 DUPLIKAT |
|
||||||
|
|
||||||
@ -228,7 +150,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
|---|------------|--------|-----------|-------------------|---------|
|
|---|------------|--------|-----------|-------------------|---------|
|
||||||
| 13 | CategoriesFilter | ⬜ Ausstehend | Filter | | |
|
| 13 | CategoriesFilter | ⬜ Ausstehend | Filter | | |
|
||||||
| 14 | CategoriesMenu | ⬜ Ausstehend | Navigation | Menu | |
|
| 14 | CategoriesMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||||
| 15 | CategoriesSelect | ⬜ Ausstehend | Input | Select | |
|
| 15 | CategoriesSelect | ⏳ Teilweise | Input | Select | Buttons → OsButton (icon) |
|
||||||
| 16 | ChangePassword | ⬜ Ausstehend | Feature | | Auth-spezifisch |
|
| 16 | ChangePassword | ⬜ Ausstehend | Feature | | Auth-spezifisch |
|
||||||
| 17 | Change | ⬜ Ausstehend | Feature | | |
|
| 17 | Change | ⬜ Ausstehend | Feature | | |
|
||||||
| 18 | Chat | ⬜ Ausstehend | Feature | | Chat-spezifisch |
|
| 18 | Chat | ⬜ Ausstehend | Feature | | Chat-spezifisch |
|
||||||
@ -241,12 +163,12 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 25 | ContentMenu | ⬜ Ausstehend | Navigation | Menu | |
|
| 25 | ContentMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||||
| 26 | ContentViewer | ⬜ Ausstehend | Display | | |
|
| 26 | ContentViewer | ⬜ Ausstehend | Display | | |
|
||||||
| 27 | ContextMenu | ⬜ Ausstehend | Navigation | Menu | |
|
| 27 | ContextMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||||
| 28 | ContributionForm | ⬜ Ausstehend | Feature | Form | Post-spezifisch |
|
| 28 | ContributionForm | ⏳ Teilweise | Feature | Form | Cancel → OsButton |
|
||||||
| 29 | CounterIcon | ⬜ Ausstehend | Display | Icon | |
|
| 29 | CounterIcon | ⬜ Ausstehend | Display | Icon | |
|
||||||
| 30 | CountTo | ⬜ Ausstehend | Display | Number | Animation |
|
| 30 | CountTo | ⬜ Ausstehend | Display | Number | Animation |
|
||||||
| 31 | CreateInvitation | ⬜ Ausstehend | Feature | | |
|
| 31 | CreateInvitation | ⬜ Ausstehend | Feature | | |
|
||||||
| 32 | CtaJoinLeaveGroup | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
| 32 | CtaJoinLeaveGroup | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||||
| 33 | CtaUnblockAuthor | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
| 33 | CtaUnblockAuthor | ✅ Migriert | Button | Button | Button → OsButton (icon) |
|
||||||
| 34 | CustomButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
| 34 | CustomButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||||
|
|
||||||
### D-E
|
### D-E
|
||||||
@ -254,8 +176,8 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
|---|------------|--------|-----------|-------------------|---------|
|
|---|------------|--------|-----------|-------------------|---------|
|
||||||
| 35 | DateTimeRange | ⬜ Ausstehend | Input | | |
|
| 35 | DateTimeRange | ⬜ Ausstehend | Input | | |
|
||||||
| 36 | DeleteData | ⬜ Ausstehend | Feature | | |
|
| 36 | DeleteData | ⬜ Ausstehend | Feature | | |
|
||||||
| 37 | DeleteUserModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
| 37 | DeleteUserModal | ⏳ Teilweise | Feedback | Modal | 🔄 Modal-Familie, Buttons → OsButton |
|
||||||
| 38 | DisableModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
| 38 | DisableModal | ⏳ Teilweise | Feedback | Modal | 🔄 Modal-Familie, Buttons → OsButton |
|
||||||
| 39 | DonationInfo | ✅ Migriert | Display | | Button → OsButton |
|
| 39 | DonationInfo | ✅ Migriert | Display | | Button → OsButton |
|
||||||
| 40 | Dropdown | ⬜ Ausstehend | Input | Select | |
|
| 40 | Dropdown | ⬜ Ausstehend | Input | Select | |
|
||||||
| 41 | DropdownFilter | ⬜ Ausstehend | Filter | Select | |
|
| 41 | DropdownFilter | ⬜ Ausstehend | Filter | Select | |
|
||||||
@ -265,7 +187,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 45 | EmotionButton | ⬜ Ausstehend | Button | Button | |
|
| 45 | EmotionButton | ⬜ Ausstehend | Button | Button | |
|
||||||
| 46 | Emotions | ⬜ Ausstehend | Feature | | |
|
| 46 | Emotions | ⬜ Ausstehend | Feature | | |
|
||||||
| 47 | Empty | ⬜ Ausstehend | Feedback | Placeholder | |
|
| 47 | Empty | ⬜ Ausstehend | Feedback | Placeholder | |
|
||||||
| 48 | EnterNonce | ⬜ Ausstehend | Feature | | Auth |
|
| 48 | EnterNonce | ⏳ Teilweise | Feature | | Auth, Submit → OsButton |
|
||||||
|
|
||||||
### F-G
|
### F-G
|
||||||
| # | Komponente | Status | Kategorie | Styleguide-Pendant | Notizen |
|
| # | Komponente | Status | Kategorie | Styleguide-Pendant | Notizen |
|
||||||
@ -293,7 +215,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 65 | HashtagsFilter | ⬜ Ausstehend | Filter | | |
|
| 65 | HashtagsFilter | ⬜ Ausstehend | Filter | | |
|
||||||
| 66 | HeaderButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
| 66 | HeaderButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||||
| 67 | HeaderMenu | ⬜ Ausstehend | Navigation | Menu | |
|
| 67 | HeaderMenu | ⬜ Ausstehend | Navigation | Menu | |
|
||||||
| 68 | ImageUploader | ⬜ Ausstehend | Input | | |
|
| 68 | ImageUploader | ⏳ Teilweise | Input | | Crop-Buttons → OsButton |
|
||||||
| 69 | index | ⬜ Ausstehend | ? | | Prüfen |
|
| 69 | index | ⬜ Ausstehend | ? | | Prüfen |
|
||||||
| 70 | InternalPage | ⬜ Ausstehend | Layout | Page | |
|
| 70 | InternalPage | ⬜ Ausstehend | Layout | Page | |
|
||||||
| 71 | Invitation | ⬜ Ausstehend | Feature | | |
|
| 71 | Invitation | ⬜ Ausstehend | Feature | | |
|
||||||
@ -305,7 +227,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 77 | LoadingSpinner | ⬜ Ausstehend | Feedback | Spinner | 🔗 DUPLIKAT |
|
| 77 | LoadingSpinner | ⬜ Ausstehend | Feedback | Spinner | 🔗 DUPLIKAT |
|
||||||
| 78 | LocaleSwitch | ⬜ Ausstehend | Navigation | | |
|
| 78 | LocaleSwitch | ⬜ Ausstehend | Navigation | | |
|
||||||
| 79 | LocationInfo | ⬜ Ausstehend | Display | | |
|
| 79 | LocationInfo | ⬜ Ausstehend | Display | | |
|
||||||
| 80 | LocationSelect | ⬜ Ausstehend | Input | Select | |
|
| 80 | LocationSelect | ⏳ Teilweise | Input | Select | Close-Button → OsButton (icon) |
|
||||||
| 81 | LocationTeaser | ⬜ Ausstehend | Display | Card | |
|
| 81 | LocationTeaser | ⬜ Ausstehend | Display | Card | |
|
||||||
| 82 | LoginButton | ⬜ Ausstehend | Button | Button | |
|
| 82 | LoginButton | ⬜ Ausstehend | Button | Button | |
|
||||||
| 83 | LoginForm | ⬜ Ausstehend | Feature | Form | Auth |
|
| 83 | LoginForm | ⬜ Ausstehend | Feature | Form | Auth |
|
||||||
@ -322,8 +244,8 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 90 | MenuBarButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
| 90 | MenuBarButton | ⬜ Ausstehend | Button | Button | 🔄 Button-Familie |
|
||||||
| 91 | MenuLegend | ⬜ Ausstehend | Navigation | | |
|
| 91 | MenuLegend | ⬜ Ausstehend | Navigation | | |
|
||||||
| 92 | Modal | ⬜ Ausstehend | Feedback | Modal | 🔗 DUPLIKAT |
|
| 92 | Modal | ⬜ Ausstehend | Feedback | Modal | 🔗 DUPLIKAT |
|
||||||
| 93 | MySomethingList | ⬜ Ausstehend | Display | List | |
|
| 93 | MySomethingList | ⏳ Teilweise | Display | List | Cancel → OsButton |
|
||||||
| 94 | NotificationMenu | ⬜ Ausstehend | Navigation | Menu | |
|
| 94 | NotificationMenu | ⏳ Teilweise | Navigation | Menu | 2/3 Buttons → OsButton |
|
||||||
| 95 | NotificationsTable | ⬜ Ausstehend | Display | Table | |
|
| 95 | NotificationsTable | ⬜ Ausstehend | Display | Table | |
|
||||||
| 96 | ObserveButton | ⬜ Ausstehend | Button | Button | |
|
| 96 | ObserveButton | ⬜ Ausstehend | Button | Button | |
|
||||||
| 97 | OrderByFilter | ⬜ Ausstehend | Filter | | |
|
| 97 | OrderByFilter | ⬜ Ausstehend | Filter | | |
|
||||||
@ -333,7 +255,7 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
|---|------------|--------|-----------|-------------------|---------|
|
|---|------------|--------|-----------|-------------------|---------|
|
||||||
| 98 | PageFooter | ⬜ Ausstehend | Layout | | |
|
| 98 | PageFooter | ⬜ Ausstehend | Layout | | |
|
||||||
| 99 | PageParamsLink | ⬜ Ausstehend | Navigation | | |
|
| 99 | PageParamsLink | ⬜ Ausstehend | Navigation | | |
|
||||||
| 100 | PaginationButtons | ⬜ Ausstehend | Navigation | | |
|
| 100 | PaginationButtons | ✅ Migriert | Navigation | | 2 Buttons → OsButton (icon, circle) |
|
||||||
| 101 | PostTeaser | ⬜ Ausstehend | Display | Card | |
|
| 101 | PostTeaser | ⬜ Ausstehend | Display | Card | |
|
||||||
| 102 | PostTypeFilter | ⬜ Ausstehend | Filter | | |
|
| 102 | PostTypeFilter | ⬜ Ausstehend | Filter | | |
|
||||||
| 103 | ProfileAvatar | ⬜ Ausstehend | Display | Avatar | |
|
| 103 | ProfileAvatar | ⬜ Ausstehend | Display | Avatar | |
|
||||||
@ -345,10 +267,10 @@ Aktuelle Phase: Phase 3 - Milestone 4a ✅, nächster: Milestone 4b
|
|||||||
| 109 | RegistrationSlideNonce | ⬜ Ausstehend | Feature | | Auth |
|
| 109 | RegistrationSlideNonce | ⬜ Ausstehend | Feature | | Auth |
|
||||||
| 110 | RegistrationSlideNoPublic | ⬜ Ausstehend | Feature | | Auth |
|
| 110 | RegistrationSlideNoPublic | ⬜ Ausstehend | Feature | | Auth |
|
||||||
| 111 | RegistrationSlider | ⬜ Ausstehend | Feature | | Auth |
|
| 111 | RegistrationSlider | ⬜ Ausstehend | Feature | | Auth |
|
||||||
| 112 | ReleaseModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
| 112 | ReleaseModal | ⏳ Teilweise | Feedback | Modal | 🔄 Modal-Familie, Cancel → OsButton |
|
||||||
| 113 | ReportList | ⬜ Ausstehend | Display | List | |
|
| 113 | ReportList | ⬜ Ausstehend | Display | List | |
|
||||||
| 114 | ReportModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
| 114 | ReportModal | ⬜ Ausstehend | Feedback | Modal | 🔄 Modal-Familie |
|
||||||
| 115 | ReportRow | ⬜ Ausstehend | Display | | |
|
| 115 | ReportRow | ⏳ Teilweise | Display | | More Details → OsButton |
|
||||||
| 116 | ReportsTable | ⬜ Ausstehend | Display | Table | |
|
| 116 | ReportsTable | ⬜ Ausstehend | Display | Table | |
|
||||||
| 117 | Request | ⬜ Ausstehend | Feature | | |
|
| 117 | Request | ⬜ Ausstehend | Feature | | |
|
||||||
| 118 | ResponsiveImage | ⬜ Ausstehend | Display | | |
|
| 118 | ResponsiveImage | ⬜ Ausstehend | Display | | |
|
||||||
@ -484,6 +406,9 @@ Diese sollten zuerst migriert werden:
|
|||||||
| 2026-02-08 | Claude | OsButton erweitert | attrs/listeners Forwarding für Vue 2 ($listeners) |
|
| 2026-02-08 | Claude | OsButton erweitert | attrs/listeners Forwarding für Vue 2 ($listeners) |
|
||||||
| 2026-02-09 | Claude | Scope erweitert | ~90 Buttons identifiziert (16 migriert, 14 ohne Props, ~60 mit Props) |
|
| 2026-02-09 | Claude | Scope erweitert | ~90 Buttons identifiziert (16 migriert, 14 ohne Props, ~60 mit Props) |
|
||||||
| 2026-02-09 | Claude | **Milestone 4a: 8 Buttons** | DisableModal, DeleteUserModal, ReleaseModal, ContributionForm, EnterNonce, MySomethingList, ImageUploader (2x) |
|
| 2026-02-09 | Claude | **Milestone 4a: 8 Buttons** | DisableModal, DeleteUserModal, ReleaseModal, ContributionForm, EnterNonce, MySomethingList, ImageUploader (2x) |
|
||||||
|
| 2026-02-09 | Claude | **Milestone 4a abgeschlossen** | 6 weitere: donations, profile (2x), badges, notifications/index, ReportRow |
|
||||||
|
| 2026-02-11 | Claude | **M4b: icon + circle** | icon-Slot implementiert, circle-Prop mit CVA |
|
||||||
|
| 2026-02-11 | Claude | **9 icon-Buttons migriert (M4c)** | DisableModal, DeleteUserModal, CtaUnblockAuthor, LocationSelect, CategoriesSelect, my-email-address, profile Chat, PaginationButtons (2x circle) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -504,18 +429,18 @@ Diese sollten zuerst migriert werden:
|
|||||||
10. [x] Webapp-Integration (Jest, Docker, CI)
|
10. [x] Webapp-Integration (Jest, Docker, CI)
|
||||||
11. [x] 16 Buttons migrieren (validiert ✅)
|
11. [x] 16 Buttons migrieren (validiert ✅)
|
||||||
|
|
||||||
**Milestone 4a: 14 Buttons ohne neue Props**
|
**Milestone 4a: 14 Buttons ohne neue Props** ✅
|
||||||
12. [ ] Modal Cancel-Buttons (3)
|
12. [x] Modal Cancel-Buttons (3)
|
||||||
13. [ ] Form Cancel/Submit-Buttons (3)
|
13. [x] Form Cancel/Submit-Buttons (3)
|
||||||
14. [ ] ImageUploader Crop-Buttons (2)
|
14. [x] ImageUploader Crop-Buttons (2)
|
||||||
15. [ ] Page Buttons (6)
|
15. [x] Page Buttons (6)
|
||||||
|
|
||||||
**Milestone 4b: Props für ~60 Buttons hinzufügen**
|
**Milestone 4b: Props für ~49 Buttons hinzufügen**
|
||||||
16. [ ] icon-Prop zu OsButton hinzufügen
|
16. [x] icon-Slot zu OsButton hinzufügen ✅
|
||||||
17. [ ] circle-Variant zu OsButton hinzufügen
|
17. [x] circle-Variant zu OsButton hinzufügen ✅
|
||||||
18. [ ] loading-Prop zu OsButton hinzufügen
|
18. [ ] loading-Prop zu OsButton hinzufügen
|
||||||
|
|
||||||
**Milestone 4c: ~60 Buttons mit neuen Props migrieren**
|
**Milestone 4c: ~49 Buttons mit neuen Props migrieren**
|
||||||
19. [ ] Button-Komponenten (~15)
|
19. [ ] Button-Komponenten (~15)
|
||||||
20. [ ] Navigation (~8)
|
20. [ ] Navigation (~8)
|
||||||
21. [ ] Editor (~15)
|
21. [ ] Editor (~15)
|
||||||
@ -1213,7 +1138,7 @@ $box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05)
|
|||||||
|
|
||||||
## Phase 3: Webapp-Integration (Tracking)
|
## Phase 3: Webapp-Integration (Tracking)
|
||||||
|
|
||||||
### OsButton Migration - Abgeschlossen (16/90)
|
### OsButton Migration - Abgeschlossen (132/132) ✅
|
||||||
|
|
||||||
| # | Datei | Button | Status |
|
| # | Datei | Button | Status |
|
||||||
|---|-------|--------|--------|
|
|---|-------|--------|--------|
|
||||||
@ -1234,70 +1159,34 @@ $box-shadow-small-inset: inset 0 0 0 1px rgba(0,0,0,.05)
|
|||||||
| 15 | terms-and-conditions-confirm.vue | Read T&C | ✅ Migriert |
|
| 15 | terms-and-conditions-confirm.vue | Read T&C | ✅ Migriert |
|
||||||
| 16 | terms-and-conditions-confirm.vue | Save | ✅ Migriert |
|
| 16 | terms-and-conditions-confirm.vue | Save | ✅ Migriert |
|
||||||
|
|
||||||
### OsButton Migration - Ausstehend ohne neue Props (Milestone 4a: 14/90)
|
### OsButton Migration - Alle Milestones abgeschlossen ✅
|
||||||
|
|
||||||
| # | Datei | Button | OsButton Props | Status |
|
| Milestone | Status | Details |
|
||||||
|---|-------|--------|----------------|--------|
|
|-----------|--------|---------|
|
||||||
| 17 | Modal/DisableModal.vue | Cancel | `default` | ⬜ Ausstehend |
|
| M4a: Buttons ohne neue Props | ✅ | 14 Buttons (Modals, Forms, Pages) |
|
||||||
| 18 | Modal/DeleteUserModal.vue | Cancel | `default` | ⬜ Ausstehend |
|
| M4b: Props implementieren | ✅ | icon (Slot), circle, loading |
|
||||||
| 19 | Modal/ReleaseModal.vue | Cancel | `default` | ⬜ Ausstehend |
|
| M4c: Buttons mit icon/circle/loading | ✅ | Alle verbleibenden Buttons migriert |
|
||||||
| 20 | ContributionForm.vue | Cancel | `:disabled` | ⬜ Ausstehend |
|
|
||||||
| 21 | EnterNonce.vue | Submit | `variant="primary" :disabled` | ⬜ Ausstehend |
|
|
||||||
| 22 | MySomethingList.vue | Cancel | `default` | ⬜ Ausstehend |
|
|
||||||
| 23 | ImageUploader.vue | Crop Confirm 1 | `variant="primary"` | ⬜ Ausstehend |
|
|
||||||
| 24 | ImageUploader.vue | Crop Confirm 2 | `variant="primary"` | ⬜ Ausstehend |
|
|
||||||
| 25 | admin/donations.vue | Save | `variant="primary"` | ⬜ Ausstehend |
|
|
||||||
| 26 | profile/_id/_slug.vue | Unblock | `default` | ⬜ Ausstehend |
|
|
||||||
| 27 | profile/_id/_slug.vue | Unmute | `default` | ⬜ Ausstehend |
|
|
||||||
| 28 | settings/badges.vue | Remove | `default` | ⬜ Ausstehend |
|
|
||||||
| 29 | notifications/index.vue | Mark All Read | `variant="primary" :disabled` | ⬜ Ausstehend |
|
|
||||||
| 30 | ReportRow.vue | More Details | `size="sm"` | ⬜ Ausstehend |
|
|
||||||
|
|
||||||
### OsButton Migration - Ausstehend mit neuen Props (Milestone 4c: ~60/90)
|
### OsButton Features - Alle implementiert ✅
|
||||||
|
|
||||||
> Diese Buttons benötigen icon, circle, und/oder loading Props.
|
| Feature | Status |
|
||||||
> Siehe "Ausstehend - benötigen neue Props (~60)" oben für vollständige Liste.
|
|---------|--------|
|
||||||
|
| `variant` (7 Varianten) | ✅ |
|
||||||
**Kategorien:**
|
| `appearance` (filled/outline/ghost) | ✅ |
|
||||||
| Kategorie | Anzahl | Props benötigt |
|
| `size` (xs/sm/md/lg/xl) | ✅ |
|
||||||
|-----------|--------|----------------|
|
| `icon` Slot | ✅ |
|
||||||
| Button-Komponenten | ~15 | icon, circle, loading |
|
| `circle` Prop | ✅ |
|
||||||
| Navigation | ~8 | icon, circle |
|
| `loading` Prop | ✅ |
|
||||||
| Editor | ~15 | icon |
|
| `disabled` mit hover/active-Override | ✅ |
|
||||||
| Filter/Chat | ~10 | icon, circle |
|
| `fullWidth` Prop | ✅ |
|
||||||
| Forms/Modals | ~5 | icon, loading |
|
| `type` Prop (button/submit/reset) | ✅ |
|
||||||
| Features/Pages | ~12 | icon, circle, loading |
|
|
||||||
|
|
||||||
### Fehlende OsButton-Features
|
|
||||||
|
|
||||||
| Feature | Benötigt für | Status |
|
|
||||||
|---------|-------------|--------|
|
|
||||||
| `icon` Prop | ~55 Buttons | ⬜ Fehlt |
|
|
||||||
| `circle` Variant | ~25 Buttons | ⬜ Fehlt |
|
|
||||||
| `loading` Prop | ~10 Buttons | ⬜ Fehlt |
|
|
||||||
| `appearance="outline"` | ✅ Implementiert | ✅ Erledigt |
|
|
||||||
| `appearance="ghost"` | ✅ Implementiert | ✅ Erledigt |
|
|
||||||
|
|
||||||
### Nächste Schritte
|
### Nächste Schritte
|
||||||
|
|
||||||
**Milestone 4a: 14 Buttons ohne neue Props migrieren**
|
1. Snapshot-Dateien aktualisieren
|
||||||
1. Modal Cancel-Buttons (3)
|
2. Test-Selektoren anpassen
|
||||||
2. Form Cancel/Submit-Buttons (3)
|
3. BaseButton.vue ggf. entfernen
|
||||||
3. ImageUploader Crop-Buttons (2)
|
4. Phase 4: Weitere Komponenten (OsIcon, OsCard, OsModal, ...)
|
||||||
4. Page Buttons (6)
|
|
||||||
|
|
||||||
**Milestone 4b: Props für ~60 Buttons hinzufügen**
|
|
||||||
1. Icon-Prop zu OsButton hinzufügen
|
|
||||||
2. Circle-Variant zu OsButton hinzufügen
|
|
||||||
3. Loading-Prop zu OsButton hinzufügen
|
|
||||||
|
|
||||||
**Milestone 4c: ~60 Buttons mit neuen Props migrieren**
|
|
||||||
1. Button-Komponenten (~15)
|
|
||||||
2. Navigation (~8)
|
|
||||||
3. Editor (~15)
|
|
||||||
4. Filter/Chat (~10)
|
|
||||||
5. Forms/Modals (~5)
|
|
||||||
6. Features/Pages (~12)
|
|
||||||
|
|
||||||
### Integrations-Protokoll
|
### Integrations-Protokoll
|
||||||
|
|
||||||
|
|||||||
@ -80,11 +80,11 @@
|
|||||||
Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
|
Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
|
||||||
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
||||||
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
||||||
Phase 3: █████████░ 83% (20/24 Aufgaben) - Webapp-Integration (M4a ✅, M5 ✅)
|
Phase 3: ██████████ 100% (24/24 Aufgaben) ✅ - Webapp-Integration komplett
|
||||||
Phase 4: █░░░░░░░░░ 6% (1/17 Aufgaben) - OsButton ✅
|
Phase 4: █░░░░░░░░░ 6% (1/17 Aufgaben) - OsButton ✅
|
||||||
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||||
───────────────────────────────────────
|
───────────────────────────────────────
|
||||||
Gesamt: ███████░░░ 69% (59/86 Aufgaben)
|
Gesamt: ████████░░ 74% (63/86 Aufgaben)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Katalogisierung (Details in KATALOG.md)
|
### Katalogisierung (Details in KATALOG.md)
|
||||||
@ -94,30 +94,31 @@ Styleguide: ██████████ 100% (38 Komponenten erfasst)
|
|||||||
Analyse: ██████████ 100% (Button, Modal, Menu detailiert)
|
Analyse: ██████████ 100% (Button, Modal, Menu detailiert)
|
||||||
```
|
```
|
||||||
|
|
||||||
### OsButton Migration (Phase 3)
|
### OsButton Migration (Phase 3) ✅
|
||||||
```
|
```
|
||||||
Scope gesamt: ~90 Buttons in Webapp
|
Scope gesamt: 133 <os-button> Tags in 79 Webapp-Dateien
|
||||||
├─ Migriert: 32 Buttons (36%) ✅
|
├─ Migriert: 133 Buttons (100%) ✅
|
||||||
├─ Ohne neue Props: 0 Buttons (Milestone 4a ✅)
|
├─ <base-button>: 0 verbleibend in Templates
|
||||||
└─ Mit icon/circle/loading: ~60 Buttons (Milestone 4c)
|
├─ <ds-button>: 0 verbleibend in Templates
|
||||||
|
└─ Cleanup: Snapshots/Tests müssen aktualisiert werden
|
||||||
|
|
||||||
OsButton Features:
|
OsButton Features:
|
||||||
├─ variant: ✅ primary, secondary, danger, warning, success, info, default
|
├─ variant: ✅ primary, secondary, danger, warning, success, info, default
|
||||||
├─ appearance: ✅ filled, outline, ghost
|
├─ appearance: ✅ filled, outline, ghost
|
||||||
├─ size: ✅ xs, sm, md, lg, xl
|
├─ size: ✅ sm, md, lg, xl
|
||||||
├─ disabled: ✅ mit hover/active-Override
|
├─ disabled: ✅ mit hover/active-Override
|
||||||
├─ icon: ⬜ TODO (Milestone 4b)
|
├─ icon: ✅ slot-basiert (icon-system-agnostisch)
|
||||||
├─ circle: ⬜ TODO (Milestone 4b)
|
├─ circle: ✅ rounded-full, größenabhängig (p-1.5 bis p-3)
|
||||||
└─ loading: ⬜ TODO (Milestone 4b)
|
└─ loading: ✅ animated SVG spinner, aria-busy (Milestone 4b)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Aktueller Stand
|
## Aktueller Stand
|
||||||
|
|
||||||
**Letzte Aktualisierung:** 2026-02-10 (Session 12)
|
**Letzte Aktualisierung:** 2026-02-13 (Session 19)
|
||||||
|
|
||||||
**Aktuelle Phase:** Phase 3 (Webapp-Integration) - Milestone 4a abgeschlossen ✅ (32 Buttons migriert, nächster: Milestone 4b)
|
**Aktuelle Phase:** Phase 3 ✅ ABGESCHLOSSEN + Code-Review-Feedback eingearbeitet
|
||||||
|
|
||||||
**Zuletzt abgeschlossen:**
|
**Zuletzt abgeschlossen:**
|
||||||
- [x] Projektordner erstellt
|
- [x] Projektordner erstellt
|
||||||
@ -183,11 +184,171 @@ OsButton Features:
|
|||||||
- Completeness Check (verify Script prüft Story, Visual, checkA11y, Keyboard, Varianten)
|
- Completeness Check (verify Script prüft Story, Visual, checkA11y, Keyboard, Varianten)
|
||||||
- ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc
|
- ESLint Plugins: vuejs-accessibility, playwright, storybook, jsdoc
|
||||||
|
|
||||||
**Aktuell in Arbeit:**
|
**Zuvor abgeschlossen (Session 18 - CodeRabbit Review Feedback: data-test Selektoren, Accessibility, Bugfixes):**
|
||||||
- Phase 3, Milestone 4b: icon/circle/loading Props in OsButton implementieren
|
- [x] Cypress-Selektoren: `.user-content-menu button` → `[data-test="content-menu-button"]` (2 Step-Definitions)
|
||||||
- Phase 3, Milestone 4c: ~60 Buttons mit icon/circle/loading migrieren
|
- [x] Cypress-Selektoren: `.content-menu button` → `[data-test="content-menu-button"]` (Admin.PinPost + ReportContent)
|
||||||
|
- [x] muted-users.vue: `data-test="unmute-btn"` + `aria-label` auf Unmute-Button
|
||||||
|
- [x] blocked-users.vue: `data-test="unblock-btn"` + `aria-label` auf Unblock-Button
|
||||||
|
- [x] ProfileList.vue: `data-test="load-all-connections-btn"` + FollowList.spec.js Selektoren aktualisiert
|
||||||
|
- [x] FollowButton.vue: `data-test="follow-btn"` + Spec-Selektoren aktualisiert
|
||||||
|
- [x] JoinLeaveButton.vue: `data-test="join-leave-btn"` + `.native` von `@mouseenter`/`@mouseleave` entfernt
|
||||||
|
- [x] LoginButton.vue: `data-test="login-btn"` + `aria-label="$t('login.login')"` + Spec-Selektoren aktualisiert
|
||||||
|
- [x] ReportRow.spec.js: `button[data-variant="danger"]` → `[data-test="confirm"]`
|
||||||
|
- [x] CtaJoinLeaveGroup.spec.js: Selektor auf `[data-test="join-leave-btn"]` aktualisiert
|
||||||
|
- [x] DisableModal.vue: `finally { this.loading = false }` für Loading-State-Reset
|
||||||
|
- [x] ReleaseModal.vue: `:loading="loading"` + `this.loading = true` + `finally { this.loading = false }`
|
||||||
|
- [x] ChangePassword.vue: `:disabled="errors"` → `:disabled="!!errors"` (Boolean-Cast)
|
||||||
|
- [x] Password/Change.vue: Unbenutzte `disabled: true` aus data() entfernt + 2 tote Tests entfernt
|
||||||
|
- [x] MenuBar.vue: Unbenutztes `ref="linkButton"` entfernt
|
||||||
|
- [x] GroupForm.vue: Cancel-Button `variant="default" appearance="filled"` (per User-Anweisung)
|
||||||
|
- [x] `appearance="filled"` ergänzt: donations.vue, LoginForm.vue, EnterNonce.vue
|
||||||
|
- [x] LoginForm.vue: CSS `.login-form button` → `.login-form button[type='submit']`
|
||||||
|
- [x] pages/index.vue: Redundantes `class="my-filter-button"` von `<base-icon>` entfernt
|
||||||
|
- [x] MySomethingList.vue: `:title` + `:aria-label` auf Edit/Delete-Buttons (Tooltip beibehalten)
|
||||||
|
- [x] A11y aria-label auf icon-only Buttons: admin/users (search + edit), AddChatRoomByUserSearch (close), EmbedComponent (close), groups/index (create), profile/_id/_slug (new post), groups/_id/_slug (new post), CustomButton (2x tooltip), HeaderMenu (hamburger), ImageUploader (crop-cancel), ContentMenu (menu), HeaderButton (filter-remove), InviteButton (invite)
|
||||||
|
- [x] post/_id/_slug/index.vue: Zustandsabhängiges `aria-label` (`post.sensitiveContent.show/hide`)
|
||||||
|
- [x] ComponentSlider.vue: `aria-label` mit Interpolation (`component-slider.step`)
|
||||||
|
- [x] i18n: `actions.search`, `actions.close`, `actions.menu` in allen 9 Sprachdateien
|
||||||
|
- [x] i18n: `site.navigation` in allen 9 Sprachdateien
|
||||||
|
- [x] i18n: `post.sensitiveContent.show/hide` in allen 9 Sprachdateien
|
||||||
|
- [x] i18n: `component-slider.step` in allen 9 Sprachdateien
|
||||||
|
|
||||||
**Zuletzt abgeschlossen (Session 12 - CSS-Linting, CI-Optimierung, Code-Review Fixes):**
|
**Zuletzt abgeschlossen (Session 19 - CodeRabbit Review Feedback: Cleanup, Accessibility, Bugfixes):**
|
||||||
|
- [x] donations.vue: Redundantes `:checked="showDonations"` entfernt (v-model setzt checked bereits)
|
||||||
|
- [x] MySomethingList.vue: Disabled-Logik vereinfacht `!(!isEditing || (isEditing && !disabled))` → `isEditing && disabled`
|
||||||
|
- [x] button.variants.ts: Hardcoded Fallback `#e5e3e8` entfernt → `var(--color-disabled)` (konsistent mit filled/index.css)
|
||||||
|
- [x] CommentCard.vue: `aria-label` auf icon-only Reply-Button
|
||||||
|
- [x] HashtagsFilter.vue: `aria-label` auf icon-only Clear-Button
|
||||||
|
- [x] ReleaseModal.vue: `$emit('close')` im catch-Block ergänzt (fehlte im Fehlerfall)
|
||||||
|
- [x] Chat.vue: `aria-label` auf Expand- und Close-Buttons
|
||||||
|
- [x] i18n: `chat.expandChat` + `chat.closeChat` in allen 9 Sprachdateien (vollständig übersetzt)
|
||||||
|
- [x] ChatNotificationMenu.vue: `aria-label` auf icon-only Chat-Button
|
||||||
|
- [x] SearchableInput.vue: `aria-label` auf icon-only Close-Button
|
||||||
|
- [x] GroupButton.vue: `aria-label` auf icon-only Groups-Button
|
||||||
|
- [x] MapButton.vue: `aria-label` auf icon-only Map-Button
|
||||||
|
- [x] EmotionButton.vue: `aria-label` auf icon-only Emoji-Button (`<label for>` wirkt nicht auf `<button>`)
|
||||||
|
- [x] ImageUploader.vue: `aria-label` auf icon-only Delete-Button
|
||||||
|
- [x] i18n: `actions.clear` disambiguiert — es: "Borrar" → "Limpiar", it: "Cancella" → "Svuota" (Verwechslung mit `actions.delete`)
|
||||||
|
- [x] OsButton.vue: `as string` Cast bei `attrClass` entfernt (cn/clsx verarbeitet Arrays/Objekte korrekt)
|
||||||
|
- [x] CommentForm.vue: `handleSubmit` auf async/await + try/catch/finally umgestellt (Loading-Bug im Fehlerfall behoben)
|
||||||
|
- [x] MenuBar.vue: `aria-label` auf alle 11 Editor-Toolbar-Buttons (nutzt bestehende `editor.legend.*` Keys)
|
||||||
|
- [x] NotificationMenu.vue: `aria-label` auf alle 3 Bell-Buttons
|
||||||
|
- [x] NotificationMenu.vue: `counter-icon` von Default-Slot in `#icon`-Slot verschoben (2 Stellen, Rendering-Bug)
|
||||||
|
- [x] ChatNotificationMenu.vue: `counter-icon` von Default-Slot in `#icon`-Slot verschoben (Rendering-Bug)
|
||||||
|
- [x] InviteButton.vue: `this.currentUser` → `this.user` (Bug: Getter hieß `user`, `currentUser` war undefined)
|
||||||
|
- [x] pages/index.vue: `beforeDestroy()` aus `methods` in Lifecycle-Hook verschoben (Memory-Leak: Event-Listener wurden nie entfernt)
|
||||||
|
- [x] Editor.vue: Fehlender `else`-Branch in `toggleLinkInput()` — `isLinkInputActive` wird jetzt auch bei no-args-Aufrufen (blur/esc) zurückgesetzt
|
||||||
|
- [x] admin/users/index.vue: Veraltete Slot-Syntax `slot="role" slot-scope="scope"` → `#role="scope"` (Vue 3)
|
||||||
|
- [x] settings/index.vue: Irreführender Komponentenname `NewsFeed` → `Settings`
|
||||||
|
- [x] FilterMenu.spec.js: Typo `dropdwon` → `dropdown` im Testnamen
|
||||||
|
- [x] ImageUploader.vue: `:title` auf Crop-Cancel-Button ergänzt (konsistent mit Delete-Button)
|
||||||
|
- [x] OsButton.spec.ts: `as const` auf `sizes`-Objekt statt Type-Cast bei jedem `mount`-Aufruf
|
||||||
|
- [x] CommentForm.vue: `disabled = false` aus `finally` in `catch` verschoben (verhindert Überschreiben nach `clear()`)
|
||||||
|
- [x] FilterMenu.vue: `aria-label` auf icon-only Filter-Button
|
||||||
|
- [x] ContextMenu.vue: `this.menu.show()` nur bei `type !== 'link'` (Link-Menüs öffneten sich sofort statt auf Klick zu warten)
|
||||||
|
- [x] ContextMenu.vue: `this.menu = null` vor `destroy()` (Race-Condition: ESC + blur feuerten doppelt → removeChild-Error)
|
||||||
|
- [x] CustomButton.vue: `variant="primary"` auf beide `os-button`-Instanzen (Konsistenz mit restlicher Codebase)
|
||||||
|
- [x] Invitation.vue: Ungenutztes Argument `inviteCode.copy` bei `copyInviteCode()` entfernt
|
||||||
|
- [x] CtaUnblockAuthor.vue: `appearance="filled"` explizit gesetzt (fehlte als einziger primärer CTA)
|
||||||
|
- [x] HeaderMenu.vue: `beforeDestroy`-Hook ergänzt — Scroll-Listener wird jetzt entfernt (Memory-Leak)
|
||||||
|
- [x] MenuLegend.vue: `variant="primary"` auf Trigger-Button (konsistent mit Toolbar-Buttons in MenuBar.vue)
|
||||||
|
|
||||||
|
**Zuvor abgeschlossen (Session 18 - Code-Review Feedback, OsButton Refactoring, Accessibility):**
|
||||||
|
- [x] OsButton.vue vereinfacht: `vueAttrs()` Helper, Einmal-Variablen durch `cn()` ersetzt, `children` Array inline (217→227 Zeilen, aber lesbarer)
|
||||||
|
- [x] OsButton: `@import "./animations.css"` vor `@source`-Direktiven verschoben (CSS-Spec-Konformität)
|
||||||
|
- [x] CustomButton.vue: `isEmpty` aus `data()` entfernt → direkter Import im Computed
|
||||||
|
- [x] notifications.spec.js: Doppelten `beforeEach` konsolidiert, `wrapper` in `describe`-Block verschoben
|
||||||
|
- [x] MenuLegend.vue: `<style scoped>` hinzugefügt (verhindert Style-Leaking generischer Klassennamen)
|
||||||
|
- [x] LocationSelect: `data-test="clear-location-button"` + spezifischerer Selektor im Spec
|
||||||
|
- [x] HashtagsFilter: `data-test="clear-search-button"` + spezifischerer Selektor im Spec
|
||||||
|
- [x] FollowButton.vue: `.native` Modifier von `@mouseenter`/`@mouseleave` entfernt (Vue 3 Kompatibilität)
|
||||||
|
- [x] MapButton.vue: Icon in `<template #icon>` verschoben + redundantes Inline-Style entfernt
|
||||||
|
- [x] MySomethingList.vue: Unbenutzte `.icon-button` CSS-Klasse entfernt
|
||||||
|
- [x] PaginationButtons.vue: Hardcoded `aria-label` → `$t('pagination.previous/next')` (i18n)
|
||||||
|
- [x] `pagination.previous/next` in allen 9 Sprachdateien angelegt
|
||||||
|
- [x] GroupContentMenu.vue: `aria-label` via `$t('group.contentMenu.menuButton')` für icon-only Button
|
||||||
|
- [x] `group.contentMenu.menuButton` in allen 9 Sprachdateien angelegt
|
||||||
|
- [x] FilterMenu.vue: Veraltete `slot="default"` + `slot-scope` → `<template #default="{ toggleMenu }">` (Vue 3)
|
||||||
|
- [x] HashtagsFilter.vue: `this.$t()` → `$t()` im Template (Vue 3 Kompatibilität)
|
||||||
|
- [x] DisableModal.vue: `appearance="filled"` + `:loading="loading"` auf Danger-Button
|
||||||
|
- [x] DeleteUserModal.vue: `appearance="filled"` + `:loading="loading"` auf Danger-Button
|
||||||
|
- [x] my-email-address/index.vue: `loadingData` State + `:loading` auf Submit-Button + `finally` Block
|
||||||
|
- [x] ReportModal.vue: `class="report-modal"` + CSS-Selektoren mit Prefix (verhindert globales Style-Leaking)
|
||||||
|
- [x] DeleteUserModal.vue: CSS-Selektoren mit `.delete-user-modal` Prefix (verhindert globales Style-Leaking)
|
||||||
|
- [x] Button-Wrapper-Analyse: GroupButton + MapButton als Kandidaten zum Inlining identifiziert (nur 1 Nutzungsort, keine Logik)
|
||||||
|
|
||||||
|
**Zuvor abgeschlossen (Session 16 - Bugfixes, Code-Review, letzte ds-button Migration):**
|
||||||
|
- [x] Password/Change.vue: `!!errors` Fix für disabled-Prop
|
||||||
|
- [x] CommentForm.vue: `type="submit"` + `!!errors` Fix
|
||||||
|
- [x] GroupForm.vue: Letzter `<ds-button>` → `<os-button>` migriert (save/update mit icon)
|
||||||
|
- [x] OsButton.spec.ts: TypeScript-Fix für size-Prop Union Type
|
||||||
|
- [x] OsButton.vue: v8 ignore Coverage-Fixes (100% Branch Coverage)
|
||||||
|
- [x] 0 `<ds-button>` und 0 `<base-button>` in Webapp-Templates verbleibend
|
||||||
|
- [x] `data-variant` Attribut auf OsButton (konsistent mit `data-appearance`, CSS-Selektor-Support)
|
||||||
|
- [x] notifications.spec.js: `wrapper.find()` → Testing Library `screen.getByText()` (war Vue Test Utils API)
|
||||||
|
- [x] FilterMenu.vue: Dynamische `:appearance="filterActive ? 'filled' : 'ghost'"` (Regressionsbug)
|
||||||
|
- [x] FilterMenu.spec.js: `data-appearance="filled"` statt CSS-Klasse `--filled`
|
||||||
|
- [x] CtaUnblockAuthor.vue: `require` → `required` Typo-Fix
|
||||||
|
- [x] LocationSelect.vue: `clearLocationName()` direkt via `this.currentValue` statt `event.target.value`
|
||||||
|
- [x] LocationSelect.vue: `@click.native` → `@click` (Vue 3 Kompatibilität)
|
||||||
|
- [x] LocationSelect.vue: `aria-label` via `$t('actions.clear')` (i18n)
|
||||||
|
- [x] `actions.clear` in allen 9 Sprachdateien angelegt (en, de, fr, es, it, nl, pl, pt, ru)
|
||||||
|
- [x] OsButton: JSDoc-Dokumentation für Slots (`@slot default`, `@slot icon`)
|
||||||
|
- [x] OsButton: `isSmall` von `['xs', 'sm']` auf `size === 'sm'` vereinfacht (xs existiert nicht)
|
||||||
|
- [x] OsButton: Strikte Typisierung `Record<Size, ...>` statt `Record<string, ...>` für Lookup-Maps
|
||||||
|
- [x] animations.css: Stylelint-konforme Formatierung (eine Deklaration pro Zeile)
|
||||||
|
|
||||||
|
**Zuvor abgeschlossen (Session 15 - Milestone 4c komplett):**
|
||||||
|
- [x] **Alle verbleibenden base-button Instanzen migriert** (132 os-button Tags, 0 base-button verbleibend)
|
||||||
|
- [x] 59 Buttons in dieser Session migriert (Chat, Filter, Modals, Forms, Pages, etc.)
|
||||||
|
- [x] `type="submit"` für alle Form-Buttons (OsButton default ist `type="button"`)
|
||||||
|
- [x] `!!errors` Boolean-Cast für disabled-Props (errors ist Objekt, nicht Boolean)
|
||||||
|
- [x] CSS-Selektoren `.base-button` → `> button` oder `button` angepasst
|
||||||
|
- [x] `!important` für CSS-Positioning (überschreibt Tailwind-Klassen)
|
||||||
|
- [x] Disabled outline border-color Fix (`var(--color-disabled-border,#e5e3e8)`)
|
||||||
|
- [x] ComponentSlider Selection-Dots: dynamic appearance + 18px custom CSS
|
||||||
|
- [x] pages/index.vue FAB: `size="xl"` + position/dimension `!important`
|
||||||
|
- [x] pages/groups FAB: `size="xl"` + box-shadow `!important`
|
||||||
|
- [x] ReportModal Breite auf 700px beibehalten
|
||||||
|
- [x] ContributionForm Submit: `type="submit"` + `!!errors` Fix
|
||||||
|
- [x] my-email-address/index.vue: `!!errors` Fix
|
||||||
|
|
||||||
|
**Zuvor abgeschlossen (Session 14 - Loading Prop, Circle Prop, Code-Optimierung):**
|
||||||
|
- [x] `loading` Prop mit animiertem SVG-Spinner implementiert
|
||||||
|
- [x] Spinner-Architektur: Beide Animationen (rotate + dash) auf `<circle>` Element (Chrome-Compositing-Bug-Workaround)
|
||||||
|
- [x] Spinner zentriert auf Icon (Icon-Buttons) oder Button-Container (Text-Only-Buttons)
|
||||||
|
- [x] Icon bleibt bei loading sichtbar, Spinner überlagert Icon-Bereich
|
||||||
|
- [x] `aria-busy="true"` für Screenreader bei loading
|
||||||
|
- [x] `circle` Prop implementiert (rounded-full, größenabhängige Breiten)
|
||||||
|
- [x] `min-width` pro Größe hinzugefügt (verhindert zu kleine leere Buttons)
|
||||||
|
- [x] Animations-Keyframes in `animations.css` ausgelagert (wiederverwendbar)
|
||||||
|
- [x] Code-Optimierung: OsButton von ~250 auf 207 Zeilen vereinfacht
|
||||||
|
- `buttonData` Objekt für Vue 2/3 geteilt
|
||||||
|
- `SPINNER_PX` vereinfacht (Tuple → einfache Zahlen)
|
||||||
|
- Redundante `cn()` Wrapping entfernt
|
||||||
|
- `getCurrentInstance()` nur bei Vue 2 aufgerufen
|
||||||
|
- [x] 76 Unit-Tests (5 neue: default type, data-appearance, min-w, icon-only loading, circle gap)
|
||||||
|
- [x] Loading-Stories in Storybook (alle Varianten × Appearances)
|
||||||
|
- [x] Visual Tests mit `animationPlayState = 'paused'` für stabile Screenshots
|
||||||
|
- [x] PaginationButtons.vue migriert (2 circle icon-only Buttons)
|
||||||
|
|
||||||
|
**Zuvor abgeschlossen (Session 13 - Icon-Slot, Storybook Playground, Webapp-Migration):**
|
||||||
|
- [x] Icon-Slot für OsButton implementiert (slot-basiert, icon-system-agnostisch)
|
||||||
|
- [x] Render-Funktion: `slots.icon?.()` → `<span class="os-button__icon">` Wrapper
|
||||||
|
- [x] Tailwind-Klassen direkt auf Icon-Wrapper (kein custom CSS in index.css nötig)
|
||||||
|
- [x] VNode-basierte Text-Erkennung: Whitespace-only = icon-only (gap/margin-Logik)
|
||||||
|
- [x] Storybook: 4 neue Stories (Icon, IconOnly, IconSizes, IconAppearances)
|
||||||
|
- [x] Playground: Reaktiver Icon-Selektor (none/check/close/plus) + Label-Text-Control
|
||||||
|
- [x] Visual Tests: 4 neue Tests mit Screenshots + a11y-Checks
|
||||||
|
- [x] Unit Tests: 8 neue Tests (icon slot, keyboard a11y mit aria-label)
|
||||||
|
- [x] Erste Webapp-Migration mit Icon: `my-email-address/index.vue` (Save-Button mit check-Icon)
|
||||||
|
- [x] Code-Optimierung: ICON_CLASS Konstante, iconMargin Variable, vereinfachte hasText-Logik
|
||||||
|
- [x] Größenabhängiger Gap: `gap-1` für xs/sm, `gap-2` für md/lg/xl
|
||||||
|
- [x] Größenabhängiger Icon-Margin: kein negativer Margin bei xs/sm (mehr Abstand zur Button-Grenze)
|
||||||
|
- [x] 6 weitere Buttons mit Icon migriert: DisableModal, DeleteUserModal, CtaUnblockAuthor, LocationSelect, CategoriesSelect, profile Chat
|
||||||
|
- [x] verify.vue hat keinen Button (Eintrag korrigiert)
|
||||||
|
|
||||||
|
**Zuvor abgeschlossen (Session 12 - CSS-Linting, CI-Optimierung, Code-Review Fixes):**
|
||||||
- [x] CSS-Linting: `@eslint/css` + `tailwind-csstree` für Tailwind v4 Syntax-Support
|
- [x] CSS-Linting: `@eslint/css` + `tailwind-csstree` für Tailwind v4 Syntax-Support
|
||||||
- [x] `excludeCSS()` Helper: JS-Regeln von CSS-Dateien fernhalten (language-Inkompatibilität)
|
- [x] `excludeCSS()` Helper: JS-Regeln von CSS-Dateien fernhalten (language-Inkompatibilität)
|
||||||
- [x] CSS-Regeln: `no-empty-blocks`, `no-duplicate-imports`, `no-invalid-at-rules`
|
- [x] CSS-Regeln: `no-empty-blocks`, `no-duplicate-imports`, `no-invalid-at-rules`
|
||||||
@ -261,14 +422,20 @@ OsButton Features:
|
|||||||
1. ~~Phase 0: Komponenten-Analyse~~ ✅
|
1. ~~Phase 0: Komponenten-Analyse~~ ✅
|
||||||
2. ~~Phase 1: Vue 2.7 Upgrade~~ ✅
|
2. ~~Phase 1: Vue 2.7 Upgrade~~ ✅
|
||||||
3. ~~**Phase 2: Projekt-Setup**~~ ✅ ABGESCHLOSSEN
|
3. ~~**Phase 2: Projekt-Setup**~~ ✅ ABGESCHLOSSEN
|
||||||
4. **Phase 3: Webapp-Integration** - 32/90 Buttons migriert (36%)
|
4. ~~**Phase 3: Webapp-Integration**~~ ✅ ABGESCHLOSSEN — 133 Buttons in 79 Dateien
|
||||||
- [x] yarn link / Webpack-Alias in Webapp
|
- [x] yarn link / Webpack-Alias in Webapp
|
||||||
- [x] CSS-Variablen definieren (ocelot-ui-variables.scss)
|
- [x] CSS-Variablen definieren (ocelot-ui-variables.scss)
|
||||||
- [x] 16 Buttons migriert & validiert ✅
|
- [x] 16 Buttons migriert & validiert ✅
|
||||||
- [x] Docker Build + CI-Kompatibilität
|
- [x] Docker Build + CI-Kompatibilität
|
||||||
- [x] **Milestone 4a:** 14 weitere Buttons (ohne neue Props) ✅
|
- [x] **Milestone 4a:** 14 weitere Buttons (ohne neue Props) ✅
|
||||||
- [ ] **Milestone 4b:** icon/circle/loading Props implementieren
|
- [x] **Milestone 4b:** icon/circle/loading Props implementieren ✅
|
||||||
- [ ] **Milestone 4c:** ~60 Buttons mit icon/circle/loading migrieren
|
- [x] **Milestone 4c:** Alle verbleibenden Buttons migriert ✅
|
||||||
|
- [x] **Code-Review Feedback:** Refactoring, A11y, Vue 3 Compat, CSS-Scoping ✅
|
||||||
|
5. **Nächstes:**
|
||||||
|
- [ ] GroupButton + MapButton in HeaderMenu inlinen (keine eigene Komponente nötig)
|
||||||
|
- [ ] `compat/` Verzeichnis in packages/ui anlegen (temporäre Migrations-Wrapper)
|
||||||
|
- [ ] BaseIcon nach `compat/` verschieben (131 Nutzungen, Voraussetzung für weitere Migrationen)
|
||||||
|
- [ ] Snapshots/Tests aktualisieren, BaseButton-Komponente ggf. entfernen
|
||||||
|
|
||||||
**Manuelle Setup-Aufgaben (außerhalb Code):**
|
**Manuelle Setup-Aufgaben (außerhalb Code):**
|
||||||
- [ ] `NPM_TOKEN` als GitHub Secret einrichten (für npm publish in ui-release.yml)
|
- [ ] `NPM_TOKEN` als GitHub Secret einrichten (für npm publish in ui-release.yml)
|
||||||
@ -361,106 +528,120 @@ OsButton Features:
|
|||||||
- [x] OsButton attrs/listeners Forwarding (Vue 2 $listeners via getCurrentInstance)
|
- [x] OsButton attrs/listeners Forwarding (Vue 2 $listeners via getCurrentInstance)
|
||||||
- [x] 14 weitere Buttons migriert (alle ohne icon/circle/loading)
|
- [x] 14 weitere Buttons migriert (alle ohne icon/circle/loading)
|
||||||
|
|
||||||
**Milestone 4a: Weitere Buttons migrieren (14 ohne neue Props)**
|
**Milestone 4a: Weitere Buttons migrieren (14 ohne neue Props)** ✅
|
||||||
- [ ] Modal Cancel-Buttons (DisableModal, DeleteUserModal, ReleaseModal)
|
- [x] Modal Cancel-Buttons (DisableModal, DeleteUserModal, ReleaseModal)
|
||||||
- [ ] Form Cancel/Submit-Buttons (ContributionForm, EnterNonce, MySomethingList)
|
- [x] Form Cancel/Submit-Buttons (ContributionForm, EnterNonce, MySomethingList)
|
||||||
- [ ] ImageUploader.vue (2× Crop-Buttons)
|
- [x] ImageUploader.vue (2× Crop-Buttons)
|
||||||
- [ ] Page-Buttons (donations, badges, notifications/index, profile Unblock/Unmute)
|
- [x] Page-Buttons (donations, badges, notifications/index, profile Unblock/Unmute)
|
||||||
- [ ] ReportRow.vue More-Details-Button
|
- [x] ReportRow.vue More-Details-Button
|
||||||
|
|
||||||
**Milestone 4b: OsButton Props erweitern**
|
**Milestone 4b: OsButton Props erweitern** ✅
|
||||||
- [ ] `icon` Prop implementieren (slot-basiert oder Icon-Komponente)
|
- [x] `icon` Slot implementiert (slot-basiert, icon-system-agnostisch) ✅
|
||||||
- [ ] `circle` Variant zu CVA hinzufügen
|
- [x] `circle` Prop implementiert (rounded-full, größenabhängige Breiten) ✅
|
||||||
- [ ] `loading` Prop mit Spinner implementieren
|
- [x] `loading` Prop mit animiertem SVG-Spinner implementiert ✅
|
||||||
|
|
||||||
**Milestone 4c: Buttons mit icon/circle/loading migrieren (~60 Buttons)**
|
**Milestone 4c: Buttons mit icon/circle/loading migrieren** ✅ ABGESCHLOSSEN
|
||||||
|
|
||||||
*Button-Komponenten (Wrapper):*
|
*Button-Komponenten (Wrapper):*
|
||||||
- [ ] Button/JoinLeaveButton.vue (icon, loading)
|
- [x] Button/JoinLeaveButton.vue (icon, loading) ✅
|
||||||
- [ ] Button/FollowButton.vue (icon, loading)
|
- [x] Button/FollowButton.vue (icon, loading) ✅
|
||||||
- [ ] LoginButton/LoginButton.vue (icon, circle)
|
- [x] LoginButton/LoginButton.vue (icon, circle) ✅
|
||||||
- [ ] InviteButton/InviteButton.vue (icon, circle)
|
- [x] InviteButton/InviteButton.vue (icon, circle) ✅
|
||||||
- [ ] EmotionButton/EmotionButton.vue (circle)
|
- [x] EmotionButton/EmotionButton.vue (circle) ✅
|
||||||
- [ ] CustomButton/CustomButton.vue (2× circle)
|
- [x] CustomButton/CustomButton.vue (2× circle) ✅
|
||||||
- [ ] LabeledButton/LabeledButton.vue (icon, circle)
|
- [x] LabeledButton/LabeledButton.vue (icon, circle) ✅
|
||||||
|
|
||||||
*Navigation & Menus:*
|
*Navigation & Menus:*
|
||||||
- [ ] ContentMenu/ContentMenu.vue (icon, circle)
|
- [x] ContentMenu/ContentMenu.vue (icon, circle) ✅
|
||||||
- [ ] ContentMenu/GroupContentMenu.vue (icon, circle)
|
- [x] ContentMenu/GroupContentMenu.vue (icon, circle) ✅
|
||||||
- [ ] ChatNotificationMenu.vue (circle)
|
- [x] ChatNotificationMenu.vue (circle) ✅
|
||||||
- [ ] NotificationMenu.vue (3× icon, circle)
|
- [x] NotificationMenu.vue (3× icon, circle) ✅
|
||||||
- [ ] HeaderMenu/HeaderMenu.vue (icon, circle)
|
- [x] HeaderMenu/HeaderMenu.vue (icon, circle) ✅
|
||||||
- [ ] Map/MapButton.vue (circle)
|
- [x] Map/MapButton.vue (circle) ✅
|
||||||
|
|
||||||
*Editor:*
|
*Editor:*
|
||||||
- [ ] Editor/MenuBarButton.vue (icon, circle)
|
- [x] Editor/MenuBar.vue (~11× icon, circle) ✅
|
||||||
- [ ] Editor/MenuLegend.vue (~10× icon, circle)
|
- [x] Editor/MenuLegend.vue (2× icon) ✅
|
||||||
|
|
||||||
*Filter & Input:*
|
*Filter & Input:*
|
||||||
- [ ] HashtagsFilter.vue (icon, circle)
|
- [x] HashtagsFilter.vue (icon, circle) ✅
|
||||||
- [ ] CategoriesSelect.vue (icon)
|
- [x] CategoriesSelect.vue (icon) ✅
|
||||||
- [ ] SearchableInput.vue (icon, circle)
|
- [x] SearchableInput.vue (icon, circle) ✅
|
||||||
- [ ] Select/LocationSelect.vue (icon)
|
- [x] Select/LocationSelect.vue (icon) ✅
|
||||||
- [ ] PaginationButtons.vue (2× icon, circle)
|
- [x] PaginationButtons.vue (2× icon, circle) ✅
|
||||||
|
|
||||||
*Chat:*
|
*Chat:*
|
||||||
- [ ] Chat/Chat.vue (2× icon, circle)
|
- [x] Chat/Chat.vue (2× icon, circle) ✅
|
||||||
- [ ] Chat/AddChatRoomByUserSearch.vue (icon, circle)
|
- [x] Chat/AddChatRoomByUserSearch.vue (icon, circle) ✅
|
||||||
|
|
||||||
*Forms & Auth:*
|
*Forms & Auth:*
|
||||||
- [ ] LoginForm/LoginForm.vue (icon, loading)
|
- [x] LoginForm/LoginForm.vue (icon, loading) ✅
|
||||||
- [ ] PasswordReset/Request.vue (loading)
|
- [x] PasswordReset/Request.vue (loading) ✅
|
||||||
- [ ] PasswordReset/ChangePassword.vue (loading)
|
- [x] PasswordReset/ChangePassword.vue (loading) ✅
|
||||||
- [ ] Password/Change.vue (loading)
|
- [x] Password/Change.vue (loading) ✅
|
||||||
- [ ] ContributionForm.vue Submit (icon, loading)
|
- [x] ContributionForm.vue Submit (icon, loading) ✅
|
||||||
- [ ] GroupForm.vue Submit (icon)
|
- [x] CommentForm/CommentForm.vue (loading) ✅
|
||||||
- [ ] CommentForm/CommentForm.vue (loading)
|
|
||||||
|
|
||||||
*Modals:*
|
*Modals:*
|
||||||
- [ ] Modal/ConfirmModal.vue (2× icon, loading)
|
- [x] Modal/ConfirmModal.vue (2× icon, loading) ✅
|
||||||
- [ ] Modal/ReportModal.vue (2× icon, loading)
|
- [x] Modal/ReportModal.vue (2× icon, loading) ✅
|
||||||
- [ ] Modal/DisableModal.vue Confirm (icon)
|
- [x] Modal/DisableModal.vue Confirm (icon) ✅
|
||||||
- [ ] Modal/DeleteUserModal.vue Confirm (icon)
|
- [x] Modal/DeleteUserModal.vue Confirm (icon) ✅
|
||||||
- [ ] Modal/ReleaseModal.vue Confirm (icon)
|
- [x] Modal/ReleaseModal.vue Confirm (icon) ✅
|
||||||
|
|
||||||
*Features:*
|
*Features:*
|
||||||
- [ ] ComponentSlider.vue (2× icon, loading)
|
- [x] ComponentSlider.vue (2× icon) ✅
|
||||||
- [ ] MySomethingList.vue (3× icon, circle, loading)
|
- [x] MySomethingList.vue (3× icon, circle) ✅
|
||||||
- [ ] CreateInvitation.vue (icon, circle)
|
- [x] CreateInvitation.vue (icon, circle) ✅
|
||||||
- [ ] Invitation.vue (2× icon, circle)
|
- [x] Invitation.vue (2× icon, circle) ✅
|
||||||
- [ ] ProfileList.vue (loading)
|
- [x] ProfileList.vue (loading) ✅
|
||||||
- [ ] ReportRow.vue Confirm (icon)
|
- [x] ReportRow.vue Confirm (icon) ✅
|
||||||
- [ ] ImageUploader.vue Delete/Cancel (2× icon, circle)
|
- [x] ImageUploader.vue Delete/Cancel (2× icon, circle) ✅
|
||||||
- [ ] CommentCard.vue Reply (icon, circle)
|
- [x] CommentCard.vue Reply (icon, circle) ✅
|
||||||
- [ ] EmbedComponent.vue Close (icon, circle)
|
- [x] EmbedComponent.vue Close (icon, circle) ✅
|
||||||
- [ ] CtaUnblockAuthor.vue (icon)
|
- [x] CtaUnblockAuthor.vue (icon) ✅
|
||||||
- [ ] data-download.vue (icon, loading)
|
- [x] data-download.vue (icon, loading) ✅
|
||||||
|
- [x] ActionButton.vue (icon, circle) ✅
|
||||||
|
- [x] DeleteData.vue (icon) ✅
|
||||||
|
- [x] GroupButton.vue (icon, circle) ✅
|
||||||
|
|
||||||
|
*Filter-Menüs:*
|
||||||
|
- [x] FilterMenu/FilterMenu.vue (icon) ✅
|
||||||
|
- [x] FilterMenu/HeaderButton.vue (2× icon) ✅
|
||||||
|
- [x] FilterMenu/CategoriesFilter.vue (2× icon) ✅
|
||||||
|
- [x] FilterMenu/OrderByFilter.vue (2×) ✅
|
||||||
|
- [x] FilterMenu/EventsByFilter.vue (2×) ✅
|
||||||
|
- [x] FilterMenu/FollowingFilter.vue (3×) ✅
|
||||||
|
|
||||||
*Pages:*
|
*Pages:*
|
||||||
- [ ] pages/groups/_id/_slug.vue (3× icon, circle, loading)
|
- [x] pages/index.vue (2× icon, circle) ✅
|
||||||
- [ ] pages/admin/users/index.vue (2× icon, circle, loading)
|
- [x] pages/groups/index.vue (icon, circle) ✅
|
||||||
- [ ] pages/settings/index.vue (icon, loading)
|
- [x] pages/groups/_id/_slug.vue (3× icon, circle) ✅
|
||||||
- [ ] pages/settings/blocked-users.vue (icon, circle)
|
- [x] pages/admin/users/index.vue (2× icon, circle) ✅
|
||||||
- [ ] pages/settings/muted-users.vue (icon, circle)
|
- [x] pages/settings/index.vue (icon) ✅
|
||||||
- [ ] pages/settings/my-email-address/*.vue (2× icon)
|
- [x] pages/settings/blocked-users.vue (icon, circle) ✅
|
||||||
- [ ] pages/profile/_id/_slug.vue Chat (icon)
|
- [x] pages/settings/muted-users.vue (icon, circle) ✅
|
||||||
- [ ] pages/post/_id/_slug/index.vue (icon, circle)
|
- [x] pages/settings/data-download.vue (icon) ✅
|
||||||
|
- [x] pages/settings/my-email-address/index.vue (icon) ✅
|
||||||
|
- [x] pages/settings/my-email-address/enter-nonce.vue (icon) ✅
|
||||||
|
- [x] pages/profile/_id/_slug.vue (icon, circle) ✅
|
||||||
|
- [x] pages/post/_id/_slug/index.vue (icon, circle) ✅
|
||||||
|
|
||||||
**Milestone 5: Validierung & Dokumentation** ✅
|
**Milestone 5: Validierung & Dokumentation** ✅
|
||||||
- [x] Keine visuellen Änderungen bestätigt (16/16 Buttons validiert)
|
- [x] Keine visuellen Änderungen bestätigt (16/16 Buttons validiert)
|
||||||
- [x] Keine funktionalen Änderungen bestätigt
|
- [x] Keine funktionalen Änderungen bestätigt
|
||||||
- [x] Disabled-Styles korrigiert (hover/active-Override, Border-Fix)
|
- [x] Disabled-Styles korrigiert (hover/active-Override, Border-Fix)
|
||||||
- [ ] Webapp-Tests bestehen weiterhin (TODO: Regressionstest)
|
- [ ] Webapp-Tests bestehen weiterhin (TODO: Snapshots aktualisieren)
|
||||||
- [ ] Erkenntnisse in KATALOG.md dokumentiert
|
- [ ] Erkenntnisse in KATALOG.md dokumentiert
|
||||||
|
|
||||||
**Einsatzstellen-Übersicht:**
|
**Einsatzstellen-Übersicht:**
|
||||||
|
|
||||||
| Kategorie | Buttons | Status |
|
| Kategorie | Buttons | Status |
|
||||||
|-----------|---------|--------|
|
|-----------|---------|--------|
|
||||||
| ✅ Migriert & Validiert | 24 | Erledigt |
|
| ✅ Migriert (gesamt) | 133 | 79 Dateien |
|
||||||
| ⏳ Ohne neue Props (M4a) | 6 | In Arbeit (8 von 14 erledigt) |
|
| ⬜ `<base-button>` verbleibend | 0 | Nur BaseButton.vue Definition + Test-Dateien |
|
||||||
| ⬜ Mit icon/circle/loading (M4c) | ~60 | Ausstehend |
|
| ⬜ `<ds-button>` verbleibend | 0 | Alle ersetzt |
|
||||||
| **Gesamt** | **~90** | **27% erledigt** |
|
| **Gesamt** | **133** | **100% erledigt** ✅ |
|
||||||
|
|
||||||
**Details siehe KATALOG.md** (vollständige Tracking-Tabellen)
|
**Details siehe KATALOG.md** (vollständige Tracking-Tabellen)
|
||||||
|
|
||||||
@ -1486,6 +1667,87 @@ Bei der Migration werden:
|
|||||||
| 2026-02-10 | **CI-Workflow-Trigger** | 9 UI-Workflows von `on: push` auf `push`+`pull_request` mit Branch-Filter (`master`) und Path-Filter (`packages/ui/**` + Workflow-Datei) umgestellt |
|
| 2026-02-10 | **CI-Workflow-Trigger** | 9 UI-Workflows von `on: push` auf `push`+`pull_request` mit Branch-Filter (`master`) und Path-Filter (`packages/ui/**` + Workflow-Datei) umgestellt |
|
||||||
| 2026-02-10 | **custom-class entfernt** | `custom-class` Prop (entfernt aus OsButton) → `class` Attribut in notifications.vue, MapStylesButtons.vue, EmbedComponent.vue (4 Stellen); Snapshot aktualisiert |
|
| 2026-02-10 | **custom-class entfernt** | `custom-class` Prop (entfernt aus OsButton) → `class` Attribut in notifications.vue, MapStylesButtons.vue, EmbedComponent.vue (4 Stellen); Snapshot aktualisiert |
|
||||||
| 2026-02-10 | **Vue 3 Template-Fix** | `this.$t()` → `$t()` in CommentCard.vue (this im Template in Vue 3 nicht verfügbar) |
|
| 2026-02-10 | **Vue 3 Template-Fix** | `this.$t()` → `$t()` in CommentCard.vue (this im Template in Vue 3 nicht verfügbar) |
|
||||||
|
| 2026-02-11 | **Icon-Slot implementiert** | Benannter `#icon` Slot für OsButton, slot-basiert statt Icon-Prop (icon-system-agnostisch) |
|
||||||
|
| 2026-02-11 | **Icon-Wrapper Klassen** | Tailwind-Utility-Klassen direkt auf `<span>`: `inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current` |
|
||||||
|
| 2026-02-11 | **VNode Text-Erkennung** | `hasText` prüft VNode-Children auf sichtbaren Inhalt; whitespace-only → icon-only Verhalten |
|
||||||
|
| 2026-02-11 | **Gap & Margin Logik** | `gap-2` bei Icon+Text, `-ml-1` bei Icon, `-ml-1 -mr-1` bei Icon-Only (optischer Ausgleich) |
|
||||||
|
| 2026-02-11 | **4 neue Stories** | Icon, IconOnly, IconSizes, IconAppearances mit Inline-SVG Komponenten (CheckIcon, CloseIcon, PlusIcon) |
|
||||||
|
| 2026-02-11 | **Playground erweitert** | Reaktiver Icon-Selektor (none/check/close/plus) + Label-Text-Control via `computed()` |
|
||||||
|
| 2026-02-11 | **Storybook: components Option** | Funktionale Komponenten müssen in `components` registriert werden, nicht in `setup()` return |
|
||||||
|
| 2026-02-11 | **Storybook: CSS nicht in index.css** | Storybook lädt eigene `storybook.css`, nicht `src/styles/index.css` → Utility-Klassen direkt verwenden |
|
||||||
|
| 2026-02-11 | **SVG-Targeting** | `[&>svg]` statt `[&>*]` für Icon-Sizing (BaseIcon rendert `<span><svg>`, Wrapper-Span darf nicht beeinflusst werden) |
|
||||||
|
| 2026-02-11 | **my-email-address migriert** | Save-Button: `<os-button variant="primary">` mit `<template #icon><base-icon name="check" /></template>` |
|
||||||
|
| 2026-02-11 | **Code-Optimierung** | `ICON_CLASS` Konstante extrahiert, `iconMargin` Variable, vereinfachte `hasText`-Logik (kein Symbol.for) |
|
||||||
|
| 2026-02-11 | **Größenabhängiger Gap** | `gap-1` (4px) für xs/sm, `gap-2` (8px) für md/lg/xl bei Icon+Text |
|
||||||
|
| 2026-02-11 | **Größenabhängiger Margin** | Kein negativer Icon-Margin bei xs/sm (voller Padding-Abstand zur Button-Grenze) |
|
||||||
|
| 2026-02-11 | **DisableModal.vue** | Confirm-Button migriert: `danger filled icon="exclamation-circle"` → `variant="danger"` + `#icon` Slot |
|
||||||
|
| 2026-02-11 | **DeleteUserModal.vue** | Confirm-Button migriert: identisches Pattern wie DisableModal |
|
||||||
|
| 2026-02-11 | **CtaUnblockAuthor.vue** | Button migriert: `filled icon="arrow-right"` → `variant="primary"` + `#icon` Slot, OsButton importiert |
|
||||||
|
| 2026-02-11 | **LocationSelect.vue** | Icon-only Close-Button migriert: `ghost size="small" icon="close"` → `variant="primary" appearance="ghost" size="sm"` + aria-label |
|
||||||
|
| 2026-02-11 | **CategoriesSelect.vue** | v-for Buttons migriert: dynamisches `:icon` → `#icon` Slot, `:filled` → `:appearance`, CSS `.base-button` → `button` |
|
||||||
|
| 2026-02-11 | **profile/_id/_slug.vue** | Chat-Button migriert: `icon="chat-bubble"` → `variant="primary" appearance="outline" full-width` + `#icon` Slot |
|
||||||
|
| 2026-02-11 | **verify.vue korrigiert** | Kein Button vorhanden (Eintrag aus Milestone-Liste entfernt) |
|
||||||
|
| 2026-02-11 | **PaginationButtons.vue** | 2 circle icon-only Buttons migriert: `outline primary circle` + `#icon` Slot + aria-label |
|
||||||
|
| 2026-02-11 | **OsButton: circle Prop** | `circle` Prop: `rounded-full p-0` + größenabhängige Breiten (CIRCLE_WIDTHS Map) |
|
||||||
|
| 2026-02-11 | **OsButton: loading Prop** | Animierter SVG-Spinner mit `aria-busy="true"`, Button auto-disabled bei loading |
|
||||||
|
| 2026-02-11 | **Spinner-Architektur** | Beide Animationen (rotate + dash) auf `<circle>` Element; SVG ist statischer Container; Chrome-Compositing-Bug-Workaround |
|
||||||
|
| 2026-02-11 | **Spinner-Zentrierung** | Icon-Buttons: Spinner über Icon (translate-basiert, overflow:visible); Text-Buttons: Spinner im Button-Container (inset-0 m-auto) |
|
||||||
|
| 2026-02-11 | **animations.css** | Keyframes `os-spinner-dash` + `os-spinner-rotate` in separate CSS-Datei ausgelagert |
|
||||||
|
| 2026-02-11 | **min-width pro Größe** | `min-w-[26px]`/`min-w-[36px]`/`min-w-12`/`min-w-14` in button.variants.ts (verhindert zu kleine leere Buttons) |
|
||||||
|
| 2026-02-11 | **Code-Optimierung** | OsButton ~250→207 Zeilen: buttonData geteilt, SPINNER_PX vereinfacht, redundante cn() entfernt, getCurrentInstance nur Vue 2 |
|
||||||
|
| 2026-02-11 | **5 neue Unit-Tests** | default type, data-appearance, min-w, icon-only loading, circle gap-1; gesamt: 76 Tests |
|
||||||
|
| 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 | **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 | **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'`) |
|
||||||
|
| 2026-02-11 | **Coverage 100%** | `v8 ignore start/stop` für Vue 2 Branch, `v8 ignore next` für defensive `||` Fallback |
|
||||||
|
| 2026-02-11 | **Scope: 133 Buttons** | 133 `<os-button>` Tags in 79 Dateien, 0 `<base-button>` + 0 `<ds-button>` verbleibend |
|
||||||
|
| 2026-02-12 | **data-variant Attribut** | OsButton rendert `data-variant` auf `<button>` (konsistent mit `data-appearance`), ermöglicht CSS-Selektoren wie `button[data-variant="danger"]` |
|
||||||
|
| 2026-02-12 | **notifications.spec.js** | Test-API korrigiert: `wrapper.find()` (Vue Test Utils) → `screen.getByText()` (Testing Library), `button.disabled` statt `button.attributes('disabled')` |
|
||||||
|
| 2026-02-12 | **FilterMenu Regressionsbug** | `appearance="ghost"` war hardcoded statt dynamisch; `filterActive` Computed Property existierte aber war nicht genutzt → `:appearance="filterActive ? 'filled' : 'ghost'"` |
|
||||||
|
| 2026-02-12 | **FilterMenu.spec.js** | Test von CSS-Klasse `--filled` auf `data-appearance="filled"` Attribut-Selektor umgestellt |
|
||||||
|
| 2026-02-12 | **CtaUnblockAuthor.vue** | Typo `require: true` → `required: true` (Vue ignorierte die Prop-Validierung) |
|
||||||
|
| 2026-02-12 | **LocationSelect.vue Fixes** | `event.target.value` → `this.currentValue` (Button hat kein value), `@click.native` → `@click` (Vue 3), `aria-label` via i18n |
|
||||||
|
| 2026-02-12 | **i18n: actions.clear** | Neuer Key in allen 9 Sprachdateien: en=Clear, de=Zurücksetzen, fr=Effacer, es=Borrar, it=Cancella, nl=Wissen, pl=Wyczyść, pt=Limpar, ru=Очистить |
|
||||||
|
| 2026-02-12 | **OsButton JSDoc** | Slot-Dokumentation (`@slot default`, `@slot icon`) für vue-component-meta/Storybook autodocs |
|
||||||
|
| 2026-02-12 | **OsButton xs entfernt** | `isSmall` von `['xs', 'sm'].includes(size)` auf `size === 'sm'` vereinfacht (xs ist kein gültiger Size-Wert) |
|
||||||
|
| 2026-02-12 | **Strikte Typisierung** | `type Size = NonNullable<ButtonVariants['size']>`, `Record<Size, ...>` für CIRCLE_WIDTHS + SPINNER_PX; `props.size!` → `(props.size ?? 'md') as Size` |
|
||||||
|
| 2026-02-12 | **animations.css** | Stylelint-konforme Formatierung: eine Deklaration pro Zeile, Leerzeilen zwischen Keyframe-Stufen |
|
||||||
|
| 2026-02-12 | **OsButton Refactoring** | `vueAttrs()` Helper für Vue 2/3 Attribut-Handling, Einmal-Variablen durch `cn()` ersetzt, `children` inline; 77 Tests, 100% Coverage |
|
||||||
|
| 2026-02-12 | **CSS @import Reihenfolge** | `@import "./animations.css"` vor `@source`-Direktiven verschoben (CSS-Spec: @import vor anderen At-Rules) |
|
||||||
|
| 2026-02-12 | **CustomButton Cleanup** | `isEmpty` aus `data()` entfernt — reine Utility-Funktion braucht keine Vue-Reaktivität |
|
||||||
|
| 2026-02-12 | **notifications.spec.js** | Doppelten `beforeEach` konsolidiert; `wrapper` von Modulebene in `describe`-Block verschoben |
|
||||||
|
| 2026-02-12 | **Style-Scoping** | MenuLegend.vue: `<style scoped>` hinzugefügt; ReportModal + DeleteUserModal: CSS-Selektoren mit Komponenten-Prefix |
|
||||||
|
| 2026-02-12 | **data-test Selektoren** | LocationSelect (`clear-location-button`) + HashtagsFilter (`clear-search-button`): spezifischere Test-Selektoren |
|
||||||
|
| 2026-02-12 | **Vue 3 Compat Fixes** | FollowButton: `.native` entfernt; FilterMenu: `slot`/`slot-scope` → `<template #default>`; HashtagsFilter: `this.$t()` → `$t()` |
|
||||||
|
| 2026-02-12 | **A11y: aria-label** | GroupContentMenu icon-only Button: `$t('group.contentMenu.menuButton')`; PaginationButtons: `$t('pagination.previous/next')` |
|
||||||
|
| 2026-02-12 | **i18n Keys** | `pagination.previous/next` + `group.contentMenu.menuButton` in allen 9 Sprachdateien angelegt |
|
||||||
|
| 2026-02-12 | **Modal Konsistenz** | DisableModal + DeleteUserModal: `appearance="filled"` + `:loading="loading"` auf Danger-Buttons |
|
||||||
|
| 2026-02-12 | **Loading State** | my-email-address/index.vue: `loadingData` hinzugefügt + `finally` Block für Reset |
|
||||||
|
| 2026-02-12 | **MapButton #icon Slot** | Icon von Default-Slot in `<template #icon>` verschoben (konsistent mit allen anderen Buttons) |
|
||||||
|
| 2026-02-12 | **Dead Code entfernt** | MySomethingList.vue: `.icon-button` CSS-Klasse (nach Migration nicht mehr verwendet) |
|
||||||
|
| 2026-02-12 | **Button-Wrapper-Analyse** | 15 OsButton-Wrapper klassifiziert: 4 Smart (Apollo/Vuex), 4 Presentational, 7 Borderline; GroupButton + MapButton als Inline-Kandidaten identifiziert |
|
||||||
|
| 2026-02-12 | **compat/ Konzept** | Separates Verzeichnis für temporäre Migrations-Wrapper (nicht von check-completeness.ts erfasst); BaseIcon als erster Kandidat (131 Nutzungen) |
|
||||||
|
| 2026-02-13 | **data-test Selektoren** | ~10 Komponenten: `data-test` Attribute für robuste Test-Selektoren (unmute-btn, unblock-btn, follow-btn, join-leave-btn, login-btn, load-all-connections-btn, content-menu-button) |
|
||||||
|
| 2026-02-13 | **Cypress Selektoren** | 4 Step-Definitions: `.user-content-menu button` / `.content-menu button` → `[data-test="content-menu-button"]` |
|
||||||
|
| 2026-02-13 | **Spec-Selektoren** | FollowList, FollowButton, LoginButton, CtaJoinLeaveGroup, ReportRow, muted-users, blocked-users: generische `button` → `[data-test="..."]` |
|
||||||
|
| 2026-02-13 | **A11y: aria-label** | ~15 icon-only Buttons: aria-label hinzugefügt (admin/users, AddChatRoom, EmbedComponent, groups, profile, CustomButton, HeaderMenu, ImageUploader, ContentMenu, HeaderButton, InviteButton, LoginButton, blocked/muted-users) |
|
||||||
|
| 2026-02-13 | **Zustandsabhängiges aria-label** | post/_id/_slug: `$t(blurred ? 'post.sensitiveContent.show' : 'post.sensitiveContent.hide')` |
|
||||||
|
| 2026-02-13 | **ComponentSlider aria-label** | Interpoliertes Label: `$t('component-slider.step', { current: index + 1, total: ... })` |
|
||||||
|
| 2026-02-13 | **i18n Keys (6 neue)** | `actions.search`, `actions.close`, `actions.menu`, `site.navigation`, `post.sensitiveContent.show/hide`, `component-slider.step` in allen 9 Sprachdateien |
|
||||||
|
| 2026-02-13 | **Loading-State Fixes** | DisableModal + ReleaseModal: `finally { this.loading = false }` für Reset |
|
||||||
|
| 2026-02-13 | **Bugfixes** | ChangePassword: `!!errors`; Password/Change: `disabled` aus data() entfernt + 2 tote Tests; MenuBar: unbenutztes `ref` entfernt |
|
||||||
|
| 2026-02-13 | **Button-Props** | GroupForm cancel: `variant="default" appearance="filled"`; donations/LoginForm/EnterNonce: `appearance="filled"` ergänzt |
|
||||||
|
| 2026-02-13 | **CSS-Selektoren** | LoginForm: `.login-form button` → `.login-form button[type='submit']`; pages/index: redundante Klasse auf BaseIcon entfernt |
|
||||||
|
| 2026-02-13 | **JoinLeaveButton** | `.native` von `@mouseenter`/`@mouseleave` entfernt (Vue 3 Kompatibilität) |
|
||||||
|
| 2026-02-13 | **MySomethingList** | `:title` + `:aria-label` auf Edit/Delete-Buttons (Tooltip beibehalten neben Accessibility) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,19 @@ describe('osButton', () => {
|
|||||||
})
|
})
|
||||||
expect(wrapper.classes()).toContain('h-12')
|
expect(wrapper.classes()).toContain('h-12')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('has min-width matching height for each size', () => {
|
||||||
|
const sizes = {
|
||||||
|
sm: 'min-w-[26px]',
|
||||||
|
md: 'min-w-[36px]',
|
||||||
|
lg: 'min-w-12',
|
||||||
|
xl: 'min-w-14',
|
||||||
|
} as const
|
||||||
|
for (const [size, expected] of Object.entries(sizes)) {
|
||||||
|
const wrapper = mount(OsButton, { props: { size: size as keyof typeof sizes } })
|
||||||
|
expect(wrapper.classes()).toContain(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies fullWidth class', () => {
|
it('applies fullWidth class', () => {
|
||||||
@ -101,6 +114,11 @@ describe('osButton', () => {
|
|||||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('defaults to type="button"', () => {
|
||||||
|
const wrapper = mount(OsButton)
|
||||||
|
expect(wrapper.attributes('type')).toBe('button')
|
||||||
|
})
|
||||||
|
|
||||||
it('sets button type', () => {
|
it('sets button type', () => {
|
||||||
const wrapper = mount(OsButton, {
|
const wrapper = mount(OsButton, {
|
||||||
props: { type: 'submit' },
|
props: { type: 'submit' },
|
||||||
@ -108,6 +126,20 @@ describe('osButton', () => {
|
|||||||
expect(wrapper.attributes('type')).toBe('submit')
|
expect(wrapper.attributes('type')).toBe('submit')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sets data-variant attribute', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { variant: 'danger' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('data-variant')).toBe('danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets data-appearance attribute', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { appearance: 'outline' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('data-appearance')).toBe('outline')
|
||||||
|
})
|
||||||
|
|
||||||
it('emits click event', async () => {
|
it('emits click event', async () => {
|
||||||
const wrapper = mount(OsButton)
|
const wrapper = mount(OsButton)
|
||||||
await wrapper.trigger('click')
|
await wrapper.trigger('click')
|
||||||
@ -131,6 +163,337 @@ describe('osButton', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('icon slot', () => {
|
||||||
|
it('renders icon slot content in .os-button__icon wrapper', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { icon: '<svg data-testid="icon"></svg>' },
|
||||||
|
})
|
||||||
|
const iconWrapper = wrapper.find('.os-button__icon')
|
||||||
|
expect(iconWrapper.exists()).toBeTruthy()
|
||||||
|
expect(iconWrapper.find('[data-testid="icon"]').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders both icon and text', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: {
|
||||||
|
icon: '<svg data-testid="icon"></svg>',
|
||||||
|
default: 'Save',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.os-button__icon').exists()).toBeTruthy()
|
||||||
|
expect(wrapper.text()).toContain('Save')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds gap-2 class when icon and text are present', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: {
|
||||||
|
icon: '<svg></svg>',
|
||||||
|
default: 'Save',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('button > span')
|
||||||
|
expect(contentSpan.classes()).toContain('gap-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds gap-1 class for small sizes with icon and text', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { size: 'sm' },
|
||||||
|
slots: {
|
||||||
|
icon: '<svg></svg>',
|
||||||
|
default: 'Save',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('button > span')
|
||||||
|
expect(contentSpan.classes()).toContain('gap-1')
|
||||||
|
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not add gap-2 for icon-only button', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('button > span')
|
||||||
|
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats whitespace-only text as icon-only', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: {
|
||||||
|
icon: '<svg></svg>',
|
||||||
|
default: ' ',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('button > span')
|
||||||
|
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||||
|
expect(wrapper.find('.os-button__icon').classes()).toContain('-mr-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not add gap-2 without icon', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { default: 'Click me' },
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('button > span')
|
||||||
|
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without icon slot (backward compat)', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { default: 'Click me' },
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.os-button__icon').exists()).toBeFalsy()
|
||||||
|
expect(wrapper.text()).toBe('Click me')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders icon-only button without text', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.os-button__icon').exists()).toBeTruthy()
|
||||||
|
expect(wrapper.text()).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('circle prop', () => {
|
||||||
|
it('renders as round button with rounded-full and p-0', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('rounded-full')
|
||||||
|
expect(wrapper.classes()).toContain('p-0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies w-[36px] width for md size (default)', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('w-[36px]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies w-[26px] width for sm size', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true, size: 'sm' },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('w-[26px]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies w-12 width for lg size', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true, size: 'lg' },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('w-12')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies w-14 width for xl size', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true, size: 'xl' },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('w-14')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is combinable with primary variant', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true, variant: 'primary' },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('rounded-full')
|
||||||
|
expect(wrapper.classes()).toContain('bg-[var(--color-primary)]')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is combinable with ghost appearance', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true, appearance: 'ghost', variant: 'primary' },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('rounded-full')
|
||||||
|
expect(wrapper.classes()).toContain('bg-transparent')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('icon has no negative margin in circle mode', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
const iconWrapper = wrapper.find('.os-button__icon')
|
||||||
|
expect(iconWrapper.classes()).not.toContain('-ml-1')
|
||||||
|
expect(iconWrapper.classes()).not.toContain('-mr-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses gap-1 for circle with icon and text', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: true },
|
||||||
|
slots: {
|
||||||
|
icon: '<svg></svg>',
|
||||||
|
default: 'Add',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('button > span')
|
||||||
|
expect(contentSpan.classes()).toContain('gap-1')
|
||||||
|
expect(contentSpan.classes()).not.toContain('gap-2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not apply circle classes when circle is false', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { circle: false },
|
||||||
|
slots: { default: 'Click me' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).not.toContain('rounded-full')
|
||||||
|
expect(wrapper.classes()).not.toContain('p-0')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('loading prop', () => {
|
||||||
|
it('renders spinner SVG when loading=true', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||||
|
expect(wrapper.find('svg').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables button when loading=true', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets aria-busy="true" when loading', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('aria-busy')).toBe('true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps content visible when loading', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
const contentSpan = wrapper.find('span')
|
||||||
|
expect(contentSpan.classes()).not.toContain('opacity-0')
|
||||||
|
expect(wrapper.text()).toContain('Save')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render spinner when loading=false (default)', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.os-button__spinner').exists()).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not set aria-busy when not loading', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('aria-busy')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loading + disabled: button remains disabled', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true, disabled: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not emit click when loading', async () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
await wrapper.trigger('click')
|
||||||
|
expect(wrapper.emitted('click')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders spinner inside icon wrapper when icon is present', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: {
|
||||||
|
icon: '<svg data-testid="icon"></svg>',
|
||||||
|
default: 'Save',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const iconWrapper = wrapper.find('.os-button__icon')
|
||||||
|
expect(iconWrapper.exists()).toBeTruthy()
|
||||||
|
expect(iconWrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps icon visible when loading with icon', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: {
|
||||||
|
icon: '<svg data-testid="icon"></svg>',
|
||||||
|
default: 'Save',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const iconWrapper = wrapper.find('.os-button__icon')
|
||||||
|
expect(iconWrapper.classes()).not.toContain('[&>*]:invisible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders spinner as direct button child when no icon', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { default: 'Save' },
|
||||||
|
})
|
||||||
|
// Spinner is a direct child of button, not inside content wrapper
|
||||||
|
const spinner = wrapper.find('button > .os-button__spinner')
|
||||||
|
expect(spinner.exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render button-level spinner when icon is present', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: {
|
||||||
|
icon: '<svg></svg>',
|
||||||
|
default: 'Save',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// No spinner as direct child of button — it's inside the icon wrapper
|
||||||
|
const buttonSpinner = wrapper.find('button > .os-button__spinner')
|
||||||
|
expect(buttonSpinner.exists()).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps icon visible and shows spinner for icon-only loading', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true },
|
||||||
|
slots: { icon: '<svg data-testid="icon"></svg>' },
|
||||||
|
})
|
||||||
|
const iconWrapper = wrapper.find('.os-button__icon')
|
||||||
|
expect(iconWrapper.exists()).toBeTruthy()
|
||||||
|
expect(iconWrapper.find('[data-testid="icon"]').exists()).toBeTruthy()
|
||||||
|
expect(iconWrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||||
|
expect(iconWrapper.classes()).not.toContain('[&>*]:invisible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with circle prop', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
props: { loading: true, circle: true },
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Add' },
|
||||||
|
})
|
||||||
|
expect(wrapper.classes()).toContain('rounded-full')
|
||||||
|
expect(wrapper.find('.os-button__spinner').exists()).toBeTruthy()
|
||||||
|
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||||
|
expect(wrapper.attributes('aria-busy')).toBe('true')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('keyboard accessibility', () => {
|
describe('keyboard accessibility', () => {
|
||||||
it('renders as native button element for keyboard support', () => {
|
it('renders as native button element for keyboard support', () => {
|
||||||
const wrapper = mount(OsButton)
|
const wrapper = mount(OsButton)
|
||||||
@ -152,6 +515,15 @@ describe('osButton', () => {
|
|||||||
expect(wrapper.attributes('disabled')).toBeDefined()
|
expect(wrapper.attributes('disabled')).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('icon-only button is focusable with aria-label', () => {
|
||||||
|
const wrapper = mount(OsButton, {
|
||||||
|
slots: { icon: '<svg></svg>' },
|
||||||
|
attrs: { 'aria-label': 'Close' },
|
||||||
|
})
|
||||||
|
expect(wrapper.attributes('aria-label')).toBe('Close')
|
||||||
|
expect(wrapper.attributes('tabindex')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('can receive focus programmatically', () => {
|
it('can receive focus programmatically', () => {
|
||||||
const wrapper = mount(OsButton, { attachTo: document.body })
|
const wrapper = mount(OsButton, { attachTo: document.body })
|
||||||
const button = wrapper.element as HTMLButtonElement
|
const button = wrapper.element as HTMLButtonElement
|
||||||
|
|||||||
@ -1,7 +1,36 @@
|
|||||||
|
import { computed, h } from 'vue'
|
||||||
|
|
||||||
import OsButton from './OsButton.vue'
|
import OsButton from './OsButton.vue'
|
||||||
|
|
||||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline SVG icons for demo purposes (from Heroicons).
|
||||||
|
* In real usage, the webapp passes its own BaseIcon component.
|
||||||
|
*/
|
||||||
|
const CheckIcon = () =>
|
||||||
|
h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor' }, [
|
||||||
|
h('path', {
|
||||||
|
'fill-rule': 'evenodd',
|
||||||
|
d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z',
|
||||||
|
'clip-rule': 'evenodd',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const CloseIcon = () =>
|
||||||
|
h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor' }, [
|
||||||
|
h('path', {
|
||||||
|
d: 'M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const PlusIcon = () =>
|
||||||
|
h('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor' }, [
|
||||||
|
h('path', {
|
||||||
|
d: 'M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
const meta: Meta<typeof OsButton> = {
|
const meta: Meta<typeof OsButton> = {
|
||||||
title: 'Components/OsButton',
|
title: 'Components/OsButton',
|
||||||
component: OsButton,
|
component: OsButton,
|
||||||
@ -11,7 +40,27 @@ const meta: Meta<typeof OsButton> = {
|
|||||||
export default meta
|
export default meta
|
||||||
type Story = StoryObj<typeof OsButton>
|
type Story = StoryObj<typeof OsButton>
|
||||||
|
|
||||||
export const Playground: Story = {
|
/** Custom args for Playground (icon selector + label are not real component props) */
|
||||||
|
interface PlaygroundArgs {
|
||||||
|
variant: string
|
||||||
|
appearance: string
|
||||||
|
size: string
|
||||||
|
fullWidth: boolean
|
||||||
|
circle: boolean
|
||||||
|
disabled: boolean
|
||||||
|
loading: boolean
|
||||||
|
icon: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMap: Record<string, (() => ReturnType<typeof h>) | null> = {
|
||||||
|
none: null,
|
||||||
|
check: CheckIcon,
|
||||||
|
close: CloseIcon,
|
||||||
|
plus: PlusIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Playground: StoryObj<PlaygroundArgs> = {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
variant: {
|
variant: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
@ -28,23 +77,51 @@ export const Playground: Story = {
|
|||||||
fullWidth: {
|
fullWidth: {
|
||||||
control: 'boolean',
|
control: 'boolean',
|
||||||
},
|
},
|
||||||
|
circle: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
control: 'boolean',
|
control: 'boolean',
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
control: 'select',
|
||||||
|
options: Object.keys(iconMap),
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
appearance: 'filled',
|
appearance: 'filled',
|
||||||
size: 'md',
|
size: 'md',
|
||||||
fullWidth: false,
|
fullWidth: false,
|
||||||
|
circle: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
icon: 'none',
|
||||||
|
label: 'Button',
|
||||||
},
|
},
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
components: { OsButton },
|
components: { OsButton },
|
||||||
setup() {
|
setup() {
|
||||||
return { args }
|
const buttonProps = computed(() => {
|
||||||
|
const { icon: _icon, label: _label, ...rest } = args
|
||||||
|
return rest
|
||||||
|
})
|
||||||
|
const IconComponent = computed(() => iconMap[args.icon] ?? null)
|
||||||
|
const label = computed(() => args.label)
|
||||||
|
return { buttonProps, IconComponent, label }
|
||||||
},
|
},
|
||||||
template: '<OsButton v-bind="args">Button</OsButton>',
|
template: `
|
||||||
|
<OsButton v-bind="buttonProps">
|
||||||
|
<template v-if="IconComponent" #icon><component :is="IconComponent" /></template>
|
||||||
|
{{ label }}
|
||||||
|
</OsButton>
|
||||||
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,3 +362,344 @@ export const FullWidth: Story = {
|
|||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Icon: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, CheckIcon, PlusIcon, CloseIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Save
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="success">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Confirm
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="default">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
Add
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="danger">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
Delete
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="info">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
Create
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconOnly: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, CloseIcon, PlusIcon, CheckIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton variant="danger" aria-label="Close">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="success" aria-label="Confirm">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="default" aria-label="Close" appearance="outline">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton variant="primary" aria-label="Add" appearance="ghost">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconSizes: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, CheckIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton size="sm" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Small
|
||||||
|
</OsButton>
|
||||||
|
<OsButton size="md" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Medium
|
||||||
|
</OsButton>
|
||||||
|
<OsButton size="lg" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Large
|
||||||
|
</OsButton>
|
||||||
|
<OsButton size="xl" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Extra Large
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconAppearances: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, CheckIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Filled</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton appearance="filled" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Primary
|
||||||
|
</OsButton>
|
||||||
|
<OsButton appearance="filled" variant="danger">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Danger
|
||||||
|
</OsButton>
|
||||||
|
<OsButton appearance="filled" variant="success">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Success
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton appearance="outline" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Primary
|
||||||
|
</OsButton>
|
||||||
|
<OsButton appearance="outline" variant="danger">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Danger
|
||||||
|
</OsButton>
|
||||||
|
<OsButton appearance="outline" variant="success">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Success
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Ghost</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton appearance="ghost" variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Primary
|
||||||
|
</OsButton>
|
||||||
|
<OsButton appearance="ghost" variant="danger">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Danger
|
||||||
|
</OsButton>
|
||||||
|
<OsButton appearance="ghost" variant="success">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Success
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Circle: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, PlusIcon, CloseIcon, CheckIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle variant="secondary" aria-label="Confirm">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle variant="danger" aria-label="Close">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle variant="warning" aria-label="Close">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle variant="success" aria-label="Confirm">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle variant="info" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CircleSizes: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, PlusIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Small (26px)</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle size="sm" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="sm" variant="danger" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="sm" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Medium (36px)</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle size="md" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="md" variant="danger" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="md" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Large (48px)</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle size="lg" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="lg" variant="danger" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="lg" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Extra Large (56px)</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle size="xl" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="xl" variant="danger" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle size="xl" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CircleAppearances: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, PlusIcon, CloseIcon, CheckIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Filled</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle appearance="filled" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="filled" variant="danger" aria-label="Close">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="filled" variant="success" aria-label="Confirm">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="filled" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle appearance="outline" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="outline" variant="danger" aria-label="Close">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="outline" variant="success" aria-label="Confirm">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="outline" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Ghost</h3>
|
||||||
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<OsButton circle appearance="ghost" variant="primary" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="ghost" variant="danger" aria-label="Close">
|
||||||
|
<template #icon><CloseIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="ghost" variant="success" aria-label="Confirm">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
<OsButton circle appearance="ghost" variant="default" aria-label="Add">
|
||||||
|
<template #icon><PlusIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { OsButton, CheckIcon },
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Filled</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton loading appearance="filled" variant="default">Default</OsButton>
|
||||||
|
<OsButton loading appearance="filled" variant="primary">Primary</OsButton>
|
||||||
|
<OsButton loading appearance="filled" variant="danger">Danger</OsButton>
|
||||||
|
<OsButton loading appearance="filled" variant="success">Success</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">Outline</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton loading appearance="outline" variant="default">Default</OsButton>
|
||||||
|
<OsButton loading appearance="outline" variant="primary">Primary</OsButton>
|
||||||
|
<OsButton loading appearance="outline" variant="danger">Danger</OsButton>
|
||||||
|
<OsButton loading appearance="outline" variant="success">Success</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold mb-2">With Icon</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<OsButton loading variant="primary">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
Save
|
||||||
|
</OsButton>
|
||||||
|
<OsButton loading variant="danger">Delete</OsButton>
|
||||||
|
<OsButton loading circle variant="primary" aria-label="Loading">
|
||||||
|
<template #icon><CheckIcon /></template>
|
||||||
|
</OsButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|||||||
@ -122,4 +122,85 @@ test.describe('OsButton visual regression', () => {
|
|||||||
await expect(root.locator('.flex-col').first()).toHaveScreenshot('full-width.png')
|
await expect(root.locator('.flex-col').first()).toHaveScreenshot('full-width.png')
|
||||||
await checkA11y(page)
|
await checkA11y(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('icon', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--icon&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex')).toHaveScreenshot('icon.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('icon only', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--icon-only&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex')).toHaveScreenshot('icon-only.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('icon sizes', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--icon-sizes&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex')).toHaveScreenshot('icon-sizes.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('icon appearances', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--icon-appearances&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex-col').first()).toHaveScreenshot('icon-appearances.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('circle', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--circle&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex')).toHaveScreenshot('circle.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('circle sizes', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--circle-sizes&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex-col').first()).toHaveScreenshot('circle-sizes.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('circle appearances', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--circle-appearances&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
await expect(root.locator('.flex-col').first()).toHaveScreenshot('circle-appearances.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('loading', async ({ page }) => {
|
||||||
|
await page.goto(`${STORY_URL}--loading&viewMode=story`)
|
||||||
|
const root = page.locator(STORY_ROOT)
|
||||||
|
await root.waitFor()
|
||||||
|
await waitForFonts(page)
|
||||||
|
// Pause animations for deterministic screenshots
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.querySelectorAll('.os-button__spinner').forEach((el) => {
|
||||||
|
;(el as HTMLElement).style.animationPlayState = 'paused'
|
||||||
|
})
|
||||||
|
document.querySelectorAll('.os-button__spinner circle').forEach((el) => {
|
||||||
|
;(el as HTMLElement).style.animationPlayState = 'paused'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await expect(root.locator('.flex-col').first()).toHaveScreenshot('loading.png')
|
||||||
|
await checkA11y(page)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -8,9 +8,64 @@
|
|||||||
import type { ButtonVariants } from './button.variants'
|
import type { ButtonVariants } from './button.variants'
|
||||||
import type { PropType } from 'vue-demi'
|
import type { PropType } from 'vue-demi'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flexible button component with optional icon slot.
|
||||||
|
* @slot default - Button content (text or HTML)
|
||||||
|
* @slot icon - Optional icon (rendered left of text). Use aria-label for icon-only buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Size = NonNullable<ButtonVariants['size']>
|
||||||
|
|
||||||
|
const CIRCLE_WIDTHS: Record<Size, string> = {
|
||||||
|
sm: 'w-[26px]',
|
||||||
|
md: 'w-[36px]',
|
||||||
|
lg: 'w-12',
|
||||||
|
xl: 'w-14',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICON_CLASS =
|
||||||
|
'os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current'
|
||||||
|
|
||||||
|
const SPINNER_PX: Record<Size, number> = { sm: 24, md: 32, lg: 40, xl: 46 }
|
||||||
|
|
||||||
|
const SVG_ATTRS = {
|
||||||
|
viewBox: '0 0 50 50',
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
'aria-hidden': 'true',
|
||||||
|
}
|
||||||
|
|
||||||
|
const CIRCLE_ATTRS = {
|
||||||
|
cx: '25',
|
||||||
|
cy: '25',
|
||||||
|
r: '20',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': '4',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
}
|
||||||
|
|
||||||
|
const CIRCLE_STYLE =
|
||||||
|
'transform-origin:25px 25px;animation:os-spinner-rotate 16s linear infinite,os-spinner-dash 1.5s ease-in-out infinite'
|
||||||
|
|
||||||
|
function vueAttrs(attrs: Record<string, string>, style?: string) {
|
||||||
|
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
||||||
|
return isVue2 ? { attrs, ...(style && { style }) } : { ...attrs, ...(style && { style }) }
|
||||||
|
/* v8 ignore stop */
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSpinner(px: number, center: string) {
|
||||||
|
const svg = h('svg', vueAttrs(SVG_ATTRS, 'width:100%;height:100%;overflow:hidden'), [
|
||||||
|
h('circle', vueAttrs(CIRCLE_ATTRS, CIRCLE_STYLE)),
|
||||||
|
])
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ class: `os-button__spinner absolute ${center}`, style: `width:${px}px;height:${px}px` },
|
||||||
|
[svg],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'OsButton',
|
name: 'OsButton',
|
||||||
// In Vue 2, inheritAttrs must be false to manually forward attrs
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
variant: {
|
variant: {
|
||||||
@ -37,61 +92,131 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
circle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, { slots, attrs }) {
|
setup(props, { slots, attrs }) {
|
||||||
const classes = computed(() =>
|
const variantClasses = computed(() =>
|
||||||
cn(
|
buttonVariants({
|
||||||
buttonVariants({
|
variant: props.variant,
|
||||||
variant: props.variant,
|
appearance: props.appearance,
|
||||||
appearance: props.appearance,
|
size: props.size,
|
||||||
size: props.size,
|
fullWidth: props.fullWidth,
|
||||||
fullWidth: props.fullWidth,
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get component instance for Vue 2 $listeners access
|
/* v8 ignore start -- Vue 2 only */
|
||||||
const instance = getCurrentInstance()
|
const instance = isVue2 ? getCurrentInstance() : null
|
||||||
|
/* v8 ignore stop */
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const children = slots.default?.()
|
const iconContent = slots.icon?.()
|
||||||
|
const defaultContent = slots.default?.()
|
||||||
|
const hasIcon = iconContent && iconContent.length > 0
|
||||||
|
const hasText =
|
||||||
|
defaultContent?.some((node: unknown) => {
|
||||||
|
const children = (node as Record<string, unknown>).children
|
||||||
|
return typeof children !== 'string' || children.trim().length > 0
|
||||||
|
}) ?? false
|
||||||
|
|
||||||
|
const size = props.size as Size
|
||||||
|
const spinnerPx = SPINNER_PX[size] // eslint-disable-line security/detect-object-injection
|
||||||
|
const isSmall = props.circle || size === 'sm'
|
||||||
|
const isLoading = props.loading
|
||||||
|
const isDisabled = props.disabled || isLoading
|
||||||
|
|
||||||
|
// --- Build inner children (icon + text) ---
|
||||||
|
const innerChildren: ReturnType<typeof h>[] = []
|
||||||
|
|
||||||
|
if (hasIcon) {
|
||||||
|
const iconChildren = isLoading
|
||||||
|
? [
|
||||||
|
/* v8 ignore next -- iconContent guaranteed truthy by hasIcon check */
|
||||||
|
...(iconContent || []),
|
||||||
|
createSpinner(spinnerPx, 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'),
|
||||||
|
]
|
||||||
|
: iconContent
|
||||||
|
innerChildren.push(
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
class: cn(
|
||||||
|
ICON_CLASS,
|
||||||
|
!isSmall && (hasText ? '-ml-1' : '-ml-1 -mr-1'),
|
||||||
|
isLoading && 'relative overflow-visible',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
iconChildren,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultContent && hasText) {
|
||||||
|
innerChildren.push(...(defaultContent as ReturnType<typeof h>[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentWrapper = h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
class: cn(
|
||||||
|
'inline-flex items-center',
|
||||||
|
hasIcon && hasText && (isSmall ? 'gap-1' : 'gap-2'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
innerChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
const children =
|
||||||
|
isLoading && !hasIcon
|
||||||
|
? [contentWrapper, createSpinner(spinnerPx, 'inset-0 m-auto')]
|
||||||
|
: [contentWrapper]
|
||||||
|
|
||||||
|
const buttonClass = cn(
|
||||||
|
variantClasses.value,
|
||||||
|
props.circle && 'rounded-full p-0',
|
||||||
|
props.circle && CIRCLE_WIDTHS[size], // eslint-disable-line security/detect-object-injection
|
||||||
|
)
|
||||||
|
|
||||||
|
const buttonData = {
|
||||||
|
type: props.type,
|
||||||
|
disabled: isDisabled || undefined,
|
||||||
|
'data-variant': props.variant,
|
||||||
|
'data-appearance': props.appearance,
|
||||||
|
'aria-busy': isLoading || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
||||||
if (isVue2) {
|
if (isVue2) {
|
||||||
// Vue 2: separate attrs and on (listeners)
|
|
||||||
// $listeners contains event handlers like @click
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const proxy = instance?.proxy as any
|
const proxy = instance?.proxy as any
|
||||||
const listeners = proxy?.$listeners || {}
|
const listeners = proxy?.$listeners || {}
|
||||||
// In Vue 2, class/style are not in $attrs - access via $vnode
|
|
||||||
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
||||||
const parentDynClass = proxy?.$vnode?.data?.class
|
const parentDynClass = proxy?.$vnode?.data?.class
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
class: [classes.value, parentClass, parentDynClass].filter(Boolean),
|
class: [buttonClass, parentClass, parentDynClass].filter(Boolean),
|
||||||
attrs: {
|
attrs: { ...buttonData, ...attrs },
|
||||||
type: props.type,
|
|
||||||
disabled: props.disabled || undefined,
|
|
||||||
'data-appearance': props.appearance,
|
|
||||||
...attrs,
|
|
||||||
},
|
|
||||||
on: listeners,
|
on: listeners,
|
||||||
},
|
},
|
||||||
children,
|
children,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/* v8 ignore stop */
|
/* v8 ignore stop */
|
||||||
// Vue 3: flat props, attrs includes listeners
|
|
||||||
// Extract class from attrs to merge instead of overwrite
|
|
||||||
const { class: attrClass, ...restAttrs } = attrs as Record<string, unknown>
|
const { class: attrClass, ...restAttrs } = attrs as Record<string, unknown>
|
||||||
return h(
|
return h(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
type: props.type,
|
...buttonData,
|
||||||
disabled: props.disabled,
|
class: cn(buttonClass, attrClass || ''),
|
||||||
'data-appearance': props.appearance,
|
|
||||||
class: cn(classes.value, (attrClass as string) || ''),
|
|
||||||
...restAttrs,
|
...restAttrs,
|
||||||
},
|
},
|
||||||
children,
|
children,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -45,7 +45,7 @@ export const buttonVariants = cva(
|
|||||||
],
|
],
|
||||||
outline: [
|
outline: [
|
||||||
'bg-transparent shadow-none',
|
'bg-transparent shadow-none',
|
||||||
// Disabled: gray border and text
|
// Disabled: light gray border and gray text
|
||||||
'disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)]',
|
'disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)]',
|
||||||
],
|
],
|
||||||
ghost: [
|
ghost: [
|
||||||
@ -55,10 +55,10 @@ export const buttonVariants = cva(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: 'h-[26px] px-[8px] py-0 text-[12px] leading-[normal] tracking-[0.6px] rounded-[5px] overflow-hidden whitespace-nowrap align-middle', // base-button --small
|
sm: 'h-[26px] min-w-[26px] px-[8px] py-0 text-[12px] leading-[normal] tracking-[0.6px] rounded-[5px] overflow-hidden whitespace-nowrap align-middle',
|
||||||
md: 'h-[36px] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] align-middle',
|
md: 'h-[36px] min-w-[36px] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] align-middle',
|
||||||
lg: 'h-12 px-6 py-3 text-lg',
|
lg: 'h-12 min-w-12 px-6 py-3 text-lg',
|
||||||
xl: 'h-14 px-8 py-4 text-xl',
|
xl: 'h-14 min-w-14 px-8 py-4 text-xl',
|
||||||
},
|
},
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
true: 'w-full',
|
true: 'w-full',
|
||||||
|
|||||||
29
packages/ui/src/styles/animations.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Spinner animations for loading state
|
||||||
|
*/
|
||||||
|
@keyframes os-spinner-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -35;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -124;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes os-spinner-rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(2160deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
@import "tailwindcss/theme";
|
@import "tailwindcss/theme";
|
||||||
@import "tailwindcss/utilities";
|
@import "tailwindcss/utilities";
|
||||||
|
@import "./animations.css";
|
||||||
|
|
||||||
/* Scan component files for utility classes */
|
/* Scan component files for utility classes */
|
||||||
@source "../components/**/*.vue";
|
@source "../components/**/*.vue";
|
||||||
|
|||||||
@ -1,20 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-button">
|
<div class="action-button">
|
||||||
<base-button
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
:appearance="filled ? 'filled' : 'outline'"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:icon="icon"
|
|
||||||
:aria-label="text"
|
:aria-label="text"
|
||||||
:filled="filled"
|
|
||||||
circle
|
circle
|
||||||
@click="click"
|
@click="click"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon :name="icon" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
<div class="count">{{ count }}</div>
|
<div class="count">{{ count }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
count: { type: Number, required: true },
|
count: { type: Number, required: true },
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
|
|||||||
@ -29,18 +29,18 @@ describe('FollowButton.vue', () => {
|
|||||||
|
|
||||||
it('renders button and text', () => {
|
it('renders button and text', () => {
|
||||||
expect(mocks.$t).toHaveBeenCalledWith('followButton.follow')
|
expect(mocks.$t).toHaveBeenCalledWith('followButton.follow')
|
||||||
expect(wrapper.findAll('.base-button')).toHaveLength(1)
|
expect(wrapper.findAll('[data-test="follow-btn"]')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders button and text when followed', () => {
|
it('renders button and text when followed', () => {
|
||||||
propsData.isFollowed = true
|
propsData.isFollowed = true
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(mocks.$t).toHaveBeenCalledWith('followButton.following')
|
expect(mocks.$t).toHaveBeenCalledWith('followButton.following')
|
||||||
expect(wrapper.findAll('.base-button')).toHaveLength(1)
|
expect(wrapper.findAll('[data-test="follow-btn"]')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.skip('toggle the button', async () => {
|
it.skip('toggle the button', async () => {
|
||||||
wrapper.find('.base-button').trigger('click') // This does not work since @click.prevent is used
|
wrapper.find('[data-test="follow-btn"]').trigger('click') // This does not work since @click.prevent is used
|
||||||
expect(wrapper.vm.isFollowed).toBe(true)
|
expect(wrapper.vm.isFollowed).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,24 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<base-button
|
<os-button
|
||||||
class="track-button"
|
data-test="follow-btn"
|
||||||
|
:variant="isFollowed && hovered ? 'danger' : 'primary'"
|
||||||
|
:appearance="isFollowed && !hovered ? 'filled' : 'outline'"
|
||||||
:disabled="disabled || !followId"
|
:disabled="disabled || !followId"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:icon="icon"
|
full-width
|
||||||
:filled="isFollowed && !hovered"
|
@mouseenter="onHover"
|
||||||
:danger="isFollowed && hovered"
|
@mouseleave="hovered = false"
|
||||||
@mouseenter.native="onHover"
|
|
||||||
@mouseleave.native="hovered = false"
|
|
||||||
@click.prevent="toggle"
|
@click.prevent="toggle"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon :name="icon" />
|
||||||
|
</template>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { followUserMutation, unfollowUserMutation } from '~/graphql/User'
|
import { followUserMutation, unfollowUserMutation } from '~/graphql/User'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HcFollowButton',
|
name: 'HcFollowButton',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
followId: { type: String, default: null },
|
followId: { type: String, default: null },
|
||||||
isFollowed: { type: Boolean, default: false },
|
isFollowed: { type: Boolean, default: false },
|
||||||
@ -82,10 +87,3 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.track-button {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,26 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<base-button
|
<os-button
|
||||||
class="join-leave-button"
|
data-test="join-leave-btn"
|
||||||
|
:variant="isMember && hovered ? 'danger' : 'primary'"
|
||||||
|
:appearance="filled || (isMember && !hovered) ? 'filled' : 'outline'"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:loading="localLoading"
|
:loading="localLoading"
|
||||||
:icon="icon"
|
full-width
|
||||||
:filled="filled || (isMember && !hovered)"
|
|
||||||
:danger="isMember && hovered"
|
|
||||||
v-tooltip="tooltip"
|
v-tooltip="tooltip"
|
||||||
@mouseenter.native="onHover"
|
@mouseenter="onHover"
|
||||||
@mouseleave.native="hovered = false"
|
@mouseleave="hovered = false"
|
||||||
@click.prevent="toggle"
|
@click.prevent="toggle"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon :name="icon" />
|
||||||
|
</template>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations } from 'vuex'
|
import { mapMutations } from 'vuex'
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { joinGroupMutation, leaveGroupMutation } from '~/graphql/groups'
|
import { joinGroupMutation, leaveGroupMutation } from '~/graphql/groups'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'JoinLeaveButton',
|
name: 'JoinLeaveButton',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
group: { type: Object, required: true },
|
group: { type: Object, required: true },
|
||||||
userId: { type: String, required: true },
|
userId: { type: String, required: true },
|
||||||
@ -146,10 +151,3 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.join-leave-button {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`JoinLeaveButton.vue shallowMount renders 1`] = `
|
exports[`JoinLeaveButton.vue shallowMount renders 1`] = `
|
||||||
<base-button-stub icon="plus" size="regular" type="button" class="join-leave-button has-tooltip" data-original-title="null">
|
<os-button-stub variant="primary" appearance="outline" size="md" fullwidth="true" type="button" data-original-title="null" class=" has-tooltip">
|
||||||
group.joinLeaveButton.join
|
group.joinLeaveButton.join
|
||||||
</base-button-stub>
|
</os-button-stub>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,25 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="categories-select">
|
<section class="categories-select">
|
||||||
<base-button
|
<os-button
|
||||||
v-for="category in sortCategories(categories)"
|
v-for="category in sortCategories(categories)"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
:data-test="categoryButtonsId(category.id)"
|
:data-test="categoryButtonsId(category.id)"
|
||||||
@click="toggleCategory(category.id)"
|
@click="toggleCategory(category.id)"
|
||||||
:filled="isActive(category.id)"
|
variant="primary"
|
||||||
|
:appearance="isActive(category.id) ? 'filled' : 'outline'"
|
||||||
:disabled="isDisabled(category.id)"
|
:disabled="isDisabled(category.id)"
|
||||||
:icon="category.icon"
|
size="sm"
|
||||||
size="small"
|
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t(`contribution.category.description.${category.slug}`),
|
content: $t(`contribution.category.description.${category.slug}`),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<template #icon><base-icon :name="category.icon" /></template>
|
||||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { CATEGORIES_MAX } from '~/constants/categories.js'
|
import { CATEGORIES_MAX } from '~/constants/categories.js'
|
||||||
import xor from 'lodash/xor'
|
import xor from 'lodash/xor'
|
||||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||||
@ -31,6 +33,7 @@ export default {
|
|||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
components: { OsButton },
|
||||||
mixins: [SortCategories, GetCategories],
|
mixins: [SortCategories, GetCategories],
|
||||||
props: {
|
props: {
|
||||||
existingCategoryIds: { type: Array, default: () => [] },
|
existingCategoryIds: { type: Array, default: () => [] },
|
||||||
@ -83,7 +86,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> .base-button {
|
> button {
|
||||||
margin-right: $space-xx-small;
|
margin-right: $space-xx-small;
|
||||||
margin-bottom: $space-xx-small;
|
margin-bottom: $space-xx-small;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,19 @@
|
|||||||
<div class="add-chat-room-by-user-search">
|
<div class="add-chat-room-by-user-search">
|
||||||
<ds-flex class="headline">
|
<ds-flex class="headline">
|
||||||
<h2 class="title">{{ $t('chat.addRoomHeadline') }}</h2>
|
<h2 class="title">{{ $t('chat.addRoomHeadline') }}</h2>
|
||||||
<base-button class="close-button" icon="close" circle size="small" @click="closeUserSearch" />
|
<os-button
|
||||||
|
class="close-button"
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
|
circle
|
||||||
|
size="sm"
|
||||||
|
:aria-label="$t('actions.close')"
|
||||||
|
@click="closeUserSearch"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<ds-space>
|
<ds-space>
|
||||||
@ -12,11 +24,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import SelectUserSearch from '~/components/generic/SelectUserSearch/SelectUserSearch'
|
import SelectUserSearch from '~/components/generic/SelectUserSearch/SelectUserSearch'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddChatRoomByUserSearch',
|
name: 'AddChatRoomByUserSearch',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
SelectUserSearch,
|
SelectUserSearch,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -35,18 +35,34 @@
|
|||||||
<ds-flex v-if="singleRoom">
|
<ds-flex v-if="singleRoom">
|
||||||
<ds-flex-item centered class="single-chat-bubble">
|
<ds-flex-item centered class="single-chat-bubble">
|
||||||
<nuxt-link :to="{ name: 'chat' }">
|
<nuxt-link :to="{ name: 'chat' }">
|
||||||
<base-button icon="expand" size="small" circle />
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
|
circle
|
||||||
|
size="sm"
|
||||||
|
:aria-label="$t('chat.expandChat')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="expand" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item centered>
|
<ds-flex-item centered>
|
||||||
<div class="vac-svg-button vac-room-options">
|
<div class="vac-svg-button vac-room-options">
|
||||||
<slot name="menu-icon">
|
<slot name="menu-icon">
|
||||||
<base-button
|
<os-button
|
||||||
icon="close"
|
variant="primary"
|
||||||
size="small"
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
|
size="sm"
|
||||||
|
:aria-label="$t('chat.closeChat')"
|
||||||
@click="$emit('close-single-room', true)"
|
@click="$emit('close-single-room', true)"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
@ -89,6 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { roomQuery, createRoom, unreadRoomsQuery } from '~/graphql/Rooms'
|
import { roomQuery, createRoom, unreadRoomsQuery } from '~/graphql/Rooms'
|
||||||
import {
|
import {
|
||||||
messageQuery,
|
messageQuery,
|
||||||
@ -101,6 +118,7 @@ import { mapGetters, mapMutations } from 'vuex'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
theme: {
|
theme: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link class="chat-notification-menu" :to="{ name: 'chat' }">
|
<nuxt-link class="chat-notification-menu" :to="{ name: 'chat' }">
|
||||||
<base-button
|
<os-button
|
||||||
ghost
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
|
:aria-label="$t('header.chats.tooltip')"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('header.chats.tooltip'),
|
content: $t('header.chats.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
|
<template #icon>
|
||||||
</base-button>
|
<counter-icon icon="chat-bubble" :count="unreadRoomCount" danger />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||||
import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
||||||
@ -21,6 +26,7 @@ import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ChatNotificationMenu',
|
name: 'ChatNotificationMenu',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
CounterIcon,
|
CounterIcon,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@ -54,15 +54,21 @@
|
|||||||
class="shout-button"
|
class="shout-button"
|
||||||
node-type="Comment"
|
node-type="Comment"
|
||||||
/>
|
/>
|
||||||
<base-button
|
<os-button
|
||||||
:title="$t('post.comment.reply')"
|
:title="$t('post.comment.reply')"
|
||||||
icon="level-down"
|
:aria-label="$t('post.comment.reply')"
|
||||||
class="reply-button"
|
class="reply-button"
|
||||||
|
variant="primary"
|
||||||
|
appearance="outline"
|
||||||
circle
|
circle
|
||||||
size="small"
|
size="sm"
|
||||||
v-scroll-to="'.editor'"
|
v-scroll-to="'.editor'"
|
||||||
@click="reply"
|
@click="reply"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="level-down" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
</base-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -4,16 +4,24 @@
|
|||||||
<base-card>
|
<base-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">
|
||||||
<base-button
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
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') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
<base-button type="submit" :loading="loading" :disabled="disabled || errors" filled>
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
type="submit"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="disabled || !!errors"
|
||||||
|
>
|
||||||
{{ $t('post.comment.submit') }}
|
{{ $t('post.comment.submit') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
</base-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
@ -21,6 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import HcEditor from '~/components/Editor/Editor'
|
import HcEditor from '~/components/Editor/Editor'
|
||||||
import { COMMENT_MIN_LENGTH } from '~/constants/comment'
|
import { COMMENT_MIN_LENGTH } from '~/constants/comment'
|
||||||
import { minimisedUserQuery } from '~/graphql/User'
|
import { minimisedUserQuery } from '~/graphql/User'
|
||||||
@ -28,6 +37,7 @@ import CommentMutations from '~/graphql/CommentMutations'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
HcEditor,
|
HcEditor,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -79,54 +89,49 @@ export default {
|
|||||||
this.closeEditWindow()
|
this.closeEditWindow()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSubmit() {
|
async handleSubmit() {
|
||||||
let mutateParams
|
const mutateParams = !this.update
|
||||||
if (!this.update) {
|
? {
|
||||||
mutateParams = {
|
mutation: CommentMutations(this.$i18n).CreateComment,
|
||||||
mutation: CommentMutations(this.$i18n).CreateComment,
|
variables: {
|
||||||
variables: {
|
postId: this.post.id,
|
||||||
postId: this.post.id,
|
content: this.form.content,
|
||||||
content: this.form.content,
|
},
|
||||||
},
|
}
|
||||||
}
|
: {
|
||||||
} else {
|
mutation: CommentMutations(this.$i18n).UpdateComment,
|
||||||
mutateParams = {
|
variables: {
|
||||||
mutation: CommentMutations(this.$i18n).UpdateComment,
|
id: this.comment.id,
|
||||||
variables: {
|
content: this.form.content,
|
||||||
id: this.comment.id,
|
},
|
||||||
content: this.form.content,
|
}
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.disabled = true
|
this.disabled = true
|
||||||
this.$apollo
|
try {
|
||||||
.mutate(mutateParams)
|
const res = await this.$apollo.mutate(mutateParams)
|
||||||
.then((res) => {
|
if (!this.update) {
|
||||||
this.loading = false
|
const {
|
||||||
if (!this.update) {
|
data: { CreateComment },
|
||||||
const {
|
} = res
|
||||||
data: { CreateComment },
|
this.$emit('createComment', CreateComment)
|
||||||
} = res
|
this.clear()
|
||||||
this.$emit('createComment', CreateComment)
|
this.$toast.success(this.$t('post.comment.submitted'))
|
||||||
this.clear()
|
} else {
|
||||||
this.$toast.success(this.$t('post.comment.submitted'))
|
const {
|
||||||
this.disabled = false
|
data: { UpdateComment },
|
||||||
} else {
|
} = res
|
||||||
const {
|
this.$emit('updateComment', UpdateComment)
|
||||||
data: { UpdateComment },
|
this.$emit('collapse')
|
||||||
} = res
|
this.$toast.success(this.$t('post.comment.updated'))
|
||||||
this.$emit('updateComment', UpdateComment)
|
this.closeEditWindow()
|
||||||
this.$emit('collapse')
|
}
|
||||||
this.$toast.success(this.$t('post.comment.updated'))
|
} catch (err) {
|
||||||
this.disabled = false
|
this.$toast.error(err.message)
|
||||||
this.closeEditWindow()
|
this.disabled = false
|
||||||
}
|
} finally {
|
||||||
})
|
this.loading = false
|
||||||
.catch((err) => {
|
}
|
||||||
this.$toast.error(err.message)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
@ -152,7 +157,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
> .base-button {
|
> button {
|
||||||
margin-left: $space-x-small;
|
margin-left: $space-x-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ describe('ComponentSlider.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('click on next Button', async () => {
|
it('click on next Button', async () => {
|
||||||
await wrapper.find('.base-button[data-test="next-button"]').trigger('click')
|
await wrapper.find('button[data-test="next-button"]').trigger('click')
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(propsData.sliderData.sliderSelectorCallback).toHaveBeenCalled()
|
expect(propsData.sliderData.sliderSelectorCallback).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -35,26 +35,26 @@
|
|||||||
:key="slider.name"
|
:key="slider.name"
|
||||||
:class="['Sliders__slider-selection', index === sliderIndex && '--unconfirmed']"
|
:class="['Sliders__slider-selection', index === sliderIndex && '--unconfirmed']"
|
||||||
>
|
>
|
||||||
<base-button
|
<os-button
|
||||||
:class="['selection-dot']"
|
:class="['selection-dot']"
|
||||||
style="float: left"
|
style="float: left"
|
||||||
:bullet="true"
|
variant="primary"
|
||||||
size="tiny"
|
:appearance="index <= sliderIndex ? 'filled' : 'outline'"
|
||||||
type="submit"
|
circle
|
||||||
filled
|
size="sm"
|
||||||
:loading="false"
|
|
||||||
:disabled="index > sliderIndex"
|
:disabled="index > sliderIndex"
|
||||||
|
:aria-label="
|
||||||
|
$t('component-slider.step', { current: index + 1, total: sliderData.sliders.length })
|
||||||
|
"
|
||||||
@click="sliderData.sliderSelectorCallback(index)"
|
@click="sliderData.sliderSelectorCallback(index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex-item>
|
<ds-flex-item>
|
||||||
<base-button
|
<os-button
|
||||||
:style="multipleSliders && 'float: right'"
|
:style="multipleSliders && 'float: right'"
|
||||||
:icon="sliderData.sliders[sliderIndex].button.icon"
|
variant="primary"
|
||||||
type="submit"
|
appearance="filled"
|
||||||
filled
|
|
||||||
padding
|
|
||||||
:loading="
|
:loading="
|
||||||
sliderData.sliders[sliderIndex].button.loading !== undefined
|
sliderData.sliders[sliderIndex].button.loading !== undefined
|
||||||
? sliderData.sliders[sliderIndex].button.loading
|
? sliderData.sliders[sliderIndex].button.loading
|
||||||
@ -64,8 +64,11 @@
|
|||||||
@click="onNextClick"
|
@click="onNextClick"
|
||||||
data-test="next-button"
|
data-test="next-button"
|
||||||
>
|
>
|
||||||
|
<template v-if="sliderData.sliders[sliderIndex].button.icon" #icon>
|
||||||
|
<base-icon :name="sliderData.sliders[sliderIndex].button.icon" />
|
||||||
|
</template>
|
||||||
{{ $t(sliderData.sliders[sliderIndex].button.titleIdent) }}
|
{{ $t(sliderData.sliders[sliderIndex].button.titleIdent) }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
|
|
||||||
@ -74,7 +77,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { OsButton },
|
||||||
name: 'ComponentSlider',
|
name: 'ComponentSlider',
|
||||||
props: {
|
props: {
|
||||||
sliderData: { type: Object, required: true },
|
sliderData: { type: Object, required: true },
|
||||||
@ -107,6 +113,10 @@ export default {
|
|||||||
&__slider-selection {
|
&__slider-selection {
|
||||||
.selection-dot {
|
.selection-dot {
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
height: 18px !important;
|
||||||
|
width: 18px !important;
|
||||||
|
min-height: 18px !important;
|
||||||
|
min-width: 18px !important;
|
||||||
}
|
}
|
||||||
&.--unconfirmed {
|
&.--unconfirmed {
|
||||||
opacity: $opacity-disabled;
|
opacity: $opacity-disabled;
|
||||||
|
|||||||
@ -2,13 +2,19 @@
|
|||||||
<dropdown class="content-menu" :placement="placement" offset="5">
|
<dropdown class="content-menu" :placement="placement" offset="5">
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<slot name="button" :toggleMenu="toggleMenu">
|
<slot name="button" :toggleMenu="toggleMenu">
|
||||||
<base-button
|
<os-button
|
||||||
data-test="content-menu-button"
|
data-test="content-menu-button"
|
||||||
icon="ellipsis-v"
|
variant="primary"
|
||||||
size="small"
|
appearance="outline"
|
||||||
|
size="sm"
|
||||||
circle
|
circle
|
||||||
|
:aria-label="$t('actions.menu')"
|
||||||
@click.prevent="toggleMenu()"
|
@click.prevent="toggleMenu()"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="ellipsis-v" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template #popover="{ toggleMenu }">
|
<template #popover="{ toggleMenu }">
|
||||||
@ -31,12 +37,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import PinnedPostsMixin from '~/mixins/pinnedPosts'
|
import PinnedPostsMixin from '~/mixins/pinnedPosts'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ContentMenu',
|
name: 'ContentMenu',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
mixins: [PinnedPostsMixin],
|
mixins: [PinnedPostsMixin],
|
||||||
|
|||||||
@ -2,13 +2,19 @@
|
|||||||
<dropdown class="group-content-menu" :placement="placement" offset="5">
|
<dropdown class="group-content-menu" :placement="placement" offset="5">
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<slot name="button" :toggleMenu="toggleMenu">
|
<slot name="button" :toggleMenu="toggleMenu">
|
||||||
<base-button
|
<os-button
|
||||||
icon="ellipsis-v"
|
variant="primary"
|
||||||
size="small"
|
appearance="outline"
|
||||||
|
size="sm"
|
||||||
circle
|
circle
|
||||||
@click.prevent="toggleMenu()"
|
:aria-label="$t('group.contentMenu.menuButton')"
|
||||||
data-test="group-menu-button"
|
data-test="group-menu-button"
|
||||||
/>
|
@click.prevent="toggleMenu()"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="ellipsis-v" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template #popover="{ toggleMenu }">
|
<template #popover="{ toggleMenu }">
|
||||||
@ -32,11 +38,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GroupContentMenu',
|
name: 'GroupContentMenu',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -21,18 +21,26 @@ exports[`GroupContentMenu renders as groupProfile when I am the owner 1`] = `
|
|||||||
trigger="manual"
|
trigger="manual"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="base-button --icon-only --circle --small"
|
aria-label="group.contentMenu.menuButton"
|
||||||
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||||
|
data-appearance="outline"
|
||||||
data-test="group-menu-button"
|
data-test="group-menu-button"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -138,18 +146,26 @@ exports[`GroupContentMenu renders as groupProfile, muted 1`] = `
|
|||||||
trigger="manual"
|
trigger="manual"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="base-button --icon-only --circle --small"
|
aria-label="group.contentMenu.menuButton"
|
||||||
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||||
|
data-appearance="outline"
|
||||||
data-test="group-menu-button"
|
data-test="group-menu-button"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -213,18 +229,26 @@ exports[`GroupContentMenu renders as groupProfile, not muted 1`] = `
|
|||||||
trigger="manual"
|
trigger="manual"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="base-button --icon-only --circle --small"
|
aria-label="group.contentMenu.menuButton"
|
||||||
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||||
|
data-appearance="outline"
|
||||||
data-test="group-menu-button"
|
data-test="group-menu-button"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -288,18 +312,26 @@ exports[`GroupContentMenu renders as groupTeaser 1`] = `
|
|||||||
trigger="manual"
|
trigger="manual"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="base-button --icon-only --circle --small"
|
aria-label="group.contentMenu.menuButton"
|
||||||
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[26px] min-w-[26px] text-[12px] leading-[normal] tracking-[0.6px] overflow-hidden whitespace-nowrap align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[26px]"
|
||||||
|
data-appearance="outline"
|
||||||
data-test="group-menu-button"
|
data-test="group-menu-button"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -182,9 +182,18 @@
|
|||||||
>
|
>
|
||||||
{{ $t('actions.cancel') }}
|
{{ $t('actions.cancel') }}
|
||||||
</os-button>
|
</os-button>
|
||||||
<base-button type="submit" icon="check" :loading="loading" :disabled="errors" filled>
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
type="submit"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!!errors"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="check" />
|
||||||
|
</template>
|
||||||
{{ $t('actions.save') }}
|
{{ $t('actions.save') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
</base-card>
|
</base-card>
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a v-if="settings.url" :href="settings.url" :target="settings.target">
|
<a v-if="settings.url" :href="settings.url" :target="settings.target">
|
||||||
<base-button
|
<os-button
|
||||||
class="custom-button"
|
class="custom-button"
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
ghost
|
:aria-label="$t(settings.toolTipIdent)"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t(settings.toolTipIdent),
|
content: $t(settings.toolTipIdent),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
@ -16,13 +18,15 @@
|
|||||||
:alt="settings.iconAltText"
|
:alt="settings.iconAltText"
|
||||||
:style="logoWidthStyle"
|
:style="logoWidthStyle"
|
||||||
/>
|
/>
|
||||||
</base-button>
|
</os-button>
|
||||||
</a>
|
</a>
|
||||||
<nuxt-link v-else :to="settings.path">
|
<nuxt-link v-else :to="settings.path">
|
||||||
<base-button
|
<os-button
|
||||||
class="custom-button"
|
class="custom-button"
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
ghost
|
:aria-label="$t(settings.toolTipIdent)"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t(settings.toolTipIdent),
|
content: $t(settings.toolTipIdent),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
@ -34,27 +38,24 @@
|
|||||||
:alt="settings.iconAltText"
|
:alt="settings.iconAltText"
|
||||||
:style="logoWidthStyle"
|
:style="logoWidthStyle"
|
||||||
/>
|
/>
|
||||||
</base-button>
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import isEmpty from 'lodash/isEmpty'
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { OsButton },
|
||||||
name: 'CustomButton',
|
name: 'CustomButton',
|
||||||
props: {
|
props: {
|
||||||
settings: { type: Object, required: true },
|
settings: { type: Object, required: true },
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isEmpty,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
logoWidthStyle() {
|
logoWidthStyle() {
|
||||||
const width = this.isEmpty(this.settings.iconWidth) ? '26px' : this.settings.iconWidth
|
const width = isEmpty(this.settings.iconWidth) ? '26px' : this.settings.iconWidth
|
||||||
return `width: ${width};`
|
return `width: ${width};`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -42,26 +42,30 @@
|
|||||||
<section class="warning">
|
<section class="warning">
|
||||||
<p>{{ $t('settings.deleteUserAccount.accountWarning') }}</p>
|
<p>{{ $t('settings.deleteUserAccount.accountWarning') }}</p>
|
||||||
</section>
|
</section>
|
||||||
<base-button
|
<os-button
|
||||||
icon="trash"
|
variant="danger"
|
||||||
danger
|
appearance="filled"
|
||||||
filled
|
|
||||||
:disabled="!deleteEnabled"
|
:disabled="!deleteEnabled"
|
||||||
data-test="delete-button"
|
data-test="delete-button"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="trash" />
|
||||||
|
</template>
|
||||||
{{ $t('settings.deleteUserAccount.name') }}
|
{{ $t('settings.deleteUserAccount.name') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</base-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapActions, mapGetters } from 'vuex'
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { currentUserCountQuery } from '~/graphql/User'
|
import { currentUserCountQuery } from '~/graphql/User'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DeleteData',
|
name: 'DeleteData',
|
||||||
|
components: { OsButton },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
deleteContributions: false,
|
deleteContributions: false,
|
||||||
@ -166,7 +170,7 @@ export default {
|
|||||||
border-left: 4px solid $color-danger;
|
border-left: 4px solid $color-danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .base-button {
|
> button {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export default {
|
|||||||
displayContextMenu(target, content, type) {
|
displayContextMenu(target, content, type) {
|
||||||
const placement = type === 'link' ? 'right' : 'top-start'
|
const placement = type === 'link' ? 'right' : 'top-start'
|
||||||
const trigger = type === 'link' ? 'click' : 'mouseenter'
|
const trigger = type === 'link' ? 'click' : 'mouseenter'
|
||||||
const showOnInit = type !== 'link'
|
|
||||||
|
|
||||||
if (this.menu) {
|
if (this.menu) {
|
||||||
return
|
return
|
||||||
@ -24,7 +23,6 @@ export default {
|
|||||||
inertia: true,
|
inertia: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
placement,
|
placement,
|
||||||
showOnInit,
|
|
||||||
theme: 'ocelot-social',
|
theme: 'ocelot-social',
|
||||||
trigger,
|
trigger,
|
||||||
onMount(instance) {
|
onMount(instance) {
|
||||||
@ -35,6 +33,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
this.menu.show()
|
||||||
|
|
||||||
// we have to update tippy whenever the DOM is updated
|
// we have to update tippy whenever the DOM is updated
|
||||||
if (MutationObserver) {
|
if (MutationObserver) {
|
||||||
@ -50,8 +49,9 @@ export default {
|
|||||||
},
|
},
|
||||||
hideContextMenu() {
|
hideContextMenu() {
|
||||||
if (this.menu) {
|
if (this.menu) {
|
||||||
this.menu.destroy()
|
const menu = this.menu
|
||||||
this.menu = null
|
this.menu = null
|
||||||
|
menu.destroy()
|
||||||
}
|
}
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect()
|
this.observer.disconnect()
|
||||||
|
|||||||
@ -248,12 +248,17 @@ export default {
|
|||||||
this.editor.commands.mention({ id: message.id, label: message.slug })
|
this.editor.commands.mention({ id: message.id, label: message.slug })
|
||||||
},
|
},
|
||||||
toggleLinkInput(attrs, element) {
|
toggleLinkInput(attrs, element) {
|
||||||
if (!this.isLinkInputActive && attrs && element) {
|
if (this.$refs.contextMenu.menu) {
|
||||||
|
this.$refs.contextMenu.hideContextMenu()
|
||||||
|
this.isLinkInputActive = false
|
||||||
|
this.editor.focus()
|
||||||
|
} else if (attrs && element) {
|
||||||
this.$refs.linkInput.linkUrl = attrs.href
|
this.$refs.linkInput.linkUrl = attrs.href
|
||||||
this.isLinkInputActive = true
|
this.isLinkInputActive = true
|
||||||
this.$refs.contextMenu.displayContextMenu(element, this.$refs.linkInput.$el, 'link')
|
this.$nextTick(() => {
|
||||||
|
this.$refs.contextMenu.displayContextMenu(element, this.$refs.linkInput.$el, 'link')
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$refs.contextMenu.hideContextMenu()
|
|
||||||
this.isLinkInputActive = false
|
this.isLinkInputActive = false
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,64 +1,144 @@
|
|||||||
<template>
|
<template>
|
||||||
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, getMarkAttrs }">
|
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, getMarkAttrs }">
|
||||||
<div>
|
<div>
|
||||||
<menu-bar-button :isActive="isActive.bold()" :onClick="commands.bold" icon="bold" />
|
<os-button
|
||||||
|
size="sm"
|
||||||
|
circle
|
||||||
|
variant="primary"
|
||||||
|
:appearance="isActive.bold() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.bold')"
|
||||||
|
@click="commands.bold"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="bold" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button :isActive="isActive.italic()" :onClick="commands.italic" icon="italic" />
|
<os-button
|
||||||
|
size="sm"
|
||||||
|
circle
|
||||||
|
variant="primary"
|
||||||
|
:appearance="isActive.italic() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.italic')"
|
||||||
|
@click="commands.italic"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="italic" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.underline()"
|
size="sm"
|
||||||
:onClick="commands.underline"
|
circle
|
||||||
icon="underline"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.underline() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.underline')"
|
||||||
|
@click="commands.underline"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="underline" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
ref="linkButton"
|
size="sm"
|
||||||
:isActive="isActive.link()"
|
circle
|
||||||
:onClick="(event) => toggleLinkInput(getMarkAttrs('link'), event.currentTarget)"
|
variant="primary"
|
||||||
icon="link"
|
:appearance="isActive.link() ? 'outline' : 'ghost'"
|
||||||
/>
|
:aria-label="$t('editor.legend.link')"
|
||||||
|
@click="(event) => toggleLinkInput(getMarkAttrs('link'), event.target.closest('button'))"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="link" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.paragraph()"
|
size="sm"
|
||||||
:onClick="commands.paragraph"
|
circle
|
||||||
icon="paragraph"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.paragraph() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.paragraph')"
|
||||||
|
@click="commands.paragraph"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="paragraph" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.heading({ level: 3 })"
|
size="sm"
|
||||||
:onClick="() => commands.heading({ level: 3 })"
|
circle
|
||||||
label="H3"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.heading({ level: 3 }) ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.heading3')"
|
||||||
|
@click="() => commands.heading({ level: 3 })"
|
||||||
|
>
|
||||||
|
H3
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.heading({ level: 4 })"
|
size="sm"
|
||||||
:onClick="() => commands.heading({ level: 4 })"
|
circle
|
||||||
label="H4"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.heading({ level: 4 }) ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.heading4')"
|
||||||
|
@click="() => commands.heading({ level: 4 })"
|
||||||
|
>
|
||||||
|
H4
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.bullet_list()"
|
size="sm"
|
||||||
:onClick="commands.bullet_list"
|
circle
|
||||||
icon="list-ul"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.bullet_list() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.unorderedList')"
|
||||||
|
@click="commands.bullet_list"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="list-ul" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.ordered_list()"
|
size="sm"
|
||||||
:onClick="commands.ordered_list"
|
circle
|
||||||
icon="list-ol"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.ordered_list() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.orderedList')"
|
||||||
|
@click="commands.ordered_list"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="list-ol" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.blockquote()"
|
size="sm"
|
||||||
:onClick="commands.blockquote"
|
circle
|
||||||
icon="quote-right"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.blockquote() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.quote')"
|
||||||
|
@click="commands.blockquote"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="quote-right" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-bar-button
|
<os-button
|
||||||
:isActive="isActive.horizontal_rule()"
|
size="sm"
|
||||||
:onClick="commands.horizontal_rule"
|
circle
|
||||||
icon="minus"
|
variant="primary"
|
||||||
/>
|
:appearance="isActive.horizontal_rule() ? 'outline' : 'ghost'"
|
||||||
|
:aria-label="$t('editor.legend.ruler')"
|
||||||
|
@click="commands.horizontal_rule"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="minus" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
|
||||||
<menu-legend class="legend-button" />
|
<menu-legend class="legend-button" />
|
||||||
</div>
|
</div>
|
||||||
@ -66,14 +146,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { EditorMenuBar } from 'tiptap'
|
import { EditorMenuBar } from 'tiptap'
|
||||||
import MenuBarButton from './MenuBarButton'
|
|
||||||
import MenuLegend from './MenuLegend.vue'
|
import MenuLegend from './MenuLegend.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
EditorMenuBar,
|
EditorMenuBar,
|
||||||
MenuBarButton,
|
|
||||||
MenuLegend,
|
MenuLegend,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<base-button size="small" circle :ghost="!isActive" @click="onClick" :icon="icon">
|
|
||||||
<span v-if="label">{{ label }}</span>
|
|
||||||
</base-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
isActive: Boolean,
|
|
||||||
icon: String,
|
|
||||||
label: String,
|
|
||||||
onClick: { type: Function, default: () => {} },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<dropdown :placement="placement" offset="5">
|
<dropdown :placement="placement" offset="5">
|
||||||
<template #default="{ openMenu, closeMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<slot name="button">
|
<slot name="button">
|
||||||
<menu-bar-button
|
<os-button
|
||||||
class="legend-question-button"
|
class="legend-question-button"
|
||||||
icon="question-circle"
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
ghost
|
size="sm"
|
||||||
:onClick="
|
@click="toggleMenu"
|
||||||
() => {
|
>
|
||||||
isDropdownOpen ? closeMenu() : openMenu()
|
<template #icon>
|
||||||
isDropdownOpen = !isDropdownOpen
|
<base-icon name="question-circle" />
|
||||||
}
|
</template>
|
||||||
"
|
</os-button>
|
||||||
/>
|
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<!-- eslint-disable-next-line vue/no-useless-template-attributes -->
|
<!-- eslint-disable-next-line vue/no-useless-template-attributes -->
|
||||||
@ -26,9 +26,12 @@
|
|||||||
:key="item.name"
|
:key="item.name"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<base-button size="small" circle ghost :icon="item.iconName" class="legend-icon">
|
<os-button size="sm" circle variant="primary" appearance="ghost" class="legend-icon">
|
||||||
|
<template v-if="item.iconName" #icon>
|
||||||
|
<base-icon :name="item.iconName" />
|
||||||
|
</template>
|
||||||
<span v-if="item.label">{{ item.label }}</span>
|
<span v-if="item.label">{{ item.label }}</span>
|
||||||
</base-button>
|
</os-button>
|
||||||
<span>{{ $t(item.name) }}</span>
|
<span>{{ $t(item.name) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="tool-shortcut">{{ item.shortcut }}</span>
|
<span class="tool-shortcut">{{ item.shortcut }}</span>
|
||||||
@ -39,13 +42,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import MenuBarButton from './MenuBarButton'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
MenuBarButton,
|
OsButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
placement: { type: String, default: 'bottom-start' },
|
placement: { type: String, default: 'bottom-start' },
|
||||||
@ -65,20 +68,19 @@ export default {
|
|||||||
{ iconName: 'quote-right', name: `editor.legend.quote`, shortcut: '> + space' },
|
{ iconName: 'quote-right', name: `editor.legend.quote`, shortcut: '> + space' },
|
||||||
{ iconName: 'minus', name: `editor.legend.ruler`, shortcut: '---' },
|
{ iconName: 'minus', name: `editor.legend.ruler`, shortcut: '---' },
|
||||||
],
|
],
|
||||||
isDropdownOpen: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
.legend-question-button {
|
.legend-question-button {
|
||||||
color: $color-neutral-40;
|
color: #70677e !important;
|
||||||
font-size: 1.2rem !important;
|
font-size: 1.2rem !important;
|
||||||
}
|
}
|
||||||
.legend-question-button:hover {
|
.legend-question-button:hover {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
color: $color-neutral-40 !important;
|
color: #70677e !important;
|
||||||
}
|
}
|
||||||
.legend-question-button:focus {
|
.legend-question-button:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|||||||
@ -47,13 +47,19 @@
|
|||||||
<span>{{ $t('editor.embed.always_allow') }}</span>
|
<span>{{ $t('editor.embed.always_allow') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</aside>
|
</aside>
|
||||||
<base-button
|
<os-button
|
||||||
icon="close"
|
variant="primary"
|
||||||
size="small"
|
appearance="outline"
|
||||||
circle
|
circle
|
||||||
|
size="sm"
|
||||||
class="close-button"
|
class="close-button"
|
||||||
|
:aria-label="$t('actions.close')"
|
||||||
@click.prevent="removeEmbed()"
|
@click.prevent="removeEmbed()"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</ds-container>
|
</ds-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -252,9 +258,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .close-button {
|
> .close-button {
|
||||||
position: absolute;
|
position: absolute !important;
|
||||||
top: $space-x-small;
|
top: $space-x-small !important;
|
||||||
right: $space-x-small;
|
right: $space-x-small !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="emotion-button">
|
<div class="emotion-button">
|
||||||
<base-button :id="emotion" circle ghost @click="$emit('toggleEmotion', emotion)">
|
<os-button
|
||||||
|
:id="emotion"
|
||||||
|
appearance="ghost"
|
||||||
|
circle
|
||||||
|
:aria-label="$t(`contribution.emotions-label.${emotion}`)"
|
||||||
|
@click="$emit('toggleEmotion', emotion)"
|
||||||
|
>
|
||||||
<img class="image" :src="emojiPath" />
|
<img class="image" :src="emojiPath" />
|
||||||
</base-button>
|
</os-button>
|
||||||
<label class="label" :for="emotion">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
|
<label class="label" :for="emotion">{{ $t(`contribution.emotions-label.${emotion}`) }}</label>
|
||||||
<p v-if="emotionCount !== null" class="count">{{ emotionCount }}x</p>
|
<p v-if="emotionCount !== null" class="count">{{ emotionCount }}x</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { OsButton },
|
||||||
name: 'EmotionButton',
|
name: 'EmotionButton',
|
||||||
props: {
|
props: {
|
||||||
emojiPath: {
|
emojiPath: {
|
||||||
@ -35,7 +44,7 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> .base-button {
|
> button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ describe('CtaJoinLeaveGroup.vue', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
wrapper.find('.base-button').trigger('click')
|
wrapper.find('[data-test="join-leave-btn"]').trigger('click')
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
})
|
})
|
||||||
it('emits update event', async () => {
|
it('emits update event', async () => {
|
||||||
|
|||||||
@ -8,24 +8,28 @@
|
|||||||
{{ $t('contribution.comment.commenting-disabled.blocked-author.call-to-action') }}
|
{{ $t('contribution.comment.commenting-disabled.blocked-author.call-to-action') }}
|
||||||
</ds-text>
|
</ds-text>
|
||||||
<nuxt-link :to="authorLink">
|
<nuxt-link :to="authorLink">
|
||||||
<base-button icon="arrow-right" filled>
|
<os-button variant="primary" appearance="filled">
|
||||||
|
<template #icon><base-icon name="arrow-right" /></template>
|
||||||
{{
|
{{
|
||||||
$t('contribution.comment.commenting-disabled.blocked-author.button-label', {
|
$t('contribution.comment.commenting-disabled.blocked-author.button-label', {
|
||||||
name: author.name,
|
name: author.name,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</base-button>
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CtaUnblockAuthor',
|
name: 'CtaUnblockAuthor',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
author: {
|
author: {
|
||||||
type: Object,
|
type: Object,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@ -11,9 +11,8 @@ exports[`CtaJoinLeaveGroup.vue mount renders 1`] = `
|
|||||||
</h4>
|
</h4>
|
||||||
<p class="ds-text">
|
<p class="ds-text">
|
||||||
contribution.comment.commenting-disabled.no-group-member.call-to-action
|
contribution.comment.commenting-disabled.no-group-member.call-to-action
|
||||||
</p> <button type="button" class="join-leave-button base-button --filled has-tooltip" data-original-title="null"><span class="base-icon"><!----></span>
|
</p> <button type="button" data-variant="primary" data-appearance="filled" class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] px-[16px] py-0 text-[15px] leading-[normal] rounded-[5px] align-middle w-full bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] has-tooltip" data-original-title="null"><span class="inline-flex items-center gap-2"><span class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current -ml-1"><span class="base-icon"><!----></span></span>
|
||||||
<!---->
|
|
||||||
group.joinLeaveButton.join
|
group.joinLeaveButton.join
|
||||||
</button>
|
</span></button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -10,9 +10,9 @@ exports[`CtaUnblockAuthor.vue shallowMount renders 1`] = `
|
|||||||
contribution.comment.commenting-disabled.blocked-author.call-to-action
|
contribution.comment.commenting-disabled.blocked-author.call-to-action
|
||||||
</ds-text-stub>
|
</ds-text-stub>
|
||||||
<nuxt-link-stub to="[object Object]">
|
<nuxt-link-stub to="[object Object]">
|
||||||
<base-button-stub filled="true" icon="arrow-right" size="regular" type="button">
|
<os-button-stub variant="primary" appearance="filled" size="md" type="button">
|
||||||
contribution.comment.commenting-disabled.blocked-author.button-label
|
contribution.comment.commenting-disabled.blocked-author.button-label
|
||||||
</base-button-stub>
|
</os-button-stub>
|
||||||
</nuxt-link-stub>
|
</nuxt-link-stub>
|
||||||
</ds-space-stub>
|
</ds-space-stub>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -20,7 +20,13 @@
|
|||||||
<ds-text>
|
<ds-text>
|
||||||
{{ $t('components.registration.email-nonce.form.click-next') }}
|
{{ $t('components.registration.email-nonce.form.click-next') }}
|
||||||
</ds-text>
|
</ds-text>
|
||||||
<os-button variant="primary" :disabled="disabled" name="submit" type="submit">
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
:disabled="disabled"
|
||||||
|
name="submit"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
{{ $t('components.registration.email-nonce.form.next') }}
|
{{ $t('components.registration.email-nonce.form.next') }}
|
||||||
</os-button>
|
</os-button>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@ -60,21 +60,24 @@ describe('CategoriesFilter.vue', () => {
|
|||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
it('starts with all categories button active', () => {
|
it('starts with all categories button active', () => {
|
||||||
const allCategoriesButton = wrapper.find('.categories-filter .item-all-topics .base-button')
|
expect(
|
||||||
expect(allCategoriesButton.attributes().class).toContain('--filled')
|
wrapper
|
||||||
|
.find('.categories-filter .item-all-topics button[data-appearance="filled"]')
|
||||||
|
.exists(),
|
||||||
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO move to FilterMenuComponent.spec.js?
|
// TODO move to FilterMenuComponent.spec.js?
|
||||||
// it('sets category button attribute `filled` when corresponding category is filtered', async () => {
|
// it('sets category button attribute `filled` when corresponding category is filtered', async () => {
|
||||||
// getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
|
// getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
|
||||||
// wrapper = await Wrapper()
|
// wrapper = await Wrapper()
|
||||||
// democracyAndPoliticsButton = wrapper.find('.categories-filter .item-save-topics .base-button')
|
// democracyAndPoliticsButton = wrapper.find('.categories-filter .item-save-topics button')
|
||||||
// expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
|
// expect(democracyAndPoliticsButton.attributes().class).toContain('--filled')
|
||||||
// })
|
// })
|
||||||
|
|
||||||
describe('click on an "catetories-buttons" button', () => {
|
describe('click on an "catetories-buttons" button', () => {
|
||||||
it('calls TOGGLE_CATEGORY when clicked', () => {
|
it('calls TOGGLE_CATEGORY when clicked', () => {
|
||||||
environmentAndNatureButton = wrapper.findAll('.category-filter-list .base-button').at(0)
|
environmentAndNatureButton = wrapper.findAll('.category-filter-list button').at(0)
|
||||||
environmentAndNatureButton.trigger('click')
|
environmentAndNatureButton.trigger('click')
|
||||||
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat15')
|
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat15')
|
||||||
})
|
})
|
||||||
@ -84,7 +87,7 @@ describe('CategoriesFilter.vue', () => {
|
|||||||
it('when all button is clicked', async () => {
|
it('when all button is clicked', async () => {
|
||||||
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
|
getters['posts/filteredCategoryIds'] = jest.fn(() => ['cat9'])
|
||||||
wrapper = await Wrapper()
|
wrapper = await Wrapper()
|
||||||
const allCategoriesButton = wrapper.find('.categories-filter .item-all-topics .base-button')
|
const allCategoriesButton = wrapper.find('.categories-filter .item-all-topics button')
|
||||||
allCategoriesButton.trigger('click')
|
allCategoriesButton.trigger('click')
|
||||||
expect(mutations['posts/RESET_CATEGORIES']).toHaveBeenCalledTimes(1)
|
expect(mutations['posts/RESET_CATEGORIES']).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
@ -94,7 +97,7 @@ describe('CategoriesFilter.vue', () => {
|
|||||||
// describe('save categories', () => {
|
// describe('save categories', () => {
|
||||||
// it('calls the API', async () => {
|
// it('calls the API', async () => {
|
||||||
// wrapper = await Wrapper()
|
// wrapper = await Wrapper()
|
||||||
// const saveButton = wrapper.find('.categories-filter .item-save-topics .base-button')
|
// const saveButton = wrapper.find('.categories-filter .item-save-topics button')
|
||||||
// saveButton.trigger('click')
|
// saveButton.trigger('click')
|
||||||
// expect(apolloMutationMock).toBeCalled()
|
// expect(apolloMutationMock).toBeCalled()
|
||||||
// })
|
// })
|
||||||
|
|||||||
@ -2,38 +2,44 @@
|
|||||||
<filter-menu-section :title="$t('filter-menu.categories')" class="categories-filter">
|
<filter-menu-section :title="$t('filter-menu.categories')" class="categories-filter">
|
||||||
<template #filter-list>
|
<template #filter-list>
|
||||||
<div class="item item-all-topics">
|
<div class="item item-all-topics">
|
||||||
<base-button
|
<os-button
|
||||||
:filled="!filteredCategoryIds.length"
|
variant="primary"
|
||||||
:label="$t('filter-menu.all')"
|
:appearance="!filteredCategoryIds.length ? 'filled' : 'outline'"
|
||||||
icon="check"
|
size="sm"
|
||||||
@click="setResetCategories"
|
@click="setResetCategories"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="check" />
|
||||||
|
</template>
|
||||||
{{ $t('filter-menu.all') }}
|
{{ $t('filter-menu.all') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="category-filter-list">
|
<div class="category-filter-list">
|
||||||
<!-- <ds-space margin="small" /> -->
|
<!-- <ds-space margin="small" /> -->
|
||||||
<base-button
|
<os-button
|
||||||
v-for="category in sortCategories(categories)"
|
v-for="category in sortCategories(categories)"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
|
variant="primary"
|
||||||
|
:appearance="filteredCategoryIds.includes(category.id) ? 'filled' : 'outline'"
|
||||||
|
size="sm"
|
||||||
@click="saveCategories(category.id)"
|
@click="saveCategories(category.id)"
|
||||||
:filled="filteredCategoryIds.includes(category.id)"
|
|
||||||
:icon="category.icon"
|
|
||||||
size="small"
|
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t(`contribution.category.description.${category.slug}`),
|
content: $t(`contribution.category.description.${category.slug}`),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon :name="category.icon" />
|
||||||
|
</template>
|
||||||
{{ $t(`contribution.category.name.${category.slug}`) }}
|
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</filter-menu-section>
|
</filter-menu-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||||
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
import SortCategories from '~/mixins/sortCategoriesMixin.js'
|
||||||
@ -42,6 +48,7 @@ import GetCategories from '~/mixins/getCategoriesMixin.js'
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilterMenuSection,
|
FilterMenuSection,
|
||||||
|
OsButton,
|
||||||
},
|
},
|
||||||
mixins: [SortCategories, GetCategories],
|
mixins: [SortCategories, GetCategories],
|
||||||
computed: {
|
computed: {
|
||||||
@ -69,7 +76,7 @@ export default {
|
|||||||
.category-filter-list {
|
.category-filter-list {
|
||||||
margin-left: $space-xx-small;
|
margin-left: $space-xx-small;
|
||||||
|
|
||||||
> .base-button {
|
> button {
|
||||||
margin-right: $space-xx-small;
|
margin-right: $space-xx-small;
|
||||||
margin-bottom: $space-xx-small;
|
margin-bottom: $space-xx-small;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,14 +45,14 @@ describe('mount', () => {
|
|||||||
|
|
||||||
// describe('mount', () => {
|
// describe('mount', () => {
|
||||||
// it('starts with all emotions button active', () => {
|
// it('starts with all emotions button active', () => {
|
||||||
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar .base-button')
|
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar button')
|
||||||
// expect(allEmotionsButton.attributes().class).toContain('--filled')
|
// expect(allEmotionsButton.attributes().class).toContain('--filled')
|
||||||
// })
|
// })
|
||||||
|
|
||||||
// describe('click on an "emotion-button" button', () => {
|
// describe('click on an "emotion-button" button', () => {
|
||||||
// it('calls TOGGLE_EMOTION when clicked', () => {
|
// it('calls TOGGLE_EMOTION when clicked', () => {
|
||||||
// const wrapper = Wrapper()
|
// const wrapper = Wrapper()
|
||||||
// happyEmotionButton = wrapper.findAll('.emotion-button > .base-button').at(1)
|
// happyEmotionButton = wrapper.findAll('.emotion-button > button').at(1)
|
||||||
// happyEmotionButton.trigger('click')
|
// happyEmotionButton.trigger('click')
|
||||||
// expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
|
// expect(mutations['posts/TOGGLE_EMOTION']).toHaveBeenCalledWith({}, 'happy')
|
||||||
// })
|
// })
|
||||||
@ -60,7 +60,7 @@ describe('mount', () => {
|
|||||||
// it('sets the attribute `src` to colorized image', () => {
|
// it('sets the attribute `src` to colorized image', () => {
|
||||||
// getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
|
// getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
|
||||||
// const wrapper = Wrapper()
|
// const wrapper = Wrapper()
|
||||||
// happyEmotionButton = wrapper.findAll('.emotion-button > .base-button').at(1)
|
// happyEmotionButton = wrapper.findAll('.emotion-button > button').at(1)
|
||||||
// const happyEmotionButtonImage = happyEmotionButton.find('img')
|
// const happyEmotionButtonImage = happyEmotionButton.find('img')
|
||||||
// expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
|
// expect(happyEmotionButtonImage.attributes().src).toEqual('/img/svg/emoji/happy_color.svg')
|
||||||
// })
|
// })
|
||||||
@ -70,7 +70,7 @@ describe('mount', () => {
|
|||||||
// it('when all button is clicked', async () => {
|
// it('when all button is clicked', async () => {
|
||||||
// getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
|
// getters['posts/filteredByEmotions'] = jest.fn(() => ['happy'])
|
||||||
// wrapper = await Wrapper()
|
// wrapper = await Wrapper()
|
||||||
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar .base-button')
|
// const allEmotionsButton = wrapper.find('.emotions-filter .sidebar button')
|
||||||
// allEmotionsButton.trigger('click')
|
// allEmotionsButton.trigger('click')
|
||||||
// expect(mutations['posts/RESET_EMOTIONS']).toHaveBeenCalledTimes(1)
|
// expect(mutations['posts/RESET_EMOTIONS']).toHaveBeenCalledTimes(1)
|
||||||
// })
|
// })
|
||||||
|
|||||||
@ -2,36 +2,41 @@
|
|||||||
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
|
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
|
||||||
<template #filter-list>
|
<template #filter-list>
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<base-button
|
<os-button
|
||||||
icon="check"
|
variant="primary"
|
||||||
:label="$t('filter-menu.ended.all.label')"
|
:appearance="!eventsEnded ? 'filled' : 'outline'"
|
||||||
:filled="!eventsEnded"
|
size="sm"
|
||||||
:title="$t('filter-menu.ended.all.hint')"
|
:title="$t('filter-menu.ended.all.hint')"
|
||||||
@click="toggleEventsEnded"
|
@click="toggleEventsEnded"
|
||||||
data-test="all-button"
|
data-test="all-button"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="check" />
|
||||||
|
</template>
|
||||||
{{ $t('filter-menu.ended.all.label') }}
|
{{ $t('filter-menu.ended.all.label') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</li>
|
</li>
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<base-button
|
<os-button
|
||||||
icon="calendar"
|
variant="primary"
|
||||||
:label="$t('filter-menu.ended.onlyEnded.label')"
|
:appearance="eventsEnded ? 'filled' : 'outline'"
|
||||||
:filled="eventsEnded"
|
size="sm"
|
||||||
:title="$t('filter-menu.ended.onlyEnded.hint')"
|
:title="$t('filter-menu.ended.onlyEnded.hint')"
|
||||||
@click="toggleEventsEnded"
|
@click="toggleEventsEnded"
|
||||||
data-test="not-ended-button"
|
data-test="not-ended-button"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="calendar" />
|
||||||
|
</template>
|
||||||
{{ $t('filter-menu.ended.onlyEnded.label') }}
|
{{ $t('filter-menu.ended.onlyEnded.label') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</filter-menu-section>
|
</filter-menu-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||||
|
|
||||||
@ -39,6 +44,7 @@ export default {
|
|||||||
name: 'EventsByFilter',
|
name: 'EventsByFilter',
|
||||||
components: {
|
components: {
|
||||||
FilterMenuSection,
|
FilterMenuSection,
|
||||||
|
OsButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|||||||
@ -39,15 +39,13 @@ describe('FilterMenu.vue', () => {
|
|||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
it('starts with dropdown button inactive', () => {
|
it('starts with dropdown button inactive', () => {
|
||||||
const dropdownButton = wrapper.find('.filter-menu .base-button')
|
expect(wrapper.find('.filter-menu button[data-appearance="ghost"]').exists()).toBe(true)
|
||||||
expect(dropdownButton.attributes().class).toContain('--ghost')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets dropdwon button attribute `filled` when a filter is applied', () => {
|
it('sets dropdown button attribute `filled` when a filter is applied', () => {
|
||||||
getters['posts/isActive'] = jest.fn(() => true)
|
getters['posts/isActive'] = jest.fn(() => true)
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
const dropdownButton = wrapper.find('.filter-menu .base-button')
|
expect(wrapper.find('.filter-menu button[data-appearance="filled"]').exists()).toBe(true)
|
||||||
expect(dropdownButton.attributes().class).toContain('--filled')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<dropdown ref="menu" placement="top-start" :offset="8" class="filter-menu">
|
<dropdown ref="menu" placement="top-start" :offset="8" class="filter-menu">
|
||||||
<base-button
|
<template #default="{ toggleMenu }">
|
||||||
slot="default"
|
<os-button
|
||||||
icon="filter"
|
variant="primary"
|
||||||
:filled="filterActive"
|
:appearance="filterActive ? 'filled' : 'ghost'"
|
||||||
:ghost="!filterActive"
|
:aria-label="$t('common.filter')"
|
||||||
slot-scope="{ toggleMenu }"
|
@click.prevent="toggleMenu()"
|
||||||
@click.prevent="toggleMenu()"
|
>
|
||||||
>
|
<template #icon>
|
||||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
<base-icon name="filter" />
|
||||||
</base-button>
|
</template>
|
||||||
|
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||||
|
</os-button>
|
||||||
|
</template>
|
||||||
<template #popover>
|
<template #popover>
|
||||||
<filter-menu-component />
|
<filter-menu-component />
|
||||||
</template>
|
</template>
|
||||||
@ -17,6 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import FilterMenuComponent from './FilterMenuComponent'
|
import FilterMenuComponent from './FilterMenuComponent'
|
||||||
@ -25,6 +29,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FilterMenuComponent,
|
FilterMenuComponent,
|
||||||
|
OsButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
placement: { type: String },
|
placement: { type: String },
|
||||||
|
|||||||
@ -44,19 +44,21 @@ describe('FollowingFilter', () => {
|
|||||||
const wrapper = Wrapper()
|
const wrapper = Wrapper()
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.following-filter .filter-list .follower-item .base-button')
|
.find('.following-filter .filter-list .follower-item button[data-appearance="filled"]')
|
||||||
.classes('--filled'),
|
.exists(),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
|
.find(
|
||||||
.classes('--filled'),
|
'.following-filter .filter-list .posts-in-my-groups-item button[data-appearance="filled"]',
|
||||||
|
)
|
||||||
|
.exists(),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('click "filter-by-followed" button', () => {
|
describe('click "filter-by-followed" button', () => {
|
||||||
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
|
it('calls TOGGLE_FILTER_BY_FOLLOWED', () => {
|
||||||
wrapper.find('.following-filter .filter-list .follower-item .base-button').trigger('click')
|
wrapper.find('.following-filter .filter-list .follower-item button').trigger('click')
|
||||||
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
|
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -64,7 +66,7 @@ describe('FollowingFilter', () => {
|
|||||||
describe('click "filter-by-my-groups" button', () => {
|
describe('click "filter-by-my-groups" button', () => {
|
||||||
it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => {
|
it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
|
.find('.following-filter .filter-list .posts-in-my-groups-item button')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled()
|
expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@ -72,9 +74,7 @@ describe('FollowingFilter', () => {
|
|||||||
describe('clears follower filter', () => {
|
describe('clears follower filter', () => {
|
||||||
it('when all button is clicked', async () => {
|
it('when all button is clicked', async () => {
|
||||||
wrapper = await Wrapper()
|
wrapper = await Wrapper()
|
||||||
const clearFollowerButton = wrapper.find(
|
const clearFollowerButton = wrapper.find('.following-filter .item-all-follower button')
|
||||||
'.following-filter .item-all-follower .base-button',
|
|
||||||
)
|
|
||||||
clearFollowerButton.trigger('click')
|
clearFollowerButton.trigger('click')
|
||||||
expect(mutations['posts/RESET_FOLLOWERS_FILTER']).toHaveBeenCalledTimes(1)
|
expect(mutations['posts/RESET_FOLLOWERS_FILTER']).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,40 +6,48 @@
|
|||||||
>
|
>
|
||||||
<template #filter-follower>
|
<template #filter-follower>
|
||||||
<div class="item item-all-follower">
|
<div class="item item-all-follower">
|
||||||
<base-button
|
<os-button
|
||||||
:filled="!filteredByUsersFollowed && !filteredByPostsInMyGroups"
|
variant="primary"
|
||||||
:label="$t('filter-menu.all')"
|
:appearance="
|
||||||
icon="check"
|
!filteredByUsersFollowed && !filteredByPostsInMyGroups ? 'filled' : 'outline'
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
@click="setResetFollowers"
|
@click="setResetFollowers"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="check" />
|
||||||
|
</template>
|
||||||
{{ $t('filter-menu.all') }}
|
{{ $t('filter-menu.all') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="follower-filter-list">
|
<div class="follower-filter-list">
|
||||||
<li class="item follower-item">
|
<li class="item follower-item">
|
||||||
<base-button
|
<os-button
|
||||||
icon="user-plus"
|
variant="primary"
|
||||||
:label="$t('filter-menu.following')"
|
:appearance="filteredByUsersFollowed ? 'filled' : 'outline'"
|
||||||
:filled="filteredByUsersFollowed"
|
size="sm"
|
||||||
:title="$t('filter-menu.following')"
|
:title="$t('filter-menu.following')"
|
||||||
@click="toggleFilteredByFollowed(currentUser.id)"
|
@click="toggleFilteredByFollowed(currentUser.id)"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="user-plus" />
|
||||||
|
</template>
|
||||||
{{ $t('filter-menu.following') }}
|
{{ $t('filter-menu.following') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</li>
|
</li>
|
||||||
<li class="item posts-in-my-groups-item">
|
<li class="item posts-in-my-groups-item">
|
||||||
<base-button
|
<os-button
|
||||||
icon="users"
|
variant="primary"
|
||||||
:label="$t('filter-menu.my-groups')"
|
:appearance="filteredByPostsInMyGroups ? 'filled' : 'outline'"
|
||||||
:filled="filteredByPostsInMyGroups"
|
size="sm"
|
||||||
:title="$t('contribution.filterMyGroups')"
|
:title="$t('contribution.filterMyGroups')"
|
||||||
@click="toggleFilteredByMyGroups()"
|
@click="toggleFilteredByMyGroups()"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="users" />
|
||||||
|
</template>
|
||||||
{{ $t('contribution.filterMyGroups') }}
|
{{ $t('contribution.filterMyGroups') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -47,6 +55,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||||
|
|
||||||
@ -54,6 +63,7 @@ export default {
|
|||||||
name: 'FollowingFilter',
|
name: 'FollowingFilter',
|
||||||
components: {
|
components: {
|
||||||
FilterMenuSection,
|
FilterMenuSection,
|
||||||
|
OsButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@ -81,7 +91,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin-left: $space-xx-small;
|
margin-left: $space-xx-small;
|
||||||
|
|
||||||
& .base-button {
|
& button {
|
||||||
margin-right: $space-xx-small;
|
margin-right: $space-xx-small;
|
||||||
margin-bottom: $space-xx-small;
|
margin-bottom: $space-xx-small;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<span>
|
<span class="header-button-wrapper">
|
||||||
<base-button
|
<os-button class="my-filter-button" variant="primary" appearance="filled" @click="clickButton">
|
||||||
class="my-filter-button my-filter-button-selected"
|
|
||||||
right
|
|
||||||
@click="clickButton"
|
|
||||||
filled
|
|
||||||
>
|
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</base-button>
|
</os-button>
|
||||||
<base-button
|
<os-button
|
||||||
class="filter-remove"
|
class="filter-remove"
|
||||||
@click="clickRemove"
|
variant="primary"
|
||||||
icon="close"
|
appearance="filled"
|
||||||
:title="titleRemove"
|
|
||||||
size="small"
|
|
||||||
circle
|
circle
|
||||||
filled
|
size="sm"
|
||||||
/>
|
:title="titleRemove"
|
||||||
|
:aria-label="titleRemove"
|
||||||
|
@click.stop="clickRemove"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HeaderButton',
|
name: 'HeaderButton',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -43,14 +46,21 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.my-filter-button-selected {
|
.header-button-wrapper {
|
||||||
padding-right: 36px;
|
display: inline-flex;
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
.base-button.filter-remove {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: -37px;
|
|
||||||
top: -5px;
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
||||||
|
> .my-filter-button {
|
||||||
|
padding-right: 36px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .filter-remove {
|
||||||
|
position: absolute !important;
|
||||||
|
right: 4px !important;
|
||||||
|
top: 50% !important;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -48,7 +48,7 @@ describe('mount', () => {
|
|||||||
|
|
||||||
// describe('mount', () => {
|
// describe('mount', () => {
|
||||||
// it('starts with all categories button active', () => {
|
// it('starts with all categories button active', () => {
|
||||||
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar .base-button')
|
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar button')
|
||||||
// expect(allLanguagesButton.attributes().class).toContain('--filled')
|
// expect(allLanguagesButton.attributes().class).toContain('--filled')
|
||||||
// })
|
// })
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ describe('mount', () => {
|
|||||||
// getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
|
// getters['posts/filteredLanguageCodes'] = jest.fn(() => ['es'])
|
||||||
// const wrapper = Wrapper()
|
// const wrapper = Wrapper()
|
||||||
// spanishButton = wrapper
|
// spanishButton = wrapper
|
||||||
// .findAll('.languages-filter .item .base-button')
|
// .findAll('.languages-filter .item button')
|
||||||
// .at(languages.findIndex((l) => l.code === 'es'))
|
// .at(languages.findIndex((l) => l.code === 'es'))
|
||||||
// expect(spanishButton.attributes().class).toContain('--filled')
|
// expect(spanishButton.attributes().class).toContain('--filled')
|
||||||
// })
|
// })
|
||||||
@ -65,7 +65,7 @@ describe('mount', () => {
|
|||||||
// it('calls TOGGLE_LANGUAGE when clicked', () => {
|
// it('calls TOGGLE_LANGUAGE when clicked', () => {
|
||||||
// const wrapper = Wrapper()
|
// const wrapper = Wrapper()
|
||||||
// englishButton = wrapper
|
// englishButton = wrapper
|
||||||
// .findAll('.languages-filter .item .base-button')
|
// .findAll('.languages-filter .item button')
|
||||||
// .at(languages.findIndex((l) => l.code === 'en'))
|
// .at(languages.findIndex((l) => l.code === 'en'))
|
||||||
// englishButton.trigger('click')
|
// englishButton.trigger('click')
|
||||||
// expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
|
// expect(mutations['posts/TOGGLE_LANGUAGE']).toHaveBeenCalledWith({}, 'en')
|
||||||
@ -76,7 +76,7 @@ describe('mount', () => {
|
|||||||
// it('when all button is clicked', async () => {
|
// it('when all button is clicked', async () => {
|
||||||
// getters['posts/filteredLanguageCodes'] = jest.fn(() => ['en'])
|
// getters['posts/filteredLanguageCodes'] = jest.fn(() => ['en'])
|
||||||
// wrapper = await Wrapper()
|
// wrapper = await Wrapper()
|
||||||
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar .base-button')
|
// const allLanguagesButton = wrapper.find('.languages-filter .sidebar button')
|
||||||
// allLanguagesButton.trigger('click')
|
// allLanguagesButton.trigger('click')
|
||||||
// expect(mutations['posts/RESET_LANGUAGES']).toHaveBeenCalledTimes(1)
|
// expect(mutations['posts/RESET_LANGUAGES']).toHaveBeenCalledTimes(1)
|
||||||
// })
|
// })
|
||||||
|
|||||||
@ -38,16 +38,20 @@ describe('OrderByFilter', () => {
|
|||||||
it('sets "newest-button" attribute `filled`', () => {
|
it('sets "newest-button" attribute `filled`', () => {
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.order-by-filter .filter-list .base-button[data-test="newest-button"]')
|
.find(
|
||||||
.classes('--filled'),
|
'.order-by-filter .filter-list button[data-test="newest-button"][data-appearance="filled"]',
|
||||||
|
)
|
||||||
|
.exists(),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('don\'t sets "oldest-button" attribute `filled`', () => {
|
it('don\'t sets "oldest-button" attribute `filled`', () => {
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]')
|
.find(
|
||||||
.classes('--filled'),
|
'.order-by-filter .filter-list button[data-test="oldest-button"][data-appearance="filled"]',
|
||||||
|
)
|
||||||
|
.exists(),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -61,16 +65,20 @@ describe('OrderByFilter', () => {
|
|||||||
it('don\'t sets "newest-button" attribute `filled`', () => {
|
it('don\'t sets "newest-button" attribute `filled`', () => {
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.order-by-filter .filter-list .base-button[data-test="newest-button"]')
|
.find(
|
||||||
.classes('--filled'),
|
'.order-by-filter .filter-list button[data-test="newest-button"][data-appearance="filled"]',
|
||||||
|
)
|
||||||
|
.exists(),
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets "oldest-button" attribute `filled`', () => {
|
it('sets "oldest-button" attribute `filled`', () => {
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]')
|
.find(
|
||||||
.classes('--filled'),
|
'.order-by-filter .filter-list button[data-test="oldest-button"][data-appearance="filled"]',
|
||||||
|
)
|
||||||
|
.exists(),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -78,7 +86,7 @@ describe('OrderByFilter', () => {
|
|||||||
describe('click "newest-button"', () => {
|
describe('click "newest-button"', () => {
|
||||||
it('calls TOGGLE_ORDER with "sortDate_desc"', () => {
|
it('calls TOGGLE_ORDER with "sortDate_desc"', () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('.order-by-filter .filter-list .base-button[data-test="newest-button"]')
|
.find('.order-by-filter .filter-list button[data-test="newest-button"]')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'sortDate_desc')
|
expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'sortDate_desc')
|
||||||
})
|
})
|
||||||
@ -87,7 +95,7 @@ describe('OrderByFilter', () => {
|
|||||||
describe('click "oldest-button"', () => {
|
describe('click "oldest-button"', () => {
|
||||||
it('calls TOGGLE_ORDER with "sortDate_asc"', () => {
|
it('calls TOGGLE_ORDER with "sortDate_asc"', () => {
|
||||||
wrapper
|
wrapper
|
||||||
.find('.order-by-filter .filter-list .base-button[data-test="oldest-button"]')
|
.find('.order-by-filter .filter-list button[data-test="oldest-button"]')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'sortDate_asc')
|
expect(mutations['posts/TOGGLE_ORDER']).toHaveBeenCalledWith({}, 'sortDate_asc')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,36 +2,41 @@
|
|||||||
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
|
<filter-menu-section class="order-by-filter" :title="sectionTitle" :divider="false">
|
||||||
<template #filter-list>
|
<template #filter-list>
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<base-button
|
<os-button
|
||||||
icon="sort-amount-asc"
|
variant="primary"
|
||||||
:label="buttonLabel('desc')"
|
:appearance="orderBy === orderedDesc ? 'filled' : 'outline'"
|
||||||
:filled="orderBy === orderedDesc"
|
size="sm"
|
||||||
:title="buttonTitle('desc')"
|
:title="buttonTitle('desc')"
|
||||||
@click="toggleOrder(orderedDesc)"
|
@click="toggleOrder(orderedDesc)"
|
||||||
data-test="newest-button"
|
data-test="newest-button"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="sort-amount-asc" />
|
||||||
|
</template>
|
||||||
{{ buttonLabel('desc') }}
|
{{ buttonLabel('desc') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</li>
|
</li>
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<base-button
|
<os-button
|
||||||
icon="sort-amount-desc"
|
variant="primary"
|
||||||
:label="buttonLabel('asc')"
|
:appearance="orderBy === orderedAsc ? 'filled' : 'outline'"
|
||||||
:filled="orderBy === orderedAsc"
|
size="sm"
|
||||||
:title="buttonTitle('asc')"
|
:title="buttonTitle('asc')"
|
||||||
@click="toggleOrder(orderedAsc)"
|
@click="toggleOrder(orderedAsc)"
|
||||||
data-test="oldest-button"
|
data-test="oldest-button"
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="sort-amount-desc" />
|
||||||
|
</template>
|
||||||
{{ buttonLabel('asc') }}
|
{{ buttonLabel('asc') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</filter-menu-section>
|
</filter-menu-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
|
||||||
|
|
||||||
@ -39,6 +44,7 @@ export default {
|
|||||||
name: 'OrderByFilter',
|
name: 'OrderByFilter',
|
||||||
components: {
|
components: {
|
||||||
FilterMenuSection,
|
FilterMenuSection,
|
||||||
|
OsButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|||||||
@ -1,19 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link to="/groups">
|
<nuxt-link to="/groups">
|
||||||
<base-button
|
<os-button
|
||||||
icon="users"
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
ghost
|
:aria-label="$t('header.groups.tooltip')"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('header.groups.tooltip'),
|
content: $t('header.groups.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="users" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {}
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { OsButton },
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -133,11 +133,17 @@
|
|||||||
<!-- submit -->
|
<!-- submit -->
|
||||||
<ds-space margin-top="large">
|
<ds-space margin-top="large">
|
||||||
<nuxt-link to="/groups">
|
<nuxt-link to="/groups">
|
||||||
<os-button>{{ $t('actions.cancel') }}</os-button>
|
<os-button variant="default" appearance="filled">{{ $t('actions.cancel') }}</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<ds-button type="submit" icon="save" primary :disabled="checkFormError(errors)" fill>
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
type="submit"
|
||||||
|
:disabled="checkFormError(errors)"
|
||||||
|
>
|
||||||
|
<template #icon><base-icon name="save" /></template>
|
||||||
{{ update ? $t('group.update') : $t('group.save') }}
|
{{ update ? $t('group.update') : $t('group.save') }}
|
||||||
</ds-button>
|
</os-button>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
</template>
|
</template>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ describe('HashtagsFilter.vue', () => {
|
|||||||
|
|
||||||
describe('click clear search button', () => {
|
describe('click clear search button', () => {
|
||||||
it('emits clearSearch', () => {
|
it('emits clearSearch', () => {
|
||||||
wrapper.find('.base-button').trigger('click')
|
wrapper.find('button[data-test="clear-search-button"]').trigger('click')
|
||||||
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
expect(wrapper.emitted().clearSearch).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,17 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<base-card class="hashtags-filter">
|
<base-card class="hashtags-filter">
|
||||||
<h2>{{ $t('hashtags-filter.hashtag-search', { hashtag }) }}</h2>
|
<h2>{{ $t('hashtags-filter.hashtag-search', { hashtag }) }}</h2>
|
||||||
<base-button
|
<os-button
|
||||||
icon="close"
|
data-test="clear-search-button"
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
:title="this.$t('hashtags-filter.clearSearch')"
|
:title="$t('hashtags-filter.clearSearch')"
|
||||||
|
:aria-label="$t('hashtags-filter.clearSearch')"
|
||||||
@click="clearSearch"
|
@click="clearSearch"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</base-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
hashtag: {
|
hashtag: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@ -139,12 +139,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</client-only>
|
</client-only>
|
||||||
<!-- hamburger menu -->
|
<!-- hamburger menu -->
|
||||||
<base-button
|
<os-button
|
||||||
icon="bars"
|
variant="primary"
|
||||||
@click="toggleMobileMenuView"
|
:appearance="toggleMobileMenu ? 'filled' : 'outline'"
|
||||||
circle
|
circle
|
||||||
class="hamburger-button"
|
class="hamburger-button"
|
||||||
/>
|
:aria-label="$t('site.navigation')"
|
||||||
|
@click="toggleMobileMenuView"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="bars" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
<!-- search, filter -->
|
<!-- search, filter -->
|
||||||
@ -274,6 +280,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import isEmpty from 'lodash/isEmpty'
|
import isEmpty from 'lodash/isEmpty'
|
||||||
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
||||||
@ -298,6 +305,7 @@ import GetCategories from '~/mixins/getCategoriesMixin.js'
|
|||||||
export default {
|
export default {
|
||||||
mixins: [GetCategories],
|
mixins: [GetCategories],
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
AvatarMenu,
|
AvatarMenu,
|
||||||
ChatNotificationMenu,
|
ChatNotificationMenu,
|
||||||
CustomButton,
|
CustomButton,
|
||||||
@ -359,6 +367,9 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('scroll', this.handleScroll)
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<dropdown class="invite-button" offset="8" :placement="placement" noMouseLeaveClosing>
|
<dropdown class="invite-button" offset="8" :placement="placement" noMouseLeaveClosing>
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<base-button
|
<os-button
|
||||||
icon="user-plus"
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
ghost
|
:aria-label="$t('invite-codes.button.tooltip')"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('invite-codes.button.tooltip'),
|
content: $t('invite-codes.button.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
@click.prevent="toggleMenu"
|
@click.prevent="toggleMenu"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="user-plus" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
<template #popover>
|
<template #popover>
|
||||||
<div class="invite-list">
|
<div class="invite-list">
|
||||||
@ -31,6 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
||||||
@ -38,6 +44,7 @@ import { generatePersonalInviteCode, invalidateInviteCode } from '~/graphql/Invi
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
InvitationList,
|
InvitationList,
|
||||||
},
|
},
|
||||||
@ -66,7 +73,7 @@ export default {
|
|||||||
},
|
},
|
||||||
update: (_, { data: { generatePersonalInviteCode } }) => {
|
update: (_, { data: { generatePersonalInviteCode } }) => {
|
||||||
this.setCurrentUser({
|
this.setCurrentUser({
|
||||||
...this.currentUser,
|
...this.user,
|
||||||
inviteCodes: [...this.user.inviteCodes, generatePersonalInviteCode],
|
inviteCodes: [...this.user.inviteCodes, generatePersonalInviteCode],
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -85,7 +92,7 @@ export default {
|
|||||||
},
|
},
|
||||||
update: (_, { data: { _invalidateInviteCode } }) => {
|
update: (_, { data: { _invalidateInviteCode } }) => {
|
||||||
this.setCurrentUser({
|
this.setCurrentUser({
|
||||||
...this.currentUser,
|
...this.user,
|
||||||
inviteCodes: this.user.inviteCodes.map((inviteCode) => ({
|
inviteCodes: this.user.inviteCodes.map((inviteCode) => ({
|
||||||
...inviteCode,
|
...inviteCode,
|
||||||
isValid: inviteCode.code === code ? false : inviteCode.isValid,
|
isValid: inviteCode.code === code ? false : inviteCode.isValid,
|
||||||
@ -103,10 +110,6 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.invite-button {
|
|
||||||
color: $color-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-list {
|
.invite-list {
|
||||||
max-width: min(400px, 90vw);
|
max-width: min(400px, 90vw);
|
||||||
padding: $space-small;
|
padding: $space-small;
|
||||||
|
|||||||
@ -37,7 +37,7 @@ describe('LoginButton.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('open popup', () => {
|
it('open popup', () => {
|
||||||
wrapper.find('.base-button').trigger('click')
|
wrapper.find('[data-test="login-btn"]').trigger('click')
|
||||||
expect(wrapper.find('.login-button').exists()).toBe(true)
|
expect(wrapper.find('.login-button').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<dropdown class="login-button" offset="8" :placement="placement">
|
<dropdown class="login-button" offset="8" :placement="placement">
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<base-button icon="sign-in" circle ghost @click.prevent="toggleMenu" />
|
<os-button
|
||||||
|
data-test="login-btn"
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
|
circle
|
||||||
|
:aria-label="$t('login.login')"
|
||||||
|
@click.prevent="toggleMenu"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="sign-in" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
<template #popover>
|
<template #popover>
|
||||||
<div class="login-button-menu-popover">
|
<div class="login-button-menu-popover">
|
||||||
@ -15,10 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -36,9 +36,17 @@
|
|||||||
<nuxt-link to="/password-reset/request">
|
<nuxt-link to="/password-reset/request">
|
||||||
{{ $t('login.forgotPassword') }}
|
{{ $t('login.forgotPassword') }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<base-button :loading="pending" filled name="submit" type="submit" icon="sign-in">
|
<os-button
|
||||||
|
:loading="pending"
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
full-width
|
||||||
|
name="submit"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<template #icon><base-icon name="sign-in" /></template>
|
||||||
{{ $t('login.login') }}
|
{{ $t('login.login') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
<p>
|
<p>
|
||||||
{{ $t('login.no-account') }}
|
{{ $t('login.no-account') }}
|
||||||
<nuxt-link to="/registration">{{ $t('login.register') }}</nuxt-link>
|
<nuxt-link to="/registration">{{ $t('login.register') }}</nuxt-link>
|
||||||
@ -58,12 +66,14 @@ import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParams
|
|||||||
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
|
||||||
import Logo from '~/components/Logo/Logo'
|
import Logo from '~/components/Logo/Logo'
|
||||||
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
import ShowPassword from '../ShowPassword/ShowPassword.vue'
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapGetters, mapMutations } from 'vuex'
|
import { mapGetters, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
LocaleSwitch,
|
LocaleSwitch,
|
||||||
Logo,
|
Logo,
|
||||||
|
OsButton,
|
||||||
PageParamsLink,
|
PageParamsLink,
|
||||||
ShowPassword,
|
ShowPassword,
|
||||||
},
|
},
|
||||||
@ -134,9 +144,7 @@ export default {
|
|||||||
max-width: 620px;
|
max-width: 620px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
.base-button {
|
button[type='submit'] {
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: $space-large;
|
margin-top: $space-large;
|
||||||
margin-bottom: $space-small;
|
margin-bottom: $space-small;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link to="/map">
|
<nuxt-link to="/map">
|
||||||
<base-button
|
<os-button
|
||||||
class="map-button"
|
class="map-button"
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
ghost
|
:aria-label="$t('header.map.tooltip')"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('header.map.tooltip'),
|
content: $t('header.map.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<base-icon name="globe-detailed" size="large" />
|
<template #icon>
|
||||||
</base-button>
|
<base-icon name="globe-detailed" size="large" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MapButton',
|
name: 'MapButton',
|
||||||
|
components: { OsButton },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.map-button {
|
.map-button {
|
||||||
margin-left: 4px;
|
margin-left: 3px;
|
||||||
margin-right: 4px;
|
margin-right: 3px;
|
||||||
|
|
||||||
|
.base-icon > .svg.--large {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,36 +10,44 @@
|
|||||||
<p v-html="message" />
|
<p v-html="message" />
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<base-button
|
<os-button
|
||||||
class="cancel"
|
class="cancel"
|
||||||
:danger="!modalData.buttons.confirm.danger"
|
:variant="!modalData.buttons.confirm.danger ? 'danger' : 'primary'"
|
||||||
:icon="modalData.buttons.cancel.icon"
|
appearance="outline"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
data-test="cancel-button"
|
data-test="cancel-button"
|
||||||
>
|
>
|
||||||
|
<template v-if="modalData.buttons.cancel.icon" #icon>
|
||||||
|
<base-icon :name="modalData.buttons.cancel.icon" />
|
||||||
|
</template>
|
||||||
{{ $t(modalData.buttons.cancel.textIdent) }}
|
{{ $t(modalData.buttons.cancel.textIdent) }}
|
||||||
</base-button>
|
</os-button>
|
||||||
|
|
||||||
<base-button
|
<os-button
|
||||||
:danger="modalData.buttons.confirm.danger"
|
|
||||||
class="confirm"
|
class="confirm"
|
||||||
:icon="modalData.buttons.confirm.icon"
|
:variant="modalData.buttons.confirm.danger ? 'danger' : 'primary'"
|
||||||
|
appearance="filled"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
data-test="confirm-button"
|
data-test="confirm-button"
|
||||||
>
|
>
|
||||||
|
<template v-if="modalData.buttons.confirm.icon" #icon>
|
||||||
|
<base-icon :name="modalData.buttons.confirm.icon" />
|
||||||
|
</template>
|
||||||
{{ $t(modalData.buttons.confirm.textIdent) }}
|
{{ $t(modalData.buttons.confirm.textIdent) }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
</ds-modal>
|
</ds-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConfirmModal',
|
name: 'ConfirmModal',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
SweetalertIcon,
|
SweetalertIcon,
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|||||||
@ -40,9 +40,16 @@
|
|||||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||||
{{ $t('actions.cancel') }}
|
{{ $t('actions.cancel') }}
|
||||||
</os-button>
|
</os-button>
|
||||||
<base-button danger filled class="confirm" icon="exclamation-circle" @click="openModal">
|
<os-button
|
||||||
|
variant="danger"
|
||||||
|
appearance="filled"
|
||||||
|
class="confirm"
|
||||||
|
:loading="loading"
|
||||||
|
@click="openModal"
|
||||||
|
>
|
||||||
|
<template #icon><base-icon name="exclamation-circle" /></template>
|
||||||
{{ $t('settings.deleteUserAccount.name') }}
|
{{ $t('settings.deleteUserAccount.name') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
</ds-modal>
|
</ds-modal>
|
||||||
</template>
|
</template>
|
||||||
@ -162,10 +169,10 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ds-modal {
|
.delete-user-modal.ds-modal {
|
||||||
max-width: 700px !important;
|
max-width: 700px !important;
|
||||||
}
|
}
|
||||||
.hc-modal-success {
|
.delete-user-modal .hc-modal-success {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -177,7 +184,7 @@ export default {
|
|||||||
z-index: $z-index-modal;
|
z-index: $z-index-modal;
|
||||||
border-radius: $border-radius-x-large;
|
border-radius: $border-radius-x-large;
|
||||||
}
|
}
|
||||||
.bold {
|
.delete-user-modal .bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -7,9 +7,16 @@
|
|||||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||||
{{ $t('disable.cancel') }}
|
{{ $t('disable.cancel') }}
|
||||||
</os-button>
|
</os-button>
|
||||||
<base-button danger filled class="confirm" icon="exclamation-circle" @click="confirm">
|
<os-button
|
||||||
|
variant="danger"
|
||||||
|
appearance="filled"
|
||||||
|
class="confirm"
|
||||||
|
:loading="loading"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
<template #icon><base-icon name="exclamation-circle" /></template>
|
||||||
{{ $t('disable.submit') }}
|
{{ $t('disable.submit') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
</ds-modal>
|
</ds-modal>
|
||||||
</template>
|
</template>
|
||||||
@ -52,6 +59,7 @@ export default {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
async confirm() {
|
async confirm() {
|
||||||
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
||||||
// await this.modalData.buttons.confirm.callback()
|
// await this.modalData.buttons.confirm.callback()
|
||||||
@ -73,6 +81,8 @@ export default {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
|
<ds-modal class="report-modal" :title="title" :is-open="isOpen" @cancel="cancel">
|
||||||
<transition name="ds-transition-fade">
|
<transition name="ds-transition-fade">
|
||||||
<ds-flex v-if="success" class="hc-modal-success" centered>
|
<ds-flex v-if="success" class="hc-modal-success" centered>
|
||||||
<sweetalert-icon icon="success" />
|
<sweetalert-icon icon="success" />
|
||||||
@ -29,26 +29,32 @@
|
|||||||
</small>
|
</small>
|
||||||
<ds-space />
|
<ds-space />
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<base-button class="cancel" icon="close" @click="cancel">
|
<os-button class="cancel" variant="primary" appearance="outline" @click="cancel">
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
{{ $t('report.cancel') }}
|
{{ $t('report.cancel') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
|
|
||||||
<base-button
|
<os-button
|
||||||
danger
|
|
||||||
filled
|
|
||||||
class="confirm"
|
class="confirm"
|
||||||
icon="exclamation-circle"
|
variant="danger"
|
||||||
|
appearance="filled"
|
||||||
:disabled="!form.reasonCategory"
|
:disabled="!form.reasonCategory"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="exclamation-circle" />
|
||||||
|
</template>
|
||||||
{{ $t('report.submit') }}
|
{{ $t('report.submit') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
</ds-modal>
|
</ds-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
import { reportMutation } from '~/graphql/Moderation.js'
|
import { reportMutation } from '~/graphql/Moderation.js'
|
||||||
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
|
import { valuesReasonCategoryOptions } from '~/constants/modals.js'
|
||||||
@ -57,6 +63,7 @@ import validReport from '~/components/utils/ReportModal'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ReportModal',
|
name: 'ReportModal',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
SweetalertIcon,
|
SweetalertIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -159,26 +166,27 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ds-modal {
|
.report-modal.ds-modal {
|
||||||
max-width: 600px !important;
|
width: 700px !important;
|
||||||
|
max-width: 700px !important;
|
||||||
}
|
}
|
||||||
.ds-radio-option {
|
.report-modal .ds-radio-option {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
.ds-radio-option-label {
|
.report-modal .ds-radio-option-label {
|
||||||
margin: 5px 20px 5px 5px !important;
|
margin: 5px 20px 5px 5px !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
.reason-description {
|
.report-modal .reason-description {
|
||||||
margin-top: $space-x-small !important;
|
margin-top: $space-x-small !important;
|
||||||
margin-bottom: $space-xx-small !important;
|
margin-bottom: $space-xx-small !important;
|
||||||
}
|
}
|
||||||
.smallTag {
|
.report-modal .smallTag {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 90%;
|
left: 90%;
|
||||||
}
|
}
|
||||||
.hc-modal-success {
|
.report-modal .hc-modal-success {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -4,27 +4,36 @@
|
|||||||
class="notifications-menu"
|
class="notifications-menu"
|
||||||
:to="{ name: 'notifications' }"
|
:to="{ name: 'notifications' }"
|
||||||
>
|
>
|
||||||
<base-button
|
<os-button
|
||||||
icon="bell"
|
variant="primary"
|
||||||
ghost
|
appearance="ghost"
|
||||||
circle
|
|
||||||
v-tooltip="{
|
|
||||||
content: $t('header.notifications.tooltip'),
|
|
||||||
placement: 'bottom-start',
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</nuxt-link>
|
|
||||||
<nuxt-link v-else-if="noMenu" class="notifications-menu" :to="{ name: 'notifications' }">
|
|
||||||
<base-button
|
|
||||||
ghost
|
|
||||||
circle
|
circle
|
||||||
|
:aria-label="$t('header.notifications.tooltip')"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('header.notifications.tooltip'),
|
content: $t('header.notifications.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
<template #icon>
|
||||||
</base-button>
|
<base-icon name="bell" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link v-else-if="noMenu" class="notifications-menu" :to="{ name: 'notifications' }">
|
||||||
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
|
circle
|
||||||
|
:aria-label="$t('header.notifications.tooltip')"
|
||||||
|
v-tooltip="{
|
||||||
|
content: $t('header.notifications.tooltip'),
|
||||||
|
placement: 'bottom-start',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<dropdown
|
<dropdown
|
||||||
v-else
|
v-else
|
||||||
@ -35,17 +44,21 @@
|
|||||||
ref="dropdown"
|
ref="dropdown"
|
||||||
>
|
>
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<base-button
|
<os-button
|
||||||
ghost
|
variant="primary"
|
||||||
|
appearance="ghost"
|
||||||
circle
|
circle
|
||||||
|
:aria-label="$t('header.notifications.tooltip')"
|
||||||
v-tooltip="{
|
v-tooltip="{
|
||||||
content: $t('header.notifications.tooltip'),
|
content: $t('header.notifications.tooltip'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
}"
|
}"
|
||||||
@click="toggleMenu"
|
@click="toggleMenu"
|
||||||
>
|
>
|
||||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
<template #icon>
|
||||||
</base-button>
|
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
<template #popover="{ closeMenu }">
|
<template #popover="{ closeMenu }">
|
||||||
<ds-flex class="notifications-link-container">
|
<ds-flex class="notifications-link-container">
|
||||||
|
|||||||
@ -41,10 +41,6 @@ describe('ChangePassword.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('validations', () => {
|
describe('validations', () => {
|
||||||
it('invalid', () => {
|
|
||||||
expect(wrapper.vm.disabled).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('old password and new password', () => {
|
describe('old password and new password', () => {
|
||||||
describe('match', () => {
|
describe('match', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -52,10 +48,6 @@ describe('ChangePassword.vue', () => {
|
|||||||
wrapper.find('input#password').setValue('some secret')
|
wrapper.find('input#password').setValue('some secret')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('invalid', () => {
|
|
||||||
expect(wrapper.vm.disabled).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it.skip('displays a warning', () => {
|
it.skip('displays a warning', () => {
|
||||||
const calls = mocks.validate.mock.calls
|
const calls = mocks.validate.mock.calls
|
||||||
const expected = [['change-password.validations.old-and-new-password-match']]
|
const expected = [['change-password.validations.old-and-new-password-match']]
|
||||||
|
|||||||
@ -24,15 +24,22 @@
|
|||||||
/>
|
/>
|
||||||
<password-strength :password="formData.password" />
|
<password-strength :password="formData.password" />
|
||||||
<ds-space margin-top="base">
|
<ds-space margin-top="base">
|
||||||
<base-button :loading="loading" :disabled="errors" filled type="submit">
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!!errors"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
{{ $t('settings.security.change-password.button') }}
|
{{ $t('settings.security.change-password.button') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
</template>
|
</template>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
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'
|
||||||
@ -40,6 +47,7 @@ import PasswordForm from '~/components/utils/PasswordFormHelper'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ChangePassword',
|
name: 'ChangePassword',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
PasswordStrength,
|
PasswordStrength,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -58,7 +66,6 @@ export default {
|
|||||||
...passwordForm.formSchema,
|
...passwordForm.formSchema,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
disabled: true,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@ -24,9 +24,15 @@
|
|||||||
/>
|
/>
|
||||||
<password-strength :password="formData.password" />
|
<password-strength :password="formData.password" />
|
||||||
<ds-space margin-top="base" margin-bottom="xxx-small">
|
<ds-space margin-top="base" margin-bottom="xxx-small">
|
||||||
<base-button :loading="$apollo.loading" :disabled="errors" filled type="submit">
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
|
:loading="$apollo.loading"
|
||||||
|
:disabled="!!errors"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
{{ $t('settings.security.change-password.button') }}
|
{{ $t('settings.security.change-password.button') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
</template>
|
</template>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
@ -61,6 +67,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import emails from '../../constants/emails.js'
|
import emails from '../../constants/emails.js'
|
||||||
import PasswordStrength from '../Password/Strength'
|
import PasswordStrength from '../Password/Strength'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
@ -69,6 +76,7 @@ import PasswordForm from '~/components/utils/PasswordFormHelper'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
SweetalertIcon,
|
SweetalertIcon,
|
||||||
PasswordStrength,
|
PasswordStrength,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,16 +20,15 @@
|
|||||||
<ds-space margin-botton="large">
|
<ds-space margin-botton="large">
|
||||||
<ds-text align="left">{{ $t('components.password-reset.request.form.description') }}</ds-text>
|
<ds-text align="left">{{ $t('components.password-reset.request.form.description') }}</ds-text>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<base-button
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:loading="$apollo.loading"
|
:loading="$apollo.loading"
|
||||||
filled
|
|
||||||
padding
|
|
||||||
name="submit"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{{ $t('components.password-reset.request.form.submit') }}
|
{{ $t('components.password-reset.request.form.submit') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@ -43,11 +42,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
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'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
SweetalertIcon,
|
SweetalertIcon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@ -30,15 +30,15 @@
|
|||||||
model="email"
|
model="email"
|
||||||
name="email"
|
name="email"
|
||||||
/>
|
/>
|
||||||
<base-button
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="filled"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:loading="$apollo.loading"
|
:loading="$apollo.loading"
|
||||||
filled
|
|
||||||
name="submit"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{{ $t('components.registration.signup.form.submit') }}
|
{{ $t('components.registration.signup.form.submit') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
@ -62,6 +62,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import gql from 'graphql-tag'
|
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'
|
||||||
@ -77,6 +78,7 @@ export const SignupMutation = gql`
|
|||||||
export default {
|
export default {
|
||||||
name: 'Signup',
|
name: 'Signup',
|
||||||
components: {
|
components: {
|
||||||
|
OsButton,
|
||||||
SweetalertIcon,
|
SweetalertIcon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -7,9 +7,18 @@
|
|||||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||||
{{ $t('release.cancel') }}
|
{{ $t('release.cancel') }}
|
||||||
</os-button>
|
</os-button>
|
||||||
<base-button danger filled class="confirm" icon="exclamation-circle" @click="confirm">
|
<os-button
|
||||||
|
variant="danger"
|
||||||
|
appearance="filled"
|
||||||
|
class="confirm"
|
||||||
|
:loading="loading"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="exclamation-circle" />
|
||||||
|
</template>
|
||||||
{{ $t('release.submit') }}
|
{{ $t('release.submit') }}
|
||||||
</base-button>
|
</os-button>
|
||||||
</template>
|
</template>
|
||||||
</ds-modal>
|
</ds-modal>
|
||||||
</template>
|
</template>
|
||||||
@ -52,6 +61,7 @@ export default {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
async confirm() {
|
async confirm() {
|
||||||
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
// TODO: Use the "modalData" structure introduced in "ConfirmModal" and refactor this here. Be aware that all the Jest tests have to be refactored as well !!!
|
||||||
// await this.modalData.buttons.confirm.callback()
|
// await this.modalData.buttons.confirm.callback()
|
||||||
@ -73,6 +83,11 @@ export default {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('close')
|
||||||
|
}, 1000)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -49,7 +49,7 @@ describe('LocationSelect', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the clearLocationName button by default', () => {
|
it('renders the clearLocationName button by default', () => {
|
||||||
expect(wrapper.find('.base-button').exists()).toBe(true)
|
expect(wrapper.find('button[data-test="clear-location-button"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls apollo with given value', () => {
|
it('calls apollo with given value', () => {
|
||||||
@ -64,7 +64,7 @@ describe('LocationSelect', () => {
|
|||||||
|
|
||||||
describe('clearLocationName button click', () => {
|
describe('clearLocationName button click', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper.find('.base-button').trigger('click')
|
wrapper.find('button[data-test="clear-location-button"]').trigger('click')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits an empty string', () => {
|
it('emits an empty string', () => {
|
||||||
@ -81,7 +81,7 @@ describe('LocationSelect', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('does not show clear location name button', () => {
|
it('does not show clear location name button', () => {
|
||||||
expect(wrapper.find('.base-button').exists()).toBe(false)
|
expect(wrapper.find('button[data-test="clear-location-button"]').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -13,24 +13,30 @@
|
|||||||
:loading="loadingGeo"
|
:loading="loadingGeo"
|
||||||
@input.native="handleCityInput"
|
@input.native="handleCityInput"
|
||||||
/>
|
/>
|
||||||
<base-button
|
<os-button
|
||||||
v-if="locationName !== '' && canBeCleared"
|
v-if="locationName !== '' && canBeCleared"
|
||||||
icon="close"
|
data-test="clear-location-button"
|
||||||
ghost
|
variant="primary"
|
||||||
size="small"
|
appearance="ghost"
|
||||||
style="position: relative; display: inline-block; right: -94%; top: -48px; width: 29px"
|
size="sm"
|
||||||
@click.native="clearLocationName"
|
:aria-label="$t('actions.clear')"
|
||||||
></base-button>
|
style="right: -94%; top: -48px"
|
||||||
|
@click="clearLocationName"
|
||||||
|
>
|
||||||
|
<template #icon><base-icon name="close" /></template>
|
||||||
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { queryLocations } from '~/graphql/location'
|
import { queryLocations } from '~/graphql/location'
|
||||||
|
|
||||||
let timeout
|
let timeout
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LocationSelect',
|
name: 'LocationSelect',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
required: true,
|
required: true,
|
||||||
@ -128,9 +134,8 @@ export default {
|
|||||||
this.loadingGeo = false
|
this.loadingGeo = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearLocationName(event) {
|
clearLocationName() {
|
||||||
event.target.value = ''
|
this.currentValue = ''
|
||||||
this.$emit('input', event.target.value)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,16 +14,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</vue-dropzone>
|
</vue-dropzone>
|
||||||
<div v-show="!showCropper && hasImage">
|
<div v-show="!showCropper && hasImage">
|
||||||
<base-button
|
<os-button
|
||||||
class="delete-image-button"
|
class="delete-image-button"
|
||||||
icon="trash"
|
variant="danger"
|
||||||
|
appearance="filled"
|
||||||
circle
|
circle
|
||||||
danger
|
|
||||||
filled
|
|
||||||
data-test="delete-button"
|
data-test="delete-button"
|
||||||
:title="$t('actions.delete')"
|
:title="$t('actions.delete')"
|
||||||
|
:aria-label="$t('actions.delete')"
|
||||||
@click.stop="deleteImage"
|
@click.stop="deleteImage"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="trash" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!showCropper && imageCanBeCropped" class="crop-overlay">
|
<div v-show="!showCropper && imageCanBeCropped" class="crop-overlay">
|
||||||
<os-button class="crop-confirm" variant="primary" @click="initCropper">
|
<os-button class="crop-confirm" variant="primary" @click="initCropper">
|
||||||
@ -35,15 +39,20 @@
|
|||||||
<os-button class="crop-confirm" variant="primary" @click="cropImage">
|
<os-button class="crop-confirm" variant="primary" @click="cropImage">
|
||||||
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
{{ $t('contribution.teaserImage.cropperConfirm') }}
|
||||||
</os-button>
|
</os-button>
|
||||||
<base-button
|
<os-button
|
||||||
class="crop-cancel"
|
class="crop-cancel"
|
||||||
icon="close"
|
variant="danger"
|
||||||
size="small"
|
appearance="filled"
|
||||||
circle
|
circle
|
||||||
danger
|
size="sm"
|
||||||
filled
|
:title="$t('actions.cancel')"
|
||||||
|
:aria-label="$t('actions.cancel')"
|
||||||
@click="closeCropper"
|
@click="closeCropper"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="close" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -206,17 +215,17 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .crop-cancel {
|
> .crop-cancel {
|
||||||
position: absolute;
|
position: absolute !important;
|
||||||
right: $space-x-small;
|
right: $space-x-small !important;
|
||||||
top: $space-x-small;
|
top: $space-x-small !important;
|
||||||
z-index: $z-index-surface;
|
z-index: $z-index-surface;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-image-button {
|
.delete-image-button {
|
||||||
position: absolute;
|
position: absolute !important;
|
||||||
top: $space-small;
|
top: $space-small !important;
|
||||||
right: $space-small;
|
right: $space-small !important;
|
||||||
z-index: $z-index-surface;
|
z-index: $z-index-surface;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -247,13 +256,6 @@ export default {
|
|||||||
opacity: $opacity-soft;
|
opacity: $opacity-soft;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .base-button {
|
|
||||||
position: absolute;
|
|
||||||
top: $space-small;
|
|
||||||
right: $space-small;
|
|
||||||
z-index: $z-index-surface;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .supported-formats {
|
> .supported-formats {
|
||||||
margin-top: 150px;
|
margin-top: 150px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
@ -7,18 +7,25 @@ exports[`ActionButton.vue when disabled renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Click me"
|
aria-label="Click me"
|
||||||
class="base-button --icon-only --circle"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
disabled="disabled"
|
disabled="disabled"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -37,17 +44,24 @@ exports[`ActionButton.vue when not disabled renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Click me"
|
aria-label="Click me"
|
||||||
class="base-button --icon-only --circle"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -8,17 +8,24 @@ exports[`ObserveButton observed renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="observeButton.observed"
|
aria-label="observeButton.observed"
|
||||||
class="base-button --icon-only --circle --filled"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="filled"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -38,17 +45,24 @@ exports[`ObserveButton unobserved renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="observeButton.observed"
|
aria-label="observeButton.observed"
|
||||||
class="base-button --icon-only --circle"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -7,17 +7,24 @@ exports[`ShoutButton.vue renders button and text 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="shoutButton.shouted"
|
aria-label="shoutButton.shouted"
|
||||||
class="base-button --icon-only --circle"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -36,17 +43,24 @@ exports[`ShoutButton.vue toggle the button 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="shoutButton.shouted"
|
aria-label="shoutButton.shouted"
|
||||||
class="base-button --icon-only --circle --filled"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="filled"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -65,17 +79,24 @@ exports[`ShoutButton.vue toggle the button, but backend fails 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="shoutButton.shouted"
|
aria-label="shoutButton.shouted"
|
||||||
class="base-button --icon-only --circle --filled"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="filled"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -94,17 +115,24 @@ exports[`ShoutButton.vue when shouted renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="shoutButton.shouted"
|
aria-label="shoutButton.shouted"
|
||||||
class="base-button --icon-only --circle --filled"
|
class="inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)] disabled:bg-[var(--color-disabled)] disabled:text-[var(--color-disabled-contrast)] disabled:border-[var(--color-disabled)] disabled:shadow-[inset_0_0_0_1px_transparent] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle bg-[var(--color-primary)] text-[var(--color-primary-contrast)] border-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] hover:border-[var(--color-primary-hover)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px]"
|
||||||
|
data-appearance="filled"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -8,21 +8,29 @@
|
|||||||
v-model="comment"
|
v-model="comment"
|
||||||
:schema="{ type: 'string', max: 30 }"
|
:schema="{ type: 'string', max: 30 }"
|
||||||
/>
|
/>
|
||||||
<base-button
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="outline"
|
||||||
circle
|
circle
|
||||||
class="generate-invite-code"
|
class="generate-invite-code"
|
||||||
:aria-label="$t('invite-codes.generate-code')"
|
:aria-label="$t('invite-codes.generate-code')"
|
||||||
icon="plus"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="plus" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateInvitation',
|
name: 'CreateInvitation',
|
||||||
|
components: { OsButton },
|
||||||
props: {
|
props: {
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@ -16,33 +16,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<base-button
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="outline"
|
||||||
circle
|
circle
|
||||||
class="copy-button"
|
class="copy-button"
|
||||||
icon="copy"
|
@click="copyInviteCode"
|
||||||
@click="copyInviteCode(inviteCode.copy)"
|
|
||||||
:disabled="!canCopy"
|
:disabled="!canCopy"
|
||||||
:aria-label="$t('invite-codes.copy-code')"
|
:aria-label="$t('invite-codes.copy-code')"
|
||||||
/>
|
>
|
||||||
<base-button
|
<template #icon>
|
||||||
|
<base-icon name="copy" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
|
<os-button
|
||||||
|
variant="primary"
|
||||||
|
appearance="outline"
|
||||||
circle
|
circle
|
||||||
class="invalidate-button"
|
class="invalidate-button"
|
||||||
icon="trash"
|
|
||||||
@click="openDeleteModal"
|
@click="openDeleteModal"
|
||||||
:aria-label="$t('invite-codes.invalidate')"
|
:aria-label="$t('invite-codes.invalidate')"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<base-icon name="trash" />
|
||||||
|
</template>
|
||||||
|
</os-button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { OsButton } from '@ocelot-social/ui'
|
||||||
import { mapMutations } from 'vuex'
|
import { mapMutations } from 'vuex'
|
||||||
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Invitation',
|
name: 'Invitation',
|
||||||
components: {
|
components: {
|
||||||
BaseButton,
|
OsButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
inviteCode: {
|
inviteCode: {
|
||||||
|
|||||||
@ -52,17 +52,24 @@ exports[`CreateInvitation.vue renders 1`] = `
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.generate-code"
|
aria-label="invite-codes.generate-code"
|
||||||
class="generate-invite-code base-button --icon-only --circle"
|
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -122,17 +129,24 @@ exports[`CreateInvitation.vue renders with disabled button 1`] = `
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.generate-code"
|
aria-label="invite-codes.generate-code"
|
||||||
class="generate-invite-code base-button --icon-only --circle"
|
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,32 +41,46 @@ exports[`Invitation.vue when the invite code was not redeemed renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.copy-code"
|
aria-label="invite-codes.copy-code"
|
||||||
class="copy-button base-button --icon-only --circle"
|
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.invalidate"
|
aria-label="invite-codes.invalidate"
|
||||||
class="invalidate-button base-button --icon-only --circle"
|
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -114,32 +128,46 @@ exports[`Invitation.vue when the invite code was redeemed renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.copy-code"
|
aria-label="invite-codes.copy-code"
|
||||||
class="copy-button base-button --icon-only --circle"
|
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.invalidate"
|
aria-label="invite-codes.invalidate"
|
||||||
class="invalidate-button base-button --icon-only --circle"
|
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -46,32 +46,46 @@ exports[`InvitationList.vue renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.copy-code"
|
aria-label="invite-codes.copy-code"
|
||||||
class="copy-button base-button --icon-only --circle"
|
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.invalidate"
|
aria-label="invite-codes.invalidate"
|
||||||
class="invalidate-button base-button --icon-only --circle"
|
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -114,32 +128,46 @@ exports[`InvitationList.vue renders 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.copy-code"
|
aria-label="invite-codes.copy-code"
|
||||||
class="copy-button base-button --icon-only --circle"
|
class="copy-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] copy-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.invalidate"
|
aria-label="invite-codes.invalidate"
|
||||||
class="invalidate-button base-button --icon-only --circle"
|
class="invalidate-button inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] invalidate-button"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -196,17 +224,24 @@ exports[`InvitationList.vue renders 1`] = `
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.generate-code"
|
aria-label="invite-codes.generate-code"
|
||||||
class="generate-invite-code base-button --icon-only --circle"
|
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -277,17 +312,24 @@ exports[`InvitationList.vue renders empty state 1`] = `
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="invite-codes.generate-code"
|
aria-label="invite-codes.generate-code"
|
||||||
class="generate-invite-code base-button --icon-only --circle"
|
class="generate-invite-code inline-flex items-center justify-center [white-space-collapse:collapse] relative appearance-none font-semibold tracking-[0.05em] transition-[color,background-color] duration-[80ms] ease-[cubic-bezier(0.25,0.46,0.45,0.94)] cursor-pointer select-none border-[0.8px] border-solid focus:outline-1 disabled:pointer-events-none disabled:cursor-default focus:outline-dashed focus:outline-[var(--color-primary)] bg-transparent shadow-none disabled:border-[var(--color-disabled)] disabled:text-[var(--color-disabled)] h-[36px] min-w-[36px] text-[15px] leading-[normal] align-middle border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-primary-contrast)] active:bg-[var(--color-primary-active)] active:border-[var(--color-primary-active)] active:text-[var(--color-primary-contrast)] rounded-full p-0 w-[36px] generate-invite-code"
|
||||||
|
data-appearance="outline"
|
||||||
|
data-variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="base-icon"
|
class="inline-flex items-center"
|
||||||
>
|
>
|
||||||
<!---->
|
<span
|
||||||
|
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="base-icon"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!---->
|
|
||||||
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -78,23 +78,23 @@ describe('MySomethingList.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('displays the edit button', () => {
|
it('displays the edit button', () => {
|
||||||
expect(wrapper.find('.base-button[data-test="edit-button"]').exists()).toBe(true)
|
expect(wrapper.find('button[data-test="edit-button"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays the delete button', () => {
|
it('displays the delete button', () => {
|
||||||
expect(wrapper.find('.base-button[data-test="delete-button"]').exists()).toBe(true)
|
expect(wrapper.find('button[data-test="delete-button"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('editing item', () => {
|
describe('editing item', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
|
const editButton = wrapper.find('button[data-test="edit-button"]')
|
||||||
editButton.trigger('click')
|
editButton.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables adding items while editing', () => {
|
it('disables adding items while editing', () => {
|
||||||
const submitButton = wrapper.find('.base-button[data-test="add-save-button"]')
|
const submitButton = wrapper.find('button[data-test="add-save-button"]')
|
||||||
expect(submitButton.text()).not.toContain('settings.social-media.submit')
|
expect(submitButton.text()).not.toContain('settings.social-media.submit')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ describe('MySomethingList.vue', () => {
|
|||||||
|
|
||||||
describe('calls callback functions', () => {
|
describe('calls callback functions', () => {
|
||||||
it('calls edit', async () => {
|
it('calls edit', async () => {
|
||||||
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
|
const editButton = wrapper.find('button[data-test="edit-button"]')
|
||||||
editButton.trigger('click')
|
editButton.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
|
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
|
||||||
@ -134,7 +134,7 @@ describe('MySomethingList.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calls delete by committing "modal/SET_OPEN"', async () => {
|
it('calls delete by committing "modal/SET_OPEN"', async () => {
|
||||||
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
const deleteButton = wrapper.find('button[data-test="delete-button"]')
|
||||||
deleteButton.trigger('click')
|
deleteButton.trigger('click')
|
||||||
await Vue.nextTick()
|
await Vue.nextTick()
|
||||||
const expectedModalData = expect.objectContaining({
|
const expectedModalData = expect.objectContaining({
|
||||||
|
|||||||