fix(web): the ticket's header fits, with «Más» for the less frequent actions

The reminder and what is new are pi-notes; the poll editor's buttons align.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:17:24 +02:00
parent 285bd5c454
commit 425db24eea
3 changed files with 339 additions and 184 deletions

View File

@@ -300,26 +300,26 @@ visualización de la plataforma:
## 4.6 Rutas ## 4.6 Rutas
| Ruta | Página | | Ruta | Página |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/login` | Inicio de sesión (solo sin sesión) | | `/login` | Inicio de sesión (solo sin sesión) |
| `/auth/callback` | Retorno de Pireon (PKCE) | | `/auth/callback` | Retorno de Pireon (PKCE) |
| `/` | Inicio: saludo según la hora con el resumen del día, «Ir a…» (`WEB-12`), primeros pasos, mis proyectos con su actividad (lo primero, para llegar antes), «Mi día» (`ts-my-day`: los tickets que elijo para hoy, tachados al terminar, mis abiertos para añadir y lo que quedó del último día), mis fijados (`ts-pinned`, una franja que se quita con ×; solo si hay), 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» | | `/` | Inicio: saludo según la hora con el resumen del día, «Ir a…» (`WEB-12`), primeros pasos, mis proyectos con su actividad (lo primero, para llegar antes), «Mi día» (`ts-my-day`: los tickets que elijo para hoy, tachados al terminar, mis abiertos para añadir y lo que quedó del último día), mis fijados (`ts-pinned`, una franja que se quita con ×; solo si hay), 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» |
| `/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. Cada proyecto lleva su semáforo de salud (`portfolioHealth`), con las causas al pasar por encima | | `/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. Cada proyecto lleva su semáforo de salud (`portfolioHealth`), con las causas al pasar por encima |
| `/explore` | Proyectos públicos (sin sesión) | | `/explore` | Proyectos públicos (sin sesión) |
| `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros). Quien puede contribuir cambia prioridad, estimación y persona desde la propia tarjeta (selectores nativos sobre sus distintivos; el título es el enlace). Mover una tarjeta o cambiar su persona ofrece «Deshacer» unos segundos (o Ctrl/⌘ Z): la orden contraria, que el flujo puede rechazar. Con el teclado: Tab entra en las tarjetas, las flechas se mueven entre ellas, Shift+←/→ pasa la tarjeta de columna (mismas reglas que arrastrar), `p`/`e`/`a` abren su prioridad, estimación o persona y Enter la abre | | `/p/:key/board` | Tablero kanban (`?release=` y `?assignee=` preseleccionan los filtros). Quien puede contribuir cambia prioridad, estimación y persona desde la propia tarjeta (selectores nativos sobre sus distintivos; el título es el enlace). Mover una tarjeta o cambiar su persona ofrece «Deshacer» unos segundos (o Ctrl/⌘ Z): la orden contraria, que el flujo puede rechazar. Con el teclado: Tab entra en las tarjetas, las flechas se mueven entre ellas, Shift+←/→ pasa la tarjeta de columna (mismas reglas que arrastrar), `p`/`e`/`a` abren su prioridad, estimación o persona y Enter la abre |
| `/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). «Fijar» lo deja a mano en el inicio y en la paleta de búsqueda. Arriba, sus decisiones (comentarios marcados con la bandera por quien puede cambiar el ticket); `?comment=<id>` lleva a un comentario y lo señala. Cambiar el estado o la persona y mandarlo a la papelera (sin confirmación) ofrecen «Deshacer». Cada conversación con respuestas se resuelve («✓ Resolver», con una conclusión opcional que queda como decisión) y se pliega en una línea; la cabecera cuenta las abiertas y terminar el ticket con alguna pregunta antes. Se ve quién está escribiendo en cada conversación («Ana está escribiendo…», `PresenceFacade.typists`), y en las respuestas Enter envía (Shift+Enter, otra línea). Si tiene subtareas, «Seguir todo» avisa de ella y de todo lo que cuelga, también lo que se añada (también desde Épicas). «Recordármelo» pone un aviso solo para mí (dentro de 1 hora, esta tarde, mañana o el lunes a las 9, en una semana o cuando diga), con nota opcional. «☀ Mi día» lo añade a los de hoy. «Pedir revisión» a alguien del equipo: lo ve en «Mi trabajo» («Te piden revisión») y responde «Todo bien» o «Pedir cambios» desde el recuadro Revisiones. Un comentario puede llevar una encuesta («📊 Añadir una encuesta»): se vota pulsando una opción y, al cerrarla, la ganadora queda como decisión. «🎥 Grabar pantalla» (con la voz, si se da permiso al micrófono) y «🎙 Grabar audio» graban en el navegador hasta 3 minutos y lo adjuntan al comentario, que puede ir sin texto; los vídeos y audios adjuntos se reproducen ahí mismo. Al abrirlo, lo que otros han hecho desde tu última visita: un aviso arriba («Desde tu última visita, hace 2 días: 3 comentarios y 1 cambio de Ana y Bob», con «Ver lo nuevo»), los comentarios nuevos marcados («Nuevo», también en una conversación plegada) y los cambios del historial resaltados. Cada comentario se puede «Convertir en ticket» (subtarea de este o enlazado, con el título sacado de su primera línea): el comentario enlaza al ticket nuevo («→ Convertido en WEB-45») | | `/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). A la vista, «Votar», «Seguir» y «Editar»; el resto (pedir revisión, mi día, recordármelo, seguir todo, fijar y duplicar) está en el menú «Más» (menú del CDK: se recorre con las flechas y Escape lo cierra). «Fijar» lo deja a mano en el inicio y en la paleta de búsqueda. Arriba, sus decisiones (comentarios marcados con la bandera por quien puede cambiar el ticket); `?comment=<id>` lleva a un comentario y lo señala. Cambiar el estado o la persona y mandarlo a la papelera (sin confirmación) ofrecen «Deshacer». Cada conversación con respuestas se resuelve («✓ Resolver», con una conclusión opcional que queda como decisión) y se pliega en una línea; la cabecera cuenta las abiertas y terminar el ticket con alguna pregunta antes. Se ve quién está escribiendo en cada conversación («Ana está escribiendo…», `PresenceFacade.typists`), y en las respuestas Enter envía (Shift+Enter, otra línea). Si tiene subtareas, «Seguir todo» avisa de ella y de todo lo que cuelga, también lo que se añada (también desde Épicas). «Recordármelo» pone un aviso solo para mí (dentro de 1 hora, esta tarde, mañana o el lunes a las 9, en una semana o cuando diga), con nota opcional. «☀ Mi día» lo añade a los de hoy. «Pedir revisión» a alguien del equipo: lo ve en «Mi trabajo» («Te piden revisión») y responde «Todo bien» o «Pedir cambios» desde el recuadro Revisiones. Un comentario puede llevar una encuesta («📊 Añadir una encuesta»): se vota pulsando una opción y, al cerrarla, la ganadora queda como decisión. «🎥 Grabar pantalla» (con la voz, si se da permiso al micrófono) y «🎙 Grabar audio» graban en el navegador hasta 3 minutos y lo adjuntan al comentario, que puede ir sin texto; los vídeos y audios adjuntos se reproducen ahí mismo. Al abrirlo, lo que otros han hecho desde tu última visita: un aviso arriba («Desde tu última visita, hace 2 días: 3 comentarios y 1 cambio de Ana y Bob», con «Ver lo nuevo»), los comentarios nuevos marcados («Nuevo», también en una conversación plegada) y los cambios del historial resaltados. Cada comentario se puede «Convertir en ticket» (subtarea de este o enlazado, con el título sacado de su primera línea): el comentario enlaza al ticket nuevo («→ Convertido en WEB-45») |
| `/p/:key/releases` | Releases. «Página pública» (quien gestiona): un enlace secreto con las notas para clientes, que se copia, renueva o retira | | `/p/:key/releases` | Releases. «Página pública» (quien gestiona): un enlace secreto con las notas para clientes, que se copia, renueva o retira |
| `/p/:key/report/:kind/:id` | Informe para imprimir de un sprint (`sprint`) o una release (`release`): datos, totales, notas y tickets por etapa. «Imprimir o guardar en PDF» abre el diálogo del navegador; al imprimir se quitan la cabecera, las pestañas y los fondos (`styles.scss`) | | `/p/:key/report/:kind/:id` | Informe para imprimir de un sprint (`sprint`) o una release (`release`): datos, totales, notas y tickets por etapa. «Imprimir o guardar en PDF» abre el diálogo del navegador; al imprimir se quitan la cabecera, las pestañas y los fondos (`styles.scss`) |
| `/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. Los bloqueos con tickets de otros proyectos (en los dos sentidos, `externalDependencies`) salen en «De otros proyectos» con barra discontinua, sus flechas y el mismo aviso si las fechas no encajan; los que no tienen fechas o no se pueden leer se nombran debajo | | `/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. Los bloqueos con tickets de otros proyectos (en los dos sentidos, `externalDependencies`) salen en «De otros proyectos» con barra discontinua, sus flechas y el mismo aviso si las fechas no encajan; los que no tienen fechas o no se pueden leer se nombran debajo |
| `/p/:key/dashboard` | Arriba, la salud del proyecto (`ts-project-health`): un semáforo (bien, atención, en riesgo) con cada causa y sus tickets, calculado en la web (`projectHealth`) con los mismos datos: atascados, vencidos, bloqueados, bugs críticos o altos, lo que devuelve QA, releases que no llegan y columnas sobre su WIP. 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, el recorrido de un ticket (dónde se va el tiempo de creado a hecho y qué parte es trabajo), la actividad de 12 semanas en un mapa de calor (pulsar un día abre lo que pasó, ticket a ticket), el embudo de propuestas del público (si las hay), equipo con sus puntos abiertos y tipos. Se recarga solo con cada cambio. «Modo pantalla» lo pone a pantalla completa para una tele del equipo: un bloque cada vez, ampliado, que cambia cada 15 s (← → para moverse, espacio para pausar, Esc para salir), con la hora y en directo | | `/p/:key/dashboard` | Arriba, la salud del proyecto (`ts-project-health`): un semáforo (bien, atención, en riesgo) con cada causa y sus tickets, calculado en la web (`projectHealth`) con los mismos datos: atascados, vencidos, bloqueados, bugs críticos o altos, lo que devuelve QA, releases que no llegan y columnas sobre su WIP. 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, el recorrido de un ticket (dónde se va el tiempo de creado a hecho y qué parte es trabajo), la actividad de 12 semanas en un mapa de calor (pulsar un día abre lo que pasó, ticket a ticket), el embudo de propuestas del público (si las hay), equipo con sus puntos abiertos y tipos. Se recarga solo con cada cambio. «Modo pantalla» lo pone a pantalla completa para una tele del equipo: un bloque cada vez, ampliado, que cambia cada 15 s (← → para moverse, espacio para pausar, Esc para salir), con la hora y en directo |
| `/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/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 (`?section=`): General (visibilidad, propuestas, panel para clientes), Flujo de trabajo (estados, transiciones, definición de «Hecho»), Tickets (tipos, áreas, campos), Equipos, Automatizaciones, Integraciones (Git, alertas → bugs con su URL secreta y cómo conectar Sentry, Uptime Kuma o Grafana, 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, panel para clientes), Flujo de trabajo (estados, transiciones, definición de «Hecho»), Tickets (tipos, áreas, campos), Equipos, Automatizaciones, Integraciones (Git, alertas → bugs con su URL secreta y cómo conectar Sentry, Uptime Kuma o Grafana, 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/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/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» |
| `/share/:token` | Panel para clientes, sin sesión: los bloques que eligió el proyecto (flujo, sprint, releases, épicas, actividad), sin enlaces hacia dentro de la aplicación; se vuelve a leer cada minuto. Si el enlace se renovó o retiró, «Este enlace ya no funciona» | | `/share/:token` | Panel para clientes, sin sesión: los bloques que eligió el proyecto (flujo, sprint, releases, épicas, actividad), sin enlaces hacia dentro de la aplicación; se vuelve a leer cada minuto. Si el enlace se renovó o retiró, «Este enlace ya no funciona» |
| `/notes/:token` | Notas de una release para sus clientes, sin cuenta: las escritas o las de sus tickets terminados | | `/notes/:token` | Notas de una release para sus clientes, sin cuenta: las escritas o las de sus tickets terminados |
| `/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 | | `/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 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 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

View File

@@ -69,12 +69,101 @@
// Follow and edit, together, level with the title's first line. // Follow and edit, together, level with the title's first line.
.ts-issue__actions { .ts-issue__actions {
display: flex; display: flex;
flex-shrink: 0; flex-wrap: wrap;
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
margin-top: 0.25rem; margin-top: 0.25rem;
} }
/* «Más»: the ticket's less frequent actions, in a menu. */
/* Laid out as pi-icon-button lays out its own (its styles are scoped). */
.ts-more {
display: flex !important;
gap: 0.5rem;
align-items: center;
mat-icon {
display: flex;
width: 18px;
height: 18px;
}
}
.ts-menu {
display: grid;
width: min(22rem, calc(100vw - 2rem));
padding: 0.375rem;
border: 1px solid var(--ts-line);
border-radius: 0.75rem;
background: var(--ts-surface);
box-shadow: 0 12px 32px rgb(0 0 0 / 14%);
}
/* @pi/ui styles every <button> globally, hence the !important. */
.ts-menu__item {
display: grid !important;
grid-template-columns: 1.25rem minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
width: 100%;
min-width: 0 !important;
height: auto !important;
padding: 0.5rem 0.625rem !important;
border: 0 !important;
border-radius: 0.5rem !important;
background: transparent !important;
color: var(--ts-ink) !important;
font: inherit;
font-size: 0.875rem;
text-align: left;
cursor: pointer;
&:hover,
&:focus,
&.cdk-menu-item-focused {
background: var(--ts-sunken) !important;
outline: none;
}
&:focus-visible {
outline: 2px solid var(--ts-brand);
outline-offset: -2px;
}
pi-icon {
color: var(--ts-ink-soft);
}
}
.ts-menu__text {
display: grid;
gap: 0.125rem;
font-weight: 600;
small {
color: var(--ts-muted);
font-size: 0.75rem;
font-weight: 400;
}
}
.ts-menu__check {
color: var(--ts-brand);
font-weight: 700;
}
/* A note above the ticket (my reminder, what is new). */
.ts-banner {
flex-wrap: wrap;
row-gap: 0.5rem;
}
.ts-banner__actions {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5rem;
}
// Workflow: one button per allowed destination, in that status' colour. // Workflow: one button per allowed destination, in that status' colour.
.ts-transitions { .ts-transitions {
display: flex; display: flex;
@@ -373,26 +462,6 @@ a.ts-key {
} }
} }
/* What others did since my last visit, above the ticket. */
.ts-news {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.625rem;
margin: 0;
padding: 0.5rem 0.875rem;
border-radius: 0.625rem;
background: color-mix(in srgb, var(--ts-brand) 9%, transparent);
font-size: 0.875rem;
}
.ts-news__dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--ts-brand);
}
.ts-comment.is-pointed { .ts-comment.is-pointed {
animation: ts-pointed 2.4s ease-out; animation: ts-pointed 2.4s ease-out;
} }
@@ -827,25 +896,6 @@ a.ts-key {
background: var(--ts-c-green, #2e9e6a); background: var(--ts-c-green, #2e9e6a);
} }
/* My own reminder on the ticket. */
.ts-reminder {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.625rem;
margin: 0;
padding: 0.5rem 0.875rem;
border-radius: 0.625rem;
background: color-mix(in srgb, var(--ts-c-amber) 14%, transparent);
font-size: 0.875rem;
}
.ts-reminder__actions {
display: inline-flex;
gap: 0.25rem;
margin-left: auto;
}
.ts-reminder__choices { .ts-reminder__choices {
display: grid; display: grid;
gap: 0.5rem; gap: 0.5rem;

View File

@@ -16,6 +16,14 @@ import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { form, FormField, required } from '@angular/forms/signals'; import { form, FormField, required } from '@angular/forms/signals';
import { catchError, distinctUntilChanged, filter, of, switchMap } from 'rxjs'; import { catchError, distinctUntilChanged, filter, of, switchMap } from 'rxjs';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import {
CdkMenu,
CdkMenuItem,
CdkMenuItemCheckbox,
CdkMenuTrigger,
} from '@angular/cdk/menu';
import { ConnectedPosition } from '@angular/cdk/overlay';
import { MatIcon } from '@angular/material/icon';
import { import {
PiActionButtonComponent, PiActionButtonComponent,
PiCheckboxComponent, PiCheckboxComponent,
@@ -25,6 +33,7 @@ import {
PiIconComponent, PiIconComponent,
PiLabelDirective, PiLabelDirective,
PiNoteComponent, PiNoteComponent,
PiNoteActionContentDirective,
PiSegmentedControlComponent, PiSegmentedControlComponent,
PiProgressBarComponent, PiProgressBarComponent,
PiSearchableSelectFieldComponent, PiSearchableSelectFieldComponent,
@@ -108,6 +117,11 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
RichEditorComponent, RichEditorComponent,
NgTemplateOutlet, NgTemplateOutlet,
RouterLink, RouterLink,
CdkMenu,
CdkMenuItem,
CdkMenuItemCheckbox,
CdkMenuTrigger,
MatIcon,
FormField, FormField,
AttachmentItem, AttachmentItem,
MediaRecorderComponent, MediaRecorderComponent,
@@ -119,6 +133,7 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
PiIconButtonComponent, PiIconButtonComponent,
PiLabelDirective, PiLabelDirective,
PiNoteComponent, PiNoteComponent,
PiNoteActionContentDirective,
PiSegmentedControlComponent, PiSegmentedControlComponent,
PiProgressBarComponent, PiProgressBarComponent,
PiSearchableSelectFieldComponent, PiSearchableSelectFieldComponent,
@@ -223,79 +238,6 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
(execute)="facade.setWatching(!facade.watching())" (execute)="facade.setWatching(!facade.watching())"
/> />
} }
@if (project.canContribute()) {
<pi-icon-button
icon="eye"
color="grey"
appearance="outline"
text="Pedir revisión"
title="Pedir a alguien del equipo que lo revise"
(execute)="openReviewRequest()"
/>
}
@if (project.myRole() && inMyDay() !== null) {
<pi-icon-button
icon="sun"
color="grey"
appearance="outline"
[text]="inMyDay() ? 'En mi día' : 'Mi día'"
[attr.aria-pressed]="inMyDay()"
[attr.title]="
inMyDay()
? 'Quitar de lo que quiero sacar hoy'
: 'Añadir a lo que quiero sacar hoy (en el inicio)'
"
(execute)="toggleMyDay(issue)"
/>
}
@if (project.myRole()) {
<pi-icon-button
icon="bell"
color="grey"
appearance="outline"
[text]="issue.myReminder ? 'Recordatorio' : 'Recordármelo'"
[attr.aria-pressed]="!!issue.myReminder"
title="Avisarme de este ticket cuando yo diga"
(execute)="openReminder()"
/>
}
@if (
project.myRole() &&
(facade.children().length || issue.followingTree)
) {
<pi-icon-button
icon="kanban"
color="grey"
appearance="outline"
[text]="
issue.followingTree
? 'Siguiendo todo · ' + facade.children().length
: 'Seguir todo'
"
[attr.aria-pressed]="issue.followingTree"
[attr.title]="
issue.followingTree
? 'Dejar de seguir lo que cuelga de este ticket'
: 'Avisarme de este ticket y de todo lo que cuelga de él, también lo que se añada'
"
(execute)="facade.setFollowingTree(!issue.followingTree)"
/>
}
@if (project.myRole()) {
<pi-icon-button
icon="pin"
color="grey"
appearance="outline"
[text]="issue.pinned ? 'Fijado' : 'Fijar'"
[attr.aria-pressed]="issue.pinned"
[attr.title]="
issue.pinned
? 'Quitar de mis fijados'
: 'Tenerlo a mano en el inicio y al buscar'
"
(execute)="facade.setPinned(!issue.pinned)"
/>
}
@if (project.canContribute()) { @if (project.canContribute()) {
<pi-icon-button <pi-icon-button
icon="edit" icon="edit"
@@ -305,64 +247,210 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
title="Editar (tecla E)" title="Editar (tecla E)"
(execute)="openEdit()" (execute)="openEdit()"
/> />
<pi-icon-button
icon="copy"
color="grey"
appearance="outline"
text="Duplicar"
title="Crear otro ticket con lo mismo (sin estado, persona ni comentarios)"
(execute)="duplicate()"
/>
} }
@if (project.myRole()) {
<!-- @pi/ui's look on a native button: the menu needs it. -->
<button
type="button"
class="ts-more has-text pi-button pi-button__appearance--outline pi-button__icon pi-button__medium pi-color__grey pi-color__mode--dark"
[cdkMenuTriggerFor]="moreMenu"
[cdkMenuPosition]="morePositions"
>
<span>Más</span>
<mat-icon svgIcon="chevron-down" aria-hidden="true" />
</button>
}
<ng-template #moreMenu>
<div class="ts-menu" cdkMenu>
@if (project.canContribute()) {
<button
type="button"
class="ts-menu__item"
cdkMenuItem
(cdkMenuItemTriggered)="openReviewRequest()"
>
<pi-icon icon="eye" size="small" />
<span class="ts-menu__text"
>Pedir revisión<small
>A alguien del equipo, que lo verá en «Mi
trabajo»</small
></span
>
</button>
}
@if (inMyDay() !== null) {
<button
type="button"
class="ts-menu__item"
cdkMenuItemCheckbox
[cdkMenuItemChecked]="!!inMyDay()"
(cdkMenuItemTriggered)="toggleMyDay(issue)"
>
<pi-icon icon="sun" size="small" />
<span class="ts-menu__text"
>{{ inMyDay() ? 'En mi día' : 'Añadir a mi día'
}}<small
>Lo que quiero sacar hoy, en el inicio</small
></span
>
@if (inMyDay()) {
<span class="ts-menu__check" aria-hidden="true"
>✓</span
>
}
</button>
}
<button
type="button"
class="ts-menu__item"
cdkMenuItem
(cdkMenuItemTriggered)="openReminder()"
>
<pi-icon icon="bell" size="small" />
<span class="ts-menu__text"
>{{
issue.myReminder
? 'Cambiar el recordatorio'
: 'Recordármelo'
}}<small
>Un aviso solo para mí, cuando yo diga</small
></span
>
</button>
@if (facade.children().length || issue.followingTree) {
<button
type="button"
class="ts-menu__item"
cdkMenuItemCheckbox
[cdkMenuItemChecked]="issue.followingTree"
(cdkMenuItemTriggered)="
facade.setFollowingTree(!issue.followingTree)
"
>
<pi-icon icon="kanban" size="small" />
<span class="ts-menu__text"
>{{
issue.followingTree
? 'Siguiendo todo · ' + facade.children().length
: 'Seguir todo'
}}<small
>Este ticket y lo que cuelga de él, también lo
nuevo</small
></span
>
@if (issue.followingTree) {
<span class="ts-menu__check" aria-hidden="true"
>✓</span
>
}
</button>
}
<button
type="button"
class="ts-menu__item"
cdkMenuItemCheckbox
[cdkMenuItemChecked]="issue.pinned"
(cdkMenuItemTriggered)="facade.setPinned(!issue.pinned)"
>
<pi-icon icon="pin" size="small" />
<span class="ts-menu__text"
>{{ issue.pinned ? 'Fijado' : 'Fijar'
}}<small>A mano en el inicio y al buscar</small></span
>
@if (issue.pinned) {
<span class="ts-menu__check" aria-hidden="true">✓</span>
}
</button>
@if (project.canContribute()) {
<button
type="button"
class="ts-menu__item"
cdkMenuItem
(cdkMenuItemTriggered)="duplicate()"
>
<pi-icon icon="copy" size="small" />
<span class="ts-menu__text"
>Duplicar<small
>Otro ticket con lo mismo, sin estado ni
comentarios</small
></span
>
</button>
}
</div>
</ng-template>
</div> </div>
</div> </div>
</header> </header>
@if (issue.myReminder; as reminder) { @if (issue.myReminder; as reminder) {
<p class="ts-reminder" role="status"> <pi-note
<span aria-hidden="true">⏰</span> class="ts-banner"
Te lo recordaré {{ when(reminder.at) }} gap="xs"
@if (reminder.note) { status="info"
· «{{ reminder.note }}» appearance="fill"
} type="status"
<span class="ts-reminder__actions"> justify="space-between"
<pi-action-button alignment="center"
size="x-small" [title]="'Te lo recordaré ' + when(reminder.at)"
color="grey" [text]="reminder.note ? '«' + reminder.note + '»' : ''"
appearance="outline" >
text="Cambiar" <pi-note-action-content>
(execute)="openReminder()" <span class="ts-banner__actions">
/> <pi-icon-button
<pi-action-button icon="edit"
size="x-small" color="grey"
color="grey" appearance="outline"
appearance="ghost" size="small"
text="Quitar" text="Cambiar"
(execute)="facade.setReminder(null)" (execute)="openReminder()"
/> />
</span> <pi-icon-button
</p> icon="trash"
color="grey"
appearance="outline"
size="small"
text="Quitar"
(execute)="facade.setReminder(null)"
/>
</span>
</pi-note-action-content>
</pi-note>
} }
@if (news(); as news) { @if (news(); as news) {
<p class="ts-news" role="status"> <pi-note
<span class="ts-news__dot" aria-hidden="true"></span> class="ts-banner"
<span gap="xs"
>Desde tu última visita, {{ ago(news.since) }}: status="info"
<strong>{{ news.what }}</strong> de {{ news.who }}.</span appearance="fill"
> type="status"
justify="space-between"
alignment="center"
[title]="
'Desde tu última visita (' +
ago(news.since) +
'): ' +
news.what +
' de ' +
news.who
"
text=""
>
@if (news.firstComment; as first) { @if (news.firstComment; as first) {
<span class="ts-reminder__actions"> <pi-note-action-content>
<pi-action-button <span class="ts-banner__actions">
size="x-small" <pi-action-button
color="grey" size="small"
appearance="outline" color="grey"
text="Ver lo nuevo" appearance="outline"
(execute)="goToNews(first)" text="Ver lo nuevo"
/> (execute)="goToNews(first)"
</span> />
</span>
</pi-note-action-content>
} }
</p> </pi-note>
} }
@if (project.canContribute() && issue.allowedTransitions.length) { @if (project.canContribute() && issue.allowedTransitions.length) {
@@ -1311,11 +1399,11 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
(execute)="pollOptions.set([...pollOptions(), ''])" (execute)="pollOptions.set([...pollOptions(), ''])"
/> />
} }
<pi-action-button <pi-icon-button
size="small" icon="trash"
color="grey" color="grey"
appearance="ghost" appearance="outline"
[hasPadding]="false" size="small"
text="Quitar la encuesta" text="Quitar la encuesta"
(execute)="pollOn.set(false)" (execute)="pollOn.set(false)"
/> />
@@ -2226,6 +2314,23 @@ export class IssuePage {
protected readonly inMyDay = signal<boolean | null>(null); protected readonly inMyDay = signal<boolean | null>(null);
/** Changes with the ticket, not with each reload of it. */ /** Changes with the ticket, not with each reload of it. */
private readonly openIssueId = computed(() => this.facade.issue()?.id); private readonly openIssueId = computed(() => this.facade.issue()?.id);
/** «Más» opens under its button, aligned to its right edge. */
protected readonly morePositions: ConnectedPosition[] = [
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top',
offsetY: 6,
},
{
originX: 'end',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom',
offsetY: -6,
},
];
/* «Lo nuevo desde tu última visita»: what others did since I last came. */ /* «Lo nuevo desde tu última visita»: what others did since I last came. */
private readonly visit = signal<{ private readonly visit = signal<{