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

@@ -301,14 +301,14 @@ 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 |

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)"
[text]="reminder.note ? '«' + reminder.note + '»' : ''"
>
<pi-note-action-content>
<span class="ts-banner__actions">
<pi-icon-button
icon="edit"
color="grey" color="grey"
appearance="outline" appearance="outline"
size="small"
text="Cambiar" text="Cambiar"
(execute)="openReminder()" (execute)="openReminder()"
/> />
<pi-action-button <pi-icon-button
size="x-small" icon="trash"
color="grey" color="grey"
appearance="ghost" appearance="outline"
size="small"
text="Quitar" text="Quitar"
(execute)="facade.setReminder(null)" (execute)="facade.setReminder(null)"
/> />
</span> </span>
</p> </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>
<span class="ts-banner__actions">
<pi-action-button <pi-action-button
size="x-small" size="small"
color="grey" color="grey"
appearance="outline" appearance="outline"
text="Ver lo nuevo" text="Ver lo nuevo"
(execute)="goToNews(first)" (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<{