fix(web): the ticket page shows its own changes at once, and a tidier header

The ticket page only refreshed when the live update arrived; a dropped or
slow socket left an edit, a new subtask or a comment unseen until the
page was reloaded. After each change made there, it now asks for the
ticket, its comments and its subtasks again (twice, since the API
projects its read model a moment after answering).

"Seguir" and "Editar" are two matching buttons, side by side and level
with the title's first line (under it on narrow screens), instead of a
large button with a stray arrow and a loose grey pencil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:07:04 +02:00
parent f6417248c7
commit 66b09ecf4d
3 changed files with 81 additions and 25 deletions

View File

@@ -237,6 +237,41 @@ export const IssueStore = signalStore(
debounceTime(150),
switchMap(({ payload }) => fetchIssue(payload.issueId)),
),
/**
* What this page just changed, shown without waiting for the live
* update, which a dropped socket loses or a slow one delays. The
* API projects its read model a moment after answering, so the
* ticket, its comments and its subtasks are asked for twice.
*/
catchUp$: events
.on(issueApiEvents.commandSucceeded, issueApiEvents.checklistSaved)
.pipe(
map(() => store.issue()),
filter((issue): issue is Issue => !!issue),
switchMap((issue) =>
merge(
...[300, 1500].map((ms) =>
timer(ms).pipe(
switchMap(() =>
merge(
fetchIssue(issue.id),
comments
.list(issue.id)
.pipe(
map((list) => issueApiEvents.commentsLoaded(list)),
),
[
issueRealtimeEvents.childrenTouched({
issueId: issue.id,
}),
],
),
),
),
),
),
),
),
liveComments$: events.on(issuePageEvents.opened).pipe(
switchMap(() => loaded$.pipe(map(({ payload }) => payload.id))),
filter((id, index) => index === 0 || id !== store.issue()?.id),

View File

@@ -47,11 +47,15 @@
.ts-issue__title-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.5rem;
gap: 0.75rem 1rem;
margin-top: 0.75rem;
h1 {
margin: 0.75rem 0 0;
flex: 1 1 20rem;
min-width: 0;
margin: 0;
font-size: clamp(1.625rem, 1.2rem + 1.4vw, 2.375rem);
font-weight: 700;
line-height: 1.1;
@@ -59,6 +63,15 @@
}
}
// Follow and edit, together, level with the title's first line.
.ts-issue__actions {
display: flex;
flex-shrink: 0;
gap: 0.5rem;
align-items: center;
margin-top: 0.25rem;
}
// Workflow: one button per allowed destination, in that status' colour.
.ts-transitions {
display: flex;

View File

@@ -136,29 +136,37 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
</div>
<div class="ts-issue__title-row">
<h1>{{ issue.title }}</h1>
@if (project.myRole()) {
<pi-action-button
size="small"
color="grey"
appearance="outline"
[text]="
facade.watching()
? 'Siguiendo · ' + issue.watchers.length
: 'Seguir'
"
[attr.aria-pressed]="facade.watching()"
(execute)="facade.setWatching(!facade.watching())"
/>
}
@if (project.canContribute()) {
<pi-icon-button
icon="edit"
color="grey"
appearance="ghost"
aria-label="Editar"
(execute)="openEdit()"
/>
}
<div class="ts-issue__actions">
@if (project.myRole()) {
<pi-icon-button
[icon]="facade.watching() ? 'eye-off' : 'eye'"
color="grey"
appearance="outline"
[text]="
facade.watching()
? 'Siguiendo · ' + issue.watchers.length
: 'Seguir'
"
[attr.aria-pressed]="facade.watching()"
[attr.title]="
facade.watching()
? 'Dejar de seguir'
: 'Avisarme de sus cambios'
"
(execute)="facade.setWatching(!facade.watching())"
/>
}
@if (project.canContribute()) {
<pi-icon-button
icon="edit"
color="grey"
appearance="outline"
text="Editar"
title="Editar (tecla E)"
(execute)="openEdit()"
/>
}
</div>
</div>
</header>