Files
task-stream/libs/web/tracker/application/src/lib/events/issue.events.ts
Josep Piguillem 70e4fbd399 feat: comments marked as decisions, on top of the ticket and searchable
Whoever can change the ticket flags a comment as a decision (new event
CommentDecisionMarkedEvent on the comment). The ticket page lists its
decisions above the description, with a link to the comment in the
conversation (`?comment=<id>` scrolls to it and points at it), and the
search palette finds them by their text across my projects.

API: markDecision / PUT-DELETE /api/comments/{id}/decision; Issue.decisions
and GET /api/issues/{issue}/decisions; decisions(projectId?, search) and
GET /api/projects/{project}/decisions, GET /api/me/decisions. MCP:
mark_decision, search_decisions, and get_issue includes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:32:07 +02:00

107 lines
3.5 KiB
TypeScript

import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events';
import {
Activity,
ChangeStatusInput,
Comment,
CommentChanged,
Issue,
IssueSummary,
LinkIssuesInput,
UpdateIssueInput,
} from '@task-stream/web/tracker/domain';
export const issuePageEvents = eventGroup({
source: 'Issue Page',
events: {
opened: type<{ idOrKey: string }>(),
statusChangeRequested: type<ChangeStatusInput>(),
assignRequested: type<{ issueId: string; assigneeId: string | null }>(),
updateRequested: type<UpdateIssueInput>(),
linkRequested: type<LinkIssuesInput>(),
/** Relate by the other ticket's key (e.g. BE-3), resolved to its id first. */
linkByKeyRequested: type<{
issueId: string;
type: LinkIssuesInput['type'];
targetKey: string;
}>(),
unlinkRequested: type<LinkIssuesInput>(),
sprintChangeRequested: type<{
issueId: string;
sprintId: string | null;
}>(),
releaseChangeRequested: type<{
issueId: string;
releaseId: string | null;
}>(),
commentAddRequested: type<{
issueId: string;
body: string;
/** Uploaded under the new comment once it exists. */
files?: File[];
/** Comment it answers (one level of replies). */
replyTo?: string | null;
}>(),
watchRequested: type<{ issueId: string; watching: boolean }>(),
pinRequested: type<{ issueId: string; pinned: boolean }>(),
trashRequested: type<{ issueId: string; trashed: boolean }>(),
/** Place the ticket under another one by its key (e.g. WEB-3); null: out. */
parentRequested: type<{ issueId: string; parentKey: string | null }>(),
subtaskCreateRequested: type<{
projectId: string;
parentId: string;
type: string;
title: string;
}>(),
checklistAddRequested: type<{ issueId: string; text: string }>(),
checklistUpdateRequested: type<{
issueId: string;
itemId: string;
changes: { text?: string; done?: boolean };
}>(),
checklistRemoveRequested: type<{ issueId: string; itemId: string }>(),
attachRequested: type<{
issueId: string;
files: File[];
commentId?: string | null;
}>(),
attachmentRemoveRequested: type<{
issueId: string;
attachmentId: string;
}>(),
commentEditRequested: type<{ commentId: string; body: string }>(),
reactRequested: type<{ commentId: string; emoji: string; on: boolean }>(),
decisionRequested: type<{ commentId: string; on: boolean }>(),
voteRequested: type<{ issueId: string; on: boolean }>(),
/** An image pasted into a text was uploaded (it joins the ticket's files). */
imagePasted: type<{ issueId: string }>(),
commentDeleteRequested: type<{ commentId: string }>(),
},
});
export const issueApiEvents = eventGroup({
source: 'Issue API',
events: {
loaded: type<Issue>(),
loadFailed: type<string>(),
commentsLoaded: type<Comment[]>(),
historyLoaded: type<Activity[]>(),
childrenLoaded: type<IssueSummary[]>(),
commandSucceeded: type<string>(),
/** A checklist step was saved (no toast for something so small). */
checklistSaved: type<void>(),
commandFailed: type<string>(),
},
});
export const issueRealtimeEvents = eventGroup({
source: 'Issue Realtime',
events: {
/** Something about this ticket changed: refetch it. */
issueTouched: type<{ issueId: string; version: number }>(),
/** A ticket under this one changed, arrived or left: reload them. */
childrenTouched: type<{ issueId: string }>(),
commentChanged: type<CommentChanged>(),
},
});