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:
173
libs/web/tracker/presenters/src/lib/ui/calendar-feed.ts
Normal file
173
libs/web/tracker/presenters/src/lib/ui/calendar-feed.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { PiActionButtonComponent, PiNoteComponent } from '@pi/ui';
|
||||
import { ToastService } from '@task-stream/web/shared/ui';
|
||||
import { CalendarFacade } from '@task-stream/web/tracker/application';
|
||||
|
||||
/**
|
||||
* My calendar URL: what to paste into Google Calendar, Outlook or Apple
|
||||
* Calendar to see my due dates, sprints and releases there. Renewing it
|
||||
* cuts off whoever had the old one.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ts-calendar-feed',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [PiActionButtonComponent, PiNoteComponent],
|
||||
template: `
|
||||
<div class="ts-cal">
|
||||
<p class="ts-muted">
|
||||
Tus tickets sin terminar con fecha límite (los asignados y los que
|
||||
sigues), los sprints en marcha o planificados y las releases con fecha
|
||||
de tus proyectos, en tu calendario. Se actualiza solo, normalmente cada
|
||||
pocas horas.
|
||||
</p>
|
||||
@if (calendar.error(); as error) {
|
||||
<pi-note
|
||||
gap="xs"
|
||||
status="error"
|
||||
appearance="fill"
|
||||
type="status"
|
||||
title="Calendario"
|
||||
[text]="error"
|
||||
/>
|
||||
}
|
||||
@if (!calendar.loaded()) {
|
||||
<p class="ts-muted">Cargando…</p>
|
||||
} @else if (calendar.feed(); as feed) {
|
||||
<label class="ts-cal__label" for="ts-cal-url"
|
||||
>Dirección del calendario</label
|
||||
>
|
||||
<div class="ts-cal__url">
|
||||
<input
|
||||
id="ts-cal-url"
|
||||
readonly
|
||||
[value]="feed.url"
|
||||
(focus)="$any($event.target).select()"
|
||||
/>
|
||||
<pi-action-button
|
||||
size="small"
|
||||
color="primary"
|
||||
text="Copiar"
|
||||
(execute)="copy(feed.url)"
|
||||
/>
|
||||
</div>
|
||||
<p class="ts-cal__warn">
|
||||
Es privada: quien la tenga ve estas fechas y títulos. Si se ha
|
||||
compartido de más, genera otra.
|
||||
</p>
|
||||
<details class="ts-cal__how">
|
||||
<summary>Cómo añadirla</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Google Calendar</strong>: Otros calendarios → «+» → Desde
|
||||
URL.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Outlook</strong>: Agregar calendario → Suscribirse desde
|
||||
la Web.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Apple Calendar</strong>: Archivo → Nueva suscripción a
|
||||
calendario.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<div class="ts-cal__actions">
|
||||
<pi-action-button
|
||||
size="small"
|
||||
appearance="outline"
|
||||
color="grey"
|
||||
text="Generar otra"
|
||||
[disabled]="calendar.busy()"
|
||||
(execute)="calendar.create()"
|
||||
/>
|
||||
<pi-action-button
|
||||
size="small"
|
||||
appearance="ghost"
|
||||
color="grey"
|
||||
text="Desactivar"
|
||||
[disabled]="calendar.busy()"
|
||||
(execute)="calendar.revoke()"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<pi-action-button
|
||||
size="w100"
|
||||
color="primary"
|
||||
text="Crear la dirección del calendario"
|
||||
[disabled]="calendar.busy()"
|
||||
(execute)="calendar.create()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.ts-cal {
|
||||
display: grid;
|
||||
gap: 0.875rem;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.ts-cal__label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ts-muted);
|
||||
}
|
||||
.ts-cal__url {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-top: -0.5rem;
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border: 1px solid var(--ts-line-strong, var(--ts-line));
|
||||
border-radius: 0.5rem;
|
||||
background: var(--ts-surface, transparent);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-family: var(--ts-mono, ui-monospace, monospace);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
.ts-cal__warn {
|
||||
color: var(--ts-muted);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.ts-cal__how {
|
||||
font-size: 0.875rem;
|
||||
summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
ul {
|
||||
margin: 0.5rem 0 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
}
|
||||
.ts-cal__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class CalendarFeedComponent implements OnInit {
|
||||
protected readonly calendar = inject(CalendarFacade);
|
||||
private readonly toast = inject(ToastService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.calendar.load();
|
||||
}
|
||||
|
||||
protected copy(url: string): void {
|
||||
navigator.clipboard
|
||||
?.writeText(url)
|
||||
.then(() => this.toast.success('Dirección copiada'))
|
||||
.catch(() => this.toast.error('No se pudo copiar'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user