refactor(web): page state with its page, the root keeps the shell's

Only what the header needs from the start stays at the root: the
session, my projects for the command palette, the inbox and push for
the bell, and their adapters. The home page's facades (my work, the
rest of the home, my views, the calendar), Explore's and the proposal
form's are provided by their routes, whose injectors keep them for
when you come back. The other adapters come with the tracker's routes,
now loaded lazily.

The web libs declare "sideEffects": false, so a chunk carries what it
uses and not everything an index.ts exports: main goes from 1.31 MB
to 432 kB of the same initial total, which is still held up by @pi/ui
(one module, with all of Material) and the GraphQL documents, both
needed by the header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 08:34:17 +02:00
parent d0efaeba80
commit f448207e88
22 changed files with 160 additions and 47 deletions

View File

@@ -16,7 +16,7 @@ import {
} from '@task-stream/web/shared/infrastructure'; } from '@task-stream/web/shared/infrastructure';
import { providePiUi, ToastService } from '@task-stream/web/shared/ui'; import { providePiUi, ToastService } from '@task-stream/web/shared/ui';
import { Notifications } from '@task-stream/web/tracker/application'; import { Notifications } from '@task-stream/web/tracker/application';
import { provideTrackerInfrastructure } from '@task-stream/web/tracker/infrastructure'; import { provideTrackerShellInfrastructure } from '@task-stream/web/tracker/infrastructure';
import { appRoutes } from './app.routes'; import { appRoutes } from './app.routes';
/* /*
@@ -39,9 +39,9 @@ export const appConfig: ApplicationConfig = {
provideApiConfig(apiConfigFromLocation()), provideApiConfig(apiConfigFromLocation()),
provideGraphql(), provideGraphql(),
// Ports → adapters // Ports → adapters: the shell's here; the pages' with their lazy routes (tracker.routes.ts).
provideIdentityInfrastructure(), provideIdentityInfrastructure(),
provideTrackerInfrastructure(), provideTrackerShellInfrastructure(),
{ provide: Notifications, useExisting: ToastService }, { provide: Notifications, useExisting: ToastService },
], ],
}; };

View File

@@ -1,15 +1,25 @@
import { Route } from '@angular/router';
import { authGuard } from '@task-stream/web/identity/presenters'; import { authGuard } from '@task-stream/web/identity/presenters';
import { appRoutes } from './app.routes'; import { appRoutes } from './app.routes';
/** The tracker's routes, behind the lazy `''` route (and its providers' parent route). */
async function trackerChildren(): Promise<Route[]> {
const lazy = appRoutes.find(
(route) => route.path === '' && route.loadChildren,
);
const loaded = (await lazy?.loadChildren?.()) as Route[];
expect(loaded[0].providers?.length).toBeGreaterThan(0);
return loaded[0].children ?? [];
}
describe('appRoutes', () => { describe('appRoutes', () => {
it('keeps home behind the auth guard, and public projects and «Explorar» open', () => { it('keeps home behind the auth guard, and public projects and «Explorar» open', async () => {
const paths = appRoutes.map((route) => route.path); const paths = appRoutes.map((route) => route.path);
expect(paths).toEqual( expect(paths).toEqual(
expect.arrayContaining(['login', 'auth/callback', '', '**']), expect.arrayContaining(['login', 'auth/callback', '', '**']),
); );
const app = appRoutes.find((route) => route.path === '' && route.children); const children = await trackerChildren();
const children = app?.children ?? [];
expect(children.map((route) => route.path)).toEqual([ expect(children.map((route) => route.path)).toEqual([
'', '',
'portfolio', 'portfolio',
@@ -27,10 +37,16 @@ describe('appRoutes', () => {
expect(children[4].redirectTo).toBe('explore'); expect(children[4].redirectTo).toBe('explore');
}); });
it('exposes every project section', () => { it('gives each page its own state, not the root', async () => {
const project = appRoutes const children = await trackerChildren();
.find((route) => route.path === '') expect(children[0].providers?.length).toBe(4);
?.children?.find((route) => route.path === 'p/:key'); expect(children[2].providers?.length).toBe(1);
});
it('exposes every project section', async () => {
const project = (await trackerChildren()).find(
(route) => route.path === 'p/:key',
);
expect(project?.children?.map((route) => route.path)).toEqual([ expect(project?.children?.map((route) => route.path)).toEqual([
'', '',
'proponer', 'proponer',

View File

@@ -1,10 +1,12 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { identityRoutes } from '@task-stream/web/identity/presenters'; import { identityRoutes } from '@task-stream/web/identity/presenters';
import { trackerRoutes } from '@task-stream/web/tracker/presenters';
export const appRoutes: Route[] = [ export const appRoutes: Route[] = [
...identityRoutes, ...identityRoutes,
// Not all behind the guard: public projects are readable without a session. // Not all behind the guard: public projects are readable without a session.
{ path: '', children: trackerRoutes }, {
path: '',
loadChildren: () => import('./tracker.routes').then((m) => m.routes),
},
{ path: '**', redirectTo: '' }, { path: '**', redirectTo: '' },
]; ];

View File

@@ -0,0 +1,15 @@
import { Route } from '@angular/router';
import { provideTrackerPagesInfrastructure } from '@task-stream/web/tracker/infrastructure';
import { trackerRoutes } from '@task-stream/web/tracker/presenters';
/**
* The tracker, loaded lazily: its pages, and the adapters only they use,
* come in this chunk instead of the one that boots the app.
*/
export const routes: Route[] = [
{
path: '',
providers: [provideTrackerPagesInfrastructure()],
children: trackerRoutes,
},
];

View File

@@ -59,8 +59,11 @@ TrackerPresentersModule.withApplication(
```ts ```ts
// apps/web/src/app/app.config.ts // apps/web/src/app/app.config.ts
provideTrackerInfrastructure(); // puertos → adaptadores GraphQL provideTrackerShellInfrastructure(); // puertos de la cabecera → adaptadores GraphQL
{ provide: Notifications, useExisting: ToastService } // puerto de la aplicación → toasts de @pi/ui { provide: Notifications, useExisting: ToastService } // puerto de la aplicación → toasts de @pi/ui
// apps/web/src/app/tracker.routes.ts (en diferido)
{ path: '', providers: [provideTrackerPagesInfrastructure()], children: trackerRoutes }
``` ```
## 2.2 Flujo completo de una escritura ## 2.2 Flujo completo de una escritura

View File

@@ -113,18 +113,30 @@ Qué gana la página:
| ---------------- | --------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------- | --------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AuthFacade` | `AuthStore` | raíz | Sesión, usuario, login (PKCE o email y contraseña de Pireon), renovación del token, restaurar al arrancar | | `AuthFacade` | `AuthStore` | raíz | Sesión, usuario, login (PKCE o email y contraseña de Pireon), renovación del token, restaurar al arrancar |
| `ProjectsFacade` | `ProjectsStore` | raíz | Mis proyectos; se actualiza cuando me añaden o quitan (`myProjectsChanged`) | | `ProjectsFacade` | `ProjectsStore` | raíz | Mis proyectos; se actualiza cuando me añaden o quitan (`myProjectsChanged`) |
| `HomeFacade` | `HomeStore` | raíz | «Mi trabajo» en la portada: mis tickets abiertos por categoría (QA fallidos primero), esperando QA y próximas releases; se recarga al volver a la pestaña | | `HomeFacade` | `HomeStore` | ruta `/` (portada) | «Mi trabajo» en la portada: mis tickets abiertos por categoría (QA fallidos primero), esperando QA y próximas releases; se recarga al volver a la pestaña |
| `MyHomeFacade` | `MyHomeStore` | raíz | El resto de la portada (`myHome`): agenda, actividad, cifras de la semana, sprints, tarjetas, sugerencias, primeros pasos y quién está; se recarga cada minuto y al volver a la pestaña | | `MyHomeFacade` | `MyHomeStore` | ruta `/` (portada) | El resto de la portada (`myHome`): agenda, actividad, cifras de la semana, sprints, tarjetas, sugerencias, primeros pasos y quién está; se recarga cada minuto y al volver a la pestaña |
| `MyViewsFacade` | `MyViewsStore` | raíz | Mis vistas guardadas de «Mi trabajo» (entre proyectos: proyectos, asignados a mí, que sigo, estados, prioridades, solo bugs, vencidos, texto) y los tickets de la elegida | | `MyViewsFacade` | `MyViewsStore` | ruta `/` (portada) | Mis vistas guardadas de «Mi trabajo» (entre proyectos: proyectos, asignados a mí, que sigo, estados, prioridades, solo bugs, vencidos, texto) y los tickets de la elegida |
| `ExploreFacade` | `ExploreStore` | raíz | Proyectos públicos («Explorar»), también sin sesión | | `ExploreFacade` | `ExploreStore` | ruta `explore` | Proyectos públicos («Explorar»), también sin sesión |
| `ProjectFacade` | `ProjectStore` | ruta `/p/:key` | Proyecto, miembros, stats, releases, actividad en vivo, búsqueda de usuarios, configuración (`workflow`, `saveSettings`) y estadísticas (`loadAnalytics`, `loadBurnup`) | | `ProjectFacade` | `ProjectStore` | ruta `/p/:key` | Proyecto, miembros, stats, releases, actividad en vivo, búsqueda de usuarios, configuración (`workflow`, `saveSettings`) y estadísticas (`loadAnalytics`, `loadBurnup`) |
| `BoardFacade` | `BoardStore` | ruta `board` | Tablero kanban, filtros, movimiento optimista, nuevo ticket, cambios en vivo | | `BoardFacade` | `BoardStore` | ruta `board` | Tablero kanban, filtros, movimiento optimista, nuevo ticket, cambios en vivo |
| `ListFacade` | `ListStore` | ruta `list` | Todos los tickets del proyecto en filas: filtros (enviados a la API), cambios de estado, prioridad y persona en la fila, edición en bloque; se recarga tras cada cambio propio y con los de otros | | `ListFacade` | `ListStore` | ruta `list` | Todos los tickets del proyecto en filas: filtros (enviados a la API), cambios de estado, prioridad y persona en la fila, edición en bloque; se recarga tras cada cambio propio y con los de otros |
| `IssueFacade` | `IssueStore` | ruta `issues/:issueKey` | Ticket, comentarios en vivo, historial y todas sus acciones | | `IssueFacade` | `IssueStore` | ruta `issues/:issueKey` | Ticket, comentarios en vivo, historial y todas sus acciones |
Las de ámbito de ruta se registran con `provideProjectFacade()`, `provideBoardFacade()` o `provideIssueFacade()` en los En la raíz solo queda lo que usa la cabecera desde el arranque: la sesión (`AuthFacade`), mis proyectos para la paleta
`providers` del componente de la página (store + facade) y desaparecen al salir (con sus subscriptions). Las páginas de comandos (`ProjectsFacade`) y la campana (`InboxFacade`, `PushFacade`). Lo demás es de su página:
hijas de `/p/:key` reciben la misma `ProjectFacade`.
- Las de una página que se vuelve a visitar a menudo (portada, Explorar, el formulario de propuestas) van en los
`providers` de su ruta (`provideHomeFacade()`, `provideMyHomeFacade()`, `provideMyViewsFacade()`,
`provideCalendarFacade()`, `provideExploreFacade()`, `provideProposeFacade()`): cargan con la página y el inyector de
la ruta las conserva, así que al volver se ve lo último al instante mientras se recarga.
- Las de un proyecto o un ticket se registran con `provideProjectFacade()`, `provideBoardFacade()` o
`provideIssueFacade()` en los `providers` del componente de la página (store + facade) y desaparecen al salir (con sus
subscriptions). Las páginas hijas de `/p/:key` reciben la misma `ProjectFacade`.
Los adaptadores siguen el mismo reparto: `provideTrackerShellInfrastructure()` (proyectos, tickets, avisos, push y tiempo
real) va en la raíz, y `provideTrackerPagesInfrastructure()` (el resto) en la ruta del tracker, que se carga en diferido
(`apps/web/src/app/tracker.routes.ts`). Las librerías web declaran `"sideEffects": false` (`libs/web/package.json`), para
que el empaquetador deje en cada trozo solo lo que usa y no todo lo que exporta un `index.ts`.
Otros contextos también usan la facade: los stores del tracker leen el usuario de `AuthFacade`, y la infraestructura Otros contextos también usan la facade: los stores del tracker leen el usuario de `AuthFacade`, y la infraestructura
de identidad (`SessionTokenSource`) le pide el token para el cliente GraphQL. de identidad (`SessionTokenSource`) le pide el token para el cliente GraphQL.

Binary file not shown.

Binary file not shown.

5
libs/web/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "@task-stream/web-libs",
"private": true,
"sideEffects": false
}

View File

@@ -1,4 +1,4 @@
import { inject, Injectable, signal } from '@angular/core'; import { inject, Injectable, Provider, signal } from '@angular/core';
import { CalendarFeed, CalendarGateway } from '@task-stream/web/tracker/domain'; import { CalendarFeed, CalendarGateway } from '@task-stream/web/tracker/domain';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -7,7 +7,7 @@ import { Observable } from 'rxjs';
* calendar app, made, renewed or dropped here. Per person and seldom * calendar app, made, renewed or dropped here. Per person and seldom
* touched, so it lives in this facade alone, without a store. * touched, so it lives in this facade alone, without a store.
*/ */
@Injectable({ providedIn: 'root' }) @Injectable()
export class CalendarFacade { export class CalendarFacade {
private readonly gateway = inject(CalendarGateway); private readonly gateway = inject(CalendarGateway);
@@ -54,3 +54,8 @@ export class CalendarFacade {
this.error.set('No se ha podido. Inténtalo de nuevo en un momento.'); this.error.set('No se ha podido. Inténtalo de nuevo en un momento.');
} }
} }
/** My calendar subscription: provided by the route that shows it, not the root. */
export function provideCalendarFacade(): Provider[] {
return [CalendarFacade];
}

View File

@@ -1,10 +1,10 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Provider } from '@angular/core';
import { injectDispatch } from '@ngrx/signals/events'; import { injectDispatch } from '@ngrx/signals/events';
import { explorePageEvents } from '../events/explore.events'; import { explorePageEvents } from '../events/explore.events';
import { ExploreStore } from '../stores/explore.store'; import { ExploreStore } from '../stores/explore.store';
/** "Explorar": public projects, for anyone (no sign-in needed). */ /** "Explorar": public projects, for anyone (no sign-in needed). */
@Injectable({ providedIn: 'root' }) @Injectable()
export class ExploreFacade { export class ExploreFacade {
private readonly store = inject(ExploreStore); private readonly store = inject(ExploreStore);
private readonly dispatch = injectDispatch(explorePageEvents); private readonly dispatch = injectDispatch(explorePageEvents);
@@ -20,3 +20,8 @@ export class ExploreFacade {
this.dispatch.opened(); this.dispatch.opened();
} }
} }
/** The public projects page: provided by the route that shows it, not the root. */
export function provideExploreFacade(): Provider[] {
return [ExploreStore, ExploreFacade];
}

View File

@@ -23,7 +23,7 @@ import { boardPageEvents } from '../events/board.events';
import { projectPageEvents } from '../events/project.events'; import { projectPageEvents } from '../events/project.events';
import { Notifications } from '../notifications'; import { Notifications } from '../notifications';
import { BoardFacade, provideBoardFacade } from './board.facade'; import { BoardFacade, provideBoardFacade } from './board.facade';
import { HomeFacade } from './home.facade'; import { HomeFacade, provideHomeFacade } from './home.facade';
import { InboxFacade } from './inbox.facade'; import { InboxFacade } from './inbox.facade';
import { IssueFacade, provideIssueFacade } from './issue.facade'; import { IssueFacade, provideIssueFacade } from './issue.facade';
import { ProjectFacade, provideProjectFacade } from './project.facade'; import { ProjectFacade, provideProjectFacade } from './project.facade';
@@ -427,6 +427,7 @@ describe('HomeFacade', () => {
); );
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
provideHomeFacade(),
{ provide: IssuesGateway, useValue: { mine } }, { provide: IssuesGateway, useValue: { mine } },
{ {
provide: ReleasesGateway, provide: ReleasesGateway,

View File

@@ -1,10 +1,10 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Provider } from '@angular/core';
import { injectDispatch } from '@ngrx/signals/events'; import { injectDispatch } from '@ngrx/signals/events';
import { homePageEvents } from '../events/home.events'; import { homePageEvents } from '../events/home.events';
import { HomeStore } from '../stores/home.store'; import { HomeStore } from '../stores/home.store';
/** "My work" on the home page: my tickets, waiting for QA, upcoming releases. */ /** "My work" on the home page: my tickets, waiting for QA, upcoming releases. */
@Injectable({ providedIn: 'root' }) @Injectable()
export class HomeFacade { export class HomeFacade {
private readonly store = inject(HomeStore); private readonly store = inject(HomeStore);
private readonly dispatch = injectDispatch(homePageEvents); private readonly dispatch = injectDispatch(homePageEvents);
@@ -33,3 +33,8 @@ export class HomeFacade {
this.dispatch.opened(); this.dispatch.opened();
} }
} }
/** «Mi trabajo» on the home page: provided by the route that shows it, not the root. */
export function provideHomeFacade(): Provider[] {
return [HomeStore, HomeFacade];
}

View File

@@ -1,4 +1,4 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Provider } from '@angular/core';
import { injectDispatch } from '@ngrx/signals/events'; import { injectDispatch } from '@ngrx/signals/events';
import { myHomePageEvents } from '../events/my-home.events'; import { myHomePageEvents } from '../events/my-home.events';
import { MyHomeStore } from '../stores/my-home.store'; import { MyHomeStore } from '../stores/my-home.store';
@@ -7,7 +7,7 @@ import { MyHomeStore } from '../stores/my-home.store';
* The home page beyond «my work»: agenda, activity, the week, sprints, * The home page beyond «my work»: agenda, activity, the week, sprints,
* project cards, suggestions, first steps and who is on what now. * project cards, suggestions, first steps and who is on what now.
*/ */
@Injectable({ providedIn: 'root' }) @Injectable()
export class MyHomeFacade { export class MyHomeFacade {
private readonly store = inject(MyHomeStore); private readonly store = inject(MyHomeStore);
private readonly dispatch = injectDispatch(myHomePageEvents); private readonly dispatch = injectDispatch(myHomePageEvents);
@@ -21,3 +21,8 @@ export class MyHomeFacade {
this.dispatch.refreshed(); this.dispatch.refreshed();
} }
} }
/** The rest of the home page: provided by the route that shows it, not the root. */
export function provideMyHomeFacade(): Provider[] {
return [MyHomeStore, MyHomeFacade];
}

View File

@@ -1,11 +1,11 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Provider } from '@angular/core';
import { injectDispatch } from '@ngrx/signals/events'; import { injectDispatch } from '@ngrx/signals/events';
import { MyViewCriteria } from '@task-stream/web/tracker/domain'; import { MyViewCriteria } from '@task-stream/web/tracker/domain';
import { myViewsPageEvents } from '../events/my-views.events'; import { myViewsPageEvents } from '../events/my-views.events';
import { MyViewsStore, VIEW_LIMIT } from '../stores/my-views.store'; import { MyViewsStore, VIEW_LIMIT } from '../stores/my-views.store';
/** My saved views of «my work», across my projects. */ /** My saved views of «my work», across my projects. */
@Injectable({ providedIn: 'root' }) @Injectable()
export class MyViewsFacade { export class MyViewsFacade {
private readonly store = inject(MyViewsStore); private readonly store = inject(MyViewsStore);
private readonly dispatch = injectDispatch(myViewsPageEvents); private readonly dispatch = injectDispatch(myViewsPageEvents);
@@ -39,3 +39,8 @@ export class MyViewsFacade {
this.dispatch.deleteRequested(viewId); this.dispatch.deleteRequested(viewId);
} }
} }
/** My saved views, on the home page: provided by the route that shows it, not the root. */
export function provideMyViewsFacade(): Provider[] {
return [MyViewsStore, MyViewsFacade];
}

View File

@@ -1,4 +1,4 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Provider } from '@angular/core';
import { import {
ProposalInput, ProposalInput,
ProposalResult, ProposalResult,
@@ -7,7 +7,7 @@ import {
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
/** The public form of a project: anyone reports a bug or proposes an idea. */ /** The public form of a project: anyone reports a bug or proposes an idea. */
@Injectable({ providedIn: 'root' }) @Injectable()
export class ProposeFacade { export class ProposeFacade {
private readonly proposals = inject(ProposalsGateway); private readonly proposals = inject(ProposalsGateway);
@@ -18,3 +18,8 @@ export class ProposeFacade {
return this.proposals.submit(projectKey, proposal); return this.proposals.submit(projectKey, proposal);
} }
} }
/** The public proposal form: provided by the route that shows it, not the root. */
export function provideProposeFacade(): Provider[] {
return [ProposeFacade];
}

View File

@@ -21,7 +21,6 @@ interface ExploreState {
/** Public projects, readable by anyone (also without signing in). */ /** Public projects, readable by anyone (also without signing in). */
export const ExploreStore = signalStore( export const ExploreStore = signalStore(
{ providedIn: 'root' },
withState<ExploreState>({ withState<ExploreState>({
projects: [], projects: [],
loaded: false, loaded: false,

View File

@@ -42,11 +42,11 @@ interface HomeState {
/** /**
* "My work" across every project I belong to: my open tickets, tickets * "My work" across every project I belong to: my open tickets, tickets
* waiting for QA, the open ones I follow and upcoming releases. Root-scoped, so coming back to the * waiting for QA, the open ones I follow and upcoming releases. Provided by the home route,
* home page shows the last data at once while it refreshes. * whose injector outlives the page, so coming back shows the last data at
* once while it refreshes.
*/ */
export const HomeStore = signalStore( export const HomeStore = signalStore(
{ providedIn: 'root' },
withState<HomeState>({ withState<HomeState>({
mine: [], mine: [],
mineTotal: 0, mineTotal: 0,

View File

@@ -18,11 +18,11 @@ interface MyHomeState {
} }
/** /**
* The home page beyond «my work». Root-scoped, so coming back shows the * The home page beyond «my work». Provided by the home route, whose injector
* last reading at once while it refreshes. * outlives the page, so coming back shows the last reading at once while it
* refreshes.
*/ */
export const MyHomeStore = signalStore( export const MyHomeStore = signalStore(
{ providedIn: 'root' },
withState<MyHomeState>({ home: null, error: null }), withState<MyHomeState>({ home: null, error: null }),
withReducer( withReducer(
on(myHomeApiEvents.loaded, ({ payload }) => ({ on(myHomeApiEvents.loaded, ({ payload }) => ({

View File

@@ -50,9 +50,8 @@ interface MyViewsState {
error: string | null; error: string | null;
} }
/** My saved views of «my work» and the tickets of the one picked. Root-scoped. */ /** My saved views of «my work» and the tickets of the one picked. Provided by the home route. */
export const MyViewsStore = signalStore( export const MyViewsStore = signalStore(
{ providedIn: 'root' },
withState<MyViewsState>({ withState<MyViewsState>({
views: [], views: [],
selectedId: null, selectedId: null,

View File

@@ -45,22 +45,34 @@ import { GraphqlWebhooksGateway } from './adapters/graphql-webhooks.gateway';
import { HttpTransferGateway } from './adapters/http-transfer.gateway'; import { HttpTransferGateway } from './adapters/http-transfer.gateway';
import { GraphqlAutomationsGateway } from './adapters/graphql-automations.gateway'; import { GraphqlAutomationsGateway } from './adapters/graphql-automations.gateway';
/** Ports → GraphQL adapters (and REST for files). */ /**
export function provideTrackerInfrastructure(): EnvironmentProviders { * The adapters the app shell needs from the start (the command palette, the
* notifications bell and live updates): provided at the root.
*/
export function provideTrackerShellInfrastructure(): EnvironmentProviders {
return makeEnvironmentProviders([ return makeEnvironmentProviders([
{ provide: ProjectsGateway, useClass: GraphqlProjectsGateway }, { provide: ProjectsGateway, useClass: GraphqlProjectsGateway },
{ provide: IssuesGateway, useClass: GraphqlIssuesGateway }, { provide: IssuesGateway, useClass: GraphqlIssuesGateway },
{ provide: NotificationsGateway, useClass: GraphqlNotificationsGateway },
{ provide: PushDevice, useClass: BrowserPushDevice },
{ provide: TrackerRealtimeGateway, useClass: GraphqlRealtimeGateway },
]);
}
/**
* Ports → GraphQL adapters (and REST for files) used only by the tracker's
* pages: provided by their lazy route, so they load with them.
*/
export function provideTrackerPagesInfrastructure(): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: CommentsGateway, useClass: GraphqlCommentsGateway }, { provide: CommentsGateway, useClass: GraphqlCommentsGateway },
{ provide: ReleasesGateway, useClass: GraphqlReleasesGateway }, { provide: ReleasesGateway, useClass: GraphqlReleasesGateway },
{ provide: ActivityGateway, useClass: GraphqlActivityGateway }, { provide: ActivityGateway, useClass: GraphqlActivityGateway },
{ provide: AttachmentsGateway, useClass: HttpAttachmentsGateway }, { provide: AttachmentsGateway, useClass: HttpAttachmentsGateway },
{ provide: ProposalsGateway, useClass: HttpProposalsGateway }, { provide: ProposalsGateway, useClass: HttpProposalsGateway },
{ provide: UsersGateway, useClass: GraphqlUsersGateway }, { provide: UsersGateway, useClass: GraphqlUsersGateway },
{ provide: NotificationsGateway, useClass: GraphqlNotificationsGateway },
{ provide: CalendarGateway, useClass: GraphqlCalendarGateway }, { provide: CalendarGateway, useClass: GraphqlCalendarGateway },
{ provide: PresenceGateway, useClass: GraphqlPresenceGateway }, { provide: PresenceGateway, useClass: GraphqlPresenceGateway },
{ provide: PushDevice, useClass: BrowserPushDevice },
{ provide: TrackerRealtimeGateway, useClass: GraphqlRealtimeGateway },
{ provide: SavedFiltersGateway, useClass: GraphqlSavedFiltersGateway }, { provide: SavedFiltersGateway, useClass: GraphqlSavedFiltersGateway },
{ provide: SprintsGateway, useClass: GraphqlSprintsGateway }, { provide: SprintsGateway, useClass: GraphqlSprintsGateway },
{ provide: GitGateway, useClass: GraphqlGitGateway }, { provide: GitGateway, useClass: GraphqlGitGateway },

View File

@@ -1,16 +1,33 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { authGuard } from '@task-stream/web/identity/presenters'; import { authGuard } from '@task-stream/web/identity/presenters';
import {
provideCalendarFacade,
provideExploreFacade,
provideHomeFacade,
provideMyHomeFacade,
provideMyViewsFacade,
provideProposeFacade,
} from '@task-stream/web/tracker/application';
/** /**
* The tracker. Home needs a session; project pages and «Explorar» don't: * The tracker. Home needs a session; project pages and «Explorar» don't:
* public projects can be read by anyone (the API decides what each visitor * public projects can be read by anyone (the API decides what each visitor
* sees, and pages hide what they cannot change). * sees, and pages hide what they cannot change).
*
* Each page's state is provided by its route, not the root: it loads with the
* page, and the route's injector keeps it for when you come back.
*/ */
export const trackerRoutes: Routes = [ export const trackerRoutes: Routes = [
{ {
path: '', path: '',
canActivate: [authGuard], canActivate: [authGuard],
title: 'Proyectos · TaskStream', title: 'Proyectos · TaskStream',
providers: [
provideHomeFacade(),
provideMyHomeFacade(),
provideMyViewsFacade(),
provideCalendarFacade(),
],
loadComponent: () => loadComponent: () =>
import('./pages/projects.page').then((m) => m.ProjectsPage), import('./pages/projects.page').then((m) => m.ProjectsPage),
}, },
@@ -24,6 +41,7 @@ export const trackerRoutes: Routes = [
{ {
path: 'explore', path: 'explore',
title: 'Proyectos públicos · TaskStream', title: 'Proyectos públicos · TaskStream',
providers: [provideExploreFacade()],
loadComponent: () => loadComponent: () =>
import('./pages/explore.page').then((m) => m.ExplorePage), import('./pages/explore.page').then((m) => m.ExplorePage),
}, },
@@ -47,6 +65,7 @@ export const trackerRoutes: Routes = [
{ {
path: 'propose', path: 'propose',
title: 'Proponer · TaskStream', title: 'Proponer · TaskStream',
providers: [provideProposeFacade()],
loadComponent: () => loadComponent: () =>
import('./pages/propose.page').then((m) => m.ProposePage), import('./pages/propose.page').then((m) => m.ProposePage),
}, },