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:
62
libs/api/tracker/application/src/lib/calendar/ics.spec.ts
Normal file
62
libs/api/tracker/application/src/lib/calendar/ics.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { buildIcs } from './ics';
|
||||
|
||||
const NOW = new Date('2026-10-14T09:30:00.000Z');
|
||||
|
||||
describe('buildIcs', () => {
|
||||
it('writes all-day events with an exclusive end', () => {
|
||||
const ics = buildIcs(
|
||||
'TaskStream',
|
||||
[
|
||||
{
|
||||
uid: 'issue-1@taskstream',
|
||||
start: '2026-10-20',
|
||||
summary: 'WEB-1 · Login',
|
||||
},
|
||||
{
|
||||
uid: 'sprint-1@taskstream',
|
||||
start: '2026-10-05',
|
||||
end: '2026-10-19',
|
||||
summary: 'Sprint 4',
|
||||
},
|
||||
],
|
||||
NOW,
|
||||
);
|
||||
expect(ics.startsWith('BEGIN:VCALENDAR\r\nVERSION:2.0\r\n')).toBe(true);
|
||||
expect(ics.endsWith('END:VCALENDAR\r\n')).toBe(true);
|
||||
expect(ics).toContain('DTSTAMP:20261014T093000Z');
|
||||
expect(ics).toContain(
|
||||
'DTSTART;VALUE=DATE:20261020\r\nDTEND;VALUE=DATE:20261021',
|
||||
);
|
||||
expect(ics).toContain(
|
||||
'DTSTART;VALUE=DATE:20261005\r\nDTEND;VALUE=DATE:20261020',
|
||||
);
|
||||
expect(ics.split('BEGIN:VEVENT')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('escapes text and skips what has no date', () => {
|
||||
const ics = buildIcs(
|
||||
'Ana; López, QA',
|
||||
[
|
||||
{ uid: 'a', start: '2026-10-20', summary: 'Pago; roto, en\nSafari' },
|
||||
{ uid: 'b', start: '', summary: 'sin fecha' },
|
||||
],
|
||||
NOW,
|
||||
);
|
||||
expect(ics).toContain('X-WR-CALNAME:Ana\\; López\\, QA');
|
||||
expect(ics).toContain('SUMMARY:Pago\\; roto\\, en\\nSafari');
|
||||
expect(ics).not.toContain('sin fecha');
|
||||
});
|
||||
|
||||
it('folds long lines at 75 octets without splitting a character', () => {
|
||||
const summary = 'Revisión de la exportación á'.repeat(5);
|
||||
const ics = buildIcs(
|
||||
'x',
|
||||
[{ uid: 'u', start: '2026-10-20', summary }],
|
||||
NOW,
|
||||
);
|
||||
for (const line of ics.split('\r\n'))
|
||||
expect(Buffer.byteLength(line, 'utf8')).toBeLessThanOrEqual(75);
|
||||
// Unfolding gives the text back.
|
||||
expect(ics.replace(/\r\n /g, '')).toContain(`SUMMARY:${summary}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user