mirror of
https://github.com/outline/outline.git
synced 2026-01-06 11:09:55 -06:00
Add notifications for document and collection access (#6460)
* Add notification for added to document * Add notifications for document and collection access * Add notification delay * fix: Collection notifications not appearing * Add notification settings
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
documentPath,
|
||||
settingsPath,
|
||||
} from "~/utils/routeHelpers";
|
||||
import Collection from "./Collection";
|
||||
import Comment from "./Comment";
|
||||
import Document from "./Document";
|
||||
import User from "./User";
|
||||
@@ -57,6 +58,14 @@ class Notification extends Model {
|
||||
*/
|
||||
collectionId?: string;
|
||||
|
||||
/**
|
||||
* The collection that the notification is associated with.
|
||||
*/
|
||||
@Relation(() => Collection, { onDelete: "cascade" })
|
||||
collection?: Collection;
|
||||
|
||||
commentId?: string;
|
||||
|
||||
/**
|
||||
* The comment that the notification is associated with.
|
||||
*/
|
||||
@@ -114,6 +123,10 @@ class Notification extends Model {
|
||||
return t("mentioned you in");
|
||||
case NotificationEventType.CreateComment:
|
||||
return t("left a comment on");
|
||||
case NotificationEventType.AddUserToDocument:
|
||||
return t("shared");
|
||||
case NotificationEventType.AddUserToCollection:
|
||||
return t("invited you to");
|
||||
default:
|
||||
return this.event;
|
||||
}
|
||||
@@ -126,7 +139,13 @@ class Notification extends Model {
|
||||
* @returns The subject
|
||||
*/
|
||||
get subject() {
|
||||
return this.document?.title;
|
||||
if (this.documentId) {
|
||||
return this.document?.title ?? "a document";
|
||||
}
|
||||
if (this.collectionId) {
|
||||
return this.collection?.name ?? "a collection";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,20 +161,22 @@ class Notification extends Model {
|
||||
case NotificationEventType.CreateRevision: {
|
||||
return this.document ? documentPath(this.document) : "";
|
||||
}
|
||||
case NotificationEventType.AddUserToCollection:
|
||||
case NotificationEventType.CreateCollection: {
|
||||
const collection = this.collectionId
|
||||
? this.store.rootStore.documents.get(this.collectionId)
|
||||
: undefined;
|
||||
return collection ? collectionPath(collection.url) : "";
|
||||
return collection ? collectionPath(collection.path) : "";
|
||||
}
|
||||
case NotificationEventType.AddUserToDocument:
|
||||
case NotificationEventType.MentionedInDocument: {
|
||||
return this.document?.url;
|
||||
return this.document?.path;
|
||||
}
|
||||
case NotificationEventType.MentionedInComment:
|
||||
case NotificationEventType.CreateComment: {
|
||||
return this.document && this.comment
|
||||
? commentPath(this.document, this.comment)
|
||||
: this.document?.url;
|
||||
: this.document?.path;
|
||||
}
|
||||
case NotificationEventType.InviteAccepted: {
|
||||
return settingsPath("members");
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CheckboxIcon,
|
||||
CollectionIcon,
|
||||
CommentIcon,
|
||||
DocumentIcon,
|
||||
EditIcon,
|
||||
EmailIcon,
|
||||
PublishIcon,
|
||||
@@ -80,6 +81,22 @@ function Notifications() {
|
||||
"Receive a notification when someone you invited creates an account"
|
||||
),
|
||||
},
|
||||
{
|
||||
event: NotificationEventType.AddUserToDocument,
|
||||
icon: <DocumentIcon />,
|
||||
title: t("Invited to document"),
|
||||
description: t(
|
||||
"Receive a notification when a document is shared with you"
|
||||
),
|
||||
},
|
||||
{
|
||||
event: NotificationEventType.AddUserToCollection,
|
||||
icon: <CollectionIcon />,
|
||||
title: t("Invited to collection"),
|
||||
description: t(
|
||||
"Receive a notification when you are given access to a collection"
|
||||
),
|
||||
},
|
||||
{
|
||||
event: NotificationEventType.ExportCompleted,
|
||||
icon: <CheckboxIcon checked />,
|
||||
|
||||
@@ -450,7 +450,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
|
||||
event,
|
||||
subscription,
|
||||
payload: {
|
||||
id: event.data.membershipId,
|
||||
id: event.modelId,
|
||||
model: model && presentMembership(model),
|
||||
collection: model && presentCollection(model.collection!),
|
||||
user: model && presentUser(model.user),
|
||||
@@ -477,7 +477,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
|
||||
event,
|
||||
subscription,
|
||||
payload: {
|
||||
id: event.data.membershipId,
|
||||
id: event.modelId,
|
||||
model: model && presentCollectionGroupMembership(model),
|
||||
collection: model && presentCollection(model.collection!),
|
||||
group: model && presentGroup(model.group),
|
||||
|
||||
102
server/emails/templates/CollectionSharedEmail.tsx
Normal file
102
server/emails/templates/CollectionSharedEmail.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as React from "react";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { Collection, UserMembership } from "@server/models";
|
||||
import BaseEmail, { EmailProps } from "./BaseEmail";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
import EmailTemplate from "./components/EmailLayout";
|
||||
import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
type InputProps = EmailProps & {
|
||||
userId: string;
|
||||
collectionId: string;
|
||||
actorName: string;
|
||||
teamUrl: string;
|
||||
};
|
||||
|
||||
type BeforeSend = {
|
||||
collection: Collection;
|
||||
membership: UserMembership;
|
||||
};
|
||||
|
||||
type Props = InputProps & BeforeSend;
|
||||
|
||||
/**
|
||||
* Email sent to a user when someone adds them to a collection.
|
||||
*/
|
||||
export default class CollectionSharedEmail extends BaseEmail<
|
||||
InputProps,
|
||||
BeforeSend
|
||||
> {
|
||||
protected async beforeSend({ userId, collectionId }: InputProps) {
|
||||
const collection = await Collection.findByPk(collectionId);
|
||||
if (!collection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const membership = await UserMembership.findOne({
|
||||
where: {
|
||||
collectionId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
if (!membership) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { collection, membership };
|
||||
}
|
||||
|
||||
protected subject({ actorName, collection }: Props) {
|
||||
return `${actorName} invited you to the “${collection.name}” collection`;
|
||||
}
|
||||
|
||||
protected preview({ actorName }: Props): string {
|
||||
return `${actorName} invited you to a collection`;
|
||||
}
|
||||
|
||||
protected fromName({ actorName }: Props) {
|
||||
return actorName;
|
||||
}
|
||||
|
||||
protected renderAsText({ actorName, teamUrl, collection }: Props): string {
|
||||
return `
|
||||
${actorName} invited you to the “${collection.name}” collection.
|
||||
|
||||
View Document: ${teamUrl}${collection.path}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(props: Props) {
|
||||
const { collection, membership, actorName, teamUrl } = props;
|
||||
const collectionUrl = `${teamUrl}${collection.path}?ref=notification-email`;
|
||||
|
||||
const permission =
|
||||
membership.permission === CollectionPermission.ReadWrite
|
||||
? "view and edit"
|
||||
: CollectionPermission.Admin
|
||||
? "manage"
|
||||
: "view";
|
||||
|
||||
return (
|
||||
<EmailTemplate
|
||||
previewText={this.preview(props)}
|
||||
goToAction={{ url: collectionUrl, name: "View Collection" }}
|
||||
>
|
||||
<Header />
|
||||
|
||||
<Body>
|
||||
<Heading>{collection.name}</Heading>
|
||||
<p>
|
||||
{actorName} invited you to {permission} documents in the{" "}
|
||||
<a href={collectionUrl}>{collection.name}</a> collection.
|
||||
</p>
|
||||
<p>
|
||||
<Button href={collectionUrl}>View Collection</Button>
|
||||
</p>
|
||||
</Body>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
}
|
||||
98
server/emails/templates/DocumentSharedEmail.tsx
Normal file
98
server/emails/templates/DocumentSharedEmail.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import * as React from "react";
|
||||
import { DocumentPermission } from "@shared/types";
|
||||
import { Document, UserMembership } from "@server/models";
|
||||
import BaseEmail, { EmailProps } from "./BaseEmail";
|
||||
import Body from "./components/Body";
|
||||
import Button from "./components/Button";
|
||||
import EmailTemplate from "./components/EmailLayout";
|
||||
import Header from "./components/Header";
|
||||
import Heading from "./components/Heading";
|
||||
|
||||
type InputProps = EmailProps & {
|
||||
userId: string;
|
||||
documentId: string;
|
||||
actorName: string;
|
||||
teamUrl: string;
|
||||
};
|
||||
|
||||
type BeforeSend = {
|
||||
document: Document;
|
||||
membership: UserMembership;
|
||||
};
|
||||
|
||||
type Props = InputProps & BeforeSend;
|
||||
|
||||
/**
|
||||
* Email sent to a user when someone adds them to a document.
|
||||
*/
|
||||
export default class DocumentSharedEmail extends BaseEmail<
|
||||
InputProps,
|
||||
BeforeSend
|
||||
> {
|
||||
protected async beforeSend({ documentId, userId }: InputProps) {
|
||||
const document = await Document.unscoped().findByPk(documentId);
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const membership = await UserMembership.findOne({
|
||||
where: {
|
||||
documentId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
if (!membership) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return { document, membership };
|
||||
}
|
||||
|
||||
protected subject({ actorName, document }: Props) {
|
||||
return `${actorName} shared “${document.title}” with you`;
|
||||
}
|
||||
|
||||
protected preview({ actorName }: Props): string {
|
||||
return `${actorName} shared a document`;
|
||||
}
|
||||
|
||||
protected fromName({ actorName }: Props) {
|
||||
return actorName;
|
||||
}
|
||||
|
||||
protected renderAsText({ actorName, teamUrl, document }: Props): string {
|
||||
return `
|
||||
${actorName} shared “${document.title}” with you.
|
||||
|
||||
View Document: ${teamUrl}${document.path}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(props: Props) {
|
||||
const { document, membership, actorName, teamUrl } = props;
|
||||
const documentUrl = `${teamUrl}${document.path}?ref=notification-email`;
|
||||
|
||||
const permission =
|
||||
membership.permission === DocumentPermission.ReadWrite ? "edit" : "view";
|
||||
|
||||
return (
|
||||
<EmailTemplate
|
||||
previewText={this.preview(props)}
|
||||
goToAction={{ url: documentUrl, name: "View Document" }}
|
||||
>
|
||||
<Header />
|
||||
|
||||
<Body>
|
||||
<Heading>{document.title}</Heading>
|
||||
<p>
|
||||
{actorName} invited you to {permission} the{" "}
|
||||
<a href={documentUrl}>{document.title}</a> document.
|
||||
</p>
|
||||
<p>
|
||||
<Button href={documentUrl}>View Document</Button>
|
||||
</p>
|
||||
</Body>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,17 @@ class Collection extends ParanoidModel<
|
||||
|
||||
// getters
|
||||
|
||||
/**
|
||||
* The frontend path to this collection.
|
||||
*
|
||||
* @deprecated Use `path` instead.
|
||||
*/
|
||||
get url(): string {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/** The frontend path to this collection. */
|
||||
get path(): string {
|
||||
if (!this.name) {
|
||||
return `/collection/untitled-${this.urlId}`;
|
||||
}
|
||||
|
||||
@@ -310,6 +310,11 @@ class Document extends ParanoidModel<
|
||||
* @deprecated Use `path` instead.
|
||||
*/
|
||||
get url() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/** The frontend path to this document. */
|
||||
get path() {
|
||||
if (!this.title) {
|
||||
return `/doc/untitled-${this.urlId}`;
|
||||
}
|
||||
@@ -317,11 +322,6 @@ class Document extends ParanoidModel<
|
||||
return `/doc/${slugifiedTitle}-${this.urlId}`;
|
||||
}
|
||||
|
||||
/** The frontend path to this document. */
|
||||
get path() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
return getTasks(this.text || "");
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ class Event extends IdModel<
|
||||
"collections.delete",
|
||||
"collections.move",
|
||||
"collections.permission_changed",
|
||||
"collections.add_user",
|
||||
"collections.remove_user",
|
||||
"documents.publish",
|
||||
"documents.unpublish",
|
||||
"documents.archive",
|
||||
|
||||
@@ -72,12 +72,15 @@ import Fix from "./decorators/Fix";
|
||||
include: [
|
||||
{
|
||||
association: "document",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
association: "comment",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
association: "actor",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -181,7 +184,9 @@ class Notification extends Model<
|
||||
userId: model.userId,
|
||||
modelId: model.id,
|
||||
teamId: model.teamId,
|
||||
commentId: model.commentId,
|
||||
documentId: model.documentId,
|
||||
collectionId: model.collectionId,
|
||||
actorId: model.actorId,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { NotificationEventType } from "@shared/types";
|
||||
import CollectionCreatedEmail from "@server/emails/templates/CollectionCreatedEmail";
|
||||
import CollectionSharedEmail from "@server/emails/templates/CollectionSharedEmail";
|
||||
import CommentCreatedEmail from "@server/emails/templates/CommentCreatedEmail";
|
||||
import CommentMentionedEmail from "@server/emails/templates/CommentMentionedEmail";
|
||||
import DocumentMentionedEmail from "@server/emails/templates/DocumentMentionedEmail";
|
||||
import DocumentPublishedOrUpdatedEmail from "@server/emails/templates/DocumentPublishedOrUpdatedEmail";
|
||||
import DocumentSharedEmail from "@server/emails/templates/DocumentSharedEmail";
|
||||
import { Notification } from "@server/models";
|
||||
import { Event, NotificationEvent } from "@server/types";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
@@ -23,6 +25,10 @@ export default class EmailsProcessor extends BaseProcessor {
|
||||
|
||||
const notificationId = notification.id;
|
||||
|
||||
if (notification.user.isSuspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (notification.event) {
|
||||
case NotificationEventType.UpdateDocument:
|
||||
case NotificationEventType.PublishDocument: {
|
||||
@@ -41,6 +47,34 @@ export default class EmailsProcessor extends BaseProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
case NotificationEventType.AddUserToDocument: {
|
||||
await new DocumentSharedEmail(
|
||||
{
|
||||
to: notification.user.email,
|
||||
userId: notification.userId,
|
||||
documentId: notification.documentId,
|
||||
teamUrl: notification.team.url,
|
||||
actorName: notification.actor.name,
|
||||
},
|
||||
{ notificationId }
|
||||
).schedule();
|
||||
return;
|
||||
}
|
||||
|
||||
case NotificationEventType.AddUserToCollection: {
|
||||
await new CollectionSharedEmail(
|
||||
{
|
||||
to: notification.user.email,
|
||||
userId: notification.userId,
|
||||
collectionId: notification.collectionId,
|
||||
teamUrl: notification.team.url,
|
||||
actorName: notification.actor.name,
|
||||
},
|
||||
{ notificationId }
|
||||
).schedule();
|
||||
return;
|
||||
}
|
||||
|
||||
case NotificationEventType.MentionedInDocument: {
|
||||
await new DocumentMentionedEmail(
|
||||
{
|
||||
|
||||
@@ -5,10 +5,14 @@ import {
|
||||
Event,
|
||||
DocumentEvent,
|
||||
CommentEvent,
|
||||
CollectionUserEvent,
|
||||
DocumentUserEvent,
|
||||
} from "@server/types";
|
||||
import CollectionAddUserNotificationsTask from "../tasks/CollectionAddUserNotificationsTask";
|
||||
import CollectionCreatedNotificationsTask from "../tasks/CollectionCreatedNotificationsTask";
|
||||
import CommentCreatedNotificationsTask from "../tasks/CommentCreatedNotificationsTask";
|
||||
import CommentUpdatedNotificationsTask from "../tasks/CommentUpdatedNotificationsTask";
|
||||
import DocumentAddUserNotificationsTask from "../tasks/DocumentAddUserNotificationsTask";
|
||||
import DocumentPublishedNotificationsTask from "../tasks/DocumentPublishedNotificationsTask";
|
||||
import RevisionCreatedNotificationsTask from "../tasks/RevisionCreatedNotificationsTask";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
@@ -16,8 +20,10 @@ import BaseProcessor from "./BaseProcessor";
|
||||
export default class NotificationsProcessor extends BaseProcessor {
|
||||
static applicableEvents: Event["name"][] = [
|
||||
"documents.publish",
|
||||
"documents.add_user",
|
||||
"revisions.create",
|
||||
"collections.create",
|
||||
"collections.add_user",
|
||||
"comments.create",
|
||||
"comments.update",
|
||||
];
|
||||
@@ -26,10 +32,14 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
switch (event.name) {
|
||||
case "documents.publish":
|
||||
return this.documentPublished(event);
|
||||
case "documents.add_user":
|
||||
return this.documentAddUser(event);
|
||||
case "revisions.create":
|
||||
return this.revisionCreated(event);
|
||||
case "collections.create":
|
||||
return this.collectionCreated(event);
|
||||
case "collections.add_user":
|
||||
return this.collectionAddUser(event);
|
||||
case "comments.create":
|
||||
return this.commentCreated(event);
|
||||
case "comments.update":
|
||||
@@ -51,6 +61,16 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
await DocumentPublishedNotificationsTask.schedule(event);
|
||||
}
|
||||
|
||||
async documentAddUser(event: DocumentUserEvent) {
|
||||
if (!event.data.isNew || event.userId === event.actorId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await DocumentAddUserNotificationsTask.schedule(event, {
|
||||
delay: Minute,
|
||||
});
|
||||
}
|
||||
|
||||
async revisionCreated(event: RevisionEvent) {
|
||||
await RevisionCreatedNotificationsTask.schedule(event);
|
||||
}
|
||||
@@ -68,6 +88,16 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
await CollectionCreatedNotificationsTask.schedule(event);
|
||||
}
|
||||
|
||||
async collectionAddUser(event: CollectionUserEvent) {
|
||||
if (!event.data.isNew || event.userId === event.actorId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await CollectionAddUserNotificationsTask.schedule(event, {
|
||||
delay: Minute,
|
||||
});
|
||||
}
|
||||
|
||||
async commentCreated(event: CommentEvent) {
|
||||
await CommentCreatedNotificationsTask.schedule(event, {
|
||||
delay: Minute,
|
||||
|
||||
32
server/queues/tasks/CollectionAddUserNotificationsTask.ts
Normal file
32
server/queues/tasks/CollectionAddUserNotificationsTask.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NotificationEventType } from "@shared/types";
|
||||
import { Notification, User } from "@server/models";
|
||||
import { CollectionUserEvent } from "@server/types";
|
||||
import BaseTask, { TaskPriority } from "./BaseTask";
|
||||
|
||||
export default class CollectionAddUserNotificationsTask extends BaseTask<CollectionUserEvent> {
|
||||
public async perform(event: CollectionUserEvent) {
|
||||
const recipient = await User.findByPk(event.userId);
|
||||
if (!recipient) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!recipient.isSuspended &&
|
||||
recipient.subscribedToEventType(NotificationEventType.AddUserToCollection)
|
||||
) {
|
||||
await Notification.create({
|
||||
event: NotificationEventType.AddUserToCollection,
|
||||
userId: event.userId,
|
||||
actorId: event.actorId,
|
||||
teamId: event.teamId,
|
||||
collectionId: event.collectionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get options() {
|
||||
return {
|
||||
priority: TaskPriority.Background,
|
||||
};
|
||||
}
|
||||
}
|
||||
32
server/queues/tasks/DocumentAddUserNotificationsTask.ts
Normal file
32
server/queues/tasks/DocumentAddUserNotificationsTask.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NotificationEventType } from "@shared/types";
|
||||
import { Notification, User } from "@server/models";
|
||||
import { DocumentUserEvent } from "@server/types";
|
||||
import BaseTask, { TaskPriority } from "./BaseTask";
|
||||
|
||||
export default class DocumentAddUserNotificationsTask extends BaseTask<DocumentUserEvent> {
|
||||
public async perform(event: DocumentUserEvent) {
|
||||
const recipient = await User.findByPk(event.userId);
|
||||
if (!recipient) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!recipient.isSuspended &&
|
||||
recipient.subscribedToEventType(NotificationEventType.AddUserToDocument)
|
||||
) {
|
||||
await Notification.create({
|
||||
event: NotificationEventType.AddUserToDocument,
|
||||
userId: event.userId,
|
||||
actorId: event.actorId,
|
||||
teamId: event.teamId,
|
||||
documentId: event.documentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get options() {
|
||||
return {
|
||||
priority: TaskPriority.Background,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -382,6 +382,7 @@ router.post(
|
||||
router.post(
|
||||
"collections.add_user",
|
||||
auth(),
|
||||
rateLimiter(RateLimiterStrategy.OneHundredPerHour),
|
||||
transaction(),
|
||||
validate(T.CollectionsAddUserSchema),
|
||||
async (ctx: APIContext<T.CollectionsAddUserReq>) => {
|
||||
@@ -397,28 +398,20 @@ router.post(
|
||||
const user = await User.findByPk(userId);
|
||||
authorize(actor, "read", user);
|
||||
|
||||
let membership = await UserMembership.findOne({
|
||||
const [membership, isNew] = await UserMembership.findOrCreate({
|
||||
where: {
|
||||
collectionId: id,
|
||||
userId,
|
||||
},
|
||||
defaults: {
|
||||
permission: permission || user.defaultCollectionPermission,
|
||||
createdById: actor.id,
|
||||
},
|
||||
transaction,
|
||||
lock: transaction.LOCK.UPDATE,
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
membership = await UserMembership.create(
|
||||
{
|
||||
collectionId: id,
|
||||
userId,
|
||||
permission: permission || user.defaultCollectionPermission,
|
||||
createdById: actor.id,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
} else if (permission) {
|
||||
if (permission) {
|
||||
membership.permission = permission;
|
||||
await membership.save({ transaction });
|
||||
}
|
||||
@@ -427,12 +420,13 @@ router.post(
|
||||
{
|
||||
name: "collections.add_user",
|
||||
userId,
|
||||
modelId: membership.id,
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: actor.id,
|
||||
data: {
|
||||
name: user.name,
|
||||
membershipId: membership.id,
|
||||
isNew,
|
||||
permission: membership.permission,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
},
|
||||
@@ -482,12 +476,12 @@ router.post(
|
||||
{
|
||||
name: "collections.remove_user",
|
||||
userId,
|
||||
modelId: membership.id,
|
||||
collectionId: collection.id,
|
||||
teamId: collection.teamId,
|
||||
actorId: actor.id,
|
||||
data: {
|
||||
name: user.name,
|
||||
membershipId: membership.id,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
},
|
||||
|
||||
@@ -1475,6 +1475,7 @@ router.post(
|
||||
"documents.add_user",
|
||||
auth(),
|
||||
validate(T.DocumentsAddUserSchema),
|
||||
rateLimiter(RateLimiterStrategy.OneHundredPerHour),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.DocumentsAddUserReq>) => {
|
||||
const { auth, transaction } = ctx.state;
|
||||
@@ -1521,7 +1522,7 @@ router.post(
|
||||
UserMemberships.length ? UserMemberships[0].index : null
|
||||
);
|
||||
|
||||
const [membership] = await UserMembership.findOrCreate({
|
||||
const [membership, isNew] = await UserMembership.findOrCreate({
|
||||
where: {
|
||||
documentId: id,
|
||||
userId,
|
||||
@@ -1553,6 +1554,11 @@ router.post(
|
||||
teamId: document.teamId,
|
||||
actorId: actor.id,
|
||||
ip: ctx.request.ip,
|
||||
data: {
|
||||
title: document.title,
|
||||
isNew,
|
||||
permission: membership.permission,
|
||||
},
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NavigationNode,
|
||||
Client,
|
||||
CollectionPermission,
|
||||
DocumentPermission,
|
||||
} from "@shared/types";
|
||||
import { BaseSchema } from "@server/routes/api/schema";
|
||||
import { AccountProvisionerResult } from "./commands/accountProvisioner";
|
||||
@@ -209,15 +210,19 @@ export type FileOperationEvent = BaseEvent & {
|
||||
export type CollectionUserEvent = BaseEvent & {
|
||||
name: "collections.add_user" | "collections.remove_user";
|
||||
userId: string;
|
||||
modelId: string;
|
||||
collectionId: string;
|
||||
data: { name: string; membershipId: string };
|
||||
data: {
|
||||
isNew?: boolean;
|
||||
permission?: CollectionPermission;
|
||||
};
|
||||
};
|
||||
|
||||
export type CollectionGroupEvent = BaseEvent & {
|
||||
name: "collections.add_group" | "collections.remove_group";
|
||||
collectionId: string;
|
||||
modelId: string;
|
||||
data: { name: string; membershipId: string };
|
||||
data: { name: string };
|
||||
};
|
||||
|
||||
export type DocumentUserEvent = BaseEvent & {
|
||||
@@ -225,6 +230,11 @@ export type DocumentUserEvent = BaseEvent & {
|
||||
userId: string;
|
||||
modelId: string;
|
||||
documentId: string;
|
||||
data: {
|
||||
title: string;
|
||||
isNew?: boolean;
|
||||
permission?: DocumentPermission;
|
||||
};
|
||||
};
|
||||
|
||||
export type CollectionEvent = BaseEvent &
|
||||
@@ -381,7 +391,10 @@ export type NotificationEvent = BaseEvent & {
|
||||
modelId: string;
|
||||
teamId: string;
|
||||
userId: string;
|
||||
actorId: string;
|
||||
commentId?: string;
|
||||
documentId?: string;
|
||||
collectionId?: string;
|
||||
};
|
||||
|
||||
export type Event =
|
||||
|
||||
@@ -459,6 +459,8 @@
|
||||
"created the collection": "created the collection",
|
||||
"mentioned you in": "mentioned you in",
|
||||
"left a comment on": "left a comment on",
|
||||
"shared": "shared",
|
||||
"invited you to": "invited you to",
|
||||
"API token created": "API token created",
|
||||
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
|
||||
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
|
||||
@@ -858,6 +860,10 @@
|
||||
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
|
||||
"Invite accepted": "Invite accepted",
|
||||
"Receive a notification when someone you invited creates an account": "Receive a notification when someone you invited creates an account",
|
||||
"Invited to document": "Invited to document",
|
||||
"Receive a notification when a document is shared with you": "Receive a notification when a document is shared with you",
|
||||
"Invited to collection": "Invited to collection",
|
||||
"Receive a notification when you are given access to a collection": "Receive a notification when you are given access to a collection",
|
||||
"Export completed": "Export completed",
|
||||
"Receive a notification when an export you requested has been completed": "Receive a notification when an export you requested has been completed",
|
||||
"Getting started": "Getting started",
|
||||
|
||||
@@ -203,6 +203,8 @@ export type CollectionSort = {
|
||||
export enum NotificationEventType {
|
||||
PublishDocument = "documents.publish",
|
||||
UpdateDocument = "documents.update",
|
||||
AddUserToDocument = "documents.add_user",
|
||||
AddUserToCollection = "collections.add_user",
|
||||
CreateRevision = "revisions.create",
|
||||
CreateCollection = "collections.create",
|
||||
CreateComment = "comments.create",
|
||||
@@ -239,6 +241,8 @@ export const NotificationEventDefaults = {
|
||||
[NotificationEventType.Onboarding]: true,
|
||||
[NotificationEventType.Features]: true,
|
||||
[NotificationEventType.ExportCompleted]: true,
|
||||
[NotificationEventType.AddUserToDocument]: true,
|
||||
[NotificationEventType.AddUserToCollection]: true,
|
||||
};
|
||||
|
||||
export enum UnfurlType {
|
||||
|
||||
Reference in New Issue
Block a user