From 4f0ff1d365a3b233ed5e337672f3f723a378e799 Mon Sep 17 00:00:00 2001 From: cstns Date: Tue, 1 Oct 2024 11:19:55 +0300 Subject: [PATCH 01/47] add bulk notification update --- forge/db/models/Notification.js | 17 ++++++++ forge/routes/api/userNotifications.js | 63 ++++++++++++++++----------- forge/services/notifications.js | 8 ++++ 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 forge/services/notifications.js diff --git a/forge/db/models/Notification.js b/forge/db/models/Notification.js index f299238e68..15f6111977 100644 --- a/forge/db/models/Notification.js +++ b/forge/db/models/Notification.js @@ -89,6 +89,23 @@ module.exports = { count, notifications: rows } + }, + markNotificationsAsRead: async ({ read = true, ids = [] }, user) => { + if (ids.length === 0) { + return + } + + ids = ids.map(id => Number.isNaN(id) ? M.Notification.decodeHashid(id) : id) + + await M.Notification.update( + { read: read ? 1 : 0 }, + { + where: { + id: ids.map(M.Notification.decodeHashid), + UserId: user.id + } + } + ) } } } diff --git a/forge/routes/api/userNotifications.js b/forge/routes/api/userNotifications.js index e921f35a6c..f2d633b0e7 100644 --- a/forge/routes/api/userNotifications.js +++ b/forge/routes/api/userNotifications.js @@ -1,3 +1,5 @@ +const { getNotifications } = require('../../services/notifications.js') + /** * User Notification api routes * @@ -27,35 +29,46 @@ module.exports = async function (app) { } } }, async (request, reply) => { - const paginationOptions = app.getPaginationOptions(request) - const notifications = await app.db.models.Notification.forUser(request.session.User, paginationOptions) - notifications.notifications = app.db.views.Notification.notificationList(notifications.notifications) + const notifications = await getNotifications(app, request) + reply.send(notifications) }) // Bulk update - // app.put('/', { - // schema: { - // summary: 'Mark notifications as read', - // tags: ['User'], - // body: { - // type: 'object', - // properties: { - // id: { type: 'string' }, - // read: { type: 'boolean' } - // } - // }, - // response: { - // 200: { - // $ref: 'APIStatus' - // }, - // '4xx': { - // $ref: 'APIError' - // } - // } - // } - // }, async (request, reply) => { - // }) + app.put('/', { + schema: { + summary: 'Mark notifications as read', + tags: ['User'], + body: { + type: 'object', + properties: { + ids: { type: 'array', items: { type: 'string' } }, + read: { type: 'boolean' } + } + }, + response: { + 200: { + type: 'object', + properties: { + meta: { $ref: 'PaginationMeta' }, + count: { type: 'number' }, + notifications: { $ref: 'NotificationList' } + } + }, + '4xx': { + $ref: 'APIError' + } + } + } + }, async (request, reply) => { + const payload = { read: request.body.read, ids: request.body.ids } + + await app.db.models.Notification.markNotificationsAsRead(payload, request.session.User) + + const notifications = await getNotifications(app, request) + + reply.send(notifications) + }) // Bulk delete // app.delete('/', { diff --git a/forge/services/notifications.js b/forge/services/notifications.js new file mode 100644 index 0000000000..9e46b90c36 --- /dev/null +++ b/forge/services/notifications.js @@ -0,0 +1,8 @@ +module.exports.getNotifications = async (app, request) => { + const paginationOptions = app.getPaginationOptions(request) + const notifications = await app.db.models.Notification.forUser(request.session.User, paginationOptions) + + notifications.notifications = app.db.views.Notification.notificationList(notifications.notifications) + + return notifications +} From d773207fd6f9631cc9a51f8fd89f6aecda5e8f83 Mon Sep 17 00:00:00 2001 From: cstns Date: Tue, 1 Oct 2024 11:20:29 +0300 Subject: [PATCH 02/47] add bulkNotification client call --- frontend/src/api/user.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index 8f21ada2ec..21bf7c906a 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -94,7 +94,18 @@ const markNotificationRead = async (id) => { read: true }) } - +const markNotificationsBulk = async (ids, data = { read: true }) => { + return client.put('/api/v1/user/notifications/', { + ids, + ...data + }).then(res => { + res.data.notifications = res.data.notifications.map(n => { + n.createdSince = daysSince(n.createdAt) + return n + }) + return res.data + }) +} const getTeamInvitations = async () => { return client.get('/api/v1/user/invitations').then(res => { res.data.invitations = res.data.invitations.map(r => { @@ -240,6 +251,7 @@ export default { deleteUser, getNotifications, markNotificationRead, + markNotificationsBulk, getTeamInvitations, acceptTeamInvitation, rejectTeamInvitation, From deb71df3f16cbdbe6108e5ebc5241a60cd5bc3ac Mon Sep 17 00:00:00 2001 From: cstns Date: Tue, 1 Oct 2024 11:20:49 +0300 Subject: [PATCH 03/47] add a set notification store action --- frontend/src/store/account.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/store/account.js b/frontend/src/store/account.js index 5c35f37e45..4d350fc413 100644 --- a/frontend/src/store/account.js +++ b/frontend/src/store/account.js @@ -389,6 +389,9 @@ const actions = { }) .catch(_ => {}) }, + setNotifications (state, notifications) { + state.commit('setNotifications', notifications) + }, async getInvitations (state) { await userApi.getTeamInvitations() .then((invitations) => { From a87a2c4eea19dbbf5cbe3c2767442ed7aa2b4758 Mon Sep 17 00:00:00 2001 From: cstns Date: Tue, 1 Oct 2024 11:22:53 +0300 Subject: [PATCH 04/47] bubble up onSelect events --- frontend/src/components/notifications/Generic.vue | 2 ++ frontend/src/components/notifications/invitations/Accepted.vue | 2 ++ frontend/src/components/notifications/invitations/Received.vue | 2 ++ 3 files changed, 6 insertions(+) diff --git a/frontend/src/components/notifications/Generic.vue b/frontend/src/components/notifications/Generic.vue index dae05e30a4..b7450d2f34 100644 --- a/frontend/src/components/notifications/Generic.vue +++ b/frontend/src/components/notifications/Generic.vue @@ -3,6 +3,8 @@ :notification="notification" :selections="selections" data-el="generic-notification" :to="to" + @selected="onSelect" + @deselected="onDeselect" > diff --git a/frontend/src/stylesheets/components/notifications.scss b/frontend/src/stylesheets/components/notifications.scss index 62c855bf92..a9766300e4 100644 --- a/frontend/src/stylesheets/components/notifications.scss +++ b/frontend/src/stylesheets/components/notifications.scss @@ -1,157 +1,237 @@ .ff-notification-interview { - max-width: 450px; - border: 1px solid $ff-grey-400; - background-color: $ff-white; - box-shadow: -6px 6px 6px #00000040; - border-radius: 0 6px 6px 0; - padding: 12px 9px 12px 18px; - border: 1px solid $ff-blue-700; - border-left: 8px solid $ff-blue-700; - h3 { - font-size: 1.1rem; - margin-bottom: 12px; - } - p { - font-size: 1.1rem; - } - &--actions { - margin-top: 24px; - gap: 12px; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - font-size: 1rem; - .ff-btn.ff-btn--primary { - background-color: $ff-blue-700; - border-color: $ff-blue-700; - &:hover { - background-color: $ff-blue-800; - } - } + max-width: 450px; + background-color: $ff-white; + box-shadow: -6px 6px 6px #00000040; + border-radius: 0 6px 6px 0; + padding: 12px 9px 12px 18px; + border: 1px solid $ff-blue-700; + border-left: 8px solid $ff-blue-700; + + h3 { + font-size: 1.1rem; + margin-bottom: 12px; + } + + p { + font-size: 1.1rem; + } + + &--actions { + margin-top: 24px; + gap: 12px; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + font-size: 1rem; + + .ff-btn.ff-btn--primary { + background-color: $ff-blue-700; + border-color: $ff-blue-700; + + &:hover { + background-color: $ff-blue-800; + } } + } } -$ff-notifications-drawer-side-padding: 12px; +$ff-notifications-drawer-side-padding: 6px; .ff-notifications-drawer { - display: flex; - flex-direction: column; - align-items: center; - height: 100%; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + width: 100%; + + > .header { + border-bottom: 1px solid $ff-grey-300; + padding: 10px 0; + width: 100%; + + .title { + padding: 0 $ff-notifications-drawer-side-padding; + margin: 0; + color: $ff-grey-800; + font-weight: bold; + font-size: 1.25rem; + line-height: 1.75rem; + } + + .actions { + margin-top: 5px; + padding: 0 12px; + display: flex; + gap: 5px; + + .forge-badge { + background-color: $ff-grey-100; + border-radius: 5px; + + &:hover { + cursor: pointer; + background-color: $ff-grey-300 + } + + &.disabled { + color: $ff-grey-400; + + &:hover { + cursor: not-allowed; + } + } + } + } + } + + .messages-wrapper { + flex: 1; width: 100%; - - > .header { - border-bottom: 1px solid $ff-grey-300; - padding: 10px 0; - width: 100%; - - .title { - padding: 0 $ff-notifications-drawer-side-padding; + background-color: $ff-grey-100; + $read: $ff-grey-400; + $info: blue; + $warning: $ff-yellow-600; + $error: $ff-red-500; + + .message-wrapper { + display: flex; + flex-direction: row; + color: $ff-grey-400; + background-color: $ff-white; + border-bottom: 1px solid $ff-grey-300; + border-left: 3px solid rgba(0, 0, 0, 0); + transition: ease-in-out .3s; + cursor: pointer; + + .counter { + margin-top: 0.2rem; + + .ff-notification-pill { + background-color: $read; + color: white; + padding: 2px 7px; + border-radius: 6px; + font-size: 0.65rem; + } + } + + .action { + display: flex; + justify-content: center; + align-items: center; + width: 40px; + cursor: default; + + .ff-checkbox { + height: 13px; + width: 13px; + padding: 0; + + span { margin: 0; - color: $ff-grey-800; - font-weight: bold; - font-size: 1.25rem; - line-height: 1.75rem; + padding: 0; + } } + } - .actions { - padding: 0 5px; - display: flex; - gap: 5px; + &:hover .title { + color: $ff-blue-500; + } - .forge-badge { - background-color: $ff-grey-100; + &.unread { + border-left: 3px solid $info; + border-left-color: $ff-blue-500; + color: $ff-grey-800; - &:hover { - cursor: pointer; - background-color: $ff-grey-300 - } + &.warning { + border-left: 3px solid $warning; - &.disabled { - color: $ff-grey-400; + .counter .ff-notification-pill { + background-color: $warning; + } + } - &:hover { - cursor: not-allowed; - } - } - } + &.error { + border-left: 3px solid $error; + + .counter .ff-notification-pill { + background-color: $error; + } } - } - .messages-wrapper { - flex: 1; - width: 100%; - background-color: $ff-grey-100; - - .message { - color: $ff-grey-400; - background-color: $ff-white; - border-bottom: 1px solid $ff-grey-300; - border-left: 3px solid rgba(0,0,0,0); - padding: 9px $ff-notifications-drawer-side-padding; - transition: ease-in-out .3s; - - cursor: pointer; + .counter .ff-notification-pill { + background-color: $info; + } + } - &:hover .title { - color: $ff-blue-500; - } - &.unread { - border-left-color: $ff-blue-500; - color: $ff-grey-800; - } - .body { - flex: 1; - display: flex; - align-items: center; - } + .body { + flex: 1; + display: flex; + flex-direction: column; + padding: 9px $ff-notifications-drawer-side-padding; - .text { - padding: 0 $ff-notifications-drawer-side-padding; - flex-grow: 1; - } + .header { + gap: 5px; + display: flex; + justify-content: space-between; + align-items: center; - .header { - display: flex; - justify-content: space-between; - .title { - flex: 1; - transition: ease-in-out .3s; - h4 { - margin: 0; - } - } - - input { - &:hover { - cursor: pointer; - } - } - } + .ff-icon { + height: 20px; + min-width: 20px; + min-height: 20px; + max-width: fit-content; + max-height: fit-content; + display: flex; + align-items: center; + justify-content: center; + } - .footer { - margin-top: 6px; - text-align: right; - color: $ff-grey-400; - font-size: 80%; - padding: 0 $ff-notifications-drawer-side-padding; - } + .title { + flex: 1; + transition: ease-in-out .3s; + margin: 0; + } + input { &:hover { - background-color: $ff-grey-100; + cursor: pointer; } - } - } - - .empty { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - color: $ff-grey-400; - width: 100%; + } + } + + .text { + display: flex; + margin: 10px 0; + align-items: center; + line-height: 1.5rem; + } + + .footer { + display: flex; + text-align: right; + color: $ff-grey-400; + font-size: 80%; + padding: 0 $ff-notifications-drawer-side-padding; + justify-content: flex-end; + } + } + + &:hover { background-color: $ff-grey-100; + } } - -} \ No newline at end of file + } + + .empty { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + color: $ff-grey-400; + width: 100%; + background-color: $ff-grey-100; + } + +} From 6401865877267e5d671c1f56ee2a3d788d39f96c Mon Sep 17 00:00:00 2001 From: cstns Date: Tue, 1 Oct 2024 16:16:11 +0300 Subject: [PATCH 08/47] styling ocs --- .../notifications/NotificationsDrawer.vue | 2 +- .../stylesheets/components/notifications.scss | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/drawers/notifications/NotificationsDrawer.vue b/frontend/src/components/drawers/notifications/NotificationsDrawer.vue index 7923a06446..3f6bbcfa19 100644 --- a/frontend/src/components/drawers/notifications/NotificationsDrawer.vue +++ b/frontend/src/components/drawers/notifications/NotificationsDrawer.vue @@ -1,7 +1,7 @@