import { IssuePriority, ReviewVerdict } from '../models/enums'; import { MyDay } from '../models/my-day'; import { Home } from '../models/home'; import { PortfolioProject, ProjectInsights } from '../models/insights'; import { DashboardShare, PublicReleaseNotes, ReleaseNotesShare, SharedBlock, SharedDashboard, } from '../models/sharing'; import { Observable } from 'rxjs'; import { Activity, Attachment, Automation, AutomationInput, BoardColumn, CalendarFeed, PushDeviceSubscription, CommandResult, AlertIntegration, Comment, Decision, Typing, Epic, GitIntegration, GitRepository, ImportReport, Issue, IssuePage, IssueSummary, Notification, NotificationSettings, Project, ProjectAnalytics, ProjectStats, Release, ReleaseBurnup, SavedFilter, Presence, MyView, MyViewCriteria, SavedFilterCriteria, Sprint, SprintBurndown, UserDirectoryEntry, Webhook, WebhookDelivery, } from '../models/models'; import { ProjectSettings } from '../models/workflow'; import { BoardFilter, ChangeStatusInput, CreateIssueInput, CreateProjectInput, CreateReleaseInput, LinkIssuesInput, MemberInput, IssueListFilter, ProposalInput, ProposalResult, MyIssuesFilter, UpdateIssueInput, UpdateProjectInput, CreateSprintInput, BulkChange, BulkResult, UpdateSprintInput, } from '../models/inputs'; /* * Outbound ports of the tracker. The application layer depends on these * abstract classes; the infrastructure layer implements them over GraphQL. */ export abstract class ProjectsGateway { abstract myProjects(): Observable; /** Projects anyone can read (no sign-in needed). */ abstract publicProjects(): Observable; abstract project(idOrKey: string): Observable; abstract stats(projectId: string): Observable; /** My active projects at a glance. */ abstract portfolio(): Observable; /** The home page beyond «my work». */ abstract myHome(): Observable; /** The dashboard; with `since`, also what changed after it. */ abstract insights( projectId: string, since: string | null, ): Observable; /** Archives the project (read-only) or brings it back. Owner only. */ abstract setArchived( projectId: string, archived: boolean, ): Observable; /** Statistics of the last `days` days. */ abstract analytics( projectId: string, days: number, ): Observable; /** Replaces statuses, transitions, types and areas (owners and admins). */ abstract updateSettings( projectId: string, settings: ProjectSettings, ): Observable; abstract create(input: CreateProjectInput): Observable; /** Name, description or visibility (owners and admins). */ abstract update(input: UpdateProjectInput): Observable; /** Owner only; `confirmKey` must be the project key. */ abstract delete( projectId: string, confirmKey: string, ): Observable; abstract addMember(input: MemberInput): Observable; abstract changeMemberRole(input: MemberInput): Observable; abstract removeMember( projectId: string, userId: string, ): Observable; } export abstract class IssuesGateway { abstract board(filter: BoardFilter): Observable; abstract issue(idOrKey: string): Observable; abstract search(projectId: string, text: string): Observable; /** A project's tickets for the list view (most recently updated first). */ abstract list(filter: IssueListFilter): Observable; /** Tickets across all my projects (most recently updated first). */ abstract mine(filter: MyIssuesFilter): Observable; abstract create(input: CreateIssueInput): Observable; /** Votes for it (`on`) or takes the vote back. */ abstract vote(issueId: string, on: boolean): Observable; /** A copy of what the ticket says; the result carries its key. */ abstract duplicate(issueId: string): Observable; abstract update(input: UpdateIssueInput): Observable; abstract changeStatus(input: ChangeStatusInput): Observable; abstract assign( issueId: string, assigneeId: string | null, ): Observable; abstract link(input: LinkIssuesInput): Observable; abstract unlink(input: LinkIssuesInput): Observable; abstract setRelease( issueId: string, releaseId: string | null, ): Observable; /** Asks a member to review the ticket. */ abstract requestReview( issueId: string, reviewerId: string, note: string | null, ): Observable; /** Answers a review I was asked for. */ abstract answerReview( issueId: string, reviewId: string, verdict: ReviewVerdict, answer: string | null, ): Observable; abstract cancelReview( issueId: string, reviewId: string, ): Observable; /** The signed-in person follows (or stops following) the ticket. */ abstract watch(issueId: string, watching: boolean): Observable; /** My pinned tickets, most recently pinned first. */ abstract pins(): Observable; /** Pins the ticket for me (or unpins it): private, at most 20. */ abstract pin(issueId: string, pinned: boolean): Observable; /** Follows the ticket with everything under it (or stops). */ abstract followTree(issueId: string, on: boolean): Observable; /** Reminds me of the ticket then (replaces the waiting one). */ abstract setReminder( issueId: string, at: string, note: string | null, ): Observable; abstract clearReminder(issueId: string): Observable; /** «Mi día» on `day` (today, `YYYY-MM-DD` in my time zone). */ abstract myDay(day: string): Observable; abstract addToMyDay(day: string, issueIds: string[]): Observable; abstract removeFromMyDay(day: string, issueId: string): Observable; /** * Decisions (comments marked as such) of all my projects by their text, * most recent first: here and not in CommentsGateway, for the palette. */ abstract searchDecisions( search: string, limit: number, ): Observable; /** Tickets hanging from `issueId`. */ abstract children( projectId: string, issueId: string, ): Observable; /** Places the ticket under `parentId` (null: back to the top). */ abstract setParent( issueId: string, parentId: string | null, ): Observable; abstract epics(projectId: string): Observable; /** Tickets due between two days (`YYYY-MM-DD`, both included). */ abstract due( projectId: string, from: string, to: string, ): Observable; /** To the trash (whoever opened it, or a manager). */ abstract trash(issueId: string): Observable; abstract restore(issueId: string): Observable; /** The project's tickets in the trash, newest first. */ abstract trashed(projectId: string): Observable; /** The same change on several tickets; each may refuse on its own. */ abstract bulkUpdate( issueIds: string[], change: BulkChange, ): Observable; /** Tickets still to start (TODO category), in backlog order. */ abstract backlog(projectId: string): Observable; /** Moves a ticket between two others of the backlog (null: an end). */ abstract rank( issueId: string, aboveIssueId: string | null, belowIssueId: string | null, ): Observable; abstract addChecklistItem( issueId: string, text: string, ): Observable; abstract updateChecklistItem( issueId: string, itemId: string, changes: { text?: string; done?: boolean }, ): Observable; abstract removeChecklistItem( issueId: string, itemId: string, ): Observable; } /** The project's automation rules (owners and admins). */ export abstract class AutomationsGateway { abstract list(projectId: string): Observable; abstract create( projectId: string, rule: AutomationInput, ): Observable; abstract update( automationId: string, changes: { name?: string; active?: boolean }, ): Observable; abstract remove(automationId: string): Observable; } /** Tickets in and out as CSV. */ export abstract class TransferGateway { abstract exportCsv(projectId: string): Observable; /** `dryRun`: only tell what would be created. */ abstract importCsv( projectId: string, file: File, dryRun: boolean, ): Observable; } /** The project's outgoing webhooks (owners and admins). */ export abstract class WebhooksGateway { abstract list(projectId: string): Observable; abstract events(): Observable; /** The secret comes back only here. */ abstract create( projectId: string, url: string, events: string[], ): Observable<{ id: string; secret: string }>; abstract update( webhookId: string, changes: { url?: string; events?: string[]; active?: boolean }, ): Observable; abstract remove(webhookId: string): Observable; abstract test(webhookId: string): Observable; } /** The project's Git webhook (owners and admins configure it). */ /** Alerts → bugs (owners and admins). */ export abstract class AlertsGateway { abstract integration(projectId: string): Observable; /** Turns it on, or changes what it opens; the URL stays. */ abstract save( projectId: string, settings: { typeId: string | null; priority: IssuePriority }, ): Observable; /** A new URL; the old one stops working. */ abstract rotateToken(projectId: string): Observable; abstract disable(projectId: string): Observable; } export abstract class GitGateway { abstract integration(projectId: string): Observable; /** A repository with its own secret (the first turns the integration on). */ abstract addRepository( projectId: string, name: string, ): Observable; abstract rotateSecret( projectId: string, repositoryId: string, ): Observable; abstract removeRepository( projectId: string, repositoryId: string, ): Observable; } /** Saved views of a project's board (mine and the shared ones). */ export abstract class SavedFiltersGateway { abstract list(projectId: string): Observable; abstract save(input: { projectId: string; name: string; shared: boolean; criteria: SavedFilterCriteria; }): Observable; abstract remove(id: string): Observable; /** My views of «my work», by name. */ abstract myViews(): Observable; abstract saveMyView(input: { name: string; criteria: MyViewCriteria; }): Observable; abstract removeMyView(id: string): Observable; } export abstract class CommentsGateway { abstract list(issueId: string): Observable; abstract add( issueId: string, body: string, replyTo?: string | null, /** A quick poll's options (2–6). */ poll?: string[] | null, ): Observable; /** Votes in a comment's poll (null takes the vote back). */ abstract votePoll( commentId: string, option: number | null, ): Observable; /** Closes a poll; with `decide`, its winner stays as a decision. */ abstract closePoll( commentId: string, decide: boolean, ): Observable; abstract edit(commentId: string, body: string): Observable; /** Adds (`on`) or takes back one's reaction. */ abstract react( commentId: string, emoji: string, on: boolean, ): Observable; abstract remove(commentId: string): Observable; /** * Settles a conversation (or reopens it); a conclusion is added as a * reply marked as a decision. */ abstract resolveThread( commentId: string, resolved: boolean, conclusion: string | null, ): Observable; /** Marks it as a decision of its ticket (`on`) or unmarks it. */ abstract markDecision( commentId: string, on: boolean, ): Observable; } /** A ticket's files. Uploads and downloads go through the REST API. */ /** The ticket's acknowledgement and the new file's id. */ export type AttachmentUploaded = CommandResult & { attachmentId: string }; /** The public form: no session needed. */ export abstract class ProposalsGateway { abstract submit( projectKey: string, proposal: ProposalInput, ): Observable; } export abstract class AttachmentsGateway { abstract upload( issueId: string, file: File, commentId?: string | null, ): Observable; /** The file's bytes, to show (images) or save. */ abstract download(issueId: string, attachment: Attachment): Observable; abstract remove( issueId: string, attachmentId: string, ): Observable; } export abstract class ReleasesGateway { abstract list(projectId: string): Observable; /** Planned releases of all my projects, soonest target date first. */ abstract upcoming(limit: number): Observable; abstract create(input: CreateReleaseInput): Observable; abstract publish( releaseId: string, force: boolean, ): Observable; /** Its unfinished tickets go to `moveTo` (an open release) or leave it. */ abstract archive( releaseId: string, moveTo?: string | null, ): Observable; abstract burnup(releaseId: string): Observable; /** A Markdown draft from the release's finished tickets (not saved). */ abstract notesDraft(releaseId: string): Observable; /** null removes them. */ abstract writeNotes( releaseId: string, notes: string | null, ): Observable; } export abstract class SprintsGateway { /** With their progress: under way, planned, then closed. */ abstract list(projectId: string): Observable; abstract burndown(sprintId: string): Observable; abstract create(input: CreateSprintInput): Observable; abstract update(input: UpdateSprintInput): Observable; abstract start(sprintId: string): Observable; /** Unfinished tickets go to `carryOverTo` (null: the backlog). */ abstract close( sprintId: string, carryOverTo: string | null, ): Observable; abstract remove(sprintId: string): Observable; abstract setIssueSprint( issueId: string, sprintId: string | null, ): Observable; } export abstract class ActivityGateway { abstract list(projectId: string, issueId?: string): Observable; /** What happened that day (`YYYY-MM-DD`, UTC), newest first (200 at most). */ abstract onDay(projectId: string, day: string): Observable; } export abstract class UsersGateway { abstract search(term: string): Observable; } /** The signed-in person's notifications. */ export abstract class CalendarGateway { /** Mine, if I made one. */ abstract mine(): Observable; /** A new secret URL; the previous one stops working. */ abstract create(): Observable; abstract revoke(): Observable; } /** The project's dashboard for clients, behind a secret link. */ export abstract class SharingGateway { /** The project's link, if it has one (owners and admins). */ abstract share(projectId: string): Observable; /** Turns it on or changes its blocks; `renew` makes a new secret. */ abstract save( projectId: string, blocks: SharedBlock[], renew: boolean, ): Observable; abstract remove(projectId: string): Observable; /** What a link shows, for anyone (null: no such link). */ abstract shared(token: string): Observable; /** A release's public notes link, if any (owners and admins). */ abstract releaseNotesShare( releaseId: string, ): Observable; /** Turns it on; `renew` makes a new link. */ abstract shareReleaseNotes( releaseId: string, renew: boolean, ): Observable; abstract unshareReleaseNotes(releaseId: string): Observable; /** A release's notes for anyone with the link (null: no such link). */ abstract publicReleaseNotes( token: string, ): Observable; } export abstract class NotificationsGateway { abstract mine( unreadOnly: boolean, limit: number, ): Observable<{ items: Notification[]; unread: number }>; /** Without ids: all of them. */ abstract markRead(ids?: string[]): Observable; abstract settings(): Observable; abstract updateSettings(email: boolean): Observable; /** Push on this device. */ abstract subscribePush( subscription: PushDeviceSubscription, ): Observable; /** Push off on this device. */ abstract unsubscribePush(endpoint: string): Observable; } /** * This browser's side of push: its service worker and push subscription. * Not every browser has it (and on iPhone only an installed web app does). */ export abstract class PushDevice { abstract get supported(): boolean; /** This browser's subscription, if push is on here. */ abstract current(): Promise; /** Asks for permission and subscribes; throws when refused. */ abstract subscribe(publicKey: string): Promise; /** Unsubscribes; the endpoint it had, if any. */ abstract unsubscribe(): Promise; } /** Realtime (GraphQL subscriptions over WebSocket). */ export interface IssueChanged { action: string; version: number; actorId: string; issue: IssueSummary; } export interface CommentChanged { action: string; commentId: string; comment: Comment | null; } /** A project changed, or was deleted (then `project` is its last state). */ export interface ProjectChanged { project: Project; deleted: boolean; } /** Realtime needs a session: without one, these streams stay silent. */ export abstract class TrackerRealtimeGateway { abstract issueChanges(projectId: string): Observable; abstract commentChanges(issueId: string): Observable; abstract activity(projectId: string): Observable; abstract releaseChanges(projectId: string): Observable; /** Something about a sprint of the project changed (or it was deleted). */ abstract sprintChanges(projectId: string): Observable<{ sprintId: string }>; abstract projectChanges(projectId: string): Observable; abstract myProjectsChanges(): Observable; abstract notifications(): Observable; /** * Each time the live connection comes back after a cut: what happened * meanwhile was not received, so views reload. */ abstract reconnects(): Observable; } /** Who is on a ticket's page: a heartbeat answers who else is. */ export abstract class PresenceGateway { abstract touch(issueId: string, editing: boolean): Observable; abstract leave(issueId: string): Observable; /** I am writing (an answer to `threadId`, or a new comment). */ abstract typing(issueId: string, threadId: string | null): Observable; /** Who is writing on the ticket, as they type (me included). */ abstract typists(issueId: string): Observable; }