Compare commits

..

1 Commits

Author SHA1 Message Date
Jakob Röhrl
6d6faf65e6 shared board option show_only_assigned_cards or all due dates
Signed-off-by: Jakob Röhrl <jakob.roehrl@web.de>
2021-03-17 09:15:11 +01:00
91 changed files with 1698 additions and 4224 deletions

55
.github/workflows/app-code-check.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Nextcloud app code check
on:
pull_request:
push:
branches:
- master
- stable*
env:
APP_NAME: deck
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4']
server-versions: ['master', 'stable18', 'stable19', 'stable20']
name: AppCode check php${{ matrix.php-versions }}-${{ matrix.server-versions }}
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v1
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: App code check
run: php occ app:check-code ${{ env.APP_NAME }}

View File

@@ -1,30 +1,7 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.4.1 - 2021-04-20
### Fixed
* [#2984](https://github.com/nextcloud/deck/pull/2984) Fix codemirror description width
* [#2990](https://github.com/nextcloud/deck/pull/2990) Fix unified comments search with postgres
* [#2994](https://github.com/nextcloud/deck/pull/2994) Remove notification on unshare and add type hints
* [#3006](https://github.com/nextcloud/deck/pull/3006) Only import debounce
* [#3008](https://github.com/nextcloud/deck/pull/3008) Do not query the lookupserver when looking for sharees
## 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
## 1.3.0 - unreleased
### 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.1</version>
<version>1.4.0</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>

View File

@@ -141,7 +141,5 @@ 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,40 +1,34 @@
{
"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",
"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"
}
"psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType"
}
}

117
composer.lock generated
View File

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

View File

@@ -69,25 +69,3 @@ 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) kartu {before} na {card}",
"{user} has renamed the card {before} to {card}" : "{user} přejmenoval(a) {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 k „%s“.",
"{user} has mentioned you in a comment on \"%s\"." : "{user} vás zmínil(a) v komentáři v „%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.",
@@ -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 tabuli",
"Board details" : "Podrobnosti o desce",
"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 s kartou aplikace Deck",
"Share with a Deck card" : "Sdílet kartu 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) kartu {before} na {card}",
"{user} has renamed the card {before} to {card}" : "{user} přejmenoval(a) {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 k „%s“.",
"{user} has mentioned you in a comment on \"%s\"." : "{user} vás zmínil(a) v komentáři v „%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.",
@@ -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 tabuli",
"Board details" : "Podrobnosti o desce",
"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 s kartou aplikace Deck",
"Share with a Deck card" : "Sdílet kartu 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,7 +109,6 @@ 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",
@@ -167,7 +166,6 @@ 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,7 +107,6 @@
"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",
@@ -165,7 +164,6 @@
"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,9 +97,6 @@ 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",
@@ -107,16 +104,9 @@ 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",
@@ -180,15 +170,9 @@ 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",
@@ -234,7 +218,6 @@ 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.",
@@ -265,14 +248,8 @@ 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}",
"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"
"Maximum file size of {size} exceeded" : "Prekoračena je maksimalna veličina datoteke od {size}"
},
"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,9 +95,6 @@
"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",
@@ -105,16 +102,9 @@
"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",
@@ -178,15 +168,9 @@
"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",
@@ -232,7 +216,6 @@
"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.",
@@ -263,14 +246,8 @@
"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}",
"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"
"Maximum file size of {size} exceeded" : "Prekoračena je maksimalna veličina datoteke od {size}"
},"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,42 +17,18 @@ 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",
@@ -67,7 +43,6 @@ 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”.",
@@ -97,9 +72,6 @@ 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",
@@ -107,16 +79,8 @@ 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",
@@ -146,8 +110,6 @@ 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",
@@ -157,8 +119,6 @@ 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)",
@@ -166,36 +126,20 @@ 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",
@@ -218,61 +162,32 @@ 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",
"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"
"Maximum file size of {size} exceeded" : "A legnagyobb fájlméret ({size}) túllépve"
},
"nplurals=2; plural=(n != 1);");

View File

@@ -15,42 +15,18 @@
"{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",
@@ -65,7 +41,6 @@
"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”.",
@@ -95,9 +70,6 @@
"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",
@@ -105,16 +77,8 @@
"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",
@@ -144,8 +108,6 @@
"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",
@@ -155,8 +117,6 @@
"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)",
@@ -164,36 +124,20 @@
"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",
@@ -216,61 +160,32 @@
"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",
"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"
"Maximum file size of {size} exceeded" : "A legnagyobb fájlméret ({size}) túllépve"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

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 katalogu tymczasowego",
"Missing a temporary folder" : "Brak folderu 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" : "Zatrzymaj udostępnianie pliku",
"Unshare file" : "Cofnij 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 katalogu tymczasowego",
"Missing a temporary folder" : "Brak folderu 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" : "Zatrzymaj udostępnianie pliku",
"Unshare file" : "Cofnij udostępnianie pliku",
"Delete Attachment" : "Usuń załącznik",
"Restore Attachment" : "Przywróć załącznik",
"File to share" : "Plik do udostępnienia",

View File

@@ -212,7 +212,7 @@ OC.L10N.register(
"Reply" : "Risponde",
"Update" : "Agiorna",
"Description" : "Descritzione",
"(Unsaved)" : "(Non sarvada)",
"(Unsaved)" : "(Non sarbada)",
"(Saving…)" : "(Sarbende…)",
"Formatting help" : "Ghia pro sa formatatzione",
"Edit description" : "Modìfica descritzione",

View File

@@ -210,7 +210,7 @@
"Reply" : "Risponde",
"Update" : "Agiorna",
"Description" : "Descritzione",
"(Unsaved)" : "(Non sarvada)",
"(Unsaved)" : "(Non sarbada)",
"(Saving…)" : "(Sarbende…)",
"Formatting help" : "Ghia pro sa formatatzione",
"Edit description" : "Modìfica descritzione",

View File

@@ -2,277 +2,69 @@ 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}",
"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." : "沒有提供用於建立附件的資料。",
"{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" : "私人的",
"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 擴充功能終止檔案的上傳",
"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" : "卡片詳細資訊",
"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- 🚀 取得您的專案組織",
"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" : "檔案已存在",
"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" : "切換簡潔模式",
"Add list" : "新增清單",
"Next 7 days" : "接下來 7 天",
"Next 30 days" : "接下來 30 天",
"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" : "刪除",
"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" : "列表已刪除",
"Delete list" : "刪除清單",
"Add card" : "增加卡片",
"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" : "已新增",
"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" : "分派給使用者",
"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" : "描述",
"(Unsaved)" : "(未儲存)",
"(Saving…)" : "(正在儲存……)",
"Formatting help" : "格式化說明",
"Edit description" : "編輯描述",
"View description" : "檢視描述",
"Add Attachment" : "新增附件",
"Write a description …" : "編寫描述……",
"Choose attachment" : "選擇附件",
"(group)" : "(群組)",
"(circle)" : "(小圈圈)",
"Assign to me" : "分配給我",
"Unassign myself" : "取消分配給我",
"(group)" : "(群組)",
"Assign to me" : "分派給我",
"Move card" : "移動卡片",
"Unarchive card" : "解除封存卡片",
"Archive card" : "封存卡片",
"Delete card" : "刪除卡片",
"Move card to another board" : "將卡片移動到其他佈告欄",
"Card deleted" : "卡片已刪除",
"Delete card" : "刪除作業",
"seconds ago" : "幾秒前",
"All boards" : "所有佈告欄",
"Archived boards" : "已封存的佈告欄",
"Shared with you" : "與您分享",
"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" : "無提醒",
"Edit board" : "編輯專案",
"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" : "連結到佈告欄",
"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" : "分享"
"Maximum file size of {size} exceeded" : "達到最大的檔案大小 {size} ",
"Error creating the share" : "建立分享時發生錯誤"
},
"nplurals=1; plural=0;");

