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,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
query MyCalendarFeed {
|
||||
myCalendarFeed {
|
||||
token
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
|
||||
mutation CreateCalendarFeed {
|
||||
createCalendarFeed {
|
||||
token
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
|
||||
mutation RevokeCalendarFeed {
|
||||
revokeCalendarFeed
|
||||
}
|
||||
@@ -438,6 +438,26 @@ export type DeleteAutomationMutationVariables = Exact<{
|
||||
|
||||
export type DeleteAutomationMutation = { deleteAutomation: boolean };
|
||||
|
||||
export type MyCalendarFeedQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type MyCalendarFeedQuery = {
|
||||
myCalendarFeed: { token: string; createdAt: string } | null;
|
||||
};
|
||||
|
||||
export type CreateCalendarFeedMutationVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type CreateCalendarFeedMutation = {
|
||||
createCalendarFeed: { token: string; createdAt: string };
|
||||
};
|
||||
|
||||
export type RevokeCalendarFeedMutationVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type RevokeCalendarFeedMutation = { revokeCalendarFeed: boolean };
|
||||
|
||||
export type UserRefFieldsFragment = { id: string; name: string };
|
||||
|
||||
export type ProjectFieldsFragment = {
|
||||
@@ -3796,6 +3816,83 @@ export const DeleteAutomationDocument = {
|
||||
DeleteAutomationMutation,
|
||||
DeleteAutomationMutationVariables
|
||||
>;
|
||||
export const MyCalendarFeedDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
name: { kind: 'Name', value: 'MyCalendarFeed' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'myCalendarFeed' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<MyCalendarFeedQuery, MyCalendarFeedQueryVariables>;
|
||||
export const CreateCalendarFeedDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'CreateCalendarFeed' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'createCalendarFeed' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'token' } },
|
||||
{ kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
CreateCalendarFeedMutation,
|
||||
CreateCalendarFeedMutationVariables
|
||||
>;
|
||||
export const RevokeCalendarFeedDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
{
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'mutation',
|
||||
name: { kind: 'Name', value: 'RevokeCalendarFeed' },
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'revokeCalendarFeed' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
RevokeCalendarFeedMutation,
|
||||
RevokeCalendarFeedMutationVariables
|
||||
>;
|
||||
export const GitIntegrationDocument = {
|
||||
kind: 'Document',
|
||||
definitions: [
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CommentsGateway,
|
||||
IssuesGateway,
|
||||
NotificationsGateway,
|
||||
CalendarGateway,
|
||||
ProjectsGateway,
|
||||
ReleasesGateway,
|
||||
SavedFiltersGateway,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
} from './adapters/graphql-collaboration.gateways';
|
||||
import { GraphqlIssuesGateway } from './adapters/graphql-issues.gateway';
|
||||
import { GraphqlNotificationsGateway } from './adapters/graphql-notifications.gateway';
|
||||
import { GraphqlCalendarGateway } from './adapters/graphql-calendar.gateway';
|
||||
import { HttpAttachmentsGateway } from './adapters/http-attachments.gateway';
|
||||
import { HttpProposalsGateway } from './adapters/http-proposals.gateway';
|
||||
import { GraphqlProjectsGateway } from './adapters/graphql-projects.gateway';
|
||||
@@ -51,6 +53,7 @@ export function provideTrackerInfrastructure(): EnvironmentProviders {
|
||||
{ provide: ProposalsGateway, useClass: HttpProposalsGateway },
|
||||
{ provide: UsersGateway, useClass: GraphqlUsersGateway },
|
||||
{ provide: NotificationsGateway, useClass: GraphqlNotificationsGateway },
|
||||
{ provide: CalendarGateway, useClass: GraphqlCalendarGateway },
|
||||
{ provide: TrackerRealtimeGateway, useClass: GraphqlRealtimeGateway },
|
||||
{ provide: SavedFiltersGateway, useClass: GraphqlSavedFiltersGateway },
|
||||
{ provide: SprintsGateway, useClass: GraphqlSprintsGateway },
|
||||
|
||||
Reference in New Issue
Block a user