From 7ef27f86226502901c8819c0ea018cdb45e29edf Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 28 Nov 2024 15:10:59 +0000 Subject: [PATCH 1/6] Add more filters for admin notification targetting --- forge/db/models/User.js | 29 +++- forge/routes/api/admin.js | 12 +- frontend/src/composables/String.js | 11 +- frontend/src/pages/admin/NotificationsHub.vue | 154 +++++++++++++++--- test/unit/forge/routes/api/admin_spec.js | 51 +++++- 5 files changed, 221 insertions(+), 36 deletions(-) diff --git a/forge/db/models/User.js b/forge/db/models/User.js index b093294b02..41c2b4853a 100644 --- a/forge/db/models/User.js +++ b/forge/db/models/User.js @@ -169,7 +169,7 @@ module.exports = { this.hasMany(M.Invitation, { foreignKey: 'inviteeId' }) this.belongsTo(M.Team, { as: 'defaultTeam' }) }, - finders: function (M) { + finders: function (M, app) { return { static: { admins: async () => { @@ -285,8 +285,11 @@ module.exports = { * Get users with a particular role * @param {Array} roles An array of valid user roles * @param {Object} options Options + * @param {Boolean} options.count only return a count of results * @param {Boolean} options.summary whether to return a limited user object that only contains id: default false - * @returns Array of users who have at least one of the specific roles + * @param {Array} options.teamTypes limit to teams of certain types + * @param {Array} options.billing array of billing states to include + * @returns Array of users who have at least one of the specific roles, or a count */ byTeamRole: async (roles = [], options) => { options = { @@ -309,7 +312,27 @@ module.exports = { where, include: { model: M.TeamMember, - attributes: ['role'] + attributes: ['role'], + include: { + model: M.Team, + attributes: ['suspended', 'TeamTypeId'], + where: { + // Never include suspended teams + suspended: false + } + } + } + } + if (options.teamTypes) { + query.include.include.where.TeamTypeId = { [Op.in]: options.teamTypes } + if (options.billing) { + query.include.include.include = { + model: app.db.models.Subscription, + attributes: ['status'], + where: { + status: { [Op.in]: options.billing.map(opt => opt.toLowerCase()) } + } + } } } if (!options.count) { diff --git a/forge/routes/api/admin.js b/forge/routes/api/admin.js index 05d5977c98..9b86413e58 100644 --- a/forge/routes/api/admin.js +++ b/forge/routes/api/admin.js @@ -449,10 +449,18 @@ module.exports = async function (app) { if (recipientRoles && !recipientRoles.every(value => Object.values(Roles).includes(value))) { return reply.code(400).send({ code: 'bad_request', error: 'Invalid Role provided.' }) } + let teamTypes + if (filter?.teamTypes && filter.teamTypes.length > 0) { + teamTypes = filter.teamTypes.map(app.db.models.TeamType.decodeHashid).flat() + } + let billing + if (filter?.billing && filter.billing.length > 0) { + billing = filter.billing + } if (mock) { // If mock is sent, return an indication of how many users would receive this notification // without actually sending them. - const count = await app.db.models.User.byTeamRole(recipientRoles, { summary: true, count: true }) + const count = await app.db.models.User.byTeamRole(recipientRoles, { teamTypes, billing, summary: true, count: true }) reply.send({ mock: true, recipientCount: count @@ -460,7 +468,7 @@ module.exports = async function (app) { return } - const recipients = await app.db.models.User.byTeamRole(recipientRoles, { summary: true }) + const recipients = await app.db.models.User.byTeamRole(recipientRoles, { teamTypes, summary: true }) const notificationType = 'announcement' const titleSlug = title.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase() const uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2) diff --git a/frontend/src/composables/String.js b/frontend/src/composables/String.js index c0befda75d..60cb047cd9 100644 --- a/frontend/src/composables/String.js +++ b/frontend/src/composables/String.js @@ -6,7 +6,16 @@ export const isValidURL = (string) => { export const capitalize = (string) => { return string.charAt(0).toUpperCase() + string.slice(1) } - +/** + * Conditionally pluralize a string + * @param {String} stem the text to pluralize based on a count + * @param {Number} count the value to pluralize for + * @param {String} append (optional) what characters to add if pluralizing. Default: 's' + * @returns The pluralized string if count requires a plural + */ +export const pluralize = (stem, count, append = 's') => { + return stem + ((count === 1) ? '' : append) +} /** * @param {Date} date * @returns {`${number}-${string}-${string}-${string}:${string}`} diff --git a/frontend/src/pages/admin/NotificationsHub.vue b/frontend/src/pages/admin/NotificationsHub.vue index 461c27bd06..2dc6be3e49 100644 --- a/frontend/src/pages/admin/NotificationsHub.vue +++ b/frontend/src/pages/admin/NotificationsHub.vue @@ -22,21 +22,53 @@
- + Audience
Select the audience of your announcement.
- - - + User Roles: +
+ +
+ Team Types: +
+ +
+
@@ -50,16 +82,21 @@