View File

@@ -1,276 +1,68 @@
{ "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}",
"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." : "沒有提供用於建立附件的資料。",
"{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" : "私人的",
"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 擴充功能終止檔案的上傳",
"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" : "卡片詳細資訊",
"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- 🚀 取得您的專案組織",
"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" : "檔案已存在",
"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" : "切換簡潔模式",
"Add list" : "新增清單",
"Next 7 days" : "接下來 7 天",
"Next 30 days" : "接下來 30 天",
"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" : "刪除",
"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" : "列表已刪除",
"Delete list" : "刪除清單",
"Add card" : "增加卡片",
"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" : "已新增",
"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" : "分派給使用者",
"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" : "描述",
"(Unsaved)" : "(未儲存)",
"(Saving…)" : "(正在儲存……)",
"Formatting help" : "格式化說明",
"Edit description" : "編輯描述",
"View description" : "檢視描述",
"Add Attachment" : "新增附件",
"Write a description …" : "編寫描述……",
"Choose attachment" : "選擇附件",
"(group)" : "(群組)",
"(circle)" : "(小圈圈)",
"Assign to me" : "分配給我",
"Unassign myself" : "取消分配給我",
"(group)" : "(群組)",
"Assign to me" : "分派給我",
"Move card" : "移動卡片",
"Unarchive card" : "解除封存卡片",
"Archive card" : "封存卡片",
"Delete card" : "刪除卡片",
"Move card to another board" : "將卡片移動到其他佈告欄",
"Card deleted" : "卡片已刪除",
"Delete card" : "刪除作業",
"seconds ago" : "幾秒前",
"All boards" : "所有佈告欄",
"Archived boards" : "已封存的佈告欄",
"Shared with you" : "與您分享",
"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" : "無提醒",
"Edit board" : "編輯專案",
"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" : "連結到佈告欄",
"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" : "分享"
"Maximum file size of {size} exceeded" : "達到最大的檔案大小 {size} ",
"Error creating the share" : "建立分享時發生錯誤"
},"pluralForm" :"nplurals=1; plural=0;"
}

View File

@@ -23,191 +23,11 @@
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\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;
$version = \OCP\Util::getVersion()[0];
if ($version >= 20) {
class Application extends Application20 {
}
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');
});
} else {
class Application extends ApplicationLegacy {
}
}

View File

@@ -0,0 +1,252 @@
<?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

@@ -0,0 +1,249 @@
<?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

