feat: what is new since my last visit, and turning a comment into a ticket

Opening a ticket notes the visit and answers the one before (issue_visits),
so the page marks others' comments and changes since. A comment can become
a ticket of its own, as a subtask or linked; it remembers where it went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:05:44 +02:00
parent 429b52e6f5
commit 285bd5c454
50 changed files with 1953 additions and 79 deletions

View File

@@ -25,6 +25,7 @@ import {
ReactToCommentDocument,
MarkDecisionDocument,
ResolveThreadDocument,
TurnCommentIntoIssueDocument,
VotePollDocument,
ClosePollDocument,
PublishReleaseDocument,
@@ -111,6 +112,22 @@ export class GraphqlCommentsGateway extends CommentsGateway {
.pipe(map((d) => toCommandResult(d.resolveThread)));
}
turnIntoIssue(
commentId: string,
title: string,
type: string,
asSubtask: boolean,
): Observable<CommandResult> {
return this.gql
.mutate(TurnCommentIntoIssueDocument, {
commentId,
title,
type,
asSubtask,
})
.pipe(map((d) => toCommandResult(d.turnCommentIntoIssue)));
}
markDecision(commentId: string, on: boolean): Observable<CommandResult> {
return this.gql
.mutate(MarkDecisionDocument, { commentId, on })

View File

@@ -57,6 +57,7 @@ import {
CancelReviewDocument,
MyPinsDocument,
FollowTreeDocument,
VisitIssueDocument,
SetReminderDocument,
MyDayDocument,
AddToMyDayDocument,
@@ -272,6 +273,12 @@ export class GraphqlIssuesGateway extends IssuesGateway {
.pipe(map(() => undefined));
}
visit(issueId: string): Observable<string | null> {
return this.gql
.mutate(VisitIssueDocument, { issueId })
.pipe(map((d) => d.visitIssue ?? null));
}
followTree(issueId: string, on: boolean): Observable<void> {
const done$: Observable<unknown> = on
? this.gql.mutate(FollowTreeDocument, { issueId })

View File

@@ -218,6 +218,10 @@ fragment CommentFields on Comment {
resolvedBy {
...UserRefFields
}
spawned {
id
key
}
createdAt
editedAt
author {

View File

@@ -754,6 +754,7 @@ export type IssueFieldsFragment = {
}>;
} | null;
resolvedBy: { id: string; name: string } | null;
spawned: Array<{ id: string; key: string }>;
author: { id: string; name: string };
}>;
reporter: { id: string; name: string };
@@ -817,6 +818,7 @@ export type CommentFieldsFragment = {
}>;
} | null;
resolvedBy: { id: string; name: string } | null;
spawned: Array<{ id: string; key: string }>;
author: { id: string; name: string };
};
@@ -1027,6 +1029,7 @@ export type IssueQuery = {
}>;
} | null;
resolvedBy: { id: string; name: string } | null;
spawned: Array<{ id: string; key: string }>;
author: { id: string; name: string };
}>;
reporter: { id: string; name: string };
@@ -1398,6 +1401,7 @@ export type IssueCommentsQuery = {
}>;
} | null;
resolvedBy: { id: string; name: string } | null;
spawned: Array<{ id: string; key: string }>;
author: { id: string; name: string };
}>;
};
@@ -1830,6 +1834,23 @@ export type ResolveThreadMutation = {
resolveThread: { id: string; key: string | null; version: number };
};
export type TurnCommentIntoIssueMutationVariables = Exact<{
commentId: string;
title: string;
type: string;
asSubtask: boolean;
}>;
export type TurnCommentIntoIssueMutation = {
turnCommentIntoIssue: { id: string; key: string | null; version: number };
};
export type VisitIssueMutationVariables = Exact<{
issueId: string;
}>;
export type VisitIssueMutation = { visitIssue: string | null };
export type MarkDecisionMutationVariables = Exact<{
commentId: string;
on: boolean;
@@ -1870,6 +1891,7 @@ export type SearchDecisionsQuery = {
}>;
} | null;
resolvedBy: { id: string; name: string } | null;
spawned: Array<{ id: string; key: string }>;
author: { id: string; name: string };
};
issue: { id: string; key: string; title: string };
@@ -3254,6 +3276,7 @@ export type CommentChangedSubscription = {
}>;
} | null;
resolvedBy: { id: string; name: string } | null;
spawned: Array<{ id: string; key: string }>;
author: { id: string; name: string };
} | null;
};
@@ -4091,6 +4114,17 @@ export const CommentFieldsFragmentDoc = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'spawned' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -4536,6 +4570,17 @@ export const IssueFieldsFragmentDoc = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'spawned' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -6853,6 +6898,17 @@ export const IssueDocument = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'spawned' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -9204,6 +9260,17 @@ export const IssueCommentsDocument = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'spawned' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -12014,6 +12081,181 @@ export const ResolveThreadDocument = {
ResolveThreadMutation,
ResolveThreadMutationVariables
>;
export const TurnCommentIntoIssueDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'TurnCommentIntoIssue' },
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'commentId' },
},
type: {
kind: 'NonNullType',
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'title' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'String' },
},
},
},
{
kind: 'VariableDefinition',
variable: { kind: 'Variable', name: { kind: 'Name', value: 'type' } },
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'String' },
},
},
},
{
kind: 'VariableDefinition',
variable: {
kind: 'Variable',
name: { kind: 'Name', value: 'asSubtask' },
},
type: {
kind: 'NonNullType',
type: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Boolean' },
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'turnCommentIntoIssue' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'commentId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'commentId' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'title' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'title' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'type' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'type' },
},
},
{
kind: 'Argument',
name: { kind: 'Name', value: 'asSubtask' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'asSubtask' },
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'CommandResultFields' },
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'CommandResultFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'CommandResult' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
{ kind: 'Field', name: { kind: 'Name', value: 'version' } },
],
},
},
],
} as unknown as DocumentNode<
TurnCommentIntoIssueMutation,
TurnCommentIntoIssueMutationVariables
>;
export const VisitIssueDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: { kind: 'Name', value: 'VisitIssue' },
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: 'visitIssue' },
arguments: [
{
kind: 'Argument',
name: { kind: 'Name', value: 'issueId' },
value: {
kind: 'Variable',
name: { kind: 'Name', value: 'issueId' },
},
},
],
},
],
},
},
],
} as unknown as DocumentNode<VisitIssueMutation, VisitIssueMutationVariables>;
export const MarkDecisionDocument = {
kind: 'Document',
definitions: [
@@ -12304,6 +12546,17 @@ export const SearchDecisionsDocument = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'spawned' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{
@@ -19548,6 +19801,17 @@ export const CommentChangedDocument = {
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'spawned' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'key' } },
],
},
},
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
{ kind: 'Field', name: { kind: 'Name', value: 'editedAt' } },
{

View File

@@ -396,6 +396,26 @@ mutation ResolveThread(
}
}
mutation TurnCommentIntoIssue(
$commentId: ID!
$title: String!
$type: String!
$asSubtask: Boolean!
) {
turnCommentIntoIssue(
commentId: $commentId
title: $title
type: $type
asSubtask: $asSubtask
) {
...CommandResultFields
}
}
mutation VisitIssue($issueId: ID!) {
visitIssue(issueId: $issueId)
}
mutation MarkDecision($commentId: ID!, $on: Boolean!) {
markDecision(commentId: $commentId, on: $on) {
...CommandResultFields