import { inject, Injectable, Provider } from '@angular/core'; import { injectDispatch } from '@ngrx/signals/events'; import { CreateReleaseInput, ProjectRole, ProjectSettings, } from '@task-stream/web/tracker/domain'; import { projectPageEvents } from '../events/project.events'; import { ProjectStore } from '../stores/project.store'; /** * The open project for presenters: details, members, releases, stats and live * activity. Commands act on the open project, so pages don't pass its id. */ @Injectable() export class ProjectFacade { private readonly store = inject(ProjectStore); private readonly dispatch = injectDispatch(projectPageEvents); readonly project = this.store.project; readonly members = this.store.members; readonly stats = this.store.stats; readonly releases = this.store.releases; readonly activity = this.store.activity; /** Pireon directory results for `searchUsers()`. */ readonly userResults = this.store.userResults; readonly loading = this.store.loading; readonly error = this.store.error; readonly myRole = this.store.myRole; /** Owner or admin. */ readonly canManage = this.store.canManage; /** Any role but viewer. */ readonly canContribute = this.store.canContribute; /** Statuses, ticket types and areas of this project (names, colours, lookups). */ readonly workflow = this.store.workflow; readonly savingSettings = this.store.savingSettings; /** Statistics of the last `analyticsDays()` days (see `loadAnalytics`). */ readonly analytics = this.store.analytics; readonly analyticsDays = this.store.analyticsDays; readonly analyticsLoading = this.store.analyticsLoading; readonly analyticsError = this.store.analyticsError; /** Burnup of the release asked for with `loadBurnup`. */ readonly burnup = this.store.burnup; /** Loads the project (by id or key) and follows its changes. */ open(idOrKey: string): void { this.dispatch.opened({ idOrKey }); } /** Searches Pireon's directory (from 2 characters, debounced). */ searchUsers(term: string): void { const query = term.trim(); if (query.length >= 2) this.dispatch.userSearchRequested(query); } addMember(userId: string, role: ProjectRole = ProjectRole.MEMBER): void { const projectId = this.projectId(); if (projectId) this.dispatch.memberAddRequested({ projectId, userId, role }); } changeMemberRole(userId: string, role: ProjectRole): void { const projectId = this.projectId(); if (projectId) this.dispatch.memberRoleChangeRequested({ projectId, userId, role }); } removeMember(userId: string): void { const projectId = this.projectId(); if (projectId) this.dispatch.memberRemoveRequested({ projectId, userId }); } createRelease(release: Omit): void { const projectId = this.projectId(); if (projectId) this.dispatch.releaseCreateRequested({ projectId, ...release }); } /** `force` publishes even with unfinished tickets. */ publishRelease(releaseId: string, force = false): void { this.dispatch.releasePublishRequested({ releaseId, force }); } archiveRelease(releaseId: string): void { this.dispatch.releaseArchiveRequested({ releaseId }); } /** Replaces statuses, transitions, types and areas (owners and admins). */ saveSettings(settings: ProjectSettings): void { const projectId = this.projectId(); if (projectId) this.dispatch.settingsSaveRequested({ projectId, settings }); } loadAnalytics(days = this.store.analyticsDays()): void { const projectId = this.projectId(); if (projectId) this.dispatch.analyticsRequested({ projectId, days }); } loadBurnup(releaseId: string): void { this.dispatch.burnupRequested({ releaseId }); } private projectId(): string | null { return this.store.project()?.id ?? null; } } /** Store + facade of the open project, for the project route's component. */ export function provideProjectFacade(): Provider[] { return [ProjectStore, ProjectFacade]; }