diff --git a/src/app/core-ui/main-header/main-header.component.spec.ts b/src/app/core-ui/main-header/main-header.component.spec.ts index dc5063e2a9..810b88366c 100644 --- a/src/app/core-ui/main-header/main-header.component.spec.ts +++ b/src/app/core-ui/main-header/main-header.component.spec.ts @@ -25,6 +25,7 @@ import { MetricService } from '../../features/metric/metric.service'; import { DateService } from '../../core/date/date.service'; import { UserProfileService } from '../../features/user-profile/user-profile.service'; import { DEFAULT_GLOBAL_CONFIG } from '../../features/config/default-global-config.const'; +import { SyncStatus } from '../../op-log/sync-exports'; // Regression test for #7477: in a project view a long title pushed the // right-side header actions (simple-counter / habit buttons) off screen. @@ -166,6 +167,7 @@ describe('MainHeaderComponent focus button visibility', () => { { provide: SyncWrapperService, useValue: { + sync: jasmine.createSpy('sync'), isEnabledAndReady$: of(false), syncState$: of('IN_SYNC'), isSyncInProgress$: of(false), @@ -173,7 +175,13 @@ describe('MainHeaderComponent focus button visibility', () => { superSyncIsConfirmedInSync$: of(false), }, }, - { provide: SnackService, useValue: { open: jasmine.createSpy('open') } }, + { + provide: SnackService, + useValue: { + open: jasmine.createSpy('open'), + hasPendingPersistentAction: jasmine.createSpy('hasPendingPersistentAction'), + }, + }, { provide: Router, useValue: { events: EMPTY } }, { provide: GlobalConfigService, @@ -229,4 +237,19 @@ describe('MainHeaderComponent focus button visibility', () => { expect(component.isFocusButtonVisible()).toBe(false); }); + + it('keeps a persistent recovery action instead of showing routine sync success', async () => { + component = createComponent(); + const syncWrapperService = TestBed.inject( + SyncWrapperService, + ) as jasmine.SpyObj; + const snackService = TestBed.inject(SnackService) as jasmine.SpyObj; + syncWrapperService.sync.and.resolveTo(SyncStatus.UpdateRemote); + snackService.hasPendingPersistentAction.and.returnValue(true); + + component.sync(); + await Promise.resolve(); + + expect(snackService.open).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/core-ui/main-header/main-header.component.ts b/src/app/core-ui/main-header/main-header.component.ts index caceffc7f2..6aa1229246 100644 --- a/src/app/core-ui/main-header/main-header.component.ts +++ b/src/app/core-ui/main-header/main-header.component.ts @@ -294,6 +294,11 @@ export class MainHeaderComponent implements OnDestroy { sync(): void { this.syncWrapperService.sync(true).then((r) => { + // Keep persistent recovery actions (for example USE_REMOTE Undo) visible; + // routine sync-success feedback must not replace them. + if (this._snackService.hasPendingPersistentAction()) { + return; + } if ( r === SyncStatus.UpdateLocal || r === SyncStatus.UpdateRemoteAll || diff --git a/src/app/core/snack/snack.service.ts b/src/app/core/snack/snack.service.ts index 9a5833cb4d..52eb7f0e69 100644 --- a/src/app/core/snack/snack.service.ts +++ b/src/app/core/snack/snack.service.ts @@ -3,7 +3,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Store } from '@ngrx/store'; import { SnackParams } from './snack.model'; import { Observable, Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { take, takeUntil } from 'rxjs/operators'; import { DEFAULT_SNACK_CFG } from './snack.const'; import { SnackCustomComponent } from './snack-custom/snack-custom.component'; import { TranslateService } from '@ngx-translate/core'; @@ -23,6 +23,7 @@ export class SnackService { private _matSnackBar = inject(MatSnackBar); private _ref?: MatSnackBarRef; + private _hasPendingPersistentAction = false; constructor() { const _onWorkContextChange$: Observable = this._actions$.pipe( @@ -37,10 +38,20 @@ export class SnackService { if (typeof params === 'string') { params = { msg: params }; } + if (params.actionStr && params.config?.duration === 0) { + // Set this before the debounced render so immediate follow-up feedback + // cannot unknowingly replace a persistent recovery action. + this._hasPendingPersistentAction = true; + } this._openSnack(params); } + hasPendingPersistentAction(): boolean { + return this._hasPendingPersistentAction; + } + close(): void { + this._hasPendingPersistentAction = false; if (this._ref) { this._ref.dismiss(); } @@ -107,6 +118,17 @@ export class SnackService { } } + const openedRef = this._ref; + this._hasPendingPersistentAction = !!actionStr && cfg.duration === 0; + openedRef + ?.afterDismissed() + .pipe(take(1)) + .subscribe(() => { + if (this._ref === openedRef) { + this._hasPendingPersistentAction = false; + } + }); + if (actionStr && actionId && this._ref) { this._ref .onAction()