feat: a sprint calendar
A Calendario tab lays the project's sprints out month by month as bands, with the releases and the tickets due on each day. Owners and admins plan a sprint by clicking the day it starts, and the whole plan downloads as an iCalendar file. Tickets can be searched by due-date range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,9 @@ export interface IssueFilter {
|
||||
/** Only the tickets hanging from this one. */
|
||||
parentId?: string;
|
||||
sprintId?: string;
|
||||
/** Due between these days (`YYYY-MM-DD`, both included). */
|
||||
dueFrom?: string;
|
||||
dueTo?: string;
|
||||
/** Only tickets in no sprint (wins over `sprintId`). */
|
||||
noSprint?: boolean;
|
||||
/** RECENT (default): last updated first. RANK: backlog order. */
|
||||
|
||||
@@ -413,6 +413,13 @@ function toMongoFilter(filter: IssueFilter): FilterQuery<IssueDoc> {
|
||||
query.labels = { $all: filter.labels.map((l) => l.toLowerCase()) };
|
||||
if (filter.releaseId) query['release.id'] = filter.releaseId;
|
||||
if (filter.parentId) query['parent.id'] = filter.parentId;
|
||||
if (filter.dueFrom || filter.dueTo)
|
||||
query['dueDate'] = {
|
||||
$ne: null,
|
||||
...(filter.dueFrom ? { $gte: filter.dueFrom } : {}),
|
||||
// Full ISO dates on the last day still count.
|
||||
...(filter.dueTo ? { $lte: `${filter.dueTo}\uffff` } : {}),
|
||||
};
|
||||
if (filter.noSprint) query['sprint'] = null;
|
||||
else if (filter.sprintId) query['sprint.id'] = filter.sprintId;
|
||||
if (filter.unassigned) query['assignee'] = null;
|
||||
|
||||
@@ -508,6 +508,20 @@ export class IssueFilterInput {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
noSprint?: boolean;
|
||||
@Field({
|
||||
nullable: true,
|
||||
description: 'Due on or after this day (YYYY-MM-DD)',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
dueFrom?: string;
|
||||
@Field({
|
||||
nullable: true,
|
||||
description: 'Due on or before this day (YYYY-MM-DD)',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
dueTo?: string;
|
||||
@Field(() => IssueSort, { nullable: true, defaultValue: IssueSort.RECENT })
|
||||
@IsOptional()
|
||||
@IsEnum(IssueSort)
|
||||
|
||||
@@ -509,6 +509,22 @@ export class IssueSearchQueryDto extends PageQueryDto {
|
||||
@IsBoolean()
|
||||
noSprint?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
format: 'date',
|
||||
description: 'Due on or after this day',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
dueFrom?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
format: 'date',
|
||||
description: 'Due on or before this day',
|
||||
})
|
||||
@IsOptional()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
dueTo?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: IssueSort,
|
||||
enumName: 'IssueSort',
|
||||
|
||||
@@ -164,6 +164,8 @@ export class IssuesController {
|
||||
labels: query.label,
|
||||
sprintId: query.sprintId,
|
||||
noSprint: query.noSprint,
|
||||
dueFrom: query.dueFrom,
|
||||
dueTo: query.dueTo,
|
||||
sort: query.sort,
|
||||
search: query.search,
|
||||
limit: query.limit,
|
||||
|
||||
Reference in New Issue
Block a user