feat: portfolio of all my projects
/cartera (and GraphQL portfolio, REST /api/me/portfolio, MCP get_portfolio) shows every active project I belong to (not archived, not as a guest) at a glance: open, overdue, blocked and stuck tickets, the sprint under way and whether it gets there, planned releases with their forecast, and the open epics with their progress. It reuses the dashboard's computations. A release with no tickets now reads «Sin tickets» instead of «Todo hecho», here and on the dashboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export * from './lib/facades/epics.facade';
|
||||
export * from './lib/facades/insights.facade';
|
||||
export * from './lib/facades/my-views.facade';
|
||||
export * from './lib/facades/presence.facade';
|
||||
export * from './lib/facades/portfolio.facade';
|
||||
export * from './lib/facades/backlog.facade';
|
||||
export * from './lib/facades/list.facade';
|
||||
export * from './lib/facades/propose.facade';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type } from '@ngrx/signals';
|
||||
import { eventGroup } from '@ngrx/signals/events';
|
||||
import { PortfolioProject } from '@task-stream/web/tracker/domain';
|
||||
|
||||
export const portfolioPageEvents = eventGroup({
|
||||
source: 'Portfolio Page',
|
||||
events: {
|
||||
opened: type<void>(),
|
||||
},
|
||||
});
|
||||
|
||||
export const portfolioApiEvents = eventGroup({
|
||||
source: 'Portfolio API',
|
||||
events: {
|
||||
loaded: type<PortfolioProject[]>(),
|
||||
loadFailed: type<string>(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { inject, Injectable, Provider } from '@angular/core';
|
||||
import { injectDispatch } from '@ngrx/signals/events';
|
||||
import { portfolioPageEvents } from '../events/portfolio.events';
|
||||
import { PortfolioStore } from '../stores/portfolio.store';
|
||||
|
||||
/** «Cartera»: every active project of mine at a glance. */
|
||||
@Injectable()
|
||||
export class PortfolioFacade {
|
||||
private readonly store = inject(PortfolioStore);
|
||||
private readonly dispatch = injectDispatch(portfolioPageEvents);
|
||||
|
||||
readonly projects = this.store.projects;
|
||||
/** False until the first load: show skeletons, not empty states. */
|
||||
readonly loaded = this.store.loaded;
|
||||
readonly loading = this.store.loading;
|
||||
readonly error = this.store.error;
|
||||
|
||||
/** Loads it (again, to refresh). */
|
||||
load(): void {
|
||||
this.dispatch.opened();
|
||||
}
|
||||
}
|
||||
|
||||
/** Store + facade of the portfolio page. */
|
||||
export function providePortfolioFacade(): Provider[] {
|
||||
return [PortfolioStore, PortfolioFacade];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { mapResponse } from '@ngrx/operators';
|
||||
import { signalStore, withState } from '@ngrx/signals';
|
||||
import {
|
||||
Events,
|
||||
on,
|
||||
withEventHandlers,
|
||||
withReducer,
|
||||
} from '@ngrx/signals/events';
|
||||
import {
|
||||
PortfolioProject,
|
||||
ProjectsGateway,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { switchMap } from 'rxjs';
|
||||
import {
|
||||
portfolioApiEvents,
|
||||
portfolioPageEvents,
|
||||
} from '../events/portfolio.events';
|
||||
import { describeError } from '../notifications';
|
||||
|
||||
interface PortfolioState {
|
||||
projects: PortfolioProject[];
|
||||
loaded: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** All my active projects at a glance. Scoped to the portfolio page. */
|
||||
export const PortfolioStore = signalStore(
|
||||
withState<PortfolioState>({
|
||||
projects: [],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
withReducer(
|
||||
on(portfolioPageEvents.opened, () => ({ loading: true, error: null })),
|
||||
on(portfolioApiEvents.loaded, ({ payload }) => ({
|
||||
projects: payload,
|
||||
loaded: true,
|
||||
loading: false,
|
||||
})),
|
||||
on(portfolioApiEvents.loadFailed, ({ payload }) => ({
|
||||
loaded: true,
|
||||
loading: false,
|
||||
error: payload,
|
||||
})),
|
||||
),
|
||||
withEventHandlers(
|
||||
(_store, events = inject(Events), projects = inject(ProjectsGateway)) => ({
|
||||
load$: events.on(portfolioPageEvents.opened).pipe(
|
||||
switchMap(() =>
|
||||
projects.portfolio().pipe(
|
||||
mapResponse({
|
||||
next: (list) => portfolioApiEvents.loaded(list),
|
||||
error: (error) =>
|
||||
portfolioApiEvents.loadFailed(describeError(error)),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user