mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add notifications system and support user mentions in comments (#9861)
* v-menu de/activated onKeyDown. No List yet. * v-list * add user suggestion * uuids replaced * user-popover working * avatars flex row with usernames in suggestions * added space to end of uuid insert * autofocus + move caret to end of last insert * removed unnecessary setTimeout() * fixed filter 500 with ids * better fix * New translations en-US.yaml (French) (#9907) * New translations en-US.yaml (French) (#9912) * New translations en-US.yaml (French) (#9916) * New translations en-US.yaml (Russian) (#9918) * New translations en-US.yaml (Swedish) (#9920) * Email updates (#9921) * add from name for emails * updatd email template style * reset password email copy * updated logo to newest version * update invite email copy * decouple field template logic * push up styling * Start on new v-template-input * Add notifications API endpoints Squashed commit of the following: commit 9d86721ef795d03bc55693c0f99bde8e269d60e9 Merge: b4458c19f34131d06eAuthor: rijkvanzanten <rijkvanzanten@me.com> Date: Mon Nov 22 09:27:43 2021 -0500 Merge branch 'mentions' into mentions-api commit b4458c19f7c54f18fa415fc04c63642c2f5a17b0 Author: rijkvanzanten <rijkvanzanten@me.com> Date: Thu Nov 18 18:34:04 2021 -0500 Remove unused import commit e6a9d36bbfdf95cb18d29336da61ecb14b677934 Author: rijkvanzanten <rijkvanzanten@me.com> Date: Thu Nov 18 18:28:31 2021 -0500 Extract user mentions from comments commit b3e571a2daa287e1740a050096913662a57e9861 Merge: c93b833d2af2a6dd7fAuthor: rijkvanzanten <rijkvanzanten@me.com> Date: Thu Nov 18 17:39:52 2021 -0500 Merge branch 'mentions' into mentions-api commit c93b833d2b848e306c434b370d4e4e11967e85d0 Author: rijkvanzanten <rijkvanzanten@me.com> Date: Thu Nov 18 17:35:45 2021 -0500 Send emails w/ parsed MD commit 64bbd6596f20a07028d2387d60e33dfe4f91c032 Author: rijkvanzanten <rijkvanzanten@me.com> Date: Thu Nov 18 16:18:16 2021 -0500 Add notifications endpoint + permissions commit fba55c02dc9c303a38b1b958350684cccd3dd82c Author: rijkvanzanten <rijkvanzanten@me.com> Date: Thu Nov 18 15:33:28 2021 -0500 Add system data for notifications * push * Make v-template-input work * Add the two-way binding * submit button posting, not clearing text area * comment text area clearing on submit * Replace insertion correctly * Added scope support to LDAP group and user search (#9529) * Added scope support LDAP group and user search * Fixed linter screwing up my markdown * Update docs/configuration/config-options.md * Always return correct DN for user with sub scope * Fix indeterminate meta and schema property in advanded field creation (#9924) * Fix impossibility to save M2M (alterations not triggered) (#9992) * Fix alterations refactor * fix roles aggregate query (#9994) * Update iis.md (#9998) added the IIS URL Rewrite module as a requirement * New translations en-US.yaml (English, United Kingdom) (#10001) * Fix LDAP race condition (#9993) * Fix input ui * Revert changes to v-field-template * Update mentions permissions * Fix linter warnings * Optimize sending flow * Revert "Rename activity->notifications module (#9446)" This reverts commit428e5d4ea9. * Add notifications drawer * Update migrations * Improve constraints * Add email notifications toggle on users * Add docs, fix graphql support * Move caret-pos to devdeps * Remove unused new triggerKeyPressed system * Remove unused use-caret composable Co-authored-by: Nitwel <nitwel@arcor.de> Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com> Co-authored-by: Ben Haynes <ben@rngr.org> Co-authored-by: Aiden Foxx <aiden.foxx@sbab.se> Co-authored-by: Oreille <33065839+Oreilles@users.noreply.github.com> Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Co-authored-by: Paul Boudewijn <paul@helderinternet.nl>
This commit is contained in:
@@ -1,12 +1,104 @@
|
||||
import { AbstractServiceOptions } from '../types';
|
||||
import { ItemsService } from './index';
|
||||
|
||||
/**
|
||||
* @TODO only return activity of the collections you have access to
|
||||
*/
|
||||
import { AbstractServiceOptions, PrimaryKey, Item, Action } from '../types';
|
||||
import { ItemsService, MutationOptions } from './index';
|
||||
import { NotificationsService } from './notifications';
|
||||
import { UsersService } from './users';
|
||||
import { AuthorizationService } from './authorization';
|
||||
import { Accountability } from '@directus/shared/types';
|
||||
import { getPermissions } from '../utils/get-permissions';
|
||||
import { ForbiddenException } from '../exceptions/forbidden';
|
||||
import logger from '../logger';
|
||||
import { userName } from '../utils/user-name';
|
||||
import { uniq } from 'lodash';
|
||||
import env from '../env';
|
||||
|
||||
export class ActivityService extends ItemsService {
|
||||
notificationsService: NotificationsService;
|
||||
usersService: UsersService;
|
||||
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
super('directus_activity', options);
|
||||
this.notificationsService = new NotificationsService({ schema: this.schema });
|
||||
this.usersService = new UsersService({ schema: this.schema });
|
||||
}
|
||||
|
||||
async createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
if (data.action === Action.COMMENT && typeof data.comment === 'string') {
|
||||
const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
|
||||
|
||||
const mentions = uniq(data.comment.match(usersRegExp) ?? []);
|
||||
|
||||
const sender = await this.usersService.readOne(this.accountability!.user!, {
|
||||
fields: ['id', 'first_name', 'last_name', 'email'],
|
||||
});
|
||||
|
||||
for (const mention of mentions) {
|
||||
const userID = mention.substring(1);
|
||||
|
||||
const user = await this.usersService.readOne(userID, {
|
||||
fields: ['id', 'first_name', 'last_name', 'email', 'role.id', 'role.admin_access', 'role.app_access'],
|
||||
});
|
||||
|
||||
const accountability: Accountability = {
|
||||
user: userID,
|
||||
role: user.role?.id ?? null,
|
||||
admin: user.role?.admin_access ?? null,
|
||||
app: user.role?.app_access ?? null,
|
||||
};
|
||||
|
||||
accountability.permissions = await getPermissions(accountability, this.schema);
|
||||
|
||||
const authorizationService = new AuthorizationService({ schema: this.schema, accountability });
|
||||
const usersService = new UsersService({ schema: this.schema, accountability });
|
||||
|
||||
try {
|
||||
await authorizationService.checkAccess('read', data.collection, data.item);
|
||||
|
||||
const templateData = await usersService.readByQuery({
|
||||
fields: ['id', 'first_name', 'last_name', 'email'],
|
||||
filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
|
||||
});
|
||||
|
||||
const userPreviews = templateData.reduce((acc, user) => {
|
||||
acc[user.id] = `<em>${userName(user)}</em>`;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
let comment = data.comment;
|
||||
|
||||
for (const mention of mentions) {
|
||||
comment = comment.replace(mention, userPreviews[mention.substring(1)] ?? '@Unknown User');
|
||||
}
|
||||
|
||||
comment = `> ${comment}`;
|
||||
|
||||
const message = `
|
||||
Hello ${userName(user)},
|
||||
|
||||
${userName(sender)} has mentioned you in a comment:
|
||||
|
||||
${comment}
|
||||
|
||||
<a href="${env.PUBLIC_URL}/admin/content/${data.collection}/${data.item}">Click here to view.</a>
|
||||
`;
|
||||
|
||||
await this.notificationsService.createOne({
|
||||
recipient: userID,
|
||||
sender: sender.id,
|
||||
subject: `You were mentioned in ${data.collection}`,
|
||||
message,
|
||||
collection: data.collection,
|
||||
item: data.item,
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err instanceof ForbiddenException) {
|
||||
logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.createOne(data, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import { FoldersService } from './folders';
|
||||
import { ItemsService } from './items';
|
||||
import { PermissionsService } from './permissions';
|
||||
import { PresetsService } from './presets';
|
||||
import { NotificationsService } from './notifications';
|
||||
import { RelationsService } from './relations';
|
||||
import { RevisionsService } from './revisions';
|
||||
import { RolesService } from './roles';
|
||||
@@ -197,6 +198,7 @@ export class GraphQLService {
|
||||
};
|
||||
|
||||
const { ReadCollectionTypes } = getReadableTypes();
|
||||
|
||||
const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes();
|
||||
|
||||
const scopeFilter = (collection: SchemaOverview['collections'][string]) => {
|
||||
@@ -1519,6 +1521,8 @@ export class GraphQLService {
|
||||
return new PermissionsService(opts);
|
||||
case 'directus_presets':
|
||||
return new PresetsService(opts);
|
||||
case 'directus_notifications':
|
||||
return new NotificationsService(opts);
|
||||
case 'directus_revisions':
|
||||
return new RevisionsService(opts);
|
||||
case 'directus_roles':
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from './graphql';
|
||||
export * from './import';
|
||||
export * from './mail';
|
||||
export * from './meta';
|
||||
export * from './notifications';
|
||||
export * from './panels';
|
||||
export * from './payload';
|
||||
export * from './permissions';
|
||||
|
||||
@@ -88,6 +88,17 @@ hr {
|
||||
div[style*="margin: 16px 0;"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: #f0f4f9 !important;
|
||||
border-radius: 4px !important;
|
||||
margin: 0 !important;
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
blockquote > p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
/*]]>*/
|
||||
</style>
|
||||
|
||||
@@ -149,4 +160,4 @@ div[style*="margin: 16px 0;"] {
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
48
api/src/services/notifications.ts
Normal file
48
api/src/services/notifications.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { UsersService, MailService } from '.';
|
||||
import { AbstractServiceOptions, PrimaryKey } from '../types';
|
||||
import { ItemsService, MutationOptions } from './items';
|
||||
import { Notification } from '@directus/shared/types';
|
||||
import { md } from '../utils/md';
|
||||
|
||||
export class NotificationsService extends ItemsService {
|
||||
usersService: UsersService;
|
||||
mailService: MailService;
|
||||
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
super('directus_notifications', options);
|
||||
this.usersService = new UsersService({ schema: this.schema });
|
||||
this.mailService = new MailService({ schema: this.schema, accountability: this.accountability });
|
||||
}
|
||||
|
||||
async createOne(data: Partial<Notification>, opts?: MutationOptions): Promise<PrimaryKey> {
|
||||
await this.sendEmail(data);
|
||||
return super.createOne(data, opts);
|
||||
}
|
||||
|
||||
async createMany(data: Partial<Notification>[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
for (const notification of data) {
|
||||
await this.sendEmail(notification);
|
||||
}
|
||||
|
||||
return super.createMany(data, opts);
|
||||
}
|
||||
|
||||
async sendEmail(data: Partial<Notification>) {
|
||||
if (data.recipient) {
|
||||
const user = await this.usersService.readOne(data.recipient, { fields: ['email', 'email_notifications'] });
|
||||
|
||||
if (user.email && user.email_notifications === true) {
|
||||
await this.mailService.send({
|
||||
template: {
|
||||
name: 'base',
|
||||
data: {
|
||||
html: data.message ? md(data.message) : '',
|
||||
},
|
||||
},
|
||||
to: user.email,
|
||||
subject: data.subject,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,8 +217,10 @@ export class UsersService extends ItemsService {
|
||||
*/
|
||||
async deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]> {
|
||||
await this.checkRemainingAdminExistence(keys);
|
||||
await super.deleteMany(keys, opts);
|
||||
|
||||
await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
|
||||
|
||||
await super.deleteMany(keys, opts);
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user