feat: saved views of my work, across my projects
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>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { IssueReadModel } from '@task-stream/api/tracker/domain';
|
||||
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';
|
||||
@@ -15,13 +19,43 @@ export class SearchMyIssuesQueryHandler
|
||||
|
||||
/** Membership is the access rule: only projects the viewer belongs to. */
|
||||
async execute(query: SearchMyIssuesQuery): Promise<Page<IssueReadModel>> {
|
||||
const projects = await this.findProjects.findByMember(query.viewerId);
|
||||
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: query.filter.categories,
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Query } from '@nestjs/cqrs';
|
||||
import {
|
||||
IssuePriority,
|
||||
IssueReadModel,
|
||||
StatusCategory,
|
||||
} from '@task-stream/api/tracker/domain';
|
||||
@@ -15,6 +16,13 @@ export interface MyIssuesFilter {
|
||||
categories?: StatusCategory[];
|
||||
/** Matches key, title or description. */
|
||||
search?: string;
|
||||
/** Only these of my projects. */
|
||||
projectIds?: string[];
|
||||
priorities?: IssuePriority[];
|
||||
/** Only tickets of a type drawn with the bug icon (in each project). */
|
||||
bugsOnly?: boolean;
|
||||
/** Only open tickets past their due date. */
|
||||
overdue?: boolean;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user