From 66b09ecf4dfe696c47acb8af1c097077ddc4ce63 Mon Sep 17 00:00:00 2001 From: Josep Piguillem Date: Fri, 9 Oct 2026 13:07:04 +0200 Subject: [PATCH] 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 --- .../application/src/lib/stores/issue.store.ts | 35 ++++++++++++ .../presenters/src/lib/pages/issue.page.scss | 17 +++++- .../presenters/src/lib/pages/issue.page.ts | 54 +++++++++++-------- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/libs/web/tracker/application/src/lib/stores/issue.store.ts b/libs/web/tracker/application/src/lib/stores/issue.store.ts index 1b2356f..1e620af 100644 --- a/libs/web/tracker/application/src/lib/stores/issue.store.ts +++ b/libs/web/tracker/application/src/lib/stores/issue.store.ts @@ -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), diff --git a/libs/web/tracker/presenters/src/lib/pages/issue.page.scss b/libs/web/tracker/presenters/src/lib/pages/issue.page.scss index 2feb3c5..d351991 100644 --- a/libs/web/tracker/presenters/src/lib/pages/issue.page.scss +++ b/libs/web/tracker/presenters/src/lib/pages/issue.page.scss @@ -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; diff --git a/libs/web/tracker/presenters/src/lib/pages/issue.page.ts b/libs/web/tracker/presenters/src/lib/pages/issue.page.ts index 4c6f883..e555df2 100644 --- a/libs/web/tracker/presenters/src/lib/pages/issue.page.ts +++ b/libs/web/tracker/presenters/src/lib/pages/issue.page.ts @@ -136,29 +136,37 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;

{{ issue.title }}

- @if (project.myRole()) { - - } - @if (project.canContribute()) { - - } +
+ @if (project.myRole()) { + + } + @if (project.canContribute()) { + + } +