0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-13 09:35:13 +02:00

fix(stopwatch): show icon dynamically in tabs loaded before stopwatch starts

The stopwatch navbar icon and popup were only rendered by the server
when a stopwatch was already active at page load.  If a tab was opened
before the stopwatch started, `initStopwatch()` found no
`.active-stopwatch` element in the DOM, returned early, and never
registered a SharedWorker listener.  As a result the WebSocket push
from the stopwatch notifier had nowhere to land and the icon never
appeared.

Fix by always rendering both the icon anchor and the popup skeleton in
the navbar (hidden with `tw-hidden` when no stopwatch is active).
`initStopwatch()` can now set up the SharedWorker in every tab, and
`updateStopwatchData` can call `showElem`/`hideElem` as stopwatch state
changes arrive in real time.

Also add `onShow` to `createTippy` so the popup content is re-cloned
from the (JS-updated) original each time the tooltip opens, keeping
it current even when the stopwatch was started after page load.

Add a new e2e test (`stopwatch appears via real-time push`) that
verifies the icon appears after `apiStartStopwatch` is called with
the page already loaded.
This commit is contained in:
Epid 2026-04-02 02:40:11 +03:00
parent bf7c30e282
commit 9cf7ea8a90
4 changed files with 51 additions and 28 deletions

View File

@ -152,31 +152,29 @@
</div><!-- end full right menu -->
{{$activeStopwatch := and .PageGlobalData (call .PageGlobalData.GetActiveStopwatch)}}
{{if $activeStopwatch}}
<div class="active-stopwatch-popup tippy-target">
<div class="flex-text-block tw-p-3">
<a class="stopwatch-link flex-text-block muted" href="{{$activeStopwatch.IssueLink}}">
{{svg "octicon-issue-opened" 16}}
<span class="stopwatch-issue">{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}</span>
</a>
<div class="tw-flex tw-gap-1">
<form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/stop">
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
>{{svg "octicon-square-fill"}}</button>
</form>
<form class="stopwatch-cancel form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/cancel">
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
>{{svg "octicon-trash"}}</button>
</form>
</div>
<div class="active-stopwatch-popup tippy-target">
<div class="flex-text-block tw-p-3">
<a class="stopwatch-link flex-text-block muted" href="{{if $activeStopwatch}}{{$activeStopwatch.IssueLink}}{{end}}">
{{svg "octicon-issue-opened" 16}}
<span class="stopwatch-issue">{{if $activeStopwatch}}{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}{{end}}</span>
</a>
<div class="tw-flex tw-gap-1">
<form class="stopwatch-commit form-fetch-action" method="post" action="{{if $activeStopwatch}}{{$activeStopwatch.IssueLink}}/times/stopwatch/stop{{end}}">
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
>{{svg "octicon-square-fill"}}</button>
</form>
<form class="stopwatch-cancel form-fetch-action" method="post" action="{{if $activeStopwatch}}{{$activeStopwatch.IssueLink}}/times/stopwatch/cancel{{end}}">
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
>{{svg "octicon-trash"}}</button>
</form>
</div>
</div>
{{end}}
</div>
</nav>
{{template "base/head_banner"}}

View File

@ -3,14 +3,12 @@
{{if and $data $data.IsSigned}}{{/* data may not exist, for example: rendering 503 page before the PageGlobalData middleware */}}
{{- $activeStopwatch := call $data.GetActiveStopwatch -}}
{{- $notificationUnreadCount := call $data.GetNotificationUnreadCount -}}
{{if $activeStopwatch}}
<a class="item active-stopwatch {{$itemExtraClass}}" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}">
<a class="item active-stopwatch{{if not $activeStopwatch}} tw-hidden{{end}} {{$itemExtraClass}}" {{if $activeStopwatch}}href="{{$activeStopwatch.IssueLink}}" data-seconds="{{$activeStopwatch.Seconds}}"{{end}} title="{{ctx.Locale.Tr "active_stopwatch"}}">
<div class="tw-relative">
{{svg "octicon-stopwatch"}}
<span class="header-stopwatch-dot"></span>
</div>
</a>
{{end}}
<a class="item {{$itemExtraClass}}" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}">
<div class="tw-relative">
{{svg "octicon-bell"}}

View File

@ -29,7 +29,7 @@ test.describe('events', () => {
await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]);
});
test('stopwatch', async ({page, request}) => {
test('stopwatch visible at page load', async ({page, request}) => {
const name = `ev-sw-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
const headers = apiUserHeaders(name);
@ -51,6 +51,28 @@ test.describe('events', () => {
await apiDeleteUser(request, name);
});
test('stopwatch appears via real-time push', async ({page, request}) => {
const name = `ev-sw-push-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
const headers = apiUserHeaders(name);
await apiCreateUser(request, name);
await apiCreateRepo(request, {name, headers});
await apiCreateIssue(request, name, name, {title: 'events stopwatch push test', headers});
// Login before starting stopwatch — page loads without active stopwatch
await loginUser(page, name);
const stopwatch = page.locator('.active-stopwatch.not-mobile');
await expect(stopwatch).toBeHidden();
// Start stopwatch after page is loaded — icon should appear via WebSocket push
await apiStartStopwatch(request, name, name, 1, {headers});
await expect(stopwatch).toBeVisible({timeout: 15000});
// Cleanup
await apiDeleteUser(request, name);
});
test('logout propagation', async ({browser, request}) => {
const name = `ev-logout-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;

View File

@ -34,6 +34,11 @@ export function initStopwatch() {
interactive: true,
hideOnClick: true,
theme: 'default',
onShow(instance) {
// Re-clone so the tooltip always reflects the latest stopwatch state,
// even when the icon became visible via a real-time WebSocket push.
instance.setContent(stopwatchPopup.cloneNode(true) as Element);
},
});
}