Compare commits

...

83 Commits

Author SHA1 Message Date
Julius Härtl
0cd1d8c148 Bump version to 1.4.0
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 17:48:22 +02:00
Julius Härtl
c12e07f938 Merge pull request #2934 from nextcloud/enh/advanced-search 2021-04-13 14:30:18 -01:00
Julius Härtl
990659b8f0 Workaround sqlite query issues
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 17:07:57 +02:00
Julius Härtl
9970ebc220 Do not log if FTS is unavailable
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:31:46 +02:00
Julius Härtl
af309f7372 Add user docs
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:15:25 +02:00
Julius Härtl
0f3bbe332b Improve quote handling in search strings
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:04 +02:00
Julius Härtl
e77ca1997b Fix global search in overview
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:04 +02:00
Julius Härtl
6b6aef03f7 Attempt to fix psalm false-positive
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:04 +02:00
Julius Härtl
0f10e1f0e1 Add inline search input
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:04 +02:00
Julius Härtl
030cc4eb57 Add card sidebar tab navigation to router
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:04 +02:00
Julius Härtl
73fce1d4ee Add composer scripts for php tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:04 +02:00
Julius Härtl
a6c4912bff Integration tests for search
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:03 +02:00
Julius Härtl
c960d21b37 Add global result frontend
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:03 +02:00
Julius Härtl
840c143b92 Implement advanced search queries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 14:13:03 +02:00
Julius Härtl
88a5e420b9 Merge pull request #2933 from nextcloud/enh/fts-events 2021-04-13 11:12:15 -01:00
Julius Härtl
174d74c483 Update psalm baseline
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 11:05:02 +02:00
Julius Härtl
322480a3b7 Move full text search to proper events
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-13 11:04:17 +02:00
Nextcloud bot
be5c4a1685 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-04-13 02:36:27 +00:00
Julius Härtl
6f8072f749 Merge pull request #2964 from nextcloud/bugfix/2951
Fix navigating to board details
2021-04-12 13:10:47 -01:00
Julius Härtl
5adc2b3b7b Fix navigating to board details (fix #2951)
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-04-12 14:26:28 +02:00
Nextcloud bot
c991ec594d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-04-10 02:43:23 +00:00
Nextcloud bot
7b647d34c5 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-04-08 02:35:55 +00:00
Julius Härtl
766ce0a48f Merge pull request #2938 from nextcloud/dependabot/npm_and_yarn/master/vue-easymde-1.4.0
Bump vue-easymde from 1.3.2 to 1.4.0
2021-04-06 16:02:42 +02:00
dependabot[bot]
af42aac5cc Bump vue-easymde from 1.3.2 to 1.4.0
Bumps [vue-easymde](https://github.com/NikulinIlya/vue-easymde) from 1.3.2 to 1.4.0.
- [Release notes](https://github.com/NikulinIlya/vue-easymde/releases)
- [Commits](https://github.com/NikulinIlya/vue-easymde/compare/v1.3.2...1.4.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-06 13:51:34 +00:00
Julius Härtl
2a9e41df71 Merge pull request #2939 from nextcloud/dependabot/npm_and_yarn/master/stylelint-config-recommended-4.0.0 2021-04-06 15:49:22 +02:00
dependabot[bot]
0f9364748c Merge pull request #2940 from nextcloud/dependabot/npm_and_yarn/master/babel/core-7.13.14 2021-04-06 13:44:36 +00:00
dependabot[bot]
7d5815c2c9 Bump @babel/core from 7.13.13 to 7.13.14
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.13 to 7.13.14.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.14/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-06 13:34:28 +00:00
Julius Härtl
4a73a74ac9 Merge pull request #2936 from nextcloud/dependabot/composer/vimeo/psalm-4.7.0
Bump vimeo/psalm from 4.6.4 to 4.7.0
2021-04-06 15:31:20 +02:00
Nextcloud bot
8a9a25d196 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-04-05 02:36:11 +00:00
dependabot[bot]
25249b3e76 Bump stylelint-config-recommended from 3.0.0 to 4.0.0
Bumps [stylelint-config-recommended](https://github.com/stylelint/stylelint-config-recommended) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/stylelint/stylelint-config-recommended/releases)
- [Changelog](https://github.com/stylelint/stylelint-config-recommended/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint-config-recommended/compare/3.0.0...4.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-03 01:07:18 +00:00
dependabot[bot]
8c4ce1afd3 Bump vimeo/psalm from 4.6.4 to 4.7.0
Bumps [vimeo/psalm](https://github.com/vimeo/psalm) from 4.6.4 to 4.7.0.
- [Release notes](https://github.com/vimeo/psalm/releases)
- [Commits](https://github.com/vimeo/psalm/compare/4.6.4...4.7.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-03 01:01:15 +00:00
Nextcloud bot
35c4bb192b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-04-02 02:37:12 +00:00
Nextcloud bot
342587454f [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-03-31 02:36:32 +00:00
Julius Härtl
702d9aaa93 Merge pull request #2885 from nextcloud/dependabot/npm_and_yarn/master/dompurify-2.2.7
Bump dompurify from 2.2.6 to 2.2.7
2021-03-30 08:07:56 +02:00
Julius Härtl
5fc3c996a4 Merge pull request #2901 from nextcloud/dependabot/composer/vimeo/psalm-4.6.4 2021-03-30 08:07:42 +02:00
Nextcloud bot
a4c4399e26 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-03-30 02:35:29 +00:00
Julius Härtl
4173ddbc3e Update psalm baseline
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-29 14:15:59 +02:00
Julius Härtl
f16dd49946 Merge pull request #2883 from nextcloud/dependabot/npm_and_yarn/master/stylelint-13.12.0
Bump stylelint from 13.11.0 to 13.12.0
2021-03-29 14:14:16 +02:00
Julius Härtl
ec71f8255a Merge pull request #2886 from nextcloud/dependabot/npm_and_yarn/master/babel/runtime-7.13.10
Bump @babel/runtime from 7.13.9 to 7.13.10
2021-03-29 14:13:42 +02:00
Julius Härtl
577c0aae8f Merge pull request #2887 from nextcloud/dependabot/npm_and_yarn/master/acorn-8.1.0
Bump acorn from 8.0.5 to 8.1.0
2021-03-29 14:13:31 +02:00
dependabot[bot]
01f6e25a55 Bump dompurify from 2.2.6 to 2.2.7
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.2.6 to 2.2.7.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.2.6...2.2.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 12:04:29 +00:00
Julius Härtl
5b9c2da665 Merge pull request #2917 from nextcloud/dependabot/npm_and_yarn/master/url-search-params-polyfill-8.1.1
Bump url-search-params-polyfill from 8.1.0 to 8.1.1
2021-03-29 14:01:33 +02:00
Julius Härtl
402cfcb035 Merge pull request #2918 from nextcloud/dependabot/npm_and_yarn/master/babel/preset-env-7.13.12
Bump @babel/preset-env from 7.13.10 to 7.13.12
2021-03-29 14:01:04 +02:00
Julius Härtl
416cbc3dd6 Merge pull request #2919 from nextcloud/dependabot/npm_and_yarn/master/babel/core-7.13.13
Bump @babel/core from 7.13.8 to 7.13.13
2021-03-29 14:00:50 +02:00
Julius Härtl
5434b3b39b Merge pull request #2923 from nextcloud/bugfix/2911
Avoid reusing the existing route object to make navigation work properly
2021-03-29 14:00:15 +02:00
Julius Härtl
bee2289e52 Merge pull request #2920 from nextcloud/dependabot/npm_and_yarn/master/nextcloud/vue-3.8.0
Bump @nextcloud/vue from 3.6.0 to 3.8.0
2021-03-29 14:00:01 +02:00
Julius Härtl
2edc1bbad0 Avoid reusing the existing route object to make navigation work properly
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-29 10:01:32 +02:00
dependabot[bot]
e16ff0140a Bump @nextcloud/vue from 3.6.0 to 3.8.0
Bumps [@nextcloud/vue](https://github.com/nextcloud/nextcloud-vue) from 3.6.0 to 3.8.0.
- [Release notes](https://github.com/nextcloud/nextcloud-vue/releases)
- [Changelog](https://github.com/nextcloud/nextcloud-vue/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nextcloud/nextcloud-vue/compare/v3.6.0...v3.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-27 02:18:28 +00:00
dependabot[bot]
dd8d674988 Bump @babel/core from 7.13.8 to 7.13.13
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.8 to 7.13.13.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.13/packages/babel-core)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-27 02:12:48 +00:00
dependabot[bot]
72c356854f Bump @babel/preset-env from 7.13.10 to 7.13.12
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.10 to 7.13.12.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.12/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-27 02:09:16 +00:00
dependabot[bot]
ad64fe2f33 Bump url-search-params-polyfill from 8.1.0 to 8.1.1
Bumps [url-search-params-polyfill](https://github.com/jerrybendy/url-search-params-polyfill) from 8.1.0 to 8.1.1.
- [Release notes](https://github.com/jerrybendy/url-search-params-polyfill/releases)
- [Commits](https://github.com/jerrybendy/url-search-params-polyfill/compare/v8.1.0...v8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-27 02:06:59 +00:00
Nextcloud bot
3ac33d0b9d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-03-25 02:37:02 +00:00
Nextcloud bot
1f2dc8ba64 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-03-24 02:36:26 +00:00
Nextcloud bot
d52d2f3500 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-03-23 02:35:09 +00:00
Nextcloud bot
7b42b283bd [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-03-22 02:34:34 +00:00
dependabot[bot]
ff4f4341df Bump vimeo/psalm from 4.6.2 to 4.6.4
Bumps [vimeo/psalm](https://github.com/vimeo/psalm) from 4.6.2 to 4.6.4.
- [Release notes](https://github.com/vimeo/psalm/releases)
- [Commits](https://github.com/vimeo/psalm/compare/4.6.2...4.6.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-20 02:01:52 +00:00
Nextcloud bot
aba617f4c6 [tx-robot] updated from transifex 2021-03-17 02:35:43 +00:00
Nextcloud bot
5f76ed5c88 [tx-robot] updated from transifex 2021-03-15 02:35:12 +00:00
dependabot[bot]
0c2db2bd07 Bump acorn from 8.0.5 to 8.1.0
Bumps [acorn](https://github.com/acornjs/acorn) from 8.0.5 to 8.1.0.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/8.0.5...8.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-13 02:16:34 +00:00
dependabot[bot]
eb2247433b Bump @babel/runtime from 7.13.9 to 7.13.10
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.13.9 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-runtime)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-13 02:14:02 +00:00
dependabot[bot]
62d75a2a80 Bump stylelint from 13.11.0 to 13.12.0
Bumps [stylelint](https://github.com/stylelint/stylelint) from 13.11.0 to 13.12.0.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/13.11.0...13.12.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-13 02:07:26 +00:00
Julius Härtl
188e576af9 Merge pull request #2871 from nextcloud/bugfix/noid/search
Only extract additional attributes from query when doing a raw search
2021-03-11 15:19:51 +01:00
Nextcloud bot
26f68475f7 [tx-robot] updated from transifex 2021-03-11 02:34:42 +00:00
Julius Härtl
3e88e8c251 Merge pull request #2864 from nextcloud/dependabot/npm_and_yarn/master/nextcloud/vue-dashboard-1.1.0
Bump @nextcloud/vue-dashboard from 1.0.1 to 1.1.0
2021-03-09 19:30:18 +01:00
Julius Härtl
e93c3c0f9b Merge pull request #2802 from nextcloud/dependabot/npm_and_yarn/master/eslint-plugin-promise-4.3.1
Bump eslint-plugin-promise from 4.2.1 to 4.3.1
2021-03-09 17:37:12 +01:00
dependabot[bot]
cd78abef5f Bump @nextcloud/vue-dashboard from 1.0.1 to 1.1.0
Bumps [@nextcloud/vue-dashboard](https://github.com/nextcloud/nextcloud-vue-dashboard) from 1.0.1 to 1.1.0.
- [Release notes](https://github.com/nextcloud/nextcloud-vue-dashboard/releases)
- [Changelog](https://github.com/nextcloud/nextcloud-vue-dashboard/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nextcloud/nextcloud-vue-dashboard/compare/v1.0.1...v1.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 16:24:07 +00:00
Julius Härtl
3811959b91 Merge pull request #2875 from nextcloud/dependabot/npm_and_yarn/master/babel/preset-env-7.13.10
Bump @babel/preset-env from 7.13.8 to 7.13.10
2021-03-09 17:17:33 +01:00
dependabot[bot]
8abfac7f93 Bump @babel/preset-env from 7.13.8 to 7.13.10
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.8 to 7.13.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.10/packages/babel-preset-env)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 16:09:40 +00:00
Julius Härtl
87a308c10f Merge pull request #2863 from nextcloud/dependabot/npm_and_yarn/master/babel/runtime-7.13.9
Bump @babel/runtime from 7.13.8 to 7.13.9
2021-03-09 17:02:38 +01:00
Nextcloud bot
e8cc17ffdf [tx-robot] updated from transifex 2021-03-09 02:35:50 +00:00
Julius Härtl
381e5e356f Only extract additional attributes from query when not using the entity mapping for the result
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-08 10:50:21 +01:00
Julius Härtl
3738d1e02b Merge pull request #2857 from nextcloud/bugfix/noid/fclose-node
Don't close tempfile as it is already done
2021-03-08 10:25:41 +01:00
Nextcloud bot
626a8bea7d [tx-robot] updated from transifex 2021-03-08 02:35:04 +00:00
Nextcloud bot
c0831a852e [tx-robot] updated from transifex 2021-03-07 02:33:06 +00:00
Nextcloud bot
2085a23b08 [tx-robot] updated from transifex 2021-03-06 02:33:44 +00:00
dependabot[bot]
a9971963b2 Bump @babel/runtime from 7.13.8 to 7.13.9
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.13.8 to 7.13.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.13.9/packages/babel-runtime)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-06 02:04:28 +00:00
Julius Härtl
20821680d9 Merge pull request #2823 from nextcloud/bugfix/noid/circles-export
Properly pass the user to fetch circles when calling through occ
2021-03-05 15:40:09 +01:00
Julius Härtl
214fb3417d No need to close the temp file as the View is already taking care of it
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-05 15:28:29 +01:00
Nextcloud bot
327d579521 [tx-robot] updated from transifex 2021-03-05 02:33:47 +00:00
Julius Härtl
225a22c93e Merge pull request #2847 from nextcloud/bugfix/noid/content-disposition
Switch to Content-Disposition attachment and check for sane mimetypes
2021-03-04 08:25:46 +01:00
Julius Härtl
9f7901519b Switch to Content-Disposition attachment and check for sane mimetypes
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-04 08:17:28 +01:00
Julius Härtl
6d8a03840e Properly pass the user to fetch circles when calling through occ
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-02-23 14:31:00 +01:00
dependabot[bot]
6ea6303c81 Bump eslint-plugin-promise from 4.2.1 to 4.3.1
Bumps [eslint-plugin-promise](https://github.com/xjamundx/eslint-plugin-promise) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/xjamundx/eslint-plugin-promise/releases)
- [Changelog](https://github.com/xjamundx/eslint-plugin-promise/blob/development/CHANGELOG.md)
- [Commits](https://github.com/xjamundx/eslint-plugin-promise/compare/v4.2.1...v4.3.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-13 02:09:32 +00:00
91 changed files with 4510 additions and 2408 deletions

View File

@@ -1,7 +1,19 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.3.0 - unreleased
## 1.4.0 - 2021-04-13
### Added
* [#2934](https://github.com/nextcloud/deck/pull/2934) Advanced search queries (see [documentation](https://deck.readthedocs.io/en/latest/User_documentation_en/#search) for more details)
* [#2933](https://github.com/nextcloud/deck/pull/2933) Move full text search to proper events
### Fixed
* [#2964](https://github.com/nextcloud/deck/pull/2964) Fix navigating to board details
* Dependency updates
## 1.3.0
### Added
* [#2638](https://github.com/nextcloud/deck/pull/2638) Sharing files to cards

View File

@@ -17,7 +17,7 @@
- 🚀 Get your project organized
</description>
<version>1.4.0-alpha1</version>
<version>1.4.0</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>

View File

@@ -141,5 +141,7 @@ return [
['name' => 'comments_api#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
]
];

View File

@@ -1,34 +1,40 @@
{
"name": "nextcloud/deck",
"type": "project",
"license": "AGPLv3",
"authors": [
{
"name": "Julius Härtl",
"email": "jus@bitgrid.net"
}
],
"require": {
"cogpowered/finediff": "0.3.*"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^21@dev",
"phpunit/phpunit": "^8",
"nextcloud/coding-standard": "^0.5.0",
"symfony/event-dispatcher": "^4.0",
"vimeo/psalm": "^4.3",
"php-parallel-lint/php-parallel-lint": "^1.2"
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"name": "nextcloud/deck",
"type": "project",
"license": "AGPLv3",
"authors": [
{
"name": "Julius Härtl",
"email": "jus@bitgrid.net"
}
],
"require": {
"cogpowered/finediff": "0.3.*"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^21@dev",
"phpunit/phpunit": "^8",
"nextcloud/coding-standard": "^0.5.0",
"symfony/event-dispatcher": "^4.0",
"vimeo/psalm": "^4.3",
"php-parallel-lint/php-parallel-lint": "^1.2"
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm",
"psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType"
}
"psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType",
"test": [
"@test:unit",
"@test:integration"
],
"test:unit": "phpunit -c tests/phpunit.xml",
"test:integration": "phpunit -c tests/phpunit.integration.xml && cd tests/integration && ./run.sh"
}
}

117
composer.lock generated
View File

@@ -154,16 +154,16 @@
},
{
"name": "amphp/byte-stream",
"version": "v1.8.0",
"version": "v1.8.1",
"source": {
"type": "git",
"url": "https://github.com/amphp/byte-stream.git",
"reference": "f0c20cf598a958ba2aa8c6e5a71c697d652c7088"
"reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/amphp/byte-stream/zipball/f0c20cf598a958ba2aa8c6e5a71c697d652c7088",
"reference": "f0c20cf598a958ba2aa8c6e5a71c697d652c7088",
"url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
"reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
"shasum": ""
},
"require": {
@@ -219,9 +219,15 @@
"support": {
"irc": "irc://irc.freenode.org/amphp",
"issues": "https://github.com/amphp/byte-stream/issues",
"source": "https://github.com/amphp/byte-stream/tree/master"
"source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
},
"time": "2020-06-29T18:35:05+00:00"
"funding": [
{
"url": "https://github.com/amphp",
"type": "github"
}
],
"time": "2021-03-30T17:13:30+00:00"
},
{
"name": "christophwurst/nextcloud",
@@ -419,16 +425,16 @@
},
{
"name": "composer/xdebug-handler",
"version": "1.4.5",
"version": "1.4.6",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
"reference": "f28d44c286812c714741478d968104c5e604a1d4"
"reference": "f27e06cd9675801df441b3656569b328e04aa37c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4",
"reference": "f28d44c286812c714741478d968104c5e604a1d4",
"url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c",
"reference": "f27e06cd9675801df441b3656569b328e04aa37c",
"shasum": ""
},
"require": {
@@ -436,7 +442,8 @@
"psr/log": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8"
"phpstan/phpstan": "^0.12.55",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"autoload": {
@@ -462,7 +469,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/xdebug-handler/issues",
"source": "https://github.com/composer/xdebug-handler/tree/1.4.5"
"source": "https://github.com/composer/xdebug-handler/tree/1.4.6"
},
"funding": [
{
@@ -478,7 +485,7 @@
"type": "tidelift"
}
],
"time": "2020-11-13T08:04:11+00:00"
"time": "2021-03-25T17:01:18+00:00"
},
{
"name": "dnoegel/php-xdg-base-dir",
@@ -2048,27 +2055,22 @@
},
{
"name": "psr/container",
"version": "1.0.0",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
@@ -2081,7 +2083,7 @@
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
@@ -2095,9 +2097,9 @@
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/master"
"source": "https://github.com/php-fig/container/tree/1.1.1"
},
"time": "2017-02-14T16:28:37+00:00"
"time": "2021-03-05T17:36:06+00:00"
},
{
"name": "psr/log",
@@ -3205,16 +3207,16 @@
},
{
"name": "symfony/console",
"version": "v5.2.3",
"version": "v5.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a"
"reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/89d4b176d12a2946a1ae4e34906a025b7b6b135a",
"reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a",
"url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d",
"reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d",
"shasum": ""
},
"require": {
@@ -3282,7 +3284,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.2.3"
"source": "https://github.com/symfony/console/tree/v5.2.6"
},
"funding": [
{
@@ -3298,7 +3300,7 @@
"type": "tidelift"
}
],
"time": "2021-01-28T22:06:19+00:00"
"time": "2021-03-28T09:42:18+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -4556,16 +4558,16 @@
},
{
"name": "symfony/string",
"version": "v5.2.3",
"version": "v5.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "c95468897f408dd0aca2ff582074423dd0455122"
"reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122",
"reference": "c95468897f408dd0aca2ff582074423dd0455122",
"url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
"reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
"shasum": ""
},
"require": {
@@ -4619,7 +4621,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.2.3"
"source": "https://github.com/symfony/string/tree/v5.2.6"
},
"funding": [
{
@@ -4635,7 +4637,7 @@
"type": "tidelift"
}
],
"time": "2021-01-25T15:14:59+00:00"
"time": "2021-03-17T17:12:15+00:00"
},
{
"name": "theseer/tokenizer",
@@ -4689,20 +4691,20 @@
},
{
"name": "vimeo/psalm",
"version": "4.6.2",
"version": "4.7.0",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
"reference": "bca09d74adc704c4eaee36a3c3e9d379e290fc3b"
"reference": "d4377c0baf3ffbf0b1ec6998e8d1be2a40971005"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/bca09d74adc704c4eaee36a3c3e9d379e290fc3b",
"reference": "bca09d74adc704c4eaee36a3c3e9d379e290fc3b",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/d4377c0baf3ffbf0b1ec6998e8d1be2a40971005",
"reference": "d4377c0baf3ffbf0b1ec6998e8d1be2a40971005",
"shasum": ""
},
"require": {
"amphp/amp": "^2.1",
"amphp/amp": "^2.4.2",
"amphp/byte-stream": "^1.5",
"composer/package-versions-deprecated": "^1.8.0",
"composer/semver": "^1.4 || ^2.0 || ^3.0",
@@ -4728,7 +4730,6 @@
"psalm/psalm": "self.version"
},
"require-dev": {
"amphp/amp": "^2.4.2",
"bamarni/composer-bin-plugin": "^1.2",
"brianium/paratest": "^4.0||^6.0",
"ext-curl": "*",
@@ -4741,6 +4742,7 @@
"slevomat/coding-standard": "^6.3.11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.3",
"weirdan/phpunit-appveyor-reporter": "^1.0.0",
"weirdan/prophecy-shim": "^1.0 || ^2.0"
},
"suggest": {
@@ -4788,36 +4790,41 @@
],
"support": {
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm/tree/4.6.2"
"source": "https://github.com/vimeo/psalm/tree/4.7.0"
},
"time": "2021-02-26T02:24:18+00:00"
"time": "2021-03-29T03:54:38+00:00"
},
{
"name": "webmozart/assert",
"version": "1.9.1",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0 || ^8.0",
"php": "^7.2 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<3.9.1"
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36 || ^7.5.13"
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
@@ -4841,9 +4848,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.9.1"
"source": "https://github.com/webmozarts/assert/tree/1.10.0"
},
"time": "2020-07-08T17:02:28+00:00"
"time": "2021-03-09T10:59:23+00:00"
},
{
"name": "webmozart/path-util",

View File

@@ -69,3 +69,25 @@ The **sharing tab** allows you to add users or even groups to your boards.
**Deleted objects** allows you to return previously deleted stacks or cards.
The **Timeline** allows you to see everything that happened in your boards. Everything!
## Search
Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls.
This search allows advanced filtering of cards across all board of the logged in user.
For example the search `project tag:ToDo assigned:alice assigned:bob` will return all cards where the card title or description contains project **and** the tag ToDo is set **and** the user alice is assigned **and** the user bob is assigned.
### Supported search filters
| Filter | Operators | Query |
| ----------- | ----------------- | ------------------------------------------------------------ |
| title | `:` | text token used for a case-insentitive search on the cards title |
| description | `:` | text token used for a case-insentitive search on the cards description |
| list | `:` | text token used for a case-insentitive search on the cards list name |
| tag | `:` | text token used for a case-insentitive search on the assigned tags |
| date | `:` | 'overdue', 'today', 'week', 'month', 'none' |
| | `>` `<` `>=` `<=` | Compare the card due date to the passed date (see [supported date formats](https://www.php.net/manual/de/datetime.formats.php)) Card due dates are always considered UTC for comparison |
| assigned | `:` | id or displayname of a user or group for a search on the assigned users or groups |
Other text tokens will be used to perform a case-insensitive search on the card title and description
In addition wuotes can be used to pass a query with spaces, e.g. `"Exact match with spaces"` or `title:"My card"`.

View File

@@ -28,7 +28,7 @@ OC.L10N.register(
"You have deleted card {card} in list {stack} on board {board}" : "Smazali jste kartu {card} ve sloupci {stack} na tabuli {board}",
"{user} has deleted card {card} in list {stack} on board {board}" : "{user} smazal(a) kartu {card} ve sloupci {board} na tabuli {board}",
"You have renamed the card {before} to {card}" : "Přejmenovali jste kartu {before} na {card}",
"{user} has renamed the card {before} to {card}" : "{user} přejmenoval(a) {before} na {card}",
"{user} has renamed the card {before} to {card}" : "{user} přejmenoval(a) kartu {before} na {card}",
"You have added a description to card {card} in list {stack} on board {board}" : "Přidali jste popis ke kartě {card} ve sloupci {stack} na tabuli {board}",
"{user} has added a description to card {card} in list {stack} on board {board}" : "{user} přidal(a) popis ke kartě {card} ve sloupci {stack} na tabuli {board}",
"You have updated the description of card {card} in list {stack} on board {board}" : "Aktualizovali jste popis karty {card} ve sloupci {stack} na tabuli {board}",
@@ -73,7 +73,7 @@ OC.L10N.register(
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} vám přiřadil(a) kartu „%s“ na „%s“.",
"The card \"%s\" on \"%s\" has reached its due date." : "U karty „%s“ z tabule „%s“ nastalo plánované datum dokončení.",
"%s has mentioned you in a comment on \"%s\"." : "%s vás zmínil(a) v komentáři k „%s“.",
"{user} has mentioned you in a comment on \"%s\"." : "{user} vás zmínil(a) v komentáři v „%s“.",
"{user} has mentioned you in a comment on \"%s\"." : "{user} vás zmínil(a) v komentáři k „%s“.",
"The board \"%s\" has been shared with you by %s." : "Uživatel %s vám nasdílel(a) tabuli „%s“.",
"{user} has shared the board %s with you." : "{user} vám nasdílel(a) tabuli %s.",
"No data was provided to create an attachment." : "Nebyla poskytnuta žádná data pro vytvoření přílohy.",
@@ -112,12 +112,12 @@ OC.L10N.register(
"Select a list" : "Vyberte sloupec",
"Card title" : "Název karty",
"Cancel" : "Storno",
"Creating the new card…" : "Vytváření nové karty...",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" bylo přidáno do \"{board}\"",
"Creating the new card…" : "Vytváření nové karty",
"\"{card}\" was added to \"{board}\"" : "{card} bylo přidáno do {board}",
"Open card" : "Otevřít kartu",
"Close" : "Zavřít",
"Create card" : "Vytvořit kartu",
"Select a card" : "Vybrat tabuli",
"Select a card" : "Vybrat kartu",
"Select the card to link to a project" : "Vyberte kartu kterou propojit s projektem",
"Link to card" : "Propojit s kartou",
"File already exists" : "Soubor už existuje",
@@ -206,7 +206,7 @@ OC.L10N.register(
"Remove due date" : "Odstranit termín",
"Select Date" : "Vybrat datum",
"Save" : "Uložit",
"The comment cannot be empty." : "Komentář je třeba vyplnit",
"The comment cannot be empty." : "Komentář je třeba vyplnit.",
"The comment cannot be longer than 1000 characters." : "Délka komentáře může být nejvýše 1 000 znaků.",
"In reply to" : "V odpověď na",
"Reply" : "Odpovědět",
@@ -238,7 +238,7 @@ OC.L10N.register(
"Show boards in calendar/tasks" : "Zobrazit tabule v kalendáři/úkolech",
"Limit deck usage of groups" : "Omezit využití deck na skupiny",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Omezení nastavené pro Deck brání uživatelům, kteří nejsou součástí těchto skupin, ve vytváření vlastních tabulí. Nicméně i tak ale pořád budou moci pracovat na tabulích, které jsou jim nasdíleny.",
"Board details" : "Podrobnosti o desce",
"Board details" : "Podrobnosti o tabuli",
"Edit board" : "Upravit tabuli",
"Clone board" : "Klonovat tabuli",
"Unarchive board" : "Vrátit tabuli zpět z archivu",
@@ -271,7 +271,7 @@ OC.L10N.register(
"Failed to upload {name}" : "Nepodařilo se nahrát {name}",
"Maximum file size of {size} exceeded" : "Překročena nejvyšší umožněná velikost souboru {size}",
"Error creating the share" : "Chyba při vytváření sdílení",
"Share with a Deck card" : "Sdílet kartu aplikace Deck",
"Share with a Deck card" : "Sdílet s kartou aplikace Deck",
"Share {file} with a Deck card" : "Sdílet {file} s kartou aplikace Deck",
"Share" : "Sdílet"
},

View File

@@ -26,7 +26,7 @@
"You have deleted card {card} in list {stack} on board {board}" : "Smazali jste kartu {card} ve sloupci {stack} na tabuli {board}",
"{user} has deleted card {card} in list {stack} on board {board}" : "{user} smazal(a) kartu {card} ve sloupci {board} na tabuli {board}",
"You have renamed the card {before} to {card}" : "Přejmenovali jste kartu {before} na {card}",
"{user} has renamed the card {before} to {card}" : "{user} přejmenoval(a) {before} na {card}",
"{user} has renamed the card {before} to {card}" : "{user} přejmenoval(a) kartu {before} na {card}",
"You have added a description to card {card} in list {stack} on board {board}" : "Přidali jste popis ke kartě {card} ve sloupci {stack} na tabuli {board}",
"{user} has added a description to card {card} in list {stack} on board {board}" : "{user} přidal(a) popis ke kartě {card} ve sloupci {stack} na tabuli {board}",
"You have updated the description of card {card} in list {stack} on board {board}" : "Aktualizovali jste popis karty {card} ve sloupci {stack} na tabuli {board}",
@@ -71,7 +71,7 @@
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} vám přiřadil(a) kartu „%s“ na „%s“.",
"The card \"%s\" on \"%s\" has reached its due date." : "U karty „%s“ z tabule „%s“ nastalo plánované datum dokončení.",
"%s has mentioned you in a comment on \"%s\"." : "%s vás zmínil(a) v komentáři k „%s“.",
"{user} has mentioned you in a comment on \"%s\"." : "{user} vás zmínil(a) v komentáři v „%s“.",
"{user} has mentioned you in a comment on \"%s\"." : "{user} vás zmínil(a) v komentáři k „%s“.",
"The board \"%s\" has been shared with you by %s." : "Uživatel %s vám nasdílel(a) tabuli „%s“.",
"{user} has shared the board %s with you." : "{user} vám nasdílel(a) tabuli %s.",
"No data was provided to create an attachment." : "Nebyla poskytnuta žádná data pro vytvoření přílohy.",
@@ -110,12 +110,12 @@
"Select a list" : "Vyberte sloupec",
"Card title" : "Název karty",
"Cancel" : "Storno",
"Creating the new card…" : "Vytváření nové karty...",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" bylo přidáno do \"{board}\"",
"Creating the new card…" : "Vytváření nové karty",
"\"{card}\" was added to \"{board}\"" : "{card} bylo přidáno do {board}",
"Open card" : "Otevřít kartu",
"Close" : "Zavřít",
"Create card" : "Vytvořit kartu",
"Select a card" : "Vybrat tabuli",
"Select a card" : "Vybrat kartu",
"Select the card to link to a project" : "Vyberte kartu kterou propojit s projektem",
"Link to card" : "Propojit s kartou",
"File already exists" : "Soubor už existuje",
@@ -204,7 +204,7 @@
"Remove due date" : "Odstranit termín",
"Select Date" : "Vybrat datum",
"Save" : "Uložit",
"The comment cannot be empty." : "Komentář je třeba vyplnit",
"The comment cannot be empty." : "Komentář je třeba vyplnit.",
"The comment cannot be longer than 1000 characters." : "Délka komentáře může být nejvýše 1 000 znaků.",
"In reply to" : "V odpověď na",
"Reply" : "Odpovědět",
@@ -236,7 +236,7 @@
"Show boards in calendar/tasks" : "Zobrazit tabule v kalendáři/úkolech",
"Limit deck usage of groups" : "Omezit využití deck na skupiny",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Omezení nastavené pro Deck brání uživatelům, kteří nejsou součástí těchto skupin, ve vytváření vlastních tabulí. Nicméně i tak ale pořád budou moci pracovat na tabulích, které jsou jim nasdíleny.",
"Board details" : "Podrobnosti o desce",
"Board details" : "Podrobnosti o tabuli",
"Edit board" : "Upravit tabuli",
"Clone board" : "Klonovat tabuli",
"Unarchive board" : "Vrátit tabuli zpět z archivu",
@@ -269,7 +269,7 @@
"Failed to upload {name}" : "Nepodařilo se nahrát {name}",
"Maximum file size of {size} exceeded" : "Překročena nejvyšší umožněná velikost souboru {size}",
"Error creating the share" : "Chyba při vytváření sdílení",
"Share with a Deck card" : "Sdílet kartu aplikace Deck",
"Share with a Deck card" : "Sdílet s kartou aplikace Deck",
"Share {file} with a Deck card" : "Sdílet {file} s kartou aplikace Deck",
"Share" : "Sdílet"
},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"

View File

@@ -109,6 +109,7 @@ OC.L10N.register(
"Select a board" : "Hautatu mahai bat",
"Select a list" : "Hautatu zerrenda bat",
"Cancel" : "Utzi",
"Close" : "Itxi",
"Select a card" : "Hautatu txartel bat",
"Select the card to link to a project" : "Hautatu proiektu bati estekatzeko txartela",
"Link to card" : "Estekatu txartelera",
@@ -166,6 +167,7 @@ OC.L10N.register(
"Archive all cards in this list" : "Artxibatu zerrenda honetako txartel guztiak",
"Add a new card" : "Gehitu txartel berri bat",
"Card name" : "Txartel izena",
"List deleted" : "Zerrenda ezabatua",
"Edit" : "Editatu",
"Add a new tag" : "Gehitu etiketa berri bat",
"title and color value must be provided" : "izenburu eta kolore balioak hornitu behar dira",

View File

@@ -107,6 +107,7 @@
"Select a board" : "Hautatu mahai bat",
"Select a list" : "Hautatu zerrenda bat",
"Cancel" : "Utzi",
"Close" : "Itxi",
"Select a card" : "Hautatu txartel bat",
"Select the card to link to a project" : "Hautatu proiektu bati estekatzeko txartela",
"Link to card" : "Estekatu txartelera",
@@ -164,6 +165,7 @@
"Archive all cards in this list" : "Artxibatu zerrenda honetako txartel guztiak",
"Add a new card" : "Gehitu txartel berri bat",
"Card name" : "Txartel izena",
"List deleted" : "Zerrenda ezabatua",
"Edit" : "Editatu",
"Add a new tag" : "Gehitu etiketa berri bat",
"title and color value must be provided" : "izenburu eta kolore balioak hornitu behar dira",

View File

@@ -97,6 +97,9 @@ OC.L10N.register(
"Could not write file to disk" : "Nije moguće zapisati datoteku na disk",
"A PHP extension stopped the file upload" : "Proširenje PHP-a zaustavilo je otpremanje datoteke",
"No file uploaded or file size exceeds maximum of %s" : "Nijedna datoteka nije otpremljena ili veličina datoteke premašuje maksimalnu veličinu od %s",
"Card not found" : "Kartica nije pronađena",
"Path is already shared with this card" : "Put je već podijeljen s ovom karticom",
"Invalid date, date format must be YYYY-MM-DD" : "Nevažeći datum, oblik datuma mora biti GGGG-MM-DD",
"Personal planning and team project organization" : "Osobno planiranje i organizacija timskih projekata",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck je organizacijski alat za kanban projekte usmjeren na osobno planiranje i organizaciju projekta za timove integrirane s Nextcloudom.\n\n\n- 📥 Dodajte svoje zadatke na kartice i poredajte ih po želji\n- 📄 Zapišite dodatne bilješke u markdown\n- 🔖 Dodijelite oznake za još bolju organizaciju\n- 👥 Dijelite sa svojim timom, prijateljima ili obitelji\n- 📎 Priložite datoteke i ugradite ih u svoj markdown opis\n- 💬 Raspravljajte sa svojim timom putem komentara\n- ⚡ Pratite promjene u strujanju aktivnosti\n- 🚀 Organizirajte svoj projekt",
"Card details" : "Pojedinosti o kartici",
@@ -104,9 +107,16 @@ OC.L10N.register(
"Select the board to link to a project" : "Odaberite ploču za povezivanje s projektom",
"Search by board title" : "Traži po naslovu ploče",
"Select board" : "Odaberi ploču",
"Create a new card" : "Stvori novu karticu",
"Select a board" : "Odaberite ploču",
"Select a list" : "Odaberi popis",
"Card title" : "Naslov kartice",
"Cancel" : "Odustani",
"Creating the new card…" : "Stvaranje nove kartice…",
"\"{card}\" was added to \"{board}\"" : "„{card}” je dodano na „{board}”",
"Open card" : "Otvori karticu",
"Close" : "Zatvori",
"Create card" : "Stvori karticu",
"Select a card" : "Odaberite karticu",
"Select the card to link to a project" : "Odaberite karticu za povezivanje s projektom",
"Link to card" : "Poveznica na karticu",
@@ -170,9 +180,15 @@ OC.L10N.register(
"title and color value must be provided" : "potrebno je odabrati naziv i vrijednost boje",
"Board name" : "Naziv ploče",
"Members" : "Članovi",
"Upload new files" : "Otpremi nove datoteke",
"Share from Files" : "Dijeli iz datoteka",
"Add this attachment" : "Dodajte ovaj privitak",
"Show in Files" : "Prikaži u datotekama",
"Unshare file" : "Prestani dijeliti datoteku",
"Delete Attachment" : "Izbriši privitak",
"Restore Attachment" : "Vrati privitak",
"File to share" : "Datoteka za dijeljenje",
"Invalid path selected" : "Odabran nevažeći put",
"Open in sidebar view" : "Otvori u bočnom prikazu",
"Open in bigger view" : "Otvori u većem prikazu",
"Attachments" : "Privici",
@@ -218,6 +234,7 @@ OC.L10N.register(
"All boards" : "Sve ploče",
"Archived boards" : "Arhivirane ploče",
"Shared with you" : "Podijeljeno s vama",
"Use bigger card view" : "Prikaži veće kartice",
"Show boards in calendar/tasks" : "Prikaži ploče u kalendaru/zadacima",
"Limit deck usage of groups" : "Ograniči uporabu decka grupama",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Ograničenjem Decka možete spriječiti korisnike koji ne sudjeluju u tim grupama da stvaraju vlastite ploče. Korisnici će i dalje moći raditi na pločama koje su dijeljene s njima.",
@@ -248,8 +265,14 @@ OC.L10N.register(
"upcoming cards" : "nadolazeće kartice",
"Link to a board" : "Poveznica na ploču",
"Link to a card" : "Poveznica na karticu",
"Create a card" : "Stvori karticu",
"Message from {author} in {conversationName}" : "Poruka od {author} u {conversationName}",
"Something went wrong" : "Nešto je pošlo po krivu",
"Failed to upload {name}" : "Neuspješno otpremanje {name}",
"Maximum file size of {size} exceeded" : "Prekoračena je maksimalna veličina datoteke od {size}"
"Maximum file size of {size} exceeded" : "Prekoračena je maksimalna veličina datoteke od {size}",
"Error creating the share" : "Pogreška pri stvaranju dijeljenja",
"Share with a Deck card" : "Dijeli s Deck karticom",
"Share {file} with a Deck card" : "Dijeli {file} s Deck karticom",
"Share" : "Dijeli"
},
"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;");

View File

@@ -95,6 +95,9 @@
"Could not write file to disk" : "Nije moguće zapisati datoteku na disk",
"A PHP extension stopped the file upload" : "Proširenje PHP-a zaustavilo je otpremanje datoteke",
"No file uploaded or file size exceeds maximum of %s" : "Nijedna datoteka nije otpremljena ili veličina datoteke premašuje maksimalnu veličinu od %s",
"Card not found" : "Kartica nije pronađena",
"Path is already shared with this card" : "Put je već podijeljen s ovom karticom",
"Invalid date, date format must be YYYY-MM-DD" : "Nevažeći datum, oblik datuma mora biti GGGG-MM-DD",
"Personal planning and team project organization" : "Osobno planiranje i organizacija timskih projekata",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck je organizacijski alat za kanban projekte usmjeren na osobno planiranje i organizaciju projekta za timove integrirane s Nextcloudom.\n\n\n- 📥 Dodajte svoje zadatke na kartice i poredajte ih po želji\n- 📄 Zapišite dodatne bilješke u markdown\n- 🔖 Dodijelite oznake za još bolju organizaciju\n- 👥 Dijelite sa svojim timom, prijateljima ili obitelji\n- 📎 Priložite datoteke i ugradite ih u svoj markdown opis\n- 💬 Raspravljajte sa svojim timom putem komentara\n- ⚡ Pratite promjene u strujanju aktivnosti\n- 🚀 Organizirajte svoj projekt",
"Card details" : "Pojedinosti o kartici",
@@ -102,9 +105,16 @@
"Select the board to link to a project" : "Odaberite ploču za povezivanje s projektom",
"Search by board title" : "Traži po naslovu ploče",
"Select board" : "Odaberi ploču",
"Create a new card" : "Stvori novu karticu",
"Select a board" : "Odaberite ploču",
"Select a list" : "Odaberi popis",
"Card title" : "Naslov kartice",
"Cancel" : "Odustani",
"Creating the new card…" : "Stvaranje nove kartice…",
"\"{card}\" was added to \"{board}\"" : "„{card}” je dodano na „{board}”",
"Open card" : "Otvori karticu",
"Close" : "Zatvori",
"Create card" : "Stvori karticu",
"Select a card" : "Odaberite karticu",
"Select the card to link to a project" : "Odaberite karticu za povezivanje s projektom",
"Link to card" : "Poveznica na karticu",
@@ -168,9 +178,15 @@
"title and color value must be provided" : "potrebno je odabrati naziv i vrijednost boje",
"Board name" : "Naziv ploče",
"Members" : "Članovi",
"Upload new files" : "Otpremi nove datoteke",
"Share from Files" : "Dijeli iz datoteka",
"Add this attachment" : "Dodajte ovaj privitak",
"Show in Files" : "Prikaži u datotekama",
"Unshare file" : "Prestani dijeliti datoteku",
"Delete Attachment" : "Izbriši privitak",
"Restore Attachment" : "Vrati privitak",
"File to share" : "Datoteka za dijeljenje",
"Invalid path selected" : "Odabran nevažeći put",
"Open in sidebar view" : "Otvori u bočnom prikazu",
"Open in bigger view" : "Otvori u većem prikazu",
"Attachments" : "Privici",
@@ -216,6 +232,7 @@
"All boards" : "Sve ploče",
"Archived boards" : "Arhivirane ploče",
"Shared with you" : "Podijeljeno s vama",
"Use bigger card view" : "Prikaži veće kartice",
"Show boards in calendar/tasks" : "Prikaži ploče u kalendaru/zadacima",
"Limit deck usage of groups" : "Ograniči uporabu decka grupama",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Ograničenjem Decka možete spriječiti korisnike koji ne sudjeluju u tim grupama da stvaraju vlastite ploče. Korisnici će i dalje moći raditi na pločama koje su dijeljene s njima.",
@@ -246,8 +263,14 @@
"upcoming cards" : "nadolazeće kartice",
"Link to a board" : "Poveznica na ploču",
"Link to a card" : "Poveznica na karticu",
"Create a card" : "Stvori karticu",
"Message from {author} in {conversationName}" : "Poruka od {author} u {conversationName}",
"Something went wrong" : "Nešto je pošlo po krivu",
"Failed to upload {name}" : "Neuspješno otpremanje {name}",
"Maximum file size of {size} exceeded" : "Prekoračena je maksimalna veličina datoteke od {size}"
"Maximum file size of {size} exceeded" : "Prekoračena je maksimalna veličina datoteke od {size}",
"Error creating the share" : "Pogreška pri stvaranju dijeljenja",
"Share with a Deck card" : "Dijeli s Deck karticom",
"Share {file} with a Deck card" : "Dijeli {file} s Deck karticom",
"Share" : "Dijeli"
},"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"
}

View File

@@ -17,18 +17,42 @@ OC.L10N.register(
"{user} has archived the board {before}" : "{user} archiválta a(z) {before} táblát",
"You have unarchived the board {board}" : "Visszavonta a(z) {board} tábla archiválását",
"{user} has unarchived the board {before}" : "{user} visszavonta a(z) {board} tábla archiválását",
"You have created a new list {stack} on board {board}" : "Létrehozta az új {stack} rakást a(z) {board} táblán",
"{user} has created a new list {stack} on board {board}" : "{user} létrehozta az új {stack} rakást a(z) {board} táblán",
"You have renamed list {before} to {stack} on board {board}" : "Átnevezte a(z) {board} tábla {before} rakását erre: {stack}",
"{user} has renamed list {before} to {stack} on board {board}" : "{user} átnevezte a(z) {board} táblá {before} rakását erre: {stack}",
"You have deleted list {stack} on board {board}" : "Törölte a(z) {stack} rakást a(z) {board} tábláról",
"{user} has deleted list {stack} on board {board}" : "{user} törölte a(z) {stack} rakást a(z) {board} tábláról",
"You have created card {card} in list {stack} on board {board}" : "Létrehozta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has created card {card} in list {stack} on board {board}" : "{user} létrehozta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"You have deleted card {card} in list {stack} on board {board}" : "Törölte a(z) {card} kártyát a(z) {stack} rakásból, a(z) {board} táblán",
"{user} has deleted card {card} in list {stack} on board {board}" : "{user} törölte a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"You have renamed the card {before} to {card}" : "Átnevezte a(z) {before} kártyát erre: {card}",
"{user} has renamed the card {before} to {card}" : "{user} átnevezte a(z) {before} kártyát erre: {card}",
"You have added a description to card {card} in list {stack} on board {board}" : "Leírást adott hozzá a(z) {card} kártyához a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has added a description to card {card} in list {stack} on board {board}" : "{user} leírást adott hozzá a(z) {card} kártyához a(z) {stack} rakásban, a(z) {board} táblán",
"You have updated the description of card {card} in list {stack} on board {board}" : "Frissítette a(z) {card} kártya leírását a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has updated the description of the card {card} in list {stack} on board {board}" : "{user} frissítette a(z) {card} kártya leírását a(z) {stack} rakásban, a(z) {board} táblán",
"You have archived card {card} in list {stack} on board {board}" : "Archiválta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has archived card {card} in list {stack} on board {board}" : "{user} archiválta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"You have unarchived card {card} in list {stack} on board {board}" : "Visszavonta a(z) {card} kártya archiválását a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has unarchived card {card} in list {stack} on board {board}" : "{user} visszavonta a(z) {card} kártya archiválását a(z) {stack} rakásban, a(z) {board} táblán",
"You have removed the due date of card {card}" : "Eltávolította a(z) {card} kártya esedékességét",
"{user} has removed the due date of card {card}" : "{user} eltávolította a(z) {card} kártya esedékességét",
"You have set the due date of card {card} to {after}" : "Beállította a(z) {card} kártya esedékességét",
"{user} has set the due date of card {card} to {after}" : "{user} beállította a(z) {card} kártya esedékességét",
"You have updated the due date of card {card} to {after}" : "Frissítette a(z) {card} kártya esedékességét erre: {after}",
"{user} has updated the due date of card {card} to {after}" : "{user} frissítette a(z) {card} kártya esedékességét erre: {after}",
"You have added the tag {label} to card {card} in list {stack} on board {board}" : "Hozzáadta a(z) {label} címkét a(z) {card} kártyához, a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has added the tag {label} to card {card} in list {stack} on board {board}" : "{user} hozzáadta a(z) {label} címkét a(z) {card} kártyához, a(z) {stack} rakásban, a(z) {board} táblán",
"You have removed the tag {label} from card {card} in list {stack} on board {board}" : "Eltávolította a(z) {label} címkét a(z) {card} kártyáról, a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has removed the tag {label} from card {card} in list {stack} on board {board}" : "{user} eltávolította a(z) {label} címkét a(z) {card} kártyáról, a(z) {stack} rakásban, a(z) {board} táblán",
"You have assigned {assigneduser} to card {card} on board {board}" : "Hozzárendelte a(z) {card} kártyát a(z) {board} táblán a következőhöz: {assigneduser}",
"{user} has assigned {assigneduser} to card {card} on board {board}" : "{user} hozzárendelte a(z) {card} kártyát a(z) {board} táblán a következőhöz: {assigneduser}",
"You have unassigned {assigneduser} from card {card} on board {board}" : "Eltávolította a(z) {card} kártyát a(z) {board} táblán a következőtől: {assigneduser}",
"{user} has unassigned {assigneduser} from card {card} on board {board}" : "{user} eltávolította a(z) {card} kártyát a(z) {board} táblán a következőtől: {assigneduser}",
"You have moved the card {card} from list {stackBefore} to {stack}" : "Áthelyezte a(z) {card} kártyát a(z) {stackBefore} rakásból a(z) {stack} rakásba",
"{user} has moved the card {card} from list {stackBefore} to {stack}" : "{user} áthelyezte a(z) {card} kártyát a(z) {stackBefore} rakásból a(z) {stack} rakásba",
"You have added the attachment {attachment} to card {card}" : "Hozzáadta a(z) {attachment} mellékletet a(z) {card} kártyához",
"{user} has added the attachment {attachment} to card {card}" : "{user} hozzáadta a(z) {attachment} mellékletet a(z) {card} kártyához",
"You have updated the attachment {attachment} on card {card}" : "Frissítette a(z) {attachment} mellékletet a(z) {card} kártyánál",
@@ -43,6 +67,7 @@ OC.L10N.register(
"Deck" : "Kártyák",
"Changes in the <strong>Deck app</strong>" : "Változások a <strong>Kártyák alkalmazásban</strong>",
"A <strong>comment</strong> was created on a card" : "Egy <strong>hozzászólás</strong> lett létrehozva egy kártyán",
"Upcoming cards" : "Közelgő kártyák",
"Personal" : "Személyes",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "A(z) „%s” kártyát a(z) „%s” táblán %s hozzárendelte Önhöz.",
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} hozzárendelte Önhöz a(z) „%s” kártyát a(z) „%s”.",
@@ -72,6 +97,9 @@ OC.L10N.register(
"Could not write file to disk" : "Nem lehet a fájlt lemezre írni",
"A PHP extension stopped the file upload" : "A PHP kiterjesztés megállította a fájl feltöltését",
"No file uploaded or file size exceeds maximum of %s" : "Nincs fájl feltöltve, vagy a fájl meghaladja a maximumot: %s",
"Card not found" : "A kártya nem található",
"Path is already shared with this card" : "Az útvonal már meg van osztva ezzel a kártyával",
"Invalid date, date format must be YYYY-MM-DD" : "Érvénytelen dátum, a dátumnak YYYY-MM-DD formátumúnak kell lennie",
"Personal planning and team project organization" : "Személyes tervezés és csapatos projektszervezés",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "A Kártyák egy kanban-stílusú szervezőeszköz, amely a személyes tervezést és a csapatok projektszervezését célozza, a Nextcloudba integrálva.\n\n\n- 📥 Saját feladatok kártyákhoz adása, és azok sorrendezése\n- 📄 További jegyzetek leírása markdownban\n- 🔖 Címkék hozzárendelése a még jobb rendszerezés miatt\n- 👥 Megosztás a csapattal, barátokkal vagy családdal\n- 📎 Fájlok hozzáadása mellékletként, vagy beágyazás a markdown leírásba\n- 💬 Megbeszélés a csapattal hozzászólások használatával\n- ⚡ A változások követése a tevékenységnaplóban\n- 🚀 Rendszerezze a projektjét",
"Card details" : "Kártya részletei",
@@ -79,8 +107,16 @@ OC.L10N.register(
"Select the board to link to a project" : "Válasszon ki egy táblát, amely egy projektre fog hivatkozni",
"Search by board title" : "Keresés táblacím szerint",
"Select board" : "Válasszon táblát",
"Create a new card" : "Új kártya létrehozása",
"Select a board" : "Válasszon egy táblát",
"Select a list" : "Válasszon listát",
"Card title" : "Kártya címe",
"Cancel" : "Mégse",
"Creating the new card…" : "Új kártya létrehozása",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" hozzáadva ehhez: \"{board}\"",
"Open card" : "Kártya megnyitása",
"Close" : "Bezárás",
"Create card" : "Kártya létrehozása",
"Select a card" : "Válasszon egy kártyát",
"Select the card to link to a project" : "Válasszon ki egy kártyát, amely egy projektre fog hivatkozni",
"Link to card" : "Hivatkozás egy kártyára",
@@ -110,6 +146,8 @@ OC.L10N.register(
"Toggle compact mode" : "Kompakt mód be/ki",
"Details" : "Részletek",
"Loading board" : "Tábla betöltése",
"No lists available" : "Nincs elérhető rakás",
"Create a new list to add cards to this board" : "Hozzon létre egy új rakást kártyák ehhez a táblához való hozzáadásához",
"Board not found" : "A tábla nem található",
"Sharing" : "Megosztás",
"Tags" : "Címkék",
@@ -119,6 +157,8 @@ OC.L10N.register(
"Undo" : "Visszavonás",
"Deleted cards" : "Törölt kártyák",
"Share board with a user, group or circle …" : "Tábla megosztása felhasználóval, csoporttal vagy körrel…",
"Searching for users, groups and circles …" : "Felhasználókkal, csoportok és körök keresése",
"No participants found" : "Nem találhatók résztvevők",
"Board owner" : "Tábla tulajdonosa",
"(Group)" : "(Csoport)",
"(Circle)" : "(Kör)",
@@ -126,20 +166,36 @@ OC.L10N.register(
"Can share" : "Megoszthatja",
"Can manage" : "Kezelheti",
"Delete" : "Törlés",
"Failed to create share with {displayName}" : "Nem lehet létrehozni a következő megosztást: {displayName}",
"Add a new list" : "Új lista hozzáadása",
"Archive all cards" : "Az összes kártya archiválása",
"Delete list" : "Lista törlése",
"Add card" : "Kártya hozzáadása",
"Archive all cards in this list" : "Archív kártyák ebben a listában",
"Add a new card" : "Új kártya hozzáadása",
"Card name" : "Kártya neve",
"List deleted" : "Lista törölve",
"Edit" : "Szerkesztés",
"Add a new tag" : "Új címke hozzáadása",
"title and color value must be provided" : "a cím és szín értékét meg kell adni",
"Board name" : "Tábla neve",
"Members" : "Tagok",
"Upload new files" : "Új fájlok feltöltése",
"Share from Files" : "Megosztás a Fájlokból",
"Add this attachment" : "E melléklet hozzáadása",
"Show in Files" : "Megjelenítése a Fájlokban",
"Unshare file" : "Fájl megosztásának visszavonása",
"Delete Attachment" : "Melléklet törlése",
"Restore Attachment" : "Melléklet visszaállítása",
"File to share" : "Fájl megosztása",
"Invalid path selected" : "Érvénytelen útvonal kiválasztva",
"Open in sidebar view" : "Oldalsáv nézet megnyitása",
"Open in bigger view" : "Megtekintés nagyobb nézetben",
"Attachments" : "Mellékletek",
"Comments" : "Hozzászólások",
"Modified" : "Módosítva",
"Created" : "Létrehozva",
"The title cannot be empty." : "A cím nem lehet üres.",
"No comments yet. Begin the discussion!" : "Még nincsenek hozzászólások. Kezdje el a beszélgetést!",
"Assign a tag to this card…" : "Címke rendelése ehhez a kártyához…",
"Assign to users" : "Felhasználókhoz rendelés",
@@ -162,32 +218,61 @@ OC.L10N.register(
"Edit description" : "Leírás szerkesztése",
"View description" : "Leírás megtekintése",
"Add Attachment" : "Melléklet hozzáadása",
"Write a description …" : "Leírás megadása",
"Choose attachment" : "Válasszon mellékletet",
"(group)" : "(csoport)",
"(circle)" : "(kör)",
"Assign to me" : "Hozzám rendelés",
"Unassign myself" : "Saját magam hozzárendelésének eltávolítása",
"Move card" : "Kártya áthelyezése",
"Unarchive card" : "Kártya archiválásának visszavonása",
"Archive card" : "Kártya archiválása",
"Delete card" : "Kártya törlése",
"Move card to another board" : "Kártya áthelyezése egy másik táblára",
"Card deleted" : "Kártya törölve",
"seconds ago" : "másodperce",
"All boards" : "Az összes tábla",
"Archived boards" : "Archivált táblák",
"Shared with you" : "Megosztva Önnel",
"Use bigger card view" : "Nagyobb kártyanézet használata",
"Show boards in calendar/tasks" : "Táblék mutatása a naptárak/teendők között",
"Limit deck usage of groups" : "A kártyák használatának csoportokra korlátozása",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "A Kártyák korlátozása blokkolja a saját táblák létrehozását azoknál a felhasználóknál, akik nem tagjai a megadott csoportoknak. A felhasználók továbbra is tudnak dolgozni a velük megosztott táblákon.",
"Board details" : "Tábla részletei",
"Edit board" : "Tábla szerkesztése",
"Clone board" : "Tábla klónozása",
"Unarchive board" : "Tábla archiválásának visszavonása",
"Archive board" : "Tábla archiválása",
"Turn on due date reminders" : "Határidő emlékeztető beállítása",
"Turn off due date reminders" : "Határidő emlékeztető kikapcsolása",
"Due date reminders" : "Határidő emlékeztetők",
"All cards" : "Összes kártya",
"Assigned cards" : "Hozzárendelt kártyák",
"No notifications" : "Nincsenek értesítések",
"Delete board" : "Tábla törlése",
"Board {0} deleted" : "Törölte a(z) {board} táblát",
"Only assigned cards" : "Csak hozzárendelt kártyák",
"No reminder" : "Nincs emlékeztető",
"An error occurred" : "Hiba történt",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Biztos, hogy törli a(z) {title} táblát? Ez törölni fogja a tábla összes adatát.",
"Delete the board?" : "Törli a táblát?",
"Loading filtered view" : "Szűrt nézet betöltése",
"Today" : "Ma",
"Tomorrow" : "Holnap",
"This week" : "Ez a hét",
"No due" : "Nincs határidő",
"No upcoming cards" : "Nincsenek közelgő kártyák",
"upcoming cards" : "közelgő kártyák",
"Link to a board" : "Hivatkozás egy táblához",
"Link to a card" : "Hivatkozás egy kártyához",
"Create a card" : "Kártya létrehozása",
"Message from {author} in {conversationName}" : "Üzenet a {conversationName} beszélgetésben tőle: {author}",
"Something went wrong" : "Valami hiba történt",
"Maximum file size of {size} exceeded" : "A legnagyobb fájlméret ({size}) túllépve"
"Failed to upload {name}" : "Feltöltés sikertelen: {name}",
"Maximum file size of {size} exceeded" : "A legnagyobb fájlméret ({size}) túllépve",
"Error creating the share" : "Megosztás létrehozása sikertelen",
"Share with a Deck card" : "Megosztás kártyával",
"Share {file} with a Deck card" : "A(z) {file} megosztása egy Kártyák kártyával",
"Share" : "Megosztás"
},
"nplurals=2; plural=(n != 1);");

View File

@@ -15,18 +15,42 @@
"{user} has archived the board {before}" : "{user} archiválta a(z) {before} táblát",
"You have unarchived the board {board}" : "Visszavonta a(z) {board} tábla archiválását",
"{user} has unarchived the board {before}" : "{user} visszavonta a(z) {board} tábla archiválását",
"You have created a new list {stack} on board {board}" : "Létrehozta az új {stack} rakást a(z) {board} táblán",
"{user} has created a new list {stack} on board {board}" : "{user} létrehozta az új {stack} rakást a(z) {board} táblán",
"You have renamed list {before} to {stack} on board {board}" : "Átnevezte a(z) {board} tábla {before} rakását erre: {stack}",
"{user} has renamed list {before} to {stack} on board {board}" : "{user} átnevezte a(z) {board} táblá {before} rakását erre: {stack}",
"You have deleted list {stack} on board {board}" : "Törölte a(z) {stack} rakást a(z) {board} tábláról",
"{user} has deleted list {stack} on board {board}" : "{user} törölte a(z) {stack} rakást a(z) {board} tábláról",
"You have created card {card} in list {stack} on board {board}" : "Létrehozta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has created card {card} in list {stack} on board {board}" : "{user} létrehozta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"You have deleted card {card} in list {stack} on board {board}" : "Törölte a(z) {card} kártyát a(z) {stack} rakásból, a(z) {board} táblán",
"{user} has deleted card {card} in list {stack} on board {board}" : "{user} törölte a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"You have renamed the card {before} to {card}" : "Átnevezte a(z) {before} kártyát erre: {card}",
"{user} has renamed the card {before} to {card}" : "{user} átnevezte a(z) {before} kártyát erre: {card}",
"You have added a description to card {card} in list {stack} on board {board}" : "Leírást adott hozzá a(z) {card} kártyához a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has added a description to card {card} in list {stack} on board {board}" : "{user} leírást adott hozzá a(z) {card} kártyához a(z) {stack} rakásban, a(z) {board} táblán",
"You have updated the description of card {card} in list {stack} on board {board}" : "Frissítette a(z) {card} kártya leírását a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has updated the description of the card {card} in list {stack} on board {board}" : "{user} frissítette a(z) {card} kártya leírását a(z) {stack} rakásban, a(z) {board} táblán",
"You have archived card {card} in list {stack} on board {board}" : "Archiválta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has archived card {card} in list {stack} on board {board}" : "{user} archiválta a(z) {card} kártyát a(z) {stack} rakásban, a(z) {board} táblán",
"You have unarchived card {card} in list {stack} on board {board}" : "Visszavonta a(z) {card} kártya archiválását a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has unarchived card {card} in list {stack} on board {board}" : "{user} visszavonta a(z) {card} kártya archiválását a(z) {stack} rakásban, a(z) {board} táblán",
"You have removed the due date of card {card}" : "Eltávolította a(z) {card} kártya esedékességét",
"{user} has removed the due date of card {card}" : "{user} eltávolította a(z) {card} kártya esedékességét",
"You have set the due date of card {card} to {after}" : "Beállította a(z) {card} kártya esedékességét",
"{user} has set the due date of card {card} to {after}" : "{user} beállította a(z) {card} kártya esedékességét",
"You have updated the due date of card {card} to {after}" : "Frissítette a(z) {card} kártya esedékességét erre: {after}",
"{user} has updated the due date of card {card} to {after}" : "{user} frissítette a(z) {card} kártya esedékességét erre: {after}",
"You have added the tag {label} to card {card} in list {stack} on board {board}" : "Hozzáadta a(z) {label} címkét a(z) {card} kártyához, a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has added the tag {label} to card {card} in list {stack} on board {board}" : "{user} hozzáadta a(z) {label} címkét a(z) {card} kártyához, a(z) {stack} rakásban, a(z) {board} táblán",
"You have removed the tag {label} from card {card} in list {stack} on board {board}" : "Eltávolította a(z) {label} címkét a(z) {card} kártyáról, a(z) {stack} rakásban, a(z) {board} táblán",
"{user} has removed the tag {label} from card {card} in list {stack} on board {board}" : "{user} eltávolította a(z) {label} címkét a(z) {card} kártyáról, a(z) {stack} rakásban, a(z) {board} táblán",
"You have assigned {assigneduser} to card {card} on board {board}" : "Hozzárendelte a(z) {card} kártyát a(z) {board} táblán a következőhöz: {assigneduser}",
"{user} has assigned {assigneduser} to card {card} on board {board}" : "{user} hozzárendelte a(z) {card} kártyát a(z) {board} táblán a következőhöz: {assigneduser}",
"You have unassigned {assigneduser} from card {card} on board {board}" : "Eltávolította a(z) {card} kártyát a(z) {board} táblán a következőtől: {assigneduser}",
"{user} has unassigned {assigneduser} from card {card} on board {board}" : "{user} eltávolította a(z) {card} kártyát a(z) {board} táblán a következőtől: {assigneduser}",
"You have moved the card {card} from list {stackBefore} to {stack}" : "Áthelyezte a(z) {card} kártyát a(z) {stackBefore} rakásból a(z) {stack} rakásba",
"{user} has moved the card {card} from list {stackBefore} to {stack}" : "{user} áthelyezte a(z) {card} kártyát a(z) {stackBefore} rakásból a(z) {stack} rakásba",
"You have added the attachment {attachment} to card {card}" : "Hozzáadta a(z) {attachment} mellékletet a(z) {card} kártyához",
"{user} has added the attachment {attachment} to card {card}" : "{user} hozzáadta a(z) {attachment} mellékletet a(z) {card} kártyához",
"You have updated the attachment {attachment} on card {card}" : "Frissítette a(z) {attachment} mellékletet a(z) {card} kártyánál",
@@ -41,6 +65,7 @@
"Deck" : "Kártyák",
"Changes in the <strong>Deck app</strong>" : "Változások a <strong>Kártyák alkalmazásban</strong>",
"A <strong>comment</strong> was created on a card" : "Egy <strong>hozzászólás</strong> lett létrehozva egy kártyán",
"Upcoming cards" : "Közelgő kártyák",
"Personal" : "Személyes",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "A(z) „%s” kártyát a(z) „%s” táblán %s hozzárendelte Önhöz.",
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} hozzárendelte Önhöz a(z) „%s” kártyát a(z) „%s”.",
@@ -70,6 +95,9 @@
"Could not write file to disk" : "Nem lehet a fájlt lemezre írni",
"A PHP extension stopped the file upload" : "A PHP kiterjesztés megállította a fájl feltöltését",
"No file uploaded or file size exceeds maximum of %s" : "Nincs fájl feltöltve, vagy a fájl meghaladja a maximumot: %s",
"Card not found" : "A kártya nem található",
"Path is already shared with this card" : "Az útvonal már meg van osztva ezzel a kártyával",
"Invalid date, date format must be YYYY-MM-DD" : "Érvénytelen dátum, a dátumnak YYYY-MM-DD formátumúnak kell lennie",
"Personal planning and team project organization" : "Személyes tervezés és csapatos projektszervezés",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "A Kártyák egy kanban-stílusú szervezőeszköz, amely a személyes tervezést és a csapatok projektszervezését célozza, a Nextcloudba integrálva.\n\n\n- 📥 Saját feladatok kártyákhoz adása, és azok sorrendezése\n- 📄 További jegyzetek leírása markdownban\n- 🔖 Címkék hozzárendelése a még jobb rendszerezés miatt\n- 👥 Megosztás a csapattal, barátokkal vagy családdal\n- 📎 Fájlok hozzáadása mellékletként, vagy beágyazás a markdown leírásba\n- 💬 Megbeszélés a csapattal hozzászólások használatával\n- ⚡ A változások követése a tevékenységnaplóban\n- 🚀 Rendszerezze a projektjét",
"Card details" : "Kártya részletei",
@@ -77,8 +105,16 @@
"Select the board to link to a project" : "Válasszon ki egy táblát, amely egy projektre fog hivatkozni",
"Search by board title" : "Keresés táblacím szerint",
"Select board" : "Válasszon táblát",
"Create a new card" : "Új kártya létrehozása",
"Select a board" : "Válasszon egy táblát",
"Select a list" : "Válasszon listát",
"Card title" : "Kártya címe",
"Cancel" : "Mégse",
"Creating the new card…" : "Új kártya létrehozása",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" hozzáadva ehhez: \"{board}\"",
"Open card" : "Kártya megnyitása",
"Close" : "Bezárás",
"Create card" : "Kártya létrehozása",
"Select a card" : "Válasszon egy kártyát",
"Select the card to link to a project" : "Válasszon ki egy kártyát, amely egy projektre fog hivatkozni",
"Link to card" : "Hivatkozás egy kártyára",
@@ -108,6 +144,8 @@
"Toggle compact mode" : "Kompakt mód be/ki",
"Details" : "Részletek",
"Loading board" : "Tábla betöltése",
"No lists available" : "Nincs elérhető rakás",
"Create a new list to add cards to this board" : "Hozzon létre egy új rakást kártyák ehhez a táblához való hozzáadásához",
"Board not found" : "A tábla nem található",
"Sharing" : "Megosztás",
"Tags" : "Címkék",
@@ -117,6 +155,8 @@
"Undo" : "Visszavonás",
"Deleted cards" : "Törölt kártyák",
"Share board with a user, group or circle …" : "Tábla megosztása felhasználóval, csoporttal vagy körrel…",
"Searching for users, groups and circles …" : "Felhasználókkal, csoportok és körök keresése",
"No participants found" : "Nem találhatók résztvevők",
"Board owner" : "Tábla tulajdonosa",
"(Group)" : "(Csoport)",
"(Circle)" : "(Kör)",
@@ -124,20 +164,36 @@
"Can share" : "Megoszthatja",
"Can manage" : "Kezelheti",
"Delete" : "Törlés",
"Failed to create share with {displayName}" : "Nem lehet létrehozni a következő megosztást: {displayName}",
"Add a new list" : "Új lista hozzáadása",
"Archive all cards" : "Az összes kártya archiválása",
"Delete list" : "Lista törlése",
"Add card" : "Kártya hozzáadása",
"Archive all cards in this list" : "Archív kártyák ebben a listában",
"Add a new card" : "Új kártya hozzáadása",
"Card name" : "Kártya neve",
"List deleted" : "Lista törölve",
"Edit" : "Szerkesztés",
"Add a new tag" : "Új címke hozzáadása",
"title and color value must be provided" : "a cím és szín értékét meg kell adni",
"Board name" : "Tábla neve",
"Members" : "Tagok",
"Upload new files" : "Új fájlok feltöltése",
"Share from Files" : "Megosztás a Fájlokból",
"Add this attachment" : "E melléklet hozzáadása",
"Show in Files" : "Megjelenítése a Fájlokban",
"Unshare file" : "Fájl megosztásának visszavonása",
"Delete Attachment" : "Melléklet törlése",
"Restore Attachment" : "Melléklet visszaállítása",
"File to share" : "Fájl megosztása",
"Invalid path selected" : "Érvénytelen útvonal kiválasztva",
"Open in sidebar view" : "Oldalsáv nézet megnyitása",
"Open in bigger view" : "Megtekintés nagyobb nézetben",
"Attachments" : "Mellékletek",
"Comments" : "Hozzászólások",
"Modified" : "Módosítva",
"Created" : "Létrehozva",
"The title cannot be empty." : "A cím nem lehet üres.",
"No comments yet. Begin the discussion!" : "Még nincsenek hozzászólások. Kezdje el a beszélgetést!",
"Assign a tag to this card…" : "Címke rendelése ehhez a kártyához…",
"Assign to users" : "Felhasználókhoz rendelés",
@@ -160,32 +216,61 @@
"Edit description" : "Leírás szerkesztése",
"View description" : "Leírás megtekintése",
"Add Attachment" : "Melléklet hozzáadása",
"Write a description …" : "Leírás megadása",
"Choose attachment" : "Válasszon mellékletet",
"(group)" : "(csoport)",
"(circle)" : "(kör)",
"Assign to me" : "Hozzám rendelés",
"Unassign myself" : "Saját magam hozzárendelésének eltávolítása",
"Move card" : "Kártya áthelyezése",
"Unarchive card" : "Kártya archiválásának visszavonása",
"Archive card" : "Kártya archiválása",
"Delete card" : "Kártya törlése",
"Move card to another board" : "Kártya áthelyezése egy másik táblára",
"Card deleted" : "Kártya törölve",
"seconds ago" : "másodperce",
"All boards" : "Az összes tábla",
"Archived boards" : "Archivált táblák",
"Shared with you" : "Megosztva Önnel",
"Use bigger card view" : "Nagyobb kártyanézet használata",
"Show boards in calendar/tasks" : "Táblék mutatása a naptárak/teendők között",
"Limit deck usage of groups" : "A kártyák használatának csoportokra korlátozása",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "A Kártyák korlátozása blokkolja a saját táblák létrehozását azoknál a felhasználóknál, akik nem tagjai a megadott csoportoknak. A felhasználók továbbra is tudnak dolgozni a velük megosztott táblákon.",
"Board details" : "Tábla részletei",
"Edit board" : "Tábla szerkesztése",
"Clone board" : "Tábla klónozása",
"Unarchive board" : "Tábla archiválásának visszavonása",
"Archive board" : "Tábla archiválása",
"Turn on due date reminders" : "Határidő emlékeztető beállítása",
"Turn off due date reminders" : "Határidő emlékeztető kikapcsolása",
"Due date reminders" : "Határidő emlékeztetők",
"All cards" : "Összes kártya",
"Assigned cards" : "Hozzárendelt kártyák",
"No notifications" : "Nincsenek értesítések",
"Delete board" : "Tábla törlése",
"Board {0} deleted" : "Törölte a(z) {board} táblát",
"Only assigned cards" : "Csak hozzárendelt kártyák",
"No reminder" : "Nincs emlékeztető",
"An error occurred" : "Hiba történt",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Biztos, hogy törli a(z) {title} táblát? Ez törölni fogja a tábla összes adatát.",
"Delete the board?" : "Törli a táblát?",
"Loading filtered view" : "Szűrt nézet betöltése",
"Today" : "Ma",
"Tomorrow" : "Holnap",
"This week" : "Ez a hét",
"No due" : "Nincs határidő",
"No upcoming cards" : "Nincsenek közelgő kártyák",
"upcoming cards" : "közelgő kártyák",
"Link to a board" : "Hivatkozás egy táblához",
"Link to a card" : "Hivatkozás egy kártyához",
"Create a card" : "Kártya létrehozása",
"Message from {author} in {conversationName}" : "Üzenet a {conversationName} beszélgetésben tőle: {author}",
"Something went wrong" : "Valami hiba történt",
"Maximum file size of {size} exceeded" : "A legnagyobb fájlméret ({size}) túllépve"
"Failed to upload {name}" : "Feltöltés sikertelen: {name}",
"Maximum file size of {size} exceeded" : "A legnagyobb fájlméret ({size}) túllépve",
"Error creating the share" : "Megosztás létrehozása sikertelen",
"Share with a Deck card" : "Megosztás kártyával",
"Share {file} with a Deck card" : "A(z) {file} megosztása egy Kártyák kártyával",
"Share" : "Megosztás"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

View File

@@ -107,9 +107,16 @@ OC.L10N.register(
"Select the board to link to a project" : "Избери табла за поврзување со проект",
"Search by board title" : "Барај по име на табла",
"Select board" : "Избери табла",
"Create a new card" : "Креирајте нова картица",
"Select a board" : "Избери табла",
"Select a list" : "Избери листа",
"Card title" : "Наслов на картицата",
"Cancel" : "Откажи",
"Creating the new card…" : "Креирање нова картица...",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" е додадена на \"{board}\"",
"Open card" : "Отвори картица",
"Close" : "Затвори",
"Create card" : "Креирајте картица",
"Select a card" : "Избери картица",
"Select the card to link to a project" : "Избери картица за поврзување со проект",
"Link to card" : "Линк до картица",
@@ -258,6 +265,8 @@ OC.L10N.register(
"upcoming cards" : "престојни картици",
"Link to a board" : "Линк до табла",
"Link to a card" : "Линк до картица",
"Create a card" : "Креирајте картица",
"Message from {author} in {conversationName}" : "Порака од {author} во {conversationName}",
"Something went wrong" : "Нешто не е во ред",
"Failed to upload {name}" : "Неуспешно прикачување {name}",
"Maximum file size of {size} exceeded" : "Максималната големина на датотека од {size} е достигната",

View File

@@ -105,9 +105,16 @@
"Select the board to link to a project" : "Избери табла за поврзување со проект",
"Search by board title" : "Барај по име на табла",
"Select board" : "Избери табла",
"Create a new card" : "Креирајте нова картица",
"Select a board" : "Избери табла",
"Select a list" : "Избери листа",
"Card title" : "Наслов на картицата",
"Cancel" : "Откажи",
"Creating the new card…" : "Креирање нова картица...",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" е додадена на \"{board}\"",
"Open card" : "Отвори картица",
"Close" : "Затвори",
"Create card" : "Креирајте картица",
"Select a card" : "Избери картица",
"Select the card to link to a project" : "Избери картица за поврзување со проект",
"Link to card" : "Линк до картица",
@@ -256,6 +263,8 @@
"upcoming cards" : "престојни картици",
"Link to a board" : "Линк до табла",
"Link to a card" : "Линк до картица",
"Create a card" : "Креирајте картица",
"Message from {author} in {conversationName}" : "Порака од {author} во {conversationName}",
"Something went wrong" : "Нешто не е во ред",
"Failed to upload {name}" : "Неуспешно прикачување {name}",
"Maximum file size of {size} exceeded" : "Максималната големина на датотека од {size} е достигната",

View File

@@ -93,7 +93,7 @@ OC.L10N.register(
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "Wysłany plik przekracza wielkość dyrektywy MAX_FILE_SIZE określonej w formularzu HTML",
"The file was only partially uploaded" : "Załadowany plik został wysłany tylko częściowo.",
"No file was uploaded" : "Nie wysłano żadnego pliku",
"Missing a temporary folder" : "Brak folderu tymczasowego",
"Missing a temporary folder" : "Brak katalogu tymczasowego",
"Could not write file to disk" : "Nie można zapisać pliku na dysk",
"A PHP extension stopped the file upload" : "Rozszerzenie PHP zatrzymało wysyłanie pliku",
"No file uploaded or file size exceeds maximum of %s" : "Brak wysłanego pliku lub rozmiar pliku przekracza maksymalny limit %s",
@@ -184,7 +184,7 @@ OC.L10N.register(
"Share from Files" : "Udostępnij z Plików",
"Add this attachment" : "Dodaj ten załącznik",
"Show in Files" : "Pokaż w Plikach",
"Unshare file" : "Cofnij udostępnianie pliku",
"Unshare file" : "Zatrzymaj udostępnianie pliku",
"Delete Attachment" : "Usuń załącznik",
"Restore Attachment" : "Przywróć załącznik",
"File to share" : "Plik do udostępnienia",

View File

@@ -91,7 +91,7 @@
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "Wysłany plik przekracza wielkość dyrektywy MAX_FILE_SIZE określonej w formularzu HTML",
"The file was only partially uploaded" : "Załadowany plik został wysłany tylko częściowo.",
"No file was uploaded" : "Nie wysłano żadnego pliku",
"Missing a temporary folder" : "Brak folderu tymczasowego",
"Missing a temporary folder" : "Brak katalogu tymczasowego",
"Could not write file to disk" : "Nie można zapisać pliku na dysk",
"A PHP extension stopped the file upload" : "Rozszerzenie PHP zatrzymało wysyłanie pliku",
"No file uploaded or file size exceeds maximum of %s" : "Brak wysłanego pliku lub rozmiar pliku przekracza maksymalny limit %s",
@@ -182,7 +182,7 @@
"Share from Files" : "Udostępnij z Plików",
"Add this attachment" : "Dodaj ten załącznik",
"Show in Files" : "Pokaż w Plikach",
"Unshare file" : "Cofnij udostępnianie pliku",
"Unshare file" : "Zatrzymaj udostępnianie pliku",
"Delete Attachment" : "Usuń załącznik",
"Restore Attachment" : "Przywróć załącznik",
"File to share" : "Plik do udostępnienia",

View File

@@ -167,11 +167,112 @@ OC.L10N.register(
"Can manage" : "Faghet a gestire",
"Delete" : "Cantzella",
"Failed to create share with {displayName}" : "No at fatu a creare cumpartzidura cun {displayName}",
"Add a new list" : "Agiunghe un'elencu nou",
"Archive all cards" : "Archìvia totu is ischedas",
"Delete list" : "Cantzella elencu",
"Add card" : "Agiunghe ischeda",
"Archive all cards in this list" : "Archìvia totu is ischedas in cust'elencu",
"Add a new card" : "Agiùnghe un'ischeda noa",
"Card name" : "Nùmene de s'ischeda",
"List deleted" : "Elencu cantzelladu",
"Edit" : "Modìfica ",
"Add a new tag" : "Agiunghe un'eticheta noa",
"title and color value must be provided" : "tocat de frunire su tìtulu e su balore de su colore",
"Board name" : "Nùmene de sa lavagna",
"Members" : "Membros",
"Upload new files" : "Carriga archìvios noos",
"Share from Files" : "Cumpartzi dae Archìvios",
"Add this attachment" : "Agiunghe custu alligongiadu",
"Show in Files" : "Mustra in Archìvios",
"Unshare file" : "Annulla cumpartzidura de s'archìviu",
"Delete Attachment" : "Cantzella alligongiadu",
"Restore Attachment" : "Riprìstina alligongiadu",
"File to share" : "Archìviu de cumpartzire",
"Invalid path selected" : "Caminu seletzionadu non bàlidu",
"Open in sidebar view" : "Aberi in s'istanca laterale",
"Open in bigger view" : "Aberi in una bista prus ampra",
"Attachments" : "Alligongiados",
"Comments" : "Cummentos",
"Modified" : "Modificadu",
"Created" : "Creadu",
"The title cannot be empty." : "Su tìtulu non podet èssere bòidu",
"No comments yet. Begin the discussion!" : "Perunu cummentu ancora. Cumintzat sa chistionada!",
"Assign a tag to this card…" : "Assigna un'eticheta a cust'ischeda...",
"Assign to users" : "Assigna a utentes",
"Assign to users/groups/circles" : "Assigna a utentes/grupos/tropas",
"Assign a user to this card…" : "Assigna utente a cust'ischeda...",
"Due date" : "Iscadèntzia",
"Set a due date" : "Imposta iscadèntzia",
"Remove due date" : "Boga s'iscadèntzia",
"Select Date" : "Seletziona data",
"Save" : "Sarva",
"Description" : "Descritzione"
"The comment cannot be empty." : "Su cummentu non podet èssere bòidu",
"The comment cannot be longer than 1000 characters." : "Su cummentu non podet èssere prus longu de 1000 caràteres.",
"In reply to" : "Rispondende a ",
"Reply" : "Risponde",
"Update" : "Agiorna",
"Description" : "Descritzione",
"(Unsaved)" : "(Non sarvada)",
"(Saving…)" : "(Sarbende…)",
"Formatting help" : "Ghia pro sa formatatzione",
"Edit description" : "Modìfica descritzione",
"View description" : "Visualiza descritzione",
"Add Attachment" : "Agiunghe alligongiadu",
"Write a description …" : "Iscrie una descritzione ...",
"Choose attachment" : "Sèbera un'alligongiadu",
"(group)" : "(grupu)",
"(circle)" : "(tropa)",
"Assign to me" : "Assigna a mie",
"Unassign myself" : "Annulla s'assignatzione a mie",
"Move card" : "Tràmuda ischeda",
"Unarchive card" : "Ischeda no archiviada",
"Archive card" : "Archìviu no archiviadu",
"Delete card" : "Cantzella ischeda",
"Move card to another board" : "Tràmuda s'ischeda a un'àtera lavagna",
"Card deleted" : "Ischeda cantzellada",
"seconds ago" : "segundos a immoe",
"All boards" : "Totu is lavagnas",
"Archived boards" : "Lavagnas archiviadas",
"Shared with you" : "Cumpartzidu cun tegus",
"Use bigger card view" : "Imprea bista cun ischedas prus mannas",
"Show boards in calendar/tasks" : "Mustra lavagnas in calendàriu/fainas",
"Limit deck usage of groups" : "Mìnima s'impreu de deck de is grupos",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Su de minimare su deck at a impedire a is utentes chi non faghent parte de cuddos grupos de si creare lavagnas pròpias. Is utentes ant a èssere ancora capassos de traballare in is lavagnas chi fiant istadas cumpartzidas cun issos etotu,",
"Board details" : "Detàllios lavagna",
"Edit board" : "Modìfica lavagna",
"Clone board" : "Clona lavagna",
"Unarchive board" : "Annulla s'archiviatzione de sa lavagna",
"Archive board" : "Archìvia lavagna",
"Turn on due date reminders" : "Allughe is notìficas pro ammentare is iscadèntzias",
"Turn off due date reminders" : "Istuda is notìficas pro ammentare is iscadèntzias",
"Due date reminders" : "Notìficas pro ammentare is iscadèntzias",
"All cards" : "Totu is ischedas",
"Assigned cards" : "Ischedas assignadas",
"No notifications" : "Peruna notìfica",
"Delete board" : "Cantzella lavagna",
"Board {0} deleted" : "Lavagna {0} cantzellada",
"Only assigned cards" : "Isceti ischedas assignadas",
"No reminder" : "Perunu apuntu",
"An error occurred" : "Ddoe at àpidu un'errore",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Ses seguru chi cheres cantzellare sa lavagna {title}? Custa operatzione at a cantzellare totu is datos de custa lavagna.",
"Delete the board?" : "Cheres cantzellare sa lavagna?",
"Loading filtered view" : "Carrigamentu de sa bista cun su filtru",
"Today" : "Oe",
"Tomorrow" : "Cras",
"This week" : "Custa chida",
"No due" : "Peruna iscadèntzia",
"No upcoming cards" : "Peruna ischeda abarrada",
"upcoming cards" : "ischedas abarradas",
"Link to a board" : "Collega a una tabella",
"Link to a card" : "Collega a un'ischeda",
"Create a card" : "Crea un'ischeda",
"Message from {author} in {conversationName}" : "Messàgiu de {author} in {conversationName}",
"Something went wrong" : "Ddoe at àpidu un'errore",
"Failed to upload {name}" : "No at fatu a agiornare {name}",
"Maximum file size of {size} exceeded" : "Mannària màssima de s'archìviu de {size} superada",
"Error creating the share" : "Errore in sa creatzione de sa cumpatzidura",
"Share with a Deck card" : "Cumpartzi cun un'ischeda deck",
"Share {file} with a Deck card" : "Cumpartzi {file} cun un'ischeda de deck",
"Share" : "Cumpartzi"
},
"nplurals=2; plural=(n != 1);");

View File

@@ -165,11 +165,112 @@
"Can manage" : "Faghet a gestire",
"Delete" : "Cantzella",
"Failed to create share with {displayName}" : "No at fatu a creare cumpartzidura cun {displayName}",
"Add a new list" : "Agiunghe un'elencu nou",
"Archive all cards" : "Archìvia totu is ischedas",
"Delete list" : "Cantzella elencu",
"Add card" : "Agiunghe ischeda",
"Archive all cards in this list" : "Archìvia totu is ischedas in cust'elencu",
"Add a new card" : "Agiùnghe un'ischeda noa",
"Card name" : "Nùmene de s'ischeda",
"List deleted" : "Elencu cantzelladu",
"Edit" : "Modìfica ",
"Add a new tag" : "Agiunghe un'eticheta noa",
"title and color value must be provided" : "tocat de frunire su tìtulu e su balore de su colore",
"Board name" : "Nùmene de sa lavagna",
"Members" : "Membros",
"Upload new files" : "Carriga archìvios noos",
"Share from Files" : "Cumpartzi dae Archìvios",
"Add this attachment" : "Agiunghe custu alligongiadu",
"Show in Files" : "Mustra in Archìvios",
"Unshare file" : "Annulla cumpartzidura de s'archìviu",
"Delete Attachment" : "Cantzella alligongiadu",
"Restore Attachment" : "Riprìstina alligongiadu",
"File to share" : "Archìviu de cumpartzire",
"Invalid path selected" : "Caminu seletzionadu non bàlidu",
"Open in sidebar view" : "Aberi in s'istanca laterale",
"Open in bigger view" : "Aberi in una bista prus ampra",
"Attachments" : "Alligongiados",
"Comments" : "Cummentos",
"Modified" : "Modificadu",
"Created" : "Creadu",
"The title cannot be empty." : "Su tìtulu non podet èssere bòidu",
"No comments yet. Begin the discussion!" : "Perunu cummentu ancora. Cumintzat sa chistionada!",
"Assign a tag to this card…" : "Assigna un'eticheta a cust'ischeda...",
"Assign to users" : "Assigna a utentes",
"Assign to users/groups/circles" : "Assigna a utentes/grupos/tropas",
"Assign a user to this card…" : "Assigna utente a cust'ischeda...",
"Due date" : "Iscadèntzia",
"Set a due date" : "Imposta iscadèntzia",
"Remove due date" : "Boga s'iscadèntzia",
"Select Date" : "Seletziona data",
"Save" : "Sarva",
"Description" : "Descritzione"
"The comment cannot be empty." : "Su cummentu non podet èssere bòidu",
"The comment cannot be longer than 1000 characters." : "Su cummentu non podet èssere prus longu de 1000 caràteres.",
"In reply to" : "Rispondende a ",
"Reply" : "Risponde",
"Update" : "Agiorna",
"Description" : "Descritzione",
"(Unsaved)" : "(Non sarvada)",
"(Saving…)" : "(Sarbende…)",
"Formatting help" : "Ghia pro sa formatatzione",
"Edit description" : "Modìfica descritzione",
"View description" : "Visualiza descritzione",
"Add Attachment" : "Agiunghe alligongiadu",
"Write a description …" : "Iscrie una descritzione ...",
"Choose attachment" : "Sèbera un'alligongiadu",
"(group)" : "(grupu)",
"(circle)" : "(tropa)",
"Assign to me" : "Assigna a mie",
"Unassign myself" : "Annulla s'assignatzione a mie",
"Move card" : "Tràmuda ischeda",
"Unarchive card" : "Ischeda no archiviada",
"Archive card" : "Archìviu no archiviadu",
"Delete card" : "Cantzella ischeda",
"Move card to another board" : "Tràmuda s'ischeda a un'àtera lavagna",
"Card deleted" : "Ischeda cantzellada",
"seconds ago" : "segundos a immoe",
"All boards" : "Totu is lavagnas",
"Archived boards" : "Lavagnas archiviadas",
"Shared with you" : "Cumpartzidu cun tegus",
"Use bigger card view" : "Imprea bista cun ischedas prus mannas",
"Show boards in calendar/tasks" : "Mustra lavagnas in calendàriu/fainas",
"Limit deck usage of groups" : "Mìnima s'impreu de deck de is grupos",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Su de minimare su deck at a impedire a is utentes chi non faghent parte de cuddos grupos de si creare lavagnas pròpias. Is utentes ant a èssere ancora capassos de traballare in is lavagnas chi fiant istadas cumpartzidas cun issos etotu,",
"Board details" : "Detàllios lavagna",
"Edit board" : "Modìfica lavagna",
"Clone board" : "Clona lavagna",
"Unarchive board" : "Annulla s'archiviatzione de sa lavagna",
"Archive board" : "Archìvia lavagna",
"Turn on due date reminders" : "Allughe is notìficas pro ammentare is iscadèntzias",
"Turn off due date reminders" : "Istuda is notìficas pro ammentare is iscadèntzias",
"Due date reminders" : "Notìficas pro ammentare is iscadèntzias",
"All cards" : "Totu is ischedas",
"Assigned cards" : "Ischedas assignadas",
"No notifications" : "Peruna notìfica",
"Delete board" : "Cantzella lavagna",
"Board {0} deleted" : "Lavagna {0} cantzellada",
"Only assigned cards" : "Isceti ischedas assignadas",
"No reminder" : "Perunu apuntu",
"An error occurred" : "Ddoe at àpidu un'errore",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Ses seguru chi cheres cantzellare sa lavagna {title}? Custa operatzione at a cantzellare totu is datos de custa lavagna.",
"Delete the board?" : "Cheres cantzellare sa lavagna?",
"Loading filtered view" : "Carrigamentu de sa bista cun su filtru",
"Today" : "Oe",
"Tomorrow" : "Cras",
"This week" : "Custa chida",
"No due" : "Peruna iscadèntzia",
"No upcoming cards" : "Peruna ischeda abarrada",
"upcoming cards" : "ischedas abarradas",
"Link to a board" : "Collega a una tabella",
"Link to a card" : "Collega a un'ischeda",
"Create a card" : "Crea un'ischeda",
"Message from {author} in {conversationName}" : "Messàgiu de {author} in {conversationName}",
"Something went wrong" : "Ddoe at àpidu un'errore",
"Failed to upload {name}" : "No at fatu a agiornare {name}",
"Maximum file size of {size} exceeded" : "Mannària màssima de s'archìviu de {size} superada",
"Error creating the share" : "Errore in sa creatzione de sa cumpatzidura",
"Share with a Deck card" : "Cumpartzi cun un'ischeda deck",
"Share {file} with a Deck card" : "Cumpartzi {file} cun un'ischeda de deck",
"Share" : "Cumpartzi"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

View File

@@ -107,9 +107,16 @@ OC.L10N.register(
"Select the board to link to a project" : "Izberite zbirko za povezavo s projektom",
"Search by board title" : "Išči po imenu zbirke",
"Select board" : "Izbor zbirke",
"Create a new card" : "Ustvari novo nalogo",
"Select a board" : "Izbor zbirke",
"Select a list" : "Izbor seznama",
"Card title" : "Naslov naloge",
"Cancel" : "Prekliči",
"Creating the new card…" : "Poteka ustvarjanje nove naloge ...",
"\"{card}\" was added to \"{board}\"" : "Naloga »{card}« je dodana v zbirko »{board}«.",
"Open card" : "Odpri nalogo",
"Close" : "Zapri",
"Create card" : "Ustvari nalogo",
"Select a card" : "Izbor naloge",
"Select the card to link to a project" : "Izbor naloge za povezavo do projekta",
"Link to card" : "Poveži nalogo",
@@ -258,6 +265,7 @@ OC.L10N.register(
"upcoming cards" : "prihajajoče naloge",
"Link to a board" : "Povezava do zbirke",
"Link to a card" : "Povezava do naloge",
"Create a card" : "Ustvari nalogo",
"Something went wrong" : "Prišlo je do napake ...",
"Failed to upload {name}" : "Pošiljanje {name} je spodletelo",
"Maximum file size of {size} exceeded" : "Omejitev velikosti datoteke {size} je prekoračena.",

View File

@@ -105,9 +105,16 @@
"Select the board to link to a project" : "Izberite zbirko za povezavo s projektom",
"Search by board title" : "Išči po imenu zbirke",
"Select board" : "Izbor zbirke",
"Create a new card" : "Ustvari novo nalogo",
"Select a board" : "Izbor zbirke",
"Select a list" : "Izbor seznama",
"Card title" : "Naslov naloge",
"Cancel" : "Prekliči",
"Creating the new card…" : "Poteka ustvarjanje nove naloge ...",
"\"{card}\" was added to \"{board}\"" : "Naloga »{card}« je dodana v zbirko »{board}«.",
"Open card" : "Odpri nalogo",
"Close" : "Zapri",
"Create card" : "Ustvari nalogo",
"Select a card" : "Izbor naloge",
"Select the card to link to a project" : "Izbor naloge za povezavo do projekta",
"Link to card" : "Poveži nalogo",
@@ -256,6 +263,7 @@
"upcoming cards" : "prihajajoče naloge",
"Link to a board" : "Povezava do zbirke",
"Link to a card" : "Povezava do naloge",
"Create a card" : "Ustvari nalogo",
"Something went wrong" : "Prišlo je do napake ...",
"Failed to upload {name}" : "Pošiljanje {name} je spodletelo",
"Maximum file size of {size} exceeded" : "Omejitev velikosti datoteke {size} je prekoračena.",

View File

@@ -61,29 +61,29 @@ OC.L10N.register(
"{user} has deleted the attachment {attachment} from card {card}" : "{user} 已從卡 {card} 中刪除附件 {attachment}",
"You have restored the attachment {attachment} to card {card}" : "您恢復了卡片 {card} 中的附件 {attachment}",
"{user} has restored the attachment {attachment} to card {card}" : "{user} 恢復了卡片 {card} 中的附件 {attachment}",
"You have commented on card {card}" : "您評論了卡片 {card}",
"{user} has commented on card {card}" : "{user} 評論了卡片 {card}",
"You have commented on card {card}" : "您意見了卡片 {card}",
"{user} has commented on card {card}" : "{user} 意見了卡片 {card}",
"A <strong>card description</strong> inside the Deck app has been changed" : "Deck 應用程式中的 <strong>卡片描述</strong> 已改變",
"Deck" : "Deck",
"Changes in the <strong>Deck app</strong>" : "<strong>Deck 應用程式</strong>中的改變",
"A <strong>comment</strong> was created on a card" : "卡片上創建了一個 <strong>評論</strong>",
"Upcoming cards" : "將到期的卡片",
"A <strong>comment</strong> was created on a card" : "卡片上創建了一個 <strong>意見</strong>",
"Upcoming cards" : "將到期的卡片",
"Personal" : "個人",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "%s 已將 “%s” 中的卡片 “%s” 指派給您。",
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} 已將 “%s” 中的卡片 “%s” 指派給您。",
"The card \"%s\" on \"%s\" has reached its due date." : "\"%s\"中的卡片 \"%s\" 已到期。",
"%s has mentioned you in a comment on \"%s\"." : "%s 在 “%s” 的評論中提到了您。",
"{user} has mentioned you in a comment on \"%s\"." : "{user} 在 “%s” 的評論中提到了您。",
"%s has mentioned you in a comment on \"%s\"." : "%s 在 “%s” 的意見中提到了您。",
"{user} has mentioned you in a comment on \"%s\"." : "{user} 在 “%s” 的意見中提到了您。",
"The board \"%s\" has been shared with you by %s." : "面板 \"%s\" 已由 %s 分享給您。",
"{user} has shared the board %s with you." : "{user} 分享面板 %s 給您。",
"No data was provided to create an attachment." : "未能提供數據以創建附件",
"Finished" : "完成",
"To review" : "回顧",
"Action needed" : "需要操作",
"Later" : "稍後",
"To review" : "待審閱",
"Action needed" : "需要採取行動",
"Later" : "稍後處理",
"copy" : "複製",
"To do" : "待辦",
"Doing" : "行中",
"Doing" : "行中",
"Done" : "完成",
"Example Task 3" : "示例任務 3",
"Example Task 2" : "示例任務 2",
@@ -95,16 +95,16 @@ OC.L10N.register(
"No file was uploaded" : "沒有檔案被上傳",
"Missing a temporary folder" : "找不到暫存資料夾",
"Could not write file to disk" : "寫入硬碟失敗",
"A PHP extension stopped the file upload" : "個 PHP 擴充功能終止檔案的上傳",
"A PHP extension stopped the file upload" : "個 PHP 擴充功能終止檔案的上傳",
"No file uploaded or file size exceeds maximum of %s" : "沒有上傳檔案或檔案大小超過 %s 的最大值",
"Card not found" : "未找到卡片",
"Path is already shared with this card" : "已和這張卡片分享了路徑",
"Invalid date, date format must be YYYY-MM-DD" : "無效的日期,需為 YYYY-MM-DD 格式",
"Personal planning and team project organization" : "個人規劃和團隊項目組織",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck是一種看板式組織工具旨在針對與Nextcloud集成的團隊進行個人計劃和項目組織。\n\n\n- 📥 增加您的任務到card和把它們整理好\n- 📄 寫下額外的筆記在markdown\n- 🔖 分配標籤以更好地組織您的工作\n- 👥 與您的團隊,朋友或家人分享\n- 📎 附加檔案並將其嵌入到您的 markdown 描述\n- 💬 使用評論與您的團隊討論\n- ⚡ 在活動流中跟踪更改\n- 🚀 讓您的項目井井有條",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck是一種看板式組織工具旨在針對與Nextcloud集成的團隊進行個人計劃和項目組織。\n\n\n- 📥 增加您的任務到card和把它們整理好\n- 📄 寫下額外的筆記在markdown\n- 🔖 分配標籤以更好地組織您的工作\n- 👥 與您的團隊,朋友或家人分享\n- 📎 附加檔案並將其嵌入到您的 markdown 描述\n- 💬 使用意見與您的團隊討論\n- ⚡ 在活動流中跟踪更改\n- 🚀 讓您的項目井井有條",
"Card details" : "卡片詳情",
"Add board" : "添加面板",
"Select the board to link to a project" : "選擇要鏈接到一個項目的面板",
"Select the board to link to a project" : "選擇要連結到一個項目的面板",
"Search by board title" : "通過標題搜索面板",
"Select board" : "選擇面板",
"Create a new card" : "建立新卡片",
@@ -117,24 +117,24 @@ OC.L10N.register(
"Open card" : "打開卡片",
"Close" : "關閉",
"Create card" : "建立卡片",
"Select a card" : "選擇一張卡片",
"Select the card to link to a project" : "選擇要鏈接到一個項目的卡片",
"Link to card" : "鏈接到卡片",
"File already exists" : "文件已存在",
"A file with the name {filename} already exists." : "使用文件名 {filename} 的文件已存在。",
"Select a card" : "選擇卡片",
"Select the card to link to a project" : "選擇要連結到一個項目的卡片",
"Link to card" : "連結到卡片",
"File already exists" : "檔案已存在",
"A file with the name {filename} already exists." : "名稱為 {filename} 的檔案已存在。",
"Do you want to overwrite it?" : "您確定要覆蓋嗎?",
"Overwrite file" : "覆蓋文件",
"Keep existing file" : "保持已存在的文件",
"This board is read only" : "此面板是讀的",
"Drop your files to upload" : "拖放您的文件以上傳",
"Overwrite file" : "覆蓋檔案",
"Keep existing file" : "保持已存在的檔案",
"This board is read only" : "此面板是讀的",
"Drop your files to upload" : "拖放您的檔案以上傳",
"Archived cards" : "已存檔卡片",
"Add list" : "添加列表",
"List name" : "列表名稱",
"Add list" : "添加清單",
"List name" : "清單名稱",
"Apply filter" : "應用過濾器",
"Filter by tag" : "標籤篩選",
"Filter by assigned user" : "根據指定用戶過濾",
"Unassigned" : "未分配",
"Filter by due date" : "根據到期日過濾",
"Filter by tag" : "標籤過濾",
"Filter by assigned user" : "以指派用戶過濾",
"Unassigned" : "未指派",
"Filter by due date" : "到期日過濾",
"Overdue" : "逾期",
"Next 24 hours" : "未來24小時",
"Next 7 days" : "未來7曰",
@@ -146,41 +146,41 @@ OC.L10N.register(
"Toggle compact mode" : "切換簡潔模式",
"Details" : "詳情",
"Loading board" : "正在加載面板",
"No lists available" : "無列表可用",
"Create a new list to add cards to this board" : "創建一個新列表來添加卡片到這個看板",
"No lists available" : "無清單可用",
"Create a new list to add cards to this board" : "創建一張新清單來添加卡片到這個看板",
"Board not found" : "未找到面板",
"Sharing" : "正在分享",
"Tags" : "標籤",
"Deleted items" : "已刪除項",
"Deleted items" : "已刪除項",
"Timeline" : "時間線",
"Deleted lists" : "已刪除的列表",
"Deleted lists" : "已刪除的清單",
"Undo" : "撤消",
"Deleted cards" : "已刪除卡片",
"Share board with a user, group or circle …" : "與一個用戶群組或圈子分享面板...",
"Searching for users, groups and circles …" : "正在搜用戶、群組和圈子 ......",
"Share board with a user, group or circle …" : "與一個用戶群組或圈子分享面板...",
"Searching for users, groups and circles …" : "正在搜用戶、群組和圈子 ......",
"No participants found" : "未找到參與者",
"Board owner" : "面板擁有者",
"Board owner" : "面板板主",
"(Group)" : "(群組)",
"(Circle)" : "(圈子)",
"Can edit" : "可以編輯",
"Can share" : "可以分享",
"Can manage" : "可以管理",
"Delete" : "刪除",
"Failed to create share with {displayName}" : " {displayName} 創建分享失敗",
"Add a new list" : "添加一個新列表",
"Failed to create share with {displayName}" : "無法為 {displayName} 創建分享",
"Add a new list" : "添加一張新清單",
"Archive all cards" : "封存所有卡片",
"Delete list" : "刪除列表",
"Delete list" : "刪除清單",
"Add card" : "添加卡片",
"Archive all cards in this list" : "存檔該列表的所有卡片",
"Archive all cards in this list" : "封存此清單內的所有卡片",
"Add a new card" : "添加一張新卡片",
"Card name" : "卡片名",
"List deleted" : "列表被刪除",
"Card name" : "卡片名",
"List deleted" : "清單已被刪除",
"Edit" : "編輯",
"Add a new tag" : "新增一個標籤",
"Add a new tag" : "添加新標籤",
"title and color value must be provided" : "必須提供標題和顏色值",
"Board name" : "板名",
"Members" : "員",
"Upload new files" : "上傳新文件",
"Board name" : "板名",
"Members" : "員",
"Upload new files" : "上傳新檔案",
"Share from Files" : "從檔案進行分享",
"Add this attachment" : "添加此附件",
"Show in Files" : "顯示在檔案中",
@@ -192,40 +192,40 @@ OC.L10N.register(
"Open in sidebar view" : "在側邊欄視圖中打開",
"Open in bigger view" : "在較大視圖中打開",
"Attachments" : "附件",
"Comments" : "評論",
"Modified" : "修改",
"Created" : "已創建",
"The title cannot be empty." : "標題不能為空",
"No comments yet. Begin the discussion!" : "還沒有評論。 開始討論吧!",
"Comments" : "意見",
"Modified" : "修改",
"Created" : "建立於",
"The title cannot be empty." : "標題不能為空",
"No comments yet. Begin the discussion!" : "尚無意見,開始討論吧!",
"Assign a tag to this card…" : "為該卡片分配標籤…",
"Assign to users" : "指派給用戶",
"Assign to users/groups/circles" : "分配給用戶/群組/圈子",
"Assign a user to this card…" : "為該卡片指派用戶",
"Due date" : "截至日期",
"Set a due date" : "設置一個到期日",
"Remove due date" : "移除截至日期",
"Assign to users/groups/circles" : "指派給用戶/群組/圈子",
"Assign a user to this card…" : "將此卡片指派用戶...",
"Due date" : "到期日",
"Set a due date" : "設置到期日",
"Remove due date" : "移除到期日",
"Select Date" : "選擇日期",
"Save" : "保存",
"The comment cannot be empty." : "註釋不能為空。",
"The comment cannot be longer than 1000 characters." : "註釋不能超過 1000 個字符。",
"In reply to" : "回",
"Reply" : "回",
"The comment cannot be empty." : "意見不能為空。",
"The comment cannot be longer than 1000 characters." : "意見不能超過 1000 個字符。",
"In reply to" : "回",
"Reply" : "回",
"Update" : "更新",
"Description" : "描述",
"(Unsaved)" : "(未保存的)",
"(Saving…)" : "正在保存...",
"(Saving…)" : "(保存...",
"Formatting help" : "格式化幫助",
"Edit description" : "編輯描述",
"View description" : "查看描述",
"Add Attachment" : "添加附件",
"Write a description …" : "寫一段描述",
"Choose attachment" : "選擇附件",
"(group)" : "(組)",
"(group)" : "組)",
"(circle)" : "(圈子)",
"Assign to me" : "指派給我",
"Unassign myself" : "自己解除分配",
"Unassign myself" : "自己解除指派",
"Move card" : "移動卡片",
"Unarchive card" : "恢復卡片存檔",
"Unarchive card" : "取消對卡片的封存",
"Archive card" : "封存卡片",
"Delete card" : "刪除卡片",
"Move card to another board" : "將卡片移到其他面板",
@@ -250,15 +250,15 @@ OC.L10N.register(
"Assigned cards" : "分配的卡片",
"No notifications" : "無通知",
"Delete board" : "刪除面板",
"Board {0} deleted" : "面板 {0} 被刪除",
"Only assigned cards" : "僅分配的卡片",
"Board {0} deleted" : "面板 {0} 被刪除",
"Only assigned cards" : "僅指派了的卡片",
"No reminder" : "無提醒",
"An error occurred" : "發生錯誤",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "你確定你要刪除 {title} 面板嗎?面板內所有數據都將因此被刪除。",
"Delete the board?" : "是否刪除面板?",
"Loading filtered view" : "正在加載已過濾視圖",
"Today" : "今",
"Tomorrow" : "明",
"Today" : "今",
"Tomorrow" : "明",
"This week" : "本星期",
"No due" : "沒有到期的",
"No upcoming cards" : "沒有快將到期的卡片",

View File

@@ -59,29 +59,29 @@
"{user} has deleted the attachment {attachment} from card {card}" : "{user} 已從卡 {card} 中刪除附件 {attachment}",
"You have restored the attachment {attachment} to card {card}" : "您恢復了卡片 {card} 中的附件 {attachment}",
"{user} has restored the attachment {attachment} to card {card}" : "{user} 恢復了卡片 {card} 中的附件 {attachment}",
"You have commented on card {card}" : "您評論了卡片 {card}",
"{user} has commented on card {card}" : "{user} 評論了卡片 {card}",
"You have commented on card {card}" : "您意見了卡片 {card}",
"{user} has commented on card {card}" : "{user} 意見了卡片 {card}",
"A <strong>card description</strong> inside the Deck app has been changed" : "Deck 應用程式中的 <strong>卡片描述</strong> 已改變",
"Deck" : "Deck",
"Changes in the <strong>Deck app</strong>" : "<strong>Deck 應用程式</strong>中的改變",
"A <strong>comment</strong> was created on a card" : "卡片上創建了一個 <strong>評論</strong>",
"Upcoming cards" : "將到期的卡片",
"A <strong>comment</strong> was created on a card" : "卡片上創建了一個 <strong>意見</strong>",
"Upcoming cards" : "將到期的卡片",
"Personal" : "個人",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "%s 已將 “%s” 中的卡片 “%s” 指派給您。",
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} 已將 “%s” 中的卡片 “%s” 指派給您。",
"The card \"%s\" on \"%s\" has reached its due date." : "\"%s\"中的卡片 \"%s\" 已到期。",
"%s has mentioned you in a comment on \"%s\"." : "%s 在 “%s” 的評論中提到了您。",
"{user} has mentioned you in a comment on \"%s\"." : "{user} 在 “%s” 的評論中提到了您。",
"%s has mentioned you in a comment on \"%s\"." : "%s 在 “%s” 的意見中提到了您。",
"{user} has mentioned you in a comment on \"%s\"." : "{user} 在 “%s” 的意見中提到了您。",
"The board \"%s\" has been shared with you by %s." : "面板 \"%s\" 已由 %s 分享給您。",
"{user} has shared the board %s with you." : "{user} 分享面板 %s 給您。",
"No data was provided to create an attachment." : "未能提供數據以創建附件",
"Finished" : "完成",
"To review" : "回顧",
"Action needed" : "需要操作",
"Later" : "稍後",
"To review" : "待審閱",
"Action needed" : "需要採取行動",
"Later" : "稍後處理",
"copy" : "複製",
"To do" : "待辦",
"Doing" : "行中",
"Doing" : "行中",
"Done" : "完成",
"Example Task 3" : "示例任務 3",
"Example Task 2" : "示例任務 2",
@@ -93,16 +93,16 @@
"No file was uploaded" : "沒有檔案被上傳",
"Missing a temporary folder" : "找不到暫存資料夾",
"Could not write file to disk" : "寫入硬碟失敗",
"A PHP extension stopped the file upload" : "個 PHP 擴充功能終止檔案的上傳",
"A PHP extension stopped the file upload" : "個 PHP 擴充功能終止檔案的上傳",
"No file uploaded or file size exceeds maximum of %s" : "沒有上傳檔案或檔案大小超過 %s 的最大值",
"Card not found" : "未找到卡片",
"Path is already shared with this card" : "已和這張卡片分享了路徑",
"Invalid date, date format must be YYYY-MM-DD" : "無效的日期,需為 YYYY-MM-DD 格式",
"Personal planning and team project organization" : "個人規劃和團隊項目組織",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck是一種看板式組織工具旨在針對與Nextcloud集成的團隊進行個人計劃和項目組織。\n\n\n- 📥 增加您的任務到card和把它們整理好\n- 📄 寫下額外的筆記在markdown\n- 🔖 分配標籤以更好地組織您的工作\n- 👥 與您的團隊,朋友或家人分享\n- 📎 附加檔案並將其嵌入到您的 markdown 描述\n- 💬 使用評論與您的團隊討論\n- ⚡ 在活動流中跟踪更改\n- 🚀 讓您的項目井井有條",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck是一種看板式組織工具旨在針對與Nextcloud集成的團隊進行個人計劃和項目組織。\n\n\n- 📥 增加您的任務到card和把它們整理好\n- 📄 寫下額外的筆記在markdown\n- 🔖 分配標籤以更好地組織您的工作\n- 👥 與您的團隊,朋友或家人分享\n- 📎 附加檔案並將其嵌入到您的 markdown 描述\n- 💬 使用意見與您的團隊討論\n- ⚡ 在活動流中跟踪更改\n- 🚀 讓您的項目井井有條",
"Card details" : "卡片詳情",
"Add board" : "添加面板",
"Select the board to link to a project" : "選擇要鏈接到一個項目的面板",
"Select the board to link to a project" : "選擇要連結到一個項目的面板",
"Search by board title" : "通過標題搜索面板",
"Select board" : "選擇面板",
"Create a new card" : "建立新卡片",
@@ -115,24 +115,24 @@
"Open card" : "打開卡片",
"Close" : "關閉",
"Create card" : "建立卡片",
"Select a card" : "選擇一張卡片",
"Select the card to link to a project" : "選擇要鏈接到一個項目的卡片",
"Link to card" : "鏈接到卡片",
"File already exists" : "文件已存在",
"A file with the name {filename} already exists." : "使用文件名 {filename} 的文件已存在。",
"Select a card" : "選擇卡片",
"Select the card to link to a project" : "選擇要連結到一個項目的卡片",
"Link to card" : "連結到卡片",
"File already exists" : "檔案已存在",
"A file with the name {filename} already exists." : "名稱為 {filename} 的檔案已存在。",
"Do you want to overwrite it?" : "您確定要覆蓋嗎?",
"Overwrite file" : "覆蓋文件",
"Keep existing file" : "保持已存在的文件",
"This board is read only" : "此面板是讀的",
"Drop your files to upload" : "拖放您的文件以上傳",
"Overwrite file" : "覆蓋檔案",
"Keep existing file" : "保持已存在的檔案",
"This board is read only" : "此面板是讀的",
"Drop your files to upload" : "拖放您的檔案以上傳",
"Archived cards" : "已存檔卡片",
"Add list" : "添加列表",
"List name" : "列表名稱",
"Add list" : "添加清單",
"List name" : "清單名稱",
"Apply filter" : "應用過濾器",
"Filter by tag" : "標籤篩選",
"Filter by assigned user" : "根據指定用戶過濾",
"Unassigned" : "未分配",
"Filter by due date" : "根據到期日過濾",
"Filter by tag" : "標籤過濾",
"Filter by assigned user" : "以指派用戶過濾",
"Unassigned" : "未指派",
"Filter by due date" : "到期日過濾",
"Overdue" : "逾期",
"Next 24 hours" : "未來24小時",
"Next 7 days" : "未來7曰",
@@ -144,41 +144,41 @@
"Toggle compact mode" : "切換簡潔模式",
"Details" : "詳情",
"Loading board" : "正在加載面板",
"No lists available" : "無列表可用",
"Create a new list to add cards to this board" : "創建一個新列表來添加卡片到這個看板",
"No lists available" : "無清單可用",
"Create a new list to add cards to this board" : "創建一張新清單來添加卡片到這個看板",
"Board not found" : "未找到面板",
"Sharing" : "正在分享",
"Tags" : "標籤",
"Deleted items" : "已刪除項",
"Deleted items" : "已刪除項",
"Timeline" : "時間線",
"Deleted lists" : "已刪除的列表",
"Deleted lists" : "已刪除的清單",
"Undo" : "撤消",
"Deleted cards" : "已刪除卡片",
"Share board with a user, group or circle …" : "與一個用戶群組或圈子分享面板...",
"Searching for users, groups and circles …" : "正在搜用戶、群組和圈子 ......",
"Share board with a user, group or circle …" : "與一個用戶群組或圈子分享面板...",
"Searching for users, groups and circles …" : "正在搜用戶、群組和圈子 ......",
"No participants found" : "未找到參與者",
"Board owner" : "面板擁有者",
"Board owner" : "面板板主",
"(Group)" : "(群組)",
"(Circle)" : "(圈子)",
"Can edit" : "可以編輯",
"Can share" : "可以分享",
"Can manage" : "可以管理",
"Delete" : "刪除",
"Failed to create share with {displayName}" : " {displayName} 創建分享失敗",
"Add a new list" : "添加一個新列表",
"Failed to create share with {displayName}" : "無法為 {displayName} 創建分享",
"Add a new list" : "添加一張新清單",
"Archive all cards" : "封存所有卡片",
"Delete list" : "刪除列表",
"Delete list" : "刪除清單",
"Add card" : "添加卡片",
"Archive all cards in this list" : "存檔該列表的所有卡片",
"Archive all cards in this list" : "封存此清單內的所有卡片",
"Add a new card" : "添加一張新卡片",
"Card name" : "卡片名",
"List deleted" : "列表被刪除",
"Card name" : "卡片名",
"List deleted" : "清單已被刪除",
"Edit" : "編輯",
"Add a new tag" : "新增一個標籤",
"Add a new tag" : "添加新標籤",
"title and color value must be provided" : "必須提供標題和顏色值",
"Board name" : "板名",
"Members" : "員",
"Upload new files" : "上傳新文件",
"Board name" : "板名",
"Members" : "員",
"Upload new files" : "上傳新檔案",
"Share from Files" : "從檔案進行分享",
"Add this attachment" : "添加此附件",
"Show in Files" : "顯示在檔案中",
@@ -190,40 +190,40 @@
"Open in sidebar view" : "在側邊欄視圖中打開",
"Open in bigger view" : "在較大視圖中打開",
"Attachments" : "附件",
"Comments" : "評論",
"Modified" : "修改",
"Created" : "已創建",
"The title cannot be empty." : "標題不能為空",
"No comments yet. Begin the discussion!" : "還沒有評論。 開始討論吧!",
"Comments" : "意見",
"Modified" : "修改",
"Created" : "建立於",
"The title cannot be empty." : "標題不能為空",
"No comments yet. Begin the discussion!" : "尚無意見,開始討論吧!",
"Assign a tag to this card…" : "為該卡片分配標籤…",
"Assign to users" : "指派給用戶",
"Assign to users/groups/circles" : "分配給用戶/群組/圈子",
"Assign a user to this card…" : "為該卡片指派用戶",
"Due date" : "截至日期",
"Set a due date" : "設置一個到期日",
"Remove due date" : "移除截至日期",
"Assign to users/groups/circles" : "指派給用戶/群組/圈子",
"Assign a user to this card…" : "將此卡片指派用戶...",
"Due date" : "到期日",
"Set a due date" : "設置到期日",
"Remove due date" : "移除到期日",
"Select Date" : "選擇日期",
"Save" : "保存",
"The comment cannot be empty." : "註釋不能為空。",
"The comment cannot be longer than 1000 characters." : "註釋不能超過 1000 個字符。",
"In reply to" : "回",
"Reply" : "回",
"The comment cannot be empty." : "意見不能為空。",
"The comment cannot be longer than 1000 characters." : "意見不能超過 1000 個字符。",
"In reply to" : "回",
"Reply" : "回",
"Update" : "更新",
"Description" : "描述",
"(Unsaved)" : "(未保存的)",
"(Saving…)" : "正在保存...",
"(Saving…)" : "(保存...",
"Formatting help" : "格式化幫助",
"Edit description" : "編輯描述",
"View description" : "查看描述",
"Add Attachment" : "添加附件",
"Write a description …" : "寫一段描述",
"Choose attachment" : "選擇附件",
"(group)" : "(組)",
"(group)" : "組)",
"(circle)" : "(圈子)",
"Assign to me" : "指派給我",
"Unassign myself" : "自己解除分配",
"Unassign myself" : "自己解除指派",
"Move card" : "移動卡片",
"Unarchive card" : "恢復卡片存檔",
"Unarchive card" : "取消對卡片的封存",
"Archive card" : "封存卡片",
"Delete card" : "刪除卡片",
"Move card to another board" : "將卡片移到其他面板",
@@ -248,15 +248,15 @@
"Assigned cards" : "分配的卡片",
"No notifications" : "無通知",
"Delete board" : "刪除面板",
"Board {0} deleted" : "面板 {0} 被刪除",
"Only assigned cards" : "僅分配的卡片",
"Board {0} deleted" : "面板 {0} 被刪除",
"Only assigned cards" : "僅指派了的卡片",
"No reminder" : "無提醒",
"An error occurred" : "發生錯誤",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "你確定你要刪除 {title} 面板嗎?面板內所有數據都將因此被刪除。",
"Delete the board?" : "是否刪除面板?",
"Loading filtered view" : "正在加載已過濾視圖",
"Today" : "今",
"Tomorrow" : "明",
"Today" : "今",
"Tomorrow" : "明",
"This week" : "本星期",
"No due" : "沒有到期的",
"No upcoming cards" : "沒有快將到期的卡片",

View File

@@ -2,69 +2,277 @@ OC.L10N.register(
"deck",
{
"You have created a new board {board}" : "您已建立新的佈告欄 {board}",
"{user} has created a new board {board}" : "{user} 已建立新的佈告欄  {board}",
"{user} has created a new board {board}" : "{user} 已建立新的佈告欄 {board}",
"You have deleted the board {board}" : "您已刪除佈告欄 {board}",
"{user} has deleted the board {board}" : "{user} 已刪除佈告欄{board}",
"You have restored the board {board}" : "您已還原佈告欄{board}",
"{user} has restored the board {board}" : "{user}已還原佈告欄{board}",
"You have shared the board {board} with {acl}" : "您已和{acl}分享佈告欄{board}",
"{user} has shared the board {board} with {acl}" : "{user} 已和{acl}分享佈告欄{board}",
"Personal" : "私人的",
"{user} has deleted the board {board}" : "{user} 已刪除佈告欄 {board}",
"You have restored the board {board}" : "您已還原佈告欄 {board}",
"{user} has restored the board {board}" : "{user} 已還原佈告欄 {board}",
"You have shared the board {board} with {acl}" : "您已和 {acl} 分享佈告欄 {board}",
"{user} has shared the board {board} with {acl}" : "{user} 已和 {acl} 分享佈告欄 {board}",
"You have removed {acl} from the board {board}" : "您已從佈告欄 {board} 移除 {acl}",
"{user} has removed {acl} from the board {board}" : "{user} 已從佈告欄 {board} 移除 {acl}",
"You have renamed the board {before} to {board}" : "您已將佈告欄 {before} 重新命名為 {board}",
"{user} has renamed the board {before} to {board}" : "{user} 已將佈告欄 {before} 重新命名為 {board}",
"You have archived the board {board}" : "您已封存佈告欄 {board}",
"{user} has archived the board {before}" : "{user} 已封存佈告欄 {before}",
"You have unarchived the board {board}" : "您已解除封存佈告欄 {board}",
"{user} has unarchived the board {before}" : "{user} 已解除封存佈告欄 {before}",
"You have created a new list {stack} on board {board}" : "您已在佈告欄 {board} 上建立新列表 {stack}",
"{user} has created a new list {stack} on board {board}" : "{user} 已在佈告欄 {board} 上建立新列表 {stack}",
"You have renamed list {before} to {stack} on board {board}" : "您已將佈告欄 {board} 上的列表 {before} 重新命名為 {stack}",
"{user} has renamed list {before} to {stack} on board {board}" : "{user} 已將佈告欄 {board} 上的列表 {before} 重新命名為 {stack}",
"You have deleted list {stack} on board {board}" : "您已刪除佈告欄 {board} 上的列表 {stack}",
"{user} has deleted list {stack} on board {board}" : "{user} 已刪除佈告欄 {board} 上的列表 {stack}",
"You have created card {card} in list {stack} on board {board}" : "您已在佈告欄 {board} 上的列表 {stack} 建立卡片 {card}",
"{user} has created card {card} in list {stack} on board {board}" : "{user} 已在佈告欄 {board} 上的列表 {stack} 建立卡片 {card}",
"You have deleted card {card} in list {stack} on board {board}" : "您已在佈告欄 {board} 上的列表 {stack} 刪除卡片 {card}",
"{user} has deleted card {card} in list {stack} on board {board}" : "{user} 已在佈告欄 {board} 上的列表 {stack} 刪除卡片 {card}",
"You have renamed the card {before} to {card}" : "您已將卡片 {before} 重新命名為 {card}",
"{user} has renamed the card {before} to {card}" : "{user} 已將卡片 {before} 重新命名為 {card}",
"You have added a description to card {card} in list {stack} on board {board}" : "您已將描述新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"{user} has added a description to card {card} in list {stack} on board {board}" : "{user} 已將描述新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"You have updated the description of card {card} in list {stack} on board {board}" : "您已更新佈告欄 {board} 上的列表 {stack} 的卡片 {card} 的描述",
"{user} has updated the description of the card {card} in list {stack} on board {board}" : "{user} 已更新佈告欄 {board} 上的列表 {stack} 的卡片 {card} 的描述",
"You have archived card {card} in list {stack} on board {board}" : "您已封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"{user} has archived card {card} in list {stack} on board {board}" : "{user} 已封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"You have unarchived card {card} in list {stack} on board {board}" : "您已解除封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"{user} has unarchived card {card} in list {stack} on board {board}" : "{user} 已解除封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"You have removed the due date of card {card}" : "您已移除卡片 {card} 的到期日",
"{user} has removed the due date of card {card}" : "{user} 已移除卡片 {card} 的到期日",
"You have set the due date of card {card} to {after}" : "您已設定卡片 {card} 的到期日",
"{user} has set the due date of card {card} to {after}" : "{user} 已設定卡片 {card} 的到期日",
"You have updated the due date of card {card} to {after}" : "您已將卡片 {card} 的到期日更新為 {after}",
"{user} has updated the due date of card {card} to {after}" : "{user} 已將卡片 {card} 的到期日更新為 {after}",
"You have added the tag {label} to card {card} in list {stack} on board {board}" : "您已將標籤 {label} 新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"{user} has added the tag {label} to card {card} in list {stack} on board {board}" : "{user} 已將標籤 {label} 新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"You have removed the tag {label} from card {card} in list {stack} on board {board}" : "您已將標籤 {label} 從佈告欄 {board} 上列表 {stack} 中的卡片 {card} 移除",
"{user} has removed the tag {label} from card {card} in list {stack} on board {board}" : "{user} 已將標籤 {label} 從佈告欄 {board} 上列表 {stack} 中的卡片 {card} 移除",
"You have assigned {assigneduser} to card {card} on board {board}" : "您已將佈告欄 {board} 上的卡片 {card} 分配給 {assigneduser}",
"{user} has assigned {assigneduser} to card {card} on board {board}" : "{user} 已將佈告欄 {board} 上的卡片 {card} 分配給 {assigneduser}",
"You have unassigned {assigneduser} from card {card} on board {board}" : "您已取消分配佈告欄 {board} 上的卡片 {card} 給 {assigneduser}",
"{user} has unassigned {assigneduser} from card {card} on board {board}" : "{user} 已取消分配佈告欄 {board} 上的卡片 {card} 給 {assigneduser}",
"You have moved the card {card} from list {stackBefore} to {stack}" : "您已將卡片 {card} 從列表 {stackBefore} 移動到 {stack}",
"{user} has moved the card {card} from list {stackBefore} to {stack}" : "{user} 已將卡片 {card} 從列表 {stackBefore} 移動到 {stack}",
"You have added the attachment {attachment} to card {card}" : "您已將附件 {attachment} 新增到 {card}",
"{user} has added the attachment {attachment} to card {card}" : "{user} 已將附件 {attachment} 新增到 {card}",
"You have updated the attachment {attachment} on card {card}" : "您已更新卡片 {card} 上的附件 {attachment}",
"{user} has updated the attachment {attachment} on card {card}" : "{user} 已更新卡片 {card} 上的附件 {attachment}",
"You have deleted the attachment {attachment} from card {card}" : "您已從卡片 {card} 刪除附件 {attachment}",
"{user} has deleted the attachment {attachment} from card {card}" : "{user} 已從卡片 {card} 刪除附件 {attachment}",
"You have restored the attachment {attachment} to card {card}" : "您已從卡片 {card} 還原附件 {attachment}",
"{user} has restored the attachment {attachment} to card {card}" : "{user} 已從卡片 {card} 還原附件 {attachment}",
"You have commented on card {card}" : "您已在卡片 {card} 上留言",
"{user} has commented on card {card}" : "{user} 已在卡片 {card} 上留言",
"A <strong>card description</strong> inside the Deck app has been changed" : "Deck 應用程式中的<strong>卡片描述</strong>已變更",
"Deck" : "Deck",
"Changes in the <strong>Deck app</strong>" : "<strong>Deck 應用程式</strong>中的變更",
"A <strong>comment</strong> was created on a card" : "已在卡片上建立了<strong>留言</strong>",
"Upcoming cards" : "接下來的卡片",
"Personal" : "個人",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "卡片「%s」位於「%s」已由 %s 分配給您。",
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} 已分配卡片「%s」位於「%s」給您。",
"The card \"%s\" on \"%s\" has reached its due date." : "卡片「%s」位於「%s」已達到期日。",
"%s has mentioned you in a comment on \"%s\"." : "%s 在「%s」的留言中提到了您。",
"{user} has mentioned you in a comment on \"%s\"." : "{user} 在「%s」的留言中提到了您。",
"The board \"%s\" has been shared with you by %s." : "佈告欄「%s」已由 %s 分享給您。",
"{user} has shared the board %s with you." : "{user} 已與您分享佈告欄 %s。",
"No data was provided to create an attachment." : "沒有提供用於建立附件的資料。",
"Finished" : "已完成",
"To review" : "待檢閱",
"Action needed" : "需要採取行動",
"Later" : "稍後",
"copy" : "複製",
"To do" : "待辦事項",
"Doing" : "正在進行",
"Done" : "完成",
"Example Task 3" : "範例工作 3",
"Example Task 2" : "範例工作 2",
"Example Task 1" : "範例工作 1",
"The file was uploaded" : "檔案已上傳",
"The uploaded file exceeds the upload_max_filesize directive in php.ini" : "上傳的檔案大小超過 php.ini 當中 upload_max_filesize 選項的限制",
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "上傳的檔案大小超過 HTML 表單中 MAX_FILE_SIZE 的限制",
"The file was only partially uploaded" : "檔案僅部份上傳",
"No file was uploaded" : "沒有檔案被上傳",
"Missing a temporary folder" : "找不到暫存資料夾",
"Could not write file to disk" : "寫入硬碟失敗",
"Could not write file to disk" : "無法寫入硬碟",
"A PHP extension stopped the file upload" : "一個 PHP 擴充功能終止檔案的上傳",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck是一種看板式組織工具旨在針對與Nextcloud集成的團隊進行個人計劃和項目組織。\n\n\n- 📥 增加您的任務到card和把它們整理好\n- 📄 寫下額外的筆記在markdown\n- 🔖 分配標籤以更好地組織\n- 👥 與您的團隊,朋友或家人分享\n- 📎 附加檔案並將其嵌入到您的 markdown 描述\n- 💬 使用評論與您的團隊討論\n- ⚡ 跟踪變化在活動流程中\n- 🚀 取得您的專案組織",
"No file uploaded or file size exceeds maximum of %s" : "沒有上傳檔案或檔案超過上限 %s",
"Card not found" : "找不到卡片",
"Path is already shared with this card" : "路徑已與此卡片分享",
"Invalid date, date format must be YYYY-MM-DD" : "無效的日期,日期格式必須為 YYYY-MM-DD",
"Personal planning and team project organization" : "個人規劃與團隊專案組織",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck 是一套看板式組織工具,提供與 Nextcloud 整合的個人規劃與團隊專案組織功能。\n\n\n- 📥 將您的工作項目新增到卡片中,並將它們按順序排列\n- 📄 以 Markdown 編寫額外的註釋\n- 🔖 分配標籤讓組織更方便\n- 👥 與您的團隊、朋友與家人分享\n- 📎 附上檔案並將其嵌入到您的 Markdown 描述中\n- 💬 使用留言與您的團隊討論\n- ⚡ 追蹤活動流程中的變動\n- 🚀 整理好您的專案",
"Card details" : "卡片詳細資訊",
"Add board" : "新增佈告欄",
"Select the board to link to a project" : "選取要連結到專案的佈告欄",
"Search by board title" : "按佈告欄標題搜尋",
"Select board" : "選取佈告欄",
"Create a new card" : "建立新卡片",
"Select a board" : "選取佈告欄",
"Select a list" : "選取列表",
"Card title" : "卡片標題",
"Cancel" : "取消",
"Creating the new card…" : "正在建立新卡片……",
"\"{card}\" was added to \"{board}\"" : "「{card}」已新增至「{board}」",
"Open card" : "開啟卡片",
"Close" : "關閉",
"Create card" : "建立卡片",
"Select a card" : "選取卡片",
"Select the card to link to a project" : "選取要連結到專案的卡片",
"Link to card" : "連結到卡片",
"File already exists" : "檔案已存在",
"Add list" : "新增清單",
"Next 7 days" : "接下來 7 天",
"Next 30 days" : "接下來 30 天",
"A file with the name {filename} already exists." : "名稱為 {filename} 的檔案已存在。",
"Do you want to overwrite it?" : "您想要覆寫它嗎?",
"Overwrite file" : "覆寫檔案",
"Keep existing file" : "保留既有檔案",
"This board is read only" : "此佈告欄唯讀",
"Drop your files to upload" : "拖曳您的檔案以上傳",
"Archived cards" : "已封存的卡片",
"Add list" : "新增列表",
"List name" : "列表名稱",
"Apply filter" : "套用過濾條件",
"Filter by tag" : "按標籤過濾",
"Filter by assigned user" : "按被分配的使用者過濾",
"Unassigned" : "未分配",
"Filter by due date" : "按到期日過濾",
"Overdue" : "超過到期日",
"Next 24 hours" : "接下來24小時",
"Next 7 days" : "接下來7天",
"Next 30 days" : "接下來30天",
"No due date" : "無到期日",
"Clear filter" : "清除過濾條件",
"Hide archived cards" : "隱藏已封存的卡片",
"Show archived cards" : "顯示已封存的卡片",
"Toggle compact mode" : "切換簡潔模式",
"Details" : "詳細資料",
"Loading board" : "正在載入佈告欄",
"No lists available" : "沒有可用的列表",
"Create a new list to add cards to this board" : "建立新列表以新增卡片到此佈告欄",
"Board not found" : "找不到佈告欄",
"Sharing" : "分享",
"Tags" : "標籤",
"Deleted items" : "刪除的項目",
"Timeline" : "時間軸",
"Deleted lists" : "已刪除的列表",
"Undo" : "復原",
"Deleted cards" : "已刪除的卡片",
"Share board with a user, group or circle …" : "與使用者、群組或小圈圈分享佈告欄……",
"Searching for users, groups and circles …" : "搜尋使用者、群組與小圈圈……",
"No participants found" : "找不到參與者",
"Board owner" : "佈告欄擁有者",
"(Group)" : "(群組)",
"(Circle)" : "(小圈圈)",
"Can edit" : "可以編輯",
"Can share" : "可以分享",
"Can manage" : "可以管理",
"Delete" : "刪除",
"Delete list" : "刪除清單",
"Add card" : "增加卡片",
"Failed to create share with {displayName}" : "無法建立與 {displayName} 的分享",
"Add a new list" : "新增列表",
"Archive all cards" : "封存所有卡片",
"Delete list" : "刪除列表",
"Add card" : "新增卡片",
"Archive all cards in this list" : "封存此列表中的所有卡片",
"Add a new card" : "新增卡片",
"Card name" : "卡片名稱",
"List deleted" : "列表已刪除",
"Edit" : "編輯",
"Add a new tag" : "新增標籤",
"title and color value must be provided" : "必須提供標題與顏色的值",
"Board name" : "佈告欄名稱",
"Members" : "成員",
"Upload new files" : "上傳新檔案",
"Share from Files" : "從「檔案」分享",
"Add this attachment" : "新增此附件",
"Show in Files" : "在「檔案」中顯示",
"Unshare file" : "取消分享檔案",
"Delete Attachment" : "刪除附件",
"Restore Attachment" : "還原附件",
"File to share" : "要分享的檔案",
"Invalid path selected" : "選取的路徑無效",
"Open in sidebar view" : "在側邊欄中開啟",
"Open in bigger view" : "以較大的檢視模式開啟",
"Attachments" : "附件",
"Comments" : "意見",
"Comments" : "留言",
"Modified" : "已修改",
"Created" : "已新增",
"Assign to users" : "分派給使用者",
"Due date" : "截止日",
"The title cannot be empty." : "標題不能為空",
"No comments yet. Begin the discussion!" : "暫無留言。開始討論吧!",
"Assign a tag to this card…" : "分配標籤到此卡片……",
"Assign to users" : "分配給使用者",
"Assign to users/groups/circles" : "分配給使用者/群組/小圈圈",
"Assign a user to this card…" : "分配使用者到此卡片……",
"Due date" : "到期日",
"Set a due date" : "設定到期日",
"Remove due date" : "移除到期日",
"Select Date" : "選擇日期",
"Save" : "儲存",
"The comment cannot be empty." : "留言不能為空。",
"The comment cannot be longer than 1000 characters." : "留言不能多於 1000 個字元。",
"In reply to" : "回覆",
"Reply" : "回覆",
"Update" : "更新",
"Description" : "描述",
"(group)" : "(群組)",
"Assign to me" : "分派給我",
"(Unsaved)" : "(未儲存)",
"(Saving…)" : "(正在儲存……)",
"Formatting help" : "格式化說明",
"Edit description" : "編輯描述",
"View description" : "檢視描述",
"Add Attachment" : "新增附件",
"Write a description …" : "編寫描述……",
"Choose attachment" : "選擇附件",
"(group)" : "(群組)",
"(circle)" : "(小圈圈)",
"Assign to me" : "分配給我",
"Unassign myself" : "取消分配給我",
"Move card" : "移動卡片",
"Unarchive card" : "解除封存卡片",
"Archive card" : "封存卡片",
"Delete card" : "刪除作業",
"Delete card" : "刪除卡片",
"Move card to another board" : "將卡片移動到其他佈告欄",
"Card deleted" : "卡片已刪除",
"seconds ago" : "幾秒前",
"All boards" : "所有佈告欄",
"Archived boards" : "已封存的佈告欄",
"Shared with you" : "與您分享",
"Edit board" : "編輯專案",
"Use bigger card view" : "使用較大的卡片檢視",
"Show boards in calendar/tasks" : "在日曆/工作項目中顯示佈告欄",
"Limit deck usage of groups" : "限制群組的 Deck 使用",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "限制 Deck 將會阻止不屬於這些群組的使用者建立自己的佈告欄。使用者仍然可以在與他們分享的佈告欄上工作。",
"Board details" : "佈告欄詳細資訊",
"Edit board" : "編輯佈告欄",
"Clone board" : "再製佈告欄",
"Unarchive board" : "解除封存佈告欄",
"Archive board" : "封存佈告欄",
"Turn on due date reminders" : "開啟到期日提醒",
"Turn off due date reminders" : "關閉到期日提醒",
"Due date reminders" : "到期日提醒",
"All cards" : "所有看片",
"Assigned cards" : "已分配的卡片",
"No notifications" : "無通知",
"Delete board" : "刪除佈告欄",
"Board {0} deleted" : "已刪除佈告欄 {0}",
"Only assigned cards" : "僅已分配的卡片",
"No reminder" : "無提醒",
"An error occurred" : "發生錯誤",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "您確定要刪除佈告欄 {title} 嗎?這將會刪除所有此佈告欄的資料。",
"Delete the board?" : "刪除佈告欄?",
"Loading filtered view" : "正在載入過濾的檢視",
"Today" : "今天",
"Tomorrow" : "明天",
"This week" : "本週",
"No due" : "無到期日",
"No upcoming cards" : "無接下來的卡片",
"upcoming cards" : "接下來的卡片",
"Link to a board" : "連結到佈告欄",
"Maximum file size of {size} exceeded" : "達到最大的檔案大小 {size} ",
"Error creating the share" : "建立分享時發生錯誤"
"Link to a card" : "連結到卡片",
"Create a card" : "建立卡片",
"Message from {author} in {conversationName}" : "來自 {conversationName} 中 {author} 的訊息",
"Something went wrong" : "出了點問題",
"Failed to upload {name}" : "上傳 {name} 失敗",
"Maximum file size of {size} exceeded" : "超過最大的檔案大小 {size} ",
"Error creating the share" : "建立分享時發生錯誤",
"Share with a Deck card" : "與 Deck 卡片分享",
"Share {file} with a Deck card" : "與 Deck 卡片分享 {file}",
"Share" : "分享"
},
"nplurals=1; plural=0;");

View File

@@ -1,68 +1,276 @@
{ "translations": {
"You have created a new board {board}" : "您已建立新的佈告欄 {board}",
"{user} has created a new board {board}" : "{user} 已建立新的佈告欄  {board}",
"{user} has created a new board {board}" : "{user} 已建立新的佈告欄 {board}",
"You have deleted the board {board}" : "您已刪除佈告欄 {board}",
"{user} has deleted the board {board}" : "{user} 已刪除佈告欄{board}",
"You have restored the board {board}" : "您已還原佈告欄{board}",
"{user} has restored the board {board}" : "{user}已還原佈告欄{board}",
"You have shared the board {board} with {acl}" : "您已和{acl}分享佈告欄{board}",
"{user} has shared the board {board} with {acl}" : "{user} 已和{acl}分享佈告欄{board}",
"Personal" : "私人的",
"{user} has deleted the board {board}" : "{user} 已刪除佈告欄 {board}",
"You have restored the board {board}" : "您已還原佈告欄 {board}",
"{user} has restored the board {board}" : "{user} 已還原佈告欄 {board}",
"You have shared the board {board} with {acl}" : "您已和 {acl} 分享佈告欄 {board}",
"{user} has shared the board {board} with {acl}" : "{user} 已和 {acl} 分享佈告欄 {board}",
"You have removed {acl} from the board {board}" : "您已從佈告欄 {board} 移除 {acl}",
"{user} has removed {acl} from the board {board}" : "{user} 已從佈告欄 {board} 移除 {acl}",
"You have renamed the board {before} to {board}" : "您已將佈告欄 {before} 重新命名為 {board}",
"{user} has renamed the board {before} to {board}" : "{user} 已將佈告欄 {before} 重新命名為 {board}",
"You have archived the board {board}" : "您已封存佈告欄 {board}",
"{user} has archived the board {before}" : "{user} 已封存佈告欄 {before}",
"You have unarchived the board {board}" : "您已解除封存佈告欄 {board}",
"{user} has unarchived the board {before}" : "{user} 已解除封存佈告欄 {before}",
"You have created a new list {stack} on board {board}" : "您已在佈告欄 {board} 上建立新列表 {stack}",
"{user} has created a new list {stack} on board {board}" : "{user} 已在佈告欄 {board} 上建立新列表 {stack}",
"You have renamed list {before} to {stack} on board {board}" : "您已將佈告欄 {board} 上的列表 {before} 重新命名為 {stack}",
"{user} has renamed list {before} to {stack} on board {board}" : "{user} 已將佈告欄 {board} 上的列表 {before} 重新命名為 {stack}",
"You have deleted list {stack} on board {board}" : "您已刪除佈告欄 {board} 上的列表 {stack}",
"{user} has deleted list {stack} on board {board}" : "{user} 已刪除佈告欄 {board} 上的列表 {stack}",
"You have created card {card} in list {stack} on board {board}" : "您已在佈告欄 {board} 上的列表 {stack} 建立卡片 {card}",
"{user} has created card {card} in list {stack} on board {board}" : "{user} 已在佈告欄 {board} 上的列表 {stack} 建立卡片 {card}",
"You have deleted card {card} in list {stack} on board {board}" : "您已在佈告欄 {board} 上的列表 {stack} 刪除卡片 {card}",
"{user} has deleted card {card} in list {stack} on board {board}" : "{user} 已在佈告欄 {board} 上的列表 {stack} 刪除卡片 {card}",
"You have renamed the card {before} to {card}" : "您已將卡片 {before} 重新命名為 {card}",
"{user} has renamed the card {before} to {card}" : "{user} 已將卡片 {before} 重新命名為 {card}",
"You have added a description to card {card} in list {stack} on board {board}" : "您已將描述新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"{user} has added a description to card {card} in list {stack} on board {board}" : "{user} 已將描述新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"You have updated the description of card {card} in list {stack} on board {board}" : "您已更新佈告欄 {board} 上的列表 {stack} 的卡片 {card} 的描述",
"{user} has updated the description of the card {card} in list {stack} on board {board}" : "{user} 已更新佈告欄 {board} 上的列表 {stack} 的卡片 {card} 的描述",
"You have archived card {card} in list {stack} on board {board}" : "您已封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"{user} has archived card {card} in list {stack} on board {board}" : "{user} 已封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"You have unarchived card {card} in list {stack} on board {board}" : "您已解除封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"{user} has unarchived card {card} in list {stack} on board {board}" : "{user} 已解除封存佈告欄 {board} 上的列表 {stack} 中的卡片 {card}",
"You have removed the due date of card {card}" : "您已移除卡片 {card} 的到期日",
"{user} has removed the due date of card {card}" : "{user} 已移除卡片 {card} 的到期日",
"You have set the due date of card {card} to {after}" : "您已設定卡片 {card} 的到期日",
"{user} has set the due date of card {card} to {after}" : "{user} 已設定卡片 {card} 的到期日",
"You have updated the due date of card {card} to {after}" : "您已將卡片 {card} 的到期日更新為 {after}",
"{user} has updated the due date of card {card} to {after}" : "{user} 已將卡片 {card} 的到期日更新為 {after}",
"You have added the tag {label} to card {card} in list {stack} on board {board}" : "您已將標籤 {label} 新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"{user} has added the tag {label} to card {card} in list {stack} on board {board}" : "{user} 已將標籤 {label} 新增到佈告欄 {board} 上的列表 {stack} 的卡片 {card}",
"You have removed the tag {label} from card {card} in list {stack} on board {board}" : "您已將標籤 {label} 從佈告欄 {board} 上列表 {stack} 中的卡片 {card} 移除",
"{user} has removed the tag {label} from card {card} in list {stack} on board {board}" : "{user} 已將標籤 {label} 從佈告欄 {board} 上列表 {stack} 中的卡片 {card} 移除",
"You have assigned {assigneduser} to card {card} on board {board}" : "您已將佈告欄 {board} 上的卡片 {card} 分配給 {assigneduser}",
"{user} has assigned {assigneduser} to card {card} on board {board}" : "{user} 已將佈告欄 {board} 上的卡片 {card} 分配給 {assigneduser}",
"You have unassigned {assigneduser} from card {card} on board {board}" : "您已取消分配佈告欄 {board} 上的卡片 {card} 給 {assigneduser}",
"{user} has unassigned {assigneduser} from card {card} on board {board}" : "{user} 已取消分配佈告欄 {board} 上的卡片 {card} 給 {assigneduser}",
"You have moved the card {card} from list {stackBefore} to {stack}" : "您已將卡片 {card} 從列表 {stackBefore} 移動到 {stack}",
"{user} has moved the card {card} from list {stackBefore} to {stack}" : "{user} 已將卡片 {card} 從列表 {stackBefore} 移動到 {stack}",
"You have added the attachment {attachment} to card {card}" : "您已將附件 {attachment} 新增到 {card}",
"{user} has added the attachment {attachment} to card {card}" : "{user} 已將附件 {attachment} 新增到 {card}",
"You have updated the attachment {attachment} on card {card}" : "您已更新卡片 {card} 上的附件 {attachment}",
"{user} has updated the attachment {attachment} on card {card}" : "{user} 已更新卡片 {card} 上的附件 {attachment}",
"You have deleted the attachment {attachment} from card {card}" : "您已從卡片 {card} 刪除附件 {attachment}",
"{user} has deleted the attachment {attachment} from card {card}" : "{user} 已從卡片 {card} 刪除附件 {attachment}",
"You have restored the attachment {attachment} to card {card}" : "您已從卡片 {card} 還原附件 {attachment}",
"{user} has restored the attachment {attachment} to card {card}" : "{user} 已從卡片 {card} 還原附件 {attachment}",
"You have commented on card {card}" : "您已在卡片 {card} 上留言",
"{user} has commented on card {card}" : "{user} 已在卡片 {card} 上留言",
"A <strong>card description</strong> inside the Deck app has been changed" : "Deck 應用程式中的<strong>卡片描述</strong>已變更",
"Deck" : "Deck",
"Changes in the <strong>Deck app</strong>" : "<strong>Deck 應用程式</strong>中的變更",
"A <strong>comment</strong> was created on a card" : "已在卡片上建立了<strong>留言</strong>",
"Upcoming cards" : "接下來的卡片",
"Personal" : "個人",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "卡片「%s」位於「%s」已由 %s 分配給您。",
"{user} has assigned the card \"%s\" on \"%s\" to you." : "{user} 已分配卡片「%s」位於「%s」給您。",
"The card \"%s\" on \"%s\" has reached its due date." : "卡片「%s」位於「%s」已達到期日。",
"%s has mentioned you in a comment on \"%s\"." : "%s 在「%s」的留言中提到了您。",
"{user} has mentioned you in a comment on \"%s\"." : "{user} 在「%s」的留言中提到了您。",
"The board \"%s\" has been shared with you by %s." : "佈告欄「%s」已由 %s 分享給您。",
"{user} has shared the board %s with you." : "{user} 已與您分享佈告欄 %s。",
"No data was provided to create an attachment." : "沒有提供用於建立附件的資料。",
"Finished" : "已完成",
"To review" : "待檢閱",
"Action needed" : "需要採取行動",
"Later" : "稍後",
"copy" : "複製",
"To do" : "待辦事項",
"Doing" : "正在進行",
"Done" : "完成",
"Example Task 3" : "範例工作 3",
"Example Task 2" : "範例工作 2",
"Example Task 1" : "範例工作 1",
"The file was uploaded" : "檔案已上傳",
"The uploaded file exceeds the upload_max_filesize directive in php.ini" : "上傳的檔案大小超過 php.ini 當中 upload_max_filesize 選項的限制",
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "上傳的檔案大小超過 HTML 表單中 MAX_FILE_SIZE 的限制",
"The file was only partially uploaded" : "檔案僅部份上傳",
"No file was uploaded" : "沒有檔案被上傳",
"Missing a temporary folder" : "找不到暫存資料夾",
"Could not write file to disk" : "寫入硬碟失敗",
"Could not write file to disk" : "無法寫入硬碟",
"A PHP extension stopped the file upload" : "一個 PHP 擴充功能終止檔案的上傳",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck是一種看板式組織工具旨在針對與Nextcloud集成的團隊進行個人計劃和項目組織。\n\n\n- 📥 增加您的任務到card和把它們整理好\n- 📄 寫下額外的筆記在markdown\n- 🔖 分配標籤以更好地組織\n- 👥 與您的團隊,朋友或家人分享\n- 📎 附加檔案並將其嵌入到您的 markdown 描述\n- 💬 使用評論與您的團隊討論\n- ⚡ 跟踪變化在活動流程中\n- 🚀 取得您的專案組織",
"No file uploaded or file size exceeds maximum of %s" : "沒有上傳檔案或檔案超過上限 %s",
"Card not found" : "找不到卡片",
"Path is already shared with this card" : "路徑已與此卡片分享",
"Invalid date, date format must be YYYY-MM-DD" : "無效的日期,日期格式必須為 YYYY-MM-DD",
"Personal planning and team project organization" : "個人規劃與團隊專案組織",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck 是一套看板式組織工具,提供與 Nextcloud 整合的個人規劃與團隊專案組織功能。\n\n\n- 📥 將您的工作項目新增到卡片中,並將它們按順序排列\n- 📄 以 Markdown 編寫額外的註釋\n- 🔖 分配標籤讓組織更方便\n- 👥 與您的團隊、朋友與家人分享\n- 📎 附上檔案並將其嵌入到您的 Markdown 描述中\n- 💬 使用留言與您的團隊討論\n- ⚡ 追蹤活動流程中的變動\n- 🚀 整理好您的專案",
"Card details" : "卡片詳細資訊",
"Add board" : "新增佈告欄",
"Select the board to link to a project" : "選取要連結到專案的佈告欄",
"Search by board title" : "按佈告欄標題搜尋",
"Select board" : "選取佈告欄",
"Create a new card" : "建立新卡片",
"Select a board" : "選取佈告欄",
"Select a list" : "選取列表",
"Card title" : "卡片標題",
"Cancel" : "取消",
"Creating the new card…" : "正在建立新卡片……",
"\"{card}\" was added to \"{board}\"" : "「{card}」已新增至「{board}」",
"Open card" : "開啟卡片",
"Close" : "關閉",
"Create card" : "建立卡片",
"Select a card" : "選取卡片",
"Select the card to link to a project" : "選取要連結到專案的卡片",
"Link to card" : "連結到卡片",
"File already exists" : "檔案已存在",
"Add list" : "新增清單",
"Next 7 days" : "接下來 7 天",
"Next 30 days" : "接下來 30 天",
"A file with the name {filename} already exists." : "名稱為 {filename} 的檔案已存在。",
"Do you want to overwrite it?" : "您想要覆寫它嗎?",
"Overwrite file" : "覆寫檔案",
"Keep existing file" : "保留既有檔案",
"This board is read only" : "此佈告欄唯讀",
"Drop your files to upload" : "拖曳您的檔案以上傳",
"Archived cards" : "已封存的卡片",
"Add list" : "新增列表",
"List name" : "列表名稱",
"Apply filter" : "套用過濾條件",
"Filter by tag" : "按標籤過濾",
"Filter by assigned user" : "按被分配的使用者過濾",
"Unassigned" : "未分配",
"Filter by due date" : "按到期日過濾",
"Overdue" : "超過到期日",
"Next 24 hours" : "接下來24小時",
"Next 7 days" : "接下來7天",
"Next 30 days" : "接下來30天",
"No due date" : "無到期日",
"Clear filter" : "清除過濾條件",
"Hide archived cards" : "隱藏已封存的卡片",
"Show archived cards" : "顯示已封存的卡片",
"Toggle compact mode" : "切換簡潔模式",
"Details" : "詳細資料",
"Loading board" : "正在載入佈告欄",
"No lists available" : "沒有可用的列表",
"Create a new list to add cards to this board" : "建立新列表以新增卡片到此佈告欄",
"Board not found" : "找不到佈告欄",
"Sharing" : "分享",
"Tags" : "標籤",
"Deleted items" : "刪除的項目",
"Timeline" : "時間軸",
"Deleted lists" : "已刪除的列表",
"Undo" : "復原",
"Deleted cards" : "已刪除的卡片",
"Share board with a user, group or circle …" : "與使用者、群組或小圈圈分享佈告欄……",
"Searching for users, groups and circles …" : "搜尋使用者、群組與小圈圈……",
"No participants found" : "找不到參與者",
"Board owner" : "佈告欄擁有者",
"(Group)" : "(群組)",
"(Circle)" : "(小圈圈)",
"Can edit" : "可以編輯",
"Can share" : "可以分享",
"Can manage" : "可以管理",
"Delete" : "刪除",
"Delete list" : "刪除清單",
"Add card" : "增加卡片",
"Failed to create share with {displayName}" : "無法建立與 {displayName} 的分享",
"Add a new list" : "新增列表",
"Archive all cards" : "封存所有卡片",
"Delete list" : "刪除列表",
"Add card" : "新增卡片",
"Archive all cards in this list" : "封存此列表中的所有卡片",
"Add a new card" : "新增卡片",
"Card name" : "卡片名稱",
"List deleted" : "列表已刪除",
"Edit" : "編輯",
"Add a new tag" : "新增標籤",
"title and color value must be provided" : "必須提供標題與顏色的值",
"Board name" : "佈告欄名稱",
"Members" : "成員",
"Upload new files" : "上傳新檔案",
"Share from Files" : "從「檔案」分享",
"Add this attachment" : "新增此附件",
"Show in Files" : "在「檔案」中顯示",
"Unshare file" : "取消分享檔案",
"Delete Attachment" : "刪除附件",
"Restore Attachment" : "還原附件",
"File to share" : "要分享的檔案",
"Invalid path selected" : "選取的路徑無效",
"Open in sidebar view" : "在側邊欄中開啟",
"Open in bigger view" : "以較大的檢視模式開啟",
"Attachments" : "附件",
"Comments" : "意見",
"Comments" : "留言",
"Modified" : "已修改",
"Created" : "已新增",
"Assign to users" : "分派給使用者",
"Due date" : "截止日",
"The title cannot be empty." : "標題不能為空",
"No comments yet. Begin the discussion!" : "暫無留言。開始討論吧!",
"Assign a tag to this card…" : "分配標籤到此卡片……",
"Assign to users" : "分配給使用者",
"Assign to users/groups/circles" : "分配給使用者/群組/小圈圈",
"Assign a user to this card…" : "分配使用者到此卡片……",
"Due date" : "到期日",
"Set a due date" : "設定到期日",
"Remove due date" : "移除到期日",
"Select Date" : "選擇日期",
"Save" : "儲存",
"The comment cannot be empty." : "留言不能為空。",
"The comment cannot be longer than 1000 characters." : "留言不能多於 1000 個字元。",
"In reply to" : "回覆",
"Reply" : "回覆",
"Update" : "更新",
"Description" : "描述",
"(group)" : "(群組)",
"Assign to me" : "分派給我",
"(Unsaved)" : "(未儲存)",
"(Saving…)" : "(正在儲存……)",
"Formatting help" : "格式化說明",
"Edit description" : "編輯描述",
"View description" : "檢視描述",
"Add Attachment" : "新增附件",
"Write a description …" : "編寫描述……",
"Choose attachment" : "選擇附件",
"(group)" : "(群組)",
"(circle)" : "(小圈圈)",
"Assign to me" : "分配給我",
"Unassign myself" : "取消分配給我",
"Move card" : "移動卡片",
"Unarchive card" : "解除封存卡片",
"Archive card" : "封存卡片",
"Delete card" : "刪除作業",
"Delete card" : "刪除卡片",
"Move card to another board" : "將卡片移動到其他佈告欄",
"Card deleted" : "卡片已刪除",
"seconds ago" : "幾秒前",
"All boards" : "所有佈告欄",
"Archived boards" : "已封存的佈告欄",
"Shared with you" : "與您分享",
"Edit board" : "編輯專案",
"Use bigger card view" : "使用較大的卡片檢視",
"Show boards in calendar/tasks" : "在日曆/工作項目中顯示佈告欄",
"Limit deck usage of groups" : "限制群組的 Deck 使用",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "限制 Deck 將會阻止不屬於這些群組的使用者建立自己的佈告欄。使用者仍然可以在與他們分享的佈告欄上工作。",
"Board details" : "佈告欄詳細資訊",
"Edit board" : "編輯佈告欄",
"Clone board" : "再製佈告欄",
"Unarchive board" : "解除封存佈告欄",
"Archive board" : "封存佈告欄",
"Turn on due date reminders" : "開啟到期日提醒",
"Turn off due date reminders" : "關閉到期日提醒",
"Due date reminders" : "到期日提醒",
"All cards" : "所有看片",
"Assigned cards" : "已分配的卡片",
"No notifications" : "無通知",
"Delete board" : "刪除佈告欄",
"Board {0} deleted" : "已刪除佈告欄 {0}",
"Only assigned cards" : "僅已分配的卡片",
"No reminder" : "無提醒",
"An error occurred" : "發生錯誤",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "您確定要刪除佈告欄 {title} 嗎?這將會刪除所有此佈告欄的資料。",
"Delete the board?" : "刪除佈告欄?",
"Loading filtered view" : "正在載入過濾的檢視",
"Today" : "今天",
"Tomorrow" : "明天",
"This week" : "本週",
"No due" : "無到期日",
"No upcoming cards" : "無接下來的卡片",
"upcoming cards" : "接下來的卡片",
"Link to a board" : "連結到佈告欄",
"Maximum file size of {size} exceeded" : "達到最大的檔案大小 {size} ",
"Error creating the share" : "建立分享時發生錯誤"
"Link to a card" : "連結到卡片",
"Create a card" : "建立卡片",
"Message from {author} in {conversationName}" : "來自 {conversationName} 中 {author} 的訊息",
"Something went wrong" : "出了點問題",
"Failed to upload {name}" : "上傳 {name} 失敗",
"Maximum file size of {size} exceeded" : "超過最大的檔案大小 {size} ",
"Error creating the share" : "建立分享時發生錯誤",
"Share with a Deck card" : "與 Deck 卡片分享",
"Share {file} with a Deck card" : "與 Deck 卡片分享 {file}",
"Share" : "分享"
},"pluralForm" :"nplurals=1; plural=0;"
}

View File

@@ -23,11 +23,191 @@
namespace OCA\Deck\AppInfo;
$version = \OCP\Util::getVersion()[0];
if ($version >= 20) {
class Application extends Application20 {
use Closure;
use Exception;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\CommentEventHandler;
use OCA\Deck\Capabilities;
use OCA\Deck\Collaboration\Resources\ResourceProvider;
use OCA\Deck\Collaboration\Resources\ResourceProviderCard;
use OCA\Deck\Dashboard\DeckWidget;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Event\AclCreatedEvent;
use OCA\Deck\Event\AclDeletedEvent;
use OCA\Deck\Event\AclUpdatedEvent;
use OCA\Deck\Event\CardCreatedEvent;
use OCA\Deck\Event\CardDeletedEvent;
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
use OCA\Deck\Listeners\FullTextSearchEventListener;
use OCA\Deck\Middleware\DefaultBoardMiddleware;
use OCA\Deck\Middleware\ExceptionMiddleware;
use OCA\Deck\Notification\Notifier;
use OCA\Deck\Search\CardCommentProvider;
use OCA\Deck\Search\DeckProvider;
use OCA\Deck\Service\PermissionService;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\Sharing\Listener;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Comments\CommentsEntityEvent;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IServerContainer;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager;
use OCP\Share\IManager;
use OCP\Util;
use Psr\Container\ContainerInterface;
class Application extends App implements IBootstrap {
public const APP_ID = 'deck';
public const COMMENT_ENTITY_TYPE = 'deckCard';
/** @var IServerContainer */
private $server;
public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
$this->server = \OC::$server;
}
} else {
class Application extends ApplicationLegacy {
public function boot(IBootContext $context): void {
$context->injectFn(Closure::fromCallable([$this, 'registerUserGroupHooks']));
$context->injectFn(Closure::fromCallable([$this, 'registerCommentsEntity']));
$context->injectFn(Closure::fromCallable([$this, 'registerCommentsEventHandler']));
$context->injectFn(Closure::fromCallable([$this, 'registerNotifications']));
$context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources']));
$context->injectFn(function (IManager $shareManager) {
$shareManager->registerShareProvider(DeckShareProvider::class);
});
$context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) {
$listener->register($eventDispatcher);
});
}
public function register(IRegistrationContext $context): void {
if ((@include_once __DIR__ . '/../../vendor/autoload.php') === false) {
throw new Exception('Cannot include autoload. Did you run install dependencies using composer?');
}
$context->registerCapability(Capabilities::class);
$context->registerMiddleWare(ExceptionMiddleware::class);
$context->registerMiddleWare(DefaultBoardMiddleware::class);
$context->registerService('databaseType', static function (ContainerInterface $c) {
return $c->get(IConfig::class)->getSystemValue('dbtype', 'sqlite');
});
$context->registerService('database4ByteSupport', static function (ContainerInterface $c) {
return $c->get(IDBConnection::class)->supports4ByteText();
});
$context->registerSearchProvider(DeckProvider::class);
$context->registerSearchProvider(CardCommentProvider::class);
$context->registerDashboardWidget(DeckWidget::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
// Event listening for full text search indexing
$context->registerEventListener(CardCreatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(CardUpdatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(CardDeletedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(AclCreatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(AclUpdatedEvent::class, FullTextSearchEventListener::class);
$context->registerEventListener(AclDeletedEvent::class, FullTextSearchEventListener::class);
}
public function registerNotifications(NotificationManager $notificationManager): void {
$notificationManager->registerNotifierService(Notifier::class);
}
private function registerUserGroupHooks(IUserManager $userManager, IGroupManager $groupManager): void {
$container = $this->getContainer();
// Delete user/group acl entries when they get deleted
$userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) {
// delete existing acl entries for deleted user
/** @var AclMapper $aclMapper */
$aclMapper = $container->query(AclMapper::class);
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID());
foreach ($acls as $acl) {
$aclMapper->delete($acl);
}
// delete existing user assignments
$assignmentMapper = $container->query(AssignmentMapper::class);
$assignments = $assignmentMapper->findByParticipant($user->getUID());
foreach ($assignments as $assignment) {
$assignmentMapper->delete($assignment);
}
/** @var BoardMapper $boardMapper */
$boardMapper = $container->query(BoardMapper::class);
$boards = $boardMapper->findAllByOwner($user->getUID());
foreach ($boards as $board) {
$boardMapper->delete($board);
}
});
$groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) {
/** @var AclMapper $aclMapper */
$aclMapper = $container->query(AclMapper::class);
$aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
foreach ($acls as $acl) {
$aclMapper->delete($acl);
}
});
}
public function registerCommentsEntity(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) {
$event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) {
/** @var CardMapper */
$cardMapper = $this->getContainer()->get(CardMapper::class);
$permissionService = $this->getContainer()->get(PermissionService::class);
try {
return $permissionService->checkPermission($cardMapper, (int) $name, Acl::PERMISSION_READ);
} catch (\Exception $e) {
return false;
}
});
});
}
protected function registerCommentsEventHandler(ICommentsManager $commentsManager): void {
$commentsManager->registerEventHandler(function () {
return $this->getContainer()->query(CommentEventHandler::class);
});
}
protected function registerCollaborationResources(IProviderManager $resourceManager, SymfonyAdapter $symfonyAdapter): void {
$resourceManager->registerResourceProvider(ResourceProvider::class);
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
$symfonyAdapter->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () {
if (strpos(\OC::$server->getRequest()->getPathInfo(), '/call/') === 0) {
// Talk integration has its own entrypoint which already includes collections handling
return;
}
Util::addScript('deck', 'collections');
});
}
}

View File

@@ -1,252 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\AppInfo;
use Closure;
use Exception;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\CommentEventHandler;
use OCA\Deck\Capabilities;
use OCA\Deck\Collaboration\Resources\ResourceProvider;
use OCA\Deck\Collaboration\Resources\ResourceProviderCard;
use OCA\Deck\Dashboard\DeckWidget;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Listeners\BeforeTemplateRenderedListener;
use OCA\Deck\Middleware\DefaultBoardMiddleware;
use OCA\Deck\Middleware\ExceptionMiddleware;
use OCA\Deck\Notification\Notifier;
use OCA\Deck\Search\DeckProvider;
use OCA\Deck\Service\FullTextSearchService;
use OCA\Deck\Service\PermissionService;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\Sharing\Listener;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Comments\CommentsEntityEvent;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\FullTextSearch\IFullTextSearchManager;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IServerContainer;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager;
use OCP\Share\IManager;
use OCP\Util;
use Psr\Container\ContainerInterface;
class Application20 extends App implements IBootstrap {
public const APP_ID = 'deck';
public const COMMENT_ENTITY_TYPE = 'deckCard';
/** @var IServerContainer */
private $server;
/** @var FullTextSearchService */
private $fullTextSearchService;
/** @var IFullTextSearchManager */
private $fullTextSearchManager;
public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
$this->server = \OC::$server;
}
public function boot(IBootContext $context): void {
$context->injectFn(Closure::fromCallable([$this, 'registerUserGroupHooks']));
$context->injectFn(Closure::fromCallable([$this, 'registerCommentsEntity']));
$context->injectFn(Closure::fromCallable([$this, 'registerCommentsEventHandler']));
$context->injectFn(Closure::fromCallable([$this, 'registerNotifications']));
$context->injectFn(Closure::fromCallable([$this, 'registerFullTextSearch']));
$context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources']));
$context->injectFn(function (IManager $shareManager) {
if (method_exists($shareManager, 'registerShareProvider')) {
$shareManager->registerShareProvider(DeckShareProvider::class);
}
});
$context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) {
$listener->register($eventDispatcher);
});
}
public function register(IRegistrationContext $context): void {
if ((@include_once __DIR__ . '/../../vendor/autoload.php') === false) {
throw new Exception('Cannot include autoload. Did you run install dependencies using composer?');
}
$context->registerCapability(Capabilities::class);
$context->registerMiddleWare(ExceptionMiddleware::class);
$context->registerMiddleWare(DefaultBoardMiddleware::class);
$context->registerService('databaseType', static function (ContainerInterface $c) {
return $c->get(IConfig::class)->getSystemValue('dbtype', 'sqlite');
});
$context->registerService('database4ByteSupport', static function (ContainerInterface $c) {
return $c->get(IDBConnection::class)->supports4ByteText();
});
$context->registerSearchProvider(DeckProvider::class);
$context->registerDashboardWidget(DeckWidget::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
}
public function registerNotifications(NotificationManager $notificationManager): void {
$notificationManager->registerNotifierService(Notifier::class);
}
private function registerUserGroupHooks(IUserManager $userManager, IGroupManager $groupManager): void {
$container = $this->getContainer();
// Delete user/group acl entries when they get deleted
$userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) {
// delete existing acl entries for deleted user
/** @var AclMapper $aclMapper */
$aclMapper = $container->query(AclMapper::class);
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID());
foreach ($acls as $acl) {
$aclMapper->delete($acl);
}
// delete existing user assignments
$assignmentMapper = $container->query(AssignmentMapper::class);
$assignments = $assignmentMapper->findByParticipant($user->getUID());
foreach ($assignments as $assignment) {
$assignmentMapper->delete($assignment);
}
/** @var BoardMapper $boardMapper */
$boardMapper = $container->query(BoardMapper::class);
$boards = $boardMapper->findAllByOwner($user->getUID());
foreach ($boards as $board) {
$boardMapper->delete($board);
}
});
$groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) {
/** @var AclMapper $aclMapper */
$aclMapper = $container->query(AclMapper::class);
$aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
foreach ($acls as $acl) {
$aclMapper->delete($acl);
}
});
}
public function registerCommentsEntity(IEventDispatcher $eventDispatcher): void {
$eventDispatcher->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) {
$event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) {
/** @var CardMapper */
$cardMapper = $this->getContainer()->get(CardMapper::class);
$permissionService = $this->getContainer()->get(PermissionService::class);
try {
return $permissionService->checkPermission($cardMapper, (int) $name, Acl::PERMISSION_READ);
} catch (\Exception $e) {
return false;
}
});
});
}
protected function registerCommentsEventHandler(ICommentsManager $commentsManager): void {
$commentsManager->registerEventHandler(function () {
return $this->getContainer()->query(CommentEventHandler::class);
});
}
protected function registerCollaborationResources(IProviderManager $resourceManager, SymfonyAdapter $symfonyAdapter): void {
$resourceManager->registerResourceProvider(ResourceProvider::class);
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
$symfonyAdapter->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () {
if (strpos(\OC::$server->getRequest()->getPathInfo(), '/call/') === 0) {
// Talk integration has its own entrypoint which already includes collections handling
return;
}
Util::addScript('deck', 'collections');
});
}
public function registerFullTextSearch(IFullTextSearchManager $fullTextSearchManager, IEventDispatcher $eventDispatcher): void {
if (!$fullTextSearchManager->isAvailable()) {
return;
}
// FIXME move to addServiceListener
$server = $this->server;
$eventDispatcher->addListener(
'\OCA\Deck\Card::onCreate', function (Event $e) use ($server) {
$fullTextSearchService = $server->get(FullTextSearchService::class);
$fullTextSearchService->onCardCreated($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Card::onUpdate', function (Event $e) use ($server) {
$fullTextSearchService = $server->get(FullTextSearchService::class);
$fullTextSearchService->onCardUpdated($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Card::onDelete', function (Event $e) use ($server) {
$fullTextSearchService = $server->get(FullTextSearchService::class);
$fullTextSearchService->onCardDeleted($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Board::onShareNew', function (Event $e) use ($server) {
$fullTextSearchService = $server->get(FullTextSearchService::class);
$fullTextSearchService->onBoardShares($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Board::onShareEdit', function (Event $e) use ($server) {
$fullTextSearchService = $server->get(FullTextSearchService::class);
$fullTextSearchService->onBoardShares($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Board::onShareDelete', function (Event $e) use ($server) {
$fullTextSearchService = $server->get(FullTextSearchService::class);
$fullTextSearchService->onBoardShares($e);
}
);
}
}

View File

@@ -1,249 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\AppInfo;
use Exception;
use OCA\Deck\Activity\CommentEventHandler;
use OCA\Deck\Capabilities;
use OCA\Deck\Collaboration\Resources\ResourceProvider;
use OCA\Deck\Collaboration\Resources\ResourceProviderCard;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Middleware\DefaultBoardMiddleware;
use OCA\Deck\Middleware\ExceptionMiddleware;
use OCA\Deck\Notification\Notifier;
use OCA\Deck\Service\FullTextSearchService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\App;
use OCP\Collaboration\Resources\IManager;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Comments\CommentsEntityEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\FullTextSearch\IFullTextSearchManager;
use OCP\IGroup;
use OCP\IServerContainer;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Util;
if ((@include_once __DIR__ . '/../../vendor/autoload.php') === false) {
throw new Exception('Cannot include autoload. Did you run install dependencies using composer?');
}
class ApplicationLegacy extends App {
public const APP_ID = 'deck';
public const COMMENT_ENTITY_TYPE = 'deckCard';
/** @var IServerContainer */
private $server;
/** @var FullTextSearchService */
private $fullTextSearchService;
/** @var IFullTextSearchManager */
private $fullTextSearchManager;
public function __construct(array $urlParams = []) {
parent::__construct('deck', $urlParams);
$container = $this->getContainer();
$server = $this->getContainer()->getServer();
$this->server = $server;
$container->registerCapability(Capabilities::class);
$container->registerMiddleWare(ExceptionMiddleware::class);
$container->registerMiddleWare(DefaultBoardMiddleware::class);
$container->registerService('databaseType', static function () use ($server) {
return $server->getConfig()->getSystemValue('dbtype', 'sqlite');
});
$container->registerService('database4ByteSupport', static function () use ($server) {
return $server->getDatabaseConnection()->supports4ByteText();
});
$this->register();
}
public function register(): void {
$this->registerUserGroupHooks();
$this->registerNotifications();
$this->registerCommentsEntity();
$this->registerFullTextSearch();
$this->registerCollaborationResources();
}
private function registerUserGroupHooks(): void {
$container = $this->getContainer();
// Delete user/group acl entries when they get deleted
/** @var IUserManager $userManager */
$userManager = $this->server->getUserManager();
$userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) {
// delete existing acl entries for deleted user
/** @var AclMapper $aclMapper */
$aclMapper = $container->query(AclMapper::class);
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID());
foreach ($acls as $acl) {
$aclMapper->delete($acl);
}
// delete existing user assignments
$assignmentMapper = $container->query(AssignmentMapper::class);
$assignments = $assignmentMapper->findByParticipant($user->getUID());
foreach ($assignments as $assignment) {
$assignmentMapper->delete($assignment);
}
/** @var BoardMapper $boardMapper */
$boardMapper = $container->query(BoardMapper::class);
$boards = $boardMapper->findAllByOwner($user->getUID());
foreach ($boards as $board) {
$boardMapper->delete($board);
}
});
/** @var IUserManager $userManager */
$groupManager = $this->server->getGroupManager();
$groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) {
/** @var AclMapper $aclMapper */
$aclMapper = $container->query(AclMapper::class);
$aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
foreach ($acls as $acl) {
$aclMapper->delete($acl);
}
});
}
public function registerNotifications(): void {
$notificationManager = $this->server->getNotificationManager();
$notificationManager->registerNotifierService(Notifier::class);
}
public function registerCommentsEntity(): void {
$this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) {
$event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) {
/** @var CardMapper */
$cardMapper = $this->getContainer()->query(CardMapper::class);
$permissionService = $this->getContainer()->query(PermissionService::class);
try {
return $permissionService->checkPermission($cardMapper, (int) $name, Acl::PERMISSION_READ);
} catch (\Exception $e) {
return false;
}
});
});
$this->registerCommentsEventHandler();
}
/**
*/
protected function registerCommentsEventHandler(): void {
$this->server->getCommentsManager()->registerEventHandler(function () {
return $this->getContainer()->query(CommentEventHandler::class);
});
}
protected function registerCollaborationResources(): void {
$version = \OCP\Util::getVersion()[0];
if ($version < 16) {
return;
}
/**
* Register Collaboration ResourceProvider
*
* @Todo: Remove if min-version is 18
*/
if ($version < 18) {
/** @var IManager $resourceManager */
$resourceManager = $this->getContainer()->query(IManager::class);
} else {
/** @var IProviderManager $resourceManager */
$resourceManager = $this->getContainer()->query(IProviderManager::class);
}
$resourceManager->registerResourceProvider(ResourceProvider::class);
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
$this->server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () {
Util::addScript('deck', 'collections');
});
}
public function registerFullTextSearch(): void {
if (Util::getVersion()[0] < 16) {
return;
}
$c = $this->getContainer();
try {
$this->fullTextSearchService = $c->query(FullTextSearchService::class);
$this->fullTextSearchManager = $c->query(IFullTextSearchManager::class);
} catch (Exception $e) {
return;
}
if (!$this->fullTextSearchManager->isAvailable()) {
return;
}
/** @var IEventDispatcher $eventDispatcher */
$eventDispatcher = $this->server->query(IEventDispatcher::class);
$eventDispatcher->addListener(
'\OCA\Deck\Card::onCreate', function (Event $e) {
$this->fullTextSearchService->onCardCreated($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Card::onUpdate', function (Event $e) {
$this->fullTextSearchService->onCardUpdated($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Card::onDelete', function (Event $e) {
$this->fullTextSearchService->onCardDeleted($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Board::onShareNew', function (Event $e) {
$this->fullTextSearchService->onBoardShares($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Board::onShareEdit', function (Event $e) {
$this->fullTextSearchService->onBoardShares($e);
}
);
$eventDispatcher->addListener(
'\OCA\Deck\Board::onShareDelete', function (Event $e) {
$this->fullTextSearchService->onBoardShares($e);
}
);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Controller;
use OCA\Deck\Db\Card;
use OCA\Deck\Service\SearchService;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
class SearchController extends OCSController {
/**
* @var SearchService
*/
private $searchService;
public function __construct(string $appName, IRequest $request, SearchService $searchService) {
parent::__construct($appName, $request);
$this->searchService = $searchService;
}
/**
* @NoAdminRequired
*/
public function search(string $term, ?int $limit = null, ?int $cursor = null): DataResponse {
$cards = $this->searchService->searchCards($term, $limit, $cursor);
return new DataResponse(array_map(function (Card $card) {
$json = $card->jsonSerialize();
$json['relatedStack'] = $card->getRelatedStack();
$json['relatedBoard'] = $card->getRelatedBoard();
return $json;
}, $cards));
}
}

View File

@@ -169,7 +169,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
}
$circles = array_map(function ($circle) {
return $circle->getUniqueId();
}, \OCA\Circles\Api\v1\Circles::joinedCircles('', true));
}, \OCA\Circles\Api\v1\Circles::joinedCircles($userId, true));
if (count($circles) === 0) {
return [];
}

View File

@@ -49,6 +49,9 @@ class Card extends RelationalEntity {
protected $notified = false;
protected $deletedAt = 0;
protected $commentsUnread = 0;
protected $relatedStack = null;
protected $relatedBoard = null;
private $databaseType = 'sqlite';
@@ -73,6 +76,9 @@ class Card extends RelationalEntity {
$this->addRelation('participants');
$this->addRelation('commentsUnread');
$this->addResolvable('owner');
$this->addRelation('relatedStack');
$this->addRelation('relatedBoard');
}
public function setDatabaseType($type) {
@@ -119,6 +125,8 @@ class Card extends RelationalEntity {
$json['duedate'] = $this->getDuedate(true);
unset($json['notified']);
unset($json['descriptionPrev']);
unset($json['relatedStack']);
unset($json['relatedBoard']);
return $json;
}

View File

@@ -23,11 +23,16 @@
namespace OCA\Deck\Db;
use DateTime;
use Exception;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Search\Query\SearchQuery;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
@@ -37,6 +42,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
private $labelMapper;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var IManager */
private $notificationManager;
private $databaseType;
@@ -46,13 +53,15 @@ class CardMapper extends QBMapper implements IPermissionMapper {
IDBConnection $db,
LabelMapper $labelMapper,
IUserManager $userManager,
IGroupManager $groupManager,
IManager $notificationManager,
$databaseType = 'sqlite',
$databaseType = 'sqlite3',
$database4ByteSupport = true
) {
parent::__construct($db, 'deck_cards', Card::class);
$this->labelMapper = $labelMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->notificationManager = $notificationManager;
$this->databaseType = $databaseType;
$this->database4ByteSupport = $database4ByteSupport;
@@ -117,7 +126,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
->addOrderBy('id');
/** @var Card $card */
$card = $this->findEntity($qb);
$labels = $this->labelMapper->findAssignedLabelsForCard($card->id);
$labels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
$card->setLabels($labels);
$this->mapOwner($card);
return $card;
@@ -149,8 +158,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function queryCardsByBoards(array $boardIds): IQueryBuilder {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*', 's.board_id')
->selectAlias('s.title', 'stack_title')
$qb->select('c.*')
->from('deck_cards', 'c')
->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id'))
->andWhere($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)));
@@ -261,27 +269,213 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function search($boardIds, $term, $limit = null, $offset = null) {
public function search(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
$qb = $this->queryCardsByBoards($boardIds);
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%')),
$qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))
)
);
$this->extendQueryByFilter($qb, $query);
if (count($query->getTextTokens()) > 0) {
$tokenMatching = $qb->expr()->andX(
...array_map(function (string $token) use ($qb) {
return $qb->expr()->orX(
$qb->expr()->iLike(
'c.title',
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR
),
$qb->expr()->iLike(
'c.description',
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR
)
);
}, $query->getTextTokens())
);
$qb->andWhere(
$tokenMatching
);
}
$qb->groupBy('c.id');
$qb->orderBy('c.last_modified', 'DESC');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
$qb->andWhere($qb->expr()->lt('c.last_modified', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
return $this->findEntities($qb);
$result = $qb->execute();
$entities = [];
while ($row = $result->fetch()) {
$entities[] = Card::fromRow($row);
}
$result->closeCursor();
return $entities;
}
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
public function searchComments(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
if (count($query->getTextTokens()) === 0) {
return [];
}
$qb = $this->queryCardsByBoards($boardIds);
$this->extendQueryByFilter($qb, $query);
$qb->innerJoin('c', 'comments', 'comments', $qb->expr()->andX(
$qb->expr()->eq('comments.object_id', 'c.id', IQueryBuilder::PARAM_STR),
$qb->expr()->eq('comments.object_type', $qb->createNamedParameter(Application::COMMENT_ENTITY_TYPE, IQueryBuilder::PARAM_STR))
));
$qb->selectAlias('comments.id', 'comment_id');
$tokenMatching = $qb->expr()->andX(
...array_map(function (string $token) use ($qb) {
return $qb->expr()->iLike(
'comments.message',
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR
);
}, $query->getTextTokens())
);
$qb->andWhere(
$tokenMatching
);
$qb->groupBy('comments.id');
$qb->orderBy('comments.id', 'DESC');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->andWhere($qb->expr()->lt('comments.id', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
$result = $qb->execute();
$entities = $result->fetchAll();
$result->closeCursor();
return $entities;
}
private function extendQueryByFilter(IQueryBuilder $qb, SearchQuery $query) {
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->innerJoin('s', 'deck_boards', 'b', $qb->expr()->eq('b.id', 's.board_id'));
$qb->andWhere($qb->expr()->eq('b.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
foreach ($query->getTitle() as $title) {
$qb->andWhere($qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($title->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
foreach ($query->getDescription() as $description) {
$qb->andWhere($qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($description->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
foreach ($query->getStack() as $stack) {
$qb->andWhere($qb->expr()->iLike('s.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($stack->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
if (count($query->getTag())) {
foreach ($query->getTag() as $index => $tag) {
$qb->innerJoin('c', 'deck_assigned_labels', 'al' . $index, $qb->expr()->eq('c.id', 'al' . $index . '.card_id'));
$qb->innerJoin('al'. $index, 'deck_labels', 'l' . $index, $qb->expr()->eq('al' . $index . '.label_id', 'l' . $index . '.id'));
$qb->andWhere($qb->expr()->iLike('l' . $index . '.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($tag->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
}
foreach ($query->getDuedate() as $duedate) {
$dueDateColumn = $this->databaseType === 'sqlite3' ? $qb->createFunction('DATETIME(`c`.`duedate`)') : 'c.duedate';
$date = $duedate->getValue();
$supportedFilters = ['overdue', 'today', 'week', 'month', 'none'];
if (in_array($date, $supportedFilters, true)) {
$currentDate = new DateTime();
$rangeDate = new DateTime();
if ($date === 'overdue') {
$qb->andWhere($qb->expr()->lt($dueDateColumn, $this->dateTimeParameter($qb, $currentDate)));
} elseif ($date === 'today') {
$rangeDate = $rangeDate->add(new \DateInterval('P1D'));
$qb->andWhere($qb->expr()->gte($dueDateColumn, $this->dateTimeParameter($qb, $currentDate)));
$qb->andWhere($qb->expr()->lte($dueDateColumn, $this->dateTimeParameter($qb, $rangeDate)));
} elseif ($date === 'week') {
$rangeDate = $rangeDate->add(new \DateInterval('P7D'));
$qb->andWhere($qb->expr()->gte($dueDateColumn, $this->dateTimeParameter($qb, $currentDate)));
$qb->andWhere($qb->expr()->lte($dueDateColumn, $this->dateTimeParameter($qb, $rangeDate)));
} elseif ($date === 'month') {
$rangeDate = $rangeDate->add(new \DateInterval('P1M'));
$qb->andWhere($qb->expr()->gte($dueDateColumn, $this->dateTimeParameter($qb, $currentDate)));
$qb->andWhere($qb->expr()->lte($dueDateColumn, $this->dateTimeParameter($qb, $rangeDate)));
} else {
$qb->andWhere($qb->expr()->isNull('c.duedate'));
}
} else {
try {
$date = new DateTime($date);
if ($duedate->getComparator() === SearchQuery::COMPARATOR_LESS) {
$qb->andWhere($qb->expr()->lt($dueDateColumn, $this->dateTimeParameter($qb, $date)));
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_LESS_EQUAL) {
// take the end of the day to include due dates at the same day (as datetime does't allow just setting the day)
$date->setTime(23, 59, 59);
$qb->andWhere($qb->expr()->lte($dueDateColumn, $this->dateTimeParameter($qb, $date)));
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_MORE) {
// take the end of the day to exclude due dates at the same day (as datetime does't allow just setting the day)
$date->setTime(23, 59, 59);
$qb->andWhere($qb->expr()->gt($dueDateColumn, $this->dateTimeParameter($qb, $date)));
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_MORE_EQUAL) {
$qb->andWhere($qb->expr()->gte($dueDateColumn, $this->dateTimeParameter($qb, $date)));
}
} catch (Exception $e) {
// Invalid date, ignoring
}
}
}
if (count($query->getAssigned()) > 0) {
foreach ($query->getAssigned() as $index => $assignment) {
$qb->innerJoin('c', 'deck_assigned_users', 'au' . $index, $qb->expr()->eq('c.id', 'au' . $index . '.card_id'));
$assignedQueryValue = $assignment->getValue();
$searchUsers = $this->userManager->searchDisplayName($assignment->getValue());
$users = array_filter($searchUsers, function (IUser $user) use ($assignedQueryValue) {
return (mb_strtolower($user->getDisplayName()) === mb_strtolower($assignedQueryValue) || $user->getUID() === $assignedQueryValue);
});
$groups = $this->groupManager->search($assignment->getValue());
foreach ($searchUsers as $user) {
$groups = array_merge($groups, $this->groupManager->getUserIdGroups($user->getUID()));
}
$assignmentSearches = [];
$hasAssignedMatches = false;
foreach ($users as $user) {
$hasAssignedMatches = true;
$assignmentSearches[] = $qb->expr()->andX(
$qb->expr()->eq('au' . $index . '.participant', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('au' . $index . '.type', $qb->createNamedParameter(Assignment::TYPE_USER, IQueryBuilder::PARAM_INT))
);
}
foreach ($groups as $group) {
$hasAssignedMatches = true;
$assignmentSearches[] = $qb->expr()->andX(
$qb->expr()->eq('au' . $index . '.participant', $qb->createNamedParameter($group->getGID(), IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('au' . $index . '.type', $qb->createNamedParameter(Assignment::TYPE_GROUP, IQueryBuilder::PARAM_INT))
);
}
if (!$hasAssignedMatches) {
return [];
}
$qb->andWhere($qb->expr()->orX(...$assignmentSearches));
}
}
}
private function dateTimeParameter(IQueryBuilder $qb, DateTime $dateTime) {
if ($this->databaseType === 'sqlite3') {
return $qb->createFunction('DATETIME("' . $dateTime->format('Y-m-d\TH:i:s') . '")');
}
return $qb->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATE);
}
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoards($boardIds)
->select('s.board_id', 'board_id')
->selectAlias('s.title', 'stack_title');
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere(
$qb->expr()->orX(

View File

@@ -1,6 +1,6 @@
<?php
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
@@ -21,31 +21,24 @@
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
use OCA\Deck\Db\Acl;
use OCP\EventDispatcher\Event;
/**
* This is a class to keep compatibility for currently used events in full text search integration
*/
class FTSEvent extends Event {
/**
* @var array
*/
private $arguments;
public function __construct($subject, $arguments = []) {
abstract class AAclEvent extends Event {
private $acl;
public function __construct(Acl $acl) {
parent::__construct();
$this->arguments = $arguments;
$this->acl = $acl;
}
public function getArgument($key) {
if (isset($this->arguments[$key])) {
return $this->arguments[$key];
}
throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key));
public function getAcl(): Acl {
return $this->acl;
}
}

44
lib/Event/ACardEvent.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
use OCA\Deck\Db\Card;
use OCP\EventDispatcher\Event;
abstract class ACardEvent extends Event {
private $card;
public function __construct(Card $card) {
parent::__construct();
$this->card = $card;
}
public function getCard(): Card {
return $this->card;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace OCA\Deck\Event;
class AclCreatedEvent extends AAclEvent {
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
class AclDeletedEvent extends AAclEvent {
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
class AclUpdatedEvent extends AAclEvent {
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
class CardCreatedEvent extends ACardEvent {
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
class CardDeletedEvent extends ACardEvent {
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
class CardUpdatedEvent extends ACardEvent {
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Listeners;
use OCA\Deck\Db\Card;
use OCA\Deck\Event\AAclEvent;
use OCA\Deck\Event\ACardEvent;
use OCA\Deck\Event\CardCreatedEvent;
use OCA\Deck\Event\CardDeletedEvent;
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\Provider\DeckProvider;
use OCA\Deck\Service\FullTextSearchService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\FullTextSearch\Exceptions\FullTextSearchAppNotAvailableException;
use OCP\FullTextSearch\IFullTextSearchManager;
use OCP\FullTextSearch\Model\IIndex;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
class FullTextSearchEventListener implements IEventListener {
/** @var string|null */
private $userId;
/** @var IFullTextSearchManager|null */
private $manager;
/** @var FullTextSearchService|null */
private $service;
/** @var LoggerInterface */
private $logger;
public function __construct(ContainerInterface $container, $userId) {
$this->userId = $userId;
$this->logger = $container->get(LoggerInterface::class);
try {
$this->manager = $container->get(IFullTextSearchManager::class);
$this->service = $container->get(FullTextSearchService::class);
} catch (\Exception $e) {
// skipping in case FTS is not available
}
}
public function handle(Event $event): void {
if (!$event instanceof ACardEvent && !$event instanceof AAclEvent) {
return;
}
try {
if ($event instanceof CardCreatedEvent) {
$this->manager->createIndex(
DeckProvider::DECK_PROVIDER_ID, (string)$event->getCard()->getId(), $this->userId
);
}
if ($event instanceof CardUpdatedEvent) {
$this->manager->updateIndexStatus(
DeckProvider::DECK_PROVIDER_ID, (string)$event->getCard()->getId(), IIndex::INDEX_CONTENT
);
}
if ($event instanceof CardDeletedEvent) {
$this->manager->updateIndexStatus(
DeckProvider::DECK_PROVIDER_ID, (string)$event->getCard()->getId(), IIndex::INDEX_REMOVE
);
}
if ($event instanceof AAclEvent) {
$acl = $event->getAcl();
$cards = array_map(
static function (Card $card) {
return (string)$card->getId();
},
$this->service->getCardsFromBoard($acl->getBoardId())
);
$this->manager->updateIndexesStatus(
DeckProvider::DECK_PROVIDER_ID, $cards, IIndex::INDEX_META
);
}
} catch (FullTextSearchAppNotAvailableException $e) {
// Skip silently if no full text search app is available
} catch (\Exception $e) {
$this->logger->error('Error when handling deck full text search event', ['exception' => $e]);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search;
use OCA\Deck\Service\SearchService;
use OCP\IL10N;
use OCP\IUser;
use OCP\Search\IProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
class CardCommentProvider implements IProvider {
/** @var SearchService */
private $searchService;
/** @var IL10N */
private $l10n;
public function __construct(
SearchService $searchService,
IL10N $l10n
) {
$this->searchService = $searchService;
$this->l10n = $l10n;
}
public function getId(): string {
return 'deck-comment';
}
public function getName(): string {
return $this->l10n->t('Card comments');
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
$cursor = $query->getCursor() !== null ? (int)$query->getCursor() : null;
$results = $this->searchService->searchComments($query->getTerm(), $query->getLimit(), $cursor);
if (count($results) < $query->getLimit()) {
return SearchResult::complete(
$this->l10n->t('Card comments'),
$results
);
}
return SearchResult::paginated(
$this->l10n->t('Card comments'),
$results,
$results[count($results) - 1]->getCommentId()
);
}
public function getOrder(string $route, array $routeParameters): int {
// Negative value to force showing deck providers on first position if the app is opened
// This provider always has an order 1 higher than the default DeckProvider
if ($route === 'deck.Page.index') {
return -4;
}
return 11;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search;
use OCA\Deck\Db\Card;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Search\SearchResultEntry;
class CommentSearchResultEntry extends SearchResultEntry {
private $commentId;
public function __construct(string $commentId, string $commentMessage, string $commentAuthor, Card $card, IURLGenerator $urlGenerator, IL10N $l10n) {
parent::__construct(
'',
// TRANSLATORS This is describing the author and card title related to a comment e.g. "Jane on MyTask"
$l10n->t('%s on %s', [$commentAuthor, $card->getTitle()]),
$commentMessage,
$urlGenerator->linkToRouteAbsolute('deck.page.index') . '#/board/' . $card->getRelatedBoard()->getId() . '/card/' . $card->getId() . '/comments/' . $commentId, // $commentId
'icon-comment');
$this->commentId = $commentId;
}
public function getCommentId(): string {
return $this->commentId;
}
}

View File

@@ -28,9 +28,7 @@ namespace OCA\Deck\Search;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\SearchService;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Search\IProvider;
@@ -40,31 +38,19 @@ use OCP\Search\SearchResult;
class DeckProvider implements IProvider {
/**
* @var BoardService
* @var SearchService
*/
private $boardService;
/**
* @var CardMapper
*/
private $cardMapper;
/**
* @var StackMapper
*/
private $stackMapper;
private $searchService;
/**
* @var IURLGenerator
*/
private $urlGenerator;
public function __construct(
BoardService $boardService,
StackMapper $stackMapper,
CardMapper $cardMapper,
SearchService $searchService,
IURLGenerator $urlGenerator
) {
$this->boardService = $boardService;
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
$this->searchService = $searchService;
$this->urlGenerator = $urlGenerator;
}
@@ -77,37 +63,34 @@ class DeckProvider implements IProvider {
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
$boards = $this->boardService->getUserBoards();
$matchedBoards = array_filter($this->boardService->getUserBoards(), static function (Board $board) use ($query) {
return mb_stripos($board->getTitle(), $query->getTerm()) > -1;
});
$matchedCards = $this->cardMapper->search(array_map(static function (Board $board) {
return $board->getId();
}, $boards), $query->getTerm(), $query->getLimit(), $query->getCursor());
$self = $this;
$cursor = $query->getCursor() !== null ? (int)$query->getCursor() : null;
$boardResults = $this->searchService->searchBoards($query->getTerm(), $query->getLimit(), $cursor);
$cardResults = $this->searchService->searchCards($query->getTerm(), $query->getLimit(), $cursor);
$results = array_merge(
array_map(function (Board $board) {
return new BoardSearchResultEntry($board, $this->urlGenerator);
}, $matchedBoards),
array_map(function (Card $card) use ($self) {
$board = $self->boardService->find($self->cardMapper->findBoardId($card->getId()));
$stack = $self->stackMapper->find($card->getStackId());
return new CardSearchResultEntry($board, $stack, $card, $this->urlGenerator);
}, $matchedCards)
}, $boardResults),
array_map(function (Card $card) {
return new CardSearchResultEntry($card->getRelatedBoard(), $card->getRelatedStack(), $card, $this->urlGenerator);
}, $cardResults)
);
return SearchResult::complete(
if (count($cardResults) < $query->getLimit()) {
return SearchResult::complete(
'Deck',
$results
);
}
return SearchResult::paginated(
'Deck',
$results
$results,
$cardResults[count($results) - 1]->getLastModified()
);
}
public function getOrder(string $route, array $routeParameters): int {
if ($route === 'deck.page.index') {
if ($route === 'deck.Page.index') {
return -5;
}
return 10;

View File

@@ -0,0 +1,125 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search;
use OCA\Deck\Search\Query\DateQueryParameter;
use OCA\Deck\Search\Query\SearchQuery;
use OCA\Deck\Search\Query\StringQueryParameter;
use OCP\IL10N;
class FilterStringParser {
/**
* @var IL10N
*/
private $l10n;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
public function parse(?string $filter): SearchQuery {
$query = new SearchQuery();
if (empty($filter)) {
return $query;
}
/**
* Match search tokens that are separated by spaces
* do not match spaces that are surrounded by single or double quotes
* in order to still match quotes
* e.g.:
* - test
* - test:query
* - test:<123
* - test:"1 2 3"
* - test:>="2020-01-01"
*/
$searchQueryExpression = '/((\w+:(<|<=|>|>=)?)?("([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')|[^\s]+)/';
preg_match_all($searchQueryExpression, $filter, $matches, PREG_SET_ORDER, 0);
foreach ($matches as $match) {
$token = $match[0];
if (!$this->parseFilterToken($query, $token)) {
$query->addTextToken($this->removeQuotes($token));
}
}
return $query;
}
private function parseFilterToken(SearchQuery $query, string $token): bool {
if (strpos($token, ':') === false) {
return false;
}
[$type, $param] = explode(':', $token, 2);
$type = strtolower($type);
$qualifier = null;
switch ($type) {
case 'date':
$comparator = SearchQuery::COMPARATOR_EQUAL;
$value = $param;
if ($param[0] === '<' || $param[0] === '>') {
$orEquals = $param[1] === '=';
$value = $orEquals ? substr($param, 2) : substr($param, 1);
$comparator = (
($param[0] === '<' ? SearchQuery::COMPARATOR_LESS : 0) |
($param[0] === '>' ? SearchQuery::COMPARATOR_MORE : 0) |
($orEquals ? SearchQuery::COMPARATOR_EQUAL : 0)
);
}
$query->addDuedate(new DateQueryParameter('date', $comparator, $this->removeQuotes($value)));
return true;
case 'title':
$query->addTitle(new StringQueryParameter('title', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'description':
$query->addDescription(new StringQueryParameter('description', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'list':
$query->addStack(new StringQueryParameter('list', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'tag':
$query->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
case 'assigned':
$query->addAssigned(new StringQueryParameter('assigned', SearchQuery::COMPARATOR_EQUAL, $this->removeQuotes($param)));
return true;
}
return false;
}
protected function removeQuotes(string $token): string {
if (mb_strlen($token) > 1) {
$token = ($token[0] === '"' && $token[mb_strlen($token) - 1] === '"') ? mb_substr($token, 1, -1) : $token;
$token = ($token[0] === '\'' && $token[mb_strlen($token) - 1] === '\'') ? mb_substr($token, 1, -1) : $token;
}
return $token;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search\Query;
class AQueryParameter {
/** @var string */
protected $field;
/** @var int */
protected $comparator;
/** @var mixed */
protected $value;
public function getValue() {
if (is_string($this->value) && mb_strlen($this->value) > 1) {
$param = ($this->value[0] === '"' && $this->value[mb_strlen($this->value) - 1] === '"') ? mb_substr($this->value, 1, -1): $this->value;
return $param;
}
return $this->value;
}
public function getComparator(): int {
return $this->comparator;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search\Query;
class DateQueryParameter extends AQueryParameter {
/** @var string|null */
protected $value;
public function __construct(string $field, int $comparator, ?string $value) {
$this->field = $field;
$this->comparator = $comparator;
$this->value = $value;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search\Query;
class SearchQuery {
public const COMPARATOR_EQUAL = 1;
public const COMPARATOR_LESS = 2;
public const COMPARATOR_MORE = 4;
public const COMPARATOR_LESS_EQUAL = 3;
public const COMPARATOR_MORE_EQUAL = 5;
/** @var string[] */
private $textTokens = [];
/** @var StringQueryParameter[] */
private $title = [];
/** @var StringQueryParameter[] */
private $description = [];
/** @var StringQueryParameter[] */
private $stack = [];
/** @var StringQueryParameter[] */
private $tag = [];
/** @var StringQueryParameter[] */
private $assigned = [];
/** @var DateQueryParameter[] */
private $duedate = [];
public function addTextToken(string $textToken): void {
$this->textTokens[] = $textToken;
}
public function getTextTokens(): array {
return $this->textTokens;
}
public function addTitle(StringQueryParameter $title): void {
$this->title[] = $title;
}
public function getTitle(): array {
return $this->title;
}
public function addDescription(StringQueryParameter $description): void {
$this->description[] = $description;
}
public function getDescription(): array {
return $this->description;
}
public function addStack(StringQueryParameter $stack): void {
$this->stack[] = $stack;
}
public function getStack(): array {
return $this->stack;
}
public function addTag(StringQueryParameter $tag): void {
$this->tag[] = $tag;
}
public function getTag(): array {
return $this->tag;
}
public function addAssigned(StringQueryParameter $assigned): void {
$this->assigned[] = $assigned;
}
public function getAssigned(): array {
return $this->assigned;
}
public function addDuedate(DateQueryParameter $date): void {
$this->duedate[] = $date;
}
public function getDuedate(): array {
return $this->duedate;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search\Query;
class StringQueryParameter extends AQueryParameter {
/** @var string */
protected $value;
public function __construct(string $field, int $comparator, string $value) {
$this->field = $field;
$this->comparator = $comparator;
$this->value = $value;
}
}

View File

@@ -31,7 +31,7 @@ use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Event\FTSEvent;
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCA\Deck\Notification\NotificationHelper;
@@ -151,9 +151,7 @@ class AssignmentService {
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_ASSIGN, ['assigneduser' => $userId]);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $assignment;
}
@@ -187,9 +185,7 @@ class AssignmentService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_UNASSIGN, ['assigneduser' => $userId]);
$this->changeHelper->cardChanged($cardId);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $assignment;
}

View File

@@ -24,7 +24,6 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\AppInfo\Application;
@@ -36,9 +35,13 @@ use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\AclCreatedEvent;
use OCA\Deck\Event\AclDeletedEvent;
use OCA\Deck\Event\AclUpdatedEvent;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Notification\NotificationHelper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
@@ -47,7 +50,6 @@ use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\LabelMapper;
use OCP\IUserManager;
use OCA\Deck\BadRequestException;
use Symfony\Component\EventDispatcher\GenericEvent;
class BoardService {
private $boardMapper;
@@ -83,7 +85,7 @@ class BoardService {
IUserManager $userManager,
IGroupManager $groupManager,
ActivityManager $activityManager,
SymfonyAdapter $eventDispatcher,
IEventDispatcher $eventDispatcher,
ChangeHelper $changeHelper,
$userId
) {
@@ -327,13 +329,6 @@ class BoardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $new_board, ActivityManager::SUBJECT_BOARD_CREATE, [], $userId);
$this->changeHelper->boardChanged($new_board->getId());
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onCreate',
new GenericEvent(
null, ['id' => $new_board->getId(), 'userId' => $userId, 'board' => $new_board]
)
);
return $new_board;
}
@@ -360,10 +355,6 @@ class BoardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $board, ActivityManager::SUBJECT_BOARD_DELETE);
$this->changeHelper->boardChanged($board->getId());
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onDelete', new GenericEvent(null, ['id' => $id])
);
return $board;
}
@@ -386,10 +377,6 @@ class BoardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $board, ActivityManager::SUBJECT_BOARD_RESTORE);
$this->changeHelper->boardChanged($board->getId());
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onUpdate', new GenericEvent(null, ['id' => $id, 'board' => $board])
);
return $board;
}
@@ -410,10 +397,6 @@ class BoardService {
$board = $this->find($id);
$delete = $this->boardMapper->delete($board);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onDelete', new GenericEvent(null, ['id' => $id])
);
return $delete;
}
@@ -457,10 +440,6 @@ class BoardService {
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_BOARD, $changes, ActivityManager::SUBJECT_BOARD_UPDATE);
$this->changeHelper->boardChanged($board->getId());
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onUpdate', new GenericEvent(null, ['id' => $id, 'board' => $board])
);
return $board;
}
@@ -532,7 +511,6 @@ class BoardService {
$acl->setPermissionShare($share);
$acl->setPermissionManage($manage);
/* Notify users about the shared board */
$this->notificationHelper->sendBoardShared($boardId, $acl);
$newAcl = $this->aclMapper->insert($acl);
@@ -541,18 +519,13 @@ class BoardService {
$this->changeHelper->boardChanged($boardId);
// TODO: use the dispatched event for this
$version = \OCP\Util::getVersion()[0];
if ($version >= 16) {
try {
$resourceProvider = \OC::$server->query(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
$resourceProvider->invalidateAccessCache($boardId);
} catch (\Exception $e) {
}
try {
$resourceProvider = \OC::$server->query(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
$resourceProvider->invalidateAccessCache($boardId);
} catch (\Exception $e) {
}
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onShareNew', new GenericEvent(null, ['id' => $newAcl->getId(), 'acl' => $newAcl, 'boardId' => $boardId])
);
$this->eventDispatcher->dispatchTyped(new AclCreatedEvent($acl));
return $newAcl;
}
@@ -597,9 +570,7 @@ class BoardService {
$board = $this->aclMapper->update($acl);
$this->changeHelper->boardChanged($acl->getBoardId());
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onShareEdit', new GenericEvent(null, ['id' => $id, 'boardId' => $acl->getBoardId(), 'acl' => $acl])
);
$this->eventDispatcher->dispatchTyped(new AclUpdatedEvent($acl));
return $board;
}
@@ -627,6 +598,9 @@ class BoardService {
$this->assignedUsersMapper->delete($assignement);
}
}
$this->notificationHelper->sendBoardShared($acl->getBoardId(), $acl);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
$this->changeHelper->boardChanged($acl->getBoardId());
@@ -640,9 +614,7 @@ class BoardService {
}
$delete = $this->aclMapper->delete($acl);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onShareDelete', new GenericEvent(null, ['id' => $id, 'boardId' => $acl->getBoardId(), 'acl' => $acl])
);
$this->eventDispatcher->dispatchTyped(new AclDeletedEvent($acl));
return $delete;
}

View File

@@ -29,13 +29,14 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\FTSEvent;
use OCA\Deck\Event\CardCreatedEvent;
use OCA\Deck\Event\CardDeletedEvent;
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\Notification\NotificationHelper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\LabelMapper;
@@ -106,6 +107,11 @@ class CardService {
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
$card->setCommentsUnread($count);
$stack = $this->stackMapper->find($card->getStackId());
$board = $this->boardService->find($stack->getBoardId());
$card->setRelatedStack($stack);
$card->setRelatedBoard($board);
}
public function fetchDeleted($boardId) {
@@ -117,22 +123,6 @@ class CardService {
return $cards;
}
public function search(string $term, int $limit = null, int $offset = null): array {
$boards = $this->boardService->getUserBoards();
$boardIds = array_map(static function (Board $board) {
return $board->getId();
}, $boards);
return $this->cardMapper->search($boardIds, $term, $limit, $offset);
}
public function searchRaw(string $term, int $limit = null, int $offset = null): array {
$boards = $this->boardService->getUserBoards();
$boardIds = array_map(static function (Board $board) {
return $board->getId();
}, $boards);
return $this->cardMapper->searchRaw($boardIds, $term, $limit, $offset);
}
/**
* @param $cardId
* @return \OCA\Deck\Db\RelationalEntity
@@ -222,15 +212,10 @@ class CardService {
$card->setDescription($description);
$card->setDuedate($duedate);
$card = $this->cardMapper->insert($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE);
$this->changeHelper->cardChanged($card->getId(), false);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onCreate',
new FTSEvent(
null, ['id' => $card->getId(), 'card' => $card, 'userId' => $owner, 'stackId' => $stackId]
)
);
$this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card));
return $card;
}
@@ -256,12 +241,10 @@ class CardService {
$card = $this->cardMapper->find($id);
$card->setDeletedAt(time());
$this->cardMapper->update($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
$this->changeHelper->cardChanged($card->getId(), false);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onDelete', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardDeletedEvent($card));
return $card;
}
@@ -360,9 +343,7 @@ class CardService {
$card = $this->cardMapper->update($card);
$this->changeHelper->cardChanged($card->getId(), true);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $card;
}
@@ -402,9 +383,7 @@ class CardService {
$this->changeHelper->cardChanged($card->getId(), false);
$update = $this->cardMapper->update($card);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $update;
}
@@ -501,9 +480,7 @@ class CardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_ARCHIVE);
$this->changeHelper->cardChanged($id, false);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $newCard;
}
@@ -532,9 +509,7 @@ class CardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_UNARCHIVE);
$this->changeHelper->cardChanged($id, false);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return $newCard;
}
@@ -570,9 +545,7 @@ class CardService {
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
}
/**
@@ -606,9 +579,7 @@ class CardService {
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]);
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
}
/**

View File

@@ -27,10 +27,9 @@ use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\StatusException;
use OCA\Deck\Exceptions\ConflictException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
@@ -49,6 +48,7 @@ class FileService implements IAttachmentService {
private $rootFolder;
private $config;
private $attachmentMapper;
private $mimeTypeDetector;
public function __construct(
IL10N $l10n,
@@ -57,7 +57,8 @@ class FileService implements IAttachmentService {
ILogger $logger,
IRootFolder $rootFolder,
IConfig $config,
AttachmentMapper $attachmentMapper
AttachmentMapper $attachmentMapper,
IMimeTypeDetector $mimeTypeDetector
) {
$this->l10n = $l10n;
$this->appData = $appData;
@@ -66,6 +67,7 @@ class FileService implements IAttachmentService {
$this->rootFolder = $rootFolder;
$this->config = $config;
$this->attachmentMapper = $attachmentMapper;
$this->mimeTypeDetector = $mimeTypeDetector;
}
/**
@@ -225,27 +227,14 @@ class FileService implements IAttachmentService {
/**
* @param Attachment $attachment
* @return FileDisplayResponse|\OCP\AppFramework\Http\Response|StreamResponse
* @return StreamResponse
* @throws \Exception
*/
public function display(Attachment $attachment) {
$file = $this->getFileFromRootFolder($attachment);
if (method_exists($file, 'fopen')) {
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
} else {
$response = new FileDisplayResponse($file);
}
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($policy);
$response->addHeader('Content-Type', $file->getMimeType());
$response = new StreamResponse($file->fopen('rb'));
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
return $response;
}

View File

@@ -26,17 +26,16 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Constants;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\Share;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
@@ -50,6 +49,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $l10n;
private $preview;
private $permissionService;
private $mimeTypeDetector;
public function __construct(
IRequest $request,
@@ -60,6 +60,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
DeckShareProvider $shareProvider,
IPreview $preview,
PermissionService $permissionService,
IMimeTypeDetector $mimeTypeDetector,
string $userId = null
) {
$this->request = $request;
@@ -70,6 +71,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$this->shareManager = $shareManager;
$this->userId = $userId;
$this->preview = $preview;
$this->mimeTypeDetector = $mimeTypeDetector;
}
public function listAttachments(int $cardId): array {
@@ -138,31 +140,20 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
}
public function display(Attachment $attachment) {
/** @psalm-suppress InvalidCatch */
try {
$share = $this->shareProvider->getShareById($attachment->getId());
} catch (Share\Exceptions\ShareNotFound $e) {
} catch (ShareNotFound $e) {
throw new NotFoundException('File not found');
}
$file = $share->getNode();
if ($file === null || $share->getSharedWith() !== (string)$attachment->getCardId()) {
throw new NotFoundException('File not found');
}
if (method_exists($file, 'fopen')) {
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
} else {
$response = new FileDisplayResponse($file);
}
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($policy);
$response->addHeader('Content-Type', $file->getMimeType());
$response = new StreamResponse($file->fopen('rb'));
$response->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$response->addHeader('Content-Type', $this->mimeTypeDetector->getSecureMimeType($file->getMimeType()));
return $response;
}
@@ -184,7 +175,6 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
throw new StatusException('Could not read file');
}
$target->putContent($content);
fclose($content);
$share = $this->shareManager->newShare();
$share->setNode($target);

View File

@@ -37,14 +37,10 @@ use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\FTSEvent;
use OCA\Deck\Provider\DeckProvider;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\FullTextSearch\Exceptions\FullTextSearchAppNotAvailableException;
use OCP\FullTextSearch\IFullTextSearchManager;
use OCP\FullTextSearch\Model\IDocumentAccess;
use OCP\FullTextSearch\Model\IIndex;
use OCP\FullTextSearch\Model\IIndexDocument;
/**
@@ -63,98 +59,15 @@ class FullTextSearchService {
/** @var CardMapper */
private $cardMapper;
/** @var IFullTextSearchManager */
private $fullTextSearchManager;
/**
* FullTextSearchService constructor.
*
* @param BoardMapper $boardMapper
* @param StackMapper $stackMapper
* @param CardMapper $cardMapper
* @param IFullTextSearchManager $fullTextSearchManager
*/
public function __construct(
BoardMapper $boardMapper, StackMapper $stackMapper, CardMapper $cardMapper,
IFullTextSearchManager $fullTextSearchManager
BoardMapper $boardMapper, StackMapper $stackMapper, CardMapper $cardMapper
) {
$this->boardMapper = $boardMapper;
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
$this->fullTextSearchManager = $fullTextSearchManager;
}
/**
* @param FTSEvent $e
*/
public function onCardCreated(FTSEvent $e) {
$cardId = $e->getArgument('id');
$userId = $e->getArgument('userId');
try {
$this->fullTextSearchManager->createIndex(
DeckProvider::DECK_PROVIDER_ID, (string)$cardId, $userId, IIndex::INDEX_FULL
);
} catch (FullTextSearchAppNotAvailableException $e) {
}
}
/**
* @param FTSEvent $e
*/
public function onCardUpdated(FTSEvent $e) {
$cardId = $e->getArgument('id');
try {
$this->fullTextSearchManager->updateIndexStatus(
DeckProvider::DECK_PROVIDER_ID, (string)$cardId, IIndex::INDEX_CONTENT
);
} catch (FullTextSearchAppNotAvailableException $e) {
}
}
/**
* @param FTSEvent $e
*/
public function onCardDeleted(FTSEvent $e) {
$cardId = $e->getArgument('id');
try {
$this->fullTextSearchManager->updateIndexStatus(
DeckProvider::DECK_PROVIDER_ID, (string)$cardId, IIndex::INDEX_REMOVE
);
} catch (FullTextSearchAppNotAvailableException $e) {
}
}
/**
* @param FTSEvent $e
*/
public function onBoardShares(FTSEvent $e) {
$boardId = (int)$e->getArgument('boardId');
$cards = array_map(
function (Card $item) {
return $item->getId();
},
$this->getCardsFromBoard($boardId)
);
try {
$this->fullTextSearchManager->updateIndexesStatus(
DeckProvider::DECK_PROVIDER_ID, $cards, IIndex::INDEX_META
);
} catch (FullTextSearchAppNotAvailableException $e) {
}
}
/**
* @param Card $card
*
@@ -175,11 +88,9 @@ class FullTextSearchService {
* @throws MultipleObjectsReturnedException
*/
public function fillIndexDocument(IIndexDocument $document) {
/** @var Card $card */
$card = $this->cardMapper->find((int)$document->getId());
$document->setTitle(($card->getTitle() === null) ? '' : $card->getTitle());
$document->setContent(($card->getDescription() === null) ? '' : $card->getDescription());
$document->setTitle(!empty($card->getTitle()) ? $card->getTitle() : '');
$document->setContent(!empty($card->getDescription()) ? $card->getDescription() : '');
$document->setAccess($this->generateDocumentAccessFromCardId((int)$card->getId()));
}

View File

@@ -0,0 +1,117 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Service;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Search\CommentSearchResultEntry;
use OCA\Deck\Search\FilterStringParser;
use OCP\Comments\ICommentsManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
class SearchService {
/** @var BoardService */
private $boardService;
/** @var CardMapper */
private $cardMapper;
/** @var CardService */
private $cardService;
/** @var ICommentsManager */
private $commentsManager;
/** @var FilterStringParser */
private $filterStringParser;
/** @var IUserManager */
private $userManager;
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(
BoardService $boardService,
CardMapper $cardMapper,
CardService $cardService,
ICommentsManager $commentsManager,
FilterStringParser $filterStringParser,
IUserManager $userManager,
IL10N $l10n,
IURLGenerator $urlGenerator
) {
$this->boardService = $boardService;
$this->cardMapper = $cardMapper;
$this->cardService = $cardService;
$this->commentsManager = $commentsManager;
$this->filterStringParser = $filterStringParser;
$this->userManager = $userManager;
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
}
public function searchCards(string $term, int $limit = null, ?int $cursor = null): array {
$boards = $this->boardService->getUserBoards();
$boardIds = array_map(static function (Board $board) {
return $board->getId();
}, $boards);
$matchedCards = $this->cardMapper->search($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
$self = $this;
return array_map(function (Card $card) use ($self) {
$self->cardService->enrich($card);
return $card;
}, $matchedCards);
}
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
$boards = $this->boardService->getUserBoards();
return array_filter($boards, static function (Board $board) use ($term) {
return mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1;
});
}
public function searchComments(string $term, ?int $limit = null, ?int $cursor = null): array {
$boards = $this->boardService->getUserBoards();
$boardIds = array_map(static function (Board $board) {
return $board->getId();
}, $boards);
$matchedComments = $this->cardMapper->searchComments($boardIds, $this->filterStringParser->parse($term), $limit, $cursor);
$self = $this;
return array_map(function ($cardRow) use ($self) {
$comment = $this->commentsManager->get($cardRow['comment_id']);
unset($cardRow['comment_id']);
$card = Card::fromRow($cardRow);
$self->cardService->enrich($card);
$user = $this->userManager->get($comment->getActorId());
$displayName = $user ? $user->getDisplayName() : '';
return new CommentSearchResultEntry($comment->getId(), $comment->getMessage(), $displayName, $card, $this->urlGenerator, $this->l10n);
}, $matchedComments);
}
}

View File

@@ -24,7 +24,6 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\BadRequestException;
@@ -37,7 +36,6 @@ use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\StatusException;
use Symfony\Component\EventDispatcher\GenericEvent;
class StackService {
private $stackMapper;
@@ -64,7 +62,6 @@ class StackService {
AssignmentMapper $assignedUsersMapper,
AttachmentService $attachmentService,
ActivityManager $activityManager,
SymfonyAdapter $eventDispatcher,
ChangeHelper $changeHelper
) {
$this->stackMapper = $stackMapper;
@@ -77,7 +74,6 @@ class StackService {
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
$this->activityManager = $activityManager;
$this->symfonyAdapter = $eventDispatcher;
$this->changeHelper = $changeHelper;
}
@@ -225,11 +221,6 @@ class StackService {
);
$this->changeHelper->boardChanged($boardId);
$this->symfonyAdapter->dispatch(
'\OCA\Deck\Stack::onCreate',
new GenericEvent(null, ['id' => $stack->getId(), 'stack' => $stack])
);
return $stack;
}
@@ -259,10 +250,6 @@ class StackService {
$this->changeHelper->boardChanged($stack->getBoardId());
$this->enrichStackWithCards($stack);
$this->symfonyAdapter->dispatch(
'\OCA\Deck\Stack::onDelete', new GenericEvent(null, ['id' => $id, 'stack' => $stack])
);
return $stack;
}
@@ -314,10 +301,6 @@ class StackService {
);
$this->changeHelper->boardChanged($stack->getBoardId());
$this->symfonyAdapter->dispatch(
'\OCA\Deck\Stack::onUpdate', new GenericEvent(null, ['id' => $id, 'stack' => $stack])
);
return $stack;
}

View File

@@ -562,6 +562,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
/**
* @inheritDoc
* @throws ShareNotFound
*/
public function getShareById($id, $recipientId = null) {
$qb = $this->dbConnection->getQueryBuilder();

1512
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.13.8",
"@babel/runtime": "^7.13.10",
"@juliushaertl/vue-richtext": "^1.0.1",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.6.0",
@@ -40,21 +40,21 @@
"@nextcloud/l10n": "^1.4.1",
"@nextcloud/moment": "^1.1.1",
"@nextcloud/router": "^1.2.0",
"@nextcloud/vue": "^3.6.0",
"@nextcloud/vue-dashboard": "^1.0.1",
"@nextcloud/vue": "^3.8.0",
"@nextcloud/vue-dashboard": "^1.1.0",
"blueimp-md5": "^2.18.0",
"dompurify": "^2.2.6",
"dompurify": "^2.2.7",
"lodash": "^4.17.21",
"markdown-it": "^12.0.4",
"markdown-it-task-lists": "^2.1.1",
"moment": "^2.29.1",
"nextcloud-vue-collections": "^0.9.0",
"p-queue": "^6.6.2",
"url-search-params-polyfill": "^8.1.0",
"url-search-params-polyfill": "^8.1.1",
"vue": "^2.6.12",
"vue-at": "^2.5.0-beta.2",
"vue-click-outside": "^1.1.0",
"vue-easymde": "^1.3.2",
"vue-easymde": "^1.4.0",
"vue-infinite-loading": "^2.4.5",
"vue-router": "^3.5.1",
"vue-smooth-dnd": "^0.8.1",
@@ -68,16 +68,16 @@
"node": ">=10.0.0"
},
"devDependencies": {
"@babel/core": "^7.13.8",
"@babel/core": "^7.13.14",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.13.8",
"@babel/preset-env": "^7.13.12",
"@nextcloud/browserslist-config": "^1.0.0",
"@nextcloud/eslint-config": "^2.2.0",
"@nextcloud/eslint-plugin": "^1.5.0",
"@nextcloud/webpack-vue-config": "^1.4.1",
"@relative-ci/agent": "^1.5.0",
"@vue/test-utils": "^1.1.3",
"acorn": "^8.0.5",
"acorn": "^8.1.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
@@ -88,7 +88,7 @@
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.2.0",
@@ -99,8 +99,8 @@
"raw-loader": "^4.0.2",
"sass-loader": "^10.1.1",
"style-loader": "^1.3.0",
"stylelint": "^13.11.0",
"stylelint-config-recommended": "^3.0.0",
"stylelint": "^13.12.0",
"stylelint-config-recommended": "^4.0.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.19.0",
"stylelint-webpack-plugin": "^2.1.1",

View File

@@ -33,8 +33,14 @@
({{ t('deck', 'Archived cards') }})
</p>
</div>
<div v-if="board" class="board-actions">
<div v-if="canManage && !showArchived && !board.archived"
<div class="board-actions">
<div v-if="searchQuery || true" class="deck-search">
<input type="search"
class="icon-search"
:value="searchQuery"
@input="$store.commit('setSearchQuery', $event.target.value)">
</div>
<div v-if="board && canManage && !showArchived && !board.archived"
id="stack-add"
v-click-outside="hideAddStack">
<Actions v-if="!isAddStackVisible">
@@ -57,7 +63,7 @@
value="">
</form>
</div>
<div class="board-action-buttons">
<div v-if="board" class="board-action-buttons">
<Popover @show="filterVisible=true" @hide="filterVisible=false">
<Actions slot="trigger" :title="t('deck', 'Apply filter')">
<ActionButton v-if="isFilterActive" icon="icon-filter_set" />
@@ -237,6 +243,7 @@ export default {
]),
...mapState({
compactMode: state => state.compactMode,
searchQuery: state => state.searchQuery,
}),
detailsRoute() {
return {
@@ -374,6 +381,13 @@ export default {
}
}
.deck-search {
input[type=search] {
background-position: 5px;
padding-left: 24px;
}
}
.filter--item {
input + label {
display: block;

View File

@@ -65,6 +65,7 @@
<p />
</div>
</transition>
<GlobalSearchResults />
</div>
</template>
@@ -75,10 +76,12 @@ import { mapState, mapGetters } from 'vuex'
import Controls from '../Controls'
import Stack from './Stack'
import { EmptyContent } from '@nextcloud/vue'
import GlobalSearchResults from '../search/GlobalSearchResults'
export default {
name: 'Board',
components: {
GlobalSearchResults,
Controls,
Container,
Draggable,
@@ -178,13 +181,17 @@ export default {
width: 100%;
height: 100%;
max-height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.board {
padding-left: $board-spacing;
position: relative;
height: calc(100% - 44px);
overflow-x: scroll;
max-height: calc(100% - 44px);
overflow: hidden;
overflow-x: auto;
flex-grow: 1;
}
/**

View File

@@ -22,6 +22,7 @@
<template>
<AppSidebar v-if="currentBoard && currentCard"
:active="tabId"
:title="title"
:subtitle="subtitle"
:title-editable="titleEditable"
@@ -65,7 +66,7 @@
:order="2"
:name="t('deck', 'Comments')"
icon="icon-comment">
<CardSidebarTabComments :card="currentCard" />
<CardSidebarTabComments :card="currentCard" :tab-query="tabQuery" />
</AppSidebarTab>
<AppSidebarTab v-if="hasActivity"
@@ -109,6 +110,16 @@ export default {
type: Number,
required: true,
},
tabId: {
type: String,
required: false,
default: null,
},
tabQuery: {
type: String,
required: false,
default: null,
},
},
data() {
return {

View File

@@ -49,6 +49,11 @@ export default {
type: Object,
default: undefined,
},
tabQuery: {
type: String,
required: false,
default: null,
},
},
data() {
return {

View File

@@ -26,12 +26,16 @@
<template>
<AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card">
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'is-editing': editing, 'card__editable': canEdit}"
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'is-editing': editing, 'card__editable': canEdit, 'card__archived': card.archived }"
tag="div"
class="card"
@click="openCard">
<div v-if="standalone" class="card-related">
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
{{ board.title }} » {{ stack.title }}
</div>
<div class="card-upper">
<h3 v-if="compactMode || isArchived || showArchived || !canEdit">
<h3 v-if="compactMode || isArchived || showArchived || !canEdit || standalone">
{{ card.title }}
</h3>
<h3 v-else-if="!editing">
@@ -98,6 +102,10 @@ export default {
type: Object,
default: null,
},
standalone: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -114,6 +122,12 @@ export default {
...mapGetters([
'isArchived',
]),
board() {
return this.$store.getters.boardById(this?.stack?.boardId)
},
stack() {
return this.$store.getters.stackById(this?.card?.stackId)
},
canEdit() {
if (this.currentBoard) {
return !this.currentBoard.archived && this.$store.getters.canEdit
@@ -233,6 +247,9 @@ export default {
&.card__editable .card-controls {
margin-right: 0;
}
&.card__archived {
background-color: var(--color-background-dark);
}
}
.duedate {
@@ -244,6 +261,24 @@ export default {
align-items: flex-start;
}
.card-related {
display: flex;
padding: 12px;
padding-bottom: 0px;
color: var(--color-text-maxcontrast);
.board-bullet {
display: inline-block;
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
background-color: transparent;
margin-top: 4px;
margin-right: 4px;
}
}
.compact {
min-height: 44px;

View File

@@ -23,7 +23,7 @@
<template>
<div v-if="card">
<div @click.stop.prevent>
<Actions v-if="canEdit && !isArchived">
<Actions>
<ActionButton v-if="showArchived === false && !isCurrentUserAssigned"
icon="icon-user"
:close-after-click="true"
@@ -43,7 +43,7 @@
{{ t('deck', 'Card details') }}
</ActionButton>
<ActionButton icon="icon-archive" :close-after-click="true" @click="archiveUnarchiveCard()">
{{ showArchived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
{{ card.archived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
</ActionButton>
<ActionButton v-if="showArchived === false"
icon="icon-delete"

View File

@@ -235,9 +235,7 @@ export default {
try {
const newBoard = await this.$store.dispatch('cloneBoard', this.board)
this.loading = false
const route = this.routeTo
route.params.id = newBoard.id
this.$router.push(route)
this.$router.push({ name: 'board', params: { id: newBoard.id } })
} catch (e) {
OC.Notification.showTemporary(t('deck', 'An error occurred'))
console.error(e)
@@ -278,9 +276,7 @@ export default {
)
},
actionDetails() {
const route = this.routeTo
route.name = 'board.details'
this.$router.push(route)
this.$router.push({ name: 'board.details', params: { id: this.board.id } })
},
applyEdit(e) {
this.editing = false
@@ -298,11 +294,6 @@ export default {
cancelEdit(e) {
this.editing = false
},
showSidebar() {
const route = this.routeTo
route.name = 'board.details'
this.$router.push(route)
},
async updateSetting(key, value) {
this.updateDueSetting = value
const setting = {}

View File

@@ -73,6 +73,8 @@
</div>
</div>
</div>
<GlobalSearchResults />
</div>
</template>
@@ -82,6 +84,7 @@ import Controls from '../Controls'
import CardItem from '../cards/CardItem'
import { mapGetters } from 'vuex'
import moment from '@nextcloud/moment'
import GlobalSearchResults from '../search/GlobalSearchResults'
const FILTER_UPCOMING = 'upcoming'
@@ -92,6 +95,7 @@ const SUPPORTED_FILTERS = [
export default {
name: 'Overview',
components: {
GlobalSearchResults,
Controls,
CardItem,
},
@@ -203,6 +207,8 @@ export default {
width: 100%;
height: 100%;
max-height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.overview {

View File

@@ -0,0 +1,199 @@
<!--
- @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div v-if="searchQuery!==''" class="global-search">
<h2><RichText :text="t('deck', 'Search for {searchQuery} in all boards')" :arguments="queryStringArgs" /></h2>
<Actions>
<ActionButton icon="icon-close" @click="$store.commit('setSearchQuery', '')" />
</Actions>
<div class="search-wrapper">
<div v-if="loading || filteredResults.length > 0" class="search-results">
<CardItem v-for="card in filteredResults"
:id="card.id"
:key="card.id"
:standalone="true" />
<Placeholder v-if="loading" />
<InfiniteLoading :identifier="searchQuery" @infinite="infiniteHandler">
<div slot="spinner" />
<div slot="no-more" />
<div slot="no-results">
{{ t('deck', 'No results found') }}
</div>
</InfiniteLoading>
</div>
<div v-else>
<p>{{ t('deck', 'No results found') }}</p>
</div>
</div>
</div>
</template>
<script>
import CardItem from '../cards/CardItem'
import { mapState } from 'vuex'
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import InfiniteLoading from 'vue-infinite-loading'
import RichText from '@juliushaertl/vue-richtext'
import Placeholder from './Placeholder'
import { Actions, ActionButton } from '@nextcloud/vue'
const createCancelToken = () => axios.CancelToken.source()
function search({ query, cursor }) {
const cancelToken = createCancelToken()
const request = async() => axios.get(generateOcsUrl('apps/deck/api/v1.0', 2) + '/search', {
cancelToken: cancelToken.token,
params: {
term: query,
limit: 20,
cursor,
},
})
return {
request,
cancel: cancelToken.cancel,
}
}
export default {
name: 'GlobalSearchResults',
components: { CardItem, InfiniteLoading, RichText, Placeholder, Actions, ActionButton },
data() {
return {
results: [],
cancel: null,
loading: false,
cursor: null,
}
},
computed: {
...mapState({
searchQuery: state => state.searchQuery,
}),
filteredResults() {
const sortFn = (a, b) => a.archived - b.archived || b.lastModified - a.lastModified
if (this.$route.params.id) {
return this.results.filter((result) => result.relatedBoard.id.toString() !== this.$route.params.id.toString()).sort(sortFn)
}
return [...this.results].sort(sortFn)
},
queryStringArgs() {
return {
searchQuery: this.searchQuery,
}
},
},
watch: {
searchQuery() {
this.cursor = null
this.loading = true
this.search()
},
},
methods: {
infiniteHandler($state) {
this.loading = true
this.search().then((data) => {
if (data.length) {
$state.loaded()
} else {
$state.complete()
}
this.loading = false
})
},
async search() {
if (this.cancel) {
this.cancel()
}
const { request, cancel } = await search({ query: this.searchQuery, cursor: this.cursor })
this.cancel = cancel
const { data } = await request()
if (this.cursor === null) {
this.results = []
}
if (data.ocs.data.length > 0) {
data.ocs.data.forEach((card) => {
this.$store.dispatch('addCardData', card)
})
this.results = [...this.results, ...data.ocs.data]
this.cursor = data.ocs.data[data.ocs.data.length - 1].lastModified
}
return data.ocs.data
},
},
}
</script>
<style lang="scss" scoped>
@import '../../css/variables.scss';
.global-search {
width: 100%;
padding: $board-spacing + $stack-spacing;
padding-bottom: 0;
overflow: hidden;
min-height: 35vh;
max-height: 50vh;
flex-shrink: 1;
flex-grow: 1;
border-top: 1px solid var(--color-border);
z-index: 1010;
position: relative;
.action-item.icon-close {
position: absolute;
top: 10px;
right: 10px;
}
.search-wrapper {
overflow: scroll;
height: 100%;
position: relative;
padding: 10px;
}
h2::v-deep span {
background-color: var(--color-background-dark);
padding: 3px;
border-radius: var(--border-radius);
}
.search-results {
display: flex;
flex-wrap: wrap;
& > div {
flex-grow: 0;
}
}
&::v-deep .card {
width: $stack-width;
margin-right: $stack-spacing;
}
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div class="card--placeholder">
<svg class="card-placeholder__gradient">
<defs>
<linearGradient id="card-placeholder__gradient">
<stop offset="0%" :stop-color="light">
<animate attributeName="stop-color"
:values="`${light}; ${light}; ${dark}; ${dark}; ${light}`"
dur="2s"
repeatCount="indefinite" />
</stop>
<stop offset="100%" :stop-color="dark">
<animate attributeName="stop-color"
:values="`${dark}; ${light}; ${light}; ${dark}; ${dark}`"
dur="2s"
repeatCount="indefinite" />
</stop>
</linearGradient>
</defs>
</svg>
<svg
class="card-placeholder__placeholder"
:class="{ 'standalone': standalone }"
xmlns="http://www.w3.org/2000/svg"
fill="url(#card-placeholder__gradient)">
<rect class="card-placeholder__placeholder-line-header" :style="{width: `calc(${randWidth()}%)`}" />
<rect class="card-placeholder__placeholder-line-one" />
<rect class="card-placeholder__placeholder-line-two" :style="{width: `calc(${randWidth()}%)`}" />
</svg>
</div>
</template>
<script>
export default {
name: 'Placeholder',
data() {
return {
light: null,
dark: null,
standalone: true,
}
},
mounted() {
const styles = getComputedStyle(document.documentElement)
this.dark = styles.getPropertyValue('--color-placeholder-dark')
this.light = styles.getPropertyValue('--color-placeholder-light')
},
methods: {
randWidth() {
return Math.floor(Math.random() * 20) + 40
},
},
}
</script>
<style lang="scss" scoped>
@import '../../css/variables.scss';
$clickable-area: 44px;
.card--placeholder {
width: $stack-width;
margin-right: $stack-spacing;
padding: $card-padding;
transition: box-shadow 0.1s ease-in-out;
box-shadow: 0 0 2px 0 var(--color-box-shadow);
border-radius: var(--border-radius-large);
font-size: 100%;
margin-bottom: $card-spacing;
height: 100px;
}
.card-placeholder__gradient {
position: fixed;
height: 0;
width: 0;
z-index: -1;
}
.card-placeholder__placeholder {
width: 100%;
&-line-header,
&-line-one,
&-line-two {
width: 100%;
height: 1em;
x: 0;
}
&-line-header {
visibility: hidden;
}
&-line-one {
y: 5px;
}
&-line-two {
y: 25px;
}
&.standalone {
.card-placeholder__placeholder-line-header {
visibility: visible;
y: 5px;
}
.card-placeholder__placeholder-line-one {
y: 40px;
}
.card-placeholder__placeholder-line-two {
y: 60px;
}
}
}
</style>

View File

@@ -116,7 +116,7 @@ export default new Router({
},
},
{
path: 'card/:cardId',
path: 'card/:cardId/:tabId?/:tabQuery?',
name: 'card',
components: {
sidebar: CardSidebar,
@@ -130,6 +130,8 @@ export default new Router({
sidebar: (route) => {
return {
id: parseInt(route.params.cardId, 10),
tabId: route.params.tabId,
tabQuery: route.params.tabQuery,
}
},
},

View File

@@ -21,6 +21,7 @@
*/
import { CardApi } from './../services/CardApi'
import moment from 'moment'
import Vue from 'vue'
const apiClient = new CardApi()
@@ -86,8 +87,90 @@ export default {
return true
}
return card.title.toLowerCase().includes(getters.getSearchQuery.toLowerCase())
|| card.description.toLowerCase().includes(getters.getSearchQuery.toLowerCase())
let hasMatch = true
const matches = getters.getSearchQuery.match(/(?:[^\s"]+|"[^"]*")+/g)
const filterOutQuotes = (q) => {
if (q[0] === '"' && q[q.length - 1] === '"') {
return q.substr(1, -1)
}
return q
}
for (const match of matches) {
let [filter, query] = match.indexOf(':') !== -1 ? match.split(/:(.+)/) : [null, match]
if (filter === 'title') {
hasMatch = hasMatch && card.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'description') {
hasMatch = hasMatch && card.description.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'list') {
const stack = this.getters.stackById(card.stackId)
if (!stack) {
return false
}
hasMatch = hasMatch && stack.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'tag') {
hasMatch = hasMatch && card.labels.findIndex((label) => label.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())) !== -1
} else if (filter === 'date') {
const datediffHour = ((new Date(card.duedate) - new Date()) / 3600 / 1000)
query = filterOutQuotes(query)
switch (query) {
case 'overdue':
hasMatch = hasMatch && (card.overdue === 3)
break
case 'today':
hasMatch = hasMatch && (datediffHour > 0 && datediffHour <= 24 && card.duedate !== null)
break
case 'week':
hasMatch = hasMatch && (datediffHour > 0 && datediffHour <= 7 * 24 && card.duedate !== null)
break
case 'month':
hasMatch = hasMatch && (datediffHour > 0 && datediffHour <= 30 * 24 && card.duedate !== null)
break
case 'none':
hasMatch = hasMatch && (card.duedate === null)
break
}
if (card.duedate === null || !hasMatch) {
return false
}
const comparator = query[0] + (query[1] === '=' ? '=' : '')
const isValidComparator = ['<', '<=', '>', '>='].indexOf(comparator) !== -1
const parsedCardDate = moment(card.duedate)
const parsedDate = moment(query.substr(isValidComparator ? comparator.length : 0))
switch (comparator) {
case '<':
hasMatch = hasMatch && parsedCardDate.isBefore(parsedDate)
break
case '<=':
hasMatch = hasMatch && parsedCardDate.isSameOrBefore(parsedDate)
break
case '>':
hasMatch = hasMatch && parsedCardDate.isAfter(parsedDate)
break
case '>=':
hasMatch = hasMatch && parsedCardDate.isSameOrAfter(parsedDate)
break
default:
hasMatch = hasMatch && parsedCardDate.isSame(parsedDate)
break
}
} else if (filter === 'assigned') {
hasMatch = hasMatch && card.assignedUsers.findIndex((assignment) => {
return assignment.participant.primaryKey.toLowerCase() === filterOutQuotes(query).toLowerCase()
|| assignment.participant.displayname.toLowerCase() === filterOutQuotes(query).toLowerCase()
}) !== -1
} else {
hasMatch = hasMatch && (card.title.toLowerCase().includes(filterOutQuotes(match).toLowerCase())
|| card.description.toLowerCase().includes(filterOutQuotes(match).toLowerCase()))
}
if (!hasMatch) {
return false
}
}
return true
})
.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)
},
@@ -210,7 +293,7 @@ export default {
}
const updatedCard = await apiClient[call](card)
commit('deleteCard', updatedCard)
commit('updateCard', updatedCard)
},
async assignCardToUser({ commit }, { card, assignee }) {
const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type)
@@ -236,5 +319,14 @@ export default {
const updatedCard = await apiClient.updateCard(card)
commit('updateCardProperty', { property: 'duedate', card: updatedCard })
},
addCardData({ commit }, cardData) {
const card = { ...cardData }
commit('addStack', card.relatedStack)
commit('addBoard', card.relatedBoard)
delete card.relatedStack
delete card.relatedBoard
commit('addCard', card)
},
},
}

View File

@@ -91,6 +91,9 @@ export default new Vuex.Store({
boards: state => {
return state.boards
},
boardById: state => (id) => {
return state.boards.find((board) => board.id === id)
},
assignables: state => {
return [
...state.assignableUsers.map((user) => ({ ...user, type: 0 })),

View File

@@ -5,10 +5,7 @@ default:
- '%paths.base%/../features/'
contexts:
- ServerContext:
baseUrl: http://localhost:8080/index.php/ocs/
admin:
- admin
- admin
regular_user_password: 123456
- BoardContext:
baseUrl: http://localhost:8080/
- RequestContext
- BoardContext
- SearchContext

View File

@@ -1,6 +1,7 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
@@ -16,25 +17,35 @@ class BoardContext implements Context {
/** @var array last card response */
private $card = null;
/** @var ServerContext */
private $serverContext;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->serverContext = $environment->getContext('ServerContext');
}
/**
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
*/
public function createsABoardNamedWithColor($title, $color) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards', [
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/boards', [
'title' => $title,
'color' => $color
]);
$this->response->getBody()->seek(0);
$this->board = json_decode((string)$this->response->getBody(), true);
$this->getResponse()->getBody()->seek(0);
$this->board = json_decode((string)$this->getResponse()->getBody(), true);
}
/**
* @When /^fetches the board named "([^"]*)"$/
*/
public function fetchesTheBoardNamed($boardName) {
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $this->board['id'], []);
$this->response->getBody()->seek(0);
$this->board = json_decode((string)$this->response->getBody(), true);
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $this->board['id'], []);
$this->getResponse()->getBody()->seek(0);
$this->board = json_decode((string)$this->getResponse()->getBody(), true);
}
/**
@@ -48,7 +59,7 @@ class BoardContext implements Context {
];
$tableRows = isset($permissions) ? $permissions->getRowsHash() : [];
$result = array_merge($defaults, $tableRows);
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
'type' => 0,
'participant' => $user,
'permissionEdit' => $result['permissionEdit'] === '1',
@@ -68,7 +79,7 @@ class BoardContext implements Context {
];
$tableRows = isset($permissions) ? $permissions->getRowsHash() : [];
$result = array_merge($defaults, $tableRows);
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
'type' => 1,
'participant' => $group,
'permissionEdit' => $result['permissionEdit'] === '1',
@@ -82,38 +93,38 @@ class BoardContext implements Context {
* @When /^fetching the board list$/
*/
public function fetchingTheBoardList() {
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards');
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards');
}
/**
* @When /^fetching the board with id "([^"]*)"$/
*/
public function fetchingTheBoardWithId($id) {
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $id);
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $id);
}
/**
* @Given /^create a stack named "([^"]*)"$/
*/
public function createAStackNamed($name) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/stacks', [
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/stacks', [
'title' => $name,
'boardId' => $this->board['id']
]);
$this->response->getBody()->seek(0);
$this->stack = json_decode((string)$this->response->getBody(), true);
$this->requestContext->getResponse()->getBody()->seek(0);
$this->stack = json_decode((string)$this->getResponse()->getBody(), true);
}
/**
* @Given /^create a card named "([^"]*)"$/
*/
public function createACardNamed($name) {
$this->sendJSONrequest('POST', '/index.php/apps/deck/cards', [
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/cards', [
'title' => $name,
'stackId' => $this->stack['id']
]);
$this->response->getBody()->seek(0);
$this->card = json_decode((string)$this->response->getBody(), true);
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
}
/**
@@ -151,4 +162,70 @@ class BoardContext implements Context {
]);
$this->serverContext->creatingShare($table);
}
/**
* @Given /^set the description to "([^"]*)"$/
*/
public function setTheDescriptionTo($description) {
$this->requestContext->sendJSONrequest('PUT', '/index.php/apps/deck/cards/' . $this->card['id'], array_merge(
$this->card,
['description' => $description]
));
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
}
/**
* @Given /^set the card attribute "([^"]*)" to "([^"]*)"$/
*/
public function setCardAttribute($attribute, $value) {
$this->requestContext->sendJSONrequest('PUT', '/index.php/apps/deck/cards/' . $this->card['id'], array_merge(
$this->card,
[$attribute => $value]
));
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
}
/**
* @Given /^set the card duedate to "([^"]*)"$/
*/
public function setTheCardDuedateTo($arg1) {
$date = new DateTime($arg1);
$this->setCardAttribute('duedate', $date->format(DateTimeInterface::ATOM));
}
/**
* @Given /^assign the card to the user "([^"]*)"$/
*/
public function assignTheCardToTheUser($user) {
$this->assignToCard($user, 0);
}
/**
* @Given /^assign the card to the group "([^"]*)"$/
*/
public function assignTheCardToTheGroup($user) {
$this->assignToCard($user, 1);
}
private function assignToCard($participant, $type) {
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/cards/' . $this->card['id'] .'/assign', [
'userId' => $participant,
'type' => $type
]);
$this->requestContext->getResponse()->getBody()->seek(0);
}
/**
* @Given /^assign the tag "([^"]*)" to the card$/
*/
public function assignTheTagToTheCard($tag) {
$filteredLabels = array_filter($this->board['labels'], function ($label) use ($tag) {
return $label['title'] === $tag;
});
$label = array_shift($filteredLabels);
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/cards/' . $this->card['id'] .'/label/' . $label['id']);
$this->requestContext->getResponse()->getBody()->seek(0);
}
}

View File

@@ -0,0 +1,140 @@
<?php
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use PHPUnit\Framework\Assert;
use Behat\Behat\Context\Context;
use Psr\Http\Message\ResponseInterface;
require_once __DIR__ . '/../../vendor/autoload.php';
class RequestContext implements Context {
private $response;
/** @var ServerContext */
private $serverContext;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->serverContext = $environment->getContext('ServerContext');
}
private function getBaseUrl() {
}
/**
* @Then the response should have a status code :code
* @param string $code
* @throws InvalidArgumentException
*/
public function theResponseShouldHaveStatusCode($code) {
$currentCode = $this->response->getStatusCode();
if ($currentCode !== (int)$code) {
throw new InvalidArgumentException(
sprintf(
'Expected %s as code got %s',
$code,
$currentCode
)
);
}
}
/**
* @Then /^the response Content-Type should be "([^"]*)"$/
* @param string $contentType
*/
public function theResponseContentTypeShouldbe($contentType) {
Assert::assertEquals($contentType, $this->response->getHeader('Content-Type')[0]);
}
/**
* @Then the response should be a JSON array with the following mandatory values
* @param TableNode $table
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues(TableNode $table) {
$this->response->getBody()->seek(0);
$expectedValues = $table->getColumnsHash();
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
foreach ($expectedValues as $value) {
if ((string)$realResponseArray[$value['key']] !== (string)$value['value']) {
throw new InvalidArgumentException(
sprintf(
'Expected %s for key %s got %s',
(string)$value['value'],
$value['key'],
(string)$realResponseArray[$value['key']]
)
);
}
}
}
/**
* @Then the response should be a JSON array with a length of :length
* @param int $length
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithALengthOf($length) {
$this->response->getBody()->seek(0);
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
if ((int)count($realResponseArray) !== (int)$length) {
throw new InvalidArgumentException(
sprintf(
'Expected %d as length got %d',
$length,
count($realResponseArray)
)
);
}
}
public function sendJSONrequest($method, $url, $data = []) {
$client = new Client;
try {
$this->response = $client->request(
$method,
rtrim($this->serverContext->getBaseUrl(), '/') . '/' . ltrim($url, '/'),
[
'cookies' => $this->serverContext->getCookieJar(),
'json' => $data,
'headers' => [
'requesttoken' => $this->serverContext->getReqestToken(),
]
]
);
} catch (ClientException $e) {
$this->response = $e->getResponse();
}
}
public function sendOCSRequest($method, $url, $data = []) {
$client = new Client;
try {
$this->response = $client->request(
$method,
rtrim($this->serverContext->getBaseUrl(), '/') . '/ocs/v2.php/' . ltrim($url, '/'),
[
'cookies' => $this->serverContext->getCookieJar(),
'json' => $data,
'headers' => [
'requesttoken' => $this->serverContext->getReqestToken(),
'OCS-APIREQUEST' => 'true',
'Accept' => 'application/json'
]
]
);
} catch (ClientException $e) {
$this->response = $e->getResponse();
}
}
public function getResponse(): ResponseInterface {
return $this->response;
}
}

View File

@@ -1,121 +1,46 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use PHPUnit\Framework\Assert;
require_once __DIR__ . '/../../vendor/autoload.php';
trait RequestTrait {
private $baseUrl;
private $adminUser;
private $regularUser;
private $cookieJar;
private $response;
public function __construct($baseUrl, $admin = 'admin', $regular_user_password = '123456') {
$this->baseUrl = $baseUrl;
$this->adminUser = $admin === 'admin' ? ['admin', 'admin'] : $admin;
$this->regularUser = $regular_user_password;
}
/** @var ServerContext */
private $serverContext;
/** @var RequestContext */
protected $requestContext;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
public function gatherRequestTraitContext(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->serverContext = $environment->getContext('ServerContext');
$this->requestContext = $environment->getContext('RequestContext');
}
/**
* @Then the response should have a status code :code
* @param string $code
* @throws InvalidArgumentException
*/
public function theResponseShouldHaveStatusCode($code) {
$currentCode = $this->response->getStatusCode();
if ($currentCode !== (int)$code) {
throw new InvalidArgumentException(
sprintf(
'Expected %s as code got %s',
$code,
$currentCode
)
);
}
}
/**
* @Then /^the response Content-Type should be "([^"]*)"$/
* @param string $contentType
*/
public function theResponseContentTypeShouldbe($contentType) {
Assert::assertEquals($contentType, $this->response->getHeader('Content-Type')[0]);
}
/**
* @Then the response should be a JSON array with the following mandatory values
* @param TableNode $table
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithTheFollowingMandatoryValues(TableNode $table) {
$this->response->getBody()->seek(0);
$expectedValues = $table->getColumnsHash();
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
foreach ($expectedValues as $value) {
if ((string)$realResponseArray[$value['key']] !== (string)$value['value']) {
throw new InvalidArgumentException(
sprintf(
'Expected %s for key %s got %s',
(string)$value['value'],
$value['key'],
(string)$realResponseArray[$value['key']]
)
);
}
}
}
/**
* @Then the response should be a JSON array with a length of :length
* @param int $length
* @throws InvalidArgumentException
*/
public function theResponseShouldBeAJsonArrayWithALengthOf($length) {
$this->response->getBody()->seek(0);
$realResponseArray = json_decode($this->response->getBody()->getContents(), true);
if ((int)count($realResponseArray) !== (int)$length) {
throw new InvalidArgumentException(
sprintf(
'Expected %d as length got %d',
$length,
count($realResponseArray)
)
);
}
}
private function sendJSONrequest($method, $url, $data = []) {
$client = new Client;
try {
$this->response = $client->request(
$method,
$this->baseUrl . $url,
[
'cookies' => $this->serverContext->getCookieJar(),
'json' => $data,
'headers' => [
'requesttoken' => $this->serverContext->getReqestToken()
]
]
);
} catch (ClientException $e) {
$this->response = $e->getResponse();
}
public function getResponse() {
return $this->requestContext->getResponse();
}
}

View File

@@ -0,0 +1,81 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use PHPUnit\Framework\Assert;
require_once __DIR__ . '/../../vendor/autoload.php';
class SearchContext implements Context {
use RequestTrait;
/** @var BoardContext */
protected $boardContext;
private $searchResults;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->boardContext = $environment->getContext('BoardContext');
}
/**
* @When /^searching for "([^"]*)"$/
* @param string $term
*/
public function searchingFor(string $term) {
$this->requestContext->sendOCSRequest('GET', '/apps/deck/api/v1.0/search?term=' . urlencode($term), []);
$this->requestContext->getResponse()->getBody()->seek(0);
$data = (string)$this->getResponse()->getBody();
$this->searchResults = json_decode($data, true);
}
/**
* @When /^searching for '([^']*)'$/
* @param string $term
*/
public function searchingForQuotes(string $term) {
$this->searchingFor($term);
}
/**
* @Then /^the board "([^"]*)" is found$/
*/
public function theBoardIsFound($arg1) {
$ocsData = $this->searchResults['ocs']['data'];
$found = false;
foreach ($ocsData as $result) {
if ($result['title'] === $arg1) {
$found = true;
}
}
Assert::assertTrue($found, 'Board can be found');
}
private function cardIsFound($arg1) {
$ocsData = $this->searchResults['ocs']['data'];
$found = false;
foreach ($ocsData as $result) {
if ($result['title'] === $arg1) {
$found = true;
}
}
return $found;
}
/**
* @Then /^the card "([^"]*)" is found$/
*/
public function theCardIsFound($arg1) {
Assert::assertTrue($this->cardIsFound($arg1), 'Card can be found');
}
/**
* @Then /^the card "([^"]*)" is not found$/
*/
public function theCardIsNotFound($arg1) {
Assert::assertFalse($this->cardIsFound($arg1), 'Card can not be found');
}
}

View File

@@ -6,7 +6,14 @@ use GuzzleHttp\Cookie\CookieJar;
require_once __DIR__ . '/../../vendor/autoload.php';
class ServerContext implements Context {
use WebDav;
use WebDav {
WebDav::__construct as private __tConstruct;
}
public function __construct($baseUrl) {
$this->rawBaseUrl = $baseUrl;
$this->__tConstruct($baseUrl . '/index.php/ocs/', ['admin', 'admin'], '123456');
}
/** @var string */
private $mappedUserId;
@@ -28,6 +35,10 @@ class ServerContext implements Context {
$this->asAn($user);
}
public function getBaseUrl(): string {
return $this->rawBaseUrl;
}
public function getCookieJar(): CookieJar {
return $this->cookieJar;
}

View File

@@ -4,23 +4,6 @@ Feature: decks
Given user "admin" exists
Given user "user0" exists
Scenario: Request the main frontend page
Given Logging in using web as "admin"
When Sending a "GET" to "/index.php/apps/deck" without requesttoken
Then the HTTP status code should be "200"
Scenario: Fetch the board list
Given Logging in using web as "admin"
When fetching the board list
Then the response should have a status code "200"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Fetch board details of a nonexisting board
Given Logging in using web as "admin"
When fetching the board with id "99999999"
Then the response should have a status code "403"
And the response Content-Type should be "application/json; charset=utf-8"
Scenario: Create a new board
Given Logging in using web as "admin"
When creates a board named "MyBoard" with color "000000"

View File

@@ -0,0 +1,259 @@
Feature: Searching for cards
Background:
Given user "admin" exists
Given user "user0" exists
Given Logging in using web as "admin"
When creates a board named "MyBoard" with color "000000"
When create a stack named "ToDo"
And create a card named "Example task 1"
And create a card named "Example task 2"
When create a stack named "In progress"
And create a card named "Progress task 1"
And create a card named "Progress task 2"
When create a stack named "Done"
And create a card named "Done task 1"
And set the description to "Done task description 1"
And create a card named "Done task 2"
And set the description to "Done task description 2"
And shares the board with user "user0"
Scenario: Search for a card with multiple terms
When searching for "Example task"
Then the card "Example task 1" is found
Then the card "Example task 2" is found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Scenario: Search for a card in a specific list
When searching for "task list:Done"
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is found
Then the card "Done task 2" is found
Scenario: Search for a card with one term
When searching for "task"
Then the card "Example task 1" is found
Then the card "Example task 2" is found
Then the card "Progress task 1" is found
Then the card "Progress task 2" is found
Then the card "Done task 1" is found
Then the card "Done task 2" is found
Scenario: Search for a card with an differently cased term
When searching for "tAsk"
Then the card "Example task 1" is found
Then the card "Example task 2" is found
Then the card "Progress task 1" is found
Then the card "Progress task 2" is found
Then the card "Done task 1" is found
Then the card "Done task 2" is found
Scenario: Search for a card title
When searching for 'title:"Done task 1"'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is found
Then the card "Done task 2" is not found
Scenario: Search for a card description
When searching for 'description:"Done task description"'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is found
Then the card "Done task 2" is found
Scenario: Search for a non-existing card description
When searching for 'description:"Example"'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Scenario: Search on shared boards
Given Logging in using web as "user0"
When searching for "task"
Then the card "Example task 1" is found
Then the card "Example task 2" is found
Then the card "Progress task 1" is found
Then the card "Progress task 2" is found
Then the card "Done task 1" is found
Then the card "Done task 2" is found
Scenario: Search for a card due date
Given create a card named "Overdue task"
And set the card attribute "duedate" to "2020-12-12"
And create a card named "Future task"
And set the card attribute "duedate" to "3000-12-12"
And create a card named "Tomorrow task"
And set the card duedate to "tomorrow"
When searching for 'date:overdue'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Then the card "Overdue task" is found
Then the card "Future task" is not found
Scenario: Search for a card due date
And create a card named "Overdue task"
And set the card attribute "duedate" to "2020-12-12"
And create a card named "Future task"
And set the card attribute "duedate" to "3000-12-12"
And create a card named "Tomorrow task"
And set the card duedate to "+12 hours"
And create a card named "Next week task"
And set the card duedate to "+5 days"
When searching for 'date:today'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Then the card "Overdue task" is not found
Then the card "Future task" is not found
Then the card "Tomorrow task" is found
Then the card "Next week task" is not found
When searching for 'date:week'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Then the card "Overdue task" is not found
Then the card "Future task" is not found
Then the card "Tomorrow task" is found
Then the card "Next week task" is found
When searching for 'date:month'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Then the card "Overdue task" is not found
Then the card "Future task" is not found
Then the card "Tomorrow task" is found
Then the card "Next week task" is found
When searching for 'date:none'
Then the card "Example task 1" is found
Then the card "Example task 2" is found
Then the card "Progress task 1" is found
Then the card "Progress task 2" is found
Then the card "Done task 1" is found
Then the card "Done task 2" is found
Then the card "Overdue task" is not found
Then the card "Future task" is not found
Then the card "Tomorrow task" is not found
Then the card "Next week task" is not found
When searching for 'date:<"+7 days"'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Then the card "Overdue task" is found
Then the card "Future task" is not found
Then the card "Tomorrow task" is found
Then the card "Next week task" is found
When searching for 'date:>"+10 days"'
Then the card "Example task 1" is not found
Then the card "Example task 2" is not found
Then the card "Progress task 1" is not found
Then the card "Progress task 2" is not found
Then the card "Done task 1" is not found
Then the card "Done task 2" is not found
Then the card "Overdue task" is not found
Then the card "Future task" is found
Then the card "Tomorrow task" is not found
Then the card "Next week task" is not found
Scenario: Search for assigned user
Given user "user1" exists
And shares the board with user "user1"
Given create a card named "Assigned card to user1"
And assign the card to the user "user1"
When searching for 'assigned:user1'
Then the card "Example task 1" is not found
And the card "Assigned card to user1" is found
Scenario: Search for assigned user by displayname
Given user "ada" with displayname "Ada Lovelace" exists
And shares the board with user "ada"
Given create a card named "Assigned card to ada"
And assign the card to the user "ada"
When searching for 'assigned:"Ada Lovelace"'
Then the card "Example task 1" is not found
And the card "Assigned card to ada" is found
Scenario: Search for assigned users
Given user "user1" exists
And shares the board with user "user1"
Given create a card named "Assigned card to user0"
And assign the card to the user "user0"
Given create a card named "Assigned card to user01"
And assign the card to the user "user0"
And assign the card to the user "user1"
When searching for 'assigned:user0 assigned:user1'
Then the card "Example task 1" is not found
And the card "Assigned card to user0" is not found
And the card "Assigned card to user01" is found
Scenario: Search for assigned group
Given user "user1" exists
And shares the board with user "user1"
Given group "group1" exists
And shares the board with group "group1"
Given user "user1" belongs to group "group1"
Given create a card named "Assigned card to group1"
And assign the card to the group "group1"
When searching for 'assigned:user1'
Then the card "Example task 1" is not found
And the card "Assigned card to group1" is found
When searching for 'assigned:group1'
Then the card "Example task 1" is not found
And the card "Assigned card to group1" is found
Scenario: Search for assigned tag
Given create a card named "Labeled card"
# Default labels from boards are used for this test case
And assign the tag "Finished" to the card
When searching for 'tag:Finished'
Then the card "Example task 1" is not found
And the card "Labeled card" is found
Given create a card named "Multi labeled card"
And assign the tag "Finished" to the card
And assign the tag "To review" to the card
When searching for 'tag:Finished tag:Later'
Then the card "Example task 1" is not found
And the card "Multi labeled card" is not found
When searching for 'tag:Finished tag:"To review"'
Then the card "Example task 1" is not found
And the card "Labeled card" is not found
And the card "Multi labeled card" is found

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.4.1@9fd7a7d885b3a216cff8dec9d8c21a132f275224">
<files psalm-version="4.7.0@d4377c0baf3ffbf0b1ec6998e8d1be2a40971005">
<file src="lib/Activity/ActivityManager.php">
<TypeDoesNotContainType occurrences="1">
<code>$message !== null</code>
@@ -10,16 +10,6 @@
<code>(int)$subjectParams['comment']</code>
</InvalidScalarArgument>
</file>
<file src="lib/AppInfo/Application.php">
<DuplicateClass occurrences="1">
<code>Application</code>
</DuplicateClass>
</file>
<file src="lib/AppInfo/Application20.php">
<RedundantCondition occurrences="1">
<code>method_exists($shareManager, 'registerShareProvider')</code>
</RedundantCondition>
</file>
<file src="lib/Command/UserExport.php">
<ImplementedReturnTypeMismatch occurrences="1">
<code>void</code>
@@ -126,12 +116,16 @@
</UndefinedClass>
</file>
<file src="lib/Db/CardMapper.php">
<InvalidArgument occurrences="1"/>
<InvalidScalarArgument occurrences="1">
<code>$entity-&gt;getId()</code>
</InvalidScalarArgument>
<ParamNameMismatch occurrences="1">
<code>$cardId</code>
</ParamNameMismatch>
<UndefinedInterfaceMethod occurrences="1">
<code>getUserIdGroups</code>
</UndefinedInterfaceMethod>
</file>
<file src="lib/Db/ChangeHelper.php">
<UndefinedThisPropertyAssignment occurrences="3">
@@ -271,11 +265,12 @@
</RedundantCondition>
</file>
<file src="lib/Service/FilesAppService.php">
<InvalidCatch occurrences="1"/>
<MissingDependency occurrences="4">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>IRootFolder</code>
<code>Share\Exceptions\ShareNotFound</code>
<code>ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Service/PermissionService.php">
@@ -284,6 +279,13 @@
<code>\OCA\Circles\Api\v1\Circles</code>
</UndefinedClass>
</file>
<file src="lib/Service/SearchService.php">
<UndefinedThisPropertyFetch occurrences="3">
<code>$this-&gt;l10n</code>
<code>$this-&gt;urlGenerator</code>
<code>$this-&gt;userManager</code>
</UndefinedThisPropertyFetch>
</file>
<file src="lib/Service/StackService.php">
<UndefinedClass occurrences="1">
<code>BadRquestException</code>
@@ -296,7 +298,7 @@
<InvalidReturnType occurrences="1">
<code>getSharesInFolder</code>
</InvalidReturnType>
<MissingDependency occurrences="7">
<MissingDependency occurrences="8">
<code>GenericShareException</code>
<code>GenericShareException</code>
<code>ShareNotFound</code>
@@ -304,6 +306,7 @@
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Sharing/Listener.php">

View File

@@ -111,7 +111,7 @@ class UserExportTest extends \Test\TestCase {
->method('find')
->willReturn($cards[0]);
$this->assignedUserMapper->expects($this->exactly(count($boards) * count($stacks) * count($cards)))
->method('find')
->method('findAll')
->willReturn([]);
$result = $this->invokePrivate($this->userExport, 'execute', [$input, $output]);
}

View File

@@ -132,9 +132,15 @@ class BoardMapperTest extends MapperTestUtility {
public function testFindAll() {
$actual = $this->boardMapper->findAll();
$this->assertEquals($this->boards[0]->getId(), $actual[0]->getId());
$this->assertEquals($this->boards[1]->getId(), $actual[1]->getId());
$this->assertEquals($this->boards[2]->getId(), $actual[2]->getId());
$this->assertEquals(1, count(array_filter($actual, function ($card) {
return $card->getId() === $this->boards[0]->getId();
})));
$this->assertEquals(1, count(array_filter($actual, function ($card) {
return $card->getId() === $this->boards[1]->getId();
})));
$this->assertEquals(1, count(array_filter($actual, function ($card) {
return $card->getId() === $this->boards[2]->getId();
})));
}
public function testFindAllToDelete() {

View File

@@ -24,7 +24,7 @@
namespace OCA\Deck\Notification;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
@@ -59,7 +59,7 @@ class NotificationHelperTest extends \Test\TestCase {
protected $cardMapper;
/** @var BoardMapper|MockObject */
protected $boardMapper;
/** @var AssignedUsersMapper|MockObject */
/** @var AssignmentMapper|MockObject */
protected $assignedUsersMapper;
/** @var PermissionService|MockObject */
protected $permissionService;
@@ -78,7 +78,7 @@ class NotificationHelperTest extends \Test\TestCase {
parent::setUp();
$this->cardMapper = $this->createMock(CardMapper::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->assignedUsersMapper = $this->createMock(AssignmentMapper::class);
$this->permissionService = $this->createMock(PermissionService::class);
$this->config = $this->createMock(IConfig::class);
$this->notificationManager = $this->createMock(IManager::class);

View File

@@ -0,0 +1,133 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Search;
use OCA\Deck\Search\Query\DateQueryParameter;
use OCA\Deck\Search\Query\SearchQuery;
use OCA\Deck\Search\Query\StringQueryParameter;
use OCP\IL10N;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
class FilterStringParserTest extends TestCase {
private $l10n;
private $parser;
public function setUp(): void {
$this->l10n = $this->createMock(IL10N::class);
$this->parser = new FilterStringParser($this->l10n);
}
public function testParseEmpty() {
$result = $this->parser->parse(null);
$expected = new SearchQuery();
Assert::assertEquals($expected, $result);
}
public function testParseTextTokens() {
$result = $this->parser->parse('a b c');
$expected = new SearchQuery();
$expected->addTextToken('a');
$expected->addTextToken('b');
$expected->addTextToken('c');
Assert::assertEquals($expected, $result);
}
public function testParseTextToken() {
$result = $this->parser->parse('abc');
$expected = new SearchQuery();
$expected->addTextToken('abc');
Assert::assertEquals($expected, $result);
}
public function testParseTextTokenQuotes() {
$result = $this->parser->parse('a b c "a b c" tag:abc tag:"a b c" tag:\'d e f\'');
$expected = new SearchQuery();
$expected->addTextToken('a');
$expected->addTextToken('b');
$expected->addTextToken('c');
$expected->addTextToken('a b c');
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, 'abc'));
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, 'a b c'));
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, 'd e f'));
Assert::assertEquals($expected, $result);
}
public function testParseTagComparatorNotSupported() {
$result = $this->parser->parse('tag:<"a tag"');
$expected = new SearchQuery();
$expected->addTag(new StringQueryParameter('tag', SearchQuery::COMPARATOR_EQUAL, '<"a tag"'));
Assert::assertEquals($expected, $result);
}
public function testParseTextTokenQuotesSingle() {
$result = $this->parser->parse('a b c \'a b c\'');
$expected = new SearchQuery();
$expected->addTextToken('a');
$expected->addTextToken('b');
$expected->addTextToken('c');
$expected->addTextToken('a b c');
Assert::assertEquals($expected, $result);
}
public function testParseTextTokenQuotesWrong() {
$result = $this->parser->parse('"a b" c"');
$expected = new SearchQuery();
$expected->addTextToken('a b');
$expected->addTextToken('c"');
Assert::assertEquals($expected, $result);
}
public function dataParseDate() {
return [
['date:today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'today')], []],
['date:>today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_MORE, 'today')], []],
['date:>=today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_MORE_EQUAL, 'today')], []],
['date:<today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS, 'today')], []],
['date:<=today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS_EQUAL, 'today')], []],
['date:<+today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS, '+today')], []],
['date:<>today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_LESS, '>today')], []],
['date:=today', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, '=today')], []],
['date:today todo', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'today')], ['todo']],
['date:"last day of next month" todo', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'last day of next month')], ['todo']],
['date:"last day of next month" "todo task" task', [new DateQueryParameter('date', SearchQuery::COMPARATOR_EQUAL, 'last day of next month')], ['todo task', 'task']],
];
}
/**
* @dataProvider dataParseDate
*/
public function testParseDate($query, $dates, array $tokens) {
$result = $this->parser->parse($query);
$expected = new SearchQuery();
foreach ($dates as $date) {
$expected->addDuedate($date);
}
foreach ($tokens as $token) {
$expected->addTextToken($token);
}
Assert::assertEquals($expected, $result);
}
}

View File

@@ -23,7 +23,6 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OC\L10N\L10N;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\Acl;
@@ -37,11 +36,11 @@ use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Notification\NotificationHelper;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IGroupManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use \Test\TestCase;
class BoardServiceTest extends TestCase {
@@ -72,7 +71,7 @@ class BoardServiceTest extends TestCase {
private $activityManager;
/** @var ChangeHelper */
private $changeHelper;
/** @var EventDispatcherInterface */
/** @var IEventDispatcher */
private $eventDispatcher;
private $userId = 'admin';
@@ -91,7 +90,7 @@ class BoardServiceTest extends TestCase {
$this->groupManager = $this->createMock(IGroupManager::class);
$this->activityManager = $this->createMock(ActivityManager::class);
$this->changeHelper = $this->createMock(ChangeHelper::class);
$this->eventDispatcher = $this->createMock(SymfonyAdapter::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->service = new BoardService(
$this->boardMapper,
@@ -383,7 +382,7 @@ class BoardServiceTest extends TestCase {
$assignment = new Assignment();
$assignment->setParticipant('admin');
$this->assignedUsersMapper->expects($this->once())
->method('findByUserId')
->method('findByParticipant')
->with('admin')
->willReturn([$assignment]);
$this->assignedUsersMapper->expects($this->once())

View File

@@ -25,9 +25,11 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\LabelMapper;
@@ -126,6 +128,17 @@ class CardServiceTest extends TestCase {
$this->userManager->expects($this->once())
->method('get')
->willReturn($user);
$this->commentsManager->expects($this->once())
->method('getNumberOfCommentsForObject')
->willReturn(0);
$boardMock = $this->createMock(Board::class);
$stackMock = $this->createMock(Stack::class);
$this->stackMapper->expects($this->any())
->method('find')
->willReturn($stackMock);
$this->boardService->expects($this->any())
->method('find')
->willReturn($boardMock);
$card = new Card();
$card->setId(1337);
$this->cardMapper->expects($this->any())
@@ -133,12 +146,14 @@ class CardServiceTest extends TestCase {
->with(123)
->willReturn($card);
$this->assignedUsersMapper->expects($this->any())
->method('find')
->method('findAll')
->with(1337)
->willReturn(['user1', 'user2']);
$cardExpected = new Card();
$cardExpected->setId(1337);
$cardExpected->setAssignedUsers(['user1', 'user2']);
$cardExpected->setRelatedBoard($boardMock);
$cardExpected->setRelatedStack($stackMock);
$this->assertEquals($cardExpected, $this->cardService->find(123));
}

View File

@@ -25,10 +25,10 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\StreamResponse;
use OCP\Files\Folder;
use OCP\Files\IAppData;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
@@ -57,6 +57,8 @@ class FileServiceTest extends TestCase {
private $config;
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var IMimeTypeDetector|MockObject */
private $mimeTypeDetector;
public function setUp(): void {
parent::setUp();
@@ -67,7 +69,8 @@ class FileServiceTest extends TestCase {
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->config = $this->createMock(IConfig::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper);
$this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper, $this->mimeTypeDetector);
}
public function mockGetFolder($cardId) {
@@ -268,51 +271,13 @@ class FileServiceTest extends TestCase {
$file->expects($this->any())
->method('fopen')
->willReturn('fileresource');
$this->mimeTypeDetector->expects($this->once())
->method('getSecureMimeType')
->willReturn('image/jpeg');
$actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Type', 'image/jpeg');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$this->assertEquals($expected, $actual);
}
public function testDisplayPdf() {
$this->config->expects($this->once())
->method('getSystemValue')
->willReturn('123');
$appDataFolder = $this->createMock(Folder::class);
$deckAppDataFolder = $this->createMock(Folder::class);
$cardFolder = $this->createMock(Folder::class);
$this->rootFolder->expects($this->once())->method('get')->willReturn($appDataFolder);
$appDataFolder->expects($this->once())->method('get')->willReturn($deckAppDataFolder);
$deckAppDataFolder->expects($this->once())->method('get')->willReturn($cardFolder);
$attachment = $this->getAttachment();
$file = $this->createMock(\OCP\Files\File::class);
$cardFolder->expects($this->once())->method('get')->willReturn($file);
$file->expects($this->any())
->method('getMimeType')
->willReturn('application/pdf');
$file->expects($this->any())
->method('getName')
->willReturn('file1');
$file->expects($this->any())
->method('fopen')
->willReturn('fileresource');
$actual = $this->fileService->display($attachment);
$expected = new StreamResponse('fileresource');
$expected->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"');
$expected->addHeader('Content-Type', 'application/pdf');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$policy->addAllowedMediaDomain('\'self\'');
$policy->addAllowedMediaDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$expected->addHeader('Content-Disposition', 'attachment; filename="' . rawurldecode($file->getName()) . '"');
$this->assertEquals($expected, $actual);
}

View File

@@ -23,7 +23,6 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Card;
@@ -34,7 +33,6 @@ use OCA\Deck\Db\Label;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use \Test\TestCase;
/**
@@ -69,8 +67,6 @@ class StackServiceTest extends TestCase {
private $activityManager;
/** @var ChangeHelper|\PHPUnit\Framework\MockObject\MockObject */
private $changeHelper;
/** @var EventDispatcherInterface */
private $eventDispatcher;
public function setUp(): void {
parent::setUp();
@@ -85,7 +81,6 @@ class StackServiceTest extends TestCase {
$this->labelMapper = $this->createMock(LabelMapper::class);
$this->activityManager = $this->createMock(ActivityManager::class);
$this->changeHelper = $this->createMock(ChangeHelper::class);
$this->eventDispatcher = $this->createMock(SymfonyAdapter::class);
$this->stackService = new StackService(
$this->stackMapper,
@@ -98,7 +93,6 @@ class StackServiceTest extends TestCase {
$this->assignedUsersMapper,
$this->attachmentService,
$this->activityManager,
$this->eventDispatcher,
$this->changeHelper
);
}