mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
Merge branch 'super-productivity:master' into patch-2
This commit is contained in:
commit
663a0ec914
87 changed files with 1341 additions and 707 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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() }}
|
||||
|
|
|
|||
3
.github/workflows/claude-code-review.yml
vendored
3
.github/workflows/claude-code-review.yml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
135
CHANGELOG.md
135
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 what’s necessary and nothing more. Your guidance must remain focused on achieving the stated learning objective.
|
||||
- Explain what’s necessary and nothing more. Your guidance must remain focused on achieving the stated learning objective.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# Configure Data Synchronization
|
||||
|
|
@ -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>
|
||||
[](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>
|
||||
[](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>
|
||||
[](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>
|
||||
[](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>
|
||||
[](https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity)
|
||||
|
||||
[](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
|
||||
|
|
|
|||
|
|
@ -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.”
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
[](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
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
[](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>
|
||||
[](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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
[](https://apps.apple.com/de/app/super-productivity/id1482572463?l=en&mt=12)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
[](https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity)
|
||||
|
||||
The sources can be [[../../android|found here]].
|
||||
[](https://f-droid.org/en/packages/com.superproductivity.superproductivity)
|
||||
|
||||
The sources can be [[../../android|found here]].
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# Manage Subtasks
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Manage Scheduled Tasks
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Manage Repeating Tasks
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Manage Task Integrations
|
||||
|
|
@ -1 +1,3 @@
|
|||
Stub.
|
||||
# Choose Sync Backend
|
||||
|
||||
Stub.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# Configure Sync Backend
|
||||
|
||||
- 2.09a: WebDAV
|
||||
- 2.09b: Dropbox
|
||||
- 2.09c: Other/Custom
|
||||
- 2.09c: Other/Custom
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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 product’s structure** so the user can work their way through it simultaneously. It doesn’t mean forcing the documentation into an unnatural structure. What’s important is that the documentation should help make sense of the product.
|
||||
- **Be consistent** in structure, language, terminology, and tone.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
Undocumented.
|
||||
# Api
|
||||
|
||||
Undocumented.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.”
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
Footer. Any valid markdown can go here.
|
||||
# Footer
|
||||
|
||||
Footer. Any valid markdown can go here.
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
Sidebar. Any valid markdown can go here.
|
||||
# Sidebar
|
||||
|
||||
Sidebar. Any valid markdown can go here.
|
||||
|
|
|
|||
1
e2e/.gitignore
vendored
1
e2e/.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
start-test-server.shscreenshots/
|
||||
/screenshots/
|
||||
|
|
|
|||
85
e2e/fixtures/webdav.fixture.ts
Normal file
85
e2e/fixtures/webdav.fixture.ts
Normal 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';
|
||||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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/*"
|
||||
|
|
|
|||
|
|
@ -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
20
scripts/wait-for-supersync.sh
Executable 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!"
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
3
src/assets/icons/calendar.svg
Normal file
3
src/assets/icons/calendar.svg
Normal 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 |
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
},
|
||||
{
|
||||
"resourceType": "font",
|
||||
"budget": 260
|
||||
"budget": 520
|
||||
},
|
||||
{
|
||||
"resourceType": "total",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue