feat(web): English paths, and a portfolio that updates itself
/cartera, /explorar and /p/:key/proponer are /portfolio, /explore and /p/:key/propose now, like every other path; the Spanish ones redirect, so links already shared keep working. Settings sections go by ?section= with English names. The portfolio reads itself again, quietly, when a ticket, sprint or release of the projects shown changes, when my projects change, or when the live connection comes back; «En directo» replaces the refresh button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15066,7 +15066,7 @@
|
||||
"path": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "/p/WEB/proponer",
|
||||
"example": "/p/WEB/propose",
|
||||
"description": "The form in the web app, while it takes them"
|
||||
},
|
||||
"top": {
|
||||
|
||||
@@ -3012,7 +3012,7 @@ export interface components {
|
||||
open: boolean;
|
||||
/**
|
||||
* @description The form in the web app, while it takes them
|
||||
* @example /p/WEB/proponer
|
||||
* @example /p/WEB/propose
|
||||
*/
|
||||
path: string | null;
|
||||
/** @description Open proposals, most voted first */
|
||||
|
||||
@@ -12,6 +12,8 @@ describe('appRoutes', () => {
|
||||
const children = app?.children ?? [];
|
||||
expect(children.map((route) => route.path)).toEqual([
|
||||
'',
|
||||
'portfolio',
|
||||
'explore',
|
||||
'cartera',
|
||||
'explorar',
|
||||
'p/:key',
|
||||
@@ -19,7 +21,10 @@ describe('appRoutes', () => {
|
||||
expect(children[0].canActivate).toContain(authGuard);
|
||||
expect(children[1].canActivate).toContain(authGuard);
|
||||
expect(children[2].canActivate).toBeUndefined();
|
||||
expect(children[3].canActivate).toBeUndefined();
|
||||
expect(children[5].canActivate).toBeUndefined();
|
||||
// The Spanish paths of shared links lead to the English ones.
|
||||
expect(children[3].redirectTo).toBe('portfolio');
|
||||
expect(children[4].redirectTo).toBe('explore');
|
||||
});
|
||||
|
||||
it('exposes every project section', () => {
|
||||
@@ -28,8 +33,9 @@ describe('appRoutes', () => {
|
||||
?.children?.find((route) => route.path === 'p/:key');
|
||||
expect(project?.children?.map((route) => route.path)).toEqual([
|
||||
'',
|
||||
'board',
|
||||
'proponer',
|
||||
'board',
|
||||
'propose',
|
||||
'list',
|
||||
'issues/:issueKey',
|
||||
'backlog',
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
<a
|
||||
shellNav
|
||||
class="ts-nav-link"
|
||||
routerLink="/cartera"
|
||||
routerLink="/portfolio"
|
||||
routerLinkActive="is-active"
|
||||
>
|
||||
Cartera
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
<a
|
||||
shellNav
|
||||
class="ts-nav-link"
|
||||
routerLink="/explorar"
|
||||
routerLink="/explore"
|
||||
routerLinkActive="is-active"
|
||||
>
|
||||
Explorar
|
||||
|
||||
@@ -70,7 +70,7 @@ lee igual que un anónimo. Las subscriptions exigen sesión; con ella, también
|
||||
en el primer estado, de tipo Bug o Feature (o el primer tipo activo). No se pide correo, porque quedaría a la vista. Contra
|
||||
el spam, el campo `website` es una trampa (si llega relleno se responde como si nada y no se guarda) y cada dirección
|
||||
envía como mucho 5 cada 15 minutos (429); el contador vive en Redis (`RateLimitStore`), así que vale igual con
|
||||
varias instancias. Los propietarios y admins del proyecto reciben un aviso (`PROPOSED`). La web tiene el formulario en `/p/{CLAVE}/proponer`, enlazado en la cabecera del proyecto.
|
||||
varias instancias. Los propietarios y admins del proyecto reciben un aviso (`PROPOSED`). La web tiene el formulario en `/p/{CLAVE}/propose`, enlazado en la cabecera del proyecto.
|
||||
|
||||
**Votos**: quien tenga sesión vota los tickets de un proyecto público, y los miembros los de uno privado
|
||||
(`voteIssue(issueId, on)`, `PUT/DELETE /api/issues/{issue}/vote`). Cada ticket da `votes` y `voted` (si he votado
|
||||
|
||||
@@ -270,7 +270,7 @@ visualización de la plataforma:
|
||||
|
||||
### Proyectos públicos y visitantes sin sesión
|
||||
|
||||
- Solo la portada (`/`) exige sesión. `/explorar` y las páginas de proyecto (`/p/:key/…`) no: la API decide qué ve
|
||||
- Solo la portada (`/`) exige sesión. `/explore` y las páginas de proyecto (`/p/:key/…`) no: la API decide qué ve
|
||||
cada visitante, y las páginas ya ocultan lo que no puede cambiar (`canContribute`, `canManage`).
|
||||
- Sin sesión, la cabecera muestra «Explorar» e «Iniciar sesión»; dentro de un proyecto público aparece un aviso de modo
|
||||
lectura, y en uno privado (o inexistente), el enlace para iniciar sesión volviendo a esa página (`returnUrl`).
|
||||
@@ -287,8 +287,8 @@ visualización de la plataforma:
|
||||
| `/login` | Inicio de sesión (solo sin sesión) |
|
||||
| `/auth/callback` | Retorno de Pireon (PKCE) |
|
||||
| `/` | Inicio: saludo según la hora con el resumen del día, «Ir a…» (`WEB-12`), primeros pasos, cuatro cifras (`ts-home-kpis`: abiertos míos, esperando QA, terminados por mí y por el equipo esta semana), la semana (`ts-week-agenda`), mis vistas guardadas y dos columnas: mis tickets y lo último en mis proyectos; sprints en curso, esperando QA, que sigo, próximas releases, quién está ahora y «Retomar». Debajo, mis proyectos con su actividad |
|
||||
| `/cartera` | Cartera (con sesión): una tarjeta por proyecto activo con lo abierto y en riesgo, el sprint en curso (¿llega?), las próximas releases con su previsión y las épicas abiertas |
|
||||
| `/explorar` | Proyectos públicos (sin sesión) |
|
||||
| `/portfolio` | Cartera (con sesión): una tarjeta por proyecto activo con lo abierto y en riesgo, el sprint en curso (¿llega?), las próximas releases con su previsión y las épicas abiertas; en directo: se vuelve a leer sola cuando cambia un ticket, sprint o release de esos proyectos |
|
||||
| `/explore` | Proyectos públicos (sin sesión) |
|
||||
| `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros) |
|
||||
| `/p/:key/issues/:issueKey` | Ticket. Encima del título, quién más lo está viendo o editando ahora (`PresenceFacade`: late cada 10 s; editar es tener abierto el formulario del ticket o de un comentario, o estar escribiendo uno) |
|
||||
| `/p/:key/releases` | Releases |
|
||||
@@ -296,10 +296,12 @@ visualización de la plataforma:
|
||||
| `/p/:key/roadmap` | Hoja de ruta: épicas en el tiempo, con sprints y releases; flechas «bloquea» / «depende de» entre épicas y aviso si una empieza antes de que acabe la que la bloquea |
|
||||
| `/p/:key/dashboard` | Seguimiento: cifras, flujo por estado, lo que ha cambiado desde tu última visita (guardada en este navegador), sprint en curso, tickets en riesgo, previsión de las próximas releases, antigüedad de lo que está en curso, cuellos de botella, salud del backlog (cada número abre la lista filtrada: `?missing=estimate`, `area`, `assignee` o `?staleDays=90`), calidad (bugs por prioridad y por semana, QA por área, bugs tras cada release), abiertos por un campo de lista a elegir, equipo con sus puntos abiertos y tipos. Se recarga solo con cada cambio |
|
||||
| `/p/:key/analytics` | Estadísticas: creados vs. resueltos, flujo acumulado, burnup de una release, tiempos de ciclo y entrega, QA y resueltos por persona (30 días a 1 año) |
|
||||
| `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto), con un menú de secciones (`?seccion=`): General (visibilidad, propuestas), Flujo de trabajo (estados, transiciones, definición de «Hecho»), Tickets (tipos, áreas, campos), Equipos, Automatizaciones, Integraciones (Git, webhooks), Importar y exportar y Zona peligrosa; archivar en lugar de borrar, y un punto en el menú donde hay cambios sin guardar |
|
||||
| `/p/:key/settings` | Configuración (propietario y admins; pestaña oculta para el resto), con un menú de secciones (`?section=`): General (visibilidad, propuestas), Flujo de trabajo (estados, transiciones, definición de «Hecho»), Tickets (tipos, áreas, campos), Equipos, Automatizaciones, Integraciones (Git, webhooks), Importar y exportar y Zona peligrosa; archivar en lugar de borrar, y un punto en el menú donde hay cambios sin guardar |
|
||||
| `/p/:key/activity` | Actividad en vivo |
|
||||
| `/p/:key/propose` | Formulario público para reportar un error o proponer una idea sin cuenta (si el proyecto lo tiene abierto); llegan como tickets con la etiqueta «propuesta» |
|
||||
| `/p/:key/members` | Miembros, con su rol (propietario, admin, miembro, lector o invitado). Un invitado de un proyecto privado solo tiene Tablero, Lista y Miembros, ve solo sus tickets y abre tickets sin asignarlos ni planificarlos |
|
||||
|
||||
Solo `/` está protegida por `authGuard` (redirige a `/login?returnUrl=…`); `/login` y `/auth/callback` son para quien
|
||||
no tiene sesión, y el resto se puede abrir sin ella si el proyecto es público. Los parámetros de ruta llegan como inputs de
|
||||
los componentes (`withComponentInputBinding`).
|
||||
los componentes (`withComponentInputBinding`). Las rutas van en inglés; las antiguas en castellano (`/cartera`,
|
||||
`/explorar`, `/p/:key/proponer`) redirigen a las nuevas, para que sigan valiendo los enlaces ya compartidos.
|
||||
|
||||
@@ -121,7 +121,7 @@ Mucho trabajo para lo que aportan hoy; se pueden recuperar si alguien los necesi
|
||||
- Presencia: en cada ticket se ve quién más lo está viendo o editando en ese momento.
|
||||
- Rol «invitado»: para clientes o gente de fuera; en un proyecto privado solo ve los tickets en los que participa,
|
||||
abre tickets y comenta en los suyos.
|
||||
- Vista de cartera (`/cartera`): todos tus proyectos activos de un vistazo, con su riesgo, sprint, releases y épicas.
|
||||
- Vista de cartera (`/portfolio`): todos tus proyectos activos de un vistazo, con su riesgo, sprint, releases y épicas.
|
||||
- Una portada con vida: saludo y resumen del día, la semana que viene, lo último en tus proyectos, quién está ahora,
|
||||
la semana en cifras (tuya y del equipo, con racha), sprints en curso, «Retomar», tarjetas de proyecto con actividad,
|
||||
qué coger cuando no tienes nada y primeros pasos para quien empieza. También por MCP (`get_my_week`).
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -169,7 +169,7 @@ export class GetShowcaseQueryHandler
|
||||
})),
|
||||
proposals: {
|
||||
open: project.acceptsProposals,
|
||||
path: project.acceptsProposals ? `/p/${project.key}/proponer` : null,
|
||||
path: project.acceptsProposals ? `/p/${project.key}/propose` : null,
|
||||
top: [...proposals.items]
|
||||
.sort((a, b) => b.votes - a.votes)
|
||||
.slice(0, 10)
|
||||
|
||||
@@ -361,7 +361,7 @@ class ShowcaseProposalsDto {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
nullable: true,
|
||||
example: '/p/WEB/proponer',
|
||||
example: '/p/WEB/propose',
|
||||
description: 'The form in the web app, while it takes them',
|
||||
})
|
||||
path!: string | null;
|
||||
|
||||
@@ -113,7 +113,7 @@ const SAMPLE_FEED = [
|
||||
</p>
|
||||
<p class="ts-login__explore">
|
||||
¿Solo quieres mirar?
|
||||
<a routerLink="/explorar">Proyectos públicos →</a>
|
||||
<a routerLink="/explore">Proyectos públicos →</a>
|
||||
</p>
|
||||
<div class="ts-feed ts-panel" aria-hidden="true">
|
||||
<div class="ts-feed__track">
|
||||
|
||||
@@ -16,3 +16,11 @@ export const portfolioApiEvents = eventGroup({
|
||||
loadFailed: type<string>(),
|
||||
},
|
||||
});
|
||||
|
||||
/** Something changed in one of my projects: reload, quietly. */
|
||||
export const portfolioLiveEvents = eventGroup({
|
||||
source: 'Portfolio Live',
|
||||
events: {
|
||||
changed: type<void>(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,10 +10,18 @@ import {
|
||||
import {
|
||||
PortfolioProject,
|
||||
ProjectsGateway,
|
||||
TrackerRealtimeGateway,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
import { switchMap } from 'rxjs';
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
merge,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
portfolioApiEvents,
|
||||
portfolioLiveEvents,
|
||||
portfolioPageEvents,
|
||||
} from '../events/portfolio.events';
|
||||
import { describeError } from '../notifications';
|
||||
@@ -25,7 +33,7 @@ interface PortfolioState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** All my active projects at a glance. Scoped to the portfolio page. */
|
||||
/** All my active projects at a glance, live. Scoped to the portfolio page. */
|
||||
export const PortfolioStore = signalStore(
|
||||
withState<PortfolioState>({
|
||||
projects: [],
|
||||
@@ -47,8 +55,16 @@ export const PortfolioStore = signalStore(
|
||||
})),
|
||||
),
|
||||
withEventHandlers(
|
||||
(_store, events = inject(Events), projects = inject(ProjectsGateway)) => ({
|
||||
load$: events.on(portfolioPageEvents.opened).pipe(
|
||||
(
|
||||
_store,
|
||||
events = inject(Events),
|
||||
projects = inject(ProjectsGateway),
|
||||
realtime = inject(TrackerRealtimeGateway),
|
||||
) => ({
|
||||
// Opened, or something changed (then quietly: no «loading»).
|
||||
load$: events
|
||||
.on(portfolioPageEvents.opened, portfolioLiveEvents.changed)
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
projects.portfolio().pipe(
|
||||
mapResponse({
|
||||
@@ -59,6 +75,27 @@ export const PortfolioStore = signalStore(
|
||||
),
|
||||
),
|
||||
),
|
||||
// Any ticket, sprint or release of the projects shown, the list of my
|
||||
// projects itself, or a cut connection: read it again, calmly.
|
||||
live$: events.on(portfolioApiEvents.loaded).pipe(
|
||||
map(({ payload }) => payload.map((p) => p.projectId).join(',')),
|
||||
distinctUntilChanged(),
|
||||
switchMap((ids) =>
|
||||
merge(
|
||||
...ids
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.flatMap((id) => [
|
||||
realtime.issueChanges(id),
|
||||
realtime.sprintChanges(id),
|
||||
realtime.releaseChanges(id),
|
||||
]),
|
||||
realtime.myProjectsChanges(),
|
||||
realtime.reconnects(),
|
||||
).pipe(debounceTime(1500)),
|
||||
),
|
||||
map(() => portfolioLiveEvents.changed()),
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GO_TO, isTyping, projectKeyIn } from './shortcuts';
|
||||
describe('shortcuts', () => {
|
||||
it('knows the project of a URL', () => {
|
||||
expect(projectKeyIn('/p/WEB/issues/WEB-3')).toBe('WEB');
|
||||
expect(projectKeyIn('/explorar')).toBeNull();
|
||||
expect(projectKeyIn('/explore')).toBeNull();
|
||||
expect(GO_TO['k']).toEqual({ path: 'backlog', inProject: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const SHORTCUTS: { keys: string; what: string; where: string }[] = [
|
||||
/** Where `g` + key goes; project pages are relative to `/p/KEY`. */
|
||||
export const GO_TO: Record<string, { path: string; inProject: boolean }> = {
|
||||
p: { path: '/', inProject: false },
|
||||
e: { path: '/explorar', inProject: false },
|
||||
e: { path: '/explore', inProject: false },
|
||||
b: { path: 'board', inProject: true },
|
||||
k: { path: 'backlog', inProject: true },
|
||||
s: { path: 'sprints', inProject: true },
|
||||
|
||||
@@ -14,7 +14,7 @@ import { AuthFacade } from '@task-stream/web/identity/application';
|
||||
import { ExploreFacade } from '@task-stream/web/tracker/application';
|
||||
import { ProjectCardComponent } from '../ui/project-card';
|
||||
|
||||
/** /explorar — public projects, for anyone (no sign-in needed). */
|
||||
/** /explore — public projects, for anyone (no sign-in needed). */
|
||||
@Component({
|
||||
selector: 'ts-explore-page',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -48,14 +48,13 @@ const short = (day: string | null) =>
|
||||
riesgo, el sprint en curso, las próximas releases y las épicas.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ts-secondary"
|
||||
[disabled]="portfolio.loading()"
|
||||
(click)="portfolio.load()"
|
||||
<p
|
||||
class="ts-live"
|
||||
title="Se actualiza sola cuando cambia algo en tus proyectos"
|
||||
>
|
||||
{{ portfolio.loading() ? 'Actualizando…' : 'Actualizar' }}
|
||||
</button>
|
||||
<span class="ts-live__dot" aria-hidden="true"></span>
|
||||
En directo
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@if (portfolio.error(); as error) {
|
||||
@@ -201,6 +200,33 @@ const short = (day: string | null) =>
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ts-live {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
color: var(--ts-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ts-live__dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--ts-c-green);
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--ts-c-green) 50%, transparent);
|
||||
animation: ts-live-pulse 2s ease-out infinite;
|
||||
}
|
||||
@keyframes ts-live-pulse {
|
||||
to {
|
||||
box-shadow: 0 0 0 0.5rem transparent;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ts-live__dot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
.ts-portfolio {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 26rem), 1fr));
|
||||
|
||||
@@ -42,7 +42,7 @@ import { projectTone, PUBLIC_TONE, ROLE } from '../ui/copy';
|
||||
@if (auth.isAuthenticated()) {
|
||||
<a class="ts-plain-link" routerLink="/">Proyectos</a> /
|
||||
} @else {
|
||||
<a class="ts-plain-link" routerLink="/explorar">Explorar</a> /
|
||||
<a class="ts-plain-link" routerLink="/explore">Explorar</a> /
|
||||
}
|
||||
{{ project.key }}
|
||||
@if (facade.myRole(); as role) {
|
||||
@@ -62,7 +62,7 @@ import { projectTone, PUBLIC_TONE, ROLE } from '../ui/copy';
|
||||
</p>
|
||||
}
|
||||
@if (project.isPublic && project.acceptsProposals) {
|
||||
<a class="ts-project-header__propose" routerLink="proponer">
|
||||
<a class="ts-project-header__propose" routerLink="propose">
|
||||
¿Has visto un error o tienes una idea? Cuéntanoslo →
|
||||
</a>
|
||||
}
|
||||
@@ -150,7 +150,7 @@ import { projectTone, PUBLIC_TONE, ROLE } from '../ui/copy';
|
||||
>Iniciar sesión</a
|
||||
>
|
||||
·
|
||||
<a class="ts-plain-link" routerLink="/explorar"
|
||||
<a class="ts-plain-link" routerLink="/explore"
|
||||
>Ver proyectos públicos</a
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ProposalResult } from '@task-stream/web/tracker/domain';
|
||||
import { RichEditorComponent } from '../ui/rich-editor';
|
||||
|
||||
/**
|
||||
* /p/:key/proponer — anyone, without an account, reports a bug or proposes
|
||||
* /p/:key/propose — anyone, without an account, reports a bug or proposes
|
||||
* an idea for a public project that takes them. It becomes a ticket
|
||||
* labelled «propuesta» that the team looks at and others can vote for.
|
||||
*/
|
||||
|
||||
@@ -61,7 +61,7 @@ const SECTIONS: SettingsSection[] = [
|
||||
hint: 'Quién ve el proyecto y si el público puede proponer cosas.',
|
||||
},
|
||||
{
|
||||
id: 'flujo',
|
||||
id: 'workflow',
|
||||
draft: true,
|
||||
label: 'Flujo de trabajo',
|
||||
hint: 'Las columnas del tablero, a cuáles se puede pasar desde cada una y cuándo algo está hecho.',
|
||||
@@ -73,31 +73,31 @@ const SECTIONS: SettingsSection[] = [
|
||||
hint: 'Tipos, áreas y campos propios. Lo que ya se usa no se borra: se archiva y deja de ofrecerse.',
|
||||
},
|
||||
{
|
||||
id: 'equipos',
|
||||
id: 'teams',
|
||||
draft: true,
|
||||
label: 'Equipos',
|
||||
hint: 'Grupos de miembros, para asignarles tickets y avisarles con «@equipo».',
|
||||
},
|
||||
{
|
||||
id: 'automatizaciones',
|
||||
id: 'automations',
|
||||
label: 'Automatizaciones',
|
||||
hint: 'Reglas «cuando… si… entonces…» que hacen solas el trabajo repetido.',
|
||||
needs: 'manage',
|
||||
},
|
||||
{
|
||||
id: 'integraciones',
|
||||
id: 'integrations',
|
||||
label: 'Integraciones',
|
||||
hint: 'Commits y pull requests de Git en cada ticket, y avisos a otros sistemas.',
|
||||
needs: 'manage',
|
||||
},
|
||||
{
|
||||
id: 'datos',
|
||||
id: 'data',
|
||||
label: 'Importar y exportar',
|
||||
hint: 'Los tickets en CSV: sacarlos, o traerlos de TaskStream o de Jira.',
|
||||
needs: 'manage',
|
||||
},
|
||||
{
|
||||
id: 'peligro',
|
||||
id: 'danger',
|
||||
label: 'Zona peligrosa',
|
||||
hint: 'Archivar el proyecto o eliminarlo del todo.',
|
||||
needs: 'owner',
|
||||
@@ -106,13 +106,13 @@ const SECTIONS: SettingsSection[] = [
|
||||
|
||||
/** Which section holds each part of the draft. */
|
||||
const DRAFT_SECTION: Partial<Record<keyof ProjectSettings, string>> = {
|
||||
statuses: 'flujo',
|
||||
transitions: 'flujo',
|
||||
checklistBeforeDone: 'flujo',
|
||||
statuses: 'workflow',
|
||||
transitions: 'workflow',
|
||||
checklistBeforeDone: 'workflow',
|
||||
issueTypes: 'tickets',
|
||||
areas: 'tickets',
|
||||
customFields: 'tickets',
|
||||
teams: 'equipos',
|
||||
teams: 'teams',
|
||||
};
|
||||
|
||||
/** "Datos y análisis" → "DATOS_Y_ANALISIS": ids of new values (they never change after saving). */
|
||||
@@ -157,9 +157,9 @@ function idFrom(name: string): string {
|
||||
@for (item of sections(); track item.id) {
|
||||
<a
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ seccion: item.id }"
|
||||
[queryParams]="{ section: item.id }"
|
||||
[class.is-active]="item.id === current().id"
|
||||
[class.is-danger]="item.id === 'peligro'"
|
||||
[class.is-danger]="item.id === 'danger'"
|
||||
[attr.aria-current]="item.id === current().id ? 'page' : null"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
@@ -233,7 +233,7 @@ function idFrom(name: string): string {
|
||||
proponga una idea.
|
||||
} @else if (project.acceptsProposals) {
|
||||
Abierto: cualquiera puede enviar un error o una idea desde
|
||||
<a class="ts-plain-link" routerLink="../proponer">el formulario</a
|
||||
<a class="ts-plain-link" routerLink="../propose">el formulario</a
|
||||
>. Llegan como tickets con la etiqueta «propuesta», y se pueden
|
||||
votar.
|
||||
} @else {
|
||||
@@ -268,7 +268,7 @@ function idFrom(name: string): string {
|
||||
>
|
||||
<fieldset [disabled]="!facade.canManage()">
|
||||
@switch (current().id) {
|
||||
@case ('flujo') {
|
||||
@case ('workflow') {
|
||||
<!-- Statuses -->
|
||||
<section class="ts-panel ts-block" aria-labelledby="st-title">
|
||||
<header class="ts-block__head">
|
||||
@@ -735,7 +735,7 @@ function idFrom(name: string): string {
|
||||
</ol>
|
||||
</section>
|
||||
}
|
||||
@case ('equipos') {
|
||||
@case ('teams') {
|
||||
<!-- Teams -->
|
||||
<section class="ts-panel ts-block" aria-labelledby="teams-title">
|
||||
<header class="ts-block__head">
|
||||
@@ -802,24 +802,24 @@ function idFrom(name: string): string {
|
||||
|
||||
@if (facade.canManage() && facade.project(); as project) {
|
||||
@switch (current().id) {
|
||||
@case ('automatizaciones') {
|
||||
@case ('automations') {
|
||||
<ts-automations [projectId]="project.id" />
|
||||
}
|
||||
@case ('integraciones') {
|
||||
@case ('integrations') {
|
||||
<ts-git-integration
|
||||
[projectId]="project.id"
|
||||
[projectKey]="project.key"
|
||||
/>
|
||||
<ts-webhooks [projectId]="project.id" />
|
||||
}
|
||||
@case ('datos') {
|
||||
@case ('data') {
|
||||
<ts-transfer [projectId]="project.id" [projectKey]="project.key" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if (
|
||||
current().id === 'peligro' && facade.isOwner() && facade.project();
|
||||
current().id === 'danger' && facade.isOwner() && facade.project();
|
||||
as project
|
||||
) {
|
||||
<section
|
||||
@@ -1610,8 +1610,8 @@ export class SettingsPage {
|
||||
protected readonly icons = TYPE_ICONS;
|
||||
protected readonly fieldTypes = FIELD_TYPES;
|
||||
|
||||
/** `?seccion=` (`withComponentInputBinding`). */
|
||||
readonly seccion = input<string | undefined>();
|
||||
/** `?section=` (`withComponentInputBinding`). */
|
||||
readonly section = input<string | undefined>();
|
||||
|
||||
protected readonly sections = computed(() =>
|
||||
SECTIONS.filter(
|
||||
@@ -1623,7 +1623,7 @@ export class SettingsPage {
|
||||
);
|
||||
protected readonly current = computed(
|
||||
() =>
|
||||
this.sections().find((s) => s.id === this.seccion()) ??
|
||||
this.sections().find((s) => s.id === this.section()) ??
|
||||
this.sections()[0],
|
||||
);
|
||||
|
||||
|
||||
@@ -15,24 +15,29 @@ export const trackerRoutes: Routes = [
|
||||
import('./pages/projects.page').then((m) => m.ProjectsPage),
|
||||
},
|
||||
{
|
||||
path: 'cartera',
|
||||
path: 'portfolio',
|
||||
canActivate: [authGuard],
|
||||
title: 'Cartera · TaskStream',
|
||||
loadComponent: () =>
|
||||
import('./pages/portfolio.page').then((m) => m.PortfolioPage),
|
||||
},
|
||||
{
|
||||
path: 'explorar',
|
||||
path: 'explore',
|
||||
title: 'Proyectos públicos · TaskStream',
|
||||
loadComponent: () =>
|
||||
import('./pages/explore.page').then((m) => m.ExplorePage),
|
||||
},
|
||||
// The Spanish paths, from links shared before.
|
||||
{ path: 'cartera', redirectTo: 'portfolio' },
|
||||
{ path: 'explorar', redirectTo: 'explore' },
|
||||
{
|
||||
path: 'p/:key',
|
||||
loadComponent: () =>
|
||||
import('./pages/project-shell.page').then((m) => m.ProjectShellPage),
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'board' },
|
||||
// The Spanish path, from links shared before.
|
||||
{ path: 'proponer', redirectTo: 'propose' },
|
||||
{
|
||||
path: 'board',
|
||||
title: 'Tablero · TaskStream',
|
||||
@@ -40,7 +45,7 @@ export const trackerRoutes: Routes = [
|
||||
import('./pages/board.page').then((m) => m.BoardPage),
|
||||
},
|
||||
{
|
||||
path: 'proponer',
|
||||
path: 'propose',
|
||||
title: 'Proponer · TaskStream',
|
||||
loadComponent: () =>
|
||||
import('./pages/propose.page').then((m) => m.ProposePage),
|
||||
|
||||
Reference in New Issue
Block a user