@@ -73,10 +73,11 @@ class BoardController extends ApiController {
* @param $title
* @param $color
* @param $archived
* @param $upcoming_show_only_assigned_cards
* @return \OCP\AppFramework\Db\Entity
*/
public function update($id, $title, $color, $archived) {
return $this->boardService->update($id, $title, $color, $archived);
public function update($id, $title, $color, $archived, $upcoming_show_only_assigned_cards) {
return $this->boardService->update($id, $title, $color, $archived, $upcoming_show_only_assigned_cards);
}
/**

View File

@@ -1,59 +0,0 @@
<?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

@@ -36,6 +36,7 @@ class Board extends RelationalEntity {
protected $stacks = [];
protected $deletedAt = 0;
protected $lastModified = 0;
protected $upcoming_show_only_assigned_cards = true;
protected $settings = [];
@@ -45,6 +46,7 @@ class Board extends RelationalEntity {
$this->addType('archived', 'boolean');
$this->addType('deletedAt', 'integer');
$this->addType('lastModified', 'integer');
$this->addType('upcoming_show_only_assigned_cards', 'boolean');
$this->addRelation('labels');
$this->addRelation('acl');
$this->addRelation('shared');

View File

@@ -66,7 +66,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
* @throws DoesNotExistException
*/
public function find($id, $withLabels = false, $withAcl = false) {
$sql = 'SELECT id, title, owner, color, archived, deleted_at, last_modified FROM `*PREFIX*deck_boards` ' .
$sql = 'SELECT id, title, owner, color, archived, deleted_at, last_modified, upcoming_show_only_assigned_cards FROM `*PREFIX*deck_boards` ' .
'WHERE `id` = ?';
$board = $this->findEntity($sql, [$id]);
@@ -105,12 +105,12 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
*/
public function findAllByUser($userId, $limit = null, $offset = null, $since = -1, $includeArchived = true) {
// FIXME: One moving to QBMapper we should allow filtering the boards probably by method chaining for additional where clauses
$sql = 'SELECT id, title, owner, color, archived, deleted_at, 0 as shared, last_modified FROM `*PREFIX*deck_boards` WHERE owner = ? AND last_modified > ?';
$sql = 'SELECT id, title, owner, color, archived, deleted_at, 0 as shared, last_modified, upcoming_show_only_assigned_cards FROM `*PREFIX*deck_boards` WHERE owner = ? AND last_modified > ?';
if (!$includeArchived) {
$sql .= ' AND NOT archived AND deleted_at = 0';
}
$sql .= ' UNION ' .
'SELECT boards.id, title, owner, color, archived, deleted_at, 1 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
'SELECT boards.id, title, owner, color, archived, deleted_at, 1 as shared, last_modified, upcoming_show_only_assigned_cards FROM `*PREFIX*deck_boards` as boards ' .
'JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE acl.participant=? AND acl.type=? AND boards.owner != ? AND last_modified > ?';
if (!$includeArchived) {
$sql .= ' AND NOT archived AND deleted_at = 0';
@@ -142,7 +142,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if (count($groups) <= 0) {
return [];
}
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified, upcoming_show_only_assigned_cards FROM `*PREFIX*deck_boards` as boards ' .
'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND (';
for ($i = 0, $iMax = count($groups); $i < $iMax; $i++) {
$sql .= 'acl.participant = ? ';
@@ -174,7 +174,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
return [];
}
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified, upcoming_show_only_assigned_cards FROM `*PREFIX*deck_boards` as boards ' .
'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND (';
for ($i = 0, $iMax = count($circles); $i < $iMax; $i++) {
$sql .= 'acl.participant = ? ';

View File

@@ -49,9 +49,6 @@ class Card extends RelationalEntity {
protected $notified = false;
protected $deletedAt = 0;
protected $commentsUnread = 0;
protected $relatedStack = null;
protected $relatedBoard = null;
private $databaseType = 'sqlite';
@@ -76,9 +73,6 @@ class Card extends RelationalEntity {
$this->addRelation('participants');
$this->addRelation('commentsUnread');
$this->addResolvable('owner');
$this->addRelation('relatedStack');
$this->addRelation('relatedBoard');
}
public function setDatabaseType($type) {
@@ -125,8 +119,6 @@ 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,16 +23,11 @@
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;
@@ -42,8 +37,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
private $labelMapper;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var IManager */
private $notificationManager;
private $databaseType;
@@ -53,15 +46,13 @@ class CardMapper extends QBMapper implements IPermissionMapper {
IDBConnection $db,
LabelMapper $labelMapper,
IUserManager $userManager,
IGroupManager $groupManager,
IManager $notificationManager,
$databaseType = 'sqlite3',
$databaseType = 'sqlite',
$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;
@@ -126,7 +117,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
->addOrderBy('id');
/** @var Card $card */
$card = $this->findEntity($qb);
$labels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
$labels = $this->labelMapper->findAssignedLabelsForCard($card->id);
$card->setLabels($labels);
$this->mapOwner($card);
return $card;
@@ -158,7 +149,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function queryCardsByBoards(array $boardIds): IQueryBuilder {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
$qb->select('c.*', 's.board_id')
->selectAlias('s.title', 'stack_title')
->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)));
@@ -269,213 +261,27 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function search(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
public function search($boardIds, $term, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoards($boardIds);
$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->andWhere($qb->expr()->lt('c.last_modified', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
$result = $qb->execute();
$entities = [];
while ($row = $result->fetch()) {
$entities[] = Card::fromRow($row);
}
$result->closeCursor();
return $entities;
}
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', $qb->expr()->castColumn('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', 'c.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)));
$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) . '%'))
)
);
if ($limit !== null) {
$qb->setMaxResults($limit);
}
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));
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
return $this->findEntities($qb);
}
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 = $this->queryCardsByBoards($boardIds);
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere(
$qb->expr()->orX(

View File

@@ -1,44 +0,0 @@
<?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

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

View File

@@ -1,30 +0,0 @@
<?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

@@ -1,30 +0,0 @@
<?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

@@ -1,30 +0,0 @@
<?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

@@ -1,30 +0,0 @@
<?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

@@ -1,30 +0,0 @@
<?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

@@ -1,6 +1,6 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
/**
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
@@ -21,24 +21,31 @@
*
*/
declare(strict_types=1);
namespace OCA\Deck\Event;
use OCA\Deck\Db\Acl;
use OCP\EventDispatcher\Event;
abstract class AAclEvent extends Event {
private $acl;
public function __construct(Acl $acl) {
/**
* 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 = []) {
parent::__construct();
$this->acl = $acl;
$this->arguments = $arguments;
}
public function getAcl(): Acl {
return $this->acl;
public function getArgument($key) {
if (isset($this->arguments[$key])) {
return $this->arguments[$key];
}
throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key));
}
}

View File

@@ -1,107 +0,0 @@
<?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,26 @@
<?php
declare(strict_types=1);
namespace OCA\Deck\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version010404Date20210305 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('deck_boards');
if (!$table->hasColumn('upcoming_show_only_assigned_cards')) {
$table->addColumn('upcoming_show_only_assigned_cards', 'boolean', [
'notnull' => false,
'default' => true
]);
}
return $schema;
}
}

View File

@@ -1,7 +1,4 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
@@ -27,24 +24,19 @@ declare(strict_types=1);
namespace OCA\Deck\Notification;
use DateTime;
use Exception;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\User;
use OCA\Deck\Service\ConfigService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Comments\IComment;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
class NotificationHelper {
@@ -88,10 +80,10 @@ class NotificationHelper {
}
/**
* @throws DoesNotExistException
* @throws Exception thrown on invalid due date
* @param $card
* @throws \OCP\AppFramework\Db\DoesNotExistException
*/
public function sendCardDuedate(Card $card): void {
public function sendCardDuedate($card) {
// check if notification has already been sent
// ideally notifications should not be deleted once seen by the user so we can
// also deliver due date notifications for users who have been added later to a board
@@ -125,7 +117,7 @@ class NotificationHelper {
$notification
->setApp('deck')
->setUser((string)$user->getUID())
->setObject('card', (string)$card->getId())
->setObject('card', $card->getId())
->setSubject('card-overdue', [
$card->getTitle(), $board->getTitle()
])
@@ -136,29 +128,25 @@ class NotificationHelper {
$this->cardMapper->markNotified($card);
}
public function markDuedateAsRead(Card $card): void {
public function markDuedateAsRead($card) {
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setObject('card', (string)$card->getId())
->setObject('card', $card->getId())
->setSubject('card-overdue', []);
$this->notificationManager->markProcessed($notification);
}
public function sendCardAssigned(Card $card, string $userId): void {
public function sendCardAssigned($card, $userId) {
$boardId = $this->cardMapper->findBoardId($card->getId());
try {
$board = $this->getBoard($boardId);
} catch (Exception $e) {
return;
}
$board = $this->getBoard($boardId);
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setUser($userId)
->setUser((string) $userId)
->setDateTime(new DateTime())
->setObject('card', (string)$card->getId())
->setObject('card', $card->getId())
->setSubject('card-assigned', [
$card->getTitle(),
$board->getTitle(),
@@ -167,56 +155,29 @@ class NotificationHelper {
$this->notificationManager->notify($notification);
}
public function markCardAssignedAsRead(Card $card, string $userId): void {
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setUser($userId)
->setObject('card', (string)$card->getId())
->setSubject('card-assigned', []);
$this->notificationManager->markProcessed($notification);
}
/**
* Send notifications that a board was shared with a user/group
*
* @param $boardId
* @param Acl $acl
* @throws \InvalidArgumentException
*/
public function sendBoardShared(int $boardId, Acl $acl, bool $markAsRead = false): void {
try {
$board = $this->getBoard($boardId);
} catch (Exception $e) {
return;
}
public function sendBoardShared($boardId, $acl) {
$board = $this->getBoard($boardId);
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
$notification = $this->generateBoardShared($board, $acl->getParticipant());
if ($markAsRead) {
$this->notificationManager->markProcessed($notification);
} else {
$notification->setDateTime(new DateTime());
$this->notificationManager->notify($notification);
}
$this->notificationManager->notify($notification);
}
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
$group = $this->groupManager->get($acl->getParticipant());
if ($group === null) {
return;
}
foreach ($group->getUsers() as $user) {
if ($user->getUID() === $this->currentUser) {
continue;
}
$notification = $this->generateBoardShared($board, $user->getUID());
if ($markAsRead) {
$this->notificationManager->markProcessed($notification);
} else {
$notification->setDateTime(new DateTime());
$this->notificationManager->notify($notification);
}
$this->notificationManager->notify($notification);
}
}
}
public function sendMention(IComment $comment): void {
public function sendMention(IComment $comment) {
foreach ($comment->getMentions() as $mention) {
$card = $this->cardMapper->find($comment->getObjectId());
$boardId = $this->cardMapper->findBoardId($card->getId());
@@ -233,22 +194,27 @@ class NotificationHelper {
}
/**
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @param $boardId
* @return Board
* @throws \OCP\AppFramework\Db\DoesNotExistException
*/
private function getBoard(int $boardId, bool $withLabels = false, bool $withAcl = false): Board {
private function getBoard($boardId, bool $withLabels = false, bool $withAcl = false) {
if (!array_key_exists($boardId, $this->boards)) {
$this->boards[$boardId] = $this->boardMapper->find($boardId, $withLabels, $withAcl);
}
return $this->boards[$boardId];
}
private function generateBoardShared(Board $board, string $userId): INotification {
/**
* @param Board $board
*/
private function generateBoardShared($board, $userId) {
$notification = $this->notificationManager->createNotification();
$notification
->setApp('deck')
->setUser($userId)
->setObject('board', (string)$board->getId())
->setUser((string) $userId)
->setDateTime(new DateTime())
->setObject('board', $board->getId())
->setSubject('board-shared', [$board->getTitle(), $this->currentUser]);
return $notification;
}

View File

@@ -1,84 +0,0 @@
<?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

@@ -1,51 +0,0 @@
<?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,7 +28,9 @@ namespace OCA\Deck\Search;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Service\SearchService;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Service\BoardService;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Search\IProvider;
@@ -38,19 +40,31 @@ use OCP\Search\SearchResult;
class DeckProvider implements IProvider {
/**
* @var SearchService
* @var BoardService
*/
private $searchService;
private $boardService;
/**
* @var CardMapper
*/
private $cardMapper;
/**
* @var StackMapper
*/
private $stackMapper;
/**
* @var IURLGenerator
*/
private $urlGenerator;
public function __construct(
SearchService $searchService,
BoardService $boardService,
StackMapper $stackMapper,
CardMapper $cardMapper,
IURLGenerator $urlGenerator
) {
$this->searchService = $searchService;
$this->boardService = $boardService;
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
$this->urlGenerator = $urlGenerator;
}
@@ -63,34 +77,37 @@ class DeckProvider implements IProvider {
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
$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);
$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;
$results = array_merge(
array_map(function (Board $board) {
return new BoardSearchResultEntry($board, $this->urlGenerator);
}, $boardResults),
array_map(function (Card $card) {
return new CardSearchResultEntry($card->getRelatedBoard(), $card->getRelatedStack(), $card, $this->urlGenerator);
}, $cardResults)
}, $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)
);
if (count($cardResults) < $query->getLimit()) {
return SearchResult::complete(
'Deck',
$results
);
}
return SearchResult::paginated(
return SearchResult::complete(
'Deck',
$results,
$cardResults[count($results) - 1]->getLastModified()
$results
);
}
public function getOrder(string $route, array $routeParameters): int {
if ($route === 'deck.Page.index') {
if ($route === 'deck.page.index') {
return -5;
}
return 10;

View File

@@ -1,125 +0,0 @@
<?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

@@ -1,49 +0,0 @@
<?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

@@ -1,38 +0,0 @@
<?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

@@ -1,109 +0,0 @@
<?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

@@ -1,39 +0,0 @@
<?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\CardUpdatedEvent;
use OCA\Deck\Event\FTSEvent;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCA\Deck\Notification\NotificationHelper;
@@ -74,8 +74,6 @@ class AssignmentService {
* @var IEventDispatcher
*/
private $eventDispatcher;
/** @var string|null */
private $currentUser;
public function __construct(
PermissionService $permissionService,
@@ -140,7 +138,8 @@ class AssignmentService {
}
if ($type === Assignment::TYPE_USER && $userId !== $this->currentUser) {
if ($userId !== $this->currentUser) {
/* Notifyuser about the card assignment */
$this->notificationHelper->sendCardAssigned($card, $userId);
}
@@ -152,7 +151,9 @@ class AssignmentService {
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_ASSIGN, ['assigneduser' => $userId]);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
return $assignment;
}
@@ -184,13 +185,11 @@ class AssignmentService {
$assignment = $this->assignedUsersMapper->delete($assignment);
$card = $this->cardMapper->find($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_UNASSIGN, ['assigneduser' => $userId]);
if ($type === Assignment::TYPE_USER && $userId !== $this->currentUser) {
$this->notificationHelper->markCardAssignedAsRead($card, $userId);
}
$this->changeHelper->cardChanged($cardId);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
return $assignment;
}

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\AppInfo\Application;
@@ -35,13 +36,9 @@ 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;
@@ -50,6 +47,7 @@ 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;
@@ -85,7 +83,7 @@ class BoardService {
IUserManager $userManager,
IGroupManager $groupManager,
ActivityManager $activityManager,
IEventDispatcher $eventDispatcher,
SymfonyAdapter $eventDispatcher,
ChangeHelper $changeHelper,
$userId
) {
@@ -329,6 +327,13 @@ 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;
}
@@ -355,6 +360,10 @@ 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;
}
@@ -377,6 +386,10 @@ 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;
}
@@ -397,6 +410,10 @@ 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;
}
@@ -405,13 +422,14 @@ class BoardService {
* @param $title
* @param $color
* @param $archived
* @param $upcoming_show_only_assigned_cards
* @return \OCP\AppFramework\Db\Entity
* @throws DoesNotExistException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function update($id, $title, $color, $archived) {
public function update($id, $title, $color, $archived, $upcoming_show_only_assigned_cards) {
if (is_numeric($id) === false) {
throw new BadRequestException('board id must be a number');
}
@@ -428,18 +446,27 @@ class BoardService {
throw new BadRequestException('archived must be a boolean');
}
if (is_bool($upcoming_show_only_assigned_cards) === false) {
throw new BadRequestException('upcoming_show_only_assigned_cards must be a boolean');
}
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
$board = $this->find($id);
$changes = new ChangeSet($board);
$board->setTitle($title);
$board->setColor($color);
$board->setArchived($archived);
$board->setUpcoming_show_only_assigned_cards($upcoming_show_only_assigned_cards);
$changes->setAfter($board);
$this->boardMapper->update($board); // operate on clone so we can check for updated fields
$this->boardMapper->mapOwner($board);
$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;
}
@@ -510,21 +537,28 @@ class BoardService {
$acl->setPermissionEdit($edit);
$acl->setPermissionShare($share);
$acl->setPermissionManage($manage);
$newAcl = $this->aclMapper->insert($acl);
/* Notify users about the shared board */
$this->notificationHelper->sendBoardShared($boardId, $acl);
$newAcl = $this->aclMapper->insert($acl);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE);
$this->notificationHelper->sendBoardShared((int)$boardId, $acl);
$this->boardMapper->mapAcl($newAcl);
$this->changeHelper->boardChanged($boardId);
// TODO: use the dispatched event for this
try {
$resourceProvider = \OC::$server->query(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
$resourceProvider->invalidateAccessCache($boardId);
} catch (\Exception $e) {
$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) {
}
}
$this->eventDispatcher->dispatchTyped(new AclCreatedEvent($acl));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onShareNew', new GenericEvent(null, ['id' => $newAcl->getId(), 'acl' => $newAcl, 'boardId' => $boardId])
);
return $newAcl;
}
@@ -569,7 +603,9 @@ class BoardService {
$board = $this->aclMapper->update($acl);
$this->changeHelper->boardChanged($acl->getBoardId());
$this->eventDispatcher->dispatchTyped(new AclUpdatedEvent($acl));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onShareEdit', new GenericEvent(null, ['id' => $id, 'boardId' => $acl->getBoardId(), 'acl' => $acl])
);
return $board;
}
@@ -597,9 +633,7 @@ class BoardService {
$this->assignedUsersMapper->delete($assignement);
}
}
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
$this->notificationHelper->sendBoardShared($acl->getBoardId(), $acl, true);
$this->changeHelper->boardChanged($acl->getBoardId());
$version = \OCP\Util::getVersion()[0];
@@ -612,7 +646,9 @@ class BoardService {
}
$delete = $this->aclMapper->delete($acl);
$this->eventDispatcher->dispatchTyped(new AclDeletedEvent($acl));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onShareDelete', new GenericEvent(null, ['id' => $id, 'boardId' => $acl->getBoardId(), 'acl' => $acl])
);
return $delete;
}

View File

@@ -29,14 +29,13 @@ 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\CardCreatedEvent;
use OCA\Deck\Event\CardDeletedEvent;
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\Event\FTSEvent;
use OCA\Deck\Notification\NotificationHelper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\LabelMapper;
@@ -107,11 +106,6 @@ 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) {
@@ -123,6 +117,22 @@ 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
@@ -212,10 +222,15 @@ 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->dispatchTyped(new CardCreatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onCreate',
new FTSEvent(
null, ['id' => $card->getId(), 'card' => $card, 'userId' => $owner, 'stackId' => $stackId]
)
);
return $card;
}
@@ -241,11 +256,12 @@ 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->notificationHelper->markDuedateAsRead($card);
$this->changeHelper->cardChanged($card->getId(), false);
$this->eventDispatcher->dispatchTyped(new CardDeletedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onDelete', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
return $card;
}
@@ -323,15 +339,6 @@ class CardService {
$card->setOrder($order);
$card->setOwner($owner);
$card->setDuedate($duedate);
$resetDuedateNotification = false;
if (
$card->getDuedate() === null ||
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate()))
) {
$card->setNotified(false);
$resetDuedateNotification = true;
}
if ($deletedAt !== null) {
$card->setDeletedAt($deletedAt);
}
@@ -351,12 +358,11 @@ class CardService {
$card = $this->cardMapper->update($card);
if ($resetDuedateNotification) {
$this->notificationHelper->markDuedateAsRead($card);
}
$this->changeHelper->cardChanged($card->getId(), true);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
return $card;
}
@@ -396,7 +402,9 @@ class CardService {
$this->changeHelper->cardChanged($card->getId(), false);
$update = $this->cardMapper->update($card);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
return $update;
}
@@ -493,7 +501,9 @@ class CardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_ARCHIVE);
$this->changeHelper->cardChanged($id, false);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
return $newCard;
}
@@ -522,7 +532,9 @@ class CardService {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_UNARCHIVE);
$this->changeHelper->cardChanged($id, false);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $id, 'card' => $card])
);
return $newCard;
}
@@ -558,7 +570,9 @@ class CardService {
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
}
/**
@@ -592,7 +606,9 @@ class CardService {
$this->changeHelper->cardChanged($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatch(
'\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card])
);
}
/**

View File

@@ -35,7 +35,7 @@ use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IPreview;
use OCP\IRequest;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share;
use OCP\Share\IManager;
use OCP\Share\IShare;
@@ -140,10 +140,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
}
public function display(Attachment $attachment) {
/** @psalm-suppress InvalidCatch */
try {
$share = $this->shareProvider->getShareById($attachment->getId());
} catch (ShareNotFound $e) {
} catch (Share\Exceptions\ShareNotFound $e) {
throw new NotFoundException('File not found');
}
$file = $share->getNode();

View File

@@ -37,10 +37,14 @@ 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;
/**
@@ -59,15 +63,98 @@ 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
BoardMapper $boardMapper, StackMapper $stackMapper, CardMapper $cardMapper,
IFullTextSearchManager $fullTextSearchManager
) {
$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
*
@@ -88,9 +175,11 @@ class FullTextSearchService {
* @throws MultipleObjectsReturnedException
*/
public function fillIndexDocument(IIndexDocument $document) {
/** @var Card $card */
$card = $this->cardMapper->find((int)$document->getId());
$document->setTitle(!empty($card->getTitle()) ? $card->getTitle() : '');
$document->setContent(!empty($card->getDescription()) ? $card->getDescription() : '');
$document->setTitle(($card->getTitle() === null) ? '' : $card->getTitle());
$document->setContent(($card->getDescription() === null) ? '' : $card->getDescription());
$document->setAccess($this->generateDocumentAccessFromCardId((int)$card->getId()));
}

View File

@@ -1,117 +0,0 @@
<?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,6 +24,7 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\BadRequestException;
@@ -36,6 +37,7 @@ 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;
@@ -62,6 +64,7 @@ class StackService {
AssignmentMapper $assignedUsersMapper,
AttachmentService $attachmentService,
ActivityManager $activityManager,
SymfonyAdapter $eventDispatcher,
ChangeHelper $changeHelper
) {
$this->stackMapper = $stackMapper;
@@ -74,6 +77,7 @@ class StackService {
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
$this->activityManager = $activityManager;
$this->symfonyAdapter = $eventDispatcher;
$this->changeHelper = $changeHelper;
}
@@ -221,6 +225,11 @@ class StackService {
);
$this->changeHelper->boardChanged($boardId);
$this->symfonyAdapter->dispatch(
'\OCA\Deck\Stack::onCreate',
new GenericEvent(null, ['id' => $stack->getId(), 'stack' => $stack])
);
return $stack;
}
@@ -250,6 +259,10 @@ 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;
}
@@ -301,6 +314,10 @@ 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,7 +562,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
/**
* @inheritDoc
* @throws ShareNotFound
*/
public function getShareById($id, $recipientId = null) {
$qb = $this->dbConnection->getQueryBuilder();

781
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.10",
"@babel/runtime": "^7.13.9",
"@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.8.0",
"@nextcloud/vue": "^3.6.0",
"@nextcloud/vue-dashboard": "^1.1.0",
"blueimp-md5": "^2.18.0",
"dompurify": "^2.2.7",
"dompurify": "^2.2.6",
"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.1",
"url-search-params-polyfill": "^8.1.0",
"vue": "^2.6.12",
"vue-at": "^2.5.0-beta.2",
"vue-click-outside": "^1.1.0",
"vue-easymde": "^1.4.0",
"vue-easymde": "^1.3.2",
"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.14",
"@babel/core": "^7.13.8",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.13.12",
"@babel/preset-env": "^7.13.10",
"@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.1.0",
"acorn": "^8.0.5",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
@@ -99,8 +99,8 @@
"raw-loader": "^4.0.2",
"sass-loader": "^10.1.1",
"style-loader": "^1.3.0",
"stylelint": "^13.12.0",
"stylelint-config-recommended": "^4.0.0",
"stylelint": "^13.11.0",
"stylelint-config-recommended": "^3.0.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.19.0",
"stylelint-webpack-plugin": "^2.1.1",

View File

@@ -33,14 +33,8 @@
({{ t('deck', 'Archived cards') }})
</p>
</div>
<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"
<div v-if="board" class="board-actions">
<div v-if="canManage && !showArchived && !board.archived"
id="stack-add"
v-click-outside="hideAddStack">
<Actions v-if="!isAddStackVisible">
@@ -63,7 +57,7 @@
value="">
</form>
</div>
<div v-if="board" class="board-action-buttons">
<div 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" />
@@ -243,7 +237,6 @@ export default {
]),
...mapState({
compactMode: state => state.compactMode,
searchQuery: state => state.searchQuery,
}),
detailsRoute() {
return {
@@ -381,13 +374,6 @@ export default {
}
}
.deck-search {
input[type=search] {
background-position: 5px;
padding-left: 24px;
}
}
.filter--item {
input + label {
display: block;

View File

@@ -65,7 +65,6 @@
<p />
</div>
</transition>
<GlobalSearchResults />
</div>
</template>
@@ -76,12 +75,10 @@ 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,
@@ -181,17 +178,13 @@ export default {
width: 100%;
height: 100%;
max-height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.board {
padding-left: $board-spacing;
position: relative;
max-height: calc(100% - 44px);
overflow: hidden;
overflow-x: auto;
flex-grow: 1;
height: calc(100% - 44px);
overflow-x: scroll;
}
/**

View File

@@ -73,7 +73,7 @@ import { CollectionList } from 'nextcloud-vue-collections'
import { mapGetters, mapState } from 'vuex'
import { getCurrentUser } from '@nextcloud/auth'
import { showError } from '@nextcloud/dialogs'
import debounce from 'lodash/debounce'
import { debounce } from 'lodash'
export default {
name: 'SharingTabSidebar',

View File

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

View File

@@ -306,7 +306,6 @@ h5 {
padding: 0;
background-color: var(--color-main-background);
color: var(--color-main-text);
width: 100%;
}
.CodeMirror-placeholder {

View File

@@ -26,16 +26,12 @@
<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, 'card__archived': card.archived }"
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'is-editing': editing, 'card__editable': canEdit}"
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 || standalone">
<h3 v-if="compactMode || isArchived || showArchived || !canEdit">
{{ card.title }}
</h3>
<h3 v-else-if="!editing">
@@ -102,10 +98,6 @@ export default {
type: Object,
default: null,
},
standalone: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -122,12 +114,6 @@ 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
@@ -247,9 +233,6 @@ export default {
&.card__editable .card-controls {
margin-right: 0;
}
&.card__archived {
background-color: var(--color-background-dark);
}
}
.duedate {
@@ -261,24 +244,6 @@ 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>
<Actions v-if="canEdit && !isArchived">
<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()">
{{ card.archived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
{{ showArchived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
</ActionButton>
<ActionButton v-if="showArchived === false"
icon="icon-delete"

View File

@@ -112,6 +112,12 @@
{{ dueDateReminderText }}
</ActionButton>
<ActionCheckbox v-if="canManage"
:checked="board.upcoming_show_only_assigned_cards"
@change="actionToggleUpcoming_show_only_assigned_cards">
{{ t('deck', 'Show only cards assigned to me in upcoming view') }}
</ActionCheckbox>
<ActionButton v-if="canManage && !isDueSubmenuActive"
icon="icon-delete"
:close-after-click="true"
@@ -133,7 +139,7 @@
</template>
<script>
import { AppNavigationIconBullet, AppNavigationCounter, AppNavigationItem, ColorPicker, Actions, ActionButton } from '@nextcloud/vue'
import { AppNavigationIconBullet, AppNavigationCounter, AppNavigationItem, ColorPicker, Actions, ActionButton, ActionCheckbox } from '@nextcloud/vue'
import ClickOutside from 'vue-click-outside'
export default {
@@ -145,6 +151,7 @@ export default {
ColorPicker,
Actions,
ActionButton,
ActionCheckbox,
},
directives: {
ClickOutside,
@@ -235,7 +242,9 @@ export default {
try {
const newBoard = await this.$store.dispatch('cloneBoard', this.board)
this.loading = false
this.$router.push({ name: 'board', params: { id: newBoard.id } })
const route = this.routeTo
route.params.id = newBoard.id
this.$router.push(route)
} catch (e) {
OC.Notification.showTemporary(t('deck', 'An error occurred'))
console.error(e)
@@ -276,7 +285,9 @@ export default {
)
},
actionDetails() {
this.$router.push({ name: 'board.details', params: { id: this.board.id } })
const route = this.routeTo
route.name = 'board.details'
this.$router.push(route)
},
applyEdit(e) {
this.editing = false
@@ -294,6 +305,11 @@ 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 = {}
@@ -302,6 +318,9 @@ export default {
this.isDueSubmenuActive = false
this.updateDueSetting = null
},
actionToggleUpcoming_show_only_assigned_cards() {
this.$store.dispatch('toggleUpcoming_show_only_assigned_cards', this.board)
},
},
inject: [
'boardApi',

View File

@@ -73,8 +73,6 @@
</div>
</div>
</div>
<GlobalSearchResults />
</div>
</template>
@@ -84,7 +82,6 @@ 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'
@@ -95,7 +92,6 @@ const SUPPORTED_FILTERS = [
export default {
name: 'Overview',
components: {
GlobalSearchResults,
Controls,
CardItem,
},
@@ -207,8 +203,6 @@ export default {
width: 100%;
height: 100%;
max-height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
.overview {

View File

@@ -1,199 +0,0 @@
<!--
- @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

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

View File

@@ -21,7 +21,6 @@
*/
import { CardApi } from './../services/CardApi'
import moment from 'moment'
import Vue from 'vue'
const apiClient = new CardApi()
@@ -87,90 +86,8 @@ export default {
return true
}
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
return card.title.toLowerCase().includes(getters.getSearchQuery.toLowerCase())
|| card.description.toLowerCase().includes(getters.getSearchQuery.toLowerCase())
})
.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt)
},
@@ -293,7 +210,7 @@ export default {
}
const updatedCard = await apiClient[call](card)
commit('updateCard', updatedCard)
commit('deleteCard', updatedCard)
},
async assignCardToUser({ commit }, { card, assignee }) {
const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type)
@@ -319,14 +236,5 @@ 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,9 +91,6 @@ 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 })),
@@ -311,6 +308,13 @@ export default new Vuex.Store({
Vue.delete(state.currentBoard.acl, removeIndex)
}
},
toggleUpcoming_show_only_assigned_cards(state, board) {
let currentBoard = state.boards.filter((b) => {
return board.id === b.id
})
currentBoard = currentBoard[0]
Vue.set(currentBoard, 'upcoming_show_only_assigned_cards', board.upcoming_show_only_assigned_cards)
},
},
actions: {
@@ -420,7 +424,6 @@ export default new Vuex.Store({
params.append('format', 'json')
params.append('perPage', 20)
params.append('itemType', [0, 1, 4, 7])
params.append('lookup', false)
const response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1') + 'sharees', { params })
commit('setSharees', response.data.ocs.data)
@@ -494,5 +497,13 @@ export default new Vuex.Store({
dispatch('loadBoardById', acl.boardId)
})
},
toggleUpcoming_show_only_assigned_cards({ commit }, board) {
const boardCopy = JSON.parse(JSON.stringify(board))
boardCopy.upcoming_show_only_assigned_cards = !boardCopy.upcoming_show_only_assigned_cards
apiClient.updateBoard(boardCopy)
.then((board) => {
commit('toggleUpcoming_show_only_assigned_cards', board)
})
},
},
})

