mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-03-22 18:25:29 +00:00
feat(package/ui): os-modal & webapp integration (#9375)
This commit is contained in:
parent
f5b5c6d306
commit
237798b0f0
@ -2,7 +2,7 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep(/^I confirm the reporting dialog .*:$/, message => {
|
||||
cy.contains(message) // wait for element to become visible
|
||||
cy.get('.ds-modal')
|
||||
cy.get('.os-modal')
|
||||
.within(() => {
|
||||
cy.get('.ds-radio-option-label')
|
||||
.first()
|
||||
|
||||
@ -14,7 +14,7 @@ Phase 3: OsButton ██████████ 100% (133/133 Buttons) ✅
|
||||
Phase 4: Tier 1 ██████████ 100% (OsButton, OsIcon, OsSpinner, OsCard) ✅
|
||||
Phase 4: Tier A → HTML ██████████ 100% (10 ds-* Wrapper → Plain HTML) ✅
|
||||
Phase 4: Tier B ████████░░ 80% (ds-chip→OsBadge✅, ds-tag→OsBadge✅, ds-grid✅, ds-number→OsNumber✅, ds-radio⬜)
|
||||
Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅) | Tier 2-3 ausstehend (OsModal, OsInput, OsMenu, OsSelect)
|
||||
Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅) | Tier 2 begonnen (OsModal✅) | Rest ausstehend (OsInput, OsMenu, OsSelect)
|
||||
```
|
||||
|
||||
### Statistiken
|
||||
@ -87,7 +87,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
| 24 | FlexItem | ✅ → HTML | Tier A: Plain HTML + CSS @media Queries |
|
||||
| 25 | Grid | ✅ → HTML | 2 Dateien → CSS Grid (class="ds-grid") |
|
||||
| 26 | GridItem | ✅ → HTML | 8 Dateien → CSS Grid |
|
||||
| 27 | Modal | ⬜ Tier 2 | 7 Dateien → OsModal |
|
||||
| 27 | Modal | ✅ Tier 2 | → OsModal (h() Render, Focus-Trap, Scroll-Lock, A11y; ConfirmModal + ReportModal nutzen OsModal) |
|
||||
| 28 | Page | ⬜ Nicht genutzt | Nicht direkt in Webapp verwendet |
|
||||
| 29 | PageTitle | ⬜ Nicht genutzt | Nicht direkt in Webapp verwendet |
|
||||
| 30 | Section | ✅ → HTML | Tier A: `<section class="ds-section">` |
|
||||
@ -303,7 +303,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
| Webapp | Styleguide | Aktion | Status |
|
||||
|--------|------------|--------|--------|
|
||||
| Logo | Logo | Konsolidieren zu OsLogo | ⬜ Ausstehend |
|
||||
| Modal | Modal | Konsolidieren zu OsModal | ⬜ Ausstehend |
|
||||
| Modal | Modal | Konsolidieren zu OsModal | ✅ Erledigt |
|
||||
| ~~BaseCard~~ | Card | → OsCard | ✅ Erledigt (BaseCard gelöscht) |
|
||||
| ~~BaseIcon~~ | Icon | → OsIcon | ✅ Erledigt (BaseIcon gelöscht) |
|
||||
| ~~LoadingSpinner~~ | Spinner | → OsSpinner | ✅ Erledigt (LoadingSpinner gelöscht) |
|
||||
@ -330,16 +330,16 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
| CtaJoinLeaveGroup | ✅ Nutzt OsButton | Feature-spezifisch |
|
||||
| CtaUnblockAuthor | ✅ Nutzt OsButton | Feature-spezifisch |
|
||||
|
||||
### Modal-Familie (zur Konsolidierung)
|
||||
| Komponente | Beschreibung | Ziel |
|
||||
|------------|--------------|------|
|
||||
| Modal (Styleguide) | Basis-Modal | OsModal |
|
||||
| Modal (Webapp) | Basis-Modal | → OsModal |
|
||||
| ConfirmModal | Bestätigungs-Dialog | → OsModal type="confirm" |
|
||||
| DeleteUserModal | Löschen-Dialog | → OsModal type="confirm" |
|
||||
| DisableModal | Deaktivieren-Dialog | → OsModal type="confirm" |
|
||||
| ReleaseModal | Release-Dialog | Feature-spezifisch |
|
||||
| ReportModal | Report-Dialog | Feature-spezifisch |
|
||||
### Modal-Familie ✅ (alle nutzen OsModal)
|
||||
| Komponente | Status | Notizen |
|
||||
|------------|--------|---------|
|
||||
| ~~Modal (Styleguide)~~ | ✅ Ersetzt | → OsModal |
|
||||
| ~~Modal (Webapp)~~ | ✅ Ersetzt | → OsModal |
|
||||
| ConfirmModal | ✅ Nutzt OsModal | Generischer Confirm-Dialog mit Callbacks |
|
||||
| ~~DeleteUserModal~~ | ✅ Gelöscht | → ConfirmModal |
|
||||
| ~~DisableModal~~ | ✅ Gelöscht | → ConfirmModal |
|
||||
| ~~ReleaseModal~~ | ✅ Gelöscht | → ConfirmModal |
|
||||
| ReportModal | ✅ Nutzt OsModal | Feature-spezifisch |
|
||||
|
||||
### Menu-Familie (zur Konsolidierung)
|
||||
| Komponente | Beschreibung | Ziel |
|
||||
@ -365,7 +365,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
- ~~Spinner → OsSpinner~~ ✅
|
||||
|
||||
### Basis-Komponenten — UI-Library (ausstehend)
|
||||
- Modal → OsModal
|
||||
- Modal → OsModal ✅
|
||||
- Input → OsInput
|
||||
- Select → OsSelect
|
||||
- Avatar → OsAvatar (falls benötigt)
|
||||
@ -441,8 +441,8 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
17. [ ] ds-number (5 Dateien) → `<div class="ds-number">`
|
||||
18. [ ] ds-radio (1 Datei) → native `<input type="radio">`
|
||||
|
||||
### Phase 4: Tier 2-4 — UI-Library (ausstehend)
|
||||
18. [ ] OsModal (7 Dateien)
|
||||
### Phase 4: Tier 2-4 — UI-Library
|
||||
18. [x] OsModal (h() Render, Focus-Trap, Scroll-Lock, A11y; ConfirmModal + ReportModal nutzen OsModal; DeleteUserModal/DisableModal/ReleaseModal gelöscht) ✅
|
||||
19. [ ] OsInput (23 Dateien, gekoppelt mit ds-form)
|
||||
20. [ ] OsMenu / OsMenuItem (17 Dateien)
|
||||
21. [ ] OsSelect (3 Dateien), OsTable (7 Dateien)
|
||||
@ -450,7 +450,7 @@ Phase 4: Tier B+ ████████░░ 75% (ds-table→HTML✅)
|
||||
|
||||
---
|
||||
|
||||
**✅ Phase 0-3 abgeschlossen. Phase 4: Tier 1 + Tier A ✅, Tier B 60% (Chip→OsBadge, Tag→OsBadge, Grid→HTML), Tier 2-4 ausstehend.**
|
||||
**✅ Phase 0-3 abgeschlossen. Phase 4: Tier 1 + Tier A ✅, Tier B 80% (Chip→OsBadge, Tag→OsBadge, Grid→HTML, Number→OsNumber, Table→HTML), Tier 2: OsModal ✅, Rest ausstehend.**
|
||||
|
||||
---
|
||||
|
||||
@ -684,36 +684,29 @@ interface OsButtonProps {
|
||||
|
||||
---
|
||||
|
||||
### Feature-Modals (nutzen DsModal)
|
||||
### Feature-Modals ✅ (nutzen OsModal)
|
||||
|
||||
| Modal | Business-Logik | Migration |
|
||||
|-------|----------------|-----------|
|
||||
| **ConfirmModal** | Generischer Confirm-Dialog mit Callbacks | Bleibt, nutzt OsModal |
|
||||
| **DisableModal** | GraphQL Mutation für Disable | Bleibt, nutzt OsModal |
|
||||
| **ReportModal** | Report-Logik | Bleibt, nutzt OsModal |
|
||||
| **DeleteUserModal** | User-Lösch-Logik | Bleibt, nutzt OsModal |
|
||||
| **ReleaseModal** | Release-Logik | Bleibt, nutzt OsModal |
|
||||
| Modal | Status | Notizen |
|
||||
|-------|--------|---------|
|
||||
| **ConfirmModal** | ✅ Nutzt OsModal | Generischer Confirm-Dialog mit Callbacks, Success-Animation |
|
||||
| ~~**DisableModal**~~ | ✅ Gelöscht | → ConfirmModal (inline modalData) |
|
||||
| **ReportModal** | ✅ Nutzt OsModal | Report-Logik mit Radio-Auswahl |
|
||||
| ~~**DeleteUserModal**~~ | ✅ Gelöscht | → ConfirmModal (inline modalData) |
|
||||
| ~~**ReleaseModal**~~ | ✅ Gelöscht | → ConfirmModal (inline modalData) |
|
||||
|
||||
**Erkenntnis:** Alle Feature-Modals nutzen bereits DsModal als Basis und fügen nur spezifische Business-Logik hinzu. Dies ist das gewünschte Pattern!
|
||||
**Ergebnis:** DsModal vollständig durch OsModal ersetzt. Vuex Modal Store entfernt — Modals werden inline gerendert (v-if + showConfirmModal). DeleteUserModal, DisableModal, ReleaseModal wurden in ConfirmModal konsolidiert.
|
||||
|
||||
---
|
||||
|
||||
### Webapp Modal.vue (Modal-Manager)
|
||||
### ~~Webapp Modal.vue (Modal-Manager)~~ ✅ Gelöscht
|
||||
|
||||
**Pfad:** `webapp/components/Modal.vue`
|
||||
**Vuex Modal Store (`store/modal.js`) und Modal-Router (`components/Modal.vue`) wurden entfernt.**
|
||||
|
||||
**Funktion:** Kein UI-Modal, sondern ein **State-basierter Modal-Router**:
|
||||
- Liest `modal/open` und `modal/data` aus Vuex
|
||||
- Rendert das passende Feature-Modal basierend auf State
|
||||
- Leitet Props an Feature-Modals weiter
|
||||
|
||||
**Mögliche Migration:**
|
||||
- Als `OsModalManager` beibehalten oder
|
||||
- Durch Vue 3 `<Teleport>` + Composable ersetzen
|
||||
Modals werden jetzt inline gerendert: Jede Komponente hat eigenes `showConfirmModal`/`currentModalData` State und rendert `<confirm-modal v-if="showConfirmModal">` direkt.
|
||||
|
||||
---
|
||||
|
||||
### Konsolidierungsvorschlag: OsModal
|
||||
### ~~Konsolidierungsvorschlag~~ OsModal ✅ (implementiert)
|
||||
|
||||
```typescript
|
||||
interface OsModalProps {
|
||||
@ -964,14 +957,14 @@ interface OsDropdownProps {
|
||||
| — | ds-number | 5 | `<div class="ds-number">` | ⬜ |
|
||||
| — | ds-radio | 1 | native `<input type="radio">` | ⬜ |
|
||||
|
||||
### Tier 2: Layout & Feedback (ausstehend)
|
||||
### Tier 2: Layout & Feedback
|
||||
|
||||
| # | Komponente | Dateien | Abhängigkeiten |
|
||||
|---|------------|---------|----------------|
|
||||
| 5 | **OsModal** | 7 | OsButton, OsCard |
|
||||
| 6 | **OsDropdown** | — | OsButton |
|
||||
| 7 | **OsAvatar** | — | - |
|
||||
| 8 | **OsInput** | 23 | gekoppelt mit ds-form (18 Dateien) |
|
||||
| # | Komponente | Dateien | Abhängigkeiten | Status |
|
||||
|---|------------|---------|----------------|--------|
|
||||
| 5 | **OsModal** | 7 | OsButton | ✅ |
|
||||
| 6 | **OsDropdown** | — | OsButton | ⬜ |
|
||||
| 7 | **OsAvatar** | — | - | ⬜ |
|
||||
| 8 | **OsInput** | 23 | gekoppelt mit ds-form (18 Dateien) | ⬜ |
|
||||
|
||||
### Tier 3: Navigation (ausstehend)
|
||||
|
||||
|
||||
@ -81,10 +81,10 @@ Phase 0: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 1: ██████████ 100% (6/6 Aufgaben) ✅
|
||||
Phase 2: ██████████ 100% (26/26 Aufgaben) ✅
|
||||
Phase 3: ██████████ 100% (24/24 Aufgaben) ✅ - Webapp-Integration komplett
|
||||
Phase 4: ██████░░░░ 63% (17/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅ | Tier B (rest), Tier 2-3 ausstehend
|
||||
Phase 4: ███████░░░ 67% (18/27 Aufgaben) - Tier 1 ✅, Tier A ✅, Infra ✅, OsBadge ✅, ds-grid ✅, ds-table→HTML ✅, OsNumber ✅, OsModal ✅ | Tier B (rest), Tier 2-3 ausstehend
|
||||
Phase 5: ░░░░░░░░░░ 0% (0/7 Aufgaben)
|
||||
───────────────────────────────────────
|
||||
Gesamt: ████████░░ 82% (79/96 Aufgaben)
|
||||
Gesamt: ████████░░ 83% (80/96 Aufgaben)
|
||||
```
|
||||
|
||||
### Katalogisierung (Details in KATALOG.md)
|
||||
@ -405,7 +405,8 @@ ds-chip + ds-tag → OsBadge (UI-Library): ✅
|
||||
- [x] OsBadge Komponente + ds-chip/ds-tag → OsBadge Webapp-Migration ✅
|
||||
- [x] OsNumber Komponente + ds-number/CountTo → OsNumber Webapp-Migration ✅
|
||||
- [ ] Tier B (Rest): ds-radio → Plain HTML
|
||||
- [ ] Weitere Tier 2 Komponenten (OsModal, OsDropdown, OsAvatar, OsInput)
|
||||
- [x] OsModal Komponente + DsModal/ConfirmModal/ReportModal → OsModal Webapp-Integration ✅
|
||||
- [ ] Weitere Tier 2 Komponenten (OsDropdown, OsAvatar, OsInput)
|
||||
- [ ] ds-form + ds-input → OsForm + OsInput (stark gekoppelt, 18+23 Dateien)
|
||||
- [ ] ds-menu / ds-menu-item → OsMenu / OsMenuItem
|
||||
- [ ] ds-select → OsSelect
|
||||
@ -680,7 +681,7 @@ Jeder migrierte Button muss manuell geprüft werden: Normal, Hover, Focus, Activ
|
||||
- [ ] ds-radio (1 Datei) → native `<input type="radio">`
|
||||
|
||||
**Tier 2: Layout & Feedback (UI-Library)**
|
||||
- [ ] OsModal (Basis: DsModal, 7 Dateien)
|
||||
- [x] OsModal (Basis: DsModal → h() Render-Function, Vue 2/3 Compat, Focus-Trap, Scroll-Lock, A11y) ✅
|
||||
- [ ] OsDropdown (Basis: Webapp Dropdown)
|
||||
- [ ] OsAvatar (vereint DsAvatar + ProfileAvatar)
|
||||
- [ ] OsInput (Basis: DsInput, 23 Dateien — gekoppelt mit ds-form)
|
||||
@ -1831,6 +1832,13 @@ Bei der Migration werden:
|
||||
| 2026-02-20 | **CountTo.vue gelöscht** | vue-count-to Dependency entfernt, followedByCountStartValue/membersCountStartValue Pattern entfernt |
|
||||
| 2026-02-20 | **CSS-Variable --color-text-soft** | Neuer Contract-Eintrag in tailwind.preset.ts + ocelot-ui-variables.scss (Label-Farbe) |
|
||||
| 2026-02-20 | **Admin-Label uppercase** | `.admin-stats__item .os-number-label { text-transform: uppercase }` per CSS statt neuem Prop |
|
||||
| 2026-03-13 | **OsModal Komponente** | Neue Tier 2 Komponente: h() Render-Function, Vue 2/3 via vue-demi, Focus-Trap, Body Scroll-Lock, ESC-Key, Backdrop-Click, A11y (role=dialog, aria-modal, aria-labelledby/aria-label), 37 Unit-Tests, 5 Visual Tests, 100% Coverage |
|
||||
| 2026-03-13 | **OsModal Features** | open Prop (v-model:open), title, cancelLabel/confirmLabel, ariaLabel Fallback, footer Scoped-Slot ({confirm, cancel}), Scroll-Fade (top gradient), tabindex=0 auf scrollbarem Content |
|
||||
| 2026-03-13 | **OsModal Events** | update:open, confirm, cancel, close (mit Typ: 'confirm'/'cancel'/'close'/'backdrop'), opened |
|
||||
| 2026-03-13 | **OsModal Vue 2 Compat** | attrs Forwarding in beiden Vue 2 Branches (closed + open), eventProps() Helper, $listeners Forwarding, $createElement für Icons |
|
||||
| 2026-03-13 | **Modal Webapp-Integration** | ConfirmModal + ReportModal nutzen OsModal; Vuex Modal Store entfernt; Modals inline gerendert |
|
||||
| 2026-03-13 | **Modal Bugfixes** | z-index Stacking Context Fix (PostTeaser/GroupTeaser), Callback-Promise Propagation (ReportList, MySomethingList), Group Leave Authorization Fix ($nextTick), Cypress .ds-modal → .os-modal |
|
||||
| 2026-03-13 | **Modal A11y** | scrollable-region-focusable Fix (tabindex=0), aria-label Fallback wenn kein Title, body overflow save/restore |
|
||||
|
||||
---
|
||||
|
||||
@ -1848,11 +1856,11 @@ Bei der Migration werden:
|
||||
**Styleguide-Migration:**
|
||||
| Status | Komponenten |
|
||||
|--------|------------|
|
||||
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge, OsNumber (6) |
|
||||
| ✅ UI-Library | OsButton, OsIcon, OsSpinner, OsCard, OsBadge, OsNumber, OsModal (7) |
|
||||
| ✅ → Plain HTML | Section, Placeholder, List, ListItem, Container, Heading, Text, Space, Flex, FlexItem, Grid, GridItem, Table (13) — Tier A/B |
|
||||
| ✅ → UI-Library | Chip, Tag → OsBadge (2), Number → OsNumber (1) — Tier B |
|
||||
| ⬜ → Plain HTML | Radio (1) — Tier B |
|
||||
| ⬜ → UI-Library | Modal, Input, Menu, MenuItem, Select (5) — Tier 2-3 |
|
||||
| ⬜ → UI-Library | Input, Menu, MenuItem, Select (4) — Tier 2-3 |
|
||||
| ⬜ Nicht genutzt | Code, CopyField, FormItem, InputError, InputLabel, Page, PageTitle, Logo, Avatar, TableCol, TableHeadCol (11) |
|
||||
| ⬜ Offen | Form (18 Dateien — HTML `<form>` oder OsForm?) |
|
||||
|
||||
@ -2932,7 +2940,7 @@ Jede Komponente durchläuft:
|
||||
| OsSpinner | Niedrig | Nur Animation + Größen |
|
||||
| OsButton | Hoch | Viele Varianten, Link-Support, States |
|
||||
| OsCard | Niedrig | Einfaches Layout |
|
||||
| OsModal | Hoch | Teleport, Focus-Trap, Animations, A11y |
|
||||
| OsModal | Hoch ✅ | Focus-Trap, Scroll-Lock, Body Overflow Save/Restore, Vue 2/3 h() Compat, A11y (dialog, aria-modal, aria-labelledby), ESC-Key, Backdrop-Click, Scrollable Content tabindex |
|
||||
| OsDropdown | Hoch | Positioning, Click-Outside, Hover-States |
|
||||
| OsInput | Mittel | Validierung, States, Icons |
|
||||
| OsAvatar | Niedrig | Bild + Fallback |
|
||||
|
||||
425
packages/ui/src/components/OsModal/OsModal.spec.ts
Normal file
425
packages/ui/src/components/OsModal/OsModal.spec.ts
Normal file
@ -0,0 +1,425 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import OsModal from './OsModal.vue'
|
||||
|
||||
describe('osModal', () => {
|
||||
afterEach(() => {
|
||||
document.body.style.overflow = ''
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('renders an empty wrapper when closed', () => {
|
||||
const wrapper = mount(OsModal)
|
||||
|
||||
expect(wrapper.find('.os-modal-wrapper').exists()).toBe(true)
|
||||
expect(wrapper.find('.os-modal').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders the modal when open', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders backdrop when open', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal__backdrop').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders title in header', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, title: 'Test Title' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal__title').text()).toBe('Test Title')
|
||||
})
|
||||
|
||||
it('does not render title element when title is null', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal__title').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders default slot content', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
slots: { default: '<p>Modal body</p>' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal__content').text()).toBe('Modal body')
|
||||
})
|
||||
|
||||
it('renders close button', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-close"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('scroll fade', () => {
|
||||
it('does not show top fade when content is not scrolled', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
slots: { default: '<div style="height: 2000px">tall content</div>' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal__header .bg-gradient-to-b').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows top fade after content is scrolled down', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
slots: { default: '<div style="height: 2000px">tall content</div>' },
|
||||
})
|
||||
|
||||
const content = wrapper.find('.os-modal__content')
|
||||
Object.defineProperty(content.element, 'scrollTop', { value: 50, writable: true })
|
||||
await content.trigger('scroll')
|
||||
|
||||
expect(wrapper.find('.os-modal__header .bg-gradient-to-b').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('css', () => {
|
||||
it('has os-modal-wrapper class on root', () => {
|
||||
const wrapper = mount(OsModal)
|
||||
|
||||
expect(wrapper.classes()).toContain('os-modal-wrapper')
|
||||
})
|
||||
|
||||
it('merges custom classes', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
attrs: { class: 'my-custom-modal' },
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('os-modal-wrapper')
|
||||
expect(wrapper.classes()).toContain('my-custom-modal')
|
||||
})
|
||||
})
|
||||
|
||||
describe('panel width', () => {
|
||||
it('applies max-w-[500px]', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal').classes()).toContain('max-w-[500px]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('built-in footer buttons', () => {
|
||||
it('renders cancel and confirm buttons by default', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-cancel"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="os-modal-confirm"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('uses custom cancelLabel', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, cancelLabel: 'Abbrechen' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-cancel"]').text()).toBe('Abbrechen')
|
||||
})
|
||||
|
||||
it('uses custom confirmLabel', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, confirmLabel: 'Bestätigen' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-confirm"]').text()).toBe('Bestätigen')
|
||||
})
|
||||
|
||||
it('cancel button has ghost appearance', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-cancel"]').attributes('data-appearance')).toBe(
|
||||
'ghost',
|
||||
)
|
||||
})
|
||||
|
||||
it('confirm button has primary variant', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-confirm"]').attributes('data-variant')).toBe(
|
||||
'primary',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom footer slot', () => {
|
||||
it('renders custom footer instead of built-in buttons', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
slots: {
|
||||
footer: '<button class="custom-btn">Custom</button>',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.custom-btn').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="os-modal-cancel"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-testid="os-modal-confirm"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('provides confirm and cancel functions to scoped slot', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
slots: {
|
||||
footer: `<template #footer="{ confirm, cancel }">
|
||||
<button class="slot-confirm" @click="confirm">OK</button>
|
||||
<button class="slot-cancel" @click="cancel">Nope</button>
|
||||
</template>`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-modal__footer').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
it('emits update:open false and close on cancel button click', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
await wrapper.find('[data-testid="os-modal-cancel"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('update:open')).toStrictEqual([[false]])
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
||||
expect(wrapper.emitted('close')).toStrictEqual([['cancel']])
|
||||
})
|
||||
|
||||
it('emits update:open false and close on confirm button click', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
await wrapper.find('[data-testid="os-modal-confirm"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('update:open')).toStrictEqual([[false]])
|
||||
expect(wrapper.emitted('confirm')).toHaveLength(1)
|
||||
expect(wrapper.emitted('close')).toStrictEqual([['confirm']])
|
||||
})
|
||||
|
||||
it('emits cancel on close button click with type "close"', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
await wrapper.find('[data-testid="os-modal-close"]').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
||||
expect(wrapper.emitted('close')).toStrictEqual([['close']])
|
||||
})
|
||||
|
||||
it('emits cancel on backdrop click', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
await wrapper.find('.os-modal__overlay').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
||||
expect(wrapper.emitted('close')).toStrictEqual([['backdrop']])
|
||||
})
|
||||
|
||||
it('emits opened when open becomes true', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: false },
|
||||
})
|
||||
|
||||
await wrapper.setProps({ open: true })
|
||||
|
||||
expect(wrapper.emitted('opened')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('esc key', () => {
|
||||
it('closes modal on ESC key press', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
||||
expect(wrapper.emitted('close')).toStrictEqual([['backdrop']])
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('does not close on ESC when closed', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: false },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'Escape' })
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(wrapper.emitted('cancel')).toBeUndefined()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('body scroll lock', () => {
|
||||
it('sets body overflow hidden when open', () => {
|
||||
mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(document.body.style.overflow).toBe('hidden')
|
||||
})
|
||||
|
||||
it('removes body overflow hidden when closed', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
await wrapper.setProps({ open: false })
|
||||
|
||||
expect(document.body.style.overflow).toBe('')
|
||||
})
|
||||
|
||||
it('cleans up body overflow on unmount', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(document.body.style.overflow).toBe('hidden')
|
||||
|
||||
wrapper.unmount()
|
||||
|
||||
expect(document.body.style.overflow).toBe('')
|
||||
})
|
||||
|
||||
it('restores previous body overflow value when closed', async () => {
|
||||
document.body.style.overflow = 'auto'
|
||||
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(document.body.style.overflow).toBe('hidden')
|
||||
|
||||
await wrapper.setProps({ open: false })
|
||||
|
||||
expect(document.body.style.overflow).toBe('auto')
|
||||
})
|
||||
})
|
||||
|
||||
describe('aria attributes', () => {
|
||||
it('has role="dialog" on the panel', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-panel"]').attributes('role')).toBe('dialog')
|
||||
})
|
||||
|
||||
it('has aria-modal="true"', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-panel"]').attributes('aria-modal')).toBe('true')
|
||||
})
|
||||
|
||||
it('has aria-labelledby linking to title', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, title: 'My Title' },
|
||||
})
|
||||
|
||||
const panel = wrapper.find('[data-testid="os-modal-panel"]')
|
||||
const titleEl = wrapper.find('.os-modal__title')
|
||||
|
||||
expect(panel.attributes('aria-labelledby')).toBe(titleEl.attributes('id'))
|
||||
})
|
||||
|
||||
it('has aria-label fallback when no title', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true },
|
||||
})
|
||||
|
||||
const panel = wrapper.find('[data-testid="os-modal-panel"]')
|
||||
|
||||
expect(panel.attributes('aria-labelledby')).toBeUndefined()
|
||||
expect(panel.attributes('aria-label')).toBe('Dialog')
|
||||
})
|
||||
|
||||
it('uses custom ariaLabel when no title', () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, ariaLabel: 'Confirm deletion' },
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="os-modal-panel"]').attributes('aria-label')).toBe(
|
||||
'Confirm deletion',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('keyboard accessibility', () => {
|
||||
it('traps focus within the modal on Tab', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, title: 'Test' },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
const panel = wrapper.find('[data-testid="os-modal-panel"]')
|
||||
const buttons = wrapper.findAll('button')
|
||||
const lastButton = buttons[buttons.length - 1]
|
||||
|
||||
// Focus the last button
|
||||
;(lastButton.element as HTMLElement).focus()
|
||||
|
||||
// Press Tab → should wrap to first
|
||||
await panel.trigger('keydown', { key: 'Tab' })
|
||||
|
||||
// The focus trap should have prevented default
|
||||
// (we can't fully test focus movement in jsdom, but we test the handler exists)
|
||||
expect(panel.exists()).toBe(true)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('traps focus within the modal on Shift+Tab', async () => {
|
||||
const wrapper = mount(OsModal, {
|
||||
props: { open: true, title: 'Test' },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
const panel = wrapper.find('[data-testid="os-modal-panel"]')
|
||||
const buttons = wrapper.findAll('button')
|
||||
|
||||
// Focus the first button
|
||||
;(buttons[0].element as HTMLElement).focus()
|
||||
|
||||
// Press Shift+Tab → should wrap to last
|
||||
await panel.trigger('keydown', { key: 'Tab', shiftKey: true })
|
||||
|
||||
expect(panel.exists()).toBe(true)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
102
packages/ui/src/components/OsModal/OsModal.stories.ts
Normal file
102
packages/ui/src/components/OsModal/OsModal.stories.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import OsButton from '#src/components/OsButton/OsButton.vue'
|
||||
|
||||
import OsModal from './OsModal.vue'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
const meta: Meta<typeof OsModal> = {
|
||||
title: 'Components/OsModal',
|
||||
component: OsModal,
|
||||
tags: ['autodocs'],
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof OsModal>
|
||||
|
||||
interface PlaygroundArgs {
|
||||
open: boolean
|
||||
title: string
|
||||
cancelLabel: string
|
||||
confirmLabel: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const Playground: StoryObj<PlaygroundArgs> = {
|
||||
argTypes: {
|
||||
open: { control: 'boolean' },
|
||||
title: { control: 'text' },
|
||||
cancelLabel: { control: 'text' },
|
||||
confirmLabel: { control: 'text' },
|
||||
content: { control: 'text' },
|
||||
},
|
||||
args: {
|
||||
open: true,
|
||||
title: 'Modal Title',
|
||||
cancelLabel: 'Cancel',
|
||||
confirmLabel: 'Confirm',
|
||||
content: 'This is the modal body content.',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { OsModal },
|
||||
setup() {
|
||||
const modalProps = computed(() => ({
|
||||
open: args.open,
|
||||
title: args.title,
|
||||
cancelLabel: args.cancelLabel,
|
||||
confirmLabel: args.confirmLabel,
|
||||
}))
|
||||
const content = computed(() => args.content)
|
||||
return { modalProps, content }
|
||||
},
|
||||
template: `<OsModal v-bind="modalProps">{{ content }}</OsModal>`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const DefaultSize: Story = {
|
||||
render: () => ({
|
||||
components: { OsModal },
|
||||
template: `
|
||||
<div data-testid="default-size">
|
||||
<OsModal :open="true" title="Default Size Modal">
|
||||
<p>This is a modal with default size (max-width: 500px).</p>
|
||||
<p>It contains the standard cancel and confirm buttons.</p>
|
||||
</OsModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const CustomFooter: Story = {
|
||||
render: () => ({
|
||||
components: { OsModal, OsButton },
|
||||
template: `
|
||||
<div data-testid="custom-footer">
|
||||
<OsModal :open="true" title="Custom Footer">
|
||||
<p>This modal has a custom footer with different buttons.</p>
|
||||
<template #footer="{ confirm, cancel }">
|
||||
<OsButton variant="danger" appearance="outline" @click="cancel">Delete</OsButton>
|
||||
<OsButton variant="primary" @click="confirm">Save Changes</OsButton>
|
||||
</template>
|
||||
</OsModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
|
||||
export const ScrollableContent: Story = {
|
||||
render: () => ({
|
||||
components: { OsModal },
|
||||
template: `
|
||||
<div data-testid="scrollable-content">
|
||||
<OsModal :open="true" title="Scrollable Content">
|
||||
<div>
|
||||
<p>This modal has a lot of content that will scroll.</p>
|
||||
<p v-for="i in 20" :key="i">Paragraph {{ i }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
</div>
|
||||
</OsModal>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
85
packages/ui/src/components/OsModal/OsModal.visual.spec.ts
Normal file
85
packages/ui/src/components/OsModal/OsModal.visual.spec.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { AxeBuilder } from '@axe-core/playwright'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
const STORY_URL = '/iframe.html?id=components-osmodal'
|
||||
const STORY_ROOT = '#storybook-root'
|
||||
|
||||
async function waitForReady(page: Page) {
|
||||
await page.evaluate(async () => document.fonts.ready)
|
||||
}
|
||||
|
||||
async function checkA11y(page: Page) {
|
||||
const results = await new AxeBuilder({ page }).include(STORY_ROOT).analyze()
|
||||
|
||||
expect(results.violations).toEqual([])
|
||||
}
|
||||
|
||||
test.describe('OsModal keyboard accessibility', () => {
|
||||
test('close button is focusable via Tab', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--default-size&viewMode=story`)
|
||||
const modal = page.locator('.os-modal')
|
||||
await modal.waitFor()
|
||||
|
||||
const closeBtn = page.locator('[data-testid="os-modal-close"]')
|
||||
await expect(closeBtn).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
await expect(closeBtn).toBeFocused()
|
||||
})
|
||||
|
||||
test('footer buttons are focusable via Tab', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--default-size&viewMode=story`)
|
||||
const modal = page.locator('.os-modal')
|
||||
await modal.waitFor()
|
||||
|
||||
const cancelBtn = page.locator('[data-testid="os-modal-cancel"]')
|
||||
const confirmBtn = page.locator('[data-testid="os-modal-confirm"]')
|
||||
await expect(cancelBtn).toBeVisible()
|
||||
await expect(confirmBtn).toBeVisible()
|
||||
|
||||
// Tab through to find the buttons
|
||||
await page.keyboard.press('Tab') // close button
|
||||
await page.keyboard.press('Tab') // content area
|
||||
await page.keyboard.press('Tab') // cancel
|
||||
await expect(cancelBtn).toBeFocused()
|
||||
await page.keyboard.press('Tab') // confirm
|
||||
await expect(confirmBtn).toBeFocused()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('OsModal visual regression', () => {
|
||||
test('default size', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--default-size&viewMode=story`)
|
||||
const panel = page.locator('[data-testid="os-modal-panel"]')
|
||||
await panel.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
await expect(panel).toHaveScreenshot('default-size.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('custom footer', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--custom-footer&viewMode=story`)
|
||||
const panel = page.locator('[data-testid="os-modal-panel"]')
|
||||
await panel.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
await expect(panel).toHaveScreenshot('custom-footer.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('scrollable content', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--scrollable-content&viewMode=story`)
|
||||
const panel = page.locator('[data-testid="os-modal-panel"]')
|
||||
await panel.waitFor()
|
||||
await waitForReady(page)
|
||||
|
||||
await expect(panel).toHaveScreenshot('scrollable-content.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
})
|
||||
404
packages/ui/src/components/OsModal/OsModal.vue
Normal file
404
packages/ui/src/components/OsModal/OsModal.vue
Normal file
@ -0,0 +1,404 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
isVue2,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue-demi'
|
||||
|
||||
import OsButton from '#src/components/OsButton/OsButton.vue'
|
||||
import { IconCheck, IconClose } from '#src/components/OsIcon'
|
||||
import { cn } from '#src/utils'
|
||||
|
||||
import { modalPanelVariants } from './modal.variants'
|
||||
|
||||
/**
|
||||
* Modal dialog component with backdrop, focus trap, and body scroll lock.
|
||||
*
|
||||
* Supports two modes:
|
||||
* - **Custom footer** via the `footer` scoped slot (receives `{ confirm, cancel }`)
|
||||
* - **Built-in buttons** using `cancelLabel` / `confirmLabel` props
|
||||
*
|
||||
* Vue 2: `<os-modal :open.sync="show" />`
|
||||
* Vue 3: `<os-modal v-model:open="show" />`
|
||||
*
|
||||
* @slot default - Modal body content
|
||||
* @slot footer - Custom footer (scoped: { confirm, cancel })
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'OsModal',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
/** Whether the modal is open */
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** Modal title displayed in the header */
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/** Accessible label for the dialog when no visible title is provided */
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: 'Dialog',
|
||||
},
|
||||
/** Label for the built-in cancel button */
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: 'Cancel',
|
||||
},
|
||||
/** Label for the built-in confirm button */
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: 'Confirm',
|
||||
},
|
||||
},
|
||||
emits: ['update:open', 'confirm', 'cancel', 'close', 'opened'],
|
||||
setup(props, { slots, attrs, emit }) {
|
||||
const panelClasses = computed(() => modalPanelVariants())
|
||||
|
||||
const modalRef = ref<HTMLElement | null>(null)
|
||||
const isScrolled = ref(false)
|
||||
let previousOverflow = ''
|
||||
const titleId = `os-modal-title-${Math.random().toString(36).slice(2, 7)}`
|
||||
|
||||
/* v8 ignore start -- Vue 2 only */
|
||||
const instance = isVue2 ? getCurrentInstance() : null
|
||||
// In Vue 2, global h() needs currentInstance; use $createElement for icon render fns
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const createElement = isVue2 ? (instance?.proxy as any)?.$createElement : h
|
||||
/* v8 ignore stop */
|
||||
|
||||
// Vue 2's h() does NOT convert onClick → on.click. Helper to build event props.
|
||||
function eventProps(
|
||||
events: Record<string, (...args: unknown[]) => void>,
|
||||
): Record<string, unknown> {
|
||||
/* v8 ignore start -- Vue 2 branch */
|
||||
if (isVue2) {
|
||||
return { on: events }
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [name, fn] of Object.entries(events)) {
|
||||
result[`on${name.charAt(0).toUpperCase()}${name.slice(1)}`] = fn
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Scroll tracking for top fade ---
|
||||
function onContentScroll(e: Event) {
|
||||
const target = e.target as HTMLElement
|
||||
isScrolled.value = target.scrollTop > 0
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
function close(type: string) {
|
||||
emit('update:open', false)
|
||||
emit('close', type)
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
emit('confirm')
|
||||
close('confirm')
|
||||
}
|
||||
|
||||
function cancel(type = 'cancel') {
|
||||
emit('cancel')
|
||||
close(type)
|
||||
}
|
||||
|
||||
// --- Body scroll lock ---
|
||||
watch(
|
||||
() => props.open,
|
||||
(show) => {
|
||||
/* v8 ignore start -- SSR guard */
|
||||
if (typeof document === 'undefined') return
|
||||
/* v8 ignore stop */
|
||||
if (show) {
|
||||
previousOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
emit('opened')
|
||||
} else {
|
||||
document.body.style.overflow = previousOverflow
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// --- ESC key handler ---
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (props.open && e.key === 'Escape') {
|
||||
cancel('backdrop')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', onKeydown)
|
||||
/* v8 ignore start -- cleanup guard */
|
||||
if (typeof document !== 'undefined') {
|
||||
document.body.style.overflow = previousOverflow
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
})
|
||||
|
||||
// --- Focus trap ---
|
||||
/* v8 ignore start -- focus wrapping requires real browser (tested in visual tests) */
|
||||
function onFocusTrap(e: KeyboardEvent) {
|
||||
if (e.key !== 'Tab' || !modalRef.value) return
|
||||
|
||||
const focusable = modalRef.value.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
)
|
||||
if (focusable.length === 0) return
|
||||
|
||||
const first = focusable[0]
|
||||
const last = focusable[focusable.length - 1]
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault()
|
||||
last.focus()
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault()
|
||||
first.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
|
||||
return () => {
|
||||
if (!props.open) {
|
||||
// Render an empty wrapper so v-if consumers can still find the root element
|
||||
/* v8 ignore start -- Vue 2 branch */
|
||||
if (isVue2) {
|
||||
const proxy = instance?.proxy as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
||||
const parentDynClass = proxy?.$vnode?.data?.class
|
||||
return h('div', {
|
||||
class: ['os-modal-wrapper', parentClass, parentDynClass].filter(Boolean),
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
const { class: closedAttrClass } = attrs as Record<string, unknown>
|
||||
return h('div', { class: cn('os-modal-wrapper', (closedAttrClass as string) || '') })
|
||||
}
|
||||
|
||||
// --- Close button (native <button> for Vue 2/3 compatibility) ---
|
||||
const closeBtn = h(
|
||||
'button',
|
||||
{
|
||||
class:
|
||||
'os-modal__close absolute top-3 right-3 flex items-center justify-center w-[26px] h-[26px] rounded-full bg-transparent border-0 cursor-pointer p-0 hover:bg-[var(--color-default-hover)] text-[var(--color-default-contrast)]',
|
||||
type: 'button',
|
||||
'aria-label': 'Close',
|
||||
'data-testid': 'os-modal-close',
|
||||
...eventProps({ click: () => cancel('close') }),
|
||||
},
|
||||
[
|
||||
h('span', { class: 'w-4 h-4 fill-current inline-flex', 'aria-hidden': 'true' }, [
|
||||
IconClose(createElement, isVue2),
|
||||
]),
|
||||
],
|
||||
)
|
||||
|
||||
// --- Header ---
|
||||
const headerChildren: ReturnType<typeof h>[] = []
|
||||
if (props.title) {
|
||||
headerChildren.push(
|
||||
h(
|
||||
'h2',
|
||||
{ class: 'os-modal__title text-[1.5rem] font-semibold m-0', id: titleId },
|
||||
props.title,
|
||||
),
|
||||
)
|
||||
}
|
||||
headerChildren.push(closeBtn)
|
||||
|
||||
// Top fade: only visible when content is scrolled down
|
||||
if (isScrolled.value) {
|
||||
headerChildren.push(
|
||||
h('div', {
|
||||
class:
|
||||
'absolute bottom-0 left-0 w-[calc(100%-10px)] h-[30px] translate-y-full bg-gradient-to-b from-white to-transparent pointer-events-none z-10',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const header = h(
|
||||
'div',
|
||||
{ class: 'os-modal__header relative px-6 pt-5 pb-2 pr-12' },
|
||||
headerChildren,
|
||||
)
|
||||
|
||||
// --- Content ---
|
||||
const content = h(
|
||||
'div',
|
||||
{
|
||||
class: 'os-modal__content px-6 pt-4 pb-6 overflow-y-auto max-h-[50vh]',
|
||||
tabindex: '0',
|
||||
...eventProps({ scroll: onContentScroll as (...args: unknown[]) => void }),
|
||||
},
|
||||
slots.default?.(),
|
||||
)
|
||||
|
||||
// --- Footer ---
|
||||
let footerContent: ReturnType<typeof h>[] | undefined
|
||||
|
||||
if (slots.footer) {
|
||||
footerContent = slots.footer({ confirm, cancel })
|
||||
} else {
|
||||
/* v8 ignore start -- Vue 2 branch: must use VNode data format for component props */
|
||||
if (isVue2) {
|
||||
footerContent = [
|
||||
h(
|
||||
OsButton,
|
||||
{
|
||||
props: { appearance: 'ghost' },
|
||||
attrs: { 'data-testid': 'os-modal-cancel' },
|
||||
on: { click: () => cancel('cancel') },
|
||||
},
|
||||
[
|
||||
h('template', { slot: 'icon' }, [IconClose(createElement, true)]),
|
||||
props.cancelLabel,
|
||||
],
|
||||
),
|
||||
h(
|
||||
OsButton,
|
||||
{
|
||||
props: { variant: 'primary' },
|
||||
attrs: { 'data-testid': 'os-modal-confirm' },
|
||||
on: { click: () => confirm() },
|
||||
},
|
||||
[
|
||||
h('template', { slot: 'icon' }, [IconCheck(createElement, true)]),
|
||||
props.confirmLabel,
|
||||
],
|
||||
),
|
||||
]
|
||||
} else {
|
||||
/* v8 ignore stop */
|
||||
footerContent = [
|
||||
h(
|
||||
OsButton,
|
||||
{
|
||||
appearance: 'ghost',
|
||||
'data-testid': 'os-modal-cancel',
|
||||
onClick: () => cancel('cancel'),
|
||||
},
|
||||
{
|
||||
icon: () => [IconClose(createElement, false)],
|
||||
default: () => [props.cancelLabel],
|
||||
},
|
||||
),
|
||||
h(
|
||||
OsButton,
|
||||
{
|
||||
variant: 'primary',
|
||||
'data-testid': 'os-modal-confirm',
|
||||
onClick: () => confirm(),
|
||||
},
|
||||
{
|
||||
icon: () => [IconCheck(createElement, false)],
|
||||
default: () => [props.confirmLabel],
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const footer = h(
|
||||
'footer',
|
||||
{
|
||||
class:
|
||||
'os-modal__footer bg-[#f5f5f5] px-3 py-3 flex justify-end gap-2 rounded-b-lg shadow-[0_-20px_15px_-10px_rgba(255,255,255,0.9)]',
|
||||
},
|
||||
footerContent,
|
||||
)
|
||||
|
||||
// --- Backdrop (click-to-close is handled on the overlay wrapper below) ---
|
||||
const backdrop = h('div', {
|
||||
class: 'os-modal__backdrop fixed inset-0 bg-black/70 z-[9998]',
|
||||
'data-testid': 'os-modal-backdrop',
|
||||
})
|
||||
|
||||
// --- Panel (stops click propagation so overlay click only fires on backdrop area) ---
|
||||
const panelProps: Record<string, unknown> = {
|
||||
class: panelClasses.value,
|
||||
role: 'dialog',
|
||||
'aria-modal': 'true',
|
||||
'data-testid': 'os-modal-panel',
|
||||
...eventProps({
|
||||
keydown: onFocusTrap as (...args: unknown[]) => void,
|
||||
click: (e: unknown) => (e as Event).stopPropagation(),
|
||||
}),
|
||||
ref: modalRef,
|
||||
}
|
||||
if (props.title) {
|
||||
panelProps['aria-labelledby'] = titleId
|
||||
} else {
|
||||
panelProps['aria-label'] = props.ariaLabel
|
||||
}
|
||||
|
||||
const panel = h('div', panelProps, [header, content, footer])
|
||||
|
||||
// --- Overlay: captures clicks outside the panel to close ---
|
||||
const overlayProps: Record<string, unknown> = {
|
||||
class: 'os-modal__overlay fixed inset-0 z-[9999] flex items-center justify-center',
|
||||
...eventProps({
|
||||
click: () => cancel('backdrop'),
|
||||
}),
|
||||
}
|
||||
|
||||
const overlay = h('div', overlayProps, [panel])
|
||||
|
||||
// --- Root wrapper ---
|
||||
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
||||
if (isVue2) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const proxy = instance?.proxy as any
|
||||
const listeners = proxy?.$listeners || {}
|
||||
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
||||
const parentDynClass = proxy?.$vnode?.data?.class
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: ['os-modal-wrapper', parentClass, parentDynClass].filter(Boolean),
|
||||
attrs,
|
||||
on: {
|
||||
...listeners,
|
||||
keydown: onFocusTrap,
|
||||
},
|
||||
},
|
||||
[backdrop, overlay],
|
||||
)
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
|
||||
const { class: attrClass, ...restAttrs } = attrs as Record<string, unknown>
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: cn('os-modal-wrapper', (attrClass as string) || ''),
|
||||
...restAttrs,
|
||||
},
|
||||
[backdrop, overlay],
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
2
packages/ui/src/components/OsModal/index.ts
Normal file
2
packages/ui/src/components/OsModal/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as OsModal } from './OsModal.vue'
|
||||
export { modalPanelVariants } from './modal.variants'
|
||||
12
packages/ui/src/components/OsModal/modal.variants.ts
Normal file
12
packages/ui/src/components/OsModal/modal.variants.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
/**
|
||||
* Modal panel classes using CVA (Class Variance Authority)
|
||||
*/
|
||||
export const modalPanelVariants = cva([
|
||||
'os-modal',
|
||||
'relative',
|
||||
'bg-white rounded-lg shadow-xl',
|
||||
'flex flex-col',
|
||||
'w-[90vw] max-w-[500px]',
|
||||
])
|
||||
@ -28,3 +28,4 @@ export {
|
||||
type BadgeVariants,
|
||||
} from './OsBadge'
|
||||
export { OsNumber, numberVariants, type NumberVariants } from './OsNumber'
|
||||
export { OsModal, modalPanelVariants } from './OsModal'
|
||||
|
||||
7
packages/ui/src/svg-icon.d.ts
vendored
7
packages/ui/src/svg-icon.d.ts
vendored
@ -1,6 +1,9 @@
|
||||
declare module '*.svg?icon' {
|
||||
import type { VNode } from 'vue-demi'
|
||||
import type { Component, VNode } from 'vue-demi'
|
||||
|
||||
const icon: () => VNode
|
||||
// Icon render functions accept optional (h, isVue2) for cross-version rendering.
|
||||
// The intersection with Component allows usage in component registrations.
|
||||
type IconFn = ((h?: unknown, isVue2?: boolean) => VNode) & Component
|
||||
const icon: IconFn
|
||||
export default icon
|
||||
}
|
||||
|
||||
@ -347,7 +347,6 @@ $z-index-page-header: 2000;
|
||||
$z-index-page-sidebar: 1500;
|
||||
$z-index-sticky-float: 150;
|
||||
$z-index-sticky: 100;
|
||||
$z-index-post-teaser-link: 5;
|
||||
$z-index-surface: 1;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,32 +1,39 @@
|
||||
<template>
|
||||
<os-button
|
||||
data-test="join-leave-btn"
|
||||
:variant="isMember && hovered ? 'danger' : 'primary'"
|
||||
:appearance="filled || (isMember && !hovered) ? 'filled' : 'outline'"
|
||||
:disabled="disabled"
|
||||
:loading="localLoading"
|
||||
full-width
|
||||
v-tooltip="tooltip"
|
||||
@mouseenter="onHover"
|
||||
@mouseleave="hovered = false"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icon" />
|
||||
</template>
|
||||
{{ label }}
|
||||
</os-button>
|
||||
<div>
|
||||
<os-button
|
||||
data-test="join-leave-btn"
|
||||
:variant="isMember && hovered ? 'danger' : 'primary'"
|
||||
:appearance="filled || (isMember && !hovered) ? 'filled' : 'outline'"
|
||||
:disabled="disabled"
|
||||
:loading="localLoading"
|
||||
full-width
|
||||
v-tooltip="tooltip"
|
||||
@mouseenter="onHover"
|
||||
@mouseleave="hovered = false"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icon" />
|
||||
</template>
|
||||
{{ label }}
|
||||
</os-button>
|
||||
<confirm-modal
|
||||
v-if="showConfirmModal"
|
||||
:modalData="leaveModalData"
|
||||
@close="showConfirmModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
import { joinGroupMutation, leaveGroupMutation } from '~/graphql/groups'
|
||||
|
||||
export default {
|
||||
name: 'JoinLeaveButton',
|
||||
components: { OsButton, OsIcon },
|
||||
components: { ConfirmModal, OsButton, OsIcon },
|
||||
props: {
|
||||
group: { type: Object, required: true },
|
||||
userId: { type: String, required: true },
|
||||
@ -40,9 +47,32 @@ export default {
|
||||
return {
|
||||
localLoading: this.loading,
|
||||
hovered: false,
|
||||
showConfirmModal: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
leaveModalData() {
|
||||
return {
|
||||
titleIdent: 'group.leaveModal.title',
|
||||
messageIdent: 'group.leaveModal.message',
|
||||
messageParams: {
|
||||
name: this.group.name,
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.icons.signOut,
|
||||
textIdent: 'group.leaveModal.confirmButton',
|
||||
callback: this.joinLeave,
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
icon() {
|
||||
if (this.isMember) {
|
||||
if (this.isNonePendingMember) {
|
||||
@ -87,9 +117,6 @@ export default {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
onHover() {
|
||||
if (!this.disabled && !this.localLoading) {
|
||||
this.hovered = true
|
||||
@ -103,36 +130,7 @@ export default {
|
||||
}
|
||||
},
|
||||
openLeaveModal() {
|
||||
this.commitModalData(this.leaveModalData())
|
||||
},
|
||||
leaveModalData() {
|
||||
return {
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: 'group.leaveModal.title',
|
||||
messageIdent: 'group.leaveModal.message',
|
||||
messageParams: {
|
||||
name: this.group.name,
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.icons.signOut,
|
||||
textIdent: 'group.leaveModal.confirmButton',
|
||||
callback: this.joinLeave,
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
this.showConfirmModal = true
|
||||
},
|
||||
async joinLeave() {
|
||||
const join = !this.isMember
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JoinLeaveButton.vue shallowMount renders 1`] = `
|
||||
<os-button-stub as="button" variant="primary" appearance="outline" size="md" fullwidth="true" type="button" data-original-title="null" class=" has-tooltip">
|
||||
group.joinLeaveButton.join
|
||||
</os-button-stub>
|
||||
<div>
|
||||
<os-button-stub as="button" variant="primary" appearance="outline" size="md" fullwidth="true" type="button" data-original-title="null" class=" has-tooltip">
|
||||
group.joinLeaveButton.join
|
||||
</os-button-stub>
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -201,7 +201,7 @@ export default {
|
||||
const {
|
||||
data: { DeleteComment },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: CommentMutations(this.$i18n).DeleteComment,
|
||||
mutation: CommentMutations().DeleteComment,
|
||||
variables: { id: this.comment.id },
|
||||
})
|
||||
this.$toast.success(this.$t(`delete.comment.success`))
|
||||
|
||||
@ -101,14 +101,14 @@ export default {
|
||||
async handleSubmit() {
|
||||
const mutateParams = !this.update
|
||||
? {
|
||||
mutation: CommentMutations(this.$i18n).CreateComment,
|
||||
mutation: CommentMutations().CreateComment,
|
||||
variables: {
|
||||
postId: this.post.id,
|
||||
content: this.form.content,
|
||||
},
|
||||
}
|
||||
: {
|
||||
mutation: CommentMutations(this.$i18n).UpdateComment,
|
||||
mutation: CommentMutations().UpdateComment,
|
||||
variables: {
|
||||
id: this.comment.id,
|
||||
content: this.form.content,
|
||||
|
||||
@ -34,9 +34,6 @@ describe('ContentMenu.vue - Group', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
'modal/SET_OPEN': jest.fn(),
|
||||
}
|
||||
const getters = {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/isAdmin': () => false,
|
||||
@ -48,7 +45,7 @@ describe('ContentMenu.vue - Group', () => {
|
||||
}
|
||||
|
||||
const openContentMenu = async (values = {}) => {
|
||||
const store = new Vuex.Store({ mutations, getters, actions })
|
||||
const store = new Vuex.Store({ getters, actions })
|
||||
const wrapper = mount(ContentMenu, {
|
||||
propsData: {
|
||||
...values,
|
||||
|
||||
@ -14,9 +14,11 @@ const stubs = {
|
||||
'router-link': {
|
||||
template: '<span><slot /></span>',
|
||||
},
|
||||
'confirm-modal': { template: '<div class="confirm-modal-stub" />' },
|
||||
'report-modal': { template: '<div class="report-modal-stub" />' },
|
||||
}
|
||||
|
||||
let getters, mutations, actions, mocks, menuToggle, openModalSpy
|
||||
let getters, actions, mocks, menuToggle, openModalSpy
|
||||
|
||||
const maxPinnedPostsMock = jest.fn()
|
||||
const currentlyPinnedPostsMock = jest.fn()
|
||||
@ -35,9 +37,6 @@ describe('ContentMenu.vue', () => {
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
mutations = {
|
||||
'modal/SET_OPEN': jest.fn(),
|
||||
}
|
||||
getters = {
|
||||
'auth/isModerator': () => false,
|
||||
'auth/isAdmin': () => false,
|
||||
@ -49,7 +48,7 @@ describe('ContentMenu.vue', () => {
|
||||
}
|
||||
|
||||
const openContentMenu = async (values = {}) => {
|
||||
const store = new Vuex.Store({ mutations, getters, actions })
|
||||
const store = new Vuex.Store({ getters, actions })
|
||||
const wrapper = mount(ContentMenu, {
|
||||
propsData: {
|
||||
...values,
|
||||
@ -614,7 +613,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'disable.contribution.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('disable')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'disable')
|
||||
})
|
||||
|
||||
it('can disable comments', async () => {
|
||||
@ -632,7 +631,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'disable.comment.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('disable')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'disable')
|
||||
})
|
||||
|
||||
it('can disable users', async () => {
|
||||
@ -650,7 +649,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'disable.user.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('disable')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'disable')
|
||||
})
|
||||
|
||||
it('can disable organizations', async () => {
|
||||
@ -668,7 +667,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'disable.organization.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('disable')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'disable')
|
||||
})
|
||||
|
||||
it('can release posts', async () => {
|
||||
@ -686,7 +685,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'release.contribution.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'release')
|
||||
})
|
||||
|
||||
it('can release comments', async () => {
|
||||
@ -704,7 +703,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'release.comment.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'release')
|
||||
})
|
||||
|
||||
it('can release users', async () => {
|
||||
@ -722,7 +721,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'release.user.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'release')
|
||||
})
|
||||
|
||||
it('can release organizations', async () => {
|
||||
@ -740,7 +739,7 @@ describe('ContentMenu.vue', () => {
|
||||
.filter((item) => item.text() === 'release.organization.title')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('release')
|
||||
expect(openModalSpy).toHaveBeenCalledWith('confirm', 'release')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,53 +1,72 @@
|
||||
<template>
|
||||
<dropdown class="content-menu" :placement="placement" offset="5">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<os-button
|
||||
data-test="content-menu-button"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
circle
|
||||
:aria-label="$t('actions.menu')"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.ellipsisV" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<div class="content-menu-popover">
|
||||
<ds-menu :routes="routes">
|
||||
<template #menuitem="item">
|
||||
<ds-menu-item
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<os-icon :icon="item.route.icon" />
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
<div class="content-menu" @click.stop.prevent>
|
||||
<dropdown :placement="placement" offset="5">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<os-button
|
||||
data-test="content-menu-button"
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
circle
|
||||
:aria-label="$t('actions.menu')"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.ellipsisV" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<div class="content-menu-popover">
|
||||
<ds-menu :routes="routes">
|
||||
<template #menuitem="item">
|
||||
<ds-menu-item
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<os-icon :icon="item.route.icon" />
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
<confirm-modal
|
||||
v-if="showConfirmModal"
|
||||
:modalData="currentModalData"
|
||||
@close="showConfirmModal = false"
|
||||
/>
|
||||
<report-modal
|
||||
v-if="showReportModal"
|
||||
:name="getResourceName()"
|
||||
:type="resourceType"
|
||||
:id="resource.id"
|
||||
@close="showReportModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
import ReportModal from '~/components/Modal/ReportModal'
|
||||
import { reviewMutation } from '~/graphql/Moderation.js'
|
||||
import PinnedPostsMixin from '~/mixins/pinnedPosts'
|
||||
|
||||
export default {
|
||||
name: 'ContentMenu',
|
||||
components: {
|
||||
ConfirmModal,
|
||||
Dropdown,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
ReportModal,
|
||||
},
|
||||
mixins: [PinnedPostsMixin],
|
||||
props: {
|
||||
@ -69,6 +88,13 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showConfirmModal: false,
|
||||
showReportModal: false,
|
||||
currentModalData: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
@ -188,7 +214,7 @@ export default {
|
||||
routes.push({
|
||||
label: this.$t(`disable.${this.resourceType}.title`),
|
||||
callback: () => {
|
||||
this.openModal('disable')
|
||||
this.openModal('confirm', 'disable')
|
||||
},
|
||||
icon: this.icons.eyeSlash,
|
||||
})
|
||||
@ -196,7 +222,7 @@ export default {
|
||||
routes.push({
|
||||
label: this.$t(`release.${this.resourceType}.title`),
|
||||
callback: () => {
|
||||
this.openModal('release')
|
||||
this.openModal('confirm', 'release')
|
||||
},
|
||||
icon: this.icons.eye,
|
||||
})
|
||||
@ -308,14 +334,67 @@ export default {
|
||||
toggleMenu()
|
||||
},
|
||||
openModal(dialog, modalDataName = null) {
|
||||
this.$store.commit('modal/SET_OPEN', {
|
||||
name: dialog,
|
||||
data: {
|
||||
type: this.resourceType,
|
||||
resource: this.resource,
|
||||
modalData: modalDataName ? this.modalsData[modalDataName] : {},
|
||||
if (dialog === 'report') {
|
||||
this.showReportModal = true
|
||||
return
|
||||
}
|
||||
let modalData = {}
|
||||
if (modalDataName) {
|
||||
if (modalDataName === 'disable' || modalDataName === 'release') {
|
||||
modalData = this.reviewModalData(modalDataName)
|
||||
} else {
|
||||
modalData = this.modalsData[modalDataName] || {}
|
||||
}
|
||||
}
|
||||
this.currentModalData = modalData
|
||||
this.showConfirmModal = true
|
||||
},
|
||||
reviewModalData(action) {
|
||||
const disable = action === 'disable'
|
||||
const name = this.getResourceName()
|
||||
return {
|
||||
titleIdent: `${action}.${this.resourceType}.title`,
|
||||
messageIdent: `${action}.${this.resourceType}.message`,
|
||||
messageParams: { name: this.$filters.truncate(name, 30) },
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.icons.exclamationCircle,
|
||||
textIdent: `${action}.submit`,
|
||||
callback: async () => {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: reviewMutation(),
|
||||
variables: { resourceId: this.resource.id, disable, closed: false },
|
||||
})
|
||||
this.$toast.success(this.$t(`${action}.success`))
|
||||
this.$set(this.resource, 'disabled', disable)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: `${action}.cancel`,
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
getResourceName() {
|
||||
switch (this.resourceType) {
|
||||
case 'user':
|
||||
case 'organization':
|
||||
return this.resource.name || ''
|
||||
case 'contribution':
|
||||
return this.resource.title || ''
|
||||
case 'comment':
|
||||
return this.resource.author?.name || ''
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,40 +1,42 @@
|
||||
<template>
|
||||
<dropdown class="group-content-menu" :placement="placement" offset="5">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
circle
|
||||
:aria-label="$t('group.contentMenu.menuButton')"
|
||||
data-test="group-menu-button"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.ellipsisV" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<div class="group-menu-popover">
|
||||
<ds-menu :routes="routes">
|
||||
<template #menuitem="item">
|
||||
{{ item.parents }}
|
||||
<ds-menu-item
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<os-icon :icon="item.route.icon" />
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
<div class="content-menu" @click.stop.prevent>
|
||||
<dropdown class="group-content-menu" :placement="placement" offset="5">
|
||||
<template #default="{ toggleMenu }">
|
||||
<slot name="button" :toggleMenu="toggleMenu">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="outline"
|
||||
size="sm"
|
||||
circle
|
||||
:aria-label="$t('group.contentMenu.menuButton')"
|
||||
data-test="group-menu-button"
|
||||
@click.prevent="toggleMenu()"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.ellipsisV" />
|
||||
</template>
|
||||
</os-button>
|
||||
</slot>
|
||||
</template>
|
||||
<template #popover="{ toggleMenu }">
|
||||
<div class="group-menu-popover">
|
||||
<ds-menu :routes="routes">
|
||||
<template #menuitem="item">
|
||||
{{ item.parents }}
|
||||
<ds-menu-item
|
||||
:route="item.route"
|
||||
:parents="item.parents"
|
||||
@click.stop.prevent="openItem(item.route, toggleMenu)"
|
||||
>
|
||||
<os-icon :icon="item.route.icon" />
|
||||
{{ item.route.label }}
|
||||
</ds-menu-item>
|
||||
</template>
|
||||
</ds-menu>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@ -2,463 +2,479 @@
|
||||
|
||||
exports[`GroupContentMenu renders as groupProfile when I am the owner 1`] = `
|
||||
<div>
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
<div
|
||||
class="content-menu"
|
||||
>
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center"
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<nav
|
||||
class="ds-menu"
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
<nav
|
||||
class="ds-menu"
|
||||
>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15 4.594v22.813l-1.719-1.688-4.719-4.719h-4.563v-10h4.563l4.719-4.719zM13 9.438l-3.281 3.281-0.313 0.281h-3.406v6h3.406l0.313 0.281 3.281 3.281v-13.125zM20.219 11.781l2.781 2.781 2.781-2.781 1.438 1.438-2.781 2.781 2.781 2.781-1.438 1.438-2.781-2.781-2.781 2.781-1.438-1.438 2.781-2.781-2.781-2.781z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.muteGroup
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/edit/groupid"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M25 4.031c0.765 0 1.517 0.298 2.094 0.875 1.154 1.154 1.154 3.034 0 4.188l-10.094 10.125-0.313 0.063-3.5 0.688-1.469 0.313 0.313-1.469 0.688-3.5 0.063-0.313 0.219-0.219 9.906-9.875c0.577-0.577 1.329-0.875 2.094-0.875zM25 5.969c-0.235 0-0.464 0.121-0.688 0.344l-9.688 9.688-0.344 1.719 1.719-0.344 9.688-9.688c0.446-0.446 0.446-0.929 0-1.375-0.223-0.223-0.453-0.344-0.688-0.344zM4 8h13.188l-2 2h-9.188v16h16v-9.188l2-2v13.188h-20v-20z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
admin.settings.name
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/edit/groupid/invites"
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M21.75 4c1.671 0 3.225 0.661 4.406 1.844s1.844 2.735 1.844 4.406-0.662 3.255-1.844 4.438l-1.469 1.469c-1.181 1.183-2.766 1.844-4.438 1.844-0.793 0-1.565-0.153-2.281-0.438l1.625-1.625c0.215 0.038 0.432 0.063 0.656 0.063 1.138 0 2.226-0.445 3.031-1.25l1.469-1.469c1.66-1.66 1.66-4.372 0-6.031-0.804-0.805-1.863-1.25-3-1.25s-2.227 0.444-3.031 1.25l-1.469 1.469c-0.997 0.996-1.391 2.393-1.188 3.688l-1.625 1.625c-0.285-0.716-0.438-1.487-0.438-2.281 0-1.671 0.662-3.255 1.844-4.438l1.469-1.469c1.181-1.183 2.766-1.844 4.438-1.844zM19.281 11.281l1.438 1.438-8 8-1.438-1.438zM11.75 14c0.793 0 1.565 0.153 2.281 0.438l-1.625 1.625c-0.215-0.038-0.432-0.063-0.656-0.063-1.138 0-2.226 0.445-3.031 1.25l-1.469 1.469c-1.66 1.66-1.66 4.372 0 6.031 0.804 0.805 1.863 1.25 3 1.25s2.227-0.444 3.031-1.25l1.469-1.469c0.997-0.996 1.391-2.393 1.188-3.688l1.625-1.625c0.285 0.716 0.438 1.487 0.438 2.281 0 1.671-0.662 3.256-1.844 4.438l-1.469 1.469c-1.181 1.183-2.766 1.844-4.438 1.844s-3.225-0.661-4.406-1.844c-1.182-1.182-1.844-2.735-1.844-4.406s0.662-3.256 1.844-4.438l1.469-1.469c1.181-1.183 2.766-1.844 4.438-1.844z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.inviteLinks
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15 4.594v22.813l-1.719-1.688-4.719-4.719h-4.563v-10h4.563l4.719-4.719zM13 9.438l-3.281 3.281-0.313 0.281h-3.406v6h3.406l0.313 0.281 3.281 3.281v-13.125zM20.219 11.781l2.781 2.781 2.781-2.781 1.438 1.438-2.781 2.781 2.781 2.781-1.438 1.438-2.781-2.781-2.781 2.781-1.438-1.438 2.781-2.781-2.781-2.781z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.muteGroup
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/edit/groupid"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M25 4.031c0.765 0 1.517 0.298 2.094 0.875 1.154 1.154 1.154 3.034 0 4.188l-10.094 10.125-0.313 0.063-3.5 0.688-1.469 0.313 0.313-1.469 0.688-3.5 0.063-0.313 0.219-0.219 9.906-9.875c0.577-0.577 1.329-0.875 2.094-0.875zM25 5.969c-0.235 0-0.464 0.121-0.688 0.344l-9.688 9.688-0.344 1.719 1.719-0.344 9.688-9.688c0.446-0.446 0.446-0.929 0-1.375-0.223-0.223-0.453-0.344-0.688-0.344zM4 8h13.188l-2 2h-9.188v16h16v-9.188l2-2v13.188h-20v-20z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
admin.settings.name
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/edit/groupid/invites"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.75 4c1.671 0 3.225 0.661 4.406 1.844s1.844 2.735 1.844 4.406-0.662 3.255-1.844 4.438l-1.469 1.469c-1.181 1.183-2.766 1.844-4.438 1.844-0.793 0-1.565-0.153-2.281-0.438l1.625-1.625c0.215 0.038 0.432 0.063 0.656 0.063 1.138 0 2.226-0.445 3.031-1.25l1.469-1.469c1.66-1.66 1.66-4.372 0-6.031-0.804-0.805-1.863-1.25-3-1.25s-2.227 0.444-3.031 1.25l-1.469 1.469c-0.997 0.996-1.391 2.393-1.188 3.688l-1.625 1.625c-0.285-0.716-0.438-1.487-0.438-2.281 0-1.671 0.662-3.255 1.844-4.438l1.469-1.469c1.181-1.183 2.766-1.844 4.438-1.844zM19.281 11.281l1.438 1.438-8 8-1.438-1.438zM11.75 14c0.793 0 1.565 0.153 2.281 0.438l-1.625 1.625c-0.215-0.038-0.432-0.063-0.656-0.063-1.138 0-2.226 0.445-3.031 1.25l-1.469 1.469c-1.66 1.66-1.66 4.372 0 6.031 0.804 0.805 1.863 1.25 3 1.25s2.227-0.444 3.031-1.25l1.469-1.469c0.997-0.996 1.391-2.393 1.188-3.688l1.625-1.625c0.285 0.716 0.438 1.487 0.438 2.281 0 1.671-0.662 3.256-1.844 4.438l-1.469 1.469c-1.181 1.183-2.766 1.844-4.438 1.844s-3.225-0.661-4.406-1.844c-1.182-1.182-1.844-2.735-1.844-4.406s0.662-3.256 1.844-4.438l1.469-1.469c1.181-1.183 2.766-1.844 4.438-1.844z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.inviteLinks
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-popover-stub>
|
||||
</v-popover-stub>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GroupContentMenu renders as groupProfile, muted 1`] = `
|
||||
<div>
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
<div
|
||||
class="content-menu"
|
||||
>
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center"
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<nav
|
||||
class="ds-menu"
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
<nav
|
||||
class="ds-menu"
|
||||
>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/"
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M15 4.594v22.813l-1.719-1.688-4.719-4.719h-4.563v-10h4.563l4.719-4.719zM24.125 6.375c2.385 2.511 3.875 5.896 3.875 9.625s-1.49 7.113-3.875 9.625l-1.406-1.406c2.024-2.149 3.281-5.041 3.281-8.219s-1.257-6.071-3.281-8.219zM21.313 9.188c1.661 1.786 2.688 4.187 2.688 6.813s-1.026 5.026-2.688 6.813l-1.406-1.438c1.3-1.424 2.094-3.3 2.094-5.375s-0.794-3.952-2.094-5.375zM13 9.438l-3.281 3.281-0.313 0.281h-3.406v6h3.406l0.313 0.281 3.281 3.281v-13.125zM18.5 12.031c0.939 1.059 1.5 2.446 1.5 3.969s-0.561 2.91-1.5 3.969l-1.438-1.438c0.578-0.694 0.938-1.559 0.938-2.531s-0.36-1.837-0.938-2.531z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.unmuteGroup
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15 4.594v22.813l-1.719-1.688-4.719-4.719h-4.563v-10h4.563l4.719-4.719zM24.125 6.375c2.385 2.511 3.875 5.896 3.875 9.625s-1.49 7.113-3.875 9.625l-1.406-1.406c2.024-2.149 3.281-5.041 3.281-8.219s-1.257-6.071-3.281-8.219zM21.313 9.188c1.661 1.786 2.688 4.187 2.688 6.813s-1.026 5.026-2.688 6.813l-1.406-1.438c1.3-1.424 2.094-3.3 2.094-5.375s-0.794-3.952-2.094-5.375zM13 9.438l-3.281 3.281-0.313 0.281h-3.406v6h3.406l0.313 0.281 3.281 3.281v-13.125zM18.5 12.031c0.939 1.059 1.5 2.446 1.5 3.969s-0.561 2.91-1.5 3.969l-1.438-1.438c0.578-0.694 0.938-1.559 0.938-2.531s-0.36-1.837-0.938-2.531z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.unmuteGroup
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-popover-stub>
|
||||
</v-popover-stub>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GroupContentMenu renders as groupProfile, not muted 1`] = `
|
||||
<div>
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
<div
|
||||
class="content-menu"
|
||||
>
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center"
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<nav
|
||||
class="ds-menu"
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
<nav
|
||||
class="ds-menu"
|
||||
>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/"
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
exact="true"
|
||||
href="/"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M15 4.594v22.813l-1.719-1.688-4.719-4.719h-4.563v-10h4.563l4.719-4.719zM13 9.438l-3.281 3.281-0.313 0.281h-3.406v6h3.406l0.313 0.281 3.281 3.281v-13.125zM20.219 11.781l2.781 2.781 2.781-2.781 1.438 1.438-2.781 2.781 2.781 2.781-1.438 1.438-2.781-2.781-2.781 2.781-1.438-1.438 2.781-2.781-2.781-2.781z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.muteGroup
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15 4.594v22.813l-1.719-1.688-4.719-4.719h-4.563v-10h4.563l4.719-4.719zM13 9.438l-3.281 3.281-0.313 0.281h-3.406v6h3.406l0.313 0.281 3.281 3.281v-13.125zM20.219 11.781l2.781 2.781 2.781-2.781 1.438 1.438-2.781 2.781 2.781 2.781-1.438 1.438-2.781-2.781-2.781 2.781-1.438-1.438 2.781-2.781-2.781-2.781z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.muteGroup
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-popover-stub>
|
||||
</v-popover-stub>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GroupContentMenu renders as groupTeaser 1`] = `
|
||||
<div>
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
<div
|
||||
class="content-menu"
|
||||
>
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
<v-popover-stub
|
||||
autohide="true"
|
||||
class="group-content-menu"
|
||||
container="body"
|
||||
delay="0"
|
||||
handleresize="true"
|
||||
offset="5"
|
||||
openclass="open"
|
||||
opengroup="0"
|
||||
placement="bottom-end"
|
||||
popoverarrowclass="tooltip-arrow popover-arrow"
|
||||
popoverbaseclass="tooltip popover"
|
||||
popoverclass="vue-popover-theme"
|
||||
popoverinnerclass="tooltip-inner popover-inner"
|
||||
popoverwrapperclass="wrapper"
|
||||
popperoptions="[object Object]"
|
||||
trigger="manual"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center"
|
||||
<button
|
||||
aria-label="group.contentMenu.menuButton"
|
||||
class="os-button 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-variant="primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="inline-flex items-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
class="os-button__icon inline-flex items-center shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 6c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 22c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<nav
|
||||
class="ds-menu"
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="group-menu-popover"
|
||||
>
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
<nav
|
||||
class="ds-menu"
|
||||
>
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
<ul
|
||||
class="ds-menu-list"
|
||||
>
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/groupid"
|
||||
|
||||
|
||||
|
||||
<li
|
||||
class="ds-menu-item ds-menu-item-level-0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
<a
|
||||
class="ds-menu-item-link"
|
||||
href="/groups/groupid"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"
|
||||
>
|
||||
<path
|
||||
d="M16 2.594l0.719 0.688 13 13-1.438 1.438-1.281-1.281v11.563h-9v-10h-4v10h-9v-11.563l-1.281 1.281-1.438-1.438 13-13zM16 5.438l-9 9v11.563h5v-10h8v10h5v-11.563z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.visitGroupPage
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16 2.594l0.719 0.688 13 13-1.438 1.438-1.281-1.281v11.563h-9v-10h-4v10h-9v-11.563l-1.281 1.281-1.438-1.438 13-13zM16 5.438l-9 9v11.563h5v-10h8v10h5v-11.563z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
group.contentMenu.visitGroupPage
|
||||
|
||||
</a>
|
||||
<!---->
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-popover-stub>
|
||||
</v-popover-stub>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -11,8 +11,11 @@ exports[`CtaJoinLeaveGroup.vue mount renders 1`] = `
|
||||
</h4>
|
||||
<p class="ds-text">
|
||||
contribution.comment.commenting-disabled.no-group-member.call-to-action
|
||||
</p> <button data-variant="primary" data-appearance="filled" type="button" class="os-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)] 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 aria-hidden="true" class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor"><path d="M15 5h2v10h10v2h-10v10h-2v-10h-10v-2h10v-10z"></path></svg></span></span>
|
||||
group.joinLeaveButton.join
|
||||
</span></button>
|
||||
</p>
|
||||
<div><button data-variant="primary" data-appearance="filled" type="button" class="os-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)] 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 aria-hidden="true" class="os-icon inline-flex items-center align-bottom shrink-0 h-[1.2em] [&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor"><path d="M15 5h2v10h10v2h-10v10h-2v-10h-10v-2h10v-10z"></path></svg></span></span>
|
||||
group.joinLeaveButton.join
|
||||
</span></button>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -4,14 +4,10 @@
|
||||
<div class="ds-mb-small"></div>
|
||||
<div class="ds-mb-large">
|
||||
<select-user-search :id="id" ref="selectUserSearch" @select-user="selectUser" />
|
||||
<ds-modal
|
||||
<os-modal
|
||||
v-if="isOpen"
|
||||
force
|
||||
extended
|
||||
:confirm-label="$t('group.modal.confirm')"
|
||||
:cancel-label="$t('group.modal.cancel')"
|
||||
:title="$t('group.modal.confirmAddGroupMemberTitle')"
|
||||
v-model="isOpen"
|
||||
:title="$t('group.addMember')"
|
||||
:open.sync="isOpen"
|
||||
@close="closeModal"
|
||||
@confirm="confirmModal"
|
||||
@cancel="cancelModal"
|
||||
@ -19,18 +15,33 @@
|
||||
<p class="ds-text ds-text-size-large">
|
||||
{{ $t('group.modal.confirmAddGroupMemberText', { name: user.name }) }}
|
||||
</p>
|
||||
</ds-modal>
|
||||
<template #footer="{ confirm, cancel }">
|
||||
<os-button appearance="outline" @click="cancel">
|
||||
<template #icon><os-icon :icon="icons.close" /></template>
|
||||
{{ $t('group.modal.cancel') }}
|
||||
</os-button>
|
||||
<os-button variant="primary" @click="confirm">
|
||||
<template #icon><os-icon :icon="icons.check" /></template>
|
||||
{{ $t('group.modal.confirm') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</os-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon, OsModal } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { changeGroupMemberRoleMutation } from '~/graphql/groups.js'
|
||||
import SelectUserSearch from '~/components/generic/SelectUserSearch/SelectUserSearch'
|
||||
|
||||
export default {
|
||||
name: 'AddGroupMember',
|
||||
components: {
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsModal,
|
||||
SelectUserSearch,
|
||||
},
|
||||
props: {
|
||||
@ -43,6 +54,9 @@ export default {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: 'search-user-to-add-to-group',
|
||||
|
||||
@ -87,7 +87,7 @@ describe('GroupMember', () => {
|
||||
})
|
||||
|
||||
it('has no modal', () => {
|
||||
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||
expect(wrapper.find('div.os-modal-wrapper').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('change user role', () => {
|
||||
@ -145,22 +145,28 @@ describe('GroupMember', () => {
|
||||
})
|
||||
|
||||
it('opens the modal', () => {
|
||||
expect(wrapper.find('div.ds-modal-wrapper').isVisible()).toBe(true)
|
||||
expect(wrapper.find('div.os-modal-wrapper').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
describe('click on cancel', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('div.ds-modal-wrapper').find('button.ds-button-ghost').trigger('click')
|
||||
wrapper
|
||||
.find('div.os-modal-wrapper')
|
||||
.find('[data-testid="os-modal-cancel"]')
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
it('closes the modal', () => {
|
||||
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||
expect(wrapper.find('div.os-modal-wrapper').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on confirm with server error', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click')
|
||||
wrapper
|
||||
.find('div.os-modal-wrapper')
|
||||
.find('[data-testid="os-modal-confirm"]')
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
@ -168,14 +174,17 @@ describe('GroupMember', () => {
|
||||
})
|
||||
|
||||
it('closes the modal', () => {
|
||||
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||
expect(wrapper.find('div.os-modal-wrapper').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on confirm with success', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click')
|
||||
wrapper
|
||||
.find('div.os-modal-wrapper')
|
||||
.find('[data-testid="os-modal-confirm"]')
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
@ -194,7 +203,7 @@ describe('GroupMember', () => {
|
||||
})
|
||||
|
||||
it('closes the modal', () => {
|
||||
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||
expect(wrapper.find('div.os-modal-wrapper').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -80,6 +80,7 @@
|
||||
@click="
|
||||
isOpen = true
|
||||
userId = member.user.id
|
||||
userName = member.user.name
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
@ -92,20 +93,30 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ds-modal
|
||||
<os-modal
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
:title="`${$t('group.removeMember')}`"
|
||||
force
|
||||
extended
|
||||
:confirm-label="$t('group.removeMember')"
|
||||
:cancel-label="$t('actions.cancel')"
|
||||
:open.sync="isOpen"
|
||||
:title="$t('group.removeMemberTitle')"
|
||||
@confirm="removeUser()"
|
||||
/>
|
||||
>
|
||||
<p class="ds-text ds-text-size-large">
|
||||
{{ $t('group.removeMemberConfirmText', { name: userName }) }}
|
||||
</p>
|
||||
<template #footer="{ confirm, cancel }">
|
||||
<os-button appearance="outline" data-testid="os-modal-cancel" @click="cancel">
|
||||
<template #icon><os-icon :icon="icons.close" /></template>
|
||||
{{ $t('actions.cancel') }}
|
||||
</os-button>
|
||||
<os-button variant="danger" data-testid="os-modal-confirm" @click="confirm">
|
||||
<template #icon><os-icon :icon="icons.check" /></template>
|
||||
{{ $t('group.removeMember') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</os-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { OsBadge, OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { OsBadge, OsButton, OsIcon, OsModal } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js'
|
||||
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
|
||||
@ -118,6 +129,7 @@ export default {
|
||||
OsBadge,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsModal,
|
||||
ProfileAvatar,
|
||||
},
|
||||
props: {
|
||||
@ -143,6 +155,7 @@ export default {
|
||||
user: {},
|
||||
isOpen: false,
|
||||
userId: null,
|
||||
userName: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<nuxt-link
|
||||
class="group-teaser"
|
||||
:to="{ name: 'groups-id-slug', params: { id: group.id, slug: group.slug } }"
|
||||
@click.native.capture="guardNavigation"
|
||||
>
|
||||
<os-card
|
||||
:class="{
|
||||
@ -108,6 +109,13 @@ export default {
|
||||
created() {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
guardNavigation(event) {
|
||||
if (event.target.closest('.content-menu')) {
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
descriptionExcerpt() {
|
||||
return this.$filters.removeLinks(this.group.descriptionExcerpt)
|
||||
@ -174,7 +182,6 @@ export default {
|
||||
|
||||
> .content-menu {
|
||||
position: relative;
|
||||
z-index: $z-index-post-teaser-link;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,43 +1,52 @@
|
||||
<template>
|
||||
<dropdown class="invite-button" offset="8" :placement="placement" noMouseLeaveClosing>
|
||||
<template #default="{ toggleMenu }">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('invite-codes.button.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('invite-codes.button.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
@click.prevent="toggleMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.userPlus" />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
<template #popover>
|
||||
<div class="invite-list">
|
||||
<h2>{{ $t('invite-codes.my-invite-links') }}</h2>
|
||||
<invitation-list
|
||||
@generate-invite-code="generatePersonalInviteCode"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
:inviteCodes="user.inviteCodes"
|
||||
:copy-message="
|
||||
$t('invite-codes.invite-link-message-personal', {
|
||||
network: $env.NETWORK_NAME,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
<div class="invite-button">
|
||||
<dropdown ref="dropdown" offset="8" :placement="placement" noMouseLeaveClosing>
|
||||
<template #default="{ toggleMenu }">
|
||||
<os-button
|
||||
variant="primary"
|
||||
appearance="ghost"
|
||||
circle
|
||||
:aria-label="$t('invite-codes.button.tooltip')"
|
||||
v-tooltip="{
|
||||
content: $t('invite-codes.button.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
@click.prevent="toggleMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.userPlus" />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
<template #popover>
|
||||
<div class="invite-list">
|
||||
<h2>{{ $t('invite-codes.my-invite-links') }}</h2>
|
||||
<invitation-list
|
||||
@generate-invite-code="generatePersonalInviteCode"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
@open-delete-modal="openDeleteModal"
|
||||
:inviteCodes="user.inviteCodes"
|
||||
:copy-message="
|
||||
$t('invite-codes.invite-link-message-personal', {
|
||||
network: $env.NETWORK_NAME,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
<confirm-modal
|
||||
v-if="showConfirmModal"
|
||||
:modalData="currentModalData"
|
||||
@close="showConfirmModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
||||
@ -45,6 +54,7 @@ import { generatePersonalInviteCode, invalidateInviteCode } from '~/graphql/Invi
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmModal,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
Dropdown,
|
||||
@ -53,6 +63,12 @@ export default {
|
||||
props: {
|
||||
placement: { type: String, default: 'top-end' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showConfirmModal: false,
|
||||
currentModalData: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
@ -68,6 +84,11 @@ export default {
|
||||
...mapMutations({
|
||||
setCurrentUser: 'auth/SET_USER_PARTIAL',
|
||||
}),
|
||||
openDeleteModal(modalData) {
|
||||
this.$refs.dropdown.isPopoverOpen = false
|
||||
this.currentModalData = modalData
|
||||
this.showConfirmModal = true
|
||||
},
|
||||
async generatePersonalInviteCode(comment) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Modal from './Modal.vue'
|
||||
import ConfirmModal from './Modal/ConfirmModal.vue'
|
||||
import DisableModal from './Modal/DisableModal.vue'
|
||||
import ReportModal from './Modal/ReportModal.vue'
|
||||
import Vuex from 'vuex'
|
||||
import { getters, mutations } from '../store/modal'
|
||||
import Vue from 'vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('Modal.vue', () => {
|
||||
let wrapper
|
||||
let store
|
||||
let state
|
||||
let mocks
|
||||
|
||||
const createWrapper = (mountMethod) => {
|
||||
return () => {
|
||||
store = new Vuex.Store({
|
||||
state,
|
||||
getters: {
|
||||
'modal/open': getters.open,
|
||||
'modal/data': getters.data,
|
||||
},
|
||||
mutations: {
|
||||
'modal/SET_OPEN': mutations.SET_OPEN,
|
||||
},
|
||||
})
|
||||
return mountMethod(Modal, {
|
||||
store,
|
||||
mocks,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$filters: {
|
||||
truncate: (a) => a,
|
||||
},
|
||||
$toast: {
|
||||
success: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
$t: () => {},
|
||||
}
|
||||
state = {
|
||||
open: null,
|
||||
data: {},
|
||||
}
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
const Wrapper = createWrapper(shallowMount)
|
||||
|
||||
it('initially empty', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.findComponent(ConfirmModal).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(DisableModal).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(ReportModal).exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('store/modal holds data to disable', () => {
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
open: 'disable',
|
||||
data: {
|
||||
type: 'contribution',
|
||||
resource: {
|
||||
id: 'c456',
|
||||
title: 'some title',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders disable modal', () => {
|
||||
expect(wrapper.findComponent(DisableModal).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('passes data to disable modal', () => {
|
||||
expect(wrapper.findComponent(DisableModal).props()).toEqual({
|
||||
type: 'contribution',
|
||||
name: 'some title',
|
||||
id: 'c456',
|
||||
})
|
||||
})
|
||||
|
||||
describe('child component emits close', () => {
|
||||
it('turns empty', async () => {
|
||||
wrapper.findComponent(DisableModal).vm.$emit('close')
|
||||
await Vue.nextTick()
|
||||
expect(wrapper.findComponent(DisableModal).exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('store/modal data contains a comment', () => {
|
||||
it('passes author name to disable modal', () => {
|
||||
state.data = {
|
||||
type: 'comment',
|
||||
resource: {
|
||||
id: 'c456',
|
||||
author: {
|
||||
name: 'Author name',
|
||||
},
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.findComponent(DisableModal).props()).toEqual({
|
||||
type: 'comment',
|
||||
name: 'Author name',
|
||||
id: 'c456',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not crash if author is undefined', () => {
|
||||
state.data = {
|
||||
type: 'comment',
|
||||
resource: {
|
||||
id: 'c456',
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.findComponent(DisableModal).props()).toEqual({
|
||||
type: 'comment',
|
||||
name: '',
|
||||
id: 'c456',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('store/modal data contains an user', () => {
|
||||
it('passes user name to report modal', () => {
|
||||
state.data = {
|
||||
type: 'user',
|
||||
resource: {
|
||||
id: 'u456',
|
||||
name: 'Username',
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.findComponent(DisableModal).props()).toEqual({
|
||||
type: 'user',
|
||||
name: 'Username',
|
||||
id: 'u456',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('store/modal data contains no valid datatype', () => {
|
||||
it('passes something as datatype to modal', () => {
|
||||
state.data = {
|
||||
type: 'something',
|
||||
resource: {
|
||||
id: 's456',
|
||||
name: 'Username',
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.findComponent(DisableModal).props()).toEqual({
|
||||
type: 'something',
|
||||
name: null,
|
||||
id: 's456',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<div class="modal-wrapper">
|
||||
<!-- Todo: Put all modals with 2 buttons and equal properties in one customiced 'danger-action-modal' -->
|
||||
<disable-modal
|
||||
v-if="open === 'disable'"
|
||||
:id="data.resource.id"
|
||||
:type="data.type"
|
||||
:name="name"
|
||||
@close="close"
|
||||
/>
|
||||
<release-modal
|
||||
v-if="open === 'release'"
|
||||
:id="data.resource.id"
|
||||
:type="data.type"
|
||||
:name="name"
|
||||
@close="close"
|
||||
/>
|
||||
<report-modal
|
||||
v-if="open === 'report'"
|
||||
:id="data.resource.id"
|
||||
:type="data.type"
|
||||
:name="name"
|
||||
@close="close"
|
||||
/>
|
||||
<!-- "id", "type", and "name" props are only used for compatibility with the other modals -->
|
||||
<confirm-modal
|
||||
v-if="open === 'confirm'"
|
||||
:id="data.resource.id"
|
||||
:type="data.type"
|
||||
:name="name"
|
||||
:modalData="data.modalData"
|
||||
@close="close"
|
||||
/>
|
||||
<delete-user-modal v-if="open === 'delete'" :userdata="data.userdata" @close="close" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
import DisableModal from '~/components/Modal/DisableModal'
|
||||
import ReleaseModal from '~/components/ReleaseModal/ReleaseModal.vue'
|
||||
import ReportModal from '~/components/Modal/ReportModal'
|
||||
import DeleteUserModal from '~/components/Modal/DeleteUserModal.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Modal',
|
||||
components: {
|
||||
DisableModal,
|
||||
ReleaseModal,
|
||||
ReportModal,
|
||||
ConfirmModal,
|
||||
DeleteUserModal,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
data: 'modal/data',
|
||||
open: 'modal/open',
|
||||
}),
|
||||
name() {
|
||||
// REFACTORING: This gets unneccesary if we use "modalData" in all modals by probaply replacing them all by "confirm-modal"
|
||||
if (!this.data || !this.data.resource) return ''
|
||||
const {
|
||||
resource: { name, title, author },
|
||||
} = this.data
|
||||
switch (this.data.type) {
|
||||
case 'user':
|
||||
return name
|
||||
case 'contribution': // REFACTORING: In ConfirmModal – Already replaced "title" by "this.menuModalsData.delete.messageParams".
|
||||
return title
|
||||
case 'comment':
|
||||
return author && author.name
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$store.commit('modal/SET_OPEN', {})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -7,6 +7,9 @@ const localVue = global.localVue
|
||||
|
||||
const stubs = {
|
||||
'sweetalert-icon': true,
|
||||
'os-modal': {
|
||||
template: '<div><slot /><slot name="footer" /></div>',
|
||||
},
|
||||
}
|
||||
|
||||
describe('ConfirmModal.vue', () => {
|
||||
@ -20,9 +23,6 @@ describe('ConfirmModal.vue', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
type: 'contribution',
|
||||
id: 'p23',
|
||||
name: postName,
|
||||
modalData: postMenuModalsData(postName, confirmCallback, cancelCallback).delete,
|
||||
}
|
||||
mocks = {
|
||||
@ -63,12 +63,6 @@ describe('ConfirmModal.vue', () => {
|
||||
|
||||
describe('given a post', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
...propsData,
|
||||
type: 'contribution',
|
||||
id: 'p23',
|
||||
name: postName,
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel" data-test="confirm-modal">
|
||||
<os-modal :title="title" :open="isOpen" @cancel="cancel" data-test="confirm-modal">
|
||||
<transition name="ds-transition-fade">
|
||||
<div v-if="success" class="ds-flex ds-flex-centered hc-modal-success">
|
||||
<sweetalert-icon icon="success" />
|
||||
@ -37,11 +37,11 @@
|
||||
{{ $t(modalData.buttons.confirm.textIdent) }}
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</os-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { OsButton, OsIcon, OsModal } from '@ocelot-social/ui'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
|
||||
export default {
|
||||
@ -49,14 +49,12 @@ export default {
|
||||
components: {
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsModal,
|
||||
SweetalertIcon,
|
||||
},
|
||||
emits: ['close'],
|
||||
props: {
|
||||
name: { type: String, default: '' }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
type: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
modalData: { type: Object, required: true },
|
||||
id: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import DeleteUserModal from './DeleteUserModal.vue'
|
||||
const localVue = global.localVue
|
||||
|
||||
const stubs = {
|
||||
'sweetalert-icon': true,
|
||||
'nuxt-link': true,
|
||||
'client-only': true,
|
||||
}
|
||||
|
||||
localVue.use(DeleteUserModal)
|
||||
|
||||
const getters = {
|
||||
'auth/isAdmin': () => true,
|
||||
'auth/isModerator': () => false,
|
||||
}
|
||||
|
||||
describe('DeleteUserModal.vue', () => {
|
||||
const store = new Vuex.Store({ getters })
|
||||
let wrapper
|
||||
let propsData = {
|
||||
userdata: {
|
||||
name: 'another-user',
|
||||
slug: 'another-user',
|
||||
createdAt: '2020-08-12T08:34:05.803Z',
|
||||
contributionsCount: '4',
|
||||
commentedCount: '2',
|
||||
},
|
||||
}
|
||||
const mocks = {
|
||||
$t: jest.fn(),
|
||||
$filters: {
|
||||
truncate: (a) => a,
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
},
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
const Wrapper = () => {
|
||||
return shallowMount(DeleteUserModal, {
|
||||
propsData,
|
||||
mocks,
|
||||
store,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
|
||||
describe('defaults', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('success false', () => {
|
||||
expect(wrapper.vm.success).toBe(false)
|
||||
})
|
||||
|
||||
it('loading false', () => {
|
||||
expect(wrapper.vm.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(DeleteUserModal, {
|
||||
propsData,
|
||||
mocks,
|
||||
store,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
describe('given another user', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
...propsData,
|
||||
type: 'user',
|
||||
id: 'u4711',
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
describe('click cancel button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('button.cancel').trigger('click')
|
||||
})
|
||||
|
||||
it('does not emit "close" yet', () => {
|
||||
expect(wrapper.emitted().close).toBeFalsy()
|
||||
})
|
||||
|
||||
it('fades away', () => {
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
beforeEach(jest.runAllTimers)
|
||||
|
||||
it('emits "close"', () => {
|
||||
expect(wrapper.emitted().close).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<ds-modal class="delete-user-modal" :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<transition name="ds-transition-fade">
|
||||
<div v-if="success" class="ds-flex ds-flex-centered hc-modal-success">
|
||||
<sweetalert-icon icon="success" />
|
||||
</div>
|
||||
</transition>
|
||||
<div>
|
||||
<section class="ds-section">
|
||||
<div class="ds-flex">
|
||||
<div style="flex: 0 0 50%; width: 50%">
|
||||
<user-teaser :user="userdata" />
|
||||
</div>
|
||||
<div style="flex: 0 0 20%; width: 20%">
|
||||
<p class="ds-text ds-text-size-small">
|
||||
<span class="bold">{{ $t('modals.deleteUser.created') }}</span>
|
||||
<br />
|
||||
<date-time :date-time="userdata.createdAt" />
|
||||
</p>
|
||||
</div>
|
||||
<div style="flex: 0 0 15%; width: 15%">
|
||||
<p class="ds-text ds-text-size-small">
|
||||
<span class="bold">{{ $t('common.post', null, userdata.contributionsCount) }}</span>
|
||||
<br />
|
||||
{{ userdata.contributionsCount }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="flex: 0 0 15%; width: 15%">
|
||||
<p class="ds-text ds-text-size-small">
|
||||
<span class="bold">{{ $t('common.comment', null, userdata.commentedCount) }}</span>
|
||||
<br />
|
||||
{{ userdata.commentedCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||
{{ $t('actions.cancel') }}
|
||||
</os-button>
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
class="confirm"
|
||||
:loading="loading"
|
||||
@click="openModal"
|
||||
>
|
||||
<template #icon><os-icon :icon="icons.exclamationCircle" /></template>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import gql from 'graphql-tag'
|
||||
import { mapMutations } from 'vuex'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
import DateTime from '~/components/DateTime'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
|
||||
export default {
|
||||
name: 'DeleteUserModal',
|
||||
components: {
|
||||
DateTime,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
SweetalertIcon,
|
||||
UserTeaser,
|
||||
},
|
||||
props: {
|
||||
userdata: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: true,
|
||||
success: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.$t('settings.deleteUserAccount.name')
|
||||
},
|
||||
|
||||
modalData() {
|
||||
return (userdata) => {
|
||||
return {
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: userdata.name,
|
||||
resource: userdata,
|
||||
modalData: {
|
||||
titleIdent: this.$t('settings.deleteUserAccount.accountWarningIsAdmin'),
|
||||
messageIdent: this.$t('settings.deleteUserAccount.infoAdmin'),
|
||||
messageParams: {},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.icons.trash,
|
||||
textIdent: this.$t('settings.deleteUserAccount.confirmDeleting'),
|
||||
callback: () => {
|
||||
this.confirm(userdata)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: this.$t('actions.cancel'),
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
openModal() {
|
||||
this.commitModalData(this.modalData(this.userdata))
|
||||
},
|
||||
cancel() {
|
||||
// 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.cancel.callback()
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: ID!, $resource: [Deletable]) {
|
||||
DeleteUser(id: $id, resource: $resource) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: this.userdata.id, resource: ['Post', 'Comment'] },
|
||||
})
|
||||
this.success = true
|
||||
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
|
||||
setTimeout(() => {
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.success = false
|
||||
this.$emit('close')
|
||||
this.$router.replace('/')
|
||||
}, 500)
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
this.success = false
|
||||
this.$toast.error(err.message)
|
||||
this.isOpen = false
|
||||
this.$emit('close')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.delete-user-modal.ds-modal {
|
||||
max-width: 700px !important;
|
||||
}
|
||||
.delete-user-modal .hc-modal-success {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
opacity: 1;
|
||||
z-index: $z-index-modal;
|
||||
border-radius: $border-radius-x-large;
|
||||
}
|
||||
.delete-user-modal .bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@ -1,195 +0,0 @@
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import DisableModal from './DisableModal.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('DisableModal.vue', () => {
|
||||
let mocks
|
||||
let propsData
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
type: 'contribution',
|
||||
id: 'c42',
|
||||
name: 'blah',
|
||||
}
|
||||
mocks = {
|
||||
$filters: {
|
||||
truncate: (a) => a,
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$t: jest.fn(),
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValueOnce().mockRejectedValue({
|
||||
message: 'Not Authorized!',
|
||||
}),
|
||||
},
|
||||
location: {
|
||||
reload: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
const Wrapper = () => {
|
||||
return shallowMount(DisableModal, {
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
|
||||
describe('given a user', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
...propsData,
|
||||
type: 'user',
|
||||
name: 'Bob Ross',
|
||||
id: 'u2',
|
||||
}
|
||||
})
|
||||
|
||||
it('mentions user name', () => {
|
||||
Wrapper()
|
||||
const calls = mocks.$t.mock.calls
|
||||
const expected = [
|
||||
[
|
||||
'disable.user.message',
|
||||
{
|
||||
name: 'Bob Ross',
|
||||
},
|
||||
],
|
||||
]
|
||||
expect(calls).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a contribution', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
...propsData,
|
||||
type: 'contribution',
|
||||
name: 'This is some post title.',
|
||||
id: 'c3',
|
||||
}
|
||||
})
|
||||
|
||||
it('mentions contribution title', () => {
|
||||
Wrapper()
|
||||
const calls = mocks.$t.mock.calls
|
||||
const expected = [
|
||||
[
|
||||
'disable.contribution.message',
|
||||
{
|
||||
name: 'This is some post title.',
|
||||
},
|
||||
],
|
||||
]
|
||||
expect(calls).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(DisableModal, {
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
describe('given id', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
...propsData,
|
||||
type: 'user',
|
||||
id: 'u4711',
|
||||
}
|
||||
})
|
||||
|
||||
describe('click cancel button', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
await wrapper.find('button.cancel').trigger('click')
|
||||
})
|
||||
|
||||
it('does not emit "close" yet', () => {
|
||||
expect(wrapper.emitted().close).toBeFalsy()
|
||||
})
|
||||
|
||||
it('fades away', () => {
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
beforeEach(jest.runAllTimers)
|
||||
|
||||
it('does not call mutation', () => {
|
||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('emits close', () => {
|
||||
expect(wrapper.emitted().close).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('click confirm button', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
await wrapper.find('button.confirm').trigger('click')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('calls mutation', () => {
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes parameters to mutation', () => {
|
||||
const calls = mocks.$apollo.mutate.mock.calls
|
||||
const [[{ variables }]] = calls
|
||||
expect(variables).toMatchObject({
|
||||
resourceId: 'u4711',
|
||||
disable: true,
|
||||
closed: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('fades away', () => {
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
beforeEach(jest.runAllTimers)
|
||||
|
||||
it('emits close', () => {
|
||||
expect(wrapper.emitted().close).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handles errors', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
// second submission causes mutation to reject
|
||||
wrapper.find('button.confirm').trigger('click')
|
||||
})
|
||||
|
||||
it('shows an error toaster when mutation rejects', async () => {
|
||||
await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorized!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
|
||||
<template #footer>
|
||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||
{{ $t('disable.cancel') }}
|
||||
</os-button>
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
class="confirm"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
>
|
||||
<template #icon><os-icon :icon="icons.exclamationCircle" /></template>
|
||||
{{ $t('disable.submit') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'DisableModal',
|
||||
components: { OsButton, OsIcon },
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
type: { type: String, required: true },
|
||||
id: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: true,
|
||||
success: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.$t(`disable.${this.type}.title`)
|
||||
},
|
||||
message() {
|
||||
const name = this.$filters.truncate(this.name, 30)
|
||||
return this.$t(`disable.${this.type}.message`, { name })
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
async cancel() {
|
||||
// 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.cancel.callback()
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
this.loading = true
|
||||
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 !!!
|
||||
// await this.modalData.buttons.confirm.callback()
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||
disable
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { resourceId: this.id, disable: true, closed: false },
|
||||
})
|
||||
this.$toast.success(this.$t('disable.success'))
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
this.isOpen = false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -6,6 +6,9 @@ const localVue = global.localVue
|
||||
|
||||
const stubs = {
|
||||
'sweetalert-icon': true,
|
||||
'os-modal': {
|
||||
template: '<div><slot /><slot name="footer" /></div>',
|
||||
},
|
||||
}
|
||||
|
||||
describe('ReportModal.vue', () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-modal class="report-modal" :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<os-modal class="report-modal" :title="title" :open="isOpen" @cancel="cancel">
|
||||
<transition name="ds-transition-fade">
|
||||
<div v-if="success" class="ds-flex ds-flex-centered hc-modal-success">
|
||||
<sweetalert-icon icon="success" />
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
<div class="ds-mb-small"></div>
|
||||
<ds-radio
|
||||
v-model="form.reasonCategory"
|
||||
:schema="formSchema.reasonCategory"
|
||||
@ -27,7 +28,6 @@
|
||||
<small class="smallTag">
|
||||
{{ form.reasonDescription.length }}/{{ formSchema.reasonDescription.max }}
|
||||
</small>
|
||||
<div class="ds-mb-large"></div>
|
||||
<template #footer>
|
||||
<os-button class="cancel" variant="primary" appearance="outline" @click="cancel">
|
||||
<template #icon>
|
||||
@ -50,11 +50,11 @@
|
||||
{{ $t('report.submit') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</os-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { OsButton, OsIcon, OsModal } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||
import { reportMutation } from '~/graphql/Moderation.js'
|
||||
@ -66,6 +66,7 @@ export default {
|
||||
components: {
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsModal,
|
||||
SweetalertIcon,
|
||||
},
|
||||
props: {
|
||||
@ -169,7 +170,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.report-modal.ds-modal {
|
||||
.report-modal.os-modal {
|
||||
width: 700px !important;
|
||||
max-width: 700px !important;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
class="post-teaser"
|
||||
:class="{ 'post-teaser--horizontal': singleColumn && post.image }"
|
||||
:to="{ name: 'post-id-slug', params: { id: post.id, slug: post.slug } }"
|
||||
@click.native.capture="guardNavigation"
|
||||
>
|
||||
<os-card
|
||||
:lang="post.language"
|
||||
@ -259,6 +260,11 @@ export default {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
guardNavigation(event) {
|
||||
if (event.target.closest('.content-menu')) {
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
async deletePostCallback() {
|
||||
try {
|
||||
const {
|
||||
@ -412,7 +418,6 @@ export default {
|
||||
|
||||
> .content-menu {
|
||||
position: relative;
|
||||
z-index: $z-index-post-teaser-link;
|
||||
}
|
||||
|
||||
.os-badge {
|
||||
|
||||
@ -1,192 +0,0 @@
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import ReleaseModal from './ReleaseModal.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('ReleaseModal.vue', () => {
|
||||
let mocks
|
||||
let propsData
|
||||
let wrapper
|
||||
let Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
type: 'contribution',
|
||||
name: 'blah',
|
||||
id: 'c42',
|
||||
}
|
||||
mocks = {
|
||||
$filters: {
|
||||
truncate: (a) => a,
|
||||
},
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
$t: jest.fn(),
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValueOnce().mockRejectedValue({ message: 'Not Authorized!' }),
|
||||
},
|
||||
location: {
|
||||
reload: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
Wrapper = () => {
|
||||
return shallowMount(ReleaseModal, {
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
|
||||
describe('given a user', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
type: 'user',
|
||||
id: 'u2',
|
||||
name: 'Bob Ross',
|
||||
}
|
||||
})
|
||||
|
||||
it('mentions user name', () => {
|
||||
Wrapper()
|
||||
const calls = mocks.$t.mock.calls
|
||||
const expected = [
|
||||
[
|
||||
'release.user.message',
|
||||
{
|
||||
name: 'Bob Ross',
|
||||
},
|
||||
],
|
||||
]
|
||||
expect(calls).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a contribution', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
type: 'contribution',
|
||||
id: 'c3',
|
||||
name: 'This is some post title.',
|
||||
}
|
||||
})
|
||||
|
||||
it('mentions contribution title', () => {
|
||||
Wrapper()
|
||||
const calls = mocks.$t.mock.calls
|
||||
const expected = [
|
||||
[
|
||||
'release.contribution.message',
|
||||
{
|
||||
name: 'This is some post title.',
|
||||
},
|
||||
],
|
||||
]
|
||||
expect(calls).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
Wrapper = () => {
|
||||
return mount(ReleaseModal, {
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
describe('given id', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
type: 'user',
|
||||
id: 'u4711',
|
||||
}
|
||||
})
|
||||
|
||||
describe('click cancel button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('button.cancel').trigger('click')
|
||||
})
|
||||
|
||||
it('does not emit "close" yet', () => {
|
||||
expect(wrapper.emitted().close).toBeFalsy()
|
||||
})
|
||||
|
||||
it('fades away', () => {
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
beforeEach(jest.runAllTimers)
|
||||
|
||||
it('does not call mutation', () => {
|
||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('emits close', () => {
|
||||
expect(wrapper.emitted().close).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('click confirm button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('button.confirm').trigger('click')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('calls mutation', () => {
|
||||
expect(mocks.$apollo.mutate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes parameters to mutation', () => {
|
||||
const calls = mocks.$apollo.mutate.mock.calls
|
||||
const [[{ variables }]] = calls
|
||||
expect(variables).toMatchObject({
|
||||
resourceId: 'u4711',
|
||||
disable: false,
|
||||
closed: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('fades away', () => {
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
beforeEach(jest.runAllTimers)
|
||||
|
||||
it('emits close', () => {
|
||||
expect(wrapper.emitted().close).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handles errors', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
// second submission causes mutation to reject
|
||||
wrapper.find('button.confirm').trigger('click')
|
||||
})
|
||||
|
||||
it('shows an error toaster when mutation rejects', async () => {
|
||||
await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorized!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="message" />
|
||||
|
||||
<template #footer>
|
||||
<os-button variant="primary" appearance="outline" class="cancel" @click="cancel">
|
||||
{{ $t('release.cancel') }}
|
||||
</os-button>
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
class="confirm"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
>
|
||||
<template #icon>
|
||||
<os-icon :icon="icons.exclamationCircle" />
|
||||
</template>
|
||||
{{ $t('release.submit') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</ds-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'ReleaseModal',
|
||||
components: { OsButton, OsIcon },
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
type: { type: String, required: true },
|
||||
id: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: true,
|
||||
success: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.$t(`release.${this.type}.title`)
|
||||
},
|
||||
message() {
|
||||
const name = this.$filters.truncate(this.name, 30)
|
||||
return this.$t(`release.${this.type}.message`, { name })
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
// 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.cancel.callback()
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
},
|
||||
async confirm() {
|
||||
this.loading = true
|
||||
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 !!!
|
||||
// await this.modalData.buttons.confirm.callback()
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
|
||||
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
|
||||
disable
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { resourceId: this.id, disable: false, closed: false },
|
||||
})
|
||||
this.$toast.success(this.$t('release.success'))
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
this.isOpen = false
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
}, 1000)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -12,29 +12,15 @@ Object.assign(navigator, {
|
||||
},
|
||||
})
|
||||
|
||||
const mutations = {
|
||||
'modal/SET_OPEN': jest.fn(),
|
||||
}
|
||||
|
||||
describe('Invitation.vue', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
mutations['modal/SET_OPEN'].mockClear()
|
||||
navigator.clipboard.writeText.mockClear()
|
||||
})
|
||||
|
||||
const Wrapper = ({ wasRedeemed = false, withCopymessage = false }) => {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
modal: {
|
||||
namespaced: true,
|
||||
mutations: {
|
||||
SET_OPEN: mutations['modal/SET_OPEN'],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const store = new Vuex.Store({})
|
||||
const propsData = {
|
||||
inviteCode: {
|
||||
code: 'test-invite-code',
|
||||
@ -47,6 +33,9 @@ describe('Invitation.vue', () => {
|
||||
localVue,
|
||||
store,
|
||||
propsData,
|
||||
stubs: {
|
||||
'confirm-modal': { template: '<div data-test="confirm-modal" />' },
|
||||
},
|
||||
mocks: {
|
||||
$t: jest.fn((v) => v),
|
||||
$toast: {
|
||||
@ -120,32 +109,11 @@ describe('Invitation.vue', () => {
|
||||
wrapper = Wrapper({ wasRedeemed: false })
|
||||
})
|
||||
|
||||
it('opens the delete modal with correct payload', async () => {
|
||||
it('emits open-delete-modal with modal data', async () => {
|
||||
const deleteButton = screen.getByLabelText('invite-codes.invalidate')
|
||||
await fireEvent.click(deleteButton)
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
name: 'confirm',
|
||||
data: expect.objectContaining({
|
||||
modalData: expect.objectContaining({
|
||||
titleIdent: 'invite-codes.delete-modal.title',
|
||||
messageIdent: 'invite-codes.delete-modal.message',
|
||||
buttons: expect.objectContaining({
|
||||
confirm: expect.objectContaining({
|
||||
danger: true,
|
||||
textIdent: 'actions.delete',
|
||||
callback: expect.any(Function),
|
||||
}),
|
||||
cancel: expect.objectContaining({
|
||||
textIdent: 'actions.cancel',
|
||||
callback: expect.any(Function),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(wrapper.emitted()['open-delete-modal']).toBeTruthy()
|
||||
expect(wrapper.emitted()['open-delete-modal'][0][0]).toHaveProperty('buttons')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,8 +48,6 @@
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Invitation',
|
||||
components: {
|
||||
@ -67,6 +65,27 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
deleteModalData() {
|
||||
return {
|
||||
titleIdent: 'invite-codes.delete-modal.title',
|
||||
messageIdent: 'invite-codes.delete-modal.message',
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.icons.trash,
|
||||
textIdent: 'actions.delete',
|
||||
callback: () => {
|
||||
this.$emit('invalidate-invite-code', this.inviteCode.code)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
inviteLink() {
|
||||
return `${window.location.origin}/registration?method=invite-code&inviteCode=${this.inviteCode.code}`
|
||||
},
|
||||
@ -84,40 +103,12 @@ export default {
|
||||
this.canCopy = !!navigator.clipboard
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
async copyInviteCode() {
|
||||
await navigator.clipboard.writeText(this.inviteMessageAndLink)
|
||||
this.$toast.success(this.$t('invite-codes.copy-success'))
|
||||
},
|
||||
openDeleteModal() {
|
||||
this.commitModalData({
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: this.$t('invite-codes.delete-modal.title'),
|
||||
messageIdent: this.$t('invite-codes.delete-modal.message'),
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.icons.trash,
|
||||
textIdent: 'actions.delete',
|
||||
callback: () => {
|
||||
this.$emit('invalidate-invite-code', this.inviteCode.code)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
this.$emit('open-delete-modal', this.deleteModalData)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
:invite-code="inviteCode"
|
||||
:copy-message="copyMessage"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
@open-delete-modal="$emit('open-delete-modal', $event)"
|
||||
/>
|
||||
</client-only>
|
||||
</ul>
|
||||
|
||||
@ -11,7 +11,6 @@ describe('MySomethingList.vue', () => {
|
||||
let propsData
|
||||
let data
|
||||
let mocks
|
||||
let mutations
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
@ -43,9 +42,6 @@ describe('MySomethingList.vue', () => {
|
||||
success: jest.fn(),
|
||||
},
|
||||
}
|
||||
mutations = {
|
||||
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
@ -55,9 +51,7 @@ describe('MySomethingList.vue', () => {
|
||||
'list-item': '<div class="list-item"></div>',
|
||||
'edit-item': '<div class="edit-item"></div>',
|
||||
}
|
||||
const store = new Vuex.Store({
|
||||
mutations,
|
||||
})
|
||||
const store = new Vuex.Store({})
|
||||
return mount(MySomethingList, {
|
||||
propsData,
|
||||
data,
|
||||
@ -65,6 +59,9 @@ describe('MySomethingList.vue', () => {
|
||||
localVue,
|
||||
slots,
|
||||
store,
|
||||
stubs: {
|
||||
'confirm-modal': { template: '<div class="confirm-modal-stub" />' },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -134,42 +131,11 @@ describe('MySomethingList.vue', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('calls delete by committing "modal/SET_OPEN"', async () => {
|
||||
it('shows ConfirmModal when delete is clicked', async () => {
|
||||
const deleteButton = wrapper.find('button[data-test="delete-button"]')
|
||||
deleteButton.trigger('click')
|
||||
await Vue.nextTick()
|
||||
const expectedModalData = expect.objectContaining({
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: 'delete-modal.title',
|
||||
messageIdent: 'delete-modal.message',
|
||||
messageParams: {
|
||||
name: 'dummy',
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: ocelotIcons.trash,
|
||||
textIdent: 'delete-modal.confirm-button',
|
||||
callback: expect.any(Function),
|
||||
},
|
||||
cancel: {
|
||||
icon: ocelotIcons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: expect.any(Function),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expectedModalData,
|
||||
)
|
||||
expect(wrapper.find('.confirm-modal-stub').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -76,17 +76,18 @@
|
||||
{{ $t('actions.cancel') }}
|
||||
</os-button>
|
||||
</div>
|
||||
<confirm-modal v-if="showConfirmModal" :modalData="currentModalData" @close="closeModal" />
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { mapMutations } from 'vuex'
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
|
||||
export default {
|
||||
name: 'MySomethingList',
|
||||
components: { OsButton, OsIcon },
|
||||
components: { ConfirmModal, OsButton, OsIcon },
|
||||
props: {
|
||||
useFormData: { type: Object, default: () => ({}) },
|
||||
useFormSchema: { type: Object, default: () => ({}) },
|
||||
@ -116,6 +117,8 @@ export default {
|
||||
disabled: true,
|
||||
loading: false,
|
||||
editingItem: null,
|
||||
showConfirmModal: false,
|
||||
currentModalData: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -136,9 +139,6 @@ export default {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
handleInput(data) {
|
||||
this.callbacks.handleInput(this, data)
|
||||
this.disabled = true
|
||||
@ -170,35 +170,31 @@ export default {
|
||||
this.openModal(item)
|
||||
},
|
||||
openModal(item) {
|
||||
this.commitModalData(this.modalData(item))
|
||||
this.currentModalData = this.modalData(item)
|
||||
this.showConfirmModal = true
|
||||
},
|
||||
closeModal() {
|
||||
this.showConfirmModal = false
|
||||
this.currentModalData = null
|
||||
},
|
||||
modalData(item) {
|
||||
return {
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: this.texts.deleteModal.titleIdent,
|
||||
messageIdent: this.texts.deleteModal.messageIdent,
|
||||
messageParams: {
|
||||
name: item[this.namePropertyKey],
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.texts.deleteModal.confirm.icon,
|
||||
textIdent: this.texts.deleteModal.confirm.buttonTextIdent,
|
||||
callback: () => {
|
||||
this.callbacks.delete(this, item)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
titleIdent: this.texts.deleteModal.titleIdent,
|
||||
messageIdent: this.texts.deleteModal.messageIdent,
|
||||
messageParams: {
|
||||
name: item[this.namePropertyKey],
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.texts.deleteModal.confirm.icon,
|
||||
textIdent: this.texts.deleteModal.confirm.buttonTextIdent,
|
||||
callback: () => this.callbacks.delete(this, item),
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -389,7 +389,7 @@ export default {
|
||||
},
|
||||
searchGroups: {
|
||||
query() {
|
||||
return searchGroups(this.i18n)
|
||||
return searchGroups(this.$i18n)
|
||||
},
|
||||
variables() {
|
||||
const { firstGroups, groupsOffset, search } = this
|
||||
|
||||
@ -10,10 +10,11 @@ const localVue = global.localVue
|
||||
const stubs = {
|
||||
'client-only': true,
|
||||
'nuxt-link': true,
|
||||
'confirm-modal': { template: '<div class="confirm-modal-stub" />' },
|
||||
}
|
||||
|
||||
describe('ReportList', () => {
|
||||
let mocks, mutations, getters, wrapper
|
||||
let mocks, getters, wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
@ -30,9 +31,6 @@ describe('ReportList', () => {
|
||||
error: jest.fn((message) => message),
|
||||
},
|
||||
}
|
||||
mutations = {
|
||||
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||
}
|
||||
getters = {
|
||||
'auth/user': () => {
|
||||
return { slug: 'awesome-user' }
|
||||
@ -44,7 +42,6 @@ describe('ReportList', () => {
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
mutations,
|
||||
getters,
|
||||
})
|
||||
return mount(ReportList, { mocks, localVue, store, stubs })
|
||||
@ -71,8 +68,8 @@ describe('ReportList', () => {
|
||||
wrapper.findComponent(ReportsTable).vm.$emit('confirm', reports[0])
|
||||
})
|
||||
|
||||
it('calls modal/SET_OPEN', () => {
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||
it('shows ConfirmModal', () => {
|
||||
expect(wrapper.find('.confirm-modal-stub').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,13 +8,21 @@
|
||||
</div>
|
||||
<reports-table :reports="reports" @confirm="openModal" />
|
||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
||||
<confirm-modal
|
||||
v-if="showConfirmModal"
|
||||
:modalData="currentModalData"
|
||||
@close="
|
||||
showConfirmModal = false
|
||||
currentModalData = null
|
||||
"
|
||||
/>
|
||||
</os-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCard } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { mapMutations } from 'vuex'
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||
import ReportsTable from '~/components/features/ReportsTable/ReportsTable'
|
||||
import { reportsListQuery, reviewMutation } from '~/graphql/Moderation.js'
|
||||
@ -22,6 +30,7 @@ import PaginationButtons from '~/components/_new/generic/PaginationButtons/Pagin
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmModal,
|
||||
OsCard,
|
||||
DropdownFilter,
|
||||
ReportsTable,
|
||||
@ -42,6 +51,8 @@ export default {
|
||||
closed: null,
|
||||
hasNext: false,
|
||||
selected: this.$t('moderation.reports.filterLabel.all'),
|
||||
showConfirmModal: false,
|
||||
currentModalData: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -73,37 +84,25 @@ export default {
|
||||
'.' +
|
||||
(report.resource.disabled ? 'disable' : 'enable')
|
||||
return {
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: report.resource.__typename,
|
||||
resource: report.resource,
|
||||
modalData: {
|
||||
titleIdent: identStart + '.title',
|
||||
messageIdent: identStart + '.message',
|
||||
messageParams: {
|
||||
name:
|
||||
report.resource.name ||
|
||||
this.$filters.truncate(report.resource.title, 30) ||
|
||||
this.$filters.truncate(
|
||||
this.$filters.removeHtml(report.resource.contentExcerpt),
|
||||
30,
|
||||
),
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: report.resource.disabled ? this.icons.eyeSlash : this.icons.eye,
|
||||
textIdent: 'moderation.reports.decideModal.submit',
|
||||
callback: () => {
|
||||
this.confirmCallback(report.resource)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'moderation.reports.decideModal.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
titleIdent: identStart + '.title',
|
||||
messageIdent: identStart + '.message',
|
||||
messageParams: {
|
||||
name:
|
||||
report.resource.name ||
|
||||
this.$filters.truncate(report.resource.title, 30) ||
|
||||
this.$filters.truncate(this.$filters.removeHtml(report.resource.contentExcerpt), 30),
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: report.resource.disabled ? this.icons.eyeSlash : this.icons.eye,
|
||||
textIdent: 'moderation.reports.decideModal.submit',
|
||||
callback: () => this.confirmCallback(report.resource),
|
||||
},
|
||||
cancel: {
|
||||
icon: this.icons.close,
|
||||
textIdent: 'moderation.reports.decideModal.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -117,9 +116,6 @@ export default {
|
||||
this.icons = iconRegistry
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
filter(option) {
|
||||
this.selected = option.label
|
||||
this.offset = 0
|
||||
@ -128,19 +124,21 @@ export default {
|
||||
},
|
||||
async confirmCallback(resource) {
|
||||
const { disabled: disable, id: resourceId } = resource
|
||||
this.$apollo
|
||||
.mutate({
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: reviewMutation(),
|
||||
variables: { disable, resourceId, closed: true },
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.success(this.$t('moderation.reports.DecisionSuccess'))
|
||||
this.$apollo.queries.reportsList.refetch()
|
||||
})
|
||||
.catch((error) => this.$toast.error(error.message))
|
||||
this.$toast.success(this.$t('moderation.reports.DecisionSuccess'))
|
||||
await this.$apollo.queries.reportsList.refetch()
|
||||
} catch (error) {
|
||||
this.$toast.error(error.message)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
openModal(report) {
|
||||
this.commitModalData(this.modalData(report))
|
||||
this.currentModalData = this.modalData(report)
|
||||
this.showConfirmModal = true
|
||||
},
|
||||
back() {
|
||||
this.offset = Math.max(this.offset - this.pageSize, 0)
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { imageUrls } from './fragments/imageUrls'
|
||||
|
||||
export default (i18n) => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
export default () => {
|
||||
return {
|
||||
CreateComment: gql`
|
||||
${imageUrls}
|
||||
@ -68,7 +67,7 @@ export default (i18n) => {
|
||||
DeleteComment: gql`
|
||||
${imageUrls}
|
||||
|
||||
mutation($id: ID!) {
|
||||
mutation ($id: ID!) {
|
||||
DeleteComment(id: $id) {
|
||||
id
|
||||
contentExcerpt
|
||||
@ -90,10 +89,7 @@ export default (i18n) => {
|
||||
commentedCount
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
location {
|
||||
name: name${lang}
|
||||
}
|
||||
badges {
|
||||
badgeTrophies {
|
||||
id
|
||||
icon
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { imageUrls } from './fragments/imageUrls'
|
||||
|
||||
export default (app) => {
|
||||
const lang = app.$i18n.locale().toUpperCase()
|
||||
export default () => {
|
||||
return gql`
|
||||
${imageUrls}
|
||||
|
||||
@ -25,10 +24,7 @@ export default (app) => {
|
||||
commentedCount
|
||||
followedByCount
|
||||
followedByCurrentUser
|
||||
location {
|
||||
name: name${lang}
|
||||
}
|
||||
badges {
|
||||
badgeTrophies {
|
||||
id
|
||||
icon
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ export const searchGroups = (i18n) => {
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
}
|
||||
avatar {
|
||||
...imageUrls
|
||||
}
|
||||
|
||||
@ -10,9 +10,6 @@
|
||||
</div>
|
||||
<page-footer class="desktop-footer" />
|
||||
<div id="overlay" />
|
||||
<client-only>
|
||||
<modal />
|
||||
</client-only>
|
||||
<div v-if="getShowChat.showChat" class="chat-modul">
|
||||
<client-only>
|
||||
<chat singleRoom :roomId="getShowChat.roomID" @close-single-room="closeSingleRoom" />
|
||||
@ -26,14 +23,12 @@ import { mapGetters, mapMutations } from 'vuex'
|
||||
import seo from '~/mixins/seo'
|
||||
import mobile from '~/mixins/mobile'
|
||||
import HeaderMenu from '~/components/HeaderMenu/HeaderMenu'
|
||||
import Modal from '~/components/Modal'
|
||||
import PageFooter from '~/components/PageFooter/PageFooter'
|
||||
import Chat from '~/components/Chat/Chat.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HeaderMenu,
|
||||
Modal,
|
||||
PageFooter,
|
||||
Chat,
|
||||
},
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regional"
|
||||
},
|
||||
"actionRadius": "Aktionsradius der Gruppe",
|
||||
"addMember": "Mitglied hinzufügen",
|
||||
"addMemberToGroup": "Zur Gruppe hinzufügen",
|
||||
"addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!",
|
||||
"addUser": "Nutzer hinzufügen",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Radius",
|
||||
"removeMember": "Mitglied aus der Gruppe entfernen?",
|
||||
"removeMemberButton": "Entfernen",
|
||||
"removeMemberConfirmText": "Nutzer \"{name}\" aus der Gruppe entfernen?",
|
||||
"removeMemberTitle": "Mitglied entfernen",
|
||||
"role": "Deine Rolle in der Gruppe",
|
||||
"roles": {
|
||||
"admin": "Administrator",
|
||||
@ -976,7 +979,7 @@
|
||||
"title": "Beitrag freigeben",
|
||||
"type": "Beitrag"
|
||||
},
|
||||
"submit": "freigeben",
|
||||
"submit": "Freigeben",
|
||||
"success": "Erfolgreich freigegeben!",
|
||||
"user": {
|
||||
"error": "Den Nutzer hast du schon gemeldet!",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regional"
|
||||
},
|
||||
"actionRadius": "Action radius of the group",
|
||||
"addMember": "Add Member",
|
||||
"addMemberToGroup": "Add to group",
|
||||
"addMemberToGroupSuccess": "“{name}” was added to the group with the role “{role}”!",
|
||||
"addUser": "Add User",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Radius",
|
||||
"removeMember": "Remove member",
|
||||
"removeMemberButton": "Remove",
|
||||
"removeMemberConfirmText": "Remove user \"{name}\" from the group?",
|
||||
"removeMemberTitle": "Remove Member",
|
||||
"role": "Your role in the group",
|
||||
"roles": {
|
||||
"admin": "Administrator",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regional"
|
||||
},
|
||||
"actionRadius": "Radio de acción del grupo",
|
||||
"addMember": "Añadir miembro",
|
||||
"addMemberToGroup": "Añadir al grupo",
|
||||
"addMemberToGroupSuccess": "¡“{name}” fue añadido al grupo con el rol “{role}”!",
|
||||
"addUser": "Añadir usuario",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Radio",
|
||||
"removeMember": "Eliminar miembro",
|
||||
"removeMemberButton": "Eliminar",
|
||||
"removeMemberConfirmText": "¿Eliminar al usuario \"{name}\" del grupo?",
|
||||
"removeMemberTitle": "Eliminar miembro",
|
||||
"role": "Tu rol en el grupo",
|
||||
"roles": {
|
||||
"admin": "Administrador",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Régional"
|
||||
},
|
||||
"actionRadius": "Rayon d'action du groupe",
|
||||
"addMember": "Ajouter un membre",
|
||||
"addMemberToGroup": "Ajouter au groupe",
|
||||
"addMemberToGroupSuccess": "“{name}” a été ajouté au groupe avec le rôle “{role}” !",
|
||||
"addUser": "Ajouter un utilisateur",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Rayon",
|
||||
"removeMember": "Retirer le membre",
|
||||
"removeMemberButton": "Retirer",
|
||||
"removeMemberConfirmText": "Retirer l'utilisateur « {name} » du groupe ?",
|
||||
"removeMemberTitle": "Retirer le membre",
|
||||
"role": "Ton rôle dans le groupe",
|
||||
"roles": {
|
||||
"admin": "Administrateur",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regionale"
|
||||
},
|
||||
"actionRadius": "Raggio d'azione del gruppo",
|
||||
"addMember": "Aggiungi membro",
|
||||
"addMemberToGroup": "Aggiungi al gruppo",
|
||||
"addMemberToGroupSuccess": "“{name}” è stato aggiunto al gruppo con il ruolo “{role}”!",
|
||||
"addUser": "Aggiungi utente",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Raggio",
|
||||
"removeMember": "Rimuovi membro",
|
||||
"removeMemberButton": "Rimuovi",
|
||||
"removeMemberConfirmText": "Rimuovere l'utente \"{name}\" dal gruppo?",
|
||||
"removeMemberTitle": "Rimuovi membro",
|
||||
"role": "Il tuo ruolo nel gruppo",
|
||||
"roles": {
|
||||
"admin": "Amministratore",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regionaal"
|
||||
},
|
||||
"actionRadius": "Actieradius van de groep",
|
||||
"addMember": "Lid toevoegen",
|
||||
"addMemberToGroup": "Toevoegen aan groep",
|
||||
"addMemberToGroupSuccess": "“{name}” is toegevoegd aan de groep met de rol “{role}”!",
|
||||
"addUser": "Gebruiker toevoegen",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Radius",
|
||||
"removeMember": "Lid verwijderen",
|
||||
"removeMemberButton": "Verwijderen",
|
||||
"removeMemberConfirmText": "Gebruiker \"{name}\" uit de groep verwijderen?",
|
||||
"removeMemberTitle": "Lid verwijderen",
|
||||
"role": "Jouw rol in de groep",
|
||||
"roles": {
|
||||
"admin": "Beheerder",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regionalny"
|
||||
},
|
||||
"actionRadius": "Zasięg działania grupy",
|
||||
"addMember": "Dodaj członka",
|
||||
"addMemberToGroup": "Dodaj do grupy",
|
||||
"addMemberToGroupSuccess": "“{name}” został/a dodany/a do grupy z rolą “{role}”!",
|
||||
"addUser": "Dodaj użytkownika",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Zasięg",
|
||||
"removeMember": "Usuń członka",
|
||||
"removeMemberButton": "Usuń",
|
||||
"removeMemberConfirmText": "Usunąć użytkownika \"{name}\" z grupy?",
|
||||
"removeMemberTitle": "Usuń członka",
|
||||
"role": "Twoja rola w grupie",
|
||||
"roles": {
|
||||
"admin": "Administrator",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Regional"
|
||||
},
|
||||
"actionRadius": "Raio de ação do grupo",
|
||||
"addMember": "Adicionar membro",
|
||||
"addMemberToGroup": "Adicionar ao grupo",
|
||||
"addMemberToGroupSuccess": "“{name}” foi adicionado ao grupo com o papel “{role}”!",
|
||||
"addUser": "Adicionar usuário",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Raio",
|
||||
"removeMember": "Remover membro",
|
||||
"removeMemberButton": "Remover",
|
||||
"removeMemberConfirmText": "Remover o usuário \"{name}\" do grupo?",
|
||||
"removeMemberTitle": "Remover membro",
|
||||
"role": "Seu papel no grupo",
|
||||
"roles": {
|
||||
"admin": "Administrador",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Региональный"
|
||||
},
|
||||
"actionRadius": "Радиус действия группы",
|
||||
"addMember": "Добавить участника",
|
||||
"addMemberToGroup": "Добавить в группу",
|
||||
"addMemberToGroupSuccess": "“{name}” добавлен в группу с ролью “{role}”!",
|
||||
"addUser": "Добавить пользователя",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Радиус",
|
||||
"removeMember": "Удалить участника",
|
||||
"removeMemberButton": "Удалить",
|
||||
"removeMemberConfirmText": "Удалить пользователя «{name}» из группы?",
|
||||
"removeMemberTitle": "Удалить участника",
|
||||
"role": "Твоя роль в группе",
|
||||
"roles": {
|
||||
"admin": "Администратор",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Rajonal"
|
||||
},
|
||||
"actionRadius": "Rrezja e veprimit të grupit",
|
||||
"addMember": "Shto anëtar",
|
||||
"addMemberToGroup": "Shto në grup",
|
||||
"addMemberToGroupSuccess": "“{name}” u shtua në grup me rolin “{role}”!",
|
||||
"addUser": "Shto përdorues",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Rrezja",
|
||||
"removeMember": "Hiq anëtarin",
|
||||
"removeMemberButton": "Hiq",
|
||||
"removeMemberConfirmText": "Hiq përdoruesin \"{name}\" nga grupi?",
|
||||
"removeMemberTitle": "Hiq anëtarin",
|
||||
"role": "Roli yt në grup",
|
||||
"roles": {
|
||||
"admin": "Administrator",
|
||||
|
||||
@ -541,6 +541,7 @@
|
||||
"regional": "Регіональний"
|
||||
},
|
||||
"actionRadius": "Радіус дії групи",
|
||||
"addMember": "Додати учасника",
|
||||
"addMemberToGroup": "Додати до групи",
|
||||
"addMemberToGroupSuccess": "«{name}» додано до групи з роллю «{role}»!",
|
||||
"addUser": "Додати користувача",
|
||||
@ -612,6 +613,8 @@
|
||||
"radius": "Радіус",
|
||||
"removeMember": "Видалити учасника",
|
||||
"removeMemberButton": "Видалити",
|
||||
"removeMemberConfirmText": "Видалити користувача «{name}» з групи?",
|
||||
"removeMemberTitle": "Видалити учасника",
|
||||
"role": "Ваша роль у групі",
|
||||
"roles": {
|
||||
"admin": "Адміністратор",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -569,12 +569,14 @@ export default {
|
||||
// this.user.followedByCurrentUser = followedByCurrentUser
|
||||
// this.user.followedBy = followedBy
|
||||
// },
|
||||
updateJoinLeave() {
|
||||
this.$apollo.queries.Group.refetch()
|
||||
if (this.isAllowedSeeingGroupMembers) {
|
||||
async updateJoinLeave() {
|
||||
this.loadGroupMembers = false
|
||||
this.GroupMembers = []
|
||||
await this.$apollo.queries.Group.refetch()
|
||||
await this.$nextTick()
|
||||
this.loadGroupMembers = this.isAllowedSeeingGroupMembers
|
||||
if (this.loadGroupMembers) {
|
||||
this.$apollo.queries.GroupMembers.refetch()
|
||||
} else {
|
||||
this.GroupMembers = []
|
||||
}
|
||||
},
|
||||
fetchAllMembers() {
|
||||
|
||||
@ -207,6 +207,8 @@ exports[`invites.vue renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<invitation-list
|
||||
@generate-invite-code="generateGroupInviteCode"
|
||||
@invalidate-invite-code="invalidateInviteCode"
|
||||
@open-delete-modal="openDeleteModal"
|
||||
:inviteCodes="group.inviteCodes"
|
||||
:copy-message="
|
||||
group.groupType === 'hidden'
|
||||
@ -19,19 +20,32 @@
|
||||
"
|
||||
/>
|
||||
</os-card>
|
||||
<confirm-modal
|
||||
v-if="showConfirmModal"
|
||||
:modalData="currentModalData"
|
||||
@close="showConfirmModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCard } from '@ocelot-social/ui'
|
||||
import ConfirmModal from '~/components/Modal/ConfirmModal'
|
||||
import InvitationList from '~/components/_new/features/Invitations/InvitationList.vue'
|
||||
import { generateGroupInviteCode, invalidateInviteCode } from '~/graphql/InviteCode'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmModal,
|
||||
OsCard,
|
||||
InvitationList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showConfirmModal: false,
|
||||
currentModalData: null,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Object,
|
||||
@ -39,6 +53,10 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openDeleteModal(modalData) {
|
||||
this.currentModalData = modalData
|
||||
this.showConfirmModal = true
|
||||
},
|
||||
async generateGroupInviteCode(comment) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -205,12 +205,63 @@
|
||||
</client-only>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<os-modal
|
||||
v-if="deleteUserData"
|
||||
:open="showDeleteModal"
|
||||
:title="$t('settings.deleteUserAccount.accountWarningIsAdmin')"
|
||||
@cancel="cancelDeleteUser"
|
||||
>
|
||||
<p>{{ $t('settings.deleteUserAccount.infoAdmin') }}</p>
|
||||
<div class="ds-flex" style="margin-top: 0.75rem">
|
||||
<div style="flex: 0 0 50%; width: 50%">
|
||||
<user-teaser :user="deleteUserData" :link-to-profile="false" :show-popover="false" />
|
||||
</div>
|
||||
<div style="flex: 0 0 20%; width: 20%">
|
||||
<p class="ds-text ds-text-size-small">
|
||||
<strong>{{ $t('modals.deleteUser.created') }}</strong>
|
||||
<br />
|
||||
<date-time :date-time="deleteUserData.createdAt" />
|
||||
</p>
|
||||
</div>
|
||||
<div style="flex: 0 0 15%; width: 15%">
|
||||
<p class="ds-text ds-text-size-small">
|
||||
<strong>{{ $t('common.post', null, deleteUserData.contributionsCount) }}</strong>
|
||||
<br />
|
||||
{{ deleteUserData.contributionsCount }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="flex: 0 0 15%; width: 15%">
|
||||
<p class="ds-text ds-text-size-small">
|
||||
<strong>{{ $t('common.comment', null, deleteUserData.commentedCount) }}</strong>
|
||||
<br />
|
||||
{{ deleteUserData.commentedCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer="{ cancel }">
|
||||
<os-button variant="primary" appearance="outline" @click="cancel">
|
||||
{{ $t('actions.cancel') }}
|
||||
</os-button>
|
||||
<os-button
|
||||
variant="danger"
|
||||
appearance="filled"
|
||||
:loading="deleteLoading"
|
||||
@click="confirmDeleteUser"
|
||||
>
|
||||
<template #icon><os-icon :icon="icons.exclamationCircle" /></template>
|
||||
{{ $t('settings.deleteUserAccount.name') }}
|
||||
</os-button>
|
||||
</template>
|
||||
</os-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton, OsCard, OsIcon, OsNumber, OsSpinner } from '@ocelot-social/ui'
|
||||
import { OsButton, OsCard, OsIcon, OsModal, OsNumber, OsSpinner } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import gql from 'graphql-tag'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import postListActions from '~/mixins/postListActions'
|
||||
@ -231,6 +282,8 @@ import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
|
||||
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
|
||||
import UpdateQuery from '~/components/utils/UpdateQuery'
|
||||
import SocialMedia from '~/components/SocialMedia/SocialMedia'
|
||||
import DateTime from '~/components/DateTime'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import LocationInfo from '~/components/LocationInfo/LocationInfo.vue'
|
||||
|
||||
const tabToFilterMapping = ({ tab, id }) => {
|
||||
@ -246,6 +299,7 @@ export default {
|
||||
OsCard,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
OsModal,
|
||||
OsNumber,
|
||||
OsSpinner,
|
||||
SocialMedia,
|
||||
@ -256,6 +310,8 @@ export default {
|
||||
ProfileAvatar,
|
||||
ContentMenu,
|
||||
AvatarUploader,
|
||||
DateTime,
|
||||
UserTeaser,
|
||||
MasonryGrid,
|
||||
MasonryGridItem,
|
||||
FollowList,
|
||||
@ -288,6 +344,9 @@ export default {
|
||||
followedByCount: followListVisibleCount,
|
||||
followingCount: followListVisibleCount,
|
||||
updateUserMutation,
|
||||
showDeleteModal: false,
|
||||
deleteUserData: null,
|
||||
deleteLoading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -337,7 +396,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
showChat: 'chat/SET_OPEN_CHAT',
|
||||
}),
|
||||
handleTab(tab) {
|
||||
@ -414,13 +472,36 @@ export default {
|
||||
this.$apollo.queries.User.refetch()
|
||||
}
|
||||
},
|
||||
async deleteUser(userdata) {
|
||||
this.commitModalData({
|
||||
name: 'delete',
|
||||
data: {
|
||||
userdata: userdata,
|
||||
},
|
||||
})
|
||||
deleteUser(userdata) {
|
||||
this.deleteUserData = userdata
|
||||
this.showDeleteModal = true
|
||||
},
|
||||
cancelDeleteUser() {
|
||||
this.showDeleteModal = false
|
||||
this.deleteUserData = null
|
||||
},
|
||||
async confirmDeleteUser() {
|
||||
this.deleteLoading = true
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: ID!, $resource: [Deletable]) {
|
||||
DeleteUser(id: $id, resource: $resource) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: this.deleteUserData.id, resource: ['Post', 'Comment'] },
|
||||
})
|
||||
this.$toast.success(this.$t('settings.deleteUserAccount.success'))
|
||||
this.showDeleteModal = false
|
||||
this.$router.replace('/')
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
this.showDeleteModal = false
|
||||
} finally {
|
||||
this.deleteLoading = false
|
||||
}
|
||||
},
|
||||
optimisticFollow({ followedByCurrentUser }) {
|
||||
const currentUser = this.$store.getters['auth/user']
|
||||
|
||||
@ -3,6 +3,7 @@ import flushPromises from 'flush-promises'
|
||||
import MySocialMedia from './my-social-media.vue'
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
import MySomethingList from '~/components/_new/features/MySomethingList/MySomethingList'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -10,7 +11,6 @@ describe('my-social-media.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let getters
|
||||
let mutations
|
||||
const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
|
||||
const newSocialMediaUrl = 'https://twitter.com/mattwr18'
|
||||
const faviconUrl = 'https://freeradical.zone/favicon.ico'
|
||||
@ -31,9 +31,6 @@ describe('my-social-media.vue', () => {
|
||||
return {}
|
||||
},
|
||||
}
|
||||
mutations = {
|
||||
'modal/SET_OPEN': jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
@ -41,9 +38,15 @@ describe('my-social-media.vue', () => {
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
mutations,
|
||||
})
|
||||
return mount(MySocialMedia, { store, mocks, localVue })
|
||||
return mount(MySocialMedia, {
|
||||
store,
|
||||
mocks,
|
||||
localVue,
|
||||
stubs: {
|
||||
'confirm-modal': { template: '<div class="confirm-modal-stub" />' },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('adding social media link', () => {
|
||||
@ -158,13 +161,7 @@ describe('my-social-media.vue', () => {
|
||||
})
|
||||
|
||||
it('opens the confirmation modal', () => {
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
name: 'confirm',
|
||||
}),
|
||||
)
|
||||
expect(wrapper.find('.confirm-modal-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('when user confirms deletion', () => {
|
||||
@ -172,8 +169,8 @@ describe('my-social-media.vue', () => {
|
||||
mocks.$apollo.mutate.mockResolvedValue({
|
||||
data: { DeleteSocialMedia: { id: 's1' } },
|
||||
})
|
||||
const modalCall = mutations['modal/SET_OPEN'].mock.calls[0][1]
|
||||
modalCall.data.modalData.buttons.confirm.callback()
|
||||
const mySomethingList = wrapper.findComponent(MySomethingList)
|
||||
mySomethingList.vm.currentModalData.buttons.confirm.callback()
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
@ -193,8 +190,8 @@ describe('my-social-media.vue', () => {
|
||||
describe('when deletion fails', () => {
|
||||
beforeEach(async () => {
|
||||
mocks.$apollo.mutate.mockRejectedValue(new Error('Network error'))
|
||||
const modalCall = mutations['modal/SET_OPEN'].mock.calls[0][1]
|
||||
modalCall.data.modalData.buttons.confirm.callback()
|
||||
const mySomethingList = wrapper.findComponent(MySomethingList)
|
||||
mySomethingList.vm.currentModalData.buttons.confirm.callback()
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
export const state = () => {
|
||||
return {
|
||||
open: null,
|
||||
data: {},
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SET_OPEN(state, ctx) {
|
||||
state.open = ctx.name || null
|
||||
state.data = ctx.data || {}
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
open(state) {
|
||||
return state.open
|
||||
},
|
||||
data(state) {
|
||||
return state.data
|
||||
},
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user