A person saves named views of «my work»: projects, assigned to me, that I follow, status categories, priorities, bugs only, overdue and text. They are private (Mongo my_views, no history), listed and deleted by their owner (GraphQL myViews / saveMyView / deleteMyView, REST /api/me/views), and run as the cross-project search, which gains project, priority, bugsOnly and overdue filters (also in MCP my_issues). The home page shows them as a bar above «my work», with a drawer to save a new one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
|
import {
|
|
DEFAULT_PROJECT_SETTINGS,
|
|
IssueReadModel,
|
|
StatusCategory,
|
|
} from '@task-stream/api/tracker/domain';
|
|
import { FindIssuesRepository, Page } from '../ports/find-issues.repository';
|
|
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
|
import { SearchMyIssuesQuery } from './search-my-issues.query';
|
|
|
|
@QueryHandler(SearchMyIssuesQuery)
|
|
export class SearchMyIssuesQueryHandler
|
|
implements IQueryHandler<SearchMyIssuesQuery, Page<IssueReadModel>>
|
|
{
|
|
constructor(
|
|
private readonly findProjects: FindProjectsRepository,
|
|
private readonly findIssues: FindIssuesRepository,
|
|
) {}
|
|
|
|
/** Membership is the access rule: only projects the viewer belongs to. */
|
|
async execute(query: SearchMyIssuesQuery): Promise<Page<IssueReadModel>> {
|
|
const { filter } = query;
|
|
const projects = (
|
|
await this.findProjects.findByMember(query.viewerId)
|
|
).filter(
|
|
(p) => !filter.projectIds?.length || filter.projectIds.includes(p.id),
|
|
);
|
|
// Bugs are each project's types drawn with the bug icon.
|
|
const bugTypes = filter.bugsOnly
|
|
? Object.fromEntries(
|
|
projects.map((p) => [
|
|
p.id,
|
|
(p.settings ?? DEFAULT_PROJECT_SETTINGS).issueTypes
|
|
.filter((t) => t.icon === 'bug')
|
|
.map((t) => t.id),
|
|
]),
|
|
)
|
|
: undefined;
|
|
const open = [
|
|
StatusCategory.TODO,
|
|
StatusCategory.IN_PROGRESS,
|
|
StatusCategory.QA,
|
|
];
|
|
return this.findIssues.searchAcross({
|
|
projectIds: projects.map((project) => project.id),
|
|
priorities: filter.priorities?.length ? filter.priorities : undefined,
|
|
typesByProject: bugTypes,
|
|
dueBefore: filter.overdue
|
|
? new Date().toISOString().slice(0, 10)
|
|
: undefined,
|
|
assigneeId: query.filter.assignedToMe ? query.viewerId : undefined,
|
|
watcherId: query.filter.watching ? query.viewerId : undefined,
|
|
statuses: query.filter.statuses,
|
|
categories: filter.overdue
|
|
? (filter.categories?.length ? filter.categories : open).filter((c) =>
|
|
open.includes(c),
|
|
)
|
|
: query.filter.categories,
|
|
search: query.filter.search?.trim().slice(0, 200) || undefined,
|
|
limit: Math.min(query.filter.limit, 200),
|
|
offset: query.filter.offset,
|
|
});
|
|
}
|
|
}
|