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:
@@ -51,6 +51,8 @@ import {
|
||||
VoteIssueDocument,
|
||||
WatchIssueDocument,
|
||||
MyPinsDocument,
|
||||
FollowTreeDocument,
|
||||
UnfollowTreeDocument,
|
||||
SearchDecisionsDocument,
|
||||
PinIssueDocument,
|
||||
UpdateChecklistItemDocument,
|
||||
@@ -218,6 +220,13 @@ export class GraphqlIssuesGateway extends IssuesGateway {
|
||||
),
|
||||
);
|
||||
}
|
||||
followTree(issueId: string, on: boolean): Observable<void> {
|
||||
const done$: Observable<unknown> = on
|
||||
? this.gql.mutate(FollowTreeDocument, { issueId })
|
||||
: this.gql.mutate(UnfollowTreeDocument, { issueId });
|
||||
return done$.pipe(map(() => undefined));
|
||||
}
|
||||
|
||||
pin(issueId: string, pinned: boolean): Observable<void> {
|
||||
const done$: Observable<unknown> = pinned
|
||||
? this.gql.mutate(PinIssueDocument, { issueId })
|
||||
@@ -259,6 +268,7 @@ export class GraphqlIssuesGateway extends IssuesGateway {
|
||||
issue: toIssueSummary(epic.issue),
|
||||
progress: { ...epic.progress },
|
||||
links: epic.issue.links.map((l) => toIssueLink(l)),
|
||||
followingTree: epic.issue.followingTree,
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -85,6 +85,8 @@ export class GraphqlProjectsGateway extends ProjectsGateway {
|
||||
issue: toIssueSummary(e.issue),
|
||||
progress: { ...e.progress },
|
||||
links: [],
|
||||
// The portfolio does not offer following.
|
||||
followingTree: false,
|
||||
})),
|
||||
})),
|
||||
),
|
||||
|
||||
@@ -121,6 +121,7 @@ fragment IssueSummaryFields on Issue {
|
||||
fragment IssueFields on Issue {
|
||||
...IssueSummaryFields
|
||||
pinned
|
||||
followingTree
|
||||
decisions {
|
||||
...CommentFields
|
||||
}
|
||||
|
||||
@@ -688,6 +688,7 @@ export type IssueSummaryFieldsFragment = {
|
||||
|
||||
export type IssueFieldsFragment = {
|
||||
pinned: boolean;
|
||||
followingTree: boolean;
|
||||
description: string | null;
|
||||
figmaUrl: string | null;
|
||||
createdAt: string;
|
||||
@@ -933,6 +934,7 @@ export type IssueQueryVariables = Exact<{
|
||||
export type IssueQuery = {
|
||||
issue: {
|
||||
pinned: boolean;
|
||||
followingTree: boolean;
|
||||
description: string | null;
|
||||
figmaUrl: string | null;
|
||||
createdAt: string;
|
||||
@@ -1151,6 +1153,18 @@ export type MyPinsQuery = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type FollowTreeMutationVariables = Exact<{
|
||||
issueId: string;
|
||||
}>;
|
||||
|
||||
export type FollowTreeMutation = { followTree: boolean };
|
||||
|
||||
export type UnfollowTreeMutationVariables = Exact<{
|
||||
issueId: string;
|
||||
}>;
|
||||
|
||||
export type UnfollowTreeMutation = { unfollowTree: boolean };
|
||||
|
||||
export type PinIssueMutationVariables = Exact<{
|
||||
issueId: string;
|
||||
}>;
|
||||
@@ -1321,6 +1335,7 @@ export type EpicsQueryVariables = Exact<{
|
||||
export type EpicsQuery = {
|
||||
epics: Array<{
|
||||
issue: {
|
||||
followingTree: boolean;
|
||||
id: string;
|
||||
projectId: string;
|
||||
key: string;
|
||||
@@ -3820,6 +3835,7 @@ export const IssueFieldsFragmentDoc = {
|
||||
name: { kind: 'Name', value: 'IssueSummaryFields' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'pinned' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'followingTree' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'decisions' },
|
||||
@@ -6420,6 +6436,7 @@ export const IssueDocument = {
|
||||
name: { kind: 'Name', value: 'IssueSummaryFields' },
|
||||
},
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'pinned' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'followingTree' } },
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'decisions' },
|
||||
@@ -7452,6 +7469,93 @@ export const MyPinsDocument = {
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<MyPinsQuery, MyPinsQueryVariables>;
|
||||
export const FollowTreeDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'FollowTree' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'issueId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'followTree' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'issueId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'issueId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<FollowTreeMutation, FollowTreeMutationVariables>;
|
||||
export const UnfollowTreeDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'UnfollowTree' },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: 'VariableDefinition',
|
||||
variable: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'issueId' },
|
||||
},
|
||||
type: {
|
||||
kind: 'NonNullType',
|
||||
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'unfollowTree' },
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: { kind: 'Name', value: 'issueId' },
|
||||
value: {
|
||||
kind: 'Variable',
|
||||
name: { kind: 'Name', value: 'issueId' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
UnfollowTreeMutation,
|
||||
UnfollowTreeMutationVariables
|
||||
>;
|
||||
export const PinIssueDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
@@ -8633,6 +8737,10 @@ export const EpicsDocument = {
|
||||
kind: 'FragmentSpread',
|
||||
name: { kind: 'Name', value: 'IssueSummaryFields' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'followingTree' },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'links' },
|
||||
|
||||
@@ -101,6 +101,14 @@ query MyPins {
|
||||
}
|
||||
}
|
||||
|
||||
mutation FollowTree($issueId: ID!) {
|
||||
followTree(issueId: $issueId)
|
||||
}
|
||||
|
||||
mutation UnfollowTree($issueId: ID!) {
|
||||
unfollowTree(issueId: $issueId)
|
||||
}
|
||||
|
||||
mutation PinIssue($issueId: ID!) {
|
||||
pinIssue(issueId: $issueId)
|
||||
}
|
||||
@@ -185,6 +193,7 @@ query Epics($projectId: ID!) {
|
||||
epics(projectId: $projectId) {
|
||||
issue {
|
||||
...IssueSummaryFields
|
||||
followingTree
|
||||
links {
|
||||
type
|
||||
direction
|
||||
|
||||
Reference in New Issue
Block a user