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:
2026-10-14 22:48:33 +02:00
parent 993f80b4b9
commit f45069a327
38 changed files with 1417 additions and 54 deletions

View File

@@ -0,0 +1,48 @@
import { inject, Injectable } from '@angular/core';
import { API_CONFIG } from '@task-stream/web/shared/infrastructure';
import { CalendarFeed, CalendarGateway } from '@task-stream/web/tracker/domain';
import { map, Observable } from 'rxjs';
import {
CreateCalendarFeedDocument,
MyCalendarFeedDocument,
RevokeCalendarFeedDocument,
} from '../graphql/generated';
import { GraphqlOperations } from './graphql-operations';
@Injectable()
export class GraphqlCalendarGateway extends CalendarGateway {
private readonly gql = inject(GraphqlOperations);
private readonly api = inject(API_CONFIG);
mine(): Observable<CalendarFeed | null> {
return this.gql
.query(MyCalendarFeedDocument)
.pipe(
map((d) => (d.myCalendarFeed ? this.toFeed(d.myCalendarFeed) : null)),
);
}
create(): Observable<CalendarFeed> {
return this.gql
.mutate(CreateCalendarFeedDocument, {})
.pipe(map((d) => this.toFeed(d.createCalendarFeed)));
}
revoke(): Observable<void> {
return this.gql
.mutate(RevokeCalendarFeedDocument, {})
.pipe(map(() => undefined));
}
/** The API answers the secret; calendar apps need the whole address. */
private toFeed(feed: { token: string; createdAt: string }): CalendarFeed {
const base = new URL(
this.api.restUrl.replace(/\/+$/, '') + '/',
location.origin,
);
return {
url: new URL(`calendar/${feed.token}.ics`, base).toString(),
createdAt: feed.createdAt,
};
}
}