View File

@@ -5,8 +5,10 @@ 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
- CommentContext
- SearchContext

View File

@@ -1,7 +1,6 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;
@@ -17,39 +16,25 @@ 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');
}
public function getLastUsedCard() {
return $this->card;
}
/**
* @Given /^creates a board named "([^"]*)" with color "([^"]*)"$/
*/
public function createsABoardNamedWithColor($title, $color) {
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/boards', [
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards', [
'title' => $title,
'color' => $color
]);
$this->getResponse()->getBody()->seek(0);
$this->board = json_decode((string)$this->getResponse()->getBody(), true);
$this->response->getBody()->seek(0);
$this->board = json_decode((string)$this->response->getBody(), true);
}
/**
* @When /^fetches the board named "([^"]*)"$/
*/
public function fetchesTheBoardNamed($boardName) {
$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);
$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);
}
/**
@@ -63,7 +48,7 @@ class BoardContext implements Context {
];
$tableRows = isset($permissions) ? $permissions->getRowsHash() : [];
$result = array_merge($defaults, $tableRows);
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
'type' => 0,
'participant' => $user,
'permissionEdit' => $result['permissionEdit'] === '1',
@@ -83,7 +68,7 @@ class BoardContext implements Context {
];
$tableRows = isset($permissions) ? $permissions->getRowsHash() : [];
$result = array_merge($defaults, $tableRows);
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
$this->sendJSONrequest('POST', '/index.php/apps/deck/boards/' . $this->board['id'] . '/acl', [
'type' => 1,
'participant' => $group,
'permissionEdit' => $result['permissionEdit'] === '1',
@@ -97,38 +82,38 @@ class BoardContext implements Context {
* @When /^fetching the board list$/
*/
public function fetchingTheBoardList() {
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards');
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards');
}
/**
* @When /^fetching the board with id "([^"]*)"$/
*/
public function fetchingTheBoardWithId($id) {
$this->requestContext->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $id);
$this->sendJSONrequest('GET', '/index.php/apps/deck/boards/' . $id);
}
/**
* @Given /^create a stack named "([^"]*)"$/
*/
public function createAStackNamed($name) {
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/stacks', [
$this->sendJSONrequest('POST', '/index.php/apps/deck/stacks', [
'title' => $name,
'boardId' => $this->board['id']
]);
$this->requestContext->getResponse()->getBody()->seek(0);
$this->stack = json_decode((string)$this->getResponse()->getBody(), true);
$this->response->getBody()->seek(0);
$this->stack = json_decode((string)$this->response->getBody(), true);
}
/**
* @Given /^create a card named "([^"]*)"$/
*/
public function createACardNamed($name) {
$this->requestContext->sendJSONrequest('POST', '/index.php/apps/deck/cards', [
$this->sendJSONrequest('POST', '/index.php/apps/deck/cards', [
'title' => $name,
'stackId' => $this->stack['id']
]);
$this->requestContext->getResponse()->getBody()->seek(0);
$this->card = json_decode((string)$this->getResponse()->getBody(), true);
$this->response->getBody()->seek(0);
$this->card = json_decode((string)$this->response->getBody(), true);
}
/**
@@ -166,70 +151,4 @@ 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

@@ -1,31 +0,0 @@
<?php
use Behat\Behat\Context\Context;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
require_once __DIR__ . '/../../vendor/autoload.php';
class CommentContext implements Context {
use RequestTrait;
/** @var BoardContext */
protected $boardContext;
/** @BeforeScenario */
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->boardContext = $environment->getContext('BoardContext');
}
/**
* @Given /^post a comment with content "([^"]*)" on the card$/
*/
public function postACommentWithContentOnTheCard($content) {
$card = $this->boardContext->getLastUsedCard();
$this->requestContext->sendOCSRequest('POST', '/apps/deck/api/v1.0/cards/' . $card['id'] . '/comments', [
'message' => $content,
'parentId' => null
]);
}
}

View File

@@ -1,140 +0,0 @@
<?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,46 +1,121 @@
<?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;
/** @var RequestContext */
protected $requestContext;
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;
/** @BeforeScenario */
public function gatherRequestTraitContext(BeforeScenarioScope $scope) {
public function gatherContexts(BeforeScenarioScope $scope) {
$environment = $scope->getEnvironment();
$this->requestContext = $environment->getContext('RequestContext');
$this->serverContext = $environment->getContext('ServerContext');
}
public function getResponse() {
return $this->requestContext->getResponse();
/**
* @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();
}
}
}

View File

@@ -1,123 +0,0 @@
<?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;
private $unifiedSearchResult;
/** @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 "([^"]*)" in comments in unified search$/
* @param string $term
* https://cloud.nextcloud.com/ocs/v2.php/search/providers/talk-conversations/search?term=an&from=%2Fapps%2Fdashboard%2F
*/
public function searchingForComments(string $term) {
$this->requestContext->sendOCSRequest('GET', '/search/providers/deck-comment/search?term=' . urlencode($term), []);
$this->requestContext->getResponse()->getBody()->seek(0);
$data = (string)$this->getResponse()->getBody();
$this->unifiedSearchResult = 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');
}
/**
* @Then /^the comment with "([^"]*)" is found$/
*/
public function theCommentWithIsFound($arg1) {
$ocsData = $this->unifiedSearchResult['ocs']['data']['entries'];
$found = null;
foreach ($ocsData as $result) {
if ($result['subline'] === $arg1) {
$found = $result;
}
}
Assert::assertNotNull($found, 'Comment was expected but was not found');
Assert::assertEquals('admin on Card with comment', $found['title']);
}
/**
* @Then /^the comment with "([^"]*)" is not found$/
*/
public function theCommentWithIsNotFound($arg1) {
$ocsData = $this->unifiedSearchResult['ocs']['data']['entries'];
$found = null;
foreach ($ocsData as $result) {
if ($result['subline'] === $arg1) {
$found = $result;
}
}
Assert::assertNull($found, 'Comment was found but not expected');
}
}

View File

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

View File

@@ -4,6 +4,23 @@ 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

@@ -1,266 +0,0 @@
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
Scenario: Search for a card comment
Given create a card named "Card with comment"
And post a comment with content "My first comment" on the card
When searching for "My first comment" in comments in unified search
Then the comment with "My first comment" is found
Then the comment with "Any other" is not found

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.7.0@d4377c0baf3ffbf0b1ec6998e8d1be2a40971005">
<files psalm-version="4.4.1@9fd7a7d885b3a216cff8dec9d8c21a132f275224">
<file src="lib/Activity/ActivityManager.php">
<TypeDoesNotContainType occurrences="1">
<code>$message !== null</code>
@@ -10,6 +10,16 @@
<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>
@@ -116,16 +126,12 @@
</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">
@@ -265,12 +271,11 @@
</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>ShareNotFound</code>
<code>Share\Exceptions\ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Service/PermissionService.php">
@@ -279,13 +284,6 @@
<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>
@@ -298,7 +296,7 @@
<InvalidReturnType occurrences="1">
<code>getSharesInFolder</code>
</InvalidReturnType>
<MissingDependency occurrences="8">
<MissingDependency occurrences="7">
<code>GenericShareException</code>
<code>GenericShareException</code>
<code>ShareNotFound</code>
@@ -306,7 +304,6 @@
<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('findAll')
->method('find')
->willReturn([]);
$result = $this->invokePrivate($this->userExport, 'execute', [$input, $output]);
}

View File

@@ -132,15 +132,9 @@ class BoardMapperTest extends MapperTestUtility {
public function testFindAll() {
$actual = $this->boardMapper->findAll();
$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();
})));
$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());
}
public function testFindAllToDelete() {

View File

@@ -24,7 +24,7 @@
namespace OCA\Deck\Notification;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\AssignedUsersMapper;
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 AssignmentMapper|MockObject */
/** @var AssignedUsersMapper|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(AssignmentMapper::class);
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->permissionService = $this->createMock(PermissionService::class);
$this->config = $this->createMock(IConfig::class);
$this->notificationManager = $this->createMock(IManager::class);
@@ -130,8 +130,7 @@ class NotificationHelperTest extends \Test\TestCase {
$card = Card::fromParams([
'notified' => false,
'id' => 123,
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
'title' => 'MyCardTitle'
]);
$this->cardMapper->expects($this->once())
->method('findBoardId')
@@ -226,8 +225,7 @@ class NotificationHelperTest extends \Test\TestCase {
$card = Card::fromParams([
'notified' => false,
'id' => 123,
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
'title' => 'MyCardTitle'
]);
$card->setAssignedUsers([
new User($users[0])
@@ -325,8 +323,7 @@ class NotificationHelperTest extends \Test\TestCase {
$card = Card::fromParams([
'notified' => false,
'id' => 123,
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
'title' => 'MyCardTitle'
]);
$card->setAssignedUsers([
new User($users[0])
@@ -473,7 +470,7 @@ class NotificationHelperTest extends \Test\TestCase {
->with(123)
->willReturn($board);
$user = $this->createMock(IUser::class);
$user->expects($this->any())
$user->expects($this->once())
->method('getUID')
->willReturn('userA');
$group = $this->createMock(IGroup::class);

View File

@@ -1,133 +0,0 @@
<?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,6 +23,7 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OC\L10N\L10N;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\Acl;
@@ -36,11 +37,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 {
@@ -71,7 +72,7 @@ class BoardServiceTest extends TestCase {
private $activityManager;
/** @var ChangeHelper */
private $changeHelper;
/** @var IEventDispatcher */
/** @var EventDispatcherInterface */
private $eventDispatcher;
private $userId = 'admin';
@@ -90,7 +91,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(IEventDispatcher::class);
$this->eventDispatcher = $this->createMock(SymfonyAdapter::class);
$this->service = new BoardService(
$this->boardMapper,
@@ -382,7 +383,7 @@ class BoardServiceTest extends TestCase {
$assignment = new Assignment();
$assignment->setParticipant('admin');
$this->assignedUsersMapper->expects($this->once())
->method('findByParticipant')
->method('findByUserId')
->with('admin')
->willReturn([$assignment]);
$this->assignedUsersMapper->expects($this->once())

View File

@@ -25,11 +25,9 @@ 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;
@@ -128,17 +126,6 @@ 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())
@@ -146,14 +133,12 @@ class CardServiceTest extends TestCase {
->with(123)
->willReturn($card);
$this->assignedUsersMapper->expects($this->any())
->method('findAll')
->method('find')
->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

@@ -23,6 +23,7 @@
namespace OCA\Deck\Service;
use OC\EventDispatcher\SymfonyAdapter;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Card;
@@ -33,6 +34,7 @@ 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;
/**
@@ -67,6 +69,8 @@ class StackServiceTest extends TestCase {
private $activityManager;
/** @var ChangeHelper|\PHPUnit\Framework\MockObject\MockObject */
private $changeHelper;
/** @var EventDispatcherInterface */
private $eventDispatcher;
public function setUp(): void {
parent::setUp();
@@ -81,6 +85,7 @@ 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,
@@ -93,6 +98,7 @@ class StackServiceTest extends TestCase {
$this->assignedUsersMapper,
$this->attachmentService,
$this->activityManager,
$this->eventDispatcher,
$this->changeHelper
);
}