feat: a calendar to subscribe to, and the backlog's corners
- Each person can subscribe their dates in Google Calendar, Outlook or
Apple Calendar: GET /api/calendar/{token}.ics (no session) brings
their unfinished tickets with a due date (assigned or followed), the
planned and running sprints and the planned releases of their
projects, as all-day events linking to the web. The token is made,
renewed or dropped under /me/calendar (and in GraphQL); in the web,
from «Calendario» in «Mi trabajo».
- The backlog panel keeps the selected rows' background inside its
rounded corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Header,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
ApiNoContentResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiProduces,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { User } from '@task-stream/api/identity/domain';
|
||||
import {
|
||||
ApiPireonAuth,
|
||||
CurrentUser,
|
||||
Public,
|
||||
} from '@task-stream/api/identity/presenters';
|
||||
import { CalendarService } from '@task-stream/api/tracker/application';
|
||||
import type { Response } from 'express';
|
||||
import { CalendarFeedDto } from './dto/responses.dto';
|
||||
|
||||
/**
|
||||
* A person's calendar as iCalendar, for Google Calendar, Outlook or Apple
|
||||
* Calendar to subscribe to. The URL carries a secret token instead of a
|
||||
* session (calendar apps cannot sign in); the person makes, renews or
|
||||
* drops it under /me/calendar.
|
||||
*/
|
||||
@ApiTags('Users')
|
||||
@Controller()
|
||||
export class CalendarController {
|
||||
constructor(
|
||||
private readonly calendar: CalendarService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('calendar/:file')
|
||||
@Public()
|
||||
@Header('Content-Type', 'text/calendar; charset=utf-8')
|
||||
@Header('Cache-Control', 'private, max-age=300')
|
||||
@ApiProduces('text/calendar')
|
||||
@ApiOperation({
|
||||
summary: 'A calendar to subscribe to (`{token}.ics`, no session)',
|
||||
description:
|
||||
'Unfinished tickets with a due date (assigned or followed), planned and running sprints, and planned releases with a date, as all-day events.',
|
||||
})
|
||||
async ics(@Param('file') file: string): Promise<string> {
|
||||
const token = /^([\w-]{20,100})\.ics$/.exec(file)?.[1];
|
||||
const body = token
|
||||
? await this.calendar.ics(token, this.config.get<string>('WEB_URL') ?? '')
|
||||
: null;
|
||||
if (body === null) throw new NotFoundException('Unknown calendar');
|
||||
return body;
|
||||
}
|
||||
|
||||
@Get('me/calendar')
|
||||
@ApiPireonAuth()
|
||||
@ApiOperation({ summary: 'My calendar URL, if I made one' })
|
||||
@ApiOkResponse({ type: CalendarFeedDto })
|
||||
@ApiNoContentResponse({ description: 'I have none' })
|
||||
async mine(
|
||||
@CurrentUser() me: User,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
const feed = await this.calendar.mine(me.id);
|
||||
if (!feed) response.status(204);
|
||||
return feed ?? undefined;
|
||||
}
|
||||
|
||||
@Post('me/calendar')
|
||||
@ApiPireonAuth()
|
||||
@ApiOperation({
|
||||
summary: 'A new secret calendar URL (the previous one stops working)',
|
||||
})
|
||||
@ApiOkResponse({ type: CalendarFeedDto })
|
||||
@HttpCode(200)
|
||||
create(@CurrentUser() me: User) {
|
||||
return this.calendar.create(me.id);
|
||||
}
|
||||
|
||||
@Delete('me/calendar')
|
||||
@ApiPireonAuth()
|
||||
@ApiOperation({ summary: 'My calendar URL stops working' })
|
||||
@ApiNoContentResponse()
|
||||
@HttpCode(204)
|
||||
async revoke(@CurrentUser() me: User): Promise<void> {
|
||||
await this.calendar.revoke(me.id);
|
||||
}
|
||||
}
|
||||
@@ -831,6 +831,15 @@ export class MyNotificationsDto {
|
||||
unread!: number;
|
||||
}
|
||||
|
||||
export class CalendarFeedDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Secret of the calendar URL: GET /api/calendar/{token}.ics (iCalendar, no session)',
|
||||
})
|
||||
token!: string;
|
||||
@ApiProperty() createdAt!: string;
|
||||
}
|
||||
|
||||
export class NotificationSettingsResponseDto {
|
||||
@ApiProperty({ description: 'Copies by e-mail' }) email!: boolean;
|
||||
@ApiProperty({ description: 'Whether this installation can send e-mail' })
|
||||
|
||||
Reference in New Issue
Block a user