Compare commits
1 Commits
v1.4.7
...
enh/flow-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ef5f8edd0 |
55
.github/workflows/app-code-check.yml
vendored
Normal file
55
.github/workflows/app-code-check.yml
vendored
Normal 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 }}
|
||||
2
.github/workflows/integration.yml
vendored
2
.github/workflows/integration.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -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:
|
||||
|
||||
79
CHANGELOG.md
79
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'],
|
||||
]
|
||||
];
|
||||
|
||||
@@ -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
117
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"`.
|
||||
|
||||
16
l10n/cs.js
16
l10n/cs.js
@@ -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"
|
||||
},
|
||||
|
||||
16
l10n/cs.json
16
l10n/cs.json
@@ -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;"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
25
l10n/hr.js
25
l10n/hr.js
@@ -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;");
|
||||
|
||||
25
l10n/hr.json
25
l10n/hr.json
@@ -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;"
|
||||
}
|
||||
87
l10n/hu.js
87
l10n/hu.js
@@ -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);");
|
||||
|
||||
87
l10n/hu.json
87
l10n/hu.json
@@ -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);"
|
||||
}
|
||||
@@ -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} е достигната",
|
||||
|
||||
@@ -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} е достигната",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
103
l10n/sc.js
103
l10n/sc.js
@@ -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);");
|
||||
|
||||
103
l10n/sc.json
103
l10n/sc.json
@@ -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);"
|
||||
}
|
||||
134
l10n/zh_HK.js
134
l10n/zh_HK.js
@@ -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" : "沒有快將到期的卡片",
|
||||
|
||||
134
l10n/zh_HK.json
134
l10n/zh_HK.json
@@ -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" : "沒有快將到期的卡片",
|
||||
|
||||
254
l10n/zh_TW.js
254
l10n/zh_TW.js
@@ -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;");
|
||||
|
||||
254
l10n/zh_TW.json
254
l10n/zh_TW.json
@@ -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;"
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
259
lib/AppInfo/Application20.php
Normal file
259
lib/AppInfo/Application20.php
Normal 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
249
lib/AppInfo/ApplicationLegacy.php
Normal file
249
lib/AppInfo/ApplicationLegacy.php
Normal 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Event;
|
||||
|
||||
class AclCreatedEvent extends AAclEvent {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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
107
lib/Flow/CardEntity.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
lib/Listeners/RegisterChecksListener.php
Normal file
51
lib/Listeners/RegisterChecksListener.php
Normal 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);
|
||||
}
|
||||
}
|
||||
47
lib/Listeners/RegisterEntityListener.php
Normal file
47
lib/Listeners/RegisterEntityListener.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) ?: '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
1512
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -306,7 +306,6 @@ h5 {
|
||||
padding: 0;
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-placeholder {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -48,5 +48,4 @@
|
||||
|
||||
.comment--content {
|
||||
margin-left: 44px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user