feat: global search and keyboard shortcuts
Ctrl/⌘ K (or /) opens a search over the projects and tickets of all my projects, driven by the keyboard. g plus a letter moves between project pages, c creates a ticket on the board, e edits the open ticket and ? lists every shortcut. My work can now be searched by text through the API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ export interface CrossProjectIssueFilter {
|
||||
assigneeId?: string;
|
||||
statuses?: string[];
|
||||
categories?: StatusCategory[];
|
||||
search?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export class SearchMyIssuesQueryHandler
|
||||
assigneeId: query.filter.assignedToMe ? query.viewerId : undefined,
|
||||
statuses: query.filter.statuses,
|
||||
categories: query.filter.categories,
|
||||
search: query.filter.search?.trim().slice(0, 200) || undefined,
|
||||
limit: Math.min(query.filter.limit, 200),
|
||||
offset: query.filter.offset,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface MyIssuesFilter {
|
||||
statuses?: string[];
|
||||
/** e.g. TODO + IN_PROGRESS + QA = still open, whatever the project calls them. */
|
||||
categories?: StatusCategory[];
|
||||
/** Matches key, title or description. */
|
||||
search?: string;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
@@ -161,6 +161,14 @@ export class MongoFindIssuesRepository extends FindIssuesRepository {
|
||||
if (filter.statuses?.length) query['status'] = { $in: filter.statuses };
|
||||
if (filter.categories?.length)
|
||||
query['statusCategory'] = { $in: filter.categories };
|
||||
if (filter.search?.trim()) {
|
||||
const pattern = new RegExp(escapeRegex(filter.search.trim()), 'i');
|
||||
query['$or'] = [
|
||||
{ key: pattern },
|
||||
{ title: pattern },
|
||||
{ description: pattern },
|
||||
];
|
||||
}
|
||||
const [docs, total] = await Promise.all([
|
||||
this.issues
|
||||
.find(query)
|
||||
|
||||
@@ -104,11 +104,18 @@ export class IssueResolver {
|
||||
categories: StatusCategory[] | undefined,
|
||||
@Args('limit', { type: () => Int, defaultValue: 50 }) limit: number,
|
||||
@Args('offset', { type: () => Int, defaultValue: 0 }) offset: number,
|
||||
@Args('search', {
|
||||
type: () => String,
|
||||
nullable: true,
|
||||
description: 'Matches key, title or description (global search)',
|
||||
})
|
||||
search?: string | null,
|
||||
) {
|
||||
return this.issuesService.mine(me.id, {
|
||||
assignedToMe,
|
||||
statuses: statuses ?? undefined,
|
||||
categories: categories ?? undefined,
|
||||
search: search ?? undefined,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
|
||||
@@ -408,6 +408,12 @@ export class MyIssuesQueryDto extends PageQueryDto {
|
||||
@Transform(toList)
|
||||
@IsEnum(StatusCategory, { each: true })
|
||||
category?: StatusCategory[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Matches key, title or description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export class UpcomingReleasesQueryDto {
|
||||
|
||||
@@ -130,6 +130,7 @@ export class IssuesController {
|
||||
assignedToMe: query.assignedToMe,
|
||||
statuses: query.status,
|
||||
categories: query.category,
|
||||
search: query.search,
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user