From a69006873d654bd87c9073a71254a4da078829b9 Mon Sep 17 00:00:00 2001 From: sebastian2357 <80636200+sebastian2357@users.noreply.github.com> Date: Sun, 25 May 2025 18:44:33 +0200 Subject: [PATCH] fix(webapp): notifications - UI Improvements (#8559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Notifications view: - restructured broken layout - joined several columns for mobile view - moved button from footer to header - set alternating colors for the table rows UserTeaser - added injectedText - added injectedDate - fixed padding * fixed race-condition with default behavior of browser * - fixed: jumping menu / menu should get closed by click on notification - fixed: NotificationList replaced by NotificationTable * - fixed: menu gets closed when cursor leaves content area, but it is still within popup * - fixed: menu top buttons should be next to each other * - fixed: popup background overlay remains after NotificationMenu disappeared after viewport change to mobile * - fixed lint errors * - fixed tests + snapshots * - fixed e2e test * fix lint error Co-authored-by: Sebastian Stein * Fix locale identifier to have single quotes 'notifications.reason.on_date' --------- Co-authored-by: Sebastian Stein Co-authored-by: Wolfgang Huß Co-authored-by: Ulf Gebhardt --- ...cation_menu_and_click_on_the_first_item.js | 2 +- .../Notification/Notification.spec.js | 200 ------------------ .../components/Notification/Notification.vue | 100 --------- .../NotificationList/NotificationList.spec.js | 103 --------- .../NotificationList/NotificationList.vue | 32 --- .../NotificationMenu/NotificationMenu.vue | 50 +++-- .../NotificationsTable.spec.js | 26 +-- .../NotificationsTable/NotificationsTable.vue | 187 ++++++++++------ webapp/components/UserTeaser/UserTeaser.vue | 8 +- .../UserTeaser/UserTeaserNonAnonymous.vue | 6 +- .../__snapshots__/UserTeaser.spec.js.snap | 60 ++++++ webapp/locales/de.json | 19 +- webapp/locales/en.json | 19 +- webapp/locales/es.json | 7 +- webapp/locales/fr.json | 1 + webapp/locales/nl.json | 1 + webapp/locales/pl.json | 1 + webapp/locales/pt.json | 1 + webapp/locales/ru.json | 1 + .../_id/__snapshots__/_slug.spec.js.snap | 160 ++++++++++++++ webapp/pages/notifications/index.vue | 26 ++- 21 files changed, 436 insertions(+), 574 deletions(-) delete mode 100644 webapp/components/Notification/Notification.spec.js delete mode 100644 webapp/components/Notification/Notification.vue delete mode 100644 webapp/components/NotificationList/NotificationList.spec.js delete mode 100644 webapp/components/NotificationList/NotificationList.vue diff --git a/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js b/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js index e209909d3..534db2a56 100644 --- a/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js +++ b/cypress/support/step_definitions/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js @@ -4,7 +4,7 @@ defineStep('open the notification menu and click on the first item', () => { cy.get('.notifications-menu') .invoke('show') .click() // 'invoke('show')' because of the delay for show the menu - cy.get('.notification .link') + cy.get('.notification-content a') .first() .click({force: true}) }) diff --git a/webapp/components/Notification/Notification.spec.js b/webapp/components/Notification/Notification.spec.js deleted file mode 100644 index 844f78e71..000000000 --- a/webapp/components/Notification/Notification.spec.js +++ /dev/null @@ -1,200 +0,0 @@ -import { mount, RouterLinkStub } from '@vue/test-utils' -import Notification from './Notification.vue' - -import Vuex from 'vuex' - -const localVue = global.localVue - -describe('Notification', () => { - let stubs - let getters - let mocks - let propsData - let wrapper - beforeEach(() => { - propsData = {} - mocks = { - $t: (key) => key, - } - stubs = { - NuxtLink: RouterLinkStub, - 'client-only': true, - } - getters = { - 'auth/user': () => { - return {} - }, - 'auth/isModerator': () => false, - } - }) - - const Wrapper = () => { - const store = new Vuex.Store({ - getters, - }) - return mount(Notification, { - stubs, - store, - mocks, - propsData, - localVue, - }) - } - - describe('given a notification about a comment on a post', () => { - beforeEach(() => { - propsData.notification = { - reason: 'commented_on_post', - from: { - __typename: 'Comment', - id: 'comment-1', - contentExcerpt: - '@dagobert-duck is the best on this comment.', - post: { - title: "It's a post title", - id: 'post-1', - slug: 'its-a-title', - contentExcerpt: 'Post content.', - }, - }, - } - }) - - it('renders reason', () => { - wrapper = Wrapper() - expect(wrapper.find('.notification > .description').text()).toEqual( - 'notifications.reason.commented_on_post', - ) - }) - it('renders title', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain("It's a post title") - }) - it('renders the identifier "notifications.comment"', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain('notifications.comment') - }) - it('renders the contentExcerpt', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.') - }) - it('has no class "--read"', () => { - wrapper = Wrapper() - expect(wrapper.classes()).not.toContain('--read') - }) - - describe('that is read', () => { - beforeEach(() => { - propsData.notification.read = true - wrapper = Wrapper() - }) - - it('has class "--read"', () => { - expect(wrapper.classes()).toContain('--read') - }) - }) - }) - - describe('given a notification about a mention in a post', () => { - beforeEach(() => { - propsData.notification = { - reason: 'mentioned_in_post', - from: { - __typename: 'Post', - title: "It's a post title", - id: 'post-1', - slug: 'its-a-title', - contentExcerpt: - '@jenny-rostock is the best on this post.', - }, - } - }) - - it('renders reason', () => { - wrapper = Wrapper() - expect(wrapper.find('.notification > .description').text()).toEqual( - 'notifications.reason.mentioned_in_post', - ) - }) - it('renders title', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain("It's a post title") - }) - it('renders the contentExcerpt', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.') - }) - it('has no class "--read"', () => { - wrapper = Wrapper() - expect(wrapper.classes()).not.toContain('--read') - }) - - describe('that is read', () => { - beforeEach(() => { - propsData.notification.read = true - wrapper = Wrapper() - }) - - it('has class "--read"', () => { - expect(wrapper.classes()).toContain('--read') - }) - }) - }) - - describe('given a notification about a mention in a comment', () => { - beforeEach(() => { - propsData.notification = { - reason: 'mentioned_in_comment', - from: { - __typename: 'Comment', - id: 'comment-1', - contentExcerpt: - '@dagobert-duck is the best on this comment.', - post: { - title: "It's a post title", - id: 'post-1', - slug: 'its-a-title', - contentExcerpt: 'Post content.', - }, - }, - } - }) - - it('renders reason', () => { - wrapper = Wrapper() - expect(wrapper.find('.notification > .description').text()).toEqual( - 'notifications.reason.mentioned_in_comment', - ) - }) - it('renders title', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain("It's a post title") - }) - - it('renders the identifier "notifications.comment"', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain('notifications.comment') - }) - - it('renders the contentExcerpt', () => { - wrapper = Wrapper() - expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.') - }) - - it('has no class "--read"', () => { - wrapper = Wrapper() - expect(wrapper.classes()).not.toContain('--read') - }) - - describe('that is read', () => { - beforeEach(() => { - propsData.notification.read = true - wrapper = Wrapper() - }) - - it('has class "--read"', () => { - expect(wrapper.classes()).toContain('--read') - }) - }) - }) -}) diff --git a/webapp/components/Notification/Notification.vue b/webapp/components/Notification/Notification.vue deleted file mode 100644 index d83995b9b..000000000 --- a/webapp/components/Notification/Notification.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - - - diff --git a/webapp/components/NotificationList/NotificationList.spec.js b/webapp/components/NotificationList/NotificationList.spec.js deleted file mode 100644 index 7f7038d59..000000000 --- a/webapp/components/NotificationList/NotificationList.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import { shallowMount, mount, RouterLinkStub } from '@vue/test-utils' -import NotificationList from './NotificationList' -import Notification from '../Notification/Notification' -import Vuex from 'vuex' - -import { notifications } from '~/components/utils/Notifications' - -const localVue = global.localVue - -localVue.filter('truncate', (string) => string) - -describe('NotificationList.vue', () => { - let wrapper - let mocks - let stubs - let store - let propsData - - beforeEach(() => { - store = new Vuex.Store({ - getters: { - 'auth/isModerator': () => false, - 'auth/user': () => { - return {} - }, - }, - }) - mocks = { - $t: jest.fn(), - } - stubs = { - NuxtLink: RouterLinkStub, - 'client-only': true, - 'v-popover': true, - } - propsData = { notifications } - }) - - describe('shallowMount', () => { - const Wrapper = () => { - return shallowMount(NotificationList, { - propsData, - mocks, - store, - localVue, - stubs, - }) - } - - beforeEach(() => { - wrapper = Wrapper() - }) - - it('renders Notification.vue for each notification of the user', () => { - expect(wrapper.findAllComponents(Notification)).toHaveLength(2) - }) - }) - - describe('mount', () => { - const Wrapper = () => { - return mount(NotificationList, { - propsData, - mocks, - stubs, - store, - localVue, - }) - } - - beforeEach(() => { - wrapper = Wrapper() - }) - - describe('click on a notification', () => { - beforeEach(() => { - wrapper.find('.notification > .link').trigger('click') - }) - - it("emits 'markAsRead' with the id of the notification source", () => { - expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-1']) - }) - }) - }) - - describe('shallowMount with no notifications', () => { - const Wrapper = () => { - return shallowMount(NotificationList, { - propsData: {}, - mocks, - store, - localVue, - }) - } - - beforeEach(() => { - wrapper = Wrapper() - }) - - it('renders Notification.vue zero times', () => { - expect(wrapper.findAllComponents(Notification)).toHaveLength(0) - }) - }) -}) diff --git a/webapp/components/NotificationList/NotificationList.vue b/webapp/components/NotificationList/NotificationList.vue deleted file mode 100644 index fd2d6366c..000000000 --- a/webapp/components/NotificationList/NotificationList.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/webapp/components/NotificationMenu/NotificationMenu.vue b/webapp/components/NotificationMenu/NotificationMenu.vue index df4b8a503..58c372f34 100644 --- a/webapp/components/NotificationMenu/NotificationMenu.vue +++ b/webapp/components/NotificationMenu/NotificationMenu.vue @@ -26,7 +26,14 @@ - + @@ -153,10 +154,15 @@ export default {