Compare commits

..

1 Commits

Author SHA1 Message Date
Julius Härtl
3ef5f8edd0 Implement basic card entity for flow integration
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-03-08 10:48:09 +01:00
120 changed files with 2884 additions and 5311 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

@@ -34,7 +34,7 @@ jobs:
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
mysql:
image: mariadb:10.5
image: mariadb
ports:
- 4444:3306/tcp
env:

View File

@@ -35,7 +35,7 @@ jobs:
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
mysql:
image: mariadb:10.5
image: mariadb
ports:
- 4444:3306/tcp
env:

View File

@@ -1,84 +1,7 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.4.7
### Fixed
- Fix release asset build
## 1.4.6
### Fixed
- #3379 Fix menu button position in card modal
- #3360 Improve combined search @eneiluj
- #3367 Fix optional parameter order
- #3393 Use displayname instead of uid for mentions
- #3359 Rich object string parameters for notifications @juliushaertl
- #3385 Extend drag-and-drop zone in card sidebar @Artem4590
- #3408 Keep exceptions http response generic
## 1.4.5
### Fixed
- #3318 Additional check for stacks
## 1.4.4
### Fixed
- #3301 Fix print style issues
- #3307 Return false instead of throwing when getting calendar setting
- #3227 Additional circle level check
- #3304 Delete file shares through attachments API
## 1.4.3 - 2021-07-09
### Fixed
* [#3143](https://github.com/nextcloud/deck/pull/3143) Always pass user id in share provider
* [#3153](https://github.com/nextcloud/deck/pull/3153) Only offer stack creation in emptycontent with proper permissions
* [#3164](https://github.com/nextcloud/deck/pull/3164) Always log generic exceptions
* [#3169](https://github.com/nextcloud/deck/pull/3169) Reduce duplicate queries when fetching user boards an permissions
## 1.4.2 - 2021-05-03
### Fixed
* [#3030](https://github.com/nextcloud/deck/pull/3030) Proper error handling when fetching comments fails
* [#3031](https://github.com/nextcloud/deck/pull/3031) Allow searching for filters without a query to match all that have a given filter set
* [#3039](https://github.com/nextcloud/deck/pull/3039) Catch any error during circle detail fetching
* [#3040](https://github.com/nextcloud/deck/pull/3040) Get attachment from the user node instead of the share source
## 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

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>deck</id>
<name>Deck</name>
<summary>Personal planning and team project organization</summary>
@@ -16,12 +17,12 @@
- 🚀 Get your project organized
</description>
<version>1.4.7</version>
<version>1.4.0-alpha1</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
<types>
<dav/>
<dav />
</types>
<category>organization</category>
<category>office</category>
@@ -35,7 +36,7 @@
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="5.5">mysql</database>
<nextcloud min-version="21" max-version="21"/>
<nextcloud min-version="21" max-version="22" />
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>

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

@@ -22,9 +22,7 @@
.icon-activity {
@include icon-color('activity-dark', 'activity', $color-black);
}
.icon-comment--unread {
@include icon-color('comment', 'actions', $color-primary, 1, true);
}
.avatardiv.circles {
background: var(--color-primary);

View File

@@ -2,26 +2,21 @@
/* hide stuff */
#body-user {
#header,
.app-navigation,
.app-sidebar,
.board-header-controls,
.board-actions,
div#app-navigation,
div.board-header-controls,
#app-navigation-toggle,
#app-navigation-toggle-custom,
div#controls.ng-scope div.crumb:not(.title),
div#controls.ng-scope div.crumb a.bullet,
a.ng-binding + a,
div.card.create,
.stack__header .action-item,
button.card-options {
display: none !important;
}
#content {
margin: 0;
padding: 0;
}
#app-content {
margin: 0 !important;
}
@@ -80,11 +75,6 @@
margin: 2cm;
}
.board {
max-height: none !important;
overflow: visible !important;
}
div#innerBoard {
display:flex;
flex-wrap: wrap;

View File

@@ -959,7 +959,6 @@ For now only `deck_file` is supported as an attachment type.
### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment
#### Request parameters
| Parameter | Type | Description |

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.",
@@ -112,12 +112,12 @@ OC.L10N.register(
"Select a list" : "Vyberte sloupec",
"Card title" : "Název karty",
"Cancel" : "Storno",
"Creating the new card…" : "Vytváření nové karty",
"\"{card}\" was added to \"{board}\"" : "{card} bylo přidáno do {board}",
"Creating the new card…" : "Vytváření nové karty...",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" bylo přidáno do \"{board}\"",
"Open card" : "Otevřít kartu",
"Close" : "Zavřít",
"Create card" : "Vytvořit kartu",
"Select a card" : "Vybrat kartu",
"Select a card" : "Vybrat tabuli",
"Select the card to link to a project" : "Vyberte kartu kterou propojit s projektem",
"Link to card" : "Propojit s kartou",
"File already exists" : "Soubor už existuje",
@@ -206,7 +206,7 @@ OC.L10N.register(
"Remove due date" : "Odstranit termín",
"Select Date" : "Vybrat datum",
"Save" : "Uložit",
"The comment cannot be empty." : "Komentář je třeba vyplnit.",
"The comment cannot be empty." : "Komentář je třeba vyplnit",
"The comment cannot be longer than 1000 characters." : "Délka komentáře může být nejvýše 1 000 znaků.",
"In reply to" : "V odpověď na",
"Reply" : "Odpovědět",
@@ -238,7 +238,7 @@ OC.L10N.register(
"Show boards in calendar/tasks" : "Zobrazit tabule v kalendáři/úkolech",
"Limit deck usage of groups" : "Omezit využití deck na skupiny",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Omezení nastavené pro Deck brání uživatelům, kteří nejsou součástí těchto skupin, ve vytváření vlastních tabulí. Nicméně i tak ale pořád budou moci pracovat na tabulích, které jsou jim nasdíleny.",
"Board details" : "Podrobnosti o 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.",
@@ -110,12 +110,12 @@
"Select a list" : "Vyberte sloupec",
"Card title" : "Název karty",
"Cancel" : "Storno",
"Creating the new card…" : "Vytváření nové karty",
"\"{card}\" was added to \"{board}\"" : "{card} bylo přidáno do {board}",
"Creating the new card…" : "Vytváření nové karty...",
"\"{card}\" was added to \"{board}\"" : "\"{card}\" bylo přidáno do \"{board}\"",
"Open card" : "Otevřít kartu",
"Close" : "Zavřít",
"Create card" : "Vytvořit kartu",
"Select a card" : "Vybrat kartu",
"Select a card" : "Vybrat tabuli",
"Select the card to link to a project" : "Vyberte kartu kterou propojit s projektem",
"Link to card" : "Propojit s kartou",
"File already exists" : "Soubor už existuje",
@@ -204,7 +204,7 @@
"Remove due date" : "Odstranit termín",
"Select Date" : "Vybrat datum",
"Save" : "Uložit",
"The comment cannot be empty." : "Komentář je třeba vyplnit.",
"The comment cannot be empty." : "Komentář je třeba vyplnit",
"The comment cannot be longer than 1000 characters." : "Délka komentáře může být nejvýše 1 000 znaků.",
"In reply to" : "V odpověď na",
"Reply" : "Odpovědět",
@@ -236,7 +236,7 @@
"Show boards in calendar/tasks" : "Zobrazit tabule v kalendáři/úkolech",
"Limit deck usage of groups" : "Omezit využití deck na skupiny",
"Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them." : "Omezení nastavené pro Deck brání uživatelům, kteří nejsou součástí těchto skupin, ve vytváření vlastních tabulí. Nicméně i tak ale pořád budou moci pracovat na tabulích, které jsou jim nasdíleny.",
"Board details" : "Podrobnosti o 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

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

View File

@@ -105,16 +105,9 @@
"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" : "Линк до картица",
@@ -263,8 +256,6 @@
"upcoming cards" : "престојни картици",
"Link to a board" : "Линк до табла",
"Link to a card" : "Линк до картица",
"Create a card" : "Креирајте картица",
"Message from {author} in {conversationName}" : "Порака од {author} во {conversationName}",
"Something went wrong" : "Нешто не е во ред",
"Failed to upload {name}" : "Неуспешно прикачување {name}",
"Maximum file size of {size} exceeded" : "Максималната големина на датотека од {size} е достигната",

View File

@@ -93,7 +93,7 @@ OC.L10N.register(
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "Wysłany plik przekracza wielkość dyrektywy MAX_FILE_SIZE określonej w formularzu HTML",
"The file was only partially uploaded" : "Załadowany plik został wysłany tylko częściowo.",
"No file was uploaded" : "Nie wysłano żadnego pliku",
"Missing a temporary folder" : "Brak 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

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

View File

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

View File

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

View File

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

View File

@@ -2,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,259 @@
<?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 OCA\Deck\Listeners\RegisterChecksListener;
use OCA\Deck\Listeners\RegisterEntityListener;
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 OCP\WorkflowEngine\Events\RegisterChecksEvent;
use OCP\WorkflowEngine\Events\RegisterEntitiesEvent;
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);
$context->registerEventListener(RegisterEntitiesEvent::class, RegisterEntityListener::class);
//$context->registerEventListener(RegisterChecksEvent::class, RegisterChecksListener::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

@@ -36,10 +36,8 @@ class CommentsApiController extends OCSController {
private $commentService;
public function __construct(
string $appName,
IRequest $request,
CommentService $commentService,
string $corsMethods = 'PUT, POST, GET, DELETE, PATCH', string $corsAllowedHeaders = 'Authorization, Content-Type, Accept', int $corsMaxAge = 1728000
$appName, IRequest $request, $corsMethods = 'PUT, POST, GET, DELETE, PATCH', $corsAllowedHeaders = 'Authorization, Content-Type, Accept', $corsMaxAge = 1728000,
CommentService $commentService
) {
parent::__construct($appName, $request, $corsMethods, $corsAllowedHeaders, $corsMaxAge);
$this->commentService = $commentService;

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

@@ -23,12 +23,11 @@
namespace OCA\Deck\Db;
use OC\Cache\CappedMemoryCache;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IDBConnection;
use OCP\ILogger;
use OCP\IUserManager;
use OCP\IGroupManager;
use Psr\Log\LoggerInterface;
class BoardMapper extends DeckMapper implements IPermissionMapper {
private $labelMapper;
@@ -36,20 +35,16 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
private $stackMapper;
private $userManager;
private $groupManager;
private $logger;
private $circlesEnabled;
private $userBoardCache;
public function __construct(
IDBConnection $db,
LabelMapper $labelMapper,
AclMapper $aclMapper,
StackMapper $stackMapper,
IUserManager $userManager,
IGroupManager $groupManager,
LoggerInterface $logger
IGroupManager $groupManager
) {
parent::__construct($db, 'deck_boards', Board::class);
$this->labelMapper = $labelMapper;
@@ -57,10 +52,6 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
$this->stackMapper = $stackMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->logger = $logger;
$this->userBoardCache = new CappedMemoryCache();
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles');
}
@@ -95,21 +86,13 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
}
public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array {
$useCache = ($since === -1 && $includeArchived === true);
if (!isset($this->userBoardCache[$userId]) || !$useCache) {
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($userId)
);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
$allBoards = array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
if ($useCache) {
$this->userBoardCache[$userId] = $allBoards;
}
return $allBoards;
}
return $this->userBoardCache[$userId];
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($userId)
);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
$groupBoards = $this->findAllByGroups($userId, $groups,null, null, $since, $includeArchived);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
}
/**
@@ -186,7 +169,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
}
$circles = array_map(function ($circle) {
return $circle->getUniqueId();
}, \OCA\Circles\Api\v1\Circles::joinedCircles($userId, true));
}, \OCA\Circles\Api\v1\Circles::joinedCircles('', true));
if (count($circles) === 0) {
return [];
}
@@ -265,7 +248,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if ($user !== null) {
return new User($user);
}
$this->logger->debug('User ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
\OC::$server->getLogger()->debug('User ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
return null;
}
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
@@ -273,7 +256,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if ($group !== null) {
return new Group($group);
}
$this->logger->debug('Group ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
\OC::$server->getLogger()->debug('Group ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
return null;
}
if ($acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
@@ -285,12 +268,11 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if ($circle) {
return new Circle($circle);
}
} catch (\Throwable $e) {
$this->logger->error('Failed to get circle details when building ACL', ['exception' => $e]);
} catch (\Exception $e) {
}
return null;
}
$this->logger->warning('Unknown permission type for mapping acl ' . $acl->getId());
\OC::$server->getLogger()->log(ILogger::WARN, 'Unknown permission type for mapping acl ' . $acl->getId());
return null;
});
}

View File

@@ -49,10 +49,6 @@ class Card extends RelationalEntity {
protected $notified = false;
protected $deletedAt = 0;
protected $commentsUnread = 0;
protected $commentsCount = 0;
protected $relatedStack = null;
protected $relatedBoard = null;
private $databaseType = 'sqlite';
@@ -76,11 +72,7 @@ class Card extends RelationalEntity {
$this->addRelation('attachmentCount');
$this->addRelation('participants');
$this->addRelation('commentsUnread');
$this->addRelation('commentsCount');
$this->addResolvable('owner');
$this->addRelation('relatedStack');
$this->addRelation('relatedBoard');
}
public function setDatabaseType($type) {
@@ -127,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,221 +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();
if ($date === "") {
$qb->andWhere($qb->expr()->isNotNull('c.duedate'));
continue;
}
$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();
if ($assignedQueryValue === "") {
$qb->andWhere($qb->expr()->isNotNull('au' . $index . '.participant'));
continue;
}
$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,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

@@ -26,5 +26,18 @@ declare(strict_types=1);
namespace OCA\Deck\Event;
class CardCreatedEvent extends ACardEvent {
use OCA\Deck\Db\Card;
class CardCreatedEvent extends \OCP\EventDispatcher\Event {
/** @var Card */
private $card;
public function __construct(Card $card) {
$this->card = $card;
}
public function getCard(): Card {
return $this->card;
}
}

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\Card;
use OCP\EventDispatcher\Event;
abstract class ACardEvent extends Event {
private $card;
public function __construct(Card $card) {
/**
* 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->card = $card;
$this->arguments = $arguments;
}
public function getCard(): Card {
return $this->card;
public function getArgument($key) {
if (isset($this->arguments[$key])) {
return $this->arguments[$key];
}
throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key));
}
}

107
lib/Flow/CardEntity.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
/*
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare(strict_types=1);
namespace OCA\Deck\Flow;
use OCA\Deck\Event\CardCreatedEvent;
use OCP\EventDispatcher\Event;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\WorkflowEngine\EntityContext\IDisplayName;
use OCP\WorkflowEngine\EntityContext\IDisplayText;
use OCP\WorkflowEngine\EntityContext\IUrl;
use OCP\WorkflowEngine\IEntity;
use OCP\WorkflowEngine\IRuleMatcher;
class CardEntity implements IEntity, IDisplayText, IDisplayName, IUrl {
/**
* @var IL10N
*/
private $l10n;
/**
* @var IURLGenerator
*/
private $urlGenerator;
private $card;
public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
$this->l10n = $l;
$this->urlGenerator = $urlGenerator;
}
/**
* @inheritDoc
*/
public function getName(): string {
return $this->l10n->t('Deck card');
}
/**
* @inheritDoc
*/
public function getIcon(): string {
return $this->urlGenerator->imagePath('deck', 'deck-dark.svg');
}
/**
* @inheritDoc
*/
public function getEvents(): array {
return [new CardEntityCreatedEvent($this->l10n)];
}
/**
* @inheritDoc
*/
public function prepareRuleMatcher(IRuleMatcher $ruleMatcher, string $eventName, Event $event): void {
if(!$event instanceof CardCreatedEvent) {
return;
}
/** @var CardCreatedEvent $event */
$ruleMatcher->setEntitySubject($this, $event->getCard());
$this->card = $event->getCard();
}
/**
* @inheritDoc
*/
public function isLegitimatedForUserId(string $userId): bool {
return true;
}
public function getDisplayName(): string {
return $this->card->getTitle();
}
public function getDisplayText(int $verbosity = 0): string {
return $this->card->getTitle() . PHP_EOL . $this->card->getDescription();
}
public function getUrl(): string {
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . '#' . '/board/1/card/' . $this->card->getId();
}
}

View File

@@ -24,21 +24,25 @@
declare(strict_types=1);
namespace OCA\Deck\Event;
namespace OCA\Deck\Flow;
use OCA\Deck\Db\Acl;
use OCP\EventDispatcher\Event;
use OCA\Deck\Event\CardCreatedEvent;
use OCP\IL10N;
use OCP\WorkflowEngine\IEntityEvent;
abstract class AAclEvent extends Event {
private $acl;
class CardEntityCreatedEvent implements IEntityEvent {
public function __construct(Acl $acl) {
parent::__construct();
/** @var IL10N */
private $l10n;
$this->acl = $acl;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
public function getDisplayName(): string {
return $this->l10n->t('Card created');
}
public function getAcl(): Acl {
return $this->acl;
public function getEventName(): string {
return CardCreatedEvent::class;
}
}

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,51 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\Listeners;
use OCA\FlowWebhooks\AppInfo\Application;
use OCA\FlowWebhooks\Flow\ParameterCheck;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
use OCP\WorkflowEngine\Events\RegisterChecksEvent;
class RegisterChecksListener implements IEventListener {
/** @var ParameterCheck */
private $parameterCheck;
public function __construct(ParameterCheck $parameterCheck) {
$this->parameterCheck = $parameterCheck;
}
public function handle(Event $event): void {
if (!($event instanceof RegisterChecksEvent)) {
return;
}
$event->registerCheck($this->parameterCheck);
Util::addScript(Application::APP_ID, Application::APP_ID);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @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\Listeners;
use OCA\Deck\Flow\CardEntity;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\WorkflowEngine\Events\RegisterEntitiesEvent;
class RegisterEntityListener implements IEventListener {
/** @var CardEntity */
private $cardEntity;
public function __construct(CardEntity $cardEntity) {
$this->cardEntity = $cardEntity;
}
public function handle(Event $event): void {
if(!$event instanceof RegisterEntitiesEvent) {
return;
}
$event->registerEntity($this->cardEntity);
}
}

View File

@@ -32,7 +32,6 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCSController;
use OCP\ILogger;
use OCP\IRequest;
use OCP\Util;
use OCP\IConfig;
@@ -42,8 +41,6 @@ class ExceptionMiddleware extends Middleware {
private $logger;
/** @var IConfig */
private $config;
/** @var IRequest */
private $request;
/**
* SharingMiddleware constructor.
@@ -51,10 +48,9 @@ class ExceptionMiddleware extends Middleware {
* @param ILogger $logger
* @param IConfig $config
*/
public function __construct(ILogger $logger, IConfig $config, IRequest $request) {
public function __construct(ILogger $logger, IConfig $config) {
$this->logger = $logger;
$this->config = $config;
$this->request = $request;
}
/**
@@ -71,10 +67,45 @@ class ExceptionMiddleware extends Middleware {
throw $exception;
}
$debugMode = $this->config->getSystemValue('debug', false);
$exceptionMessage = $debugMode !== true
? 'Internal server error: Please contact the server administrator if this error reappears multiple times, please include the request ID "' . $this->request->getId() . '" below in your report.'
: $exception->getMessage();
if ($exception instanceof ConflictException) {
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage(),
'data' => $exception->getData(),
], $exception->getStatus());
}
if ($exception instanceof StatusException) {
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
if ($controller instanceof OCSController) {
$exception = new OCSException($exception->getMessage(), $exception->getStatus(), $exception);
throw $exception;
}
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage()
], $exception->getStatus());
}
if (strpos(get_class($controller), 'OCA\\Deck\\Controller\\') === 0) {
$response = [
'status' => 500,
'message' => $exception->getMessage()
];
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
if ($this->config->getSystemValue('debug', true) === true) {
$response['exception'] = (array) $exception;
}
return new JSONResponse($response, 500);
}
// uncatched DoesNotExistExceptions will be thrown when the main entity is not found
// we return a 403 so we don't leak information over existing entries
@@ -85,43 +116,6 @@ class ExceptionMiddleware extends Middleware {
'message' => 'Permission denied'
], 403);
}
if ($exception instanceof StatusException) {
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
$this->logger->logException($exception);
}
if ($exception instanceof ConflictException) {
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage(),
'data' => $exception->getData(),
], $exception->getStatus());
}
if ($controller instanceof OCSController) {
$exception = new OCSException($exception->getMessage(), $exception->getStatus(), $exception);
throw $exception;
}
return new JSONResponse([
'status' => $exception->getStatus(),
'message' => $exception->getMessage(),
], $exception->getStatus());
}
if (strpos(get_class($controller), 'OCA\\Deck\\Controller\\') === 0) {
$response = [
'status' => 500,
'message' => $exceptionMessage,
'requestId' => $this->request->getId(),
];
$this->logger->logException($exception);
if ($debugMode === true) {
$response['exception'] = (array) $exception;
}
return new JSONResponse($response, 500);
}
throw $exception;
}

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

@@ -25,7 +25,6 @@ namespace OCA\Deck\Notification;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
@@ -42,8 +41,6 @@ class Notifier implements INotifier {
protected $userManager;
/** @var CardMapper */
protected $cardMapper;
/** @var StackMapper */
protected $stackMapper;
/** @var BoardMapper */
protected $boardMapper;
@@ -52,14 +49,12 @@ class Notifier implements INotifier {
IURLGenerator $url,
IUserManager $userManager,
CardMapper $cardMapper,
StackMapper $stackMapper,
BoardMapper $boardMapper
) {
$this->l10nFactory = $l10nFactory;
$this->url = $url;
$this->userManager = $userManager;
$this->cardMapper = $cardMapper;
$this->stackMapper = $stackMapper;
$this->boardMapper = $boardMapper;
}
@@ -105,11 +100,6 @@ class Notifier implements INotifier {
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$initiator = $this->userManager->get($params[2]);
if ($initiator !== null) {
$dn = $initiator->getDisplayName();
@@ -120,22 +110,8 @@ class Notifier implements INotifier {
(string) $l->t('The card "%s" on "%s" has been assigned to you by %s.', [$params[0], $params[1], $dn])
);
$notification->setRichSubject(
$l->t('{user} has assigned the card {deck-card} on {deck-board} to you.'),
(string) $l->t('{user} has assigned the card "%s" on "%s" to you.', [$params[0], $params[1]]),
[
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => $params[0],
'boardname' => $params[1],
'stackname' => $stack->getTitle(),
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '',
],
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $params[1],
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId,
],
'user' => [
'type' => 'user',
'id' => $params[2],
@@ -151,33 +127,9 @@ class Notifier implements INotifier {
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$notification->setParsedSubject(
(string) $l->t('The card "%s" on "%s" has reached its due date.', $params)
);
$notification->setRichSubject(
$l->t('The card {deck-card} on {deck-board} has reached its due date.'),
[
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => $params[0],
'boardname' => $params[1],
'stackname' => $stack->getTitle(),
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '',
],
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $params[1],
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId,
],
]
);
$notification->setLink($this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '');
break;
case 'card-comment-mentioned':
@@ -186,11 +138,6 @@ class Notifier implements INotifier {
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$initiator = $this->userManager->get($params[2]);
if ($initiator !== null) {
$dn = $initiator->getDisplayName();
@@ -201,16 +148,8 @@ class Notifier implements INotifier {
(string) $l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])
);
$notification->setRichSubject(
$l->t('{user} has mentioned you in a comment on {deck-card}.'),
(string) $l->t('{user} has mentioned you in a comment on "%s".', [$params[0]]),
[
'deck-card' => [
'type' => 'deck-card',
'id' => $cardId,
'name' => $params[0],
'boardname' => $params[1],
'stackname' => $stack->getTitle(),
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $cardId . '',
],
'user' => [
'type' => 'user',
'id' => $params[2],
@@ -238,14 +177,8 @@ class Notifier implements INotifier {
(string) $l->t('The board "%s" has been shared with you by %s.', [$params[0], $dn])
);
$notification->setRichSubject(
$l->t('{user} has shared {deck-board} with you.'),
(string) $l->t('{user} has shared the board %s with you.', [$params[0]]),
[
'deck-board' => [
'type' => 'deck-board',
'id' => $boardId,
'name' => $params[0],
'link' => $this->url->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId,
],
'user' => [
'type' => 'user',
'id' => $params[1],

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,82 +77,39 @@ class DeckProvider implements IProvider {
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
$cursor = $query->getCursor();
[$boardCursor, $cardCursor] = $this->parseCursor($cursor);
$boards = $this->boardService->getUserBoards();
$boardObjects = $this->searchService->searchBoards($query->getTerm(), $query->getLimit(), $boardCursor);
$boardResults = array_map(function (Board $board) {
return [
'object' => $board,
'entry' => new BoardSearchResultEntry($board, $this->urlGenerator)
];
}, $boardObjects);
$cardObjects = $this->searchService->searchCards($query->getTerm(), $query->getLimit(), $cardCursor);
$cardResults = array_map(function (Card $card) {
return [
'object' => $card,
'entry' => new CardSearchResultEntry($card->getRelatedBoard(), $card->getRelatedStack(), $card, $this->urlGenerator)
];
}, $cardObjects);
$results = array_merge($boardResults, $cardResults);
usort($results, function ($a, $b) {
$ta = $a['object']->getLastModified();
$tb = $b['object']->getLastModified();
return $ta === $tb
? 0
: ($ta > $tb ? -1 : 1);
$matchedBoards = array_filter($this->boardService->getUserBoards(), static function (Board $board) use ($query) {
return mb_stripos($board->getTitle(), $query->getTerm()) > -1;
});
$resultEntries = array_map(function (array $result) {
return $result['entry'];
}, $results);
$matchedCards = $this->cardMapper->search(array_map(static function (Board $board) {
return $board->getId();
}, $boards), $query->getTerm(), $query->getLimit(), $query->getCursor());
// if both cards and boards results are less then the limit, we know we won't get more
if (count($resultEntries) < $query->getLimit()) {
return SearchResult::complete(
'Deck',
$resultEntries
);
}
$self = $this;
$results = array_merge(
array_map(function (Board $board) {
return new BoardSearchResultEntry($board, $this->urlGenerator);
}, $matchedBoards),
$newCursor = $this->getNewCursor($boardObjects, $cardObjects);
return SearchResult::paginated(
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)
);
return SearchResult::complete(
'Deck',
$resultEntries,
$newCursor
$results
);
}
public function getOrder(string $route, array $routeParameters): int {
if ($route === 'deck.Page.index') {
if ($route === 'deck.page.index') {
return -5;
}
return 10;
}
private function parseCursor(?string $cursor): array {
$boardCursor = null;
$cardCursor = null;
if ($cursor !== null) {
$splitCursor = explode('|', $cursor);
if (count($splitCursor) >= 2) {
$boardCursor = (int)$splitCursor[0] ?: null;
$cardCursor = (int)$splitCursor[1] ?: null;
}
}
return [$boardCursor, $cardCursor];
}
private function getNewCursor(array $boards, array $cards): string {
$boardTimestamps = array_map(function (Board $board) {
return $board->getLastModified();
}, $boards);
$cardTimestamps = array_map(function (Card $card) {
return $card->getLastModified();
}, $cards);
return (min($boardTimestamps) ?: '') . '|' . (min($cardTimestamps) ?: '');
}
}

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 = (mb_substr($this->value, 0, 1) === '"' && mb_substr($this->value, -1, 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

@@ -35,7 +35,6 @@ use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCA\Deck\StatusException;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http\Response;
use OCP\ICache;
use OCP\ICacheFactory;
@@ -321,10 +320,14 @@ class AttachmentService {
* Either mark an attachment as deleted for later removal or just remove it depending
* on the IAttachmentService implementation
*
* @throws NoPermissionException
* @throws NotFoundException
* @param $attachmentId
* @return \OCP\AppFramework\Db\Entity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function delete(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
public function delete($cardId, $attachmentId, $type = 'deck_file') {
try {
$service = $this->getService($type);
} catch (InvalidAttachmentType $e) {
@@ -337,32 +340,40 @@ class AttachmentService {
$attachment->setType($type);
$attachment->setCardId($cardId);
$service->extendData($attachment);
} else {
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (IMapperException $e) {
throw new NoPermissionException('Permission denied');
}
$service->delete($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
}
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {
throw new NoPermissionException('Permission denied');
}
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $attachment->getCardId());
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
$attachment = $this->attachmentMapper->update($attachment);
} else {
$service->delete($attachment);
if (!$service instanceof ICustomAttachmentService) {
$attachment = $this->attachmentMapper->delete($attachment);
}
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
$this->changeHelper->cardChanged($attachment->getCardId());
return $this->attachmentMapper->update($attachment);
}
$service->delete($attachment);
$this->cache->clear('card-' . $attachment->getCardId());
$attachment = $this->attachmentMapper->delete($attachment);
$this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
}
public function restore(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
public function restore($cardId, $attachmentId, $type = 'deck_file') {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
}
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {

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;
}
@@ -440,6 +457,10 @@ class BoardService {
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_BOARD, $changes, ActivityManager::SUBJECT_BOARD_UPDATE);
$this->changeHelper->boardChanged($board->getId());
$this->eventDispatcher->dispatch(
'\OCA\Deck\Board::onUpdate', new GenericEvent(null, ['id' => $id, 'board' => $board])
);
return $board;
}
@@ -510,21 +531,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 +597,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 +627,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 +640,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,14 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\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;
@@ -105,15 +105,8 @@ class CardService {
$card->setAttachmentCount($this->attachmentService->count($cardId));
$user = $this->userManager->get($this->currentUser);
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
$countUnreadComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
$card->setCommentsUnread($countUnreadComments);
$card->setCommentsCount($countComments);
$stack = $this->stackMapper->find($card->getStackId());
$board = $this->boardService->find($stack->getBoardId());
$card->setRelatedStack($stack);
$card->setRelatedBoard($board);
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
$card->setCommentsUnread($count);
}
public function fetchDeleted($boardId) {
@@ -125,6 +118,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
@@ -214,10 +223,16 @@ 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;
}
@@ -243,11 +258,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;
}
@@ -325,15 +341,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);
}
@@ -353,12 +360,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;
}
@@ -398,7 +404,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;
}
@@ -495,7 +503,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;
}
@@ -524,7 +534,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;
}
@@ -560,7 +572,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])
);
}
/**
@@ -594,7 +608,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

@@ -26,7 +26,6 @@ declare(strict_types=1);
namespace OCA\Deck\Service;
use OCA\Circles\Api\v1\Circles;
use OCP\App\IAppManager;
/**
@@ -54,8 +53,8 @@ class CirclesService {
}
try {
$member = \OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
return $member->getLevel() >= Circles::LEVEL_MEMBER;
\OCA\Circles\Api\v1\Circles::getMember($circleId, $userId, 1, true);
return true;
} catch (\Exception $e) {
}
return false;

View File

@@ -73,20 +73,16 @@ class ConfigService {
if (!$this->groupManager->isAdmin($this->userId)) {
throw new NoPermissionException('You must be admin to get the group limit');
}
return $this->getGroupLimit();
$result = $this->getGroupLimit();
break;
case 'calendar':
if ($this->userId === null) {
return false;
}
return (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
$result = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
break;
}
return $result;
}
public function isCalendarEnabled(int $boardId = null): bool {
if ($this->userId === null) {
return false;
}
$defaultState = (bool)$this->config->getUserValue($this->userId, Application::APP_ID, 'calendar', true);
if ($boardId === null) {
return $defaultState;

View File

@@ -23,10 +23,7 @@
namespace OCA\Deck\Service;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\StreamResponse;
@@ -38,10 +35,9 @@ 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;
use Psr\Log\LoggerInterface;
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $request;
@@ -52,10 +48,8 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $configService;
private $l10n;
private $preview;
private $mimeTypeDetector;
private $permissionService;
private $cardMapper;
private $logger;
private $mimeTypeDetector;
public function __construct(
IRequest $request,
@@ -65,10 +59,8 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
ConfigService $configService,
DeckShareProvider $shareProvider,
IPreview $preview,
IMimeTypeDetector $mimeTypeDetector,
PermissionService $permissionService,
CardMapper $cardMapper,
LoggerInterface $logger,
IMimeTypeDetector $mimeTypeDetector,
string $userId = null
) {
$this->request = $request;
@@ -80,20 +72,15 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$this->userId = $userId;
$this->preview = $preview;
$this->mimeTypeDetector = $mimeTypeDetector;
$this->permissionService = $permissionService;
$this->cardMapper = $cardMapper;
$this->logger = $logger;
}
public function listAttachments(int $cardId): array {
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
return array_filter(array_map(function (IShare $share) use ($cardId) {
try {
$file = $share->getNode();
} catch (NotFoundException $e) {
$this->logger->debug('Unable to find node for share with ID ' . $share->getId());
return null;
}
$shares = array_filter($shares, function ($share) {
return $share->getPermissions() > 0;
});
return array_map(function (IShare $share) use ($cardId) {
$file = $share->getNode();
$attachment = new Attachment();
$attachment->setType('file');
$attachment->setId((int)$share->getId());
@@ -102,9 +89,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$attachment->setData($file->getName());
$attachment->setLastModified($file->getMTime());
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
$attachment->setDeletedAt($share->getPermissions() === 0 ? $share->getShareTime()->getTimestamp() : 0);
$attachment->setDeletedAt(0);
return $attachment;
}, $shares));
}, $shares);
}
public function getAttachmentCount(int $cardId): int {
@@ -138,11 +125,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
public function extendData(Attachment $attachment) {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$share = $this->shareProvider->getShareById($attachment->getId());
$files = $userFolder->getById($share->getNode()->getId());
if (count($files) === 0) {
return $attachment;
}
$file = array_shift($files);
$file = $share->getNode();
$attachment->setExtendedData([
'path' => $userFolder->getRelativePath($file->getPath()),
'fileid' => $file->getId(),
@@ -157,11 +140,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
}
public function display(Attachment $attachment) {
// Problem: Folders
/** @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();
@@ -179,9 +160,6 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$file = $this->getUploadedFile();
$fileName = $file['name'];
// get shares for current card
// check if similar filename already exists
$userFolder = $this->rootFolder->getUserFolder($this->userId);
try {
$folder = $userFolder->get($this->configService->getAttachmentFolder());
@@ -196,6 +174,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
throw new StatusException('Could not read file');
}
$target->putContent($content);
fclose($content);
$share = $this->shareManager->newShare();
$share->setNode($target);
@@ -262,16 +241,12 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$file = $share->getNode();
$attachment->setData($file->getName());
// Deleting a Nextcloud file attachment will remove the share to the card, keeping the source file untouched
// Opt-out of individual shares per user is no longer performed within deck but can still be done through the files app
$canEdit = $this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$isFileOwner = $file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId;
if ($isFileOwner || $canEdit) {
$this->shareManager->deleteShare($share);
if ($file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId) {
$file->delete();
return;
}
throw new NoPermissionException('No permission to remove the attachment from the card');
$this->shareManager->deleteFromSelf($share, $this->userId);
}
public function allowUndo() {

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

@@ -23,8 +23,6 @@
namespace OCA\Deck\Service;
use OC\Cache\CappedMemoryCache;
use OCA\Circles\Model\Member;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Board;
@@ -33,6 +31,7 @@ use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\Db\User;
use OCA\Deck\NoPermissionException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\IConfig;
use OCP\IGroupManager;
@@ -62,7 +61,6 @@ class PermissionService {
private $users = [];
private $circlesEnabled = false;
private $boardCache;
public function __construct(
ILogger $logger,
@@ -83,8 +81,6 @@ class PermissionService {
$this->config = $config;
$this->userId = $userId;
$this->boardCache = new CappedMemoryCache();
$this->circlesEnabled = \OC::$server->getAppManager()->isEnabledForUser('circles') &&
(version_compare(\OC::$server->getAppManager()->getAppVersion('circles'), '0.17.1') >= 0);
}
@@ -153,13 +149,10 @@ class PermissionService {
return true;
}
try {
$acls = $this->getBoard($boardId)->getAcl();
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;
}
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
$acls = $this->aclMapper->findAll($boardId);
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;
}
// Throw NoPermission to not leak information about existing entries
@@ -175,24 +168,13 @@ class PermissionService {
$userId = $this->userId;
}
try {
$board = $this->getBoard($boardId);
return $userId === $board->getOwner();
$board = $this->boardMapper->find($boardId);
return $board && $userId === $board->getOwner();
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
}
return false;
}
/**
* @throws MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
private function getBoard($boardId): Board {
if (!isset($this->boardCache[$boardId])) {
$this->boardCache[$boardId] = $this->boardMapper->find($boardId, false, true);
}
return $this->boardCache[$boardId];
}
/**
* Check if permission matches the acl rules for current user and groups
*
@@ -212,8 +194,8 @@ class PermissionService {
if ($this->circlesEnabled && $acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
try {
$member = \OCA\Circles\Api\v1\Circles::getMember($acl->getParticipant(), $this->userId, 1, true);
return $member->getLevel() >= Member::LEVEL_MEMBER && $acl->getPermission($permission);
\OCA\Circles\Api\v1\Circles::getMember($acl->getParticipant(), $this->userId, 1, true);
return $acl->getPermission($permission);
} catch (\Exception $e) {
$this->logger->info('Member not found in circle that was accessed. This should not happen.');
}

View File

@@ -1,132 +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();
// get boards that have a lastmodified date which is lower than the cursor
// and which match the search term
$filteredBoards = array_filter($boards, static function (Board $board) use ($term, $cursor) {
return (
($cursor === null || $board->getLastModified() < $cursor)
&& mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1
);
});
// sort the boards, recently modified first
usort($filteredBoards, function ($boardA, $boardB) {
$ta = $boardA->getLastModified();
$tb = $boardB->getLastModified();
return $ta === $tb
? 0
: ($ta > $tb ? -1 : 1);
});
// limit the number of results
return array_slice($filteredBoards, 0, $limit);
}
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;
@@ -48,6 +50,7 @@ class StackService {
private $assignedUsersMapper;
private $attachmentService;
private $activityManager;
private $symfonyAdapter;
private $changeHelper;
public function __construct(
@@ -61,6 +64,7 @@ class StackService {
AssignmentMapper $assignedUsersMapper,
AttachmentService $attachmentService,
ActivityManager $activityManager,
SymfonyAdapter $eventDispatcher,
ChangeHelper $changeHelper
) {
$this->stackMapper = $stackMapper;
@@ -73,6 +77,7 @@ class StackService {
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
$this->activityManager = $activityManager;
$this->symfonyAdapter = $eventDispatcher;
$this->changeHelper = $changeHelper;
}
@@ -109,7 +114,6 @@ class StackService {
throw new BadRequestException('stack id must be a number');
}
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_READ);
$stack = $this->stackMapper->find($stackId);
$cards = $this->cardMapper->findAll($stackId);
foreach ($cards as $cardIndex => $card) {
@@ -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

@@ -271,9 +271,9 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
return $share;
}
private function applyBoardPermission($share, $permissions, $userId) {
private function applyBoardPermission($share, $permissions) {
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_EDIT, $userId);
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_EDIT);
} catch (NoPermissionException $e) {
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE;
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
@@ -281,7 +281,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
}
try {
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_SHARE, $userId);
$this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_SHARE);
} catch (NoPermissionException $e) {
$permissions &= Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE;
}
@@ -562,7 +562,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
/**
* @inheritDoc
* @throws ShareNotFound
*/
public function getShareById($id, $recipientId = null) {
$qb = $this->dbConnection->getQueryBuilder();
@@ -646,7 +645,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$stmt = $query->execute();
while ($data = $stmt->fetch()) {
$this->applyBoardPermission($shareMap[$data['parent']], (int)$data['permissions'], $userId);
$this->applyBoardPermission($shareMap[$data['parent']], (int)$data['permissions']);
$shareMap[$data['parent']]->setTarget($data['file_target']);
}

View File

@@ -23,12 +23,6 @@
namespace OCA\Deck;
/**
* User facing exception that can be thrown with an error being reported to the frontend
* or consumers of the API
*
* This exception is catched in the ExceptionMiddleware
*/
class StatusException extends \Exception {
public function __construct($message) {
parent::__construct($message);

1512
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "deck",
"description": "",
"version": "1.4.7",
"version": "1.0.0",
"authors": [
{
"name": "Julius Härtl",
@@ -29,7 +29,7 @@
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.13.10",
"@babel/runtime": "^7.13.8",
"@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-dashboard": "^1.1.0",
"@nextcloud/vue": "^3.6.0",
"@nextcloud/vue-dashboard": "^1.0.1",
"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.8",
"@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",
@@ -88,7 +88,7 @@
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.2.0",
@@ -99,8 +99,8 @@
"raw-loader": "^4.0.2",
"sass-loader": "^10.1.1",
"style-loader": "^1.3.0",
"stylelint": "^13.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",
@@ -129,4 +129,4 @@
"<rootDir>/node_modules/jest-serializer-vue"
]
}
}
}

View File

@@ -131,12 +131,6 @@ export default {
position: relative;
}
.attachments-drag-zone.drop-upload--sidebar {
display: flex;
flex-direction: column;
flex-basis: 100%;
}
.dragover {
position: absolute;
background: var(--color-primary-light);

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

@@ -32,7 +32,7 @@
</div>
<EmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
{{ t('deck', 'No lists available') }}
<template v-if="canManage" #desc>
<template #desc>
{{ t('deck', 'Create a new list to add cards to this board') }}
<form @submit.prevent="addNewStack()">
<input id="new-stack-input-main"
@@ -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,
@@ -110,7 +107,6 @@ export default {
}),
...mapGetters([
'canEdit',
'canManage',
]),
stacksByBoard() {
return this.$store.getters.stacksByBoard(this.board.id)
@@ -182,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,7 @@
<template>
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
<div class="button-group" v-if="!isReadOnly">
<div class="button-group">
<button class="icon-upload" @click="uploadNewFile()">
{{ t('deck', 'Upload new files') }}
</button>
@@ -49,25 +49,18 @@
</li>
<li v-for="attachment in attachments"
:key="attachment.id"
class="attachment"
:class="{ 'attachment--deleted': attachment.deletedAt > 0 }">
class="attachment">
<a class="fileicon"
:href="internalLink(attachment)"
:style="mimetypeForAttachment(attachment)"
@click.prevent="showViewer(attachment)" />
<div class="details">
<a :href="internalLink(attachment)" @click.prevent="showViewer(attachment)">
<a @click.prevent="showViewer(attachment)">
<div class="filename">
<span class="basename">{{ attachment.data }}</span>
</div>
<div v-if="attachment.deletedAt === 0">
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
<span class="filedate">{{ attachment.createdBy }}</span>
</div>
<div v-else>
<span class="attachment--info">{{ t('deck', 'Pending share') }}</span>
</div>
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
<span class="filedate">{{ attachment.createdBy }}</span>
</a>
</div>
<Actions v-if="selectable">
@@ -75,12 +68,12 @@
{{ t('deck', 'Add this attachment') }}
</ActionButton>
</Actions>
<Actions v-if="removable && !isReadOnly" :force-menu="true">
<Actions v-if="removable" :force-menu="true">
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
{{ t('deck', 'Show in Files') }}
</ActionLink>
<ActionButton v-if="attachment.extendedData.fileid && !isReadOnly" icon="icon-delete" @click="unshareAttachment(attachment)">
{{ t('deck', 'Remove attachment') }}
<ActionButton v-if="attachment.extendedData.fileid" icon="icon-delete" @click="unshareAttachment(attachment)">
{{ t('deck', 'Unshare file') }}
</ActionButton>
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
@@ -150,7 +143,6 @@ export default {
},
computed: {
attachments() {
// FIXME sort propertly by last modified / deleted at
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
},
mimetypeForAttachment() {
@@ -328,10 +320,9 @@ export default {
opacity: 0.7;
}
}
.attachment--info,
.filesize, .filedate {
font-size: 90%;
color: var(--color-text-maxcontrast);
color: darkgray;
}
.app-popover-menu-utils {
position: relative;

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 {
@@ -185,13 +174,6 @@ export default {
<style lang="scss" scoped>
section.app-sidebar__tab--active {
min-height: auto;
display: flex;
flex-direction: column;
height: 100%;
}
// FIXME: Obivously we should at some point not randomly reuse the sidebar component
// since this is not oficially supported
.modal__card .app-sidebar {
@@ -209,6 +191,7 @@ export default {
.app-sidebar-header {
position: sticky;
top: 0;
padding-top: $modal-padding;
z-index: 100;
background-color: var(--color-main-background);
}
@@ -220,6 +203,12 @@ export default {
background-color: var(--color-main-background);
}
section.app-sidebar__tab--active {
min-height: auto;
display: flex;
flex-direction: column;
}
#emptycontent, .emptycontent {
margin-top: 88px;
}

View File

@@ -7,11 +7,7 @@
</span>
</div>
<CommentItem v-if="replyTo"
:comment="replyTo"
:reply="true"
:preview="true"
@cancel="cancelReply" />
<CommentItem v-if="replyTo" :comment="replyTo" :reply="true" />
<CommentForm v-model="newComment" @submit="createComment" />
<ul v-if="getCommentsForCard(card.id).length > 0" id="commentsFeed">
@@ -27,8 +23,8 @@
</ul>
<div v-else-if="isLoading" class="icon icon-loading" />
<div v-else class="emptycontent">
<div :class="{ 'icon-comment': !error, 'icon-error': error }" />
<p>{{ error || t('deck', 'No comments yet. Begin the discussion!') }}</p>
<div class="icon-comment" />
<p>{{ t('deck', 'No comments yet. Begin the discussion!') }}</p>
</div>
</div>
</template>
@@ -40,7 +36,6 @@ import CommentItem from './CommentItem'
import CommentForm from './CommentForm'
import InfiniteLoading from 'vue-infinite-loading'
import { getCurrentUser } from '@nextcloud/auth'
export default {
name: 'CardSidebarTabComments',
components: {
@@ -54,18 +49,12 @@ export default {
type: Object,
default: undefined,
},
tabQuery: {
type: String,
required: false,
default: null,
},
},
data() {
return {
newComment: '',
isLoading: false,
currentUser: getCurrentUser(),
error: null,
}
},
computed: {
@@ -91,34 +80,19 @@ export default {
},
methods: {
async infiniteHandler($state) {
this.error = null
try {
await this.loadMore()
if (this.hasMoreComments(this.card.id)) {
$state.loaded()
} else {
$state.complete()
}
} catch (e) {
console.error('Failed to fetch more comments during infinite loading', e)
this.error = t('deck', 'Failed to load comments')
await this.loadMore()
if (this.hasMoreComments(this.card.id)) {
$state.loaded()
} else {
$state.complete()
}
},
async loadComments() {
this.$store.dispatch('setReplyTo', null)
this.error = null
this.isLoading = true
try {
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
this.isLoading = false
if (this.card.commentsUnread > 0) {
await this.$store.dispatch('markCommentsAsRead', this.card.id)
}
} catch (e) {
this.isLoading = false
console.error('Failed to fetch more comments during infinite loading', e)
this.error = t('deck', 'Failed to load comments')
await this.$store.dispatch('fetchComments', { cardId: this.card.id })
this.isLoading = false
if (this.card.commentsUnread > 0) {
await this.$store.dispatch('markCommentsAsRead', this.card.id)
}
},
async createComment(content) {
@@ -136,9 +110,6 @@ export default {
await this.$store.dispatch('fetchMore', { cardId: this.card.id })
this.isLoading = false
},
cancelReply() {
this.$store.dispatch('setReplyTo', null)
},
},
}
</script>

View File

@@ -26,7 +26,7 @@
<At ref="at"
v-model="commentText"
:members="members"
name-key="displayname"
name-key="uid"
:tab-select="true">
<template v-slot:item="s">
<Avatar class="atwho-li--avatar" :user="s.item.uid" :size="24" />
@@ -41,7 +41,6 @@
</span>
</template>
<div ref="contentEditable"
class="comment-form__contenteditable"
contenteditable
@keydown.enter="handleKeydown"
@paste="onPaste"
@@ -176,11 +175,6 @@ export default {
<style scoped lang="scss">
@import '../../css/comments';
.comment-form__contenteditable {
word-break: break-word;
border-radius: var(--border-radius-large)
}
.atwho-wrap {
width: 100%;
& > div[contenteditable] {

View File

@@ -1,22 +1,10 @@
<template>
<div v-if="reply" class="reply" :class="{ 'reply--preview': preview }">
<div class="reply--wrapper">
<div class="reply--header">
<div class="reply--hint">
{{ t('deck', 'In reply to') }}
<UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" />
</div>
<Actions v-if="preview" class="reply--cancel">
<ActionButton icon="icon-close" @click="$emit('cancel')">
{{ t('deck', 'Cancel reply') }}
</ActionButton>
</Actions>
</div>
<RichText class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
</div>
<div v-if="reply" class="reply">
<span class="reply--hint">{{ t('deck', 'In reply to') }} <UserBubble :user="comment.actorId" :display-name="comment.actorDisplayName" /></span>
<RichText class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
</div>
<li v-else class="comment">
<template>
@@ -26,19 +14,13 @@
{{ comment.actorDisplayName }}
</span>
<Actions v-show="!edit" :force-menu="true">
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
<ActionButton icon="icon-reply" @click="replyTo()">
{{ t('deck', 'Reply') }}
</ActionButton>
<ActionButton v-if="canEdit"
icon="icon-rename"
:close-after-click="true"
@click="showUpdateForm()">
<ActionButton v-if="canEdit" icon="icon-rename" @click="showUpdateForm()">
{{ t('deck', 'Update') }}
</ActionButton>
<ActionButton v-if="canEdit"
icon="icon-delete"
:close-after-click="true"
@click="deleteComment()">
<ActionButton v-if="canEdit" icon="icon-delete" @click="deleteComment()">
{{ t('deck', 'Delete') }}
</ActionButton>
</Actions>
@@ -104,10 +86,6 @@ export default {
type: Boolean,
default: false,
},
preview: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -197,41 +175,20 @@ export default {
@import '../../css/comments';
.reply {
margin: 0 0 0 44px;
&.reply--preview {
margin: 4px 0;
padding: 8px;
background-color: var(--color-background-hover);
border-radius: var(--border-radius-large);
.reply--wrapper {
margin: 8px;
}
.reply--cancel {
margin-right: -12px;
margin-top: -12px;
}
}
.reply--wrapper {
border-left: 4px solid var(--color-border-dark);
padding-left: 8px;
}
border-left: 3px solid var(--color-primary-element);
padding-left: 5px;
margin-left: 2px;
margin-bottom: 5px;
&::v-deep .rich-text--wrapper {
margin-top: -3px;
color: var(--color-text-lighter);
}
.reply--header {
display: flex;
color: var(--color-text-light);
}
.reply--hint {
font-size: 0.9em;
color: var(--color-text-lighter);
flex-grow: 1;
vertical-align: top;
}
.comment--content {

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

@@ -22,13 +22,7 @@
<template>
<div v-if="card" class="badges">
<div v-if="card.commentsCount > 0"
v-tooltip="commentsHint"
class="icon icon-comment"
:class="{ 'icon-comment--unread': card.commentsUnread > 0 }"
@click.stop="openComments">
{{ card.commentsCount }}
</div>
<div v-if="card.commentsUnread > 0" class="icon icon-comment" />
<div v-if="card.description && checkListCount > 0" class="card-tasks icon icon-checkmark">
{{ checkListCheckedCount }}/{{ checkListCount }}
@@ -64,21 +58,6 @@ export default {
checkListCheckedCount() {
return (this.card.description.match(/^\s*([*+-]|(\d\.))\s+\[\s*x\s*\](.*)$/gim) || []).length
},
commentsHint() {
if (this.card.commentsUnread > 0) {
return t('deck', '{count} comments, {unread} unread', {
count: this.card.commentsCount,
unread: this.card.commentsUnread
})
}
return null
},
},
methods: {
openComments() {
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id, tabId: 'comments' } })
},
},
}
</script>
@@ -91,7 +70,7 @@ export default {
.icon {
opacity: 0.5;
padding: 10px 20px;
padding: 12px 18px;
padding-right: 4px;
margin-right: 5px;
background-position: left;
@@ -99,8 +78,8 @@ export default {
span {
margin-left: 18px;
}
&.icon-comment--unread {
opacity: 1;
&.icon-edit {
opacity: 0.5;
}
}
}

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

@@ -235,7 +235,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 +278,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 +298,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 = {}

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,224 +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" />
<div v-if="loading" class="icon-loading-small" />
</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: {
async searchQuery() {
this.cursor = null
this.loading = true
try {
await this.search()
this.loading = false
} catch (e) {
if (!axios.isCancel(e)) {
console.error('Search request failed', e)
this.loading = false
}
}
},
},
methods: {
async infiniteHandler($state) {
this.loading = true
try {
const data = await this.search()
if (data.length) {
$state.loaded()
} else {
$state.complete()
}
this.loading = false
} catch (e) {
if (!axios.isCancel(e)) {
console.error('Search request failed', e)
$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 > div {
display: inline-block;
&.icon-loading-small {
margin-right: 20px;
}
}
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

@@ -48,5 +48,4 @@
.comment--content {
margin-left: 44px;
word-break: break-word;
}

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,109 +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, q.length - 2)
}
return q
}
for (const match of matches) {
let [filter, query] = match.indexOf(':') !== -1 ? match.split(/:(.*)/) : [null, match]
const isEmptyQuery = typeof query === 'undefined' || filterOutQuotes(query) === ''
if (filter === 'title') {
if (isEmptyQuery) {
continue
}
hasMatch = hasMatch && card.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'description') {
if (isEmptyQuery) {
hasMatch = hasMatch && !!card.description
continue
}
hasMatch = hasMatch && card.description.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'list') {
if (isEmptyQuery) {
continue
}
const stack = getters.stackById(card.stackId)
if (!stack) {
return false
}
hasMatch = hasMatch && stack.title.toLowerCase().includes(filterOutQuotes(query).toLowerCase())
} else if (filter === 'tag') {
if (isEmptyQuery) {
hasMatch = hasMatch && card.labels.length > 0
continue
}
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') {
if (isEmptyQuery) {
hasMatch = hasMatch && card.assignedUsers.length > 0
continue
}
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)
},
@@ -312,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)
@@ -338,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 })),
@@ -420,7 +417,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)

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
]);
}
}

Some files were not shown because too many files have changed in this diff Show More