feat: follow an epic with everything under it
«Seguir todo» on a ticket with subtasks (and on each epic of the Epics
page) tells you of it and of every ticket hanging from it, at any depth,
also the ones added later: their moves, assignments, comments and plan
changes, as a follower of each, plus when a ticket is placed under it.
Guests of a private project are left out. Personal, no history (Mongo
`tree_follows`).
API: followTree / unfollowTree, Issue.followingTree, PUT/DELETE
/api/issues/{issue}/follow-tree. MCP: watch_issue with tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,7 @@ export const ReadModels = {
|
||||
PinnedIssue: 'PinnedIssue',
|
||||
AlertIntegration: 'AlertIntegration',
|
||||
AlertTrace: 'AlertTrace',
|
||||
TreeFollow: 'TreeFollow',
|
||||
Sprint: 'SprintReadModel',
|
||||
GitIntegration: 'GitIntegration',
|
||||
OutgoingWebhook: 'OutgoingWebhook',
|
||||
@@ -360,6 +361,15 @@ export const AlertTraceSchema =
|
||||
SchemaFactory.createForClass(AlertTraceDocument);
|
||||
AlertTraceSchema.index({ projectId: 1, fingerprint: 1 }, { unique: true });
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'tree_follows' })
|
||||
class TreeFollowDocument {
|
||||
@Prop({ type: String }) userId!: string;
|
||||
@Prop({ type: String, index: true }) issueId!: string;
|
||||
}
|
||||
export const TreeFollowSchema =
|
||||
SchemaFactory.createForClass(TreeFollowDocument);
|
||||
TreeFollowSchema.index({ userId: 1, issueId: 1 }, { unique: true });
|
||||
|
||||
@Schema({ ...LOOSE, collection: 'git_integrations' })
|
||||
class GitIntegrationDocument {
|
||||
/** The project's id. */
|
||||
@@ -465,6 +475,7 @@ export const READ_MODEL_SCHEMAS = [
|
||||
{ name: ReadModels.PinnedIssue, schema: PinnedIssueSchema },
|
||||
{ name: ReadModels.AlertIntegration, schema: AlertIntegrationSchema },
|
||||
{ name: ReadModels.AlertTrace, schema: AlertTraceSchema },
|
||||
{ name: ReadModels.TreeFollow, schema: TreeFollowSchema },
|
||||
{ name: ReadModels.CalendarFeed, schema: CalendarFeedSchema },
|
||||
{ name: ReadModels.DashboardShare, schema: DashboardShareSchema },
|
||||
{ name: ReadModels.PushSubscription, schema: PushSubscriptionSchema },
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import {
|
||||
TreeFollow,
|
||||
TreeFollowsRepository,
|
||||
} from '@task-stream/api/tracker/application';
|
||||
import { Model } from 'mongoose';
|
||||
import { ReadModels } from './read-model.schemas';
|
||||
|
||||
@Injectable()
|
||||
export class MongoTreeFollowsRepository extends TreeFollowsRepository {
|
||||
constructor(
|
||||
@InjectModel(ReadModels.TreeFollow)
|
||||
private readonly follows: Model<TreeFollow>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async follow(follow: TreeFollow): Promise<void> {
|
||||
await this.follows.updateOne(
|
||||
{ userId: follow.userId, issueId: follow.issueId },
|
||||
{ $setOnInsert: follow },
|
||||
{ upsert: true },
|
||||
);
|
||||
}
|
||||
|
||||
async unfollow(userId: string, issueId: string): Promise<void> {
|
||||
await this.follows.deleteOne({ userId, issueId });
|
||||
}
|
||||
|
||||
async isFollowing(userId: string, issueId: string): Promise<boolean> {
|
||||
return !!(await this.follows.exists({ userId, issueId }));
|
||||
}
|
||||
|
||||
async followersOf(issueIds: string[]): Promise<string[]> {
|
||||
if (!issueIds.length) return [];
|
||||
return this.follows.distinct('userId', { issueId: { $in: issueIds } });
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
MyViewsRepository,
|
||||
PinsRepository,
|
||||
AlertIntegrationsRepository,
|
||||
TreeFollowsRepository,
|
||||
GitIntegrationsRepository,
|
||||
OutgoingWebhooksRepository,
|
||||
AutomationsRepository,
|
||||
@@ -82,6 +83,7 @@ import { MongoSavedFiltersRepository } from './mongo/saved-filters.repository';
|
||||
import { MongoMyViewsRepository } from './mongo/my-views.repository';
|
||||
import { MongoPinsRepository } from './mongo/pins.repository';
|
||||
import { MongoAlertIntegrationsRepository } from './mongo/alert-integrations.repository';
|
||||
import { MongoTreeFollowsRepository } from './mongo/tree-follows.repository';
|
||||
import { MongoGitIntegrationsRepository } from './mongo/git-integrations.repository';
|
||||
import { MongoOutgoingWebhooksRepository } from './mongo/outgoing-webhooks.repository';
|
||||
import { HttpWebhookSender } from './webhooks/http-webhook.sender';
|
||||
@@ -163,6 +165,7 @@ const PORTS = [
|
||||
provide: AlertIntegrationsRepository,
|
||||
useClass: MongoAlertIntegrationsRepository,
|
||||
},
|
||||
{ provide: TreeFollowsRepository, useClass: MongoTreeFollowsRepository },
|
||||
{
|
||||
provide: GitIntegrationsRepository,
|
||||
useClass: MongoGitIntegrationsRepository,
|
||||
|
||||
Reference in New Issue
Block a user