feat: tickets I follow, and more news for followers
"Que sigo" on the home page lists the open tickets I follow, whoever works on them; `watching` filters my issues (REST, GraphQL, MCP) and a project's search (none without a session). Followers now also hear when a ticket's priority or due date changes, and when it enters or leaves a sprint. Smaller edits (title, labels…) still do not notify. Docs (with the REPLIED row the replies change missed), Word and PDF updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ export const homeApiEvents = eventGroup({
|
||||
loaded: type<{
|
||||
mine: IssuePage;
|
||||
awaitingQa: IssuePage;
|
||||
following: IssuePage;
|
||||
releases: Release[];
|
||||
}>(),
|
||||
loadFailed: type<string>(),
|
||||
|
||||
@@ -390,19 +390,22 @@ describe('HomeFacade', () => {
|
||||
({ id: key, key, status, statusCategory, title: key }) as never;
|
||||
|
||||
it('loads my work and groups it by category, QA failures first', () => {
|
||||
const mine = vi.fn((filter: { assignedToMe: boolean }) =>
|
||||
of(
|
||||
filter.assignedToMe
|
||||
? {
|
||||
items: [
|
||||
issue('WEB-1', 'IN_PROGRESS', 'IN_PROGRESS'),
|
||||
issue('WEB-4', 'BACKLOG', 'TODO'),
|
||||
issue('WEB-2', 'QA_FAILED', 'IN_PROGRESS'),
|
||||
],
|
||||
total: 3,
|
||||
}
|
||||
: { items: [issue('WEB-3', 'STAGING', 'QA')], total: 1 },
|
||||
),
|
||||
const mine = vi.fn(
|
||||
(filter: { assignedToMe: boolean; watching?: boolean }) =>
|
||||
of(
|
||||
filter.watching
|
||||
? { items: [issue('API-7', 'IN_REVIEW', 'IN_PROGRESS')], total: 1 }
|
||||
: filter.assignedToMe
|
||||
? {
|
||||
items: [
|
||||
issue('WEB-1', 'IN_PROGRESS', 'IN_PROGRESS'),
|
||||
issue('WEB-4', 'BACKLOG', 'TODO'),
|
||||
issue('WEB-2', 'QA_FAILED', 'IN_PROGRESS'),
|
||||
],
|
||||
total: 3,
|
||||
}
|
||||
: { items: [issue('WEB-3', 'STAGING', 'QA')], total: 1 },
|
||||
),
|
||||
);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -437,6 +440,10 @@ describe('HomeFacade', () => {
|
||||
]);
|
||||
expect(home.qaFailed()).toBe(1);
|
||||
expect(home.awaitingQaTotal()).toBe(1);
|
||||
expect(mine).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ assignedToMe: false, watching: true }),
|
||||
);
|
||||
expect(home.following().map((i) => i.key)).toEqual(['API-7']);
|
||||
expect(home.releases().map((r) => r.name)).toEqual(['v1.4.0']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,9 @@ export class HomeFacade {
|
||||
/** Tickets of my projects in a QA-category status (anyone's). */
|
||||
readonly awaitingQa = this.store.awaitingQa;
|
||||
readonly awaitingQaTotal = this.store.awaitingQaTotal;
|
||||
/** Open tickets I follow, whoever works on them. */
|
||||
readonly following = this.store.following;
|
||||
readonly followingTotal = this.store.followingTotal;
|
||||
/** Planned releases of my projects, soonest first. */
|
||||
readonly releases = this.store.releases;
|
||||
/** False until the first load: show skeletons, not empty states. */
|
||||
|
||||
@@ -32,6 +32,8 @@ interface HomeState {
|
||||
mineTotal: number;
|
||||
awaitingQa: IssueSummary[];
|
||||
awaitingQaTotal: number;
|
||||
following: IssueSummary[];
|
||||
followingTotal: number;
|
||||
releases: Release[];
|
||||
loaded: boolean;
|
||||
loading: boolean;
|
||||
@@ -40,7 +42,7 @@ interface HomeState {
|
||||
|
||||
/**
|
||||
* "My work" across every project I belong to: my open tickets, tickets
|
||||
* waiting for QA and upcoming releases. Root-scoped, so coming back to the
|
||||
* waiting for QA, the open ones I follow and upcoming releases. Root-scoped, so coming back to the
|
||||
* home page shows the last data at once while it refreshes.
|
||||
*/
|
||||
export const HomeStore = signalStore(
|
||||
@@ -50,6 +52,8 @@ export const HomeStore = signalStore(
|
||||
mineTotal: 0,
|
||||
awaitingQa: [],
|
||||
awaitingQaTotal: 0,
|
||||
following: [],
|
||||
followingTotal: 0,
|
||||
releases: [],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
@@ -82,6 +86,8 @@ export const HomeStore = signalStore(
|
||||
mineTotal: payload.mine.total,
|
||||
awaitingQa: payload.awaitingQa.items,
|
||||
awaitingQaTotal: payload.awaitingQa.total,
|
||||
following: payload.following.items,
|
||||
followingTotal: payload.following.total,
|
||||
releases: payload.releases,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
@@ -111,6 +117,12 @@ export const HomeStore = signalStore(
|
||||
categories: [StatusCategory.QA],
|
||||
limit: 20,
|
||||
}),
|
||||
following: issues.mine({
|
||||
assignedToMe: false,
|
||||
watching: true,
|
||||
categories: [...OPEN_CATEGORIES],
|
||||
limit: 20,
|
||||
}),
|
||||
releases: releases.upcoming(5),
|
||||
}).pipe(
|
||||
mapResponse({
|
||||
|
||||
@@ -72,6 +72,8 @@ export interface CreateReleaseInput {
|
||||
export interface MyIssuesFilter {
|
||||
/** true: assigned to me. false: any ticket of my projects. */
|
||||
assignedToMe: boolean;
|
||||
/** Only the tickets I follow. */
|
||||
watching?: boolean;
|
||||
categories?: StatusCategory[];
|
||||
/** Matches key, title or description. */
|
||||
search?: string;
|
||||
|
||||
@@ -89,6 +89,7 @@ export class GraphqlIssuesGateway extends IssuesGateway {
|
||||
return this.gql
|
||||
.query(MyIssuesDocument, {
|
||||
assignedToMe: filter.assignedToMe,
|
||||
watching: filter.watching ?? false,
|
||||
categories: filter.categories as never,
|
||||
limit: filter.limit,
|
||||
search: filter.search?.trim() || null,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -132,12 +132,14 @@ mutation DeleteComment($commentId: ID!) {
|
||||
|
||||
query MyIssues(
|
||||
$assignedToMe: Boolean!
|
||||
$watching: Boolean
|
||||
$categories: [StatusCategory!]
|
||||
$limit: Int!
|
||||
$search: String
|
||||
) {
|
||||
myIssues(
|
||||
assignedToMe: $assignedToMe
|
||||
watching: $watching
|
||||
categories: $categories
|
||||
limit: $limit
|
||||
search: $search
|
||||
|
||||
@@ -33,7 +33,8 @@ const VISIBLE = 12;
|
||||
|
||||
/**
|
||||
* "Mi trabajo" on the home page, across every project I belong to: my open
|
||||
* tickets (QA failures first), tickets waiting for QA and upcoming releases.
|
||||
* tickets (QA failures first), tickets waiting for QA, the open ones I follow
|
||||
* and upcoming releases.
|
||||
* Reloads when the tab becomes visible again.
|
||||
*/
|
||||
@Component({
|
||||
@@ -147,6 +148,39 @@ const VISIBLE = 12;
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="ts-panel ts-work__panel"
|
||||
aria-labelledby="following-title"
|
||||
>
|
||||
<header class="ts-work__head">
|
||||
<h2 id="following-title">Que sigo</h2>
|
||||
<span class="ts-muted ts-mono">{{ home.followingTotal() }}</span>
|
||||
</header>
|
||||
<ul class="ts-work__list">
|
||||
@for (issue of home.following().slice(0, 6); track issue.id) {
|
||||
<li>
|
||||
<a
|
||||
class="ts-work__row ts-work__row--qa"
|
||||
[routerLink]="['/p', keyOf(issue.key), 'issues', issue.key]"
|
||||
[style.--type]="statusTone(issue)"
|
||||
>
|
||||
<span class="ts-key">{{ issue.key }}</span>
|
||||
<span class="ts-work__title">{{ issue.title }}</span>
|
||||
@if (issue.assignee; as assignee) {
|
||||
<span class="ts-work__who" [attr.title]="assignee.name">{{
|
||||
initialsOf(assignee.name)
|
||||
}}</span>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
} @empty {
|
||||
<li class="ts-work__empty">
|
||||
No sigues ningún ticket abierto. En la ficha de uno, «Seguir».
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="ts-panel ts-work__panel"
|
||||
aria-labelledby="releases-title"
|
||||
|
||||
Reference in New Issue
Block a user