Merge branch 'super-productivity:master' into patch-2

This commit is contained in:
Gitoffthelawn 2026-01-21 20:28:10 -08:00 committed by GitHub
commit 663a0ec914
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 1341 additions and 707 deletions

View file

@ -60,7 +60,7 @@ jobs:
run: npm run test
- name: Test E2E
run: npm run e2e
run: npx playwright test --config e2e/playwright.config.ts --grep-invert "@webdav|@supersync"
- name: 'Upload E2E results on failure'
if: ${{ failure() }}

View file

@ -45,6 +45,9 @@ jobs:
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# Allow all PR authors regardless of repository permissions
# This is safe because pull_request_target runs in the base repo context
allowed_non_write_users: '*'
# Allow common dependency management bots to trigger reviews
allowed_bots: 'dependabot[bot],renovate[bot]'
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'

View file

@ -1,3 +1,138 @@
# [17.0.0-RC.13](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.12...v17.0.0-RC.13) (2026-01-21)
### Bug Fixes
- add todo comment to bump CURRENT_SCHEMA_VERSION for upcoming migration ([5002bae](https://github.com/super-productivity/super-productivity/commit/5002bae1c0df36d024f1e7d7978f7dc46bcf595e))
- **android:** show dialog for overdue reminders instead of skipping ([#6068](https://github.com/super-productivity/super-productivity/issues/6068)) ([f784c9c](https://github.com/super-productivity/super-productivity/commit/f784c9c0b9ec7d1219c4df920de06cd750abb596))
- **ci:** allow Dependabot PRs to trigger code review workflow ([01f8c6c](https://github.com/super-productivity/super-productivity/commit/01f8c6cd5fa8226100433b7c07f2ebadb74c7bf6))
- **ci:** allow external contributors to trigger Claude Code review workflow ([623971e](https://github.com/super-productivity/super-productivity/commit/623971eacd4d27175e8898c8e39fa12e1b032e8d))
- **ci:** exclude WebDAV and SuperSync tests from build workflow ([cd5151f](https://github.com/super-productivity/super-productivity/commit/cd5151f4f79bf4897468eff5d8aeb9b580b63af2))
- **ci:** grant write permissions for fork PRs in Claude Code review ([9e7a9cc](https://github.com/super-productivity/super-productivity/commit/9e7a9ccdc9d91f4a4fe344bc887edd85358d36f0))
- conditional issue sections including and add emojis to enhance clarity in feature request template ([dfd6711](https://github.com/super-productivity/super-productivity/commit/dfd671122aa0330bd2928b5dba0693ec99ecb567))
- conditionally include console output field in bug report template ([5fcd96b](https://github.com/super-productivity/super-productivity/commit/5fcd96b7b7ffb51304054931925a0ab4b691dc36))
- **config:** handle undefined state in config selectors ([7dcf0b7](https://github.com/super-productivity/super-productivity/commit/7dcf0b77df6cdcc1ec8b923d1665d543a69af7bc)), closes [#6052](https://github.com/super-productivity/super-productivity/issues/6052)
- correct property name in GlobalConfigService from misc$ to tasks$ ([a27fff8](https://github.com/super-productivity/super-productivity/commit/a27fff8a2ff9e967631d22502e006dc8cd0a6731))
- correct task confirmation field name in migration test ([0d6d17c](https://github.com/super-productivity/super-productivity/commit/0d6d17c103b94629b7a93ac7683f28af5cc5ef0f))
- correct task migration field names and add markdown formatting flag ([5f4e1cf](https://github.com/super-productivity/super-productivity/commit/5f4e1cf24e8376a47bba149ca8610dee946112ab))
- correct typo in isAutoMarkParentAsDone property to isAutMarkParentAsDone ([5540ddf](https://github.com/super-productivity/super-productivity/commit/5540ddf5b284dfd73ae2a8fbac56c7a76cf0eb49))
- **data-repair:** preserve archiveOld separately during repair ([2b5bf17](https://github.com/super-productivity/super-productivity/commit/2b5bf17027cae27c99c3b053ad01140ce7b67351))
- **docker:** add missing shared-schema package to Dockerfile ([b230216](https://github.com/super-productivity/super-productivity/commit/b230216e3394b2635906d6a0848bfd0839c0e444))
- **docs:** resolve markdown linting errors in all wiki files ([73c1848](https://github.com/super-productivity/super-productivity/commit/73c1848ba9f20489a2be7b95e2b49a2696c46267)), closes [#21212863659](https://github.com/super-productivity/super-productivity/issues/21212863659)
- **e2e:** add missing PluginService assertion and fix detection logic ([338727f](https://github.com/super-productivity/super-productivity/commit/338727f4f84bff606fbe9a69fac805f2280c450e))
- **e2e:** add polling for window.ng to prevent intermittent test failures ([05bfd96](https://github.com/super-productivity/super-productivity/commit/05bfd96e5522164192d52fb554c54e0ca644dc71))
- **e2e:** dismiss welcome tour in archive sync test ([90bdfe5](https://github.com/super-productivity/super-productivity/commit/90bdfe54e19da39bdb7d61c1872b109c5cfa7865))
- **e2e:** fix focus-mode test failures and incorrect expectations ([66a0ab8](https://github.com/super-productivity/super-productivity/commit/66a0ab856ed33e2e4e6824599f0c5597d04370ff)), closes [#5995](https://github.com/super-productivity/super-productivity/issues/5995) [#6044](https://github.com/super-productivity/super-productivity/issues/6044)
- **e2e:** fix schedule dialog submit button selector ([9c5704c](https://github.com/super-productivity/super-productivity/commit/9c5704c6c1e0648483e8ad5424208c305c828c2f))
- **e2e:** resolve test failures and improve encryption UX ([054acbd](https://github.com/super-productivity/super-productivity/commit/054acbdf630855dd7f578fb2813da6441b9de6d2))
- **e2e:** wait for dialog close animation in deleteTask helper ([cf31703](https://github.com/super-productivity/super-productivity/commit/cf317036def41545f51cf42cc870622cfeb3fb05))
- **electron:** resolve macOS app quit not responding ([09d86d8](https://github.com/super-productivity/super-productivity/commit/09d86d8afb0e10a7957295908a75da15854897e2))
- **electron:** restore hidden window on taskbar click ([fa8bad6](https://github.com/super-productivity/super-productivity/commit/fa8bad62923a07ae2b4af9e9e046cd3ef47c0c75)), closes [#6042](https://github.com/super-productivity/super-productivity/issues/6042)
- enhance Tasks tab with tooltip and section title ([7f61c63](https://github.com/super-productivity/super-productivity/commit/7f61c638c3f4f6b0b2532a8cd171821c69becb21))
- ensure default tasks configuration is used when tasks are undefined ([5ceb5fd](https://github.com/super-productivity/super-productivity/commit/5ceb5fdb03986e09031adf5368ff82bf527a3e0f))
- **focus-mode:** align manual break cycle calculation with auto-start behavior ([0c85354](https://github.com/super-productivity/super-productivity/commit/0c853549cf4c8bf2918580c582d6d5aea080a5cd)), closes [#6044](https://github.com/super-productivity/super-productivity/issues/6044) [#6044](https://github.com/super-productivity/super-productivity/issues/6044)
- **focus-mode:** do not auto-complete session when manual breaks enabled ([6b28221](https://github.com/super-productivity/super-productivity/commit/6b2822121c68cab9dbc404ad87629358c757cc6f))
- **focus-mode:** get latest isResumingBreak value in effect ([#5995](https://github.com/super-productivity/super-productivity/issues/5995)) ([fb6041c](https://github.com/super-productivity/super-productivity/commit/fb6041c0b60c24762df40dc443c14c0e044f33c9))
- **focus-mode:** long break now occurs after 4th session, not 5th ([cfc3437](https://github.com/super-productivity/super-productivity/commit/cfc3437fd0aa8bc6c3f3b90db2551cd33dc5c838)), closes [#6044](https://github.com/super-productivity/super-productivity/issues/6044)
- **focus-mode:** prevent break skip when resuming from pause ([#5995](https://github.com/super-productivity/super-productivity/issues/5995)) ([b713903](https://github.com/super-productivity/super-productivity/commit/b7139036f740fb099c0626252f68c82d10ac32ca))
- **focus-mode:** prevent taskbar progress bar filling every second ([b77f18f](https://github.com/super-productivity/super-productivity/commit/b77f18f68175baa70d60403242ea784ae720a557)), closes [#6061](https://github.com/super-productivity/super-productivity/issues/6061)
- **focus-mode:** prevent tray indicator jumping during focus sessions ([9f2d2b9](https://github.com/super-productivity/super-productivity/commit/9f2d2b9a6e3472352794a86ad276a69f5aed6d86))
- **focus-mode:** reset break timer on Pomodoro break start ([#6064](https://github.com/super-productivity/super-productivity/issues/6064)) ([548ec8b](https://github.com/super-productivity/super-productivity/commit/548ec8b6cbe21b42d1a5031a8b53f7f25aa276ed))
- **gitignore:** correct screenshots directory path in .gitignore ([ff0acbd](https://github.com/super-productivity/super-productivity/commit/ff0acbdd3702c73565fa6c26807171b5c63cb75e))
- **icons:** add missing calendar icon for ICAL provider ([dee9faa](https://github.com/super-productivity/super-productivity/commit/dee9faad4f702b247ba839f80fd1d9a0278da8dc))
- **icons:** update schedule nav icon from early_on SVG to schedule Material Symbol ([c0fbf5d](https://github.com/super-productivity/super-productivity/commit/c0fbf5ddd8d03de2fa186271824583da5a2e0163))
- increase minimum height of dialog content to improve layout ([d0572ac](https://github.com/super-productivity/super-productivity/commit/d0572ac14c624f558223cc10320c36441ce5cad2))
- **ios:** position add task bar above keyboard ([292337e](https://github.com/super-productivity/super-productivity/commit/292337ed6cab7772f64a52a15aac197f91c89956))
- **ios:** prevent keyboard from overlapping inputs ([1421151](https://github.com/super-productivity/super-productivity/commit/1421151724dbd2c456ddd2f5c740481c6f432dc3))
- **ios:** prevent share overlay from reappearing after dismissal ([5a9f52e](https://github.com/super-productivity/super-productivity/commit/5a9f52ee62e643eca5edb00f6ffb4eb772e494b5))
- **ios:** remove double safe-area padding from bottom navigation ([e942db5](https://github.com/super-productivity/super-productivity/commit/e942db5ade0288d4b476a8512a4fa801766bee00))
- **ios:** remove white frame from app icon by eliminating alpha channel ([f2c1c2a](https://github.com/super-productivity/super-productivity/commit/f2c1c2ab5e6cb57cd3426c77b691f7a67773e0b8))
- **issue-templates:** remove conditional from bug report and feature request templates ([21579be](https://github.com/super-productivity/super-productivity/commit/21579be27d3c4dbe4bef917363d0f18d3cd8ab18))
- **metric:** add validation for logFocusSession operation payload ([9ebf98f](https://github.com/super-productivity/super-productivity/commit/9ebf98ff3c6b654cc1e294fb1110f02e5c1e1ce4))
- **migration:** preload translations before showing dialog ([4de1155](https://github.com/super-productivity/super-productivity/commit/4de11552801ed10022b37f2ae383fd120a92965e))
- **migrations:** ensure unique IDs and prevent data loss in split operations ([be4b8ba](https://github.com/super-productivity/super-productivity/commit/be4b8ba2419149758e026893ea65ef02cf71f9e1))
- **reminders:** clear scheduled time when adding to today from dialog ([286e048](https://github.com/super-productivity/super-productivity/commit/286e04834e24bce5caebaa29a8e6850cfdcc4808))
- **reminders:** clear scheduled time when adding to today from dialog ([853bbcf](https://github.com/super-productivity/super-productivity/commit/853bbcf268537bd9d5ff1f4d03a5407729cb3bb5))
- remove outdated todo comment regarding schema version synchronization ([f2940fd](https://github.com/super-productivity/super-productivity/commit/f2940fd7ae0c2b4150c435512a0fa9ef84351d73))
- rename "Domina Mode" to "Voice Reminder" in en.json. ([1e49f1b](https://github.com/super-productivity/super-productivity/commit/1e49f1beea737c7cfff21986fcb5cfb00491cffd))
- rename defaultTaskNoteTemplate to defaultTaskNotesTemplate for consistency ([e000f25](https://github.com/super-productivity/super-productivity/commit/e000f2568fb2d29202ef6d9c556859fcc6d74283))
- rename isMarkdownFormattingInNotesEnabled to isMarkdownFormattingEnabled for consistency ([7920067](https://github.com/super-productivity/super-productivity/commit/7920067fa4c1a70bf92e2b6b7282d0d4834a9a14))
- replace date formatting with getDbDateStr for consistency in plugin tests ([9294a8b](https://github.com/super-productivity/super-productivity/commit/9294a8b4f3dddd69cf7041ffa0e407f8c580b88b))
- revert CURRENT_SCHEMA_VERSION to 1 ([325e24f](https://github.com/super-productivity/super-productivity/commit/325e24f4611c41d781696b0169620969f007df21))
- **schedule:** fix timezone issues when parsing ISO date strings ([0e13e14](https://github.com/super-productivity/super-productivity/commit/0e13e1452034f00dbe2239d51b33507684dd1cfd))
- **schedule:** force horizontal scrollbar to always be visible ([c3983fb](https://github.com/super-productivity/super-productivity/commit/c3983fbdb2d22a9d28d2f2a1bbed3580a440b63a))
- **schedule:** make horizontal scrollbar always visible at viewport level ([f4d3c61](https://github.com/super-productivity/super-productivity/commit/f4d3c61ec9c7f63d2a9802f81f1615aebaf81367))
- **share:** prevent iOS share sheet from reopening on dismiss ([806dbc2](https://github.com/super-productivity/super-productivity/commit/806dbc2dc3400484cbdef03470c4da78a8436d78))
- **sync:** implement OAuth redirect for Dropbox on mobile ([40b18c4](https://github.com/super-productivity/super-productivity/commit/40b18c469397cbfe4b1117c92d7658fde5d45cc8))
- **sync:** prevent orphaned repeatCfgId during conflict resolution ([0bd1baf](https://github.com/super-productivity/super-productivity/commit/0bd1bafcefdc9eeb9b5dafe153c063d1006e6b09))
- **sync:** prevent SuperSync accessToken overwrite by empty form values ([6dba923](https://github.com/super-productivity/super-productivity/commit/6dba9237e2127a4861d06b8238e036b09a08b264))
- **sync:** restore entity from DELETE payload when UPDATE wins LWW conflict ([86850c7](https://github.com/super-productivity/super-productivity/commit/86850c711a60f09439628030ac0f5ff2d4c713de))
- **sync:** restore missing force upload button in new config UI ([222b347](https://github.com/super-productivity/super-productivity/commit/222b3474b8961d645f56d4a1929836219a65c9d5))
- **tags:** respect menu tree order in tag selection menu ([c4a9a05](https://github.com/super-productivity/super-productivity/commit/c4a9a050552996a7caa590574360c350fa8ca5a8)), closes [#6046](https://github.com/super-productivity/super-productivity/issues/6046)
- **task-view-customizer:** persist sort, group, and filter settings to localStorage ([337afed](https://github.com/super-productivity/super-productivity/commit/337afed4820e8401c08a019feccefd01ad763d2d)), closes [#6095](https://github.com/super-productivity/super-productivity/issues/6095)
- **tasks:** correct spelling of 'isAutoMarkParentAsDone' in configuration and tests ([fe3a7c6](https://github.com/super-productivity/super-productivity/commit/fe3a7c6f0df9ff04b4785ddda2ae8745594383cf))
- **tasks:** correct URL basename extraction for trailing slashes ([22adb1d](https://github.com/super-productivity/super-productivity/commit/22adb1df459bbaa2ed74712d90142f99e1d04e01))
- **tasks:** hide close button in bottom panel on mobile ([94e1550](https://github.com/super-productivity/super-productivity/commit/94e1550227263b95c808305edd8bfdc98a888013))
- **tests:** remove non-existent taskIdsToUnlink from test expectations ([b8d05a2](https://github.com/super-productivity/super-productivity/commit/b8d05a2aa751a2236a341229d9f4ca82bf116dc2))
- update CURRENT_SCHEMA_VERSION to 17 for new migrations ([92d7d4a](https://github.com/super-productivity/super-productivity/commit/92d7d4aafe73b8c59122751d3d52c8eec84c1e20))
- update CURRENT_SCHEMA_VERSION to 2 for upcoming migration ([b3da4e4](https://github.com/super-productivity/super-productivity/commit/b3da4e4850a281187ef6b931b37bd633476e5afc))
- update getMigrations method to accept version range parameters and fix tests ([e8d5dff](https://github.com/super-productivity/super-productivity/commit/e8d5dff3b95687f3e23764eb803e92d8fe7c6856))
- update GlobalConfigService mock to use 'tasks' instead of 'misc' for notes template ([f0e2e12](https://github.com/super-productivity/super-productivity/commit/f0e2e12984ac61f6dcff754a07ba191d328ef090))
- update GlobalConfigService mock to use tasks$ for add-task-bar-spec ([48148a5](https://github.com/super-productivity/super-productivity/commit/48148a5a27922a4c7fd38398b196880b96ad4cc3))
- update globalConfigServiceMock to use tasks$ instead of misc$ for consistency ([778ef2e](https://github.com/super-productivity/super-productivity/commit/778ef2e31d330e932e733c25867c01319269b7cd))
- update incompatible version logic to use CURRENT_SCHEMA_VERSION ([b602993](https://github.com/super-productivity/super-productivity/commit/b602993864d6d748bc7d11b0ca3c2b373b148652))
- update migration test to correctly structure migrated state with globalConfig ([2473b96](https://github.com/super-productivity/super-productivity/commit/2473b9698d2cf6112d3b7526333d0451e7748fb1))
- update migration versions from 16 to 1 and 17 to 2 for consistency ([bd2615e](https://github.com/super-productivity/super-productivity/commit/bd2615e7d76a63e6b35dce0865e20ae2af249da5))
- update MiscConfig to mark isTurnOffMarkdown as deprecated ([a617ff4](https://github.com/super-productivity/super-productivity/commit/a617ff4e29e31c30fb43ef5d6a6d5a354f6359ce))
- update project and task configurations to use 'tasks' instead of 'misc' in tests ([773ca25](https://github.com/super-productivity/super-productivity/commit/773ca2514f7384e33ba2115e1fdd5cb34dc706d0))
- update task confirmation and tray display labels for consistency across languages ([fb6f714](https://github.com/super-productivity/super-productivity/commit/fb6f7142d28b129dfc7a5667ca97ec21bfec674c))
- update tests to reflect current schema version 2 after migration ([aad5cfd](https://github.com/super-productivity/super-productivity/commit/aad5cfd892152abe89355b2e04e346666a5a29f4))
### Features
- add cancel button to schedule task dialog actions ([376675d](https://github.com/super-productivity/super-productivity/commit/376675d2091eac7d5e76980770c6f1f1130b7118))
- add migration to move settings from MiscConfig to TasksConfig ([b565173](https://github.com/super-productivity/super-productivity/commit/b565173664d89028137fb8a71a9facee70a2e6f1))
- add migration to move settings from MiscConfig to TasksConfig as separate file ([651d5dc](https://github.com/super-productivity/super-productivity/commit/651d5dc183e6535e1f494a3e54c98f836e102852))
- **archive:** add batch methods for archive operations ([e43adba](https://github.com/super-productivity/super-productivity/commit/e43adba6185b1c82129e914ca3fd7ad9a2c1ba6f))
- change bottom nav order again ([cfb1c65](https://github.com/super-productivity/super-productivity/commit/cfb1c656dd7bd44627b4f74ff6e1ed6ac8f469df))
- **config-page:** add new Tasks tab with placeholder for task settings ([49923bb](https://github.com/super-productivity/super-productivity/commit/49923bb151c998271d8ab639e1552e4783be6c35))
- **config-page:** add section titles to each tab in settings ([86be687](https://github.com/super-productivity/super-productivity/commit/86be6872bff8c7348edc69209ec16696d825f14a))
- **config-page:** hide tabs labes in 'md' size screens ([d1f5045](https://github.com/super-productivity/super-productivity/commit/d1f5045646e8e7a6ceb3f81022e5b91b00c8dab5))
- **docker:** add curl for healthcheck support in E2E tests ([d9cdbf4](https://github.com/super-productivity/super-productivity/commit/d9cdbf43f2da5a1e1985abe4dd28511de67abc51))
- **e2e:** add npm run e2e:docker:all command for running all E2E tests ([2d49efa](https://github.com/super-productivity/super-productivity/commit/2d49efaf2441f33f5cc53e70dc0422ab49df9fcb))
- **e2e:** enable SuperSync tests in e2e:docker:all script ([3a9d351](https://github.com/super-productivity/super-productivity/commit/3a9d35149dbd2545e2624d76a8a07f4cbd655968))
- enhance migration tests for settings and operations handling ([356278f](https://github.com/super-productivity/super-productivity/commit/356278fc87afdb5446d9e269dacf9ce368f1d19a))
- **focus-mode:** add end focus session button to completion banner ([f8a9347](https://github.com/super-productivity/super-productivity/commit/f8a9347681d4a81a63d2eaa6d1ce8fa5d5fa9645))
- **i18n:** add "Tasks" tab label to English and Russian translations ([19d41c7](https://github.com/super-productivity/super-productivity/commit/19d41c75887a40bce3b564f5f9be7e65db2cfa4b))
- **icons:** upgrade from Material Icons to Material Symbols ([709e688](https://github.com/super-productivity/super-productivity/commit/709e688d6ded88c191bdbe3c0cdae09f525b2336)), closes [#6079](https://github.com/super-productivity/super-productivity/issues/6079)
- implement migration of settings from misc to tasks with operation handling ([218e74f](https://github.com/super-productivity/super-productivity/commit/218e74f88270ebb416c41ca4aa821146bb75ca6f))
- implement migration to move settings from MiscConfig to TasksConfig ([6705033](https://github.com/super-productivity/super-productivity/commit/6705033d154cbb1a1d5f1f5ab774a97d66bfe406))
- **markdown:** move 'isTurnOffMarkdown' setting to tasks configuration and update related components ([4eb6a97](https://github.com/super-productivity/super-productivity/commit/4eb6a97a86bca4b7f95e314fda8789b91fdda7c6))
- **mobile-nav:** open drawer from right side to match button position ([5c851e5](https://github.com/super-productivity/super-productivity/commit/5c851e52d3a78e7ac45c53c2e98f03af08ca5c8f))
- **planner:** implement endless scroll for future days ([c6ceaa5](https://github.com/super-productivity/super-productivity/commit/c6ceaa5f6b70b1b216466f2e500931c27d4f04d1))
- **schedule:** add horizontal scroll for week view on narrow viewports ([7a98831](https://github.com/super-productivity/super-productivity/commit/7a98831835754fdd3c2fdff9488e1828d627ab95))
- **schedule:** add navigation controls with week-aware task filtering ([bda98c9](https://github.com/super-productivity/super-productivity/commit/bda98c954cd5b9d9acba38e9f6792d4bd9d675ec))
- **schedule:** make week view navigation responsive to viewport width ([2392ecb](https://github.com/super-productivity/super-productivity/commit/2392ecb09186509cddb15ab11d031866616d7184))
- **schedule:** restore always 7 days with horizontal scroll for week view ([a35331f](https://github.com/super-productivity/super-productivity/commit/a35331f4ff1feb414cc01e32565aec54a5bd8799))
- **sync:** add comprehensive timeout handling for large operations ([ae40f0b](https://github.com/super-productivity/super-productivity/commit/ae40f0ba2ef7505f1776f53124450c14169fdfd9))
- **tasks:** add URL attachment support in task short syntax ([522ebb3](https://github.com/super-productivity/super-productivity/commit/522ebb39a7c769b257ad371ff0b6b5fd4015dc6f)), closes [#tag](https://github.com/super-productivity/super-productivity/issues/tag) [#6067](https://github.com/super-productivity/super-productivity/issues/6067)
- **tasks:** implement task settings configuration and integrate with global config ([d94ce06](https://github.com/super-productivity/super-productivity/commit/d94ce06ea7cf7491719fd34a80d851f85da8a1e6))
- update localization files to integrate task-related settings ([2e1b48a](https://github.com/super-productivity/super-productivity/commit/2e1b48aebda74f0b03313bb7da6351354fccaee7))
- update migration functions to support splitting operations into multiple results ([263495b](https://github.com/super-productivity/super-productivity/commit/263495b8cd7d13057566c0f4ae8c6dd6686806e3))
### Performance Improvements
- **archive:** optimize bulk archive operations with single load ([269eb99](https://github.com/super-productivity/super-productivity/commit/269eb9952a331829ea4909042b2642f993d2c9e6))
- **e2e:** cache WebDAV health checks at worker level ([867b708](https://github.com/super-productivity/super-productivity/commit/867b7084133ae093e8ab08d409673cd5d93f00d3))
- **e2e:** optimize polling intervals in helpers ([b3ddfcb](https://github.com/super-productivity/super-productivity/commit/b3ddfcbf205f6894711776ebd3a2bb1ad97d3eb7))
- **e2e:** optimize setupSuperSync() wait intervals ([8c62b87](https://github.com/super-productivity/super-productivity/commit/8c62b8731553c38abe3718f2d33ce12758b2d9b8))
- **e2e:** reduce arbitrary delays in tests ([aef7c07](https://github.com/super-productivity/super-productivity/commit/aef7c079216bfdced0a135cccc51221aae43bc31))
- **e2e:** reduce defensive waits after confirmed operations ([b723a63](https://github.com/super-productivity/super-productivity/commit/b723a63cf208905919a28ebf1b7d35839d449e31))
- **e2e:** reduce post-sync settle delay from 300ms to 100ms ([4c73818](https://github.com/super-productivity/super-productivity/commit/4c738186f3012b0b91a8bc372d4d0bc691a1e81f))
- **icons:** implement lazy loading for Material Icons to reduce bundle size ([4317e65](https://github.com/super-productivity/super-productivity/commit/4317e6575d573a3157eb6b310d7eaa2a5953c89f))
- **sync:** add event loop yielding in archive operation handler ([b59aa6b](https://github.com/super-productivity/super-productivity/commit/b59aa6b8f77c0ec88042ebe0d12278eecf7c3109))
- **sync:** parallelize archive task existence checks for bulk updates ([c49209d](https://github.com/super-productivity/super-productivity/commit/c49209d364338914484ebf5bb5f177279be5b862))
- **tests:** use jasmine.clock() to speed up retry tests ([2bcdd52](https://github.com/super-productivity/super-productivity/commit/2bcdd52037316fa955838952069fa7dae8cb6c96))
# [17.0.0-RC.12](https://github.com/super-productivity/super-productivity/compare/v17.0.0-RC.11...v17.0.0-RC.12) (2026-01-18)
### Bug Fixes

View file

@ -43,9 +43,6 @@
</a>
</p>
<br>
<br>
@ -379,9 +376,12 @@ There are several ways to help.
Recently support for Super Productivity has been growing! A big thank you to all our sponsors, especially the ones below!
<p style="font-size:21px; color:black;">Browser testing via
<a href="https://www.lambdatest.com/?utm_source=superproductivity&utm_medium=sponsor" target="_blank">
<img src="https://www.lambdatest.com/blue-logo.png" style="vertical-align: middle;" width="250" height="45" />
<p style="font-size:21px; color:black;">Agentic AI Quality Engineering via
<a href="https://www.testmu.ai/?utm_source=superproductivity&utm_medium=sponsor" target="_blank">
<picture>
<source srcset="https://super-productivity.com/_astro/test-mu-log-dark.Dy0yXuJ7.svg" media="(prefers-color-scheme: dark)" />
<img src="https://super-productivity.com/_astro/test-mu-log-light.CehEzLCt.svg" style="vertical-align: middle;" width="250" height="45" alt="TestMu AI" />
</picture>
</a>
</p>

View file

@ -20,8 +20,8 @@ android {
minSdkVersion 24
targetSdkVersion 35
compileSdk 35
versionCode 17_00_00_0012
versionName "17.0.0-RC.12"
versionCode 17_00_00_0013
versionName "17.0.0-RC.13"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
hostName : "app.super-productivity.com",

View file

@ -13,9 +13,8 @@ const config: CapacitorConfig = {
smallIcon: 'ic_stat_sp',
},
Keyboard: {
// Resize the web view when keyboard appears (iOS)
// Default: resize body (Android)
resize: 'body',
// Style keyboard accessory bar
resizeOnFullScreen: true,
},
StatusBar: {
@ -33,6 +32,15 @@ const config: CapacitorConfig = {
allowsLinkPreview: true,
// Scroll behavior
scrollEnabled: true,
// iOS-specific plugin overrides
plugins: {
Keyboard: {
// Resize the native WebView when keyboard appears
// This shrinks the viewport so 100vh/100% automatically fits above keyboard
resize: 'native',
resizeOnFullScreen: true,
},
},
},
};

View file

@ -1,10 +1,13 @@
# Wiki Documentation Framework
This Wiki is structured based on the [Diátaxis](https://diataxis.fr/) framework for documentation. A more practical description of how to implement it is found at [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#open-edx-diataxis-guide]].
> [!quote]
> The [Diataxis framework](https://diataxis.fr/) is an approach to quality in technical documentation and creates a systematic organization. Diataxis identifies four modes of documentation:
>
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#quickstart]]: teach you how to do something
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#id2]]: tell you what to do to solve a problem or complete a task
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#reference]]: factual, static information
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#concept]]: explain the theory, context, purpose, and/or utility of something
[[Contributing|https://github.com/johannesjo/super-productivity#hearts-contributing]] to Super Productivity
[[Contributing|<https://github.com/johannesjo/super-productivity#hearts-contributin>g]] to Super Productivity

View file

@ -1,31 +1,40 @@
# Wiki Structure and Organization
This Wiki is structured based on the [Diátaxis](https://diataxis.fr/) framework for documentation. A more practical description of how to implement it is found at [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#open-edx-diataxis-guide]].
> The [Diataxis framework](https://diataxis.fr/) is an approach to quality in technical documentation and creates a systematic organization. Diataxis identifies four modes of documentation:
>
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#quickstart]]: teach you how to do something
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#id2]]: tell you what to do to solve a problem or complete a task
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#reference]]: factual, static information
> - [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#concept]]: explain the theory, context, purpose, and/or utility of something
[[Contributing|<https://github.com/johannesjo/super-productivity#hearts-contributin>g]] to Super Productivity
[[Contributing|https://github.com/johannesjo/super-productivity#hearts-contributing]] to Super Productivity
## Planned Structure
### 1. Quickstarts (1.-Quickstarts.md)
#### Getting-Started.md
1.-Quickstarts.md
## Getting-Started.md
Can link to other How_To via anchor links.
2.-How_To.md
Download Instructions
Create Tasks and all other relevant Guides from the dev.to guide
Contribute to the Wiki (describe the basic structure and principles, etc. and then styling)
3. Concepts
- What is a concept? Answers a question.
Make connections to other things.
Provide background and context.
(About) Topic should be implicit.
4. Reference
- Keyboard Shortcuts
- User Data Location
- Known Incompatibilities?
- API
Can link to other How_To via anchor links.
### 2. How To (2.-How_To.md)
- Download Instructions
- Create Tasks and all other relevant Guides from the dev.to guide
- Contribute to the Wiki (describe the basic structure and principles, etc. and then styling)
### 3. Concepts
- What is a concept? Answers a question.
- Make connections to other things.
- Provide background and context.
- (About) Topic should be implicit.
### 4. Reference
- Keyboard Shortcuts
- User Data Location
- Known Incompatibilities?
- API

View file

@ -1,66 +1,74 @@
# Quickstarts
### Getting Started
* Desktop Versus Web Version
* [[Downloading Desktop App|https://github.com/johannesjo/super-productivity#computer-downloads--install]]
* [[Accessing Web Version|https://github.com/johannesjo/super-productivity#globe_with_meridians-web-version]]
* Upgrading
* Uninstalling
## Getting Started
- Desktop Versus Web Version
- [[Downloading Desktop App|<https://github.com/johannesjo/super-productivity#computer-downloads--instal>l]]
- [[Accessing Web Version|<https://github.com/johannesjo/super-productivity#globe_with_meridians-web-versio>n]]
- Upgrading
- Uninstalling
### [[3.-Concepts#Views]]
* The Today Page
* Timeline
* Scheduled Tasks
* Projects
* Tags
- The Today Page
- Timeline
- Scheduled Tasks
- Projects
- Tags
### [[3.-Concepts#Tasks]]
* Adding New Tasks
* Task Attributes
* Projects and Tags
* Estimating Time
* Subtasks
* Scheduled Tasks
* Repeat Tasks
- Adding New Tasks
- Task Attributes
- Projects and Tags
- Estimating Time
- Subtasks
- Scheduled Tasks
- Repeat Tasks
### [[3.-Concepts#Time-Tracking]]
* Starting the Task
* How Time is Logged
* Marking Tasks Complete
* Estimated vs Actual Time
- Starting the Task
- How Time is Logged
- Marking Tasks Complete
- Estimated vs Actual Time
### [[3.-Concepts#Productivity-Helpers]]
* Timers (Pomodoro and Simple)
* Break Reminders
* Idle Time Reminders
- Timers (Pomodoro and Simple)
- Break Reminders
- Idle Time Reminders
### [[3.-Concepts#Projects]]
* Customizing Project Appearance
* Notes
* Bookmarks
* Hiding Projects
- Customizing Project Appearance
- Notes
- Bookmarks
- Hiding Projects
### [[3.-Concepts#Completing-Your-Day]]
* Reflection
* Metrics
* Task Archiving
- Reflection
- Metrics
- Task Archiving
### [[3.-Concepts#Reporting]]
* Quick History
* Worklog
* Metrics
- Quick History
- Worklog
- Metrics
### [[3.-Concepts#Managing-Your-Data]]
* Importing data
* Exporting
* Syncing
* Backups
* Where Data is Stored
- Importing data
- Exporting
- Syncing
- Backups
- Where Data is Stored
### [[3.-Concepts#Integrations-with-Other-Apps]]
* Jira
* GitHub
* GitLab
* OpenProject
- Jira
- GitHub
- GitLab
- OpenProject

View file

@ -4,7 +4,8 @@
### [[1.01-First-Steps]]
Check out *Super Productivity 101* a.k.a. [[1.01-First-Steps|First Steps]] where we will teach you:
Check out _Super Productivity 101_ a.k.a. [[1.01-First-Steps|First Steps]] where we will teach you:
- how to use Super Productivity in the web app.
- how to install the desktop or mobile app.
- how to find other resources once you are done with the basics.
@ -13,28 +14,32 @@ Check out *Super Productivity 101* a.k.a. [[1.01-First-Steps|First Steps]] where
1. [[2.08-Choose-Sync-Backend]]
2. [[2.09-Configure-Sync-Backend]]
1. WebDAV
2. Dropbox
3. Other/Custom
1. WebDAV
2. Dropbox
3. Other/Custom
3. Tweak the Settings in [[3.02-Settings-and-Preferences#sync-and-export.Sync-and-Export]]
4. Ensure data safety by knowing how to [[2.02-Restore-Data-From-Backup]]
### 1.03 Example Workflow 1
- settings used
- project and/or tag usage and naming
- time tracking and focus mode usage
### 1.45 Example Workflow 2
- settings used
- project and/or tag usage and naming
- time tracking and focus mode usage
### 1.34 Example Workflow 3
- settings used
- project and/or tag usage and naming
- time tracking and focus mode usage
# How to Write `Quickstarts`
## How to Write `Quickstarts`
The `1.XX` sections should strive to follow these [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#technical-guidelines]].
By following these recommendations, you will be able to write a good-quality Quickstart:
@ -47,4 +52,4 @@ By following these recommendations, you will be able to write a good-quality Qui
- Every step the learner follows should produce an understandable result, however small.
- Your Quickstart should work for all users every time. This means you must consider different types of devices and software and test your Quickstart once a year to ensure it is updated.
- Be specific about actions and outcomes.
- Explain whats necessary and nothing more. Your guidance must remain focused on achieving the stated learning objective.
- Explain whats necessary and nothing more. Your guidance must remain focused on achieving the stated learning objective.

View file

@ -1,8 +1,11 @@
# First Steps
Welcome!
This series of notes will get you up and running right away so you can see if Super Productivity works for you.
First, walk through the most basic tasks in the Web-version of the App in [[1.01a-First-Steps_Quick-Tour]].
What is covered:
- adding tasks
- editing tasks
- deleting tasks
@ -11,4 +14,4 @@ What is covered:
If you are ready to move up to the Desktop version take a look at [[1.01b-First-Steps_Download-and-Install]].
With the basics sorted out you will likely want to start doing *more*. [[1.01c-First-Steps_Additional-Resources]] will help direct you to solve specific problems or answer questions.
With the basics sorted out you will likely want to start doing _more_. [[1.01c-First-Steps_Additional-Resources]] will help direct you to solve specific problems or answer questions.

View file

@ -1,4 +1,6 @@
### The Basics of 'Super Productivity'
# A First Steps Quick Tour
## The Basics of 'Super Productivity'
Open the [Web App](https://app.super-productivity.com/). This doesn't contain all the features or capabilities of the desktop version but it's more than enough to get started.
@ -13,7 +15,7 @@ Add as many tasks as desired. There is a short-hand syntax available that is sim
![[assets/1.01-First-Steps-finish-task-creation.png]]
*Super Productivity* integrates time tracking as a core feature. It is easy to remove or add time usage later if the idea of a running stopwatch adds too much pressure. Starting and stopping can be done at anytime no matter where you are in the app.
_Super Productivity_ integrates time tracking as a core feature. It is easy to remove or add time usage later if the idea of a running stopwatch adds too much pressure. Starting and stopping can be done at anytime no matter where you are in the app.
![[assets/1.01-First-Steps-start-time-tracking.png]]
@ -50,10 +52,8 @@ The second section is entirely subjective and help track trends of how you feel
![[assets/1.01-First-Steps-finish-day-2.png]]
---
That's it! *Super Productivity* is still undergoing rapid development and supports a wide variety of working styles. With flexibility comes complexity... so focus on the basics of adding and managing tasks and learning to work with the time tracker for now. There are many options and ways to configure the app to accommodate you when that time comes.
That's it! _Super Productivity_ is still undergoing rapid development and supports a wide variety of working styles. With flexibility comes complexity... so focus on the basics of adding and managing tasks and learning to work with the time tracker for now. There are many options and ways to configure the app to accommodate you when that time comes.
See [[1.01b-First-Steps_Download-and-Install]] for more.

View file

@ -1,8 +1,10 @@
# B First Steps Download And Install
See [[1.01a-First-Steps_Quick-Tour]] for the intro that precedes this.
---
### Download and Install
## Download and Install
[[1.01b-First-Steps_Download-and-Install]]
@ -10,4 +12,4 @@ Include additional guidance here as needed. It's possible to just use the Web ap
---
See [[1.01c-First-Steps_Additional-Resources]] for more.
See [[1.01c-First-Steps_Additional-Resources]] for more.

View file

@ -1,3 +1,5 @@
# C First Steps Additional Resources
If you make any errors, you can restore your data from backups. Refer to [[2.02-Restore-Data-From-Backup]].
!TODO: Describe some of these in more detail:
@ -6,4 +8,4 @@ If you make any errors, you can restore your data from backups. Refer to [[2.02-
[[1.02-Configure-Data-Synchronization]]
Example Workflows in the future such as:
[[1.00-Quickstarts#1.03 Example Workflow 1]]
[[1.00-Quickstarts#1.03 Example Workflow 1]]

View file

@ -0,0 +1 @@
# Configure Data Synchronization

View file

@ -1,3 +1,5 @@
# How-To Guides
## Downloads & Install
### All Platforms
@ -8,7 +10,7 @@
Due to certification issues it's recommended to download the app from the Microsoft Store:
<a href='//www.microsoft.com/store/apps/9nhfvg8361tw?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png' alt='English badge' width="127" height="52"/></a>
[![Get it from Microsoft Store](https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png)](https://www.microsoft.com/store/apps/9nhfvg8361tw?cid=storebadge&ocid=badge)
You can also install the app using [Chocolatey](https://community.chocolatey.org/packages/super-productivity):
@ -23,42 +25,45 @@ choco install super-productivity
Install via command-line:
```bash
# stable
## stable
sudo snap install superproductivity
# edge channel releases
## edge channel releases
sudo snap install --channel=edge superproductivity
# it is also recommended to disable updates to the app while it is running:
## it is also recommended to disable updates to the app while it is running:
sudo snap set core experimental.refresh-app-awareness=true
```
<a href="https://snapcraft.io/superproductivity">
<img alt="Get it from the Snap Store" src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg" />
</a>
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/superproductivity)
#### Flatpak - Most distributions
### Flatpak - Most distributions
Must install Flatpak first. See [setup instructions for all distributions](https://flathub.org/setup).
Install via command-line:
```bash
# install
## install
flatpak install flathub com.super_productivity.SuperProductivity
# run
## run
flatpak run com.super_productivity.SuperProductivity
```
<a href='https://flathub.org/apps/com.super_productivity.SuperProductivity'>
<img width='175' alt='Get it on Flathub' src='https://flathub.org/api/badge?locale=en'/>
</a>
[![Get it on Flathub](https://flathub.org/api/badge?locale=en)](https://flathub.org/apps/com.super_productivity.SuperProductivity)
#### Aur - Arch Linux
### Aur - Arch Linux
```bash
git clone https://aur.archlinux.org/superproductivity-bin.git
git clone <https://aur.archlinux.org/superproductivity-bin.git>
cd superproductivity-bin
makepkg -si
```
@ -66,7 +71,7 @@ makepkg -si
#### AppImage
If you encounter problems, please have a look here:
https://github.com/super-productivity/super-productivity/issues/3193#issuecomment-2131315513
<https://github.com/super-productivity/super-productivity/issues/3193#issuecomment-2131315513>
### MacOS
@ -76,11 +81,7 @@ Install via [homebrew cask](https://github.com/caskroom/homebrew-cask):
brew install --cask superproductivity
```
<a href='//apps.apple.com/de/app/super-productivity/id1482572463?l=en&mt=12' target="_blank">
<img src='docs/screens/app-store-badge.svg'
alt='App Store Badge'
height="50" />
</a>
[![App Store Badge](docs/screens/app-store-badge.svg)](https://apps.apple.com/de/app/super-productivity/id1482572463?l=en&mt=12)
### Android
@ -92,29 +93,20 @@ Stay tuned for even more exciting updates!
You can find the Android app here:
<a href='//play.google.com/store/apps/details?id=com.superproductivity.superproductivity' target="_blank">
<img src='docs/screens/google-play-badge.png'
align="center"
alt='App Store Badge'
height="50" />
</a>
<a href='//f-droid.org/en/packages/com.superproductivity.superproductivity' target="_blank">
<img src='https://f-droid.org/assets/fdroid-logo-text_S0MUfk_FsnAYL7n2MQye-34IoSNm6QM6xYjDnMqkufo=.svg'
align="center"
alt='F-Droid Badge'
height="50" />
</a>
[![Google Play Badge](docs/screens/google-play-badge.png)](https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity)
[![F-Droid Badge](https://f-droid.org/assets/fdroid-logo-text_S0MUfk_FsnAYL7n2MQye-34IoSNm6QM6xYjDnMqkufo=.svg)](https://f-droid.org/en/packages/com.superproductivity.superproductivity)
The sources can be [[../android|found here]].
#### Restore Data From Backup
##### Standard Method
###### Restore Data From Backup
**Standard Method**
The backup path should be shown under settings and then "Automatic Backups". You can then import the backup under "Import / Export".
**Alternative Method: Pre-Clearing Application**
##### Alternative Method: Pre-Clearing Application
In case the app does not properly start (e.g. [inconsistent task state](https://github.com/johannesjo/super-productivity/issues/3052)), the data must be cleared first:
1. locate the backup and possibly make another copy of it (not strictly necessary, but can't hurt :))
@ -123,4 +115,4 @@ In case the app does not properly start (e.g. [inconsistent task state](https://
4. go to application/storage
5. hit clear site data
6. hit strg+r to reload the app
7. within SP you go to settings and import the previously located backup
7. within SP you go to settings and import the previously located backup

View file

@ -18,6 +18,7 @@ For all platforms, you can always install [[../../releases|from the releases pag
## Tasks
### [[2.03-Add-Tasks]]
Include all the actions that branch out from this such as subtasks, scheduling, repetition, and integrations via [[4.00-Concepts#Integrations-with-Other-Apps]].
### [[2.04-Manage-Subtasks]]
@ -27,28 +28,34 @@ Include all the actions that branch out from this such as subtasks, scheduling,
### [[2.06-Manage-Repeating-Tasks]]
Time-Tracking
- Starting the Task
- Marking Tasks Complete
- Estimated vs Actual Time
Projects and Tags
- Customizing Project Appearance
- Hiding Projects
- Use Tags
Complete the Day
- How to Complete the Day
Managing Your Data
- Importing data
- Exporting
- Syncing
- [[2.02-Restore-Data-From-Backup]]
Integrations
- How to configure for each
# How to Write a `How To`
## How to Write a `How To`
The `2.XX` sections should strive to follow these [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#id3]].
There is an assumed "How to..." preceding `2.XX` note titles.
@ -59,4 +66,4 @@ By following these recommendations, you will be able to write good quality how-t
- Solve a particular task. The problem or task is the concern of a how-to guide: stick to that practical goal.
- Do not explain concepts—link to other documents for further explanation.
- Omit the unnecessary. Practical usability is more helpful than completeness.
- Pay attention to naming. Choose action-based titles that say precisely what the how-to guide shows, such as “Import A Course” or “Copy And Paste Course Content.”
- Pay attention to naming. Choose action-based titles that say precisely what the how-to guide shows, such as “Import A Course” or “Copy And Paste Course Content.”

View file

@ -1,9 +1,11 @@
# A Download And Install Windows
Due to certification issues it's recommended to download the app from the Microsoft Store:
<a href='//www.microsoft.com/store/apps/9nhfvg8361tw?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png' alt='English badge' width="127" height="52"/></a>
[![Get it from Microsoft Store](https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png)](https://www.microsoft.com/store/apps/9nhfvg8361tw?cid=storebadge&ocid=badge)
You can also install the app using [Chocolatey](https://community.chocolatey.org/packages/super-productivity):
```powershell
choco install super-productivity
```
```

View file

@ -1,44 +1,49 @@
#### Snap - Most distributions
# B Download And Install Linux
## Snap - Most distributions
Install via command-line:
```bash
# stable
## stable
sudo snap install superproductivity
# edge channel releases
## edge channel releases
sudo snap install --channel=edge superproductivity
# it is also recommended to disable updates to the app while it is running:
## it is also recommended to disable updates to the app while it is running:
sudo snap set core experimental.refresh-app-awareness=true
```
<a href="https://snapcraft.io/superproductivity">
<img alt="Get it from the Snap Store" src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg" />
</a>
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/superproductivity)
#### Flatpak - Most distributions
### Flatpak - Most distributions
Must install Flatpak first. See [setup instructions for all distributions](https://flathub.org/setup).
Install via command-line:
```bash
# install
## install
flatpak install flathub com.super_productivity.SuperProductivity
# run
## run
flatpak run com.super_productivity.SuperProductivity
```
<a href='https://flathub.org/apps/com.super_productivity.SuperProductivity'>
<img width='175' alt='Get it on Flathub' src='https://flathub.org/api/badge?locale=en'/>
</a>
[![Get it on Flathub](https://flathub.org/api/badge?locale=en)](https://flathub.org/apps/com.super_productivity.SuperProductivity)
#### Aur - Arch Linux
### Aur - Arch Linux
```bash
git clone https://aur.archlinux.org/superproductivity-bin.git
git clone <https://aur.archlinux.org/superproductivity-bin.git>
cd superproductivity-bin
makepkg -si
```
@ -46,4 +51,4 @@ makepkg -si
#### AppImage
If you encounter problems, please have a look here:
https://github.com/super-productivity/super-productivity/issues/3193#issuecomment-2131315513
<https://github.com/super-productivity/super-productivity/issues/3193#issuecomment-2131315513>

View file

@ -1,4 +1,6 @@
### MacOS
# C Download And Install Macos
## MacOS
Install via [homebrew cask](https://github.com/caskroom/homebrew-cask):
@ -6,8 +8,4 @@ Install via [homebrew cask](https://github.com/caskroom/homebrew-cask):
brew install --cask superproductivity
```
<a href='//apps.apple.com/de/app/super-productivity/id1482572463?l=en&mt=12' target="_blank">
<img src='docs/screens/app-store-badge.svg'
alt='App Store Badge'
height="50" />
</a>
[![App Store Badge](docs/screens/app-store-badge.svg)](https://apps.apple.com/de/app/super-productivity/id1482572463?l=en&mt=12)

View file

@ -1,3 +1,5 @@
# D Download And Install Android
A new version of the Android app is now available with **Connectivity-Free Mode**, allowing you to use the app without an internet connection.
This update offers more flexibility, supporting both fully offline usage and integration with services like WebDAV and Dropbox for syncing. Enjoy a smoother, more reliable experience whether you're online or offline.
@ -6,17 +8,8 @@ Stay tuned for even more exciting updates!
You can find the Android app here:
<a href='//play.google.com/store/apps/details?id=com.superproductivity.superproductivity' target="_blank">
<img src='docs/screens/google-play-badge.png'
align="center"
alt='App Store Badge'
height="50" />
</a>
<a href='//f-droid.org/en/packages/com.superproductivity.superproductivity' target="_blank">
<img src='https://f-droid.org/assets/fdroid-logo-text_S0MUfk_FsnAYL7n2MQye-34IoSNm6QM6xYjDnMqkufo=.svg'
align="center"
alt='F-Droid Badge'
height="50" />
</a>
[![Google Play Badge](docs/screens/google-play-badge.png)](https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity)
The sources can be [[../../android|found here]].
[![F-Droid Badge](https://f-droid.org/assets/fdroid-logo-text_S0MUfk_FsnAYL7n2MQye-34IoSNm6QM6xYjDnMqkufo=.svg)](https://f-droid.org/en/packages/com.superproductivity.superproductivity)
The sources can be [[../../android|found here]].

View file

@ -1,7 +1,11 @@
**Standard Method**
# Restore Data From Backup
## Standard Method
The backup path should be shown under settings and then "Automatic Backups". You can then import the backup under "Import / Export".
**Alternative Method: Pre-Clearing Application**
## Alternative Method: Pre-Clearing Application
In case the app does not properly start (e.g. [inconsistent task state](https://github.com/johannesjo/super-productivity/issues/3052)), the data must be cleared first:
1. locate the backup and possibly make another copy of it (not strictly necessary, but can't hurt :))
@ -10,4 +14,4 @@ In case the app does not properly start (e.g. [inconsistent task state](https://
4. go to application/storage
5. hit clear site data
6. hit strg+r to reload the app
7. within SP you go to settings and import the previously located backup
7. within SP you go to settings and import the previously located backup

View file

@ -1,6 +1,6 @@
# Add Tasks
[[2.04-Manage-Subtasks]]
[[2.05-Manage-Scheduled-Tasks]]
[[2.06-Manage-Repeating-Tasks]]
[[2.07-Manage-Task-Integrations]]
[[2.07-Manage-Task-Integrations]]

View file

@ -0,0 +1 @@
# Manage Subtasks

View file

@ -0,0 +1 @@
# Manage Scheduled Tasks

View file

@ -0,0 +1 @@
# Manage Repeating Tasks

View file

@ -0,0 +1 @@
# Manage Task Integrations

View file

@ -1 +1,3 @@
Stub.
# Choose Sync Backend
Stub.

View file

@ -1,3 +1,5 @@
# Configure Sync Backend
- 2.09a: WebDAV
- 2.09b: Dropbox
- 2.09c: Other/Custom
- 2.09c: Other/Custom

View file

@ -1,67 +1,74 @@
# Concepts
## Views
### Views
* The Today Page
* Timeline
* Scheduled Tasks
* Projects
* Tags
- The Today Page
- Timeline
- Scheduled Tasks
- Projects
- Tags
### Tasks
* Adding New Tasks
* Task Attributes
* Projects and Tags
* Estimating Time
* Subtasks
* Scheduled Tasks
* Repeat Tasks
- Adding New Tasks
- Task Attributes
- Projects and Tags
- Estimating Time
- Subtasks
- Scheduled Tasks
- Repeat Tasks
### Time-Tracking
* Starting the Task
* How Time is Logged
* Marking Tasks Complete
* Estimated vs Actual Time
- Starting the Task
- How Time is Logged
- Marking Tasks Complete
- Estimated vs Actual Time
### Productivity-Helpers
* Timers (Pomodoro and Simple)
* Break Reminders
* Idle Time Reminders
- Timers (Pomodoro and Simple)
- Break Reminders
- Idle Time Reminders
### Projects
* Customizing Project Appearance
* Notes
* Bookmarks
* Hiding Projects
- Customizing Project Appearance
- Notes
- Bookmarks
- Hiding Projects
### Completing-Your-Day
* Reflection
* Metrics
* Task Archiving
- Reflection
- Metrics
- Task Archiving
### Reporting
* Quick History
* Worklog
* Metrics
- Quick History
- Worklog
- Metrics
### Managing-Your-Data
* Importing data
* Exporting
* Syncing
* Backups
* Where Data is Stored
- Importing data
- Exporting
- Syncing
- Backups
- Where Data is Stored
### Integrations-with-Other-Apps
* Jira
* GitHub
* GitLab
* OpenProject
- Jira
- GitHub
- GitLab
- OpenProject
### Miscellaneous
* [[Settings and Preferences]]
* [[Keyboard Shortcuts]]
* [[Upgrading]]
* [[Uninstalling]]
* [[JSON / Models]]
- [[Settings and Preferences]]
- [[Keyboard Shortcuts]]
- [[Upgrading]]
- [[Uninstalling]]
- [[JSON / Models]]

View file

@ -7,18 +7,12 @@
[[3.03-Keyboard-Shortcuts]]
[[3.04-Short-Syntax]]
# How to Write `Reference Material`
## How to Write `Reference Material`
The `3.XX` sections should strive to follow these [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#id5]].
- Do nothing but describe. References have one job: **to explain** and do that **accurately and comprehensively**.
- **Be accurate.** These descriptions must be accurate and kept up-to-date.
- **Provide examples.** It is a valuable way of providing illustrations that help readers understand the references without becoming distracted from the job of describing them.
- Do nothing but describe. References have one job: **to explain** and do that **accurately and comprehensively**.
- **Be accurate.** These descriptions must be accurate and kept up-to-date.
- **Provide examples.** It is a valuable way of providing illustrations that help readers understand the references without becoming distracted from the job of describing them.
- **The documentation structure should mirror the products structure** so the user can work their way through it simultaneously. It doesnt mean forcing the documentation into an unnatural structure. Whats important is that the documentation should help make sense of the product.
- **Be consistent** in structure, language, terminology, and tone.

View file

@ -1 +1,3 @@
Undocumented.
# Api
Undocumented.

View file

@ -1,29 +1,36 @@
# Settings And Preferences
Note: All settings will be shown with the default values.
Note: Differences between the Web app and the Desktop app will be specified.
### Global Settings
## Global Settings
### global-settings.Localization
#### global-settings.Localization
#### global-settings.App-Features
#### global-settings.Misc-Settings
#### global-settings.Short-Syntax
#### global-settings.Idle-Handling
#### global-settings.Keyboard-Shortcuts
See [[3.03-Keyboard-Shortcuts]] for full list.
#### global-settings.Time-Tracking
#### global-settings.Reminders
#### global-settings.Schedule
#### global-settings.Sound
#### global-settings.Reminders
#### global-settings.Schedule
#### global-settings.Sound
### Plugins
#### plugins.Plugins
### Productivity Helper
#### productivity-helper.Focus-Mode
@ -36,7 +43,6 @@ See [[3.03-Keyboard-Shortcuts]] for full list.
#### productivity-helper.Domina-Mode
### Sync & Export
#### sync-and-export.Sync
@ -44,4 +50,3 @@ See [[3.03-Keyboard-Shortcuts]] for full list.
#### sync-and-export.Sync-and-Export
#### sync-and-export.Sync-Safety-Backups

View file

@ -1,5 +1,7 @@
# Keyboard Shortcuts
## Global Shortcuts
### Global Shortcuts
These work everywhere in the app.
- `Shift`+`A`: Open add task bar
@ -22,6 +24,7 @@ These work everywhere in the app.
- `Ctrl`+`S`: Trigger sync (if configured)
### Tasks
The following shortcuts apply for the currently selected task (selected via tab or mouse).
- `<no default binding>`: Edit Title
@ -47,6 +50,7 @@ The following shortcuts apply for the currently selected task (selected via tab
- `<no default binding>`: Collapse Sub Tasks
- `Y`: Toggle tracking time to currently focused task
**Unconfigurable**
#### Unconfigurable
- `Arrow keys`: Navigate around task list
- `ArrowRight`: Open additional info panel for currently focused task
- `ArrowRight`: Open additional info panel for currently focused task

View file

@ -1,11 +1,21 @@
# Short Syntax
Can be used when adding a task. Each of these can be disabled in [[3.02-Settings-and-Preferences#global-settings.Short-Syntax]].
- `# <tag-name>`: add a tag to the task
- `# <tag-name>`: add a tag to the task
(`"task-description #tag1"`)
- `<number>m` or `<number>h`: set time-estimate for the task
- `<number>m` or `<number>h`: set time-estimate for the task
(`"task-description 10m"` or `"task-description 5h"`)
- `@<time>`: add due time to the task
(`"task-description @fri 10pm"`)
- `+ <project-name>`: add the task to an existing project
- `+ <project-name>`: add the task to an existing project
(`"task-description +Important Project"`)
- `Ctr + 2`: toggle between moving the new task to the bottom and top of the list
- `Ctr + 2`: toggle between moving the new task to the bottom and top of the list

View file

@ -1,3 +1,5 @@
# Other
## Web-App-Limitations
Time tracking only works if the app is open and for idle time tracking to work, the chrome extension must be installed.
@ -22,4 +24,4 @@ This will automatically apply compatibility fixes including:
- Disabling GPU VSync to prevent GetVSyncParametersIfAvailable() errors
- Setting the appropriate environment variables for X11 compatibility
The application will automatically detect Wayland sessions and apply these fixes, but you can use this flag if automatic detection doesn't work properly.
The application will automatically detect Wayland sessions and apply these fixes, but you can use this flag if automatic detection doesn't work properly.

View file

@ -1,7 +1,8 @@
# Reference
Settings and Preferences
### Keyboard-Shortcuts
## Keyboard-Shortcuts
- `Shift`+`P`: Open create project dialog
- `Shift`+`A`: Open add task bar
@ -18,21 +19,28 @@ Settings and Preferences
### Short-Syntax
Can be used when adding a task. <strong>(Each of these can be disabled in settings->short syntax)</strong>
Can be used when adding a task. **(Each of these can be disabled in settings->short syntax)**
- `# <tag-name>`: add a tag to the task
- `# <tag-name>`: add a tag to the task
(`"task-description #tag1"`)
- `<number>m` or `<number>h`: set time-estimate for the task
- `<number>m` or `<number>h`: set time-estimate for the task
(`"task-description 10m"` or `"task-description 5h"`)
- `@<time>`: add due time to the task
(`"task-description @fri 10pm"`)
- `+ <project-name>`: add the task to an existing project
- `+ <project-name>`: add the task to an existing project
(`"task-description +Important Project"`)
- `Ctr + 2`: toggle between moving the new task to the bottom and top of the list
### JSON-Models
### Wayland-Compatibility
If you're experiencing issues running Super Productivity on Wayland (such as rendering problems, VSync errors, or GLib-GObject warnings), you can force the application to use X11 mode by starting it with the `--force-x11` parameter:
@ -47,4 +55,4 @@ This will automatically apply compatibility fixes including:
- Disabling GPU VSync to prevent GetVSyncParametersIfAvailable() errors
- Setting the appropriate environment variables for X11 compatibility
The application will automatically detect Wayland sessions and apply these fixes, but you can use this flag if automatic detection doesn't work properly.
The application will automatically detect Wayland sessions and apply these fixes, but you can use this flag if automatic detection doesn't work properly.

View file

@ -1,6 +1,7 @@
# Index of `Concepts`
## Organizing
- Projects
- Tags
- Estimating Time
@ -10,10 +11,12 @@
- Estimated vs Actual Time
## Planning
- Timeline
- Scheduled Tasks
## Doing
- The Today Page
- Timers/Focus Mode
- Break Reminders
@ -23,10 +26,12 @@
## Reviewing
### Time-Tracking
- How Time is Logged
- Estimated vs Actual Time
### Completing-Your-Day
- Reflection
- Metrics
- Task Archiving
@ -40,19 +45,22 @@
- Where Data is Stored
## Integrations-with-Other-Apps
- Jira
- GitHub
- GitLab
- OpenProject
### Miscellaneous
- [[3.02-Settings-and-Preferences]]
- [[3.03-Keyboard-Shortcuts]]
- Upgrading
- Uninstalling
- [[3.99-Other#JSON-Models]]
# How to Write a `Concept`
## How to Write a `Concept`
The `4.XX` sections should strive to follow these [[https://docs.openedx.org/en/open-release-sumac.master/documentors/concepts/content_types.html#id7]].
There is an assumed "About..." preceding `4.XX` note titles.
@ -63,4 +71,4 @@ By following these recommendations, you will be able to write good quality how-t
- Solve a particular task. The problem or task is the concern of a how-to guide: stick to that practical goal.
- Do not explain concepts—link to other documents for further explanation.
- Omit the unnecessary. Practical usability is more helpful than completeness.
- Pay attention to naming. Choose action-based titles that say precisely what the how-to guide shows, such as “Import A Course” or “Copy And Paste Course Content.”
- Pay attention to naming. Choose action-based titles that say precisely what the how-to guide shows, such as “Import A Course” or “Copy And Paste Course Content.”

View file

@ -1,11 +1,14 @@
## New Users: Start Here!
# Home
## New Users: Start Here
Check out [[1.01-First-Steps]] where we will teach you:
- how to use Super Productivity in the web app.
- how to install the desktop or mobile app.
- how to find other resources once you are done with the basics.
## Devs & Contributors: Start Here!
## Devs & Contributors: Start Here
The wiki is not yet set up to handle all the dev-related documentation. For now, here are some relevant links to sections within the README:
@ -13,4 +16,4 @@ The wiki is not yet set up to handle all the dev-related documentation. For now,
[[https://github.com/super-productivity/super-productivity#running-the-development-server]]
[[https://github.com/super-productivity/super-productivity#run-as-docker-container]]
[[https://github.com/super-productivity/super-productivity#run-as-docker-container]]

View file

@ -1 +1,3 @@
Footer. Any valid markdown can go here.
# Footer
Footer. Any valid markdown can go here.

View file

@ -1 +1,3 @@
Sidebar. Any valid markdown can go here.
# Sidebar
Sidebar. Any valid markdown can go here.

1
e2e/.gitignore vendored
View file

@ -1 +1,2 @@
start-test-server.shscreenshots/
/screenshots/

View file

@ -0,0 +1,85 @@
import { test as base } from '@playwright/test';
import { isWebDavServerUp } from '../utils/check-webdav';
/**
* Extended test fixture for WebDAV E2E tests.
*
* Provides:
* - Automatic server health check (skips tests if server unavailable)
* - Worker-level caching to avoid redundant health checks
*
* This reduces health check overhead from ~8s to ~2s when WebDAV is unavailable
* by checking once per worker instead of once per test file.
*
* Usage:
* ```typescript
* import { test, expect } from '../../fixtures/webdav.fixture';
*
* test.describe('@webdav My Tests', () => {
* test('should sync', async ({ page, request }) => {
* // Server health already checked, test will skip if unavailable
* });
* });
* ```
*/
export interface WebDavFixtures {
/** Whether the WebDAV server is reachable and available */
webdavServerUp: boolean;
}
// Cache server health check result per worker to avoid repeated checks
let serverHealthCache: boolean | null = null;
export const test = base.extend<WebDavFixtures>({
/**
* Check WebDAV server health once per worker and cache the result.
* Tests are automatically skipped if the server is not reachable.
*/
webdavServerUp: async ({}, use, testInfo) => {
// Only check once per worker
if (serverHealthCache === null) {
serverHealthCache = await isWebDavServerUp();
if (!serverHealthCache) {
console.warn(
'WebDAV server not reachable at http://127.0.0.1:2345/ - skipping tests',
);
}
}
// Skip the test if server is not reachable
testInfo.skip(!serverHealthCache, 'WebDAV server not reachable');
await use(serverHealthCache);
},
});
/**
* Helper to create a describe block that auto-checks WebDAV server health.
* Use this instead of manually adding beforeAll health checks.
*
* @param title - Test suite title (will be prefixed with @webdav)
* @param fn - Test suite function
*
* @example
* ```typescript
* webdavDescribe('Archive Sync', () => {
* test('should sync archive', async ({ page, webdavServerUp }) => {
* // Server health already checked
* });
* });
* ```
*/
export const webdavDescribe = (title: string, fn: () => void): void => {
test.describe(`@webdav ${title}`, () => {
// The webdavServerUp fixture will auto-skip if server unavailable
test.beforeEach(async ({ webdavServerUp }) => {
// This line ensures the fixture is evaluated and test is skipped if needed
void webdavServerUp;
});
fn();
});
};
// Re-export expect for convenience
export { expect } from '@playwright/test';

View file

@ -2,53 +2,25 @@ import { test, expect } from '../../fixtures/test.fixture';
test.describe.serial('Plugin Feature Check', () => {
test('check if PluginService exists', async ({ page, workViewPage }) => {
// Wait for work view to be ready
// Wait for Angular app to be fully loaded
await workViewPage.waitForTaskList();
const result = await page.evaluate(() => {
// Check if Angular is loaded
const hasAngular = !!(window as any).ng;
// Navigate to config/plugin management
await page.goto('/#/config');
// Check if PluginService is accessible through Angular's injector
let hasPluginService = false;
let errorMessage = '';
// Click on the Plugins tab to show plugin management
const pluginsTab = page.getByRole('tab', { name: 'Plugins' });
await pluginsTab.click();
try {
if (hasAngular) {
const ng = (window as any).ng;
const appElement = document.querySelector('app-root');
if (appElement) {
try {
// Get the component and its injector
const component = ng.getComponent?.(appElement);
// Verify plugin management component exists (proves PluginService is loaded)
// The plugin-management component requires PluginService to be injected and functional
const pluginMgmt = page.locator('plugin-management');
await expect(pluginMgmt).toBeAttached({ timeout: 10000 });
if (component) {
// If Angular is fully loaded with app-root component,
// all root-level services (providedIn: 'root') are guaranteed to exist
// This includes PluginService
hasPluginService = true;
}
} catch (e: any) {
errorMessage = e.toString();
}
}
}
} catch (e: any) {
errorMessage = e.toString();
}
return {
hasAngular,
hasPluginService,
errorMessage,
};
});
// console.log('Plugin service check:', result);
if (result && typeof result === 'object' && 'hasAngular' in result) {
expect(result.hasAngular).toBe(true);
expect(result.hasPluginService).toBe(true);
}
// Additional verification: check that plugin management has rendered content
// This confirms the service is not only loaded but also working correctly
const pluginCards = pluginMgmt.locator('mat-card');
await expect(pluginCards.first()).toBeVisible({ timeout: 10000 });
});
test('check plugin UI elements in DOM', async ({ page, workViewPage }) => {

View file

@ -154,7 +154,13 @@ test.describe('Plugin Lifecycle', () => {
// Verify we navigated to the plugin page
await expect(page).toHaveURL(/\/plugins\/api-test-plugin\/index/, { timeout: 10000 });
await expect(page.locator('iframe')).toBeVisible({ timeout: 10000 });
// Wait for Angular component initialization after navigation
await expect(async () => {
const iframe = page.locator('iframe');
await expect(iframe).toBeAttached({ timeout: 2000 });
await expect(iframe).toBeVisible({ timeout: 2000 });
}).toPass({ timeout: 10000, intervals: [500, 1000] });
// Go back to work view
await page.goto('/#/tag/TODAY');

View file

@ -636,7 +636,9 @@ test.describe('@supersync SuperSync Edge Cases', () => {
// 4. Client A clicks Undo (snackbar should be visible)
// The snackbar appears for 5 seconds with an "Undo" action
// Use snack-custom .action selector (app uses custom snackbar component)
const undoButton = clientA.page.locator('snack-custom button.action');
const undoButton = clientA.page.locator(
'snack-custom button.action, .mat-mdc-snack-bar-container button',
);
await undoButton.waitFor({ state: 'visible', timeout: 5000 });
await undoButton.click();

View file

@ -1,8 +1,7 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { waitForStatePersistence } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -21,16 +20,9 @@ import { waitForAppReady } from '../../utils/waits';
* 2. Client B has DIFFERENT data, sets up WebDAV sync to same folder
* 3. Expected: Conflict dialog with "Use Local" vs "Use Remote" options
*/
test.describe('WebDAV First Sync Conflict', () => {
test.describe('@webdav WebDAV First Sync Conflict', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
test.skip(true, 'WebDAV server not reachable');
}
});
test('should show conflict dialog and allow USE_LOCAL to upload local data', async ({
browser,
baseURL,

View file

@ -1,8 +1,7 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { waitForStatePersistence } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
createSyncFolder,
@ -31,17 +30,9 @@ import legacyDataCollisionB from '../../fixtures/legacy-migration-collision-b.js
*
* Run with: npm run e2e:file e2e/tests/sync/webdav-legacy-migration-sync.spec.ts -- --retries=0
*/
test.describe('@migration WebDAV Legacy Migration Sync', () => {
test.describe('@webdav @migration WebDAV Legacy Migration Sync', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
/**
* Test: Both clients migrated from legacy - Keep local resolution
*

View file

@ -1,8 +1,7 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { waitForStatePersistence } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -34,14 +33,6 @@ test.describe('@webdav WebDAV Provider Switch', () => {
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
};
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should sync tasks when Client B connects to existing WebDAV server (provider switch)', async ({
browser,
baseURL,

View file

@ -1,7 +1,6 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -10,18 +9,10 @@ import {
generateSyncFolderName,
} from '../../utils/sync-helpers';
test.describe('WebDAV Sync Advanced Features', () => {
test.describe('@webdav WebDAV Sync Advanced Features', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should sync sub-tasks correctly', async ({ browser, baseURL, request }) => {
const SYNC_FOLDER_NAME = generateSyncFolderName('e2e-advanced-sub');
await createSyncFolder(request, SYNC_FOLDER_NAME);

View file

@ -1,10 +1,9 @@
import { expect, Page } from '@playwright/test';
import { test } from '../../fixtures/test.fixture';
import { Page } from '@playwright/test';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { TaskPage } from '../../pages/task.page';
import { waitForStatePersistence } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -57,14 +56,6 @@ test.describe('@webdav WebDAV Archive Sync', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV archive tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
/**
* Scenario 1: Two clients archive different tasks
*

View file

@ -1,12 +1,11 @@
import { expect } from '@playwright/test';
import { test } from '../../fixtures/test.fixture';
import { test } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { TaskPage } from '../../pages/task.page';
import { TagPage } from '../../pages/tag.page';
import { ProjectPage } from '../../pages/project.page';
import { waitForStatePersistence, waitForAppReady } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -51,14 +50,6 @@ const archiveDoneTasks = async (page: Page): Promise<void> => {
test.describe('@webdav WebDAV Delete Cascade Sync', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV delete cascade tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
/**
* Test 1.1: Delete tag with archived tasks syncs to other client
*

View file

@ -1,7 +1,6 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
createUniqueSyncFolder,
@ -13,18 +12,10 @@ import {
} from '../../utils/sync-helpers';
import { waitForStatePersistence } from '../../utils/waits';
test.describe('WebDAV Sync Error Handling', () => {
test.describe('@webdav WebDAV Sync Error Handling', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should handle server unavailable during sync', async ({
browser,
baseURL,

View file

@ -1,9 +1,8 @@
import { expect, test } from '../../fixtures/test.fixture';
import { expect, test } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { ProjectPage } from '../../pages/project.page';
import { waitForStatePersistence } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
createSyncFolder,
generateSyncFolderName,
@ -16,18 +15,10 @@ import {
const WEBDAV_TIMESTAMP_DELAY_MS = 2000;
test.describe('WebDAV Sync Expansion', () => {
test.describe('@webdav WebDAV Sync Expansion', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should sync projects', async ({ browser, baseURL, request }) => {
test.slow();
const SYNC_FOLDER_NAME = generateSyncFolderName('e2e-expansion-proj');

View file

@ -1,8 +1,7 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { waitForAppReady, waitForStatePersistence } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -12,7 +11,7 @@ import {
dismissTourIfVisible,
} from '../../utils/sync-helpers';
test.describe('WebDAV Sync Full Flow', () => {
test.describe('@webdav WebDAV Sync Full Flow', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
@ -24,14 +23,6 @@ test.describe('WebDAV Sync Full Flow', () => {
syncFolderPath: `/${SYNC_FOLDER_NAME}`,
};
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should sync data between two clients', async ({ browser, baseURL, request }) => {
test.slow(); // Sync tests might take longer
console.log('Using baseURL:', baseURL);

View file

@ -1,8 +1,7 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { TagPage } from '../../pages/tag.page';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
createUniqueSyncFolder,
@ -11,18 +10,10 @@ import {
waitForSync,
} from '../../utils/sync-helpers';
test.describe('WebDAV Sync Tags', () => {
test.describe('@webdav WebDAV Sync Tags', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should sync tag creation between clients', async ({
browser,
baseURL,

View file

@ -1,7 +1,6 @@
import { test, expect } from '../../fixtures/test.fixture';
import { test, expect } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
createUniqueSyncFolder,
@ -10,18 +9,10 @@ import {
waitForSync,
} from '../../utils/sync-helpers';
test.describe('WebDAV Sync Task Order', () => {
test.describe('@webdav WebDAV Sync Task Order', () => {
// Run sync tests serially to avoid WebDAV server contention
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
test('should preserve task order after sync', async ({ browser, baseURL, request }) => {
test.slow();
const SYNC_FOLDER_NAME = createUniqueSyncFolder('task-order');

View file

@ -1,10 +1,9 @@
import { expect } from '@playwright/test';
import { test } from '../../fixtures/test.fixture';
import { test } from '../../fixtures/webdav.fixture';
import { SyncPage } from '../../pages/sync.page';
import { WorkViewPage } from '../../pages/work-view.page';
import { TaskPage } from '../../pages/task.page';
import { waitForStatePersistence, waitForAppReady } from '../../utils/waits';
import { isWebDavServerUp } from '../../utils/check-webdav';
import {
WEBDAV_CONFIG_TEMPLATE,
setupSyncClient,
@ -28,14 +27,6 @@ import {
test.describe('@webdav WebDAV TODAY Tag Sync', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const isUp = await isWebDavServerUp(WEBDAV_CONFIG_TEMPLATE.baseUrl);
if (!isUp) {
console.warn('WebDAV server not reachable. Skipping WebDAV TODAY tag tests.');
test.skip(true, 'WebDAV server not reachable');
}
});
/**
* Test 2.1: Concurrent task reordering in TODAY view
*

View file

@ -5,7 +5,6 @@ import {
BrowserWindowConstructorOptions,
ipcMain,
Menu,
MenuItem,
MenuItemConstructorOptions,
nativeTheme,
shell,
@ -34,9 +33,9 @@ let mainWin: BrowserWindow;
* Semi-transparent to ensure window controls are always visible.
*/
const getTitleBarColor = (isDark: boolean): string => {
// Dark: matches --bg (#131314) with 95% opacity
// Light: matches --bg (#f8f8f7) with 95% opacity
return isDark ? 'rgba(19, 19, 20, 0.95)' : 'rgba(248, 248, 247, 0.95)';
// Dark: matches --bg (#131314) with 0% opacity (fully transparent)
// Light: matches --bg (#f8f8f7) with 0% opacity (fully transparent)
return isDark ? 'rgba(19, 19, 20, 0)' : 'rgba(248, 248, 247, 0)';
};
const mainWinModule: {
@ -305,44 +304,41 @@ function initWinEventListeners(app: Electron.App): void {
}
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function createMenu(
quitApp: (
menuItem: MenuItem,
browserWindow: BrowserWindow | undefined,
event: KeyboardEvent,
) => void,
): void {
function createMenu(quitApp: () => void): void {
// Create application menu to enable copy & pasting on MacOS
const menuTpl = [
const menuTpl: MenuItemConstructorOptions[] = [
{
label: 'Application',
submenu: [
{ label: 'About Super Productivity', selector: 'orderFrontStandardAboutPanel:' },
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{
label: 'Quit',
click: quitApp,
accelerator: 'CmdOrCtrl+Q',
click: quitApp,
},
],
},
{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' },
],
},
];
const menuTplOUT = menuTpl as MenuItemConstructorOptions[];
// we need to set a menu to get copy & paste working for mac os x
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTplOUT));
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTpl));
}
// TODO this is ugly as f+ck

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "superProductivity",
"version": "17.0.0-RC.12",
"version": "17.0.0-RC.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "superProductivity",
"version": "17.0.0-RC.12",
"version": "17.0.0-RC.13",
"license": "MIT",
"workspaces": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "superProductivity",
"version": "17.0.0-RC.12",
"version": "17.0.0-RC.13",
"description": "ToDo list and Time Tracking",
"keywords": [
"ToDo",
@ -65,7 +65,7 @@
"e2e:supersync:down": "docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml down supersync",
"e2e:docker": "docker compose -f docker-compose.e2e.yaml up -d app && ./scripts/wait-for-app.sh && E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e; docker compose -f docker-compose.e2e.yaml down",
"e2e:docker:webdav": "docker compose -f docker-compose.e2e.yaml up -d app webdav && ./scripts/wait-for-app.sh && ./scripts/wait-for-webdav.sh && E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e; docker compose -f docker-compose.e2e.yaml down",
"e2e:docker:all": "docker compose -f docker-compose.e2e.yaml up -d app webdav && ./scripts/wait-for-app.sh && ./scripts/wait-for-webdav.sh && E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e; docker compose -f docker-compose.e2e.yaml down",
"e2e:docker:all": "docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml up -d app webdav db supersync && ./scripts/wait-for-app.sh && ./scripts/wait-for-webdav.sh && ./scripts/wait-for-supersync.sh && E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e -- --grep-invert @supersync; PHASE1_EXIT=$?; E2E_BASE_URL=http://localhost:${APP_PORT:-4242} npm run e2e -- --grep @supersync --workers=3; PHASE2_EXIT=$?; docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml down; if [ $PHASE1_EXIT -ne 0 ]; then exit $PHASE1_EXIT; fi; exit $PHASE2_EXIT",
"electron": "NODE_ENV=PROD electron .",
"electron:build": "tsc -p electron/tsconfig.electron.json",
"electron:watch": "tsc -p electron/tsconfig.electron.json --watch",

20
scripts/wait-for-supersync.sh Executable file
View file

@ -0,0 +1,20 @@
#!/bin/bash
# Wait for SuperSync server to be ready at http://localhost:1901
# Retries for up to 90 seconds (accounts for PostgreSQL + SuperSync startup)
echo "Waiting for SuperSync server on http://localhost:1901..."
MAX_WAIT=90
elapsed=0
until curl -s http://localhost:1901/health > /dev/null 2>&1; do
if [ $elapsed -ge $MAX_WAIT ]; then
echo "Timeout: SuperSync server did not start within ${MAX_WAIT}s"
echo "--- SuperSync logs ---"
docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml logs supersync
exit 1
fi
sleep 1
elapsed=$((elapsed + 1))
done
echo "SuperSync server is ready!"

View file

@ -1,12 +1,4 @@
<nav class="mobile-bottom-nav">
<button
mat-button
class="nav-button"
(click)="toggleMobileNav()"
>
<mat-icon>menu</mat-icon>
</button>
<button
mat-button
class="nav-button"
@ -16,6 +8,15 @@
<mat-icon>wb_sunny</mat-icon>
</button>
<button
mat-button
class="nav-button"
[class.active]="currentRoute() === '/planner'"
routerLink="/planner"
>
<mat-icon>edit_calendar</mat-icon>
</button>
<button
mat-fab
class="add-task-button"
@ -29,18 +30,17 @@
<button
mat-button
class="nav-button"
[class.active]="currentRoute() === '/planner'"
routerLink="/planner"
[matMenuTriggerFor]="panelsMenu"
>
<mat-icon>edit_calendar</mat-icon>
<mat-icon>view_sidebar</mat-icon>
</button>
<button
mat-button
class="nav-button"
[matMenuTriggerFor]="panelsMenu"
(click)="toggleMobileNav()"
>
<mat-icon>view_sidebar</mat-icon>
<mat-icon>menu</mat-icon>
</button>
</nav>

View file

@ -8,7 +8,6 @@ import {
} from '@angular/core';
import { WorkContextType } from '../../features/work-context/work-context.model';
import { T } from 'src/app/t.const';
import { IS_IOS } from '../../util/is-ios';
import { TODAY_TAG } from '../../features/tag/tag.const';
import { DialogConfirmComponent } from '../../ui/dialog-confirm/dialog-confirm.component';
import { MatDialog } from '@angular/material/dialog';
@ -58,7 +57,6 @@ export class WorkContextMenuComponent implements OnInit {
isForProject: boolean = true;
base: string = 'project';
shareSupport: ShareSupport = 'none';
private _isShareInProgress = false;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@ -135,73 +133,53 @@ export class WorkContextMenuComponent implements OnInit {
protected readonly INBOX_PROJECT = INBOX_PROJECT;
async shareTasksAsMarkdown(): Promise<void> {
// Guard against concurrent share operations
if (this._isShareInProgress) {
const { status, markdown, contextTitle } =
await this._markdownService.getMarkdownForContext(
this.contextId,
this.isForProject,
);
if (status === 'empty' || !markdown) {
this._snackService.open(T.GLOBAL_SNACK.NO_TASKS_TO_COPY);
return;
}
this._isShareInProgress = true;
const shareResult = await this._shareService.shareText({
title: contextTitle ?? 'Super Productivity',
text: markdown,
});
try {
const { status, markdown, contextTitle } =
await this._markdownService.getMarkdownForContext(
this.contextId,
this.isForProject,
);
if (status === 'empty' || !markdown) {
this._snackService.open(T.GLOBAL_SNACK.NO_TASKS_TO_COPY);
return;
}
const shareResult = await this._shareService.shareText({
title: contextTitle ?? 'Super Productivity',
text: markdown,
});
if (shareResult === 'shared') {
if (this.shareSupport === 'none') {
const support = await this._shareService.getShareSupport();
this._setShareSupport(support);
}
return;
}
if (shareResult === 'cancelled') {
return;
}
const didCopy = await this._markdownService.copyMarkdownText(markdown);
if (didCopy) {
if (shareResult === 'unavailable') {
this._snackService.open(T.GLOBAL_SNACK.SHARE_UNAVAILABLE_FALLBACK);
this._setShareSupport('none');
} else if (shareResult === 'failed') {
this._snackService.open(T.GLOBAL_SNACK.SHARE_FAILED_FALLBACK);
this._setShareSupport('none');
} else {
this._snackService.open(T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD);
}
return;
}
this._snackService.open({
msg: T.GLOBAL_SNACK.SHARE_FAILED,
type: 'ERROR',
});
this._setShareSupport('none');
} finally {
// iOS-specific: Delay clearing flag to prevent re-trigger from focus events
// On iOS, dismissing the native share sheet fires window focus events
// that can cause the method to be called again
if (IS_IOS) {
setTimeout(() => {
this._isShareInProgress = false;
}, 500);
} else {
this._isShareInProgress = false;
if (shareResult === 'shared') {
if (this.shareSupport === 'none') {
const support = await this._shareService.getShareSupport();
this._setShareSupport(support);
}
return;
}
if (shareResult === 'cancelled') {
return;
}
const didCopy = await this._markdownService.copyMarkdownText(markdown);
if (didCopy) {
if (shareResult === 'unavailable') {
this._snackService.open(T.GLOBAL_SNACK.SHARE_UNAVAILABLE_FALLBACK);
this._setShareSupport('none');
} else if (shareResult === 'failed') {
this._snackService.open(T.GLOBAL_SNACK.SHARE_FAILED_FALLBACK);
this._setShareSupport('none');
} else {
this._snackService.open(T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD);
}
return;
}
this._snackService.open({
msg: T.GLOBAL_SNACK.SHARE_FAILED,
type: 'ERROR',
});
this._setShareSupport('none');
}
async unplanAllTodayTasks(): Promise<void> {

View file

@ -55,6 +55,11 @@ export enum LS {
NAV_SIDEBAR_EXPANDED = 'SUP_NAV_SIDEBAR_EXPANDED',
NAV_SIDEBAR_WIDTH = 'SUP_NAV_SIDEBAR_WIDTH',
RIGHT_PANEL_WIDTH = 'SUP_RIGHT_PANEL_WIDTH',
// Task view customizer
TASK_VIEW_CUSTOMIZER_SORT = 'SUP_TASK_VIEW_CUSTOMIZER_SORT',
TASK_VIEW_CUSTOMIZER_GROUP = 'SUP_TASK_VIEW_CUSTOMIZER_GROUP',
TASK_VIEW_CUSTOMIZER_FILTER = 'SUP_TASK_VIEW_CUSTOMIZER_FILTER',
}
// SESSION STORAGE

View file

@ -2,6 +2,7 @@ import { Injectable, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Directory, Filesystem } from '@capacitor/filesystem';
import { IS_NATIVE_PLATFORM } from '../../util/is-native-platform';
import { IS_IOS } from '../../util/is-ios';
import { SnackService } from '../snack/snack.service';
import {
ShareCanvasImageParams,
@ -35,6 +36,13 @@ interface ShareParams {
export class ShareService {
private _shareSupportPromise?: Promise<SharePlatformUtil.ShareSupport>;
// iOS: Timestamp-based debounce to prevent re-trigger when share sheet
// dismissal fires synthetic click events that reopen the menu.
// The component-level guard doesn't work because the menu component
// is destroyed when closed (inside ng-template matMenuContent).
private _lastNativeShareAttemptMs = 0;
private readonly _IOS_SHARE_DEBOUNCE_MS = 1000;
private _snackService = inject(SnackService);
private _matDialog = inject(MatDialog);
@ -202,6 +210,16 @@ export class ShareService {
* Public API for dialog component.
*/
async tryNativeShare(payload: SharePayload): Promise<ShareResult> {
// iOS: Debounce to prevent re-trigger when share sheet dismissal
// fires synthetic click events that reopen the menu
if (IS_IOS) {
const now = Date.now();
if (now - this._lastNativeShareAttemptMs < this._IOS_SHARE_DEBOUNCE_MS) {
return { success: false, error: 'Share debounced' };
}
this._lastNativeShareAttemptMs = now;
}
const normalized = ShareTextUtil.ensureShareText(payload);
const capacitorShare = SharePlatformUtil.getCapacitorSharePlugin();
@ -220,8 +238,14 @@ export class ShareService {
usedNative: true,
target: 'native',
};
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
} catch (error: unknown) {
// Check for AbortError (standard) or Capacitor iOS error format
const err = error as { name?: string; errorMessage?: string; message?: string };
const isCancelled =
err?.name === 'AbortError' ||
/cancel/i.test(err?.errorMessage ?? '') ||
/cancel/i.test(err?.message ?? '');
if (isCancelled) {
return {
success: false,
error: 'Share cancelled',

View file

@ -31,7 +31,8 @@ import { IS_ANDROID_WEB_VIEW } from '../../util/is-android-web-view';
import { androidInterface } from '../../features/android/android-interface';
import { HttpClient } from '@angular/common/http';
import { CapacitorPlatformService } from '../platform/capacitor-platform.service';
import { Keyboard } from '@capacitor/keyboard';
import { Keyboard, KeyboardInfo } from '@capacitor/keyboard';
import { PluginListenerHandle } from '@capacitor/core';
import { StatusBar, Style } from '@capacitor/status-bar';
import { LS } from '../persistence/storage-keys.const';
import { CustomThemeService } from './custom-theme.service';
@ -59,6 +60,8 @@ export class GlobalThemeService {
private _environmentInjector = inject(EnvironmentInjector);
private _destroyRef = inject(DestroyRef);
private _hasInitialized = false;
private _keyboardListenerHandles: PluginListenerHandle[] = [];
private _focusinListener: ((event: FocusEvent) => void) | null = null;
darkMode = signal<DarkModeCfg>(
(localStorage.getItem(LS.DARK_MODE) as DarkModeCfg) || 'system',
@ -158,6 +161,7 @@ export class GlobalThemeService {
['gitlab', 'assets/icons/gitlab.svg'],
['jira', 'assets/icons/jira.svg'],
['caldav', 'assets/icons/caldav.svg'],
['calendar', 'assets/icons/calendar.svg'],
['open_project', 'assets/icons/open-project.svg'],
['remove_today', 'assets/icons/remove-today-48px.svg'],
['working_today', 'assets/icons/working-today.svg'],
@ -429,7 +433,7 @@ export class GlobalThemeService {
* Adds/removes CSS classes when keyboard shows/hides.
*/
private _initIOSKeyboardHandling(): void {
Keyboard.addListener('keyboardWillShow', (info) => {
Keyboard.addListener('keyboardWillShow', (info: KeyboardInfo) => {
Log.log('iOS keyboard will show', info);
this.document.body.classList.add(BodyClass.isKeyboardVisible);
// Set CSS variable for keyboard height to adjust layout
@ -437,15 +441,67 @@ export class GlobalThemeService {
'--keyboard-height',
`${info.keyboardHeight}px`,
);
});
}).then((handle) => this._keyboardListenerHandles.push(handle));
// Use keyboardDidShow for scroll (after animation completes)
Keyboard.addListener('keyboardDidShow', () => {
this._scrollActiveInputIntoView();
}).then((handle) => this._keyboardListenerHandles.push(handle));
Keyboard.addListener('keyboardWillHide', () => {
Log.log('iOS keyboard will hide');
this.document.body.classList.remove(BodyClass.isKeyboardVisible);
this.document.documentElement.style.setProperty('--keyboard-height', '0px');
}).then((handle) => this._keyboardListenerHandles.push(handle));
// Also handle focus changes while keyboard is already visible
this._focusinListener = (event: FocusEvent): void => {
const target = event.target as HTMLElement;
if (
this.document.body.classList.contains(BodyClass.isKeyboardVisible) &&
this._isInputElement(target)
) {
// Small delay to let CSS padding apply, validate element is still focused
setTimeout(() => {
if (this.document.activeElement === target) {
this._scrollActiveInputIntoView();
}
}, 50);
}
};
this.document.addEventListener('focusin', this._focusinListener, { passive: true });
// Cleanup listeners on destroy
this._destroyRef.onDestroy(() => {
this._keyboardListenerHandles.forEach((handle) => handle.remove());
if (this._focusinListener) {
this.document.removeEventListener('focusin', this._focusinListener);
}
});
}
private _isInputElement(el: HTMLElement): boolean {
const tagName = el.tagName.toLowerCase();
return (
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select' ||
el.isContentEditable
);
}
private _scrollActiveInputIntoView(): void {
const activeEl = this.document.activeElement as HTMLElement;
if (activeEl && this._isInputElement(activeEl)) {
// scrollIntoViewIfNeeded is non-standard but well-supported in iOS WebView
if ('scrollIntoViewIfNeeded' in activeEl) {
(activeEl as any).scrollIntoViewIfNeeded(true);
} else {
activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
/**
* Initialize iOS status bar styling.
* Syncs status bar style with app dark/light mode.

View file

@ -75,7 +75,7 @@ export class FinishDayBeforeCloseEffects {
const doneTasks = todayMainTasks.filter((t) => t.isDone);
if (doneTasks.length) {
if (
confirm(
!confirm(
this._translateService.instant(
T.F.FINISH_DAY_BEFORE_EXIT.C.FINISH_DAY_BEFORE_EXIT,
{
@ -84,13 +84,12 @@ export class FinishDayBeforeCloseEffects {
),
)
) {
this._execBeforeCloseService.setDone(EXEC_BEFORE_CLOSE_ID);
} else {
// User wants to finish day first - navigate them there
this._router.navigate([`tag/${TODAY_TAG.id}/daily-summary`]);
}
} else {
this._execBeforeCloseService.setDone(EXEC_BEFORE_CLOSE_ID);
}
// Always complete the handler to prevent hanging
this._execBeforeCloseService.setDone(EXEC_BEFORE_CLOSE_ID);
}),
),
{ dispatch: false },

View file

@ -70,7 +70,7 @@ header {
border-bottom: 1px solid var(--extra-border-color);
background: var(--bg-lighter);
flex-wrap: nowrap;
min-height: 48px;
//min-height: 48px;
button {
flex-shrink: 0;

View file

@ -90,8 +90,13 @@ export class ScheduleComponent {
);
shouldEnableHorizontalScroll = computed(() => {
// No longer needed - we adjust day count to fit viewport instead
return false;
const selectedView = this._currentTimeViewMode();
// Only enable horizontal scroll for week view when viewport is narrow
if (selectedView !== 'week') {
return false;
}
// Enable scroll when viewport is smaller than what's needed for 7 days
return this._windowSize().width < 1900;
});
private _daysToShowCount = computed(() => {
@ -114,16 +119,8 @@ export class ScheduleComponent {
}
}
// Week view: responsive day count based on viewport width
if (width >= 1200) {
return 7; // Desktop: full week
} else if (width >= 768) {
return 5; // Tablet: 5 days
} else if (width >= 480) {
return 3; // Mobile: 3 days
} else {
return 2; // Small mobile: 2 days
}
// Week view: always 7 days
return 7;
});
daysToShow = computed(() => {

View file

@ -27,6 +27,7 @@ import {
} from './types';
import { DateAdapter } from '@angular/material/core';
import { DEFAULT_FIRST_DAY_OF_WEEK } from 'src/app/core/locale.constants';
import { LS } from '../../core/persistence/storage-keys.const';
describe('TaskViewCustomizerService', () => {
let service: TaskViewCustomizerService;
@ -114,6 +115,9 @@ describe('TaskViewCustomizerService', () => {
];
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
mockWorkContextService = {
activeWorkContextId: null,
activeWorkContextType: null,
@ -498,4 +502,176 @@ describe('TaskViewCustomizerService', () => {
expect(service.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
});
});
describe('localStorage persistence', () => {
it('should initialize with default values when localStorage is empty', () => {
expect(service.selectedSort()).toEqual(DEFAULT_OPTIONS.sort);
expect(service.selectedGroup()).toEqual(DEFAULT_OPTIONS.group);
expect(service.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
});
it('should restore sort option from localStorage on initialization', () => {
const savedSort: SortOption = {
type: SORT_OPTION_TYPE.name,
order: SORT_ORDER.ASC,
label: 'Name',
};
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_SORT, JSON.stringify(savedSort));
// Reset TestBed to create a new service instance
TestBed.resetTestingModule();
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
});
TestBed.configureTestingModule({
providers: [
TaskViewCustomizerService,
{ provide: DateAdapter, useValue: dateAdapter },
{ provide: WorkContextService, useValue: mockWorkContextService },
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
provideMockStore({
selectors: [
{ selector: selectAllProjects, value: mockProjects },
{ selector: selectAllTags, value: mockTags },
],
}),
],
});
const newService = TestBed.inject(TaskViewCustomizerService);
(newService as any)._allProjects = mockProjects;
(newService as any)._allTags = mockTags;
expect(newService.selectedSort()).toEqual(savedSort);
});
it('should restore group option from localStorage on initialization', () => {
const savedGroup: GroupOption = { type: GROUP_OPTION_TYPE.tag, label: 'Tag' };
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_GROUP, JSON.stringify(savedGroup));
TestBed.resetTestingModule();
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
});
TestBed.configureTestingModule({
providers: [
TaskViewCustomizerService,
{ provide: DateAdapter, useValue: dateAdapter },
{ provide: WorkContextService, useValue: mockWorkContextService },
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
provideMockStore({
selectors: [
{ selector: selectAllProjects, value: mockProjects },
{ selector: selectAllTags, value: mockTags },
],
}),
],
});
const newService = TestBed.inject(TaskViewCustomizerService);
(newService as any)._allProjects = mockProjects;
(newService as any)._allTags = mockTags;
expect(newService.selectedGroup()).toEqual(savedGroup);
});
it('should restore filter option from localStorage on initialization', () => {
const savedFilter: FilterOption = {
type: FILTER_OPTION_TYPE.tag,
preset: 'Tag A',
label: 'Tag',
};
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_FILTER, JSON.stringify(savedFilter));
TestBed.resetTestingModule();
const dateAdapter = jasmine.createSpyObj<DateAdapter<Date>>('DateAdapter', [], {
getFirstDayOfWeek: () => DEFAULT_FIRST_DAY_OF_WEEK,
});
TestBed.configureTestingModule({
providers: [
TaskViewCustomizerService,
{ provide: DateAdapter, useValue: dateAdapter },
{ provide: WorkContextService, useValue: mockWorkContextService },
{ provide: ProjectService, useValue: { update: projectUpdateSpy } },
{ provide: TagService, useValue: { updateTag: tagUpdateSpy } },
provideMockStore({
selectors: [
{ selector: selectAllProjects, value: mockProjects },
{ selector: selectAllTags, value: mockTags },
],
}),
],
});
const newService = TestBed.inject(TaskViewCustomizerService);
(newService as any)._allProjects = mockProjects;
(newService as any)._allTags = mockTags;
expect(newService.selectedFilter()).toEqual(savedFilter);
});
it('should persist sort option to localStorage when changed', (done) => {
const newSort: SortOption = {
type: SORT_OPTION_TYPE.name,
order: SORT_ORDER.ASC,
label: 'Name',
};
service.setSort(newSort);
// Wait for effect to run
setTimeout(() => {
const stored = localStorage.getItem(LS.TASK_VIEW_CUSTOMIZER_SORT);
expect(stored).toBeTruthy();
expect(JSON.parse(stored!)).toEqual(newSort);
done();
}, 50);
});
it('should persist group option to localStorage when changed', (done) => {
const newGroup: GroupOption = { type: GROUP_OPTION_TYPE.tag, label: 'Tag' };
service.setGroup(newGroup);
setTimeout(() => {
const stored = localStorage.getItem(LS.TASK_VIEW_CUSTOMIZER_GROUP);
expect(stored).toBeTruthy();
expect(JSON.parse(stored!)).toEqual(newGroup);
done();
}, 50);
});
it('should persist filter option to localStorage when changed', (done) => {
const newFilter: FilterOption = {
type: FILTER_OPTION_TYPE.tag,
preset: 'Tag A',
label: 'Tag',
};
service.setFilter(newFilter);
setTimeout(() => {
const stored = localStorage.getItem(LS.TASK_VIEW_CUSTOMIZER_FILTER);
expect(stored).toBeTruthy();
expect(JSON.parse(stored!)).toEqual(newFilter);
done();
}, 50);
});
it('should fallback to defaults when localStorage contains invalid JSON', () => {
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_SORT, 'invalid json{');
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_GROUP, '{broken');
localStorage.setItem(LS.TASK_VIEW_CUSTOMIZER_FILTER, 'not json');
const newService = TestBed.inject(TaskViewCustomizerService);
(newService as any)._allProjects = mockProjects;
(newService as any)._allTags = mockTags;
expect(newService.selectedSort()).toEqual(DEFAULT_OPTIONS.sort);
expect(newService.selectedGroup()).toEqual(DEFAULT_OPTIONS.group);
expect(newService.selectedFilter()).toEqual(DEFAULT_OPTIONS.filter);
});
});
});

View file

@ -1,4 +1,4 @@
import { Injectable, signal, inject } from '@angular/core';
import { Injectable, signal, inject, effect } from '@angular/core';
import { Observable, animationFrameScheduler, combineLatest } from 'rxjs';
import { map, observeOn, take } from 'rxjs/operators';
import { TaskWithSubTasks } from '../tasks/task.model';
@ -28,6 +28,8 @@ import {
FILTER_COMMON,
} from './types';
import { DateAdapter } from '@angular/material/core';
import { lsGetJSON, lsSetJSON } from '../../util/ls-util';
import { LS } from '../../core/persistence/storage-keys.const';
@Injectable({ providedIn: 'root' })
export class TaskViewCustomizerService {
@ -37,9 +39,18 @@ export class TaskViewCustomizerService {
private _projectService = inject(ProjectService);
private _tagService = inject(TagService);
public selectedSort = signal<SortOption>(DEFAULT_OPTIONS.sort);
public selectedGroup = signal<GroupOption>(DEFAULT_OPTIONS.group);
public selectedFilter = signal<FilterOption>(DEFAULT_OPTIONS.filter);
public selectedSort = signal<SortOption>(
lsGetJSON<SortOption>(LS.TASK_VIEW_CUSTOMIZER_SORT, DEFAULT_OPTIONS.sort) ??
DEFAULT_OPTIONS.sort,
);
public selectedGroup = signal<GroupOption>(
lsGetJSON<GroupOption>(LS.TASK_VIEW_CUSTOMIZER_GROUP, DEFAULT_OPTIONS.group) ??
DEFAULT_OPTIONS.group,
);
public selectedFilter = signal<FilterOption>(
lsGetJSON<FilterOption>(LS.TASK_VIEW_CUSTOMIZER_FILTER, DEFAULT_OPTIONS.filter) ??
DEFAULT_OPTIONS.filter,
);
isCustomized = computed(() => {
return [
@ -52,6 +63,18 @@ export class TaskViewCustomizerService {
constructor() {
this._initProjects();
this._initTags();
effect(() => {
lsSetJSON(LS.TASK_VIEW_CUSTOMIZER_SORT, this.selectedSort());
});
effect(() => {
lsSetJSON(LS.TASK_VIEW_CUSTOMIZER_GROUP, this.selectedGroup());
});
effect(() => {
lsSetJSON(LS.TASK_VIEW_CUSTOMIZER_FILTER, this.selectedFilter());
});
}
private _allProjects: Project[] = [];

View file

@ -250,7 +250,7 @@
}
<div>
@if (t.notes || (t.issueId && t.issueType !== ICAL_TYPE) || isSelected()) {
@if (isShowToggleButton()) {
<button
(click)="toggleShowDetailPanel()"
title="{{ T.F.TASK.CMP.TOGGLE_DETAIL_PANEL | translate }} {{
@ -262,19 +262,12 @@
color=""
mat-icon-button
>
@if (!t.issueWasUpdated && !isSelected()) {
<mat-icon>chat</mat-icon>
}
@if (!t.issueWasUpdated && isSelected()) {
<mat-icon>close</mat-icon>
}
@if (t.issueWasUpdated) {
<mat-icon
class="updated-icon"
color="accent"
>update
</mat-icon>
}
<mat-icon
[class.updated-icon]="toggleButtonIcon() === 'update'"
[color]="toggleButtonIcon() === 'update' ? 'accent' : ''"
>
{{ toggleButtonIcon() }}
</mat-icon>
</button>
}
</div>

View file

@ -148,6 +148,27 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
// Use shared signals from services to avoid creating 600+ subscriptions on initial render
isCurrent = computed(() => this._taskService.currentTaskId() === this.task().id);
isSelected = computed(() => this._taskService.selectedTaskId() === this.task().id);
isShowCloseButton = computed(() => {
// Only show close button when task is selected AND not on mobile (bottom panel)
return this.isSelected() && !this.layoutService.isXs();
});
// Determines if the toggle detail panel button should be visible
isShowToggleButton = computed(() => {
const t = this.task();
return (
t.notes || (t.issueId && t.issueType !== ICAL_TYPE) || this.isShowCloseButton()
);
});
// Determines which icon to show in the toggle button
toggleButtonIcon = computed((): 'chat' | 'close' | 'update' => {
const t = this.task();
if (t.issueWasUpdated) return 'update';
if (this.isShowCloseButton()) return 'close';
return 'chat';
});
isTaskOnTodayList = computed(() =>
this._taskService.todayListSet().has(this.task().id),
);

View file

@ -530,6 +530,17 @@ export class SyncWrapperService {
}
private _handleDecryptionError(): void {
// Set ERROR status so sync button shows error icon
this._providerManager.setSyncStatus('ERROR');
// Show snackbar (consistent with other error handlers)
this._snackService.open({
msg: T.F.SYNC.S.DECRYPTION_FAILED,
type: 'ERROR',
config: { duration: 10000 }, // Longer duration for critical errors
});
// Open dialog for password correction
this._matDialog
.open(DialogHandleDecryptErrorComponent, {
disableClose: true,
@ -543,6 +554,10 @@ export class SyncWrapperService {
if (isForceUpload) {
this.forceUpload();
}
// Reset status if user cancelled without taking action
if (!isReSync && !isForceUpload) {
this._providerManager.setSyncStatus('UNKNOWN_OR_CHANGED');
}
});
}

View file

@ -82,9 +82,9 @@ export class SyncEffects {
msg: T.F.DROPBOX.S.SYNC_ERROR,
type: 'ERROR',
});
if (confirm('Sync failed. Close App anyway?')) {
this._execBeforeCloseService.setDone(SYNC_BEFORE_CLOSE_ID);
}
// Inform user but always allow close - sync already failed
alert('Sync failed. The app will close.');
this._execBeforeCloseService.setDone(SYNC_BEFORE_CLOSE_ID);
}),
),
),

View file

@ -10,7 +10,14 @@ import { Task } from '../../features/tasks/task.model';
import { Project } from '../../features/project/project.model';
import { Tag } from '../../features/tag/tag.model';
// Set to true to run stress tests (10k+ operations)
// These tests take 1-2 seconds each and are skipped by default to speed up test runs
const RUN_STRESS_TESTS = false;
describe('bulkHydrationMetaReducer', () => {
// Helper to conditionally run stress tests
const stressTest = RUN_STRESS_TESTS ? it : xit;
// Track all reducer calls for verification
let reducerCalls: { state: unknown; action: Action }[];
let mockReducer: jasmine.Spy;
@ -355,113 +362,122 @@ describe('bulkHydrationMetaReducer', () => {
* - Performance degradation (O(n) expected, not O(n²))
*
* Use case: User syncing after extended offline period with many changes.
*
* NOTE: Skipped by default to speed up test runs. Set RUN_STRESS_TESTS=true to enable.
*/
it('should handle 10,000+ operations without blocking main thread for too long', () => {
const reducer = bulkHydrationMetaReducer(mockReducer);
const state = createMockState();
stressTest(
'should handle 10,000+ operations without blocking main thread for too long',
() => {
const reducer = bulkHydrationMetaReducer(mockReducer);
const state = createMockState();
// Create 10,000 operations - mix of different types for realistic scenario
const operations: Operation[] = [];
for (let i = 0; i < 10000; i++) {
// Alternate between update and create operations for variety
if (i % 10 === 0) {
// Every 10th operation: create a new task
const newTaskId = `task-${i}`;
operations.push(
// Create 10,000 operations - mix of different types for realistic scenario
const operations: Operation[] = [];
for (let i = 0; i < 10000; i++) {
// Alternate between update and create operations for variety
if (i % 10 === 0) {
// Every 10th operation: create a new task
const newTaskId = `task-${i}`;
operations.push(
createMockOperation({
id: `op-create-${i}`,
opType: OpType.Create,
entityId: newTaskId,
actionType: '[Task] Add Task' as ActionType,
payload: { task: createMockTask({ id: newTaskId, title: `Task ${i}` }) },
}),
);
} else {
// Regular update operation
operations.push(
createMockOperation({
id: `op-update-${i}`,
payload: { task: { id: TASK_ID, changes: { title: `Update ${i}` } } },
}),
);
}
}
const action = bulkApplyHydrationOperations({ operations });
const startTime = performance.now();
const result = reducer(state, action);
const endTime = performance.now();
const elapsedMs = endTime - startTime;
// Should complete in under 5 seconds even with 10k ops
// This is generous to account for CI variability
expect(elapsedMs).toBeLessThan(5000);
// Log performance for visibility in test output
console.log(
`[STRESS TEST] 10,000 operations completed in ${elapsedMs.toFixed(2)}ms`,
);
// All operations should have been applied
expect(mockReducer).toHaveBeenCalledTimes(10000);
// Final state should reflect last update
const taskState = (result as Partial<RootState>)[TASK_FEATURE_NAME];
expect(taskState?.entities[TASK_ID]?.title).toBe('Update 9999');
// Verify some created tasks exist
expect(taskState?.entities['task-0']).toBeDefined();
expect(taskState?.entities['task-9990']).toBeDefined();
},
);
// Stress test: Skip by default, set RUN_STRESS_TESTS=true to enable
stressTest(
'should maintain O(n) performance - 20k ops should take ~2x 10k ops',
() => {
const reducer = bulkHydrationMetaReducer(mockReducer);
// Measure 5k ops
const state5k = createMockState();
const ops5k: Operation[] = [];
for (let i = 0; i < 5000; i++) {
ops5k.push(
createMockOperation({
id: `op-create-${i}`,
opType: OpType.Create,
entityId: newTaskId,
actionType: '[Task] Add Task' as ActionType,
payload: { task: createMockTask({ id: newTaskId, title: `Task ${i}` }) },
}),
);
} else {
// Regular update operation
operations.push(
createMockOperation({
id: `op-update-${i}`,
id: `op-5k-${i}`,
payload: { task: { id: TASK_ID, changes: { title: `Update ${i}` } } },
}),
);
}
}
const action = bulkApplyHydrationOperations({ operations });
const startTime = performance.now();
const result = reducer(state, action);
const endTime = performance.now();
const elapsedMs = endTime - startTime;
const start5k = performance.now();
reducer(state5k, bulkApplyHydrationOperations({ operations: ops5k }));
const time5k = performance.now() - start5k;
// Should complete in under 5 seconds even with 10k ops
// This is generous to account for CI variability
expect(elapsedMs).toBeLessThan(5000);
// Reset mock
mockReducer.calls.reset();
// Log performance for visibility in test output
console.log(
`[STRESS TEST] 10,000 operations completed in ${elapsedMs.toFixed(2)}ms`,
);
// Measure 20k ops
const state20k = createMockState();
const ops20k: Operation[] = [];
for (let i = 0; i < 20000; i++) {
ops20k.push(
createMockOperation({
id: `op-20k-${i}`,
payload: { task: { id: TASK_ID, changes: { title: `Update ${i}` } } },
}),
);
}
// All operations should have been applied
expect(mockReducer).toHaveBeenCalledTimes(10000);
const start20k = performance.now();
reducer(state20k, bulkApplyHydrationOperations({ operations: ops20k }));
const time20k = performance.now() - start20k;
// Final state should reflect last update
const taskState = (result as Partial<RootState>)[TASK_FEATURE_NAME];
expect(taskState?.entities[TASK_ID]?.title).toBe('Update 9999');
// Verify some created tasks exist
expect(taskState?.entities['task-0']).toBeDefined();
expect(taskState?.entities['task-9990']).toBeDefined();
});
it('should maintain O(n) performance - 20k ops should take ~2x 10k ops', () => {
const reducer = bulkHydrationMetaReducer(mockReducer);
// Measure 5k ops
const state5k = createMockState();
const ops5k: Operation[] = [];
for (let i = 0; i < 5000; i++) {
ops5k.push(
createMockOperation({
id: `op-5k-${i}`,
payload: { task: { id: TASK_ID, changes: { title: `Update ${i}` } } },
}),
console.log(
`[PERF TEST] 5k ops: ${time5k.toFixed(2)}ms, 20k ops: ${time20k.toFixed(2)}ms, ratio: ${(time20k / time5k).toFixed(2)}x`,
);
}
const start5k = performance.now();
reducer(state5k, bulkApplyHydrationOperations({ operations: ops5k }));
const time5k = performance.now() - start5k;
// Reset mock
mockReducer.calls.reset();
// Measure 20k ops
const state20k = createMockState();
const ops20k: Operation[] = [];
for (let i = 0; i < 20000; i++) {
ops20k.push(
createMockOperation({
id: `op-20k-${i}`,
payload: { task: { id: TASK_ID, changes: { title: `Update ${i}` } } },
}),
);
}
const start20k = performance.now();
reducer(state20k, bulkApplyHydrationOperations({ operations: ops20k }));
const time20k = performance.now() - start20k;
console.log(
`[PERF TEST] 5k ops: ${time5k.toFixed(2)}ms, 20k ops: ${time20k.toFixed(2)}ms, ratio: ${(time20k / time5k).toFixed(2)}x`,
);
// 20k should be roughly 4x 5k (linear scaling)
// We allow up to 20x to account for overhead, cache effects, and CI variability
// macOS CI has shown ratios up to ~15.5x, so we need a generous threshold
const ratio = time20k / time5k;
expect(ratio).toBeLessThan(20);
});
// 20k should be roughly 4x 5k (linear scaling)
// We allow up to 20x to account for overhead, cache effects, and CI variability
// macOS CI has shown ratios up to ~15.5x, so we need a generous threshold
const ratio = time20k / time5k;
expect(ratio).toBeLessThan(20);
},
);
});
describe('undefined state handling', () => {

View file

@ -717,6 +717,67 @@ describe('ConflictResolutionService', () => {
expect(mockOpLogStore.markRejected).toHaveBeenCalledWith(['local-del']);
});
it('should extract entity from DELETE payload when UPDATE wins but entity not in store', () => {
// This tests the helper method that extracts entity state from DELETE operations
// Used when remote DELETE is applied first, then local UPDATE wins LWW
const taskEntity = {
id: 'task-1',
title: 'Test Task',
projectId: 'project-1',
tagIds: [],
};
const conflict: EntityConflict = createConflict(
'task-1',
[
{
...createOpWithTimestamp('local-upd', 'client-a', Date.now()),
opType: OpType.Update,
payload: { task: taskEntity },
},
],
[
{
...createOpWithTimestamp('remote-del', 'client-b', Date.now() - 1000),
opType: OpType.Delete,
payload: { task: taskEntity }, // DELETE payload contains the deleted entity
},
],
);
// Call the private extraction method
const extractedEntity = (service as any)._extractEntityFromDeleteOperation(
conflict,
);
// Verify it extracted the entity from the DELETE operation's payload
expect(extractedEntity).toEqual(taskEntity);
});
it('should return undefined when no DELETE operation in conflict', () => {
const conflict: EntityConflict = createConflict(
'task-1',
[
{
...createOpWithTimestamp('local-upd', 'client-a', Date.now()),
opType: OpType.Update,
},
],
[
{
...createOpWithTimestamp('remote-upd', 'client-b', Date.now() - 1000),
opType: OpType.Update,
},
],
);
const extractedEntity = (service as any)._extractEntityFromDeleteOperation(
conflict,
);
expect(extractedEntity).toBeUndefined();
});
it('should handle CREATE vs CREATE conflict using LWW', async () => {
// Two clients create entity with same ID (rare but possible)
const now = Date.now();

View file

@ -550,17 +550,29 @@ export class ConflictResolutionService {
conflict: EntityConflict,
): Promise<Operation | undefined> {
// Get current entity state from store
const entityState = await this.getCurrentEntityState(
let entityState = await this.getCurrentEntityState(
conflict.entityType,
conflict.entityId,
);
if (entityState === undefined) {
OpLog.warn(
`ConflictResolutionService: Cannot create local-win op - entity not found: ` +
`${conflict.entityType}:${conflict.entityId}`,
);
return undefined;
// Try to extract entity from remote DELETE operation
// This handles the case where a remote DELETE was applied before LWW resolution,
// and the local UPDATE wins. We need to recreate the entity from the DELETE payload.
entityState = this._extractEntityFromDeleteOperation(conflict);
if (entityState !== undefined) {
OpLog.warn(
`ConflictResolutionService: Extracted entity from DELETE op for LWW update: ` +
`${conflict.entityType}:${conflict.entityId}`,
);
} else {
OpLog.warn(
`ConflictResolutionService: Cannot create local-win op - entity not found: ` +
`${conflict.entityType}:${conflict.entityId}`,
);
return undefined;
}
}
// Get client ID
@ -596,6 +608,35 @@ export class ConflictResolutionService {
);
}
/**
* Extracts entity state from a remote DELETE operation payload.
*
* When a remote DELETE wins the conflict but we need the entity state for LWW resolution,
* we can extract it from the DELETE operation's payload (which contains the deleted entity).
*
* @param conflict - The conflict containing remote DELETE operation
* @returns Entity state from DELETE payload, or undefined if not found
*/
private _extractEntityFromDeleteOperation(
conflict: EntityConflict,
): unknown | undefined {
// Find the DELETE operation in remote ops
const deleteOp = conflict.remoteOps.find((op) => op.opType === OpType.Delete);
if (!deleteOp) {
return undefined;
}
// Extract entity from payload based on entity type
// For TASK: payload.task
// For PROJECT: payload.project
// For TAG: payload.tag
// etc.
const payload = deleteOp.payload as Record<string, unknown>;
const entityKey = conflict.entityType.toLowerCase();
return payload[entityKey];
}
/**
* Gets the current state of an entity from the NgRx store.
* Uses the entity registry to look up the appropriate selector.

View file

@ -6,7 +6,7 @@ import { MockSyncServer } from './helpers/mock-sync-server.helper';
import { SimulatedClient } from './helpers/simulated-client.helper';
import { createMinimalTaskPayload } from './helpers/operation-factory.helper';
// Timeout constants for large batch tests - creating/syncing 500-1500 ops
// Timeout constants for large batch tests - creating/syncing 500-1000 ops
// can exceed the default 2000ms timeout under load
const LARGE_BATCH_TIMEOUT = 15000;
@ -75,11 +75,11 @@ describe('Large Batch Sync Integration', () => {
describe('Large Batch Download (Pagination)', () => {
it(
'should download 1500 operations using pagination',
'should download 1000 operations using pagination',
async () => {
const clientA = new SimulatedClient('client-a', storeService);
const clientB = new SimulatedClient('client-b', storeService);
const totalOps = 1500;
const totalOps = 1000;
// Client A populates server (in batches to avoid timeout during setup)
// Note: We bypass clientA.sync() for speed and populate server directly if possible,
@ -101,7 +101,7 @@ describe('Large Batch Sync Integration', () => {
expect(server.getAllOps().length).toBe(totalOps);
// Client B syncs - should download all 1500
// Client B syncs - should download all 1000
// Mock server default limit is 500, so this should trigger multiple internal fetches
// if SimulatedClient/SyncService handles it, OR we have to call sync multiple times.
//
@ -117,13 +117,9 @@ describe('Large Batch Sync Integration', () => {
const result2 = await clientB.sync(server);
expect(result2.downloaded).toBe(500);
// Third sync
// Third sync - empty
const result3 = await clientB.sync(server);
expect(result3.downloaded).toBe(500);
// Fourth sync - empty
const result4 = await clientB.sync(server);
expect(result4.downloaded).toBe(0);
expect(result3.downloaded).toBe(0);
// Verify total
const allOps = await clientB.getAllOps();

View file

@ -63,9 +63,9 @@ describe('Performance Integration', () => {
});
describe('Large operation log handling', () => {
it('should handle 1000 operations efficiently', async () => {
it('should handle 500 operations efficiently', async () => {
const client = new TestClient('client-test');
const operationCount = 1000;
const operationCount = 500;
const writeStartTime = Date.now();
@ -104,7 +104,7 @@ describe('Performance Integration', () => {
it('should maintain sequence integrity under load', async () => {
const client = new TestClient('client-test');
const operationCount = 500;
const operationCount = 250;
for (let i = 0; i < operationCount; i++) {
await storeService.append(
@ -115,14 +115,26 @@ describe('Performance Integration', () => {
const ops = await storeService.getOpsAfterSeq(0);
// Verify we have exactly the expected number of operations (test isolation check)
expect(ops.length)
.withContext(
`Expected exactly ${operationCount} operations, but found ${ops.length}. ` +
`This suggests database cleanup failed or tests are interfering with each other.`,
)
.toBe(operationCount);
// Verify strict sequence ordering
for (let i = 1; i < ops.length; i++) {
expect(ops[i].seq).toBeGreaterThan(ops[i - 1].seq);
expect(ops[i].seq)
.withContext(`Sequence at index ${i} should be greater than previous`)
.toBeGreaterThan(ops[i - 1].seq);
}
// Verify vector clock progression
for (let i = 0; i < ops.length; i++) {
expect(ops[i].op.vectorClock['client-test']).toBe(i + 1);
expect(ops[i].op.vectorClock['client-test'])
.withContext(`Vector clock at operation ${i} (seq: ${ops[i].seq})`)
.toBe(i + 1);
}
});
});
@ -215,7 +227,7 @@ describe('Performance Integration', () => {
describe('Sync batch performance', () => {
it('should mark operations as synced efficiently', async () => {
const client = new TestClient('client-test');
const operationCount = 500;
const operationCount = 250;
// Create operations
for (let i = 0; i < operationCount; i++) {
@ -245,7 +257,7 @@ describe('Performance Integration', () => {
const client = new TestClient('client-test');
// Create mix of synced and unsynced
for (let i = 0; i < 300; i++) {
for (let i = 0; i < 150; i++) {
await storeService.append(
createTaskOperation(client, `task-${i}`, OpType.Create, { title: `Task ${i}` }),
'local',
@ -254,7 +266,7 @@ describe('Performance Integration', () => {
const allOps = await storeService.getOpsAfterSeq(0);
// Mark first half as synced
const syncedSeqs = allOps.slice(0, 150).map((op) => op.seq);
const syncedSeqs = allOps.slice(0, 75).map((op) => op.seq);
await storeService.markSynced(syncedSeqs);
// Measure unsynced query
@ -262,16 +274,16 @@ describe('Performance Integration', () => {
const unsynced = await storeService.getUnsynced();
const queryDuration = Date.now() - queryStart;
expect(unsynced.length).toBe(150);
expect(unsynced.length).toBe(75);
expect(queryDuration).toBeLessThan(1000); // < 1 second
console.log(`Get unsynced: ${queryDuration}ms for 150 of 300 ops`);
console.log(`Get unsynced: ${queryDuration}ms for 75 of 150 ops`);
});
});
describe('Compaction performance', () => {
it('should compact operations efficiently', async () => {
const client = new TestClient('client-test');
const operationCount = 500;
const operationCount = 250;
// Create operations
for (let i = 0; i < operationCount; i++) {

View file

@ -69,11 +69,17 @@
background-color: var(--bg-lightest);
display: flex;
flex-direction: column;
// Allow proper flex shrinking
min-height: 0;
overflow: hidden;
.editor-section {
display: flex;
flex-direction: column;
flex-grow: 1;
// Allow proper flex shrinking
min-height: 0;
overflow: hidden;
textarea {
flex-grow: 1;
@ -87,6 +93,8 @@
display: block;
resize: none;
font-size: 14px;
// Allow proper flex shrinking
min-height: 0;
@include scrollY;

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zM5 8V6h14v2H5zm2 4h10v2H7v-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 252 B

View file

@ -1,6 +1,6 @@
// this file is automatically generated by git.version.ts script
export const versions = {
version: '17.0.0-RC.12',
version: '17.0.0-RC.13',
revision: 'NO_REV',
branch: 'NO_BRANCH',
};

View file

@ -17,9 +17,9 @@
@include material-icon.materialIconBase();
// Set default font variation settings for consistent appearance
// FILL: 1 (filled), wght: 400 (normal), GRAD: 0 (default), opsz: 24 (24px)
// FILL: 0 (outlined), wght: 400 (normal), GRAD: 0 (default), opsz: 24 (24px)
font-variation-settings:
'FILL' 1,
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;

View file

@ -23,7 +23,7 @@
"interactive": ["warn", { "maxNumericValue": 5000 }],
"resource-summary.script.count": ["warn", { "maxNumericValue": 140 }],
"resource-summary.total.count": ["warn", { "maxNumericValue": 150 }],
"resource-summary.font.size": ["warn", { "maxNumericValue": 260000 }]
"resource-summary.font.size": ["warn", { "maxNumericValue": 520000 }]
}
},
"upload": {

View file

@ -16,7 +16,7 @@
},
{
"resourceType": "font",
"budget": 260
"budget": 520
},
{
"resourceType": "total",