diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 0bec0a9a7..9695fa9b0 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -99,7 +99,7 @@ jobs: - name: Get npm cache directory id: npm-cache-dir run: | - echo "::set-output name=dir::$(npm config get cache)" + echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - uses: actions/cache@v5 id: npm-cache @@ -125,8 +125,14 @@ jobs: echo "Setting iOS version to $VERSION (build $BUILD_NUMBER) [from $FULL_VERSION]" cd ios/App - xcrun agvtool new-marketing-version "$VERSION" - xcrun agvtool new-version -all "$BUILD_NUMBER" + if ! xcrun agvtool new-marketing-version "$VERSION"; then + echo "Error: Failed to set marketing version to $VERSION" + exit 1 + fi + if ! xcrun agvtool new-version -all "$BUILD_NUMBER"; then + echo "Error: Failed to set build number to $BUILD_NUMBER" + exit 1 + fi - name: Build Angular frontend run: npm run buildFrontend:prodWeb diff --git a/capacitor.config.ts b/capacitor.config.ts index 57b6e3001..b68adb08d 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,7 +1,7 @@ import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { - appId: 'com.superproductivity.superproductivity', + appId: 'com.super-productivity.app', appName: 'Super Productivity', webDir: 'dist/browser', plugins: { diff --git a/e2e/pages/base.page.ts b/e2e/pages/base.page.ts index e53b41039..0a5e28b38 100644 --- a/e2e/pages/base.page.ts +++ b/e2e/pages/base.page.ts @@ -28,45 +28,46 @@ export abstract class BasePage { } /** - * Waits for all overlay backdrops to be removed from the DOM. + * Ensures all overlay backdrops are removed from the DOM before proceeding. * This is critical before interacting with elements that might be blocked by overlays. + * Uses Escape key to dismiss overlays if they don't close naturally. */ - async waitForOverlaysToClose(): Promise { - // Try waiting for overlays to close naturally first - const overlaysClosed = await this.page - .waitForFunction( - () => { - const backdrops = document.querySelectorAll('.cdk-overlay-backdrop'); - return backdrops.length === 0; - }, - { timeout: 3000 }, - ) - .then(() => true) - .catch(() => false); + async ensureOverlaysClosed(): Promise { + const backdrop = this.page.locator('.cdk-overlay-backdrop'); - // If overlays didn't close, press Escape and wait again - if (!overlaysClosed) { - await this.page.keyboard.press('Escape'); - await this.page.waitForTimeout(300); - - // Wait again after pressing Escape - await this.page - .waitForFunction( - () => { - const backdrops = document.querySelectorAll('.cdk-overlay-backdrop'); - return backdrops.length === 0; - }, - { timeout: 3000 }, - ) - .catch(async () => { - // If still not closed, press Escape again and force wait - await this.page.keyboard.press('Escape'); - await this.page.waitForTimeout(500); - }); + // Check if any overlays are present + const count = await backdrop.count(); + if (count === 0) { + return; // No overlays - nothing to do } - // Additional wait for any animations to complete - await this.page.waitForTimeout(200); + // Overlays present - try dismissing with Escape + console.log( + `[ensureOverlaysClosed] Found ${count} overlay(s), attempting to dismiss with Escape`, + ); + await this.page.keyboard.press('Escape'); + + try { + // Wait for backdrop to be removed (uses Playwright's smart waiting) + await backdrop.first().waitFor({ state: 'detached', timeout: 3000 }); + } catch (e) { + // Fallback: try Escape again for stacked overlays + const remaining = await backdrop.count(); + if (remaining > 0) { + console.warn( + `[ensureOverlaysClosed] ${remaining} overlay(s) still present after first Escape, trying again`, + ); + await this.page.keyboard.press('Escape'); + await backdrop + .first() + .waitFor({ state: 'detached', timeout: 2000 }) + .catch(() => { + console.error( + '[ensureOverlaysClosed] Failed to close overlays after multiple attempts', + ); + }); + } + } } async addTask(taskName: string, skipClose = false): Promise { diff --git a/e2e/pages/tag.page.ts b/e2e/pages/tag.page.ts index 10e536ebb..3a287f39b 100644 --- a/e2e/pages/tag.page.ts +++ b/e2e/pages/tag.page.ts @@ -68,11 +68,8 @@ export class TagPage extends BasePage { */ async assignTagToTask(task: Locator, tagName: string): Promise { // Ensure no overlays are blocking before we start - await this.waitForOverlaysToClose(); - - // Exit any edit mode by pressing Escape first - await this.page.keyboard.press('Escape'); - await this.page.waitForTimeout(300); + // Note: This also exits any edit mode + await this.ensureOverlaysClosed(); // Right-click to open context menu await task.click({ button: 'right' }); @@ -116,8 +113,8 @@ export class TagPage extends BasePage { await tagNameInput.waitFor({ state: 'hidden', timeout: 3000 }); } - // Wait for all overlays to close before returning - await this.waitForOverlaysToClose(); + // Wait for all overlays to close to ensure clean state for next operation + await this.ensureOverlaysClosed(); } /** @@ -125,11 +122,8 @@ export class TagPage extends BasePage { */ async removeTagFromTask(task: Locator, tagName: string): Promise { // Ensure no overlays are blocking before we start - await this.waitForOverlaysToClose(); - - // Exit any edit mode by pressing Escape first - await this.page.keyboard.press('Escape'); - await this.page.waitForTimeout(300); + // Note: This also exits any edit mode + await this.ensureOverlaysClosed(); // Right-click to open context menu await task.click({ button: 'right' }); @@ -151,8 +145,8 @@ export class TagPage extends BasePage { await tagOption.waitFor({ state: 'visible', timeout: 3000 }); await tagOption.click(); - // Wait for all overlays to close before returning - await this.waitForOverlaysToClose(); + // Wait for all overlays to close to ensure clean state for next operation + await this.ensureOverlaysClosed(); } /** @@ -221,7 +215,7 @@ export class TagPage extends BasePage { */ async deleteTag(tagName: string): Promise { // Ensure any open menus/overlays are closed before starting - await this.waitForOverlaysToClose(); + await this.ensureOverlaysClosed(); // Ensure Tags section is expanded const tagsGroupBtn = this.tagsGroup @@ -264,7 +258,7 @@ export class TagPage extends BasePage { // Wait for tag to be removed from sidebar await tagTreeItem.waitFor({ state: 'hidden', timeout: 5000 }); - // Wait for all overlays to close before returning - await this.waitForOverlaysToClose(); + // Wait for all overlays to close to ensure clean state for next operation + await this.ensureOverlaysClosed(); } } diff --git a/src/app/features/metric/all-tasks-metrics.service.ts b/src/app/features/metric/all-tasks-metrics.service.ts index 630d58590..23bc720d7 100644 --- a/src/app/features/metric/all-tasks-metrics.service.ts +++ b/src/app/features/metric/all-tasks-metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { combineLatest, from, Observable } from 'rxjs'; import { SimpleMetrics } from './metric.model'; -import { delay, filter, map, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { mapSimpleMetrics } from './metric.util'; import { TaskService } from '../tasks/task.service'; import { WorklogService } from '../worklog/worklog.service'; @@ -29,8 +29,9 @@ export class AllTasksMetricsService { private _simpleMetricsObs$: Observable = this._workContextService.activeWorkContext$.pipe( filter((ctx) => ctx?.type === WorkContextType.TAG && ctx.id === TODAY_TAG.id), - // wait for worklog to load after context switch - delay(100), + // Ensure worklog is loaded before computing metrics + withLatestFrom(this._worklogService.worklog$), + filter(([, worklog]) => !!worklog), switchMap(() => combineLatest([ this._getAllBreakNr$(), diff --git a/src/app/features/planner/planner.service.ts b/src/app/features/planner/planner.service.ts index 66aee232c..bdbca6f25 100644 --- a/src/app/features/planner/planner.service.ts +++ b/src/app/features/planner/planner.service.ts @@ -77,7 +77,7 @@ export class PlannerService { // TODO better solution, gets called very often // tap((val) => Log.log('days$', val)), // tap((val) => Log.log('days$ SIs', val[0]?.scheduledIItems)), - shareReplay(1), + shareReplay({ bufferSize: 1, refCount: true }), ); tomorrow$ = this.days$.pipe( map((days) => { @@ -87,7 +87,7 @@ export class PlannerService { const tomorrowStr = getDbDateStr(tomorrowMs); return days.find((d) => d.dayDate === tomorrowStr) ?? null; }), - shareReplay(1), + shareReplay({ bufferSize: 1, refCount: true }), ); // plannedTaskDayMap$: Observable<{ [taskId: string]: string }> = this._store