From 237798b0f0b9002d524c3f24486f0e00d81d4af9 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 13 Mar 2026 03:30:54 +0100 Subject: [PATCH] feat(package/ui): os-modal & webapp integration (#9375) --- .../I_confirm_the_reporting_dialog.js | 2 +- packages/ui/KATALOG.md | 81 +- packages/ui/PROJEKT.md | 22 +- .../ui/src/components/OsModal/OsModal.spec.ts | 425 ++++ .../src/components/OsModal/OsModal.stories.ts | 102 + .../components/OsModal/OsModal.visual.spec.ts | 85 + .../ui/src/components/OsModal/OsModal.vue | 404 ++++ .../chromium/custom-footer.png | Bin 0 -> 9821 bytes .../__screenshots__/chromium/default-size.png | Bin 0 -> 13399 bytes .../chromium/scrollable-content.png | Bin 0 -> 43888 bytes packages/ui/src/components/OsModal/index.ts | 2 + .../src/components/OsModal/modal.variants.ts | 12 + packages/ui/src/components/index.ts | 1 + packages/ui/src/svg-icon.d.ts | 7 +- webapp/assets/_new/styles/tokens.scss | 1 - webapp/components/Button/JoinLeaveButton.vue | 102 +- .../JoinLeaveButton.spec.js.snap | 9 +- webapp/components/CommentCard/CommentCard.vue | 2 +- webapp/components/CommentForm/CommentForm.vue | 4 +- .../ContentMenu/ContentMenu.Group.spec.js | 5 +- .../ContentMenu/ContentMenu.spec.js | 25 +- webapp/components/ContentMenu/ContentMenu.vue | 167 +- .../ContentMenu/GroupContentMenu.vue | 74 +- .../GroupContentMenu.spec.js.snap | 762 +++---- .../CtaJoinLeaveGroup.spec.js.snap | 9 +- webapp/components/Group/AddGroupMember.vue | 30 +- webapp/components/Group/GroupMember.spec.js | 25 +- webapp/components/Group/GroupMember.vue | 31 +- webapp/components/Group/GroupTeaser.vue | 9 +- .../components/InviteButton/InviteButton.vue | 89 +- webapp/components/Modal.spec.js | 172 -- webapp/components/Modal.vue | 84 - webapp/components/Modal/ConfirmModal.spec.js | 12 +- webapp/components/Modal/ConfirmModal.vue | 10 +- .../components/Modal/DeleteUserModal.spec.js | 123 -- webapp/components/Modal/DeleteUserModal.vue | 195 -- webapp/components/Modal/DisableModal.spec.js | 195 -- webapp/components/Modal/DisableModal.vue | 94 - webapp/components/Modal/ReportModal.spec.js | 3 + webapp/components/Modal/ReportModal.vue | 11 +- webapp/components/PostTeaser/PostTeaser.vue | 7 +- .../ReleaseModal/ReleaseModal.spec.js | 192 -- .../components/ReleaseModal/ReleaseModal.vue | 99 - .../features/Invitations/Invitation.spec.js | 46 +- .../_new/features/Invitations/Invitation.vue | 53 +- .../features/Invitations/InvitationList.vue | 1 + .../MySomethingList/MySomethingList.spec.js | 46 +- .../MySomethingList/MySomethingList.vue | 58 +- .../features/SearchResults/SearchResults.vue | 2 +- .../features/ReportList/ReportList.spec.js | 11 +- .../features/ReportList/ReportList.vue | 84 +- webapp/graphql/CommentMutations.js | 10 +- webapp/graphql/CommentQuery.js | 8 +- webapp/graphql/Search.js | 2 +- webapp/layouts/default.vue | 5 - webapp/locales/de.json | 5 +- webapp/locales/en.json | 3 + webapp/locales/es.json | 3 + webapp/locales/fr.json | 3 + webapp/locales/it.json | 3 + webapp/locales/nl.json | 3 + webapp/locales/pl.json | 3 + webapp/locales/pt.json | 3 + webapp/locales/ru.json | 3 + webapp/locales/sq.json | 3 + webapp/locales/uk.json | 3 + .../_id/__snapshots__/_slug.spec.js.snap | 1862 +++++++++-------- webapp/pages/groups/_id/_slug.vue | 12 +- .../_id/__snapshots__/invites.spec.js.snap | 2 + webapp/pages/groups/edit/_id/invites.vue | 18 + .../_id/__snapshots__/_slug.spec.js.snap | 932 +++++---- webapp/pages/profile/_id/_slug.vue | 99 +- webapp/pages/settings/my-social-media.spec.js | 31 +- webapp/store/modal.js | 22 - 74 files changed, 3590 insertions(+), 3433 deletions(-) create mode 100644 packages/ui/src/components/OsModal/OsModal.spec.ts create mode 100644 packages/ui/src/components/OsModal/OsModal.stories.ts create mode 100644 packages/ui/src/components/OsModal/OsModal.visual.spec.ts create mode 100644 packages/ui/src/components/OsModal/OsModal.vue create mode 100644 packages/ui/src/components/OsModal/__screenshots__/chromium/custom-footer.png create mode 100644 packages/ui/src/components/OsModal/__screenshots__/chromium/default-size.png create mode 100644 packages/ui/src/components/OsModal/__screenshots__/chromium/scrollable-content.png create mode 100644 packages/ui/src/components/OsModal/index.ts create mode 100644 packages/ui/src/components/OsModal/modal.variants.ts delete mode 100644 webapp/components/Modal.spec.js delete mode 100644 webapp/components/Modal.vue delete mode 100644 webapp/components/Modal/DeleteUserModal.spec.js delete mode 100644 webapp/components/Modal/DeleteUserModal.vue delete mode 100644 webapp/components/Modal/DisableModal.spec.js delete mode 100644 webapp/components/Modal/DisableModal.vue delete mode 100644 webapp/components/ReleaseModal/ReleaseModal.spec.js delete mode 100644 webapp/components/ReleaseModal/ReleaseModal.vue delete mode 100644 webapp/store/modal.js diff --git a/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js b/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js index 085641b94..f14f1bb58 100644 --- a/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js +++ b/cypress/support/step_definitions/Moderation.ReportContent/I_confirm_the_reporting_dialog.js @@ -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() diff --git a/packages/ui/KATALOG.md b/packages/ui/KATALOG.md index 4f10bbe04..c43f3f13d 100644 --- a/packages/ui/KATALOG.md +++ b/packages/ui/KATALOG.md @@ -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: `
` | @@ -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) → `
` 18. [ ] ds-radio (1 Datei) → native `` -### 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 `` + Composable ersetzen +Modals werden jetzt inline gerendert: Jede Komponente hat eigenes `showConfirmModal`/`currentModalData` State und rendert `` direkt. --- -### Konsolidierungsvorschlag: OsModal +### ~~Konsolidierungsvorschlag~~ OsModal ✅ (implementiert) ```typescript interface OsModalProps { @@ -964,14 +957,14 @@ interface OsDropdownProps { | — | ds-number | 5 | `
` | ⬜ | | — | ds-radio | 1 | native `` | ⬜ | -### 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) diff --git a/packages/ui/PROJEKT.md b/packages/ui/PROJEKT.md index 095a927be..a857ffbcf 100644 --- a/packages/ui/PROJEKT.md +++ b/packages/ui/PROJEKT.md @@ -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 `` **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 `
` 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 | diff --git a/packages/ui/src/components/OsModal/OsModal.spec.ts b/packages/ui/src/components/OsModal/OsModal.spec.ts new file mode 100644 index 000000000..20b9608b7 --- /dev/null +++ b/packages/ui/src/components/OsModal/OsModal.spec.ts @@ -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: '

Modal body

' }, + }) + + 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: '
tall content
' }, + }) + + 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: '
tall content
' }, + }) + + 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: '', + }, + }) + + 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: ``, + }, + }) + + 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() + }) + }) +}) diff --git a/packages/ui/src/components/OsModal/OsModal.stories.ts b/packages/ui/src/components/OsModal/OsModal.stories.ts new file mode 100644 index 000000000..db2ba49bc --- /dev/null +++ b/packages/ui/src/components/OsModal/OsModal.stories.ts @@ -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 = { + title: 'Components/OsModal', + component: OsModal, + tags: ['autodocs'], +} + +export default meta +type Story = StoryObj + +interface PlaygroundArgs { + open: boolean + title: string + cancelLabel: string + confirmLabel: string + content: string +} + +export const Playground: StoryObj = { + 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: `{{ content }}`, + }), +} + +export const DefaultSize: Story = { + render: () => ({ + components: { OsModal }, + template: ` +
+ +

This is a modal with default size (max-width: 500px).

+

It contains the standard cancel and confirm buttons.

+
+
+ `, + }), +} + +export const CustomFooter: Story = { + render: () => ({ + components: { OsModal, OsButton }, + template: ` +
+ +

This modal has a custom footer with different buttons.

+ +
+
+ `, + }), +} + +export const ScrollableContent: Story = { + render: () => ({ + components: { OsModal }, + template: ` +
+ +
+

This modal has a lot of content that will scroll.

+

Paragraph {{ i }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+
+
+ `, + }), +} diff --git a/packages/ui/src/components/OsModal/OsModal.visual.spec.ts b/packages/ui/src/components/OsModal/OsModal.visual.spec.ts new file mode 100644 index 000000000..f887925d8 --- /dev/null +++ b/packages/ui/src/components/OsModal/OsModal.visual.spec.ts @@ -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) + }) +}) diff --git a/packages/ui/src/components/OsModal/OsModal.vue b/packages/ui/src/components/OsModal/OsModal.vue new file mode 100644 index 000000000..b2c3b4df8 --- /dev/null +++ b/packages/ui/src/components/OsModal/OsModal.vue @@ -0,0 +1,404 @@ + diff --git a/packages/ui/src/components/OsModal/__screenshots__/chromium/custom-footer.png b/packages/ui/src/components/OsModal/__screenshots__/chromium/custom-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..93bccf04e64e9e2cceca40207c4c576870853dfb GIT binary patch literal 9821 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%0DB;0O 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 literal 0 HcmV?d00001 diff --git a/packages/ui/src/components/OsModal/__screenshots__/chromium/scrollable-content.png b/packages/ui/src/components/OsModal/__screenshots__/chromium/scrollable-content.png new file mode 100644 index 0000000000000000000000000000000000000000..50057dbc0d8ec02da3e4fdcda1e2aa02d4636182 GIT binary patch literal 43888 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 y0hdZ-lr7u)-=2Yi;s5`&8U4aQ1{ 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 } diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index 69c269f9e..08d18329b 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -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; /** diff --git a/webapp/components/Button/JoinLeaveButton.vue b/webapp/components/Button/JoinLeaveButton.vue index 5904538bd..1e2a22874 100644 --- a/webapp/components/Button/JoinLeaveButton.vue +++ b/webapp/components/Button/JoinLeaveButton.vue @@ -1,32 +1,39 @@ diff --git a/webapp/components/Modal/ConfirmModal.spec.js b/webapp/components/Modal/ConfirmModal.spec.js index 28fef3058..d478bf8fd 100644 --- a/webapp/components/Modal/ConfirmModal.spec.js +++ b/webapp/components/Modal/ConfirmModal.spec.js @@ -7,6 +7,9 @@ const localVue = global.localVue const stubs = { 'sweetalert-icon': true, + 'os-modal': { + template: '
', + }, } 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() }) diff --git a/webapp/components/Modal/ConfirmModal.vue b/webapp/components/Modal/ConfirmModal.vue index b2373faa7..77fa6c895 100644 --- a/webapp/components/Modal/ConfirmModal.vue +++ b/webapp/components/Modal/ConfirmModal.vue @@ -1,5 +1,5 @@ - + - - diff --git a/webapp/components/Modal/DisableModal.spec.js b/webapp/components/Modal/DisableModal.spec.js deleted file mode 100644 index 0a7ffe25b..000000000 --- a/webapp/components/Modal/DisableModal.spec.js +++ /dev/null @@ -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!') - }) - }) - }) - }) - }) -}) diff --git a/webapp/components/Modal/DisableModal.vue b/webapp/components/Modal/DisableModal.vue deleted file mode 100644 index 74f8ea0fc..000000000 --- a/webapp/components/Modal/DisableModal.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/webapp/components/Modal/ReportModal.spec.js b/webapp/components/Modal/ReportModal.spec.js index d993e8103..3b0ded99c 100644 --- a/webapp/components/Modal/ReportModal.spec.js +++ b/webapp/components/Modal/ReportModal.spec.js @@ -6,6 +6,9 @@ const localVue = global.localVue const stubs = { 'sweetalert-icon': true, + 'os-modal': { + template: '
', + }, } describe('ReportModal.vue', () => { diff --git a/webapp/components/Modal/ReportModal.vue b/webapp/components/Modal/ReportModal.vue index fe25debfb..9d4bcb6ab 100644 --- a/webapp/components/Modal/ReportModal.vue +++ b/webapp/components/Modal/ReportModal.vue @@ -1,5 +1,5 @@