diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7e854696..14e3234ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ env: on: pull_request: + merge_group: push: tags: - v* diff --git a/CHANGELOG.md b/CHANGELOG.md index f9da1371c..1b7ae0e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,258 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). All releases can be found on https://code.vikunja.io/vikunja/releases. +## [2.2.0] - 2026-03-20 + +### Bug Fixes + +* *(attachments)* Sync kanban store and task ref on attachment changes +* *(auth)* Use SameSite=None for refresh token cookie to fix desktop app +* *(auth)* Make SameSite=None conditional on HTTPS for refresh cookie +* *(caldav)* Eliminate nested db session in CalDAV auth +* *(caldav)* Parse timestamps in configured timezone +* *(caldav)* Use /dav/projects/ as home to make iOS/MacOS reminders work (#2417) +* *(ci)* Remove HTML comments inside table that break markdown rendering +* *(cli)* Make user deletion confirmation check Windows compatible (#2339) +* *(db)* Prevent SQLite "database is locked" errors under concurrent writes +* *(db)* Use immediate txlock for SQLite instead of MaxOpenConns(1) +* *(db)* Use WAL mode for SQLite and temp file for ephemeral databases +* *(desktop)* Disable nodeIntegration and enable contextIsolation/sandbox +* *(desktop)* Validate URL schemes before shell.openExternal +* *(desktop)* Block same-window navigation to external origins +* *(docker)* Remove COPY for deleted patches directory +* *(e2e)* Drain event handlers and stop browser between tests +* *(events)* Defer task event dispatch until after transaction commit +* *(events)* Defer event dispatch for task sub-entities +* *(events)* Defer event dispatch for project operations +* *(events)* Defer event dispatch for team operations +* *(events)* Defer event dispatch for user creation and task positions +* *(events)* Dispatch pending events in CalDAV handlers after commit +* *(events)* Dispatch pending events in migration and export handlers +* *(frontend)* Add horizontal overflow handling to tables on mobile +* *(frontend)* Use semantic class instead of targeting Tailwind utility +* *(frontend)* Use mbs-2 utility class instead of scoped CSS +* *(gantt)* Always show relation arrows and fix arrow Y positioning +* *(gantt)* Update relation arrows in real-time during drag and resize +* *(gantt)* Make relation arrows smaller and dash precedes lines +* *(gantt)* Spread overlapping relation arrows at shared endpoints +* *(gantt)* Improve parent task bar styling and visual grouping +* *(gantt)* Make collapse/expand triangle smaller +* *(gantt)* Move parent diamonds outward with stroke and remove hover effect +* *(gantt)* Only set hasDerivedDates when children have actual dates +* *(gantt)* Clamp collapse chevron x position to prevent negative offset +* *(gantt)* Remove unreachable hover rule on relation arrows +* *(gantt)* Render collapse chevron after bars for correct SVG paint order +* *(menu)* Prevent dropdown from closing when cursor crosses offset gap (#2367) +* *(menu)* Show all project menu items in sidebar dropdown +* *(migration)* Support space-separated date format in TickTick importer +* *(nav)* Project drag handle position +* *(shortcuts)* Resolve lint errors in shortcut module +* *(shortcuts)* Track active sequences explicitly to prevent misfires +* *(tasks)* Support both expand and expand[] query parameter formats (#2415) +* *(test)* Update mobile kanban test to use close button instead of back button +* *(views)* Assign default position when creating new project views +* Use MinPositionSpacing threshold in calculateNewPositionForTask (#2320) ([3ca4913](3ca4913fcb6dc287adec552dd62024a3b63f477a)) +* Remove invalidateAvatarCache call that broke request deduplication (#2317) ([7297682](7297682cadae3e2c48f2a09d20a6191b561c1eeb)) +* Add /tmp directory to Docker image to fix data export ([84d563c](84d563c51b6cd15000f4af6e058362c5e45c8dc2)) +* Update old kolaente.dev URLs to code.vikunja.io (#2342) ([a160048](a160048cc3259773405654746117bf6dc0565eee)) +* Validate default settings timezone on startup (#2345) ([40bcf2b](40bcf2b36f777c6338a40581a472333974770c93)) +* Correct package.json indentation after dependency removal ([f8763d8](f8763d812e2a7c7f9b2d28ff3e502693419f859e)) +* Remove duplicate close button on mobile task detail view ([8a4f3a9](8a4f3a916f2eae71f0106c42d257b5ee4dc77928)) +* Prevent nil pointer panic in mention notification listeners ([18f1687](18f16878a84952cf5d0ddb583385dc340d1f5ff3)) +* Only drop Vikunja-owned tables in WipeEverything ([14e2c95](14e2c95a830eb4206390a58f85b4bc49068f23cd)) +* Only dump Vikunja-owned tables ([cd7d405](cd7d40583aaa43e1d9445e9f54ea81d14eb12232)) +* Remove debug log statements from task duplicate ([6da0f68](6da0f685624c66806027070d537648be9b100e29)) +* Close source file handle when duplicating attachments ([7aad96b](7aad96b1991a981245cc119bce189de327ea36ce)) +* Preserve cover image when duplicating task ([9c23e19](9c23e196440830d0b94ca18bfb1002a0db27b54c)) +* Allow browser caching for file downloads (#2349) ([54d9775](54d977532e9e9a99281bc56965583d07f3913b21)) +* Handle deleted user in saved filter view event listener ([7288483](72884838790db52852c8643ab17be5f6fc0067f0)) +* Include remote IP address in HTTP request logs ([f9cb0a2](f9cb0a2de1d7ed64aa04f74f4209f117ea60186f)) +* Use ParadeDB v2 fuzzy prefix matching for search (#2346) ([0a38ec0](0a38ec08388c9d2716f9e41185af0bcfb0ed7f8d)) +* Prefer working directory for service.rootpath default ([d3cbc4f](d3cbc4fc4fb7d7fe054c4c022656f2b4d5c42bde)) +* Ensure /tmp is writable by container user in Docker image ([f497e8b](f497e8bb6d78f3b01c2a87540e28d7727e17676e)) +* Remove debounce from color picker to prevent stale color on save ([d196af0](d196af0503053d00e05afb8d2585a67b229a5144)) +* Send account deletion notification before deleting user row ([79a612a](79a612aa5d95f89cd84148295146a92ccddefa74)) +* Register bulk label route correctly for API token permissions ([e19bea8](e19bea8e3a2804485479748b1c91dc58719dbe11)) +* Prevent authenticated UI flash when server rejects JWT session (#2387) ([28cc9e0](28cc9e0571c98bb04d216e5fe47aaa503a1e887b)) +* Preserve CalDAV inverse relations when parent has no RELATED-TO (#2389) ([ada2eba](ada2ebab9e1738bb145db1c498d2dda84d11c10b)) +* Collapse view buttons into dropdown when overflowing (#2306) ([7b6b432](7b6b4323015239098a55adcb134d12dc9785f5cb)) +* Invalidate all sessions when enabling TOTP ([3bc0093](3bc009368628fb286632b456f9bf2d575a8bfa43)) +* Make mage fmt skip gitignored files ([e74265d](e74265d921b9b12bf89882e791743758b42f5f3d)) +* Ensure frontend dist directory exists for lint and fmt commands ([c62b7e6](c62b7e680f82253d89f8cefbfe4bb4b4bb64c5e9)) +* Handle S3 backend in user export download ([b0ede53](b0ede53c051d45a3e861450187e64c5342be5362)) +* Use file mime type instead of hardcoded application/zip in S3 export ([4cd63f9](4cd63f93a48d784dd2566c26a0642ec0c69d3d8f)) +* Configure Echo IPExtractor to prevent rate limit bypass via spoofed headers ([a498dd6](a498dd69915a006c07e9d82660a2185d7e8136ee)) +* Block login for StatusAccountLocked users ([4c80932](4c80932b6475ad54a2e2a81541d89a3b8471a762)) +* Prevent password reset from re-enabling admin-disabled accounts ([d8570c6](d8570c603da1f26635ce6048d6af85ede827abfb)) +* Reject password reset token requests for disabled users ([708ccab](708ccab895a23ed59b330db4a58a441bf5fbfcb2)) +* Prevent email confirmation from re-enabling admin-disabled accounts ([049f4a6](049f4a6be46f9460bd516f489ef9f569574bc70d)) +* Update test expectations for new disabled user fixture ([89923eb](89923ebe7090038c57ee3ad23eca86858c9c2eca)) +* Reject images exceeding 50M pixels before decode ([af61d0f](af61d0f1a0d6e9394546d2d64dff043cfbe641f7)) +* Adapt image preview DoS protection to new FileStorage interface ([be0aaa7](be0aaa70601af919f68fa1153f76bcf6335bc0b5)) +* Verify comment belongs to task in URL to prevent IDOR ([bc6d843](bc6d843ed4df82a6c89f10aa676a7a33d27bf2fd)) +* Require CanUpdate for project background deletion ([f066eb3](f066eb3ea4d1648ef925a745836e48a71b600a5f)) +* Only enforce task_id check when TaskID is provided ([4941961](49419619bd0052bdd7e727404a9284acd928a903)) +* Use require.Error instead of assert.Error for error assertions ([b7a1408](b7a14080983d2781e1428be9b77fae319e7788e4)) +* Reject CalDAV basic auth when TOTP is enabled ([cdf5d30](cdf5d30a425d032f749b78b98b828f25ad882615)) +* Use user10 instead of user1 for TOTP fixture to avoid breaking login tests ([659e73a](659e73af05af154dda315d025e8b3a12705e4a7e)) +* Update TOTP fixtures and tests to avoid conflicts with existing enrollment tests ([1ed813c](1ed813caf00224d90c3c89c5b8078788f5730f51)) + +### Dependencies + +* *(deps)* Update dev-dependencies +* *(deps)* Upgrade serialize-javascript to 7.0.3 +* *(deps)* Update dependency @vue/tsconfig to v0.9.0 +* *(deps)* Use forked afero-s3 to fix S3 read performance regression (#2313) +* *(deps)* Update dependency flexsearch to v0.8.212 +* *(deps)* Remove obsolete flexsearch 0.7.43 patch +* *(deps)* Remove @github/hotkey dependency +* *(deps)* Update dependency rollup-plugin-visualizer to v6.0.11 +* *(deps)* Update dependency electron to v40.7.0 +* *(deps)* Update immutable to 5.1.5 +* *(deps)* Update svgo to 3.3.3 +* *(deps)* Update tar to 7.5.10 and @tootallnate/once to 3.0.1 in desktop +* *(deps)* Update dependency vite-svg-loader to v5.1.1 +* *(deps)* Bump dompurify from 3.3.1 to 3.3.2 in /frontend +* *(deps)* Update dependency eslint to v9.39.4 +* *(deps)* Update dev-dependencies to v8.57.0 +* *(deps)* Update dependency sass-embedded to v1.98.0 +* *(deps)* Update dev-dependencies (#2395) +* *(deps)* Update dependency caniuse-lite to v1.0.30001779 +* *(deps)* Override flatted to 3.4.1 to fix unbounded recursion DoS +* *(deps)* Update tar override to 7.5.11 to fix symlink path traversal +* *(deps)* Update dependency vue-tsc to v3.2.6 +* *(deps)* Update dependency electron to v40.8.3 +* *(deps)* Update dev-dependencies to v4.2.2 +* *(deps)* Add daenney/ssrf for webhook SSRF protection +* *(deps)* Update dependency stylelint to v17.5.0 + +### Documentation + +* Update user search endpoint description for external team bypass ([b5086fe](b5086febc71a80467302584b9d41e10459d9d77e)) +* Update rootpath description to mention working directory default ([ddfc565](ddfc565c614761d3dda037902c8309bf5a27fdd1)) +* Document database.schema config option for PostgreSQL ([8868b21](8868b214ca2f0b34a6506066af1c4c96e13ca40d)) +* Document IP extraction and trusted proxy config options ([015a172](015a172c2a07d3fc3827645d9e1bfe986ee58a03)) + +### Features + +* *(ci)* Post preview deployment comment on PRs +* *(ci)* Enable merge queue trigger +* *(config)* Add webhooks.allownonroutableips setting +* *(events)* Add DispatchOnCommit/DispatchPending for deferred event dispatch +* *(frontend)* Upgrade Tailwind CSS from v3 to v4 +* *(frontend)* Highlight overdue tasks consistently (#958) +* *(gantt)* Add expand=subtasks to Gantt API params +* *(gantt)* Add task tree builder utility for hierarchy +* *(gantt)* Add dependency arrow data builder +* *(gantt)* Integrate task tree into Gantt rendering with collapse +* *(gantt)* Add collapse/expand chevron and indent indicators +* *(gantt)* Render parent summary bars with diamond endpoints +* *(gantt)* Create arrow SVG overlay component for relations +* *(gantt)* Wire relation arrows into GanttChart with toggle +* *(handlers)* Dispatch pending events after transaction commit +* *(release)* Update frontend package.json version on release +* *(shortcuts)* Add event.code-based shortcut module +* *(webhooks)* Add built-in SSRF protection using daenney/ssrf +* Ensure forms submit on Enter (#959) ([e1d1e7c](e1d1e7c848bb2f0062a5fa522c7a357a2d3c723f)) +* Use offical vite plugin for sentry (#873) ([0a9586e](0a9586e8d4351e47edacb63fa6667193d99ff7ee)) +* Mini tiptap improvements ([b92735b](b92735b0e907bf7613b106ea633b82efa7f1781a)) +* Surface API validation errors to registration form fields (#1902) ([c6f0d8b](c6f0d8babe6f36e6d25d22a932c9f0a075a5a359)) +* Add table registration to db package ([d26936f](d26936f869c8489b06b0d9377af489236765a9e1)) +* Register Vikunja tables with db package at init ([3dd2ba4](3dd2ba4aa4309b589e809621de2ecee89ee54159)) +* Add RegisteredTableNames helper to db package ([0a8534d](0a8534ded9fca162fb1721a86d835677b30f2cdb)) +* Add task duplicate backend model and tests ([d8f3a96](d8f3a96b06fc40d4b30954cc71a3bb43890f8cfc)) +* Register task duplicate API route ([77fdf1b](77fdf1b84b27f80f4f332a26e9d7cf1ad032f211)) +* Add task duplicate frontend model and service ([52bee37](52bee379d417d37b21b3d6f0cac8e67f83716925)) +* Add duplicateTask action to task store ([2014d50](2014d50b953f86fb5a66bf32c74035b8d42c2e7a)) +* Add duplicate button to task detail view ([6c9407c](6c9407c58f4ed01c0eac37aa51e7939cd5a11a1d)) +* Bypass discoverability settings for external team members ([28b913f](28b913f29f812ef51f3b8fe967d5560c1d8ed927)) +* Add InitEventsForTesting and Unfake for real event dispatch in tests ([1b1e8e5](1b1e8e5b19e9dd32a0d6089759d18c81883f8ffc)) +* Add mage test:e2e-api target for e2e API tests ([24b800d](24b800d48d27a90447bfb9765f23093e5b9bde41)) +* Add conversational email template and rendering ([d4b0302](d4b03026f0b98734a95e9cc22d3e77e89a7d3f4f)) +* Convert notifications to conversational email style ([b3572c5](b3572c5932ba9eb7159e48129c1e52f0333cf96e)) +* Add translation keys for conversational emails ([def73e2](def73e2f8eeadf807c9b2e2a422e2335444280dd)) +* Add user_id to webhooks and user-directed event infrastructure ([d4577c6](d4577c660f5550a59f1b90a2ef1f5fba49cb73c6)) +* Extend WebhookListener for user-level webhooks ([dbbc80a](dbbc80aea613779d43b015479fef0f7301d8e7e2)) +* Add API routes for user-level webhooks ([47a0775](47a0775c7378faf6c8b3af3cd1429d3be7c51e70)) +* Add user-level webhooks settings page ([2e1648e](2e1648ef4c7b1d1a05542567cd2a682f1038b03c)) +* Replace afero-s3 with minimal S3 afero.Fs implementation ([b065c62](b065c6200782bfd6e9eea889847e83f1dead906d)) +* Add service.ipextractionmethod and service.trustedproxies config options ([26324a7](26324a740a73d19748eea3c745c74f91f60cc86b)) +* Add StatusAccountLocked user status for TOTP lockouts ([f42a045](f42a045bdc175fbffee4f8ee9592fa8dfedbc8aa)) + +### Miscellaneous Tasks + +* *(dev)* Update devenv +* *(i18n)* Update translations via Crowdin +* Remove feature request issue template ([06ead58](06ead58ea3bb366970473d587db82bb36db07887)) + +### Other + +* *(other)* [skip ci] Updated swagger docs +* *(other)* Add e2e API tests to CI pipeline +* *(other)* Upgrade ParadeDB image to support v2 fuzzy search API + +### Refactor + +* *(attachments)* Read from task prop instead of global store +* *(attachments)* Return uploaded attachments instead of writing to store +* *(attachments)* Use local state instead of global attachment store +* *(attachments)* Remove global attachment store +* *(shortcuts)* Update directive to use new shortcut module +* *(shortcuts)* Update v-shortcut values to event.code format +* *(shortcuts)* Replace eventToHotkeyString with eventToShortcutString +* *(shortcuts)* Use event.code for raw keyboard handlers +* Batch label inserts during task duplication ([e07eeed](e07eeed21156ab2bdc6c02aceede9cbc91468a28)) +* Use TaskRelation.Create for copy relation ([692357a](692357a648367f1beb9ba192e3ed3425f8648893)) +* Move ListUsers tests from pkg/user to pkg/models ([54c7c4a](54c7c4aef2fbdf7d4c04630d75cd36a0d121daec)) +* Enable golangci-lint on magefile, fix errors ([cea8c78](cea8c7807d060e0a187c37c80ba42d02d4aa7637)) +* Fix contextcheck lint errors on magefile by passing mage context ([0a1104b](0a1104b75ce1a6fcadb0cd0678400cf3585a0eb1)) +* Merge last unique build tag "tools" into go.mod tools section ([1b5f3f4](1b5f3f4ccd15a954d1b3ac4fa49a99c2f299deff)) +* Add centralized ResolvePath for rootpath-relative paths ([2a7165a](2a7165aaba736c53be32bb8cf0cf77e6fb7cd501)) +* Use config.ResolvePath for all rootpath-relative paths ([a043940](a043940e14f686faa15339ecc06f91dd191d22d1)) +* Replace afero with FileStorage interface ([0e1f44e](0e1f44e57efe06d08a47d980fa49bdd260f5fac3)) +* Use StatusAccountLocked for TOTP lockouts ([7792bf6](7792bf6cea36ede6c38b9966f587222b476176cb)) +* Rename checkProjectBackgroundWriteRights to checkProjectBackgroundWritePermissions ([4b91e5e](4b91e5efa173c90346567d4b296ab6233a9cc093)) + +### Styling + +* Fix alignment in config key declarations ([ddd9ef5](ddd9ef5f2206dc5936cc14d359c70312806de233)) + +### Testing + +* *(shortcuts)* Add unit tests for shortcut parsing logic +* *(webhooks)* Add SSRF protection tests +* *(webhooks)* Allow non-routable IPs in E2E tests +* Update event assertions to work with deferred dispatch ([f516bbe](f516bbe560a7b2a0d348e71ecdab00229c5cf554)) +* Add web integration tests for task duplication ([4d494ba](4d494ba442b7bc6b4d7d06a3a3919f8d1bc6e066)) +* Add user 11 to external team 14 for discoverability tests ([64e455a](64e455a613134b74c5734570eef19f3631253738)) +* Add tests for external team user discoverability bypass ([3a73016](3a730165bc15f0fa2593aa8961e27192e93fcafb)) +* Verify email masking for external team name search ([0661789](06617891fafa7c73c1c7110d404cb0a76812842d)) +* Add e2e API test package with webhook pipeline verification ([1f3509b](1f3509bf27a9102ac96578d441d3731fb444dfa9)) +* Add fixture task with compound word for prefix search testing ([275f714](275f714224cc93f0f9cd7b4590ba2b07a79398e4)) +* Add web tests for prefix/substring search (#2346) ([892b38b](892b38b3b696e024e673dba3c0e302d5afa714fe)) +* Rewrite MultiFieldSearch tests with SQL output verification ([ee2723d](ee2723d9cf3c603bd22be9e5411d67f1c9f38799)) +* Call real MultiFieldSearch function and branch on db engine ([e6cbd67](e6cbd67ab52e92afadeaf0e9b3dbd96de3b3e1c1)) +* Add task #48 to expected results in feature tests ([3568aaa](3568aaacee6d102ec8b749409cb1c8ca73c096f8)) +* Adjust ParadeDB search tests for fuzzy prefix match broadening ([6268c48](6268c48f15955d812c6a569edb9c2d56e454fc27)) +* Fix lint and adjust project search test for ParadeDB fuzzy matching ([b69705e](b69705e64bc45b93a834f877936aea5a7886bd9a)) +* Add result count assertions for ParadeDB search tests ([c7c63e8](c7c63e8eadb174d163516590ec5c7ed945670cd5)) +* Fix non-ParadeDB project search count assertion ([df0e3a8](df0e3a84a9cdf94b8a3f581ab7bf1690d36a6fe9)) +* Fix ParadeDB project search count to 27 ([d36ac9d](d36ac9ddda5ddbc781a06017ee6d45ff2f8a45d8)) +* Add tests for conversational email system ([aacf650](aacf650ec2c2817447107043620989d1b4c72130)) +* Add e2e tests for user-level webhooks ([05cc65f](05cc65fe9e4fa448cda437d58480a9f3f19d69ed)) +* Add web tests for bulk label task endpoint ([675dfb3](675dfb3ea47dd882de7e49ab1b0ace79a5e8bb9b)) +* Add failing test for bulk label API token route registration ([554593c](554593cdb6bc0d31a1809c4b969b4fda9423edc3)) +* Add FileStat assertion to validate storage path in attachment test ([17eccd8](17eccd848fd8688cd18f5dd46d1beb2c6ce96442)) +* Add tests for disabled user password reset prevention ([241b0e8](241b0e80b6d9e91cda1f03a9e3a6368710d1fe36)) +* Add web test for disabled user password reset rejection ([2260d76](2260d763b56290fcf8bfe5a9acfdee1a4332a65e)) +* Add failing test for image preview with oversized dimensions ([f7592e2](f7592e2cfdc11fb06441007a4fb1d2ca5a2f1c5a)) +* Add failing test for task comment IDOR ([2da8925](2da89258e53068253dcf8ef17d4dad141dba7d31)) +* Add failing test for project background delete with read-only access ([f60f3af](f60f3af70b6d8258dd342a9ac15b71f48326e9af)) +* Add TOTP fixture data for user1 ([27ef92b](27ef92b9bf36f437b151df13f801a504e73bddc8)) +* Add failing test for CalDAV 2FA bypass via basic auth ([bda16e7](bda16e770fa76f212d15b1faec5c83f9046a0bb3)) +* Register totp fixture in test setup ([a66bda2](a66bda2f51d4f7df8d353066a100de2d8c0aab32)) +* Verify CalDAV token auth bypasses TOTP check ([1f2aef7](1f2aef776ccdd0ac1405fc8bcbb47084091d42eb)) + ## [2.1.0] - 2026-02-27 ### Bug Fixes diff --git a/README.md b/README.md index c506ccc12..7beee3d78 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://github.com/go-vikunja/vikunja/actions/workflows/ci.yml/badge.svg)](https://github.com/go-vikunja/vikunja/actions/workflows/ci.yml) [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE) -[![Install](https://img.shields.io/badge/download-v2.1.0-brightgreen.svg)](https://vikunja.io/docs/installing) +[![Install](https://img.shields.io/badge/download-v2.2.0-brightgreen.svg)](https://vikunja.io/docs/installing) [![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/vikunja.svg)](https://hub.docker.com/r/vikunja/vikunja/) [![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs) [![Go Report Card](https://goreportcard.com/badge/code.vikunja.io/api)](https://goreportcard.com/report/code.vikunja.io/api) diff --git a/config-raw.json b/config-raw.json index 721bb1a3f..2c3d4e5e7 100644 --- a/config-raw.json +++ b/config-raw.json @@ -147,6 +147,16 @@ "key": "enableopenidteamusersearch", "default_value": "false", "comment": "If enabled, users will only find other users who are part of an existing team when they are searching for a user by their partial name. The other existing team may be created from openid. It is still possible to add users to teams with their exact email address even when this is enabled." + }, + { + "key": "ipextractionmethod", + "default_value": "direct", + "comment": "Method for extracting client IP addresses. 'direct' (default) uses the TCP remote address and ignores forwarding headers — use this when Vikunja faces the internet directly. 'xff' extracts from the X-Forwarded-For header — use this behind proxies like nginx, Traefik, or cloud load balancers. 'realip' extracts from the X-Real-IP header. When using 'xff' or 'realip', configure 'service.trustedproxies' with your proxy CIDR ranges." + }, + { + "key": "trustedproxies", + "default_value": "", + "comment": "Comma-separated list of CIDR ranges for trusted reverse proxies. Only used when service.ipextractionmethod is 'xff' or 'realip'. X-Forwarded-For / X-Real-IP headers are only trusted from these addresses. Example: '127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12'" } ] }, diff --git a/desktop/main.js b/desktop/main.js index 27c107620..f9698ff70 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -12,14 +12,43 @@ function createWindow() { width: 1680, height: 960, webPreferences: { - nodeIntegration: true, + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + webviewTag: false, + navigateOnDragDrop: false, } }) - // Open external links in the browser + // Open external links in the browser, but only allow protocols + // that the TipTap editor also allows (see frontend/src/components/input/editor/TipTap.vue). + // TipTap allows: http, https (built-in) + ftp, git, obsidian, notion, message + // We also allow mailto since it's a standard safe protocol for email links. mainWindow.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url); - return { action: 'deny' }; + try { + const parsedUrl = new URL(url); + const allowedProtocols = [ + 'http:', 'https:', 'mailto:', + 'ftp:', 'git:', 'obsidian:', 'notion:', 'message:', + ]; + if (allowedProtocols.includes(parsedUrl.protocol)) { + shell.openExternal(url); + } + } catch { + // Invalid URL, ignore silently + } + return { action: 'deny' }; + }); + + // Prevent same-window navigation to external origins. + // Only allow navigation to the local express server. + mainWindow.webContents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + // Allow navigations to the local express server + if (parsedUrl.hostname === '127.0.0.1' || parsedUrl.hostname === 'localhost') { + return; + } + event.preventDefault(); }); // Hide the toolbar diff --git a/frontend/package.json b/frontend/package.json index 18cacf499..65870b739 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "vikunja-frontend", "description": "The todo app to organize your life.", "private": true, - "version": "0.10.0", + "version": "2.2.0", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -125,7 +125,7 @@ "@vueuse/shared": "14.2.1", "autoprefixer": "10.4.27", "browserslist": "4.28.1", - "caniuse-lite": "1.0.30001780", + "caniuse-lite": "1.0.30001781", "csstype": "3.2.3", "esbuild": "0.27.4", "eslint": "9.39.4", @@ -136,10 +136,10 @@ "postcss": "8.5.8", "postcss-easing-gradients": "3.0.1", "postcss-preset-env": "11.2.0", - "rollup": "4.59.0", + "rollup": "4.60.0", "rollup-plugin-visualizer": "6.0.11", "sass-embedded": "1.98.0", - "stylelint": "17.4.0", + "stylelint": "17.5.0", "stylelint-config-property-sort-order-smacss": "10.0.0", "stylelint-config-recommended-vue": "1.6.1", "stylelint-config-standard-scss": "17.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 6de1beb9c..3c0a1a021 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: overrides: minimatch: ^10.2.3 - rollup: 4.59.0 + rollup: 4.60.0 basic-ftp: 5.2.0 serialize-javascript: ^7.0.3 flatted: ^3.4.1 @@ -32,7 +32,7 @@ importers: version: 3.1.3(@fortawesome/fontawesome-svg-core@7.1.0)(vue@3.5.27(typescript@5.9.3)) '@intlify/unplugin-vue-i18n': specifier: 11.0.3 - version: 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.59.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) + version: 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.60.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) '@kyvg/vue3-notification': specifier: 3.4.2 version: 3.4.2(vue@3.5.27(typescript@5.9.3)) @@ -233,8 +233,8 @@ importers: specifier: 4.28.1 version: 4.28.1 caniuse-lite: - specifier: 1.0.30001780 - version: 1.0.30001780 + specifier: 1.0.30001781 + version: 1.0.30001781 csstype: specifier: 3.2.3 version: 3.2.3 @@ -266,29 +266,29 @@ importers: specifier: 11.2.0 version: 11.2.0(postcss@8.5.8) rollup: - specifier: 4.59.0 - version: 4.59.0 + specifier: 4.60.0 + version: 4.60.0 rollup-plugin-visualizer: specifier: 6.0.11 - version: 6.0.11(rollup@4.59.0) + version: 6.0.11(rollup@4.60.0) sass-embedded: specifier: 1.98.0 version: 1.98.0 stylelint: - specifier: 17.4.0 - version: 17.4.0(typescript@5.9.3) + specifier: 17.5.0 + version: 17.5.0(typescript@5.9.3) stylelint-config-property-sort-order-smacss: specifier: 10.0.0 - version: 10.0.0(stylelint@17.4.0(typescript@5.9.3)) + version: 10.0.0(stylelint@17.5.0(typescript@5.9.3)) stylelint-config-recommended-vue: specifier: 1.6.1 - version: 1.6.1(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3)) + version: 1.6.1(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3)) stylelint-config-standard-scss: specifier: 17.0.0 - version: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3)) stylelint-use-logical: specifier: 2.1.3 - version: 2.1.3(stylelint@17.4.0(typescript@5.9.3)) + version: 2.1.3(stylelint@17.5.0(typescript@5.9.3)) tailwindcss: specifier: 4.2.2 version: 4.2.2 @@ -964,12 +964,17 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.0.26': - resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} - '@csstools/css-syntax-patches-for-csstree@1.0.28': resolution: {integrity: sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==} + '@csstools/css-syntax-patches-for-csstree@1.1.1': + resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} @@ -2013,7 +2018,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: 4.59.0 + rollup: 4.60.0 peerDependenciesMeta: '@types/babel__core': optional: true @@ -2022,7 +2027,7 @@ packages: resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.59.0 + rollup: 4.60.0 peerDependenciesMeta: rollup: optional: true @@ -2030,13 +2035,13 @@ packages: '@rollup/plugin-replace@2.4.2': resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} peerDependencies: - rollup: 4.59.0 + rollup: 4.60.0 '@rollup/plugin-terser@0.4.4': resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.59.0 + rollup: 4.60.0 peerDependenciesMeta: rollup: optional: true @@ -2045,139 +2050,139 @@ packages: resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} peerDependencies: - rollup: 4.59.0 + rollup: 4.60.0 '@rollup/pluginutils@5.1.3': resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.59.0 + rollup: 4.60.0 peerDependenciesMeta: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} cpu: [x64] os: [win32] @@ -3056,6 +3061,10 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -3284,8 +3293,8 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001780: - resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} capture-website@4.2.0: resolution: {integrity: sha512-EmkSn36CXTC8tUsS6aNmvvsdpfVTYYkuRp7U5bV9gcJwcDbqqA5c0Op/iskYPKtDdOkuVp61mjn/LLywX0h7cw==} @@ -3426,6 +3435,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -3477,6 +3495,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -4066,6 +4088,10 @@ packages: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4141,8 +4167,8 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} - globby@16.1.0: - resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} + globby@16.1.1: + resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} engines: {node: '>=20'} globjoin@0.1.4: @@ -4821,11 +4847,14 @@ packages: mdn-data@2.26.0: resolution: {integrity: sha512-ZqI0qjKWHMPcGUfLmlr80NPNVHIOjPMHtIOe1qXYFGS0YBZ1YKAzo9yk8W+gGrLCN0Xdv/RKxqdIsqPakEfmow==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - meow@14.0.0: - resolution: {integrity: sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==} + meow@14.1.0: + resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} meow@7.1.1: @@ -5589,15 +5618,15 @@ packages: hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta - rollup: 4.59.0 + rollup: 4.60.0 peerDependenciesMeta: rolldown: optional: true rollup: optional: true - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5943,8 +5972,8 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.1.1: - resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==} + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} string.prototype.matchall@4.0.11: @@ -5980,6 +6009,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom-string@1.0.0: resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} engines: {node: '>=0.10.0'} @@ -6084,8 +6117,8 @@ packages: peerDependencies: stylelint: '>= 11 < 18' - stylelint@17.4.0: - resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==} + stylelint@17.5.0: + resolution: {integrity: sha512-o/NS6zhsPZFmgUm5tXX4pVNg1XDOZSlucLdf2qow/lVn4JIyzZIQ5b3kad1ugqUj3GSIgr2u5lQw7X8rjqw33g==} engines: {node: '>=20.19.0'} hasBin: true @@ -7703,10 +7736,12 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.0.26': {} - '@csstools/css-syntax-patches-for-csstree@1.0.28': {} + '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@3.0.4': {} '@csstools/css-tokenizer@4.0.0': {} @@ -8399,13 +8434,13 @@ snapshots: '@intlify/shared@11.2.8': {} - '@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.59.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': + '@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.60.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4(jiti@2.4.2)) '@intlify/bundle-utils': 11.0.3(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3))) '@intlify/shared': 11.2.2 '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.2)(@vue/compiler-dom@3.5.27)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) - '@rollup/pluginutils': 5.1.3(rollup@4.59.0) + '@rollup/pluginutils': 5.1.3(rollup@4.60.0) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) debug: 4.4.3 @@ -8629,128 +8664,128 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(rollup@4.59.0)': + '@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(rollup@4.60.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-module-imports': 7.25.9 - '@rollup/pluginutils': 3.1.0(rollup@4.59.0) - rollup: 4.59.0 + '@rollup/pluginutils': 3.1.0(rollup@4.60.0) + rollup: 4.60.0 transitivePeerDependencies: - supports-color - '@rollup/plugin-node-resolve@15.2.3(rollup@4.59.0)': + '@rollup/plugin-node-resolve@15.2.3(rollup@4.60.0)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.59.0) + '@rollup/pluginutils': 5.1.3(rollup@4.60.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.0 - '@rollup/plugin-replace@2.4.2(rollup@4.59.0)': + '@rollup/plugin-replace@2.4.2(rollup@4.60.0)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@4.59.0) + '@rollup/pluginutils': 3.1.0(rollup@4.60.0) magic-string: 0.25.9 - rollup: 4.59.0 + rollup: 4.60.0 - '@rollup/plugin-terser@0.4.4(rollup@4.59.0)': + '@rollup/plugin-terser@0.4.4(rollup@4.60.0)': dependencies: serialize-javascript: 7.0.3 smob: 1.5.0 terser: 5.31.6 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.0 - '@rollup/pluginutils@3.1.0(rollup@4.59.0)': + '@rollup/pluginutils@3.1.0(rollup@4.60.0)': dependencies: '@types/estree': 0.0.39 estree-walker: 1.0.1 picomatch: 2.3.1 - rollup: 4.59.0 + rollup: 4.60.0 - '@rollup/pluginutils@5.1.3(rollup@4.59.0)': + '@rollup/pluginutils@5.1.3(rollup@4.60.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.0 - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/rollup-android-arm-eabi@4.60.0': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.60.0': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.60.0': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.0': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.60.0': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.60.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.60.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.60.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.60.0': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.0': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.60.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.60.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.60.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true '@sentry-internal/browser-utils@10.36.0': @@ -9766,6 +9801,8 @@ snapshots: ansi-regex@6.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -9823,7 +9860,7 @@ snapshots: autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001780 + caniuse-lite: 1.0.30001781 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.8 @@ -9942,7 +9979,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.4 - caniuse-lite: 1.0.30001780 + caniuse-lite: 1.0.30001781 electron-to-chromium: 1.5.266 node-releases: 2.0.27 update-browserslist-db: 1.2.2(browserslist@4.28.1) @@ -10004,7 +10041,7 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001780: {} + caniuse-lite@1.0.30001781: {} capture-website@4.2.0(typescript@5.9.3): dependencies: @@ -10146,6 +10183,15 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + crelt@1.0.6: {} cross-spawn@7.0.6: @@ -10199,6 +10245,11 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.1.0: {} cssdb@8.8.0: {} @@ -10212,7 +10263,7 @@ snapshots: cssstyle@5.3.7: dependencies: '@asamuzakjp/css-color': 4.1.1 - '@csstools/css-syntax-patches-for-csstree': 1.0.26 + '@csstools/css-syntax-patches-for-csstree': 1.0.28 css-tree: 3.1.0 lru-cache: 11.2.4 @@ -10857,6 +10908,8 @@ snapshots: get-east-asian-width@1.4.0: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10960,7 +11013,7 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 - globby@16.1.0: + globby@16.1.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 @@ -11640,9 +11693,11 @@ snapshots: mdn-data@2.26.0: {} + mdn-data@2.27.1: {} + mdurl@2.0.0: {} - meow@14.0.0: {} + meow@14.1.0: {} meow@7.1.1: dependencies: @@ -12517,44 +12572,44 @@ snapshots: rfdc@1.4.1: {} - rollup-plugin-visualizer@6.0.11(rollup@4.59.0): + rollup-plugin-visualizer@6.0.11(rollup@4.60.0): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: - rollup: 4.59.0 + rollup: 4.60.0 - rollup@4.59.0: + rollup@4.60.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 fsevents: 2.3.3 rope-sequence@1.3.4: {} @@ -12887,10 +12942,10 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.0 - string-width@8.1.1: + string-width@8.2.0: dependencies: - get-east-asian-width: 1.4.0 - strip-ansi: 7.1.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 string.prototype.matchall@4.0.11: dependencies: @@ -12949,6 +13004,10 @@ snapshots: dependencies: ansi-regex: 6.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} strip-comments@2.0.1: {} @@ -12969,62 +13028,62 @@ snapshots: style-mod@4.1.2: {} - stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3)): dependencies: postcss-html: 1.8.0 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.5.0(typescript@5.9.3) - stylelint-config-property-sort-order-smacss@10.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-property-sort-order-smacss@10.0.0(stylelint@17.5.0(typescript@5.9.3)): dependencies: css-property-sort-order-smacss: 2.2.0 - stylelint: 17.4.0(typescript@5.9.3) - stylelint-order: 6.0.4(stylelint@17.4.0(typescript@5.9.3)) + stylelint: 17.5.0(typescript@5.9.3) + stylelint-order: 6.0.4(stylelint@17.5.0(typescript@5.9.3)) - stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3)): dependencies: postcss-scss: 4.0.9(postcss@8.5.8) - stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) - stylelint-scss: 7.0.0(stylelint@17.4.0(typescript@5.9.3)) + stylelint: 17.5.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.5.0(typescript@5.9.3)) + stylelint-scss: 7.0.0(stylelint@17.5.0(typescript@5.9.3)) optionalDependencies: postcss: 8.5.8 - stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3)): dependencies: postcss-html: 1.8.0 semver: 7.7.1 - stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3)) - stylelint-config-recommended: 17.0.0(stylelint@17.4.0(typescript@5.9.3)) + stylelint: 17.5.0(typescript@5.9.3) + stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3)) + stylelint-config-recommended: 17.0.0(stylelint@17.5.0(typescript@5.9.3)) - stylelint-config-recommended@17.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended@17.0.0(stylelint@17.5.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.5.0(typescript@5.9.3) - stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.5.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.5.0(typescript@5.9.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)) - stylelint-config-standard: 40.0.0(stylelint@17.4.0(typescript@5.9.3)) + stylelint: 17.5.0(typescript@5.9.3) + stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3)) + stylelint-config-standard: 40.0.0(stylelint@17.5.0(typescript@5.9.3)) optionalDependencies: postcss: 8.5.8 - stylelint-config-standard@40.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-standard@40.0.0(stylelint@17.5.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) + stylelint: 17.5.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.5.0(typescript@5.9.3)) - stylelint-order@6.0.4(stylelint@17.4.0(typescript@5.9.3)): + stylelint-order@6.0.4(stylelint@17.5.0(typescript@5.9.3)): dependencies: postcss: 8.5.8 postcss-sorting: 8.0.2(postcss@8.5.8) - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.5.0(typescript@5.9.3) - stylelint-scss@7.0.0(stylelint@17.4.0(typescript@5.9.3)): + stylelint-scss@7.0.0(stylelint@17.5.0(typescript@5.9.3)): dependencies: css-tree: 3.1.0 is-plain-object: 5.0.0 @@ -13034,31 +13093,31 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.5.0(typescript@5.9.3) - stylelint-use-logical@2.1.3(stylelint@17.4.0(typescript@5.9.3)): + stylelint-use-logical@2.1.3(stylelint@17.5.0(typescript@5.9.3)): dependencies: - stylelint: 17.4.0(typescript@5.9.3) + stylelint: 17.5.0(typescript@5.9.3) - stylelint@17.4.0(typescript@5.9.3): + stylelint@17.5.0(typescript@5.9.3): dependencies: '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.0.28 + '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1) colord: 2.9.3 - cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig: 9.0.1(typescript@5.9.3) css-functions-list: 3.3.3 - css-tree: 3.1.0 + css-tree: 3.2.1 debug: 4.4.3 fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 11.1.2 global-modules: 2.0.0 - globby: 16.1.0 + globby: 16.1.1 globjoin: 0.1.4 html-tags: 5.1.0 ignore: 7.0.5 @@ -13066,7 +13125,7 @@ snapshots: imurmurhash: 0.1.4 is-plain-object: 5.0.0 mathml-tag-names: 4.0.0 - meow: 14.0.0 + meow: 14.1.0 micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 @@ -13074,7 +13133,7 @@ snapshots: postcss-safe-parser: 7.0.1(postcss@8.5.8) postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - string-width: 8.1.1 + string-width: 8.2.0 supports-hyperlinks: 4.4.0 svg-tags: 1.0.0 table: 6.9.0 @@ -13546,7 +13605,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.8 - rollup: 4.59.0 + rollup: 4.60.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.12.0 @@ -13761,10 +13820,10 @@ snapshots: '@babel/core': 7.26.0 '@babel/preset-env': 7.26.0(@babel/core@7.26.0) '@babel/runtime': 7.25.4 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(rollup@4.59.0) - '@rollup/plugin-node-resolve': 15.2.3(rollup@4.59.0) - '@rollup/plugin-replace': 2.4.2(rollup@4.59.0) - '@rollup/plugin-terser': 0.4.4(rollup@4.59.0) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(rollup@4.60.0) + '@rollup/plugin-node-resolve': 15.2.3(rollup@4.60.0) + '@rollup/plugin-replace': 2.4.2(rollup@4.60.0) + '@rollup/plugin-terser': 0.4.4(rollup@4.60.0) '@surma/rollup-plugin-off-main-thread': 2.2.3 ajv: 8.18.0 common-tags: 1.8.2 @@ -13773,7 +13832,7 @@ snapshots: glob: 11.1.0 lodash: 4.17.23 pretty-bytes: 5.6.0 - rollup: 4.59.0 + rollup: 4.60.0 source-map: 0.8.0-beta.0 stringify-object: 3.3.0 strip-comments: 2.0.1 diff --git a/frontend/src/components/tasks/partials/Attachments.vue b/frontend/src/components/tasks/partials/Attachments.vue index 553d501d0..b246adcd0 100644 --- a/frontend/src/components/tasks/partials/Attachments.vue +++ b/frontend/src/components/tasks/partials/Attachments.vue @@ -184,7 +184,6 @@ import {canPreview} from '@/models/attachment' import type {IAttachment} from '@/modelTypes/IAttachment' import type {ITask} from '@/modelTypes/ITask' -import {useAttachmentStore} from '@/stores/attachments' import {formatDisplayDate, formatDateLong} from '@/helpers/time/formatDate' import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments' import {getHumanSize} from '@/helpers/getHumanSize' @@ -201,9 +200,9 @@ const props = withDefaults(defineProps<{ editEnabled: true, }) -// FIXME: this should go through the store const emit = defineEmits<{ 'taskChanged': [ITask], + 'update:attachments': [IAttachment[]], }>() const EDITOR_SELECTOR = '.tiptap, .tiptap__editor, [contenteditable]' @@ -232,8 +231,7 @@ const {t} = useI18n({useScope: 'global'}) const attachmentService = shallowReactive(new AttachmentService()) -const attachmentStore = useAttachmentStore() -const attachments = computed(() => attachmentStore.attachments) +const attachments = computed(() => props.task.attachments ?? []) const loading = computed(() => attachmentService.loading || taskStore.isLoading) @@ -335,7 +333,10 @@ function uploadNewAttachment() { async function uploadFilesToTask(files: File[] | FileList) { try { - await uploadFiles(attachmentService, props.task.id, files) + const uploaded = await uploadFiles(attachmentService, props.task.id, files) + if (uploaded.length > 0) { + emit('update:attachments', [...attachments.value, ...uploaded]) + } } catch (e) { error(e) } @@ -354,7 +355,8 @@ async function deleteAttachment() { try { const r = await attachmentService.delete(attachmentToDelete.value) - attachmentStore.removeById(attachmentToDelete.value.id) + const updated = attachments.value.filter(a => a.id !== attachmentToDelete.value!.id) + emit('update:attachments', updated) success(r) setAttachmentToDelete(null) } catch (e) { diff --git a/frontend/src/helpers/attachments.ts b/frontend/src/helpers/attachments.ts index 86e43951b..645eabdce 100644 --- a/frontend/src/helpers/attachments.ts +++ b/frontend/src/helpers/attachments.ts @@ -2,9 +2,8 @@ import AttachmentModel from '@/models/attachment' import type {IAttachment} from '@/modelTypes/IAttachment' import AttachmentService from '@/services/attachment' -import {useTaskStore} from '@/stores/tasks' -export async function uploadFile(taskId: number, file: File, onSuccess?: (url: string) => void) { +export async function uploadFile(taskId: number, file: File, onSuccess?: (url: string) => void): Promise { const attachmentService = new AttachmentService() const files = [file] @@ -16,16 +15,14 @@ export async function uploadFiles( taskId: number, files: File[] | FileList, onSuccess?: (attachmentUrl: string) => void, -) { +): Promise { const attachmentModel = new AttachmentModel({taskId}) const response = await attachmentService.create(attachmentModel, files) console.debug(`Uploaded attachments for task ${taskId}, response was`, response) + const uploaded: IAttachment[] = [] response.success?.map((attachment: IAttachment) => { - useTaskStore().addTaskAttachment({ - taskId, - attachment, - }) + uploaded.push(attachment) onSuccess?.(generateAttachmentUrl(taskId, attachment.id)) }) @@ -33,6 +30,8 @@ export async function uploadFiles( const messages = response.errors.map((e: {message: string}) => e.message) throw new Error(messages.join('\n')) } + + return uploaded } export function generateAttachmentUrl(taskId: number, attachmentId: number) { diff --git a/frontend/src/i18n/lang/cs-CZ.json b/frontend/src/i18n/lang/cs-CZ.json index 6281cafba..e5dcf2b15 100644 --- a/frontend/src/i18n/lang/cs-CZ.json +++ b/frontend/src/i18n/lang/cs-CZ.json @@ -43,6 +43,7 @@ "forgotPassword": "Zapomenuté heslo?", "resetPassword": "Obnovit heslo", "resetPasswordAction": "Poslat odkaz na obnovení hesla", + "resetPasswordSuccess": "Zkontrolujte doručenou poštu! Měli byste obdržet email s pokyny, jak obnovit své heslo.", "passwordsDontMatch": "Hesla se neshodují", "confirmEmailSuccess": "Úspěšně jste potvrdili svůj e-mail! Nyní se můžete přihlásit.", "totpTitle": "Kód dvoufaktorového ověření", @@ -60,43 +61,88 @@ "usernameMustNotLookLikeUrl": "Uživatelské jméno nesmí vypadat jako adresa URL.", "passwordRequired": "Zadejte prosím heslo.", "passwordNotMin": "Heslo musí mít nejméně 8 znaků.", + "passwordNotMax": "Heslo může mít maximálně 72 znaků.", "showPassword": "Ukázat heslo", "hidePassword": "Skrýt heslo", "noAccountYet": "Ještě nemáte účet?", "alreadyHaveAnAccount": "Už máte svůj účet?", - "remember": "Zůstat trvale přihlášen" + "remember": "Zůstat trvale přihlášen", + "registrationDisabled": "Registrace je vypnuta.", + "passwordResetTokenMissing": "Chybí token pro obnovení hesla.", + "registrationFailed": "Při registraci došlo k chybě. Zkontrolujte prosím zadané údaje a zkuste to znovu." }, "settings": { "title": "Nastavení", "newPasswordTitle": "Aktualizujte své heslo", + "newPassword": "Nové heslo", + "newPasswordConfirm": "Potvrzení nového hesla", + "currentPassword": "Stávající heslo", "currentPasswordPlaceholder": "Vaše současné heslo", "passwordsDontMatch": "Nové heslo se neshoduje s potvrzením hesla.", "passwordUpdateSuccess": "Heslo bylo úspěšně změněno.", "updateEmailTitle": "Aktualizujte svou e-mailovou adresu", + "updateEmailNew": "Nová emailová adresa", "updateEmailSuccess": "Vaše e-mailová adresa byla úspěšně aktualizována. Poslali jsme vám odkaz pro její potvrzení.", "general": { "title": "Obecná nastavení", + "name": "Moje jméno", + "newName": "Nové jméno", "savedSuccess": "Nastavení bylo úspěšně aktualizováno.", + "emailReminders": "Posílat mi připomenutí pro úkoly emailem", "overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů", "discoverableByName": "Umožnit ostatním uživatelům přidat mě jako člena do týmů nebo projektů, když hledají mé jméno", "discoverableByEmail": "Umožnit ostatním uživatelům, aby mě přidali jako člena do týmů nebo projektů, když hledají můj úplný e-mail", "playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo", + "allowIconChanges": "Zobrazit speciální loga v určitých časech", + "alwaysShowBucketTaskCount": "Vždy zobrazit počet úkolů ve sloupcích Kanbanu", + "defaultTaskRelationType": "Výchozí typ vztahu úkolů", "weekStart": "První den týdne", "weekStartSunday": "Neděle", "weekStartMonday": "Pondělí", "language": "Jazyk", + "defaultProject": "Výchozí projekt", + "defaultView": "Výchozí zobrazení", + "timezone": "Časové pásmo", "overdueTasksRemindersTime": "Čas odeslání emailu o zpožděných úkolech", - "filterUsedOnOverview": "Uložený filtr použitý na stránce přehledu" + "filterUsedOnOverview": "Uložený filtr použitý na stránce přehledu", + "minimumPriority": "Minimální viditelná priorita úkolu", + "dateDisplay": "Formát zobrazení data", + "dateDisplayOptions": { + "relative": "Relativní (např. před 3 dny)", + "mm-dd-yyyy": "MM-DD-RRRR", + "dd-mm-yyyy": "DD-MM-RRRR", + "yyyy-mm-dd": "RRRR-MM-DD", + "mm/dd/yyyy": "MM/DD/RRRR", + "dd/mm/yyyy": "DD/MM/RRRR", + "yyyy/mm/dd": "RRRR/MM/DD" + }, + "timeFormat": "Formát času", + "timeFormatOptions": { + "12h": "12 hodinový (AM/PM)", + "24h": "24 hodinový (HH:mm)" + }, + "externalUserNameChange": "Vaše jméno je spravováno vaším poskytovatelem přihlášení ({provider}). Chcete-li ho změnit, aktualizujte ho tam." + }, + "sections": { + "personalInformation": "Osobní údaje", + "taskAndNotifications": "Projekty a úkoly", + "privacy": "Ochrana soukromí", + "localization": "Lokalizace", + "appearance": "Vzhled a chování" }, "totp": { "title": "Dvoufaktorové ověření", "enroll": "Zápis", + "finishSetupPart1": "Chcete-li dokončit nastavení, použijte tento tajný klíč ve vaší TOTP aplikaci (Google Authenticator nebo podobné):", "finishSetupPart2": "Poté zadejte kód z vaší aplikace.", "scanQR": "Případně můžete naskenovat tento QR kód:", "passcode": "Přístupový kód", + "passcodePlaceholder": "Kód vygenerovaný vaší TOTP aplikací", + "confirmNotice": "Po povolení dvoufázového ověřování budete odhlášeni ze všech relací a musíte se znovu přihlásit.", "setupSuccess": "Úspěšně jste nastavili dvoufaktorové ověření!", "enterPassword": "Zadejte prosím heslo", "disable": "Zakázat dvoufaktorové ověření", + "confirmSuccess": "Úspěšně jste povolili dvoufázové ověřování!", "disableSuccess": "Dvoufázové ověření bylo úspěšně vypnuto." }, "caldav": { @@ -119,7 +165,9 @@ "upload": "Nahrát", "uploadAvatar": "Nahrát avatara", "statusUpdateSuccess": "Stav avatara byl úspěšně aktualizován!", - "setSuccess": "Avatar byl úspěšně nastaven!" + "setSuccess": "Avatar byl úspěšně nastaven!", + "ldap": "Váš avatar je automaticky synchronizován s adresářovou službou vaší organizace (LDAP). Informujte se u vašeho IT týmu o tom, jak jej změnit.", + "openid": "Váš avatar je automaticky synchronizován s poskytovatelem přihlášení ({provider}). Chcete-li ho změnit, aktualizujte ho tam." }, "quickAddMagic": { "title": "Kouzelný režim pro rychlé přidání", @@ -136,6 +184,13 @@ "dark": "Tmavý" } }, + "backgroundBrightness": { + "title": "Jas pozadí" + }, + "webhooks": { + "title": "Webhook oznámení", + "description": "Nastavte URL webhooků, který přijme POST požadavek při překročení termínu nebo připomenutí. Tyto webhooky přijímají události ze všech vašci projektů." + }, "apiTokens": { "title": "API Tokeny", "general": "API tokeny umožňují používat Vikunja API bez uživatelských údajů.", @@ -147,6 +202,7 @@ "90d": "90 dní", "permissionExplanation": "Oprávnění vám umožní nastavit, k čemu lze api token použít.", "titleRequired": "Je vyžadován název", + "permissionRequired": "Vyberte prosím alespoň jedno oprávnění ze seznamu.", "expired": "Platnost tohoto tokenu vypršela {ago}.", "tokenCreatedSuccess": "Zde je tvůj nový api token: {token}", "tokenCreatedNotSeeAgain": "Ulož jej na zabezpečeném místě, už ho znovu neuvidíš!", @@ -162,6 +218,20 @@ "expiresAt": "Vyprší v", "permissions": "Oprávnění" } + }, + "sessions": { + "title": "Relace", + "description": "Toto jsou všechna zařízení aktuálně přihlášená k vašemu účtu. Můžete zrušit jakoukoli relaci a odhlásit zařízení. Ukončení platnosti může trvat až 10 minut.", + "deviceInfo": "Zařízení", + "ipAddress": "IP adresa", + "lastActive": "Poslední aktivita", + "current": "Tato relace", + "delete": { + "header": "Zrušit relaci", + "text": "Opravdu chcete zrušit tuto relaci? Zařízení bude odhlášeno. Úplné vypršení platnosti relace může trvat až 10 minut." + }, + "deleteSuccess": "Relace byla úspěšně zrušena. Úplné vypršení platnosti relace může trvat až 10 minut.", + "noOtherSessions": "Žádné další aktivní relace." } }, "deletion": { @@ -186,13 +256,16 @@ "descriptionPasswordRequired": "Pokračujte zadáním vašeho hesla:", "request": "Požádat o kopii mých dat", "success": "Úspěšně jste požádali o svá data! Jakmile budou připravena ke stažení, pošleme Vám e-mail.", - "downloadTitle": "Stáhnout exportovaná Vikunja data" + "downloadTitle": "Stáhnout exportovaná Vikunja data", + "ready": "Váš export je připraven ke stažení. Můžete jej stáhnout do {0}.", + "requestNew": "Požádat o další export" } }, "project": { "archivedMessage": "Tento projekt je archivován. Není možné vytvořit ani upravovat jeho úkoly.", "archived": "Archivováno", "showArchived": "Zobrazit archivované", + "title": "Název", "color": "Barva", "projects": "Projekty", "parent": "Nadřazený projekt", @@ -201,6 +274,11 @@ "shared": "Sdílené projekty", "noDescriptionAvailable": "Není k dispozici žádný popis projektu.", "inboxTitle": "Inbox", + "favorite": "Označit tento projekt jako oblíbený", + "unfavorite": "Odstranit tento projekt z oblíbených", + "openSettingsMenu": "Otevřít nastavení projektu", + "description": "Popis projektu", + "favoriteDescription": "Tento projekt má všechny úkoly označené jako oblíbené.", "create": { "header": "Nový projekt", "titlePlaceholder": "Název projektu přijde sem…", @@ -288,11 +366,20 @@ "addedSuccess": "{type} byl úspěšně přidán.", "updatedSuccess": "{type} byl úspěšně přidán." }, + "permission": { + "title": "Oprávnění", + "read": "Pouze pro čtení", + "readWrite": "Čtení a zápis", + "admin": "Administrátor" + }, "attributes": { "link": "Odkaz", "delete": "Smazat" } }, + "first": { + "title": "První pohled" + }, "list": { "title": "Seznam", "add": "Přidat", @@ -308,7 +395,27 @@ "month": "Měsíc", "day": "Den", "hour": "Hodina", - "range": "Časové období" + "range": "Časové období", + "chartLabel": "Projektový Ganttův diagram", + "taskBarsForRow": "Chlívky pro řádek {rowId}", + "taskBarLabel": "Úkol: {task}. Od {startDate} do {endDate}. {dateType}. Klikněte pro úpravu, přetáhněte pro přesun.", + "scheduledDates": "Plánovaná data", + "estimatedDates": "Předpokládaná data", + "resizeStartDate": "Změňte počáteční datum pro {task}", + "resizeEndDate": "Změňte koncové datum pro {task}", + "timelineHeader": "Hlavička časového plánu s měsíci a dny", + "monthsRow": "Řádek měsíců", + "daysRow": "Řádek dnů", + "monthLabel": "Měsíc: {month}", + "dayLabel": "Den: {date}, {weekday}", + "dayLabelToday": "Dnes: {date}, {weekday}", + "taskAriaLabel": "Úkol: {task}", + "taskAriaLabelById": "Úkol {id}", + "partialDatesStart": "Pouze datum zahájení (otevřený konec)", + "partialDatesEnd": "Pouze datum konce (otevřený konec)", + "expandGroup": "Rozbalit skupinu: {task}", + "collapseGroup": "Sbalit skupinu: {task}", + "toggleRelationArrows": "Přepnout šipky relací" }, "table": { "title": "Tabulka", @@ -337,7 +444,8 @@ "deleteBucketSuccess": "Sloupec byl úspěšně smazán.", "bucketTitleSavedSuccess": "Název sloupce byl úspěšně uložen.", "bucketLimitSavedSuccess": "Limit sloupce byl úspěšně uložen.", - "collapse": "Sbalit tento sloupec" + "collapse": "Sbalit tento sloupec", + "bucketLimitReached": "Dosáhli jste limitu sloupce. Odstraňte úkoly nebo zvýšte limit pro přidání nových úkolů." }, "pseudo": { "favorites": { @@ -349,12 +457,16 @@ "targetUrl": "Cílová URL", "targetUrlInvalid": "Zadejte prosím platnou URL adresu.", "events": "Události", + "eventsHint": "Vyberte všechny události, pro které by tento webhook měl dostávat aktualizace (v rámci aktuálního projektu).", "mustSelectEvents": "Musíte zvolit alespoň jednu událost.", "delete": "Smazat tento webhook", "deleteText": "Opravdu chcete odstranit tento webhook? Externí cíle již nebudou informovány o jeho událostech.", "deleteSuccess": "Webhook byl úspěšně odstraněn.", "create": "Vytvořit webhook", "secret": "Tajný klíč", + "basicauthuser": "Uživatel pro Basic Auth", + "basicauthpassword": "Heslo pro Basic Auth", + "basicauthlink": "Použít Basic Auth?", "secretHint": "Pokud je zadáno, všechny požadavky na cílovou adresu URL webhooku budou podepsány pomocí HMAC.", "secretDocs": "Další podrobnosti o používání tajných klíčů naleznete v dokumentaci." }, @@ -372,13 +484,18 @@ "titleRequired": "Zadejte prosím název.", "delete": "Odstranit tento pohled", "deleteText": "Jste si jisti, že chcete odstranit tento pohled? Nebude již možné jej použít k zobrazení úkolů v tomto projektu. Tato akce neodstraní žádné úkoly. Toto nelze vrátit zpět!", - "onlyAdminsCanEdit": "Pohledy mohou upravovat pouze správci projektu." + "deleteSuccess": "Pohled byl úspěšně odstraněn.", + "onlyAdminsCanEdit": "Pohledy mohou upravovat pouze správci projektu.", + "updateSuccess": "Pohled byl úspěšně upraven." } }, "filters": { "title": "Filtry", "clear": "Vymazat filtry", "showResults": "Zobrazit výsledky", + "noResults": "Žádné výsledky", + "fromView": "Aktuální pohled má také nastavený filtr:", + "fromViewBoth": "Bude použito v kombinaci s tím, co zde zadáte.", "attributes": { "title": "Název", "titlePlaceholder": "Název uloženého filtru přijde sem…", @@ -416,6 +533,7 @@ "help": { "intro": "K filtrování úkolů můžete použít syntaxi dotazů podobnou SQL. Dostupná pole pro filtrování zahrnují:", "link": "Jak to funguje?", + "canUseDatemath": "Můžete použít matematiku a nastavit relativní data. Pro více informací klikněte na datum v dotazu.", "fields": { "done": "Zda je úkol dokončen nebo ne", "priority": "Úroveň priority úkolu (1-5)", @@ -426,7 +544,10 @@ "doneAt": "Datum a čas, kdy byl úkol dokončen", "assignees": "Přiřazení uživatelé", "labels": "Štítky přiřazené úkolu", - "project": "Projekt, do kterého úkol patří (k dispozici pouze pro uložené filtry, ne na úrovni projektu)" + "project": "Projekt, do kterého úkol patří (k dispozici pouze pro uložené filtry, ne na úrovni projektu)", + "reminders": "Připomenutí úlohy jako pole s datem, vrátí všechny úkoly s alespoň jednou upomínkou odpovídající dotazu", + "created": "Čas a datum, kdy byl úkol vytvořen", + "updated": "Čas a datum, kdy byl úkol naposledy změněn" }, "operators": { "intro": "Dostupné operátory pro filtrování zahrnují:", @@ -436,7 +557,9 @@ "greaterThanOrEqual": "Větší nebo rovno než", "lessThan": "Menší než", "lessThanOrEqual": "Menší nebo rovno než", - "like": "Odpovídá vzoru (s použitím zástupného znaku %)" + "like": "Odpovídá vzoru (s použitím zástupného znaku %)", + "in": "Odpovídá libovolné hodnotě v seznamu hodnot oddělených čárkou", + "notIn": "Odpovídá libovolné hodnotě nepřítomné v seznamu hodnot oddělených čárkou" }, "logicalOperators": { "intro": "Pro kombinování více podmínek můžete použít tyto logické operátory:", @@ -502,7 +625,11 @@ "authenticating": "Ověřování…", "passwordRequired": "Tento sdílený projekt vyžaduje heslo. Zadejte jej níže:", "error": "Došlo k chybě.", - "invalidPassword": "Neplatné heslo." + "invalidPassword": "Neplatné heslo.", + "accessDenied": "Přístup byl odepřen. Zkontrolujte svá oprávnění a zkuste to znovu.", + "serverError": "Došlo k chybě serveru. Opakujte akci později.", + "projectLoadError": "Nepodařilo se načíst informace o projektu.", + "retry": "Opakovat" }, "navigation": { "overview": "Přehled", @@ -543,7 +670,8 @@ "created": "Vytvořeno", "createdBy": "Vytvořil(a) {0}", "actions": "Akce", - "cannotBeUndone": "Toto nelze vrátit!" + "cannotBeUndone": "Toto nelze vrátit!", + "avatarOfUser": "Profilový obrázek {user}" }, "input": { "resetColor": "Obnovit barvu", @@ -700,6 +828,7 @@ "addReminder": "Přidat připomínku…", "doneSuccess": "Úkol byl úspěšně označen jako dokončený.", "undoneSuccess": "Úkol byl úspěšně znovu otevřen.", + "movedToProject": "Úkol byl přesunut do {project}.", "undo": "Vrátit zpět", "openDetail": "Otevřít zobrazení detailu úkolu", "checklistTotal": "{checked} z {total} úkolů", @@ -707,10 +836,13 @@ "show": { "titleCurrent": "Aktuální úkoly", "titleDates": "Úkoly od {from} do {to}", + "noDates": "Zobrazit úkoly bez datumu", "overdue": "Zobrazit zpožděné úkoly", "fromuntil": "Úkoly od {from} do {until}", "select": "Vyberte rozsah dat", - "noTasks": "Nic na práci - užij si pěkný den!" + "noTasks": "Nic na práci - užij si pěkný den!", + "filterByLabel": "Filtrování podle štítku {label}", + "clearLabelFilter": "Vymazat filtr štítků" }, "detail": { "chooseDueDate": "Klikněte zde pro nastavení termínu dokončení", @@ -724,9 +856,12 @@ "doneAt": "Dokončeno {0}", "updateSuccess": "Úkol byl úspěšně uložen.", "deleteSuccess": "Úkol byl úspěšně smazán.", + "duplicateSuccess": "Úkol byl úspěšně duplikován.", "belongsToProject": "Tento úkol patří do projektu '{project}'", + "back": "Zpět k projektu", "due": "Termín {at}", "closePopup": "Zavřít vyskakovací okno", + "scrollToBottom": "Přejít na konec", "organization": "Organizace", "management": "Management", "dateAndTime": "Datum a čas", @@ -748,6 +883,7 @@ "attachments": "Přidat přílohy", "relatedTasks": "Přidat vztah", "moveProject": "Přesunout", + "duplicate": "Duplikovat", "color": "Nastavit barvu", "delete": "Smazat", "favorite": "Přidat do oblíbených", @@ -766,9 +902,12 @@ "labels": "Štítky", "percentDone": "Průběh", "priority": "Priorita", + "project": "Projekt", "relatedTasks": "Související úkoly", "reminders": "Připomínky", "repeat": "Opakovat", + "comment": "{count} komentář | {count} komentáře", + "commentCount": "Počet komentářů", "startDate": "Počáteční datum", "title": "Název", "updated": "Aktualizováno", @@ -813,7 +952,13 @@ "delete": "Smazat tento komentář", "deleteText1": "Opravdu chcete smazat tento komentář?", "deleteSuccess": "Komentář byl úspěšně odstraněn.", - "addedSuccess": "Komentář byl úspěšně přidán." + "addedSuccess": "Komentář byl úspěšně přidán.", + "permalink": "Kopírovat trvalý odkaz na tento komentář", + "sortNewestFirst": "Od nejnovějších", + "sortOldestFirst": "Od nejstarších" + }, + "mention": { + "noUsersFound": "Nenalezeni žádní uživatelé" }, "deferDueDate": { "title": "Odložit datum dokončení", @@ -922,6 +1067,7 @@ "project4": "Například: {prefix}\"Projekt s mezerami\".", "dateAndTime": "Datum a čas", "date": "Jakékoliv datum bude použito jako datum dokončení nového úkolu. Můžete použít data v kterémkoli z těchto formátů:", + "dateWeekday": "jakýkoli pracovní den, použije se další datum s tímto dnem", "dateCurrentYear": "použije aktuální rok", "dateNth": "použije {day}. den aktuálního měsíce", "dateTime": "Pro nastavení času zkombinujte libovolný formát data s \"{time}\" (nebo {timePM}).", @@ -1003,7 +1149,12 @@ "delete": "Smazat tento úkol", "priority": "Změnit prioritu tohoto úkolu", "favorite": "Označit tuto úlohu jako oblíbenou / odebrat oblíbené", - "save": "Uložit aktuální úkol" + "openProject": "Otevřít projekt tohoto úkolu", + "save": "Uložit aktuální úkol", + "copyIdentifier": "Kopírovat identifikátor úkolu do schránky", + "copyIdentifierAndTitle": "Kopírovat identifikátor úkolu a název do schránky", + "copyIdentifierTitleAndUrl": "Kopírovat identifikátor úkolu, název a URL do schránky", + "copyUrl": "Kopírovat URL úkolu do schránky" }, "project": { "title": "Zobrazení projektu", @@ -1019,6 +1170,21 @@ "labels": "Přejít na štítky", "teams": "Přejít na týmy", "projects": "Přejít na projekty" + }, + "list": { + "title": "Seznam úkolů", + "navigateDown": "Zvýraznit další úkol", + "navigateUp": "Zvýraznit předchozí úkol", + "open": "Otevřít zvýrazněný úkol" + }, + "gantt": { + "title": "Ganttův diagram", + "moveTaskLeft": "Přesunout úkol na dřívější datum", + "moveTaskRight": "Přesunout úkol na pozdější datum", + "expandTaskLeft": "Posuň počáteční datum na dříve", + "expandTaskRight": "Posuň koncové datum na později", + "shrinkTaskLeft": "Posuň počáteční datum na později", + "shrinkTaskRight": "Posuň koncové datum na dříve" } }, "update": { @@ -1056,6 +1222,7 @@ "notification": { "title": "Oznámení", "none": "Nemáte žádná oznámení. Mějte příjemný den!", + "explainer": "Upozornění se zobrazí zde, když proběhne akce na projektech nebo úkolech, ke kterým jste se přihlásili.", "markAllRead": "Označit všechna oznámení za přečtená", "markAllReadSuccess": "Všechna oznámení byla označena jako přečtená." }, @@ -1079,6 +1246,7 @@ } }, "date": { + "altFormatLong": "j M Y, H:i", "altFormatShort": "j M Y" }, "reaction": { @@ -1100,15 +1268,28 @@ "1012": "E-mailová adresa uživatele nebyla potvrzena.", "1013": "Nové heslo je prázdné.", "1014": "Staré heslo je prázdné.", + "1015": "TOTP je již pro tohoto uživatele povoleno.", + "1016": "Totp není pro tohoto uživatele povoleno.", + "1017": "Kód TOTP je neplatný.", "1018": "Nastavení typu avatara uživatele je neplatné.", + "1019": "Poskytovatel OpenID neposkytl žádnou e-mailovou adresu. Ujistěte se, že poskytovatel openID veřejně poskytuje e-mailovou adresu pro váš účet.", + "1020": "Tento účet je zakázán. Zkontrolujte své e-maily nebo se zeptejte správce.", + "1021": "Tento účet je spravován poskytovatelem ověření třetí strany.", + "1022": "Uživatelské jméno nesmí obsahovat mezery.", + "1023": "To nemůžete udělat se sdíleným odkazem.", + "1024": "Neplatná žádost o data pro pole {field} typu {type}.", + "1025": "Časové pásmo '{timezone}' je neplatné. Zvolte prosím platné časové pásmo ze seznamu.", "2001": "ID nemůže být prázdné nebo 0.", "2002": "Některé údaje požadavku byly neplatné.", + "2003": "Časové pásmo '{timezone}' je neplatné.", "3001": "Projekt neexistuje.", "3004": "Pro provedení této akce musíte mít oprávnění ke čtení k tomuto projektu.", "3005": "Název projektu nemůže být prázdný.", "3006": "Sdílení projektu neexistuje.", "3007": "Projekt s tímto identifikátorem již existuje.", "3008": "Projekt je archivován, a proto je přístupný pouze pro čtení. To platí i pro všechny úkoly spojené s tímto projektem.", + "4001": "Název úkolu nemůže být prázdný.", + "4002": "Tento úkol neexistuje.", "4003": "Všechny úkoly pro hromadnou úpravu musí patřit do stejného projektu.", "4004": "Při hromadných úpravách úkolů je potřeba alespoň jeden úkol.", "4005": "Nemáte právo vidět tento úkol.", @@ -1126,12 +1307,18 @@ "4017": "Neplatný komparátor filtru úkolů.", "4018": "Neplatné zřetězení filtru úkolů.", "4019": "Neplatná hodnota filtru úkolů.", + "4020": "Tato příloha nepatří k tomu úkolu.", + "4021": "Tento uživatel je k tomuto úkolu již přiřazen.", + "4022": "Uveďte prosím datum, s nímž připomenutí souvisí.", + "4023": "Nelze vytvořit cyklus vztahů mezi úkoly.", "6001": "Název týmu nemůže být prázdný.", "6002": "Tým neexistuje.", "6004": "Tým již má k tomuto projektu přístup.", "6005": "Uživatel je již členem tohoto týmu.", "6006": "Nelze odstranit posledního člena týmu.", "6007": "Tým nemá přístup k seznamu pro provedení této akce.", + "6008": "Pro dané OIDC ID a vydavatele nebyl nalezen žádný tým.", + "6009": "Pro uživatele nebyly nalezeny žádné týmy s vlastností oidcId.", "7002": "Uživatel již má přístup k tomuto projektu.", "7003": "K tomuto projektu nemáte přístup.", "8001": "Tento štítek již v tomto úkolu existuje.", @@ -1147,14 +1334,21 @@ "11002": "Uložené filtry nejsou k dispozici pro sdílení odkazů.", "12001": "Typ předplatného je neplatný.", "12002": "Již jste přihlášeni k odběru samotného subjektu nebo nadřazeného subjektu.", + "12003": "Musíte zadat uživatele pro načtení odběru.", "13001": "Tento sdílený odkaz vyžaduje heslo k ověření, ale žádné nebylo poskytnuto.", + "13002": "Zadané heslo pro sdílený odkaz je neplatné.", + "13003": "Zadaný token pro sdílený odkaz je neplatný.", + "14001": "Zadaný api token je neplatný.", + "14002": "Oprávnění {permission} skupiny {group} je neplatné.", "error": "Chyba", "success": "Úspěch", "0001": "Nejste oprávněni to udělat." }, "about": { "title": "O aplikaci", - "version": "Verze: {version}" + "version": "Verze: {version}", + "frontendVersion": "Verze frontendu: {version}", + "apiVersion": "Verze API: {version}" }, "time": { "units": { diff --git a/frontend/src/i18n/lang/de-DE.json b/frontend/src/i18n/lang/de-DE.json index 5dfc90609..baaf609c9 100644 --- a/frontend/src/i18n/lang/de-DE.json +++ b/frontend/src/i18n/lang/de-DE.json @@ -138,9 +138,11 @@ "scanQR": "Alternativ kannst du auch diesen QR-Code scannen:", "passcode": "Code", "passcodePlaceholder": "Ein von deiner TOTP-App generierter Code", + "confirmNotice": "Nach der Aktivierung der Zwei-Faktor-Authentifizierung wirst du überall abgemeldet und musst dich erneut anmelden.", "setupSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!", "enterPassword": "Bitte gib dein Passwort ein", "disable": "Zwei-Faktor-Authentifizierung deaktivieren", + "confirmSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!", "disableSuccess": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert." }, "caldav": { diff --git a/frontend/src/i18n/lang/de-swiss.json b/frontend/src/i18n/lang/de-swiss.json index 52dd55e98..f31a106d1 100644 --- a/frontend/src/i18n/lang/de-swiss.json +++ b/frontend/src/i18n/lang/de-swiss.json @@ -138,9 +138,11 @@ "scanQR": "Alternativ chasch au de QR Code scanne:", "passcode": "Code", "passcodePlaceholder": "Ein von deiner TOTP-App generierter Code", + "confirmNotice": "Nach der Aktivierung der Zwei-Faktor-Authentifizierung wirst du überall abgemeldet und musst dich erneut anmelden.", "setupSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!", "enterPassword": "Bitte gib diis Passwort iih", "disable": "Zweifaktor Authentifizierig uusschalte", + "confirmSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!", "disableSuccess": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert." }, "caldav": { diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json index f1e09e803..a1854b90f 100644 --- a/frontend/src/i18n/lang/en.json +++ b/frontend/src/i18n/lang/en.json @@ -214,6 +214,13 @@ "tokenCreatedSuccess": "Here is your new api token: {token}", "tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!", "selectAll": "Select all", + "presets": { + "title": "Quick presets", + "readOnly": "Read only", + "tasks": "Task management", + "projects": "Project management", + "fullAccess": "Full access" + }, "delete": { "header": "Delete this token", "text1": "Are you sure you want to delete the token \"{token}\"?", diff --git a/frontend/src/i18n/lang/ru-RU.json b/frontend/src/i18n/lang/ru-RU.json index 61e6e3917..b724e0230 100644 --- a/frontend/src/i18n/lang/ru-RU.json +++ b/frontend/src/i18n/lang/ru-RU.json @@ -68,7 +68,8 @@ "alreadyHaveAnAccount": "Уже есть аккаунт?", "remember": "Оставаться в системе", "registrationDisabled": "Регистрация отключена.", - "passwordResetTokenMissing": "Не указан токен сброса пароля." + "passwordResetTokenMissing": "Не указан токен сброса пароля.", + "registrationFailed": "Произошла ошибка при регистрации. Проверьте введённые данные и повторите попытку." }, "settings": { "title": "Настройки", @@ -93,6 +94,7 @@ "discoverableByEmail": "Разрешить другим пользователям добавлять меня в состав команд или проектов при поиске моего полного email", "playSoundWhenDone": "Проигрывать звук, когда задача помечается завершённой", "allowIconChanges": "Показывать специальные логотипы в определённое время", + "alwaysShowBucketTaskCount": "Всегда показывать количество задач в колонках Канбана", "defaultTaskRelationType": "Тип связанной задачи по умолчанию", "weekStart": "Первый день недели", "weekStartSunday": "Воскресенье", @@ -136,9 +138,11 @@ "scanQR": "Или вы можете отсканировать этот QR-код:", "passcode": "Код", "passcodePlaceholder": "Код, который сгенерировало приложение TOTP", + "confirmNotice": "После включения двухфакторной аутентификации все ваши сеансы будут завершены и вам нужно будет войти заново.", "setupSuccess": "Двухфакторная аутентификация успешно подключена!", "enterPassword": "Введите свой пароль", "disable": "Отключить двухфакторную аутентификацию", + "confirmSuccess": "Двухфакторная аутентификация успешно подключена!", "disableSuccess": "Двухфакторная аутентификация отключена." }, "caldav": { @@ -183,6 +187,10 @@ "backgroundBrightness": { "title": "Яркость фона" }, + "webhooks": { + "title": "Уведомления через вебхуки", + "description": "Настройка адресов, которые будут получать POST-запросы при достижении времени напоминания или истечении срока задачи. Эти вебхуки будут получать события из всех ваших проектов." + }, "apiTokens": { "title": "Токены API", "general": "Токены API позволяют использовать Vikunja API без использования данных для входа пользователя.", @@ -210,6 +218,20 @@ "expiresAt": "Срок действия", "permissions": "Разрешения" } + }, + "sessions": { + "title": "Сеансы", + "description": "Список всех устройств, на которых выполнен вход в ваш аккаунт. Вы можете отозвать любой сеанс, чтобы выйти из аккаунта на указанном устройстве. Это может занять до 10 минут.", + "deviceInfo": "Устройство", + "ipAddress": "IP-адрес", + "lastActive": "Последняя активность", + "current": "Текущий сеанс", + "delete": { + "header": "Отозвать сеанс", + "text": "Вы уверены, что хотите отозвать этот сеанс? Будет выполнен выход из аккаунта на этом устройстве. Это может занять до 10 минут." + }, + "deleteSuccess": "Сеанс отозван. Полное завершение его действия может занять до 10 минут.", + "noOtherSessions": "Нет других активных сеансов." } }, "deletion": { @@ -388,7 +410,11 @@ "dayLabel": "День: {date}, {weekday}", "dayLabelToday": "Сегодня: {date}, {weekday}", "taskAriaLabel": "Задача: {task}", - "taskAriaLabelById": "Задача {id}" + "taskAriaLabelById": "Задача {id}", + "partialDatesStart": "Только дата начала (без окончания)", + "partialDatesEnd": "Только дата окончания (без начала)", + "expandGroup": "Развернуть группу: {task}", + "collapseGroup": "Свернуть группу: {task}" }, "table": { "title": "Таблица", @@ -437,6 +463,9 @@ "deleteSuccess": "Вебхук успешно удалён.", "create": "Создать вебхук", "secret": "Секрет", + "basicauthuser": "Basic Auth пользователь", + "basicauthpassword": "Basic Auth пароль", + "basicauthlink": "Использовать HTTP Basic аутентификацию?", "secretHint": "Если указан, все запросы к URL обработчика будут подписаны с помощью HMAC.", "secretDocs": "Подробнее об использовании секретов в документации." }, @@ -797,6 +826,7 @@ "addReminder": "Добавить напоминание…", "doneSuccess": "Задача отмечена как завершённая.", "undoneSuccess": "Задача отмечена как незавершённая.", + "movedToProject": "Задача перемещена в проект «{project}».", "undo": "Отменить", "openDetail": "Открыть подробный просмотр задачи", "checklistTotal": "{checked} из {total} задач", @@ -824,10 +854,12 @@ "doneAt": "Завершено {0}", "updateSuccess": "Задача сохранена.", "deleteSuccess": "Задача удалена.", + "duplicateSuccess": "Задача продублирована.", "belongsToProject": "Задача принадлежит проекту «{project}»", "back": "Вернуться к проекту", "due": "Истекает {at}", "closePopup": "Закрыть всплывающее окно", + "scrollToBottom": "Прокрутить до конца страницы", "organization": "Организация", "management": "Управление", "dateAndTime": "Дата и время", @@ -849,6 +881,7 @@ "attachments": "Добавить вложения", "relatedTasks": "Добавить связь", "moveProject": "Переместить", + "duplicate": "Создать копию", "color": "Выбрать цвет", "delete": "Удалить", "favorite": "Добавить в избранное", @@ -867,6 +900,7 @@ "labels": "Метки", "percentDone": "Прогресс", "priority": "Приоритет", + "project": "Проект", "relatedTasks": "Связанные задачи", "reminders": "Напоминания", "repeat": "Повтор", @@ -917,7 +951,9 @@ "deleteText1": "Удалить этот комментарий?", "deleteSuccess": "Комментарий удалён.", "addedSuccess": "Комментарий добавлен.", - "permalink": "Скопировать постоянную ссылку на комментарий" + "permalink": "Скопировать постоянную ссылку на комментарий", + "sortNewestFirst": "Сначала новые", + "sortOldestFirst": "Сначала старые" }, "mention": { "noUsersFound": "Пользователи не найдены" @@ -1112,7 +1148,11 @@ "priority": "Изменить приоритет задачи", "favorite": "Добавить задачу в избранное или удалить из избранного", "openProject": "Открыть проект с этой задачей", - "save": "Сохранить текущую задачу" + "save": "Сохранить текущую задачу", + "copyIdentifier": "Скопировать идентификатор задачи в буфер обмена", + "copyIdentifierAndTitle": "Скопировать идентификатор и заголовок задачи в буфер обмена", + "copyIdentifierTitleAndUrl": "Скопировать идентификатор, заголовок и URL задачи в буфер обмена", + "copyUrl": "Скопировать URL задачи в буфер обмена" }, "project": { "title": "Просмотр проекта", diff --git a/frontend/src/modules/parseTaskText.ts b/frontend/src/modules/parseTaskText.ts deleted file mode 100644 index 8e1fbb045..000000000 --- a/frontend/src/modules/parseTaskText.ts +++ /dev/null @@ -1,306 +0,0 @@ -import {parseDate} from '../helpers/time/parseDate' -import {PRIORITIES} from '@/constants/priorities' -import {REPEAT_TYPES, type IRepeatAfter, type IRepeatType} from '@/types/IRepeatAfter' - -const VIKUNJA_PREFIXES: Prefixes = { - label: '*', - project: '+', - priority: '!', - assignee: '@', -} - -const TODOIST_PREFIXES: Prefixes = { - label: '@', - project: '#', - priority: '!', - assignee: '+', -} - -export enum PrefixMode { - Disabled = 'disabled', - Default = 'vikunja', - Todoist = 'todoist', -} - -export const PREFIXES = { - [PrefixMode.Disabled]: undefined, - [PrefixMode.Default]: VIKUNJA_PREFIXES, - [PrefixMode.Todoist]: TODOIST_PREFIXES, -} - -interface repeatParsedResult { - textWithoutMatched: string, - repeats: IRepeatAfter | null, -} - -export interface ParsedTaskText { - text: string, - date: Date | null, - labels: string[], - project: string | null, - priority: number | null, - assignees: string[], - repeats: IRepeatAfter | null, -} - -interface Prefixes { - label: string, - project: string, - priority: string, - assignee: string, -} - -/** - * Parses task text for dates, assignees, labels, projects, priorities and returns an object with all found intents. - * - * @param text - */ -export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default, now: Date = new Date()): ParsedTaskText => { - const result: ParsedTaskText = { - text: text, - date: null, - labels: [], - project: null, - priority: null, - assignees: [], - repeats: null, - } - - const prefixes = PREFIXES[prefixesMode] - if (prefixes === undefined) { - return result - } - - result.labels = getLabelsFromPrefix(text, prefixesMode) ?? [] - result.text = cleanupItemText(result.text, result.labels, prefixes.label) - - result.project = getProjectFromPrefix(result.text, prefixesMode) - result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text - - result.priority = getPriority(result.text, prefixes.priority) - result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text - - result.assignees = getItemsFromPrefix(result.text, prefixes.assignee) - - const {textWithoutMatched, repeats} = getRepeats(result.text) - result.text = textWithoutMatched - result.repeats = repeats - - const {newText, date} = parseDate(result.text, now) - result.text = newText - result.date = date - - return cleanupResult(result, prefixes) -} - -const getItemsFromPrefix = (text: string, prefix: string): string[] => { - const items: string[] = [] - - const itemParts = text.split(' ' + prefix) - if (text.startsWith(prefix)) { - const firstItem = text.split(prefix)[1] - itemParts.unshift(firstItem) - } - - itemParts.forEach((p, index) => { - // First part contains the rest - if (index < 1) { - return - } - - if (p.startsWith(prefix)) { - p = p.substring(1) - } - - let itemText - if (p.charAt(0) === '\'') { - itemText = p.split('\'')[1] - } else if (p.charAt(0) === '"') { - itemText = p.split('"')[1] - } else { - // Only until the next space - itemText = p.split(' ')[0] - } - - if (itemText !== '') { - items.push(itemText) - } - }) - - return Array.from(new Set(items)) -} - -export const getProjectFromPrefix = (text: string, prefixMode: PrefixMode): string | null => { - const projectPrefix = PREFIXES[prefixMode]?.project - if(typeof projectPrefix === 'undefined') { - return null - } - const projects: string[] = getItemsFromPrefix(text, projectPrefix) - return projects.length > 0 ? projects[0] : null -} - -export const getLabelsFromPrefix = (text: string, prefixMode: PrefixMode): string[] | null => { - const labelsPrefix = PREFIXES[prefixMode]?.label - if(typeof labelsPrefix === 'undefined') { - return null - } - return getItemsFromPrefix(text, labelsPrefix) -} - -const getPriority = (text: string, prefix: string): number | null => { - const ps = getItemsFromPrefix(text, prefix) - if (ps.length === 0) { - return null - } - - for (const p of ps) { - for (const pi of Object.values(PRIORITIES)) { - if (pi === parseInt(p)) { - return parseInt(p) - } - } - } - - return null -} - -const getRepeats = (text: string): repeatParsedResult => { - const regex = /(^| )(((every|each) (([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten) )?(hours?|days?|weeks?|months?|years?))|(annually|biannually|semiannually|biennially|daily|hourly|monthly|weekly|yearly))($| )/ig - const results = regex.exec(text) - if (results === null) { - return { - textWithoutMatched: text, - repeats: null, - } - } - - let amount = 1 - switch (results[5] ? results[5].trim() : undefined) { - case 'one': - amount = 1 - break - case 'two': - amount = 2 - break - case 'three': - amount = 3 - break - case 'four': - amount = 4 - break - case 'five': - amount = 5 - break - case 'six': - amount = 6 - break - case 'seven': - amount = 7 - break - case 'eight': - amount = 8 - break - case 'nine': - amount = 9 - break - case 'ten': - amount = 10 - break - default: - amount = results[5] ? parseInt(results[5]) : 1 - } - let type: IRepeatType = REPEAT_TYPES.Hours - - switch (results[2]) { - case 'biennially': - type = REPEAT_TYPES.Years - amount = 2 - break - case 'biannually': - case 'semiannually': - type = REPEAT_TYPES.Months - amount = 6 - break - case 'yearly': - case 'annually': - type = REPEAT_TYPES.Years - break - case 'daily': - type = REPEAT_TYPES.Days - break - case 'hourly': - type = REPEAT_TYPES.Hours - break - case 'monthly': - type = REPEAT_TYPES.Months - break - case 'weekly': - type = REPEAT_TYPES.Weeks - break - default: - switch (results[7]) { - case 'hour': - case 'hours': - type = REPEAT_TYPES.Hours - break - case 'day': - case 'days': - type = REPEAT_TYPES.Days - break - case 'week': - case 'weeks': - type = REPEAT_TYPES.Weeks - break - case 'month': - case 'months': - type = REPEAT_TYPES.Months - break - case 'year': - case 'years': - type = REPEAT_TYPES.Years - break - } - } - - let matchedText = results[0] - if(matchedText.endsWith(' ')) { - matchedText = matchedText.substring(0, matchedText.length - 1) - } - - return { - textWithoutMatched: text.replace(matchedText, ''), - repeats: { - amount, - type, - }, - } -} - -const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - -export const cleanupItemText = (text: string, items: string[], prefix: string): string => { - items.forEach(l => { - if (l === '') { - return - } - const escaped = escapeRegExp(l) - text = text - .replace(new RegExp(`\\${prefix}'${escaped}' `, 'ig'), '') - .replace(new RegExp(`\\${prefix}'${escaped}'`, 'ig'), '') - .replace(new RegExp(`\\${prefix}"${escaped}" `, 'ig'), '') - .replace(new RegExp(`\\${prefix}"${escaped}"`, 'ig'), '') - .replace(new RegExp(`\\${prefix}${escaped} `, 'ig'), '') - .replace(new RegExp(`\\${prefix}${escaped}`, 'ig'), '') - }) - return text -} - -const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => { - result.text = cleanupItemText(result.text, result.labels, prefixes.label) - result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text - result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text - // Not removing assignees to avoid removing @text where the user does not exist - result.text = result.text.trim() - - return result -} diff --git a/frontend/src/helpers/time/parseDate.ts b/frontend/src/modules/parseTaskText/dateParser.ts similarity index 96% rename from frontend/src/helpers/time/parseDate.ts rename to frontend/src/modules/parseTaskText/dateParser.ts index d74e55ecb..e1621d324 100644 --- a/frontend/src/helpers/time/parseDate.ts +++ b/frontend/src/modules/parseTaskText/dateParser.ts @@ -1,6 +1,6 @@ -import {calculateDayInterval} from './calculateDayInterval' -import {calculateNearestHours} from './calculateNearestHours' -import {replaceAll} from '../replaceAll' +import {calculateDayInterval} from '@/helpers/time/calculateDayInterval' +import {calculateNearestHours} from '@/helpers/time/calculateNearestHours' +import {replaceAll} from '@/helpers/replaceAll' export interface dateParseResult { newText: string, @@ -196,7 +196,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => { result = `${month}/${day}/${tmp_year}` result = !isNaN(new Date(result).getTime()) ? result : `${day}/${month}/${tmp_year}` result = !isNaN(new Date(result).getTime()) ? result : null - + if(result !== null){ foundText = found break @@ -212,7 +212,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => { foundText = results === null ? '' : results[0].trim() containsYear = false } - + if (result === null) { return { foundText, @@ -328,7 +328,7 @@ const getDateFromWeekday = (text: string, date: Date = new Date()): dateFoundRes const distance: number = (day + 7 - currentDay) % 7 date.setDate(date.getDate() + distance) - // This a space at the end of the found text to not break parsing suffix strings like "at 14:00" in cases where the + // This a space at the end of the found text to not break parsing suffix strings like "at 14:00" in cases where the // matched string comes with a space at the end (last part of the regex). let foundText = results[0] if (foundText.endsWith(' ')) { @@ -357,9 +357,9 @@ const getDayFromText = (text: string, now: Date = new Date()) => { const day = parseInt(results[0]) date.setDate(day) - // If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days, + // If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days, // setting the day to 31 will "overflow" the date to the next month, but the first. - // This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after + // This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after // setting it for the first time and set it again if it isn't - that would mean the month overflowed. while (date < now) { date.setMonth(date.getMonth() + 1) diff --git a/frontend/src/modules/parseTaskText/index.ts b/frontend/src/modules/parseTaskText/index.ts new file mode 100644 index 000000000..134f96ee2 --- /dev/null +++ b/frontend/src/modules/parseTaskText/index.ts @@ -0,0 +1,5 @@ +export {parseTaskText} from './parseTaskText' +export {PrefixMode, PREFIXES} from './prefixes' +export {getLabelsFromPrefix, getProjectFromPrefix} from './prefixParser' +export {cleanupItemText} from './textCleanup' +export type {ParsedTaskText} from './types' diff --git a/frontend/src/modules/parseTaskText.test.ts b/frontend/src/modules/parseTaskText/parseTaskText.test.ts similarity index 99% rename from frontend/src/modules/parseTaskText.test.ts rename to frontend/src/modules/parseTaskText/parseTaskText.test.ts index 2df6cc41c..1137567e1 100644 --- a/frontend/src/modules/parseTaskText.test.ts +++ b/frontend/src/modules/parseTaskText/parseTaskText.test.ts @@ -1,8 +1,9 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest' -import {ParsedTaskText, parseTaskText, PrefixMode} from './parseTaskText' -import {parseDate} from '../helpers/time/parseDate' -import {calculateDayInterval} from '../helpers/time/calculateDayInterval' +import {parseTaskText, PrefixMode} from '.' +import type {ParsedTaskText} from '.' +import {parseDate} from './dateParser' +import {calculateDayInterval} from '@/helpers/time/calculateDayInterval' import {PRIORITIES} from '@/constants/priorities' import {MILLISECONDS_A_DAY} from '@/constants/date' import type {IRepeatAfter} from '@/types/IRepeatAfter' diff --git a/frontend/src/modules/parseTaskText/parseTaskText.ts b/frontend/src/modules/parseTaskText/parseTaskText.ts new file mode 100644 index 000000000..3a65989d3 --- /dev/null +++ b/frontend/src/modules/parseTaskText/parseTaskText.ts @@ -0,0 +1,50 @@ +import {parseDate} from './dateParser' +import {PREFIXES, PrefixMode} from './prefixes' +import {getItemsFromPrefix, getLabelsFromPrefix, getProjectFromPrefix} from './prefixParser' +import {getPriority} from './priorityParser' +import {getRepeats} from './repeatParser' +import {cleanupItemText, cleanupResult} from './textCleanup' +import type {ParsedTaskText} from './types' + +/** + * Parses task text for dates, assignees, labels, projects, priorities and returns an object with all found intents. + * + * @param text + */ +export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default, now: Date = new Date()): ParsedTaskText => { + const result: ParsedTaskText = { + text: text, + date: null, + labels: [], + project: null, + priority: null, + assignees: [], + repeats: null, + } + + const prefixes = PREFIXES[prefixesMode] + if (prefixes === undefined) { + return result + } + + result.labels = getLabelsFromPrefix(text, prefixesMode) ?? [] + result.text = cleanupItemText(result.text, result.labels, prefixes.label) + + result.project = getProjectFromPrefix(result.text, prefixesMode) + result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text + + result.priority = getPriority(result.text, prefixes.priority) + result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text + + result.assignees = getItemsFromPrefix(result.text, prefixes.assignee) + + const {textWithoutMatched, repeats} = getRepeats(result.text) + result.text = textWithoutMatched + result.repeats = repeats + + const {newText, date} = parseDate(result.text, now) + result.text = newText + result.date = date + + return cleanupResult(result, prefixes) +} diff --git a/frontend/src/modules/parseTaskText/prefixParser.ts b/frontend/src/modules/parseTaskText/prefixParser.ts new file mode 100644 index 000000000..9dfc6e95c --- /dev/null +++ b/frontend/src/modules/parseTaskText/prefixParser.ts @@ -0,0 +1,55 @@ +import {PREFIXES, PrefixMode} from './prefixes' + +export const getItemsFromPrefix = (text: string, prefix: string): string[] => { + const items: string[] = [] + + const itemParts = text.split(' ' + prefix) + if (text.startsWith(prefix)) { + const firstItem = text.split(prefix)[1] + itemParts.unshift(firstItem) + } + + itemParts.forEach((p, index) => { + // First part contains the rest + if (index < 1) { + return + } + + if (p.startsWith(prefix)) { + p = p.substring(1) + } + + let itemText + if (p.charAt(0) === '\'') { + itemText = p.split('\'')[1] + } else if (p.charAt(0) === '"') { + itemText = p.split('"')[1] + } else { + // Only until the next space + itemText = p.split(' ')[0] + } + + if (itemText !== '') { + items.push(itemText) + } + }) + + return Array.from(new Set(items)) +} + +export const getProjectFromPrefix = (text: string, prefixMode: PrefixMode): string | null => { + const projectPrefix = PREFIXES[prefixMode]?.project + if(typeof projectPrefix === 'undefined') { + return null + } + const projects: string[] = getItemsFromPrefix(text, projectPrefix) + return projects.length > 0 ? projects[0] : null +} + +export const getLabelsFromPrefix = (text: string, prefixMode: PrefixMode): string[] | null => { + const labelsPrefix = PREFIXES[prefixMode]?.label + if(typeof labelsPrefix === 'undefined') { + return null + } + return getItemsFromPrefix(text, labelsPrefix) +} diff --git a/frontend/src/modules/parseTaskText/prefixes.ts b/frontend/src/modules/parseTaskText/prefixes.ts new file mode 100644 index 000000000..9b198e009 --- /dev/null +++ b/frontend/src/modules/parseTaskText/prefixes.ts @@ -0,0 +1,27 @@ +import type {Prefixes} from './types' + +const VIKUNJA_PREFIXES: Prefixes = { + label: '*', + project: '+', + priority: '!', + assignee: '@', +} + +const TODOIST_PREFIXES: Prefixes = { + label: '@', + project: '#', + priority: '!', + assignee: '+', +} + +export enum PrefixMode { + Disabled = 'disabled', + Default = 'vikunja', + Todoist = 'todoist', +} + +export const PREFIXES = { + [PrefixMode.Disabled]: undefined, + [PrefixMode.Default]: VIKUNJA_PREFIXES, + [PrefixMode.Todoist]: TODOIST_PREFIXES, +} diff --git a/frontend/src/modules/parseTaskText/priorityParser.ts b/frontend/src/modules/parseTaskText/priorityParser.ts new file mode 100644 index 000000000..f1dc2db2e --- /dev/null +++ b/frontend/src/modules/parseTaskText/priorityParser.ts @@ -0,0 +1,19 @@ +import {PRIORITIES} from '@/constants/priorities' +import {getItemsFromPrefix} from './prefixParser' + +export const getPriority = (text: string, prefix: string): number | null => { + const ps = getItemsFromPrefix(text, prefix) + if (ps.length === 0) { + return null + } + + for (const p of ps) { + for (const pi of Object.values(PRIORITIES)) { + if (pi === parseInt(p)) { + return parseInt(p) + } + } + } + + return null +} diff --git a/frontend/src/modules/parseTaskText/repeatParser.ts b/frontend/src/modules/parseTaskText/repeatParser.ts new file mode 100644 index 000000000..1b061ee7d --- /dev/null +++ b/frontend/src/modules/parseTaskText/repeatParser.ts @@ -0,0 +1,114 @@ +import {REPEAT_TYPES, type IRepeatType} from '@/types/IRepeatAfter' +import type {repeatParsedResult} from './types' + +export const getRepeats = (text: string): repeatParsedResult => { + const regex = /(^| )(((every|each) (([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten) )?(hours?|days?|weeks?|months?|years?))|(annually|biannually|semiannually|biennially|daily|hourly|monthly|weekly|yearly))($| )/ig + const results = regex.exec(text) + if (results === null) { + return { + textWithoutMatched: text, + repeats: null, + } + } + + let amount = 1 + switch (results[5] ? results[5].trim() : undefined) { + case 'one': + amount = 1 + break + case 'two': + amount = 2 + break + case 'three': + amount = 3 + break + case 'four': + amount = 4 + break + case 'five': + amount = 5 + break + case 'six': + amount = 6 + break + case 'seven': + amount = 7 + break + case 'eight': + amount = 8 + break + case 'nine': + amount = 9 + break + case 'ten': + amount = 10 + break + default: + amount = results[5] ? parseInt(results[5]) : 1 + } + let type: IRepeatType = REPEAT_TYPES.Hours + + switch (results[2]) { + case 'biennially': + type = REPEAT_TYPES.Years + amount = 2 + break + case 'biannually': + case 'semiannually': + type = REPEAT_TYPES.Months + amount = 6 + break + case 'yearly': + case 'annually': + type = REPEAT_TYPES.Years + break + case 'daily': + type = REPEAT_TYPES.Days + break + case 'hourly': + type = REPEAT_TYPES.Hours + break + case 'monthly': + type = REPEAT_TYPES.Months + break + case 'weekly': + type = REPEAT_TYPES.Weeks + break + default: + switch (results[7]) { + case 'hour': + case 'hours': + type = REPEAT_TYPES.Hours + break + case 'day': + case 'days': + type = REPEAT_TYPES.Days + break + case 'week': + case 'weeks': + type = REPEAT_TYPES.Weeks + break + case 'month': + case 'months': + type = REPEAT_TYPES.Months + break + case 'year': + case 'years': + type = REPEAT_TYPES.Years + break + } + } + + let matchedText = results[0] + if(matchedText.endsWith(' ')) { + matchedText = matchedText.substring(0, matchedText.length - 1) + } + + return { + textWithoutMatched: text.replace(matchedText, ''), + repeats: { + amount, + type, + }, + } +} diff --git a/frontend/src/modules/parseTaskText/textCleanup.ts b/frontend/src/modules/parseTaskText/textCleanup.ts new file mode 100644 index 000000000..05d5ab6d7 --- /dev/null +++ b/frontend/src/modules/parseTaskText/textCleanup.ts @@ -0,0 +1,30 @@ +import type {ParsedTaskText, Prefixes} from './types' + +const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +export const cleanupItemText = (text: string, items: string[], prefix: string): string => { + items.forEach(l => { + if (l === '') { + return + } + const escaped = escapeRegExp(l) + text = text + .replace(new RegExp(`\\${prefix}'${escaped}' `, 'ig'), '') + .replace(new RegExp(`\\${prefix}'${escaped}'`, 'ig'), '') + .replace(new RegExp(`\\${prefix}"${escaped}" `, 'ig'), '') + .replace(new RegExp(`\\${prefix}"${escaped}"`, 'ig'), '') + .replace(new RegExp(`\\${prefix}${escaped} `, 'ig'), '') + .replace(new RegExp(`\\${prefix}${escaped}`, 'ig'), '') + }) + return text +} + +export const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => { + result.text = cleanupItemText(result.text, result.labels, prefixes.label) + result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text + result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text + // Not removing assignees to avoid removing @text where the user does not exist + result.text = result.text.trim() + + return result +} diff --git a/frontend/src/modules/parseTaskText/types.ts b/frontend/src/modules/parseTaskText/types.ts new file mode 100644 index 000000000..c8fdb5796 --- /dev/null +++ b/frontend/src/modules/parseTaskText/types.ts @@ -0,0 +1,23 @@ +import type {IRepeatAfter} from '@/types/IRepeatAfter' + +export interface repeatParsedResult { + textWithoutMatched: string, + repeats: IRepeatAfter | null, +} + +export interface ParsedTaskText { + text: string, + date: Date | null, + labels: string[], + project: string | null, + priority: number | null, + assignees: string[], + repeats: IRepeatAfter | null, +} + +export interface Prefixes { + label: string, + project: string, + priority: string, + assignee: string, +} diff --git a/frontend/src/stores/attachments.ts b/frontend/src/stores/attachments.ts deleted file mode 100644 index 61294d48b..000000000 --- a/frontend/src/stores/attachments.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {ref, computed, readonly} from 'vue' -import {defineStore, acceptHMRUpdate} from 'pinia' -import {findIndexById} from '@/helpers/utils' - -import type {IAttachment} from '@/modelTypes/IAttachment' - -export const useAttachmentStore = defineStore('attachment', () => { - const attachments = ref([]) - - function set(newAttachments: IAttachment[]) { - console.debug('Set attachments', newAttachments) - attachments.value = newAttachments - } - - function add(attachment: IAttachment) { - console.debug('Add attachement', attachment) - attachments.value.push(attachment) - } - - function removeById(id: IAttachment['id']) { - const attachmentIndex = findIndexById(attachments.value, id) - attachments.value.splice(attachmentIndex, 1) - console.debug('Remove attachement', id) - } - - const hasAttachments = computed(() => attachments.value.length > 0) - - return { - attachments: readonly(attachments), - set, - add, - removeById, - hasAttachments, - } -}) - -// support hot reloading -if (import.meta.hot) { - import.meta.hot.accept(acceptHMRUpdate(useAttachmentStore, import.meta.hot)) -} diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts index e5f70d54f..40f47a4c0 100644 --- a/frontend/src/stores/config.ts +++ b/frontend/src/stores/config.ts @@ -87,12 +87,13 @@ export const useConfigStore = defineStore('config', () => { const migratorsEnabled = computed(() => state.availableMigrators?.length > 0) const apiBase = computed(() => { - const {host, protocol, href} = parseURL(window.API_URL) + const {host, protocol, pathname} = parseURL(window.API_URL) - const cleanHref = href ? (href.endsWith('/') - ? href.slice(0, -1) - : href) : '' - return `${protocol}//${host}${cleanHref ? `/${cleanHref}` : ''}` + // Strip the /api/v1 suffix (and optional trailing slash) to get the deployment base. + const basePath = pathname + .replace(/\/api\/v1\/?$/, '') + .replace(/\/+$/, '') + return `${protocol}//${host}${basePath}` }) function setConfig(config: ConfigState) { diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index b4c7b10b9..6926171c4 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -25,7 +25,6 @@ import type {IProject} from '@/modelTypes/IProject' import {setModuleLoading} from '@/stores/helper' import {useLabelStore} from '@/stores/labels' import {useProjectStore} from '@/stores/projects' -import {useAttachmentStore} from '@/stores/attachments' import {useKanbanStore} from '@/stores/kanban' import {useBaseStore} from '@/stores/base' import ProjectUserService from '@/services/projectUsers' @@ -108,7 +107,6 @@ async function findAssignees(parsedTaskAssignees: string[], projectId: number): export const useTaskStore = defineStore('task', () => { const baseStore = useBaseStore() const kanbanStore = useKanbanStore() - const attachmentStore = useAttachmentStore() const labelStore = useLabelStore() const projectStore = useProjectStore() const authStore = useAuthStore() @@ -205,7 +203,6 @@ export const useTaskStore = defineStore('task', () => { } kanbanStore.setTaskInBucketByIndex(newTask) } - attachmentStore.add(attachment) } async function addAssignee({ diff --git a/frontend/src/views/tasks/TaskDetailView.vue b/frontend/src/views/tasks/TaskDetailView.vue index 8a61a498a..11eaf0900 100644 --- a/frontend/src/views/tasks/TaskDetailView.vue +++ b/frontend/src/views/tasks/TaskDetailView.vue @@ -355,6 +355,7 @@ :edit-enabled="canWrite" :task="task" @taskChanged="({coverImageAttachmentId}) => task.coverImageAttachmentId = coverImageAttachmentId" + @update:attachments="onAttachmentsUpdated" /> @@ -622,7 +623,6 @@