Compare commits

...

88 Commits

Author SHA1 Message Date
Julius Härtl
d9014903ac Bump version to 1.9.0
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-03-15 14:57:12 +01:00
Nextcloud bot
e7529e2d74 Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-03-15 02:38:43 +00:00
Julius Härtl
c0e07dc202 Merge pull request #4530 from nextcloud/automated/noid/stable26-update-nextcloud-ocp 2023-03-12 15:22:56 +01:00
nextcloud-command
2619219618 chore(dev-deps): Bump nextcloud/ocp package
Signed-off-by: GitHub <noreply@github.com>
2023-03-12 03:16:40 +00:00
Julius Härtl
87a0a4ed4f Merge pull request #4525 from nextcloud/backport/4510/stable26 2023-03-10 08:54:08 +01:00
Julius Härtl
df01d8ef79 fix(sessions): Do not send close request without token
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-03-10 07:27:22 +00:00
Nextcloud bot
9f38e51d9b Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-03-10 02:40:05 +00:00
Julius Härtl
3f2e343541 Merge pull request #4521 from nextcloud/backport/4512/stable26 2023-03-08 18:59:04 +01:00
Julius Härtl
ea6006bec0 fix(cards): Fix card sizing by limiting too wide style rules
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-03-08 17:41:03 +00:00
Joas Schilling
1e625d3955 Merge pull request #4519 from nextcloud/backport/4518/stable26
[stable26] fix(API): Fix board API details parameter to work as expected
2023-03-08 09:22:32 +01:00
Joas Schilling
4b0a27d6b5 fix(API): Fix board API details parameter to work as expected
Signed-off-by: Joas Schilling <coding@schilljs.com>
2023-03-08 07:43:46 +00:00
Marcel Klehr
9cc38000fd Merge pull request #4516 from nextcloud/backport/4514/stable26 2023-03-07 10:51:40 +01:00
Julius Härtl
3574abe0cb fix(references): Mute NoPermissionException as it is expected to happen for references
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-03-07 09:24:39 +00:00
Julius Härtl
fb5aed2143 chore(release): Bump version to 1.8.0-beta.2
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-03-06 07:14:34 +01:00
Julius Härtl
3506ac2a42 Merge pull request #4509 from nextcloud/automated/noid/stable26-update-nextcloud-ocp 2023-03-06 07:12:08 +01:00
Nextcloud bot
1f2f8fe001 Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-03-06 02:39:11 +00:00
nextcloud-command
97d9c4cc2c chore(dev-deps): Bump nextcloud/ocp package
Signed-off-by: GitHub <noreply@github.com>
2023-03-05 03:18:56 +00:00
Julius Härtl
b169ecd0fe Merge pull request #4498 from nextcloud/update-stable26-target-versions 2023-03-04 11:55:03 +01:00
Nextcloud bot
912376a99d Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-03-04 02:40:24 +00:00
Joas Schilling
3ec2ad99b1 chore(CI): Adjust testing matrix for Nextcloud 26 on stable26
Signed-off-by: Joas Schilling <coding@schilljs.com>
2023-03-03 13:25:30 +01:00
Julius Härtl
a4c7a65ffa Merge pull request #4459 from nextcloud/dependabot/npm_and_yarn/main/nextcloud/dialogs-4.0.1 2023-03-02 22:41:39 +01:00
Julius Härtl
8a67125503 Merge pull request #4457 from nextcloud/dependabot/npm_and_yarn/main/dompurify-3.0.0 2023-03-02 22:41:34 +01:00
dependabot[bot]
f7fc54e628 Chore(deps): Bump dompurify from 2.4.3 to 3.0.0
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.3 to 3.0.0.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.4.3...3.0.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-02 22:34:16 +01:00
dependabot[bot]
28ea6ed03e bump @nextcloud/dialogs from 3.2.0 to 4.0.1
---
updated-dependencies:
- dependency-name: "@nextcloud/dialogs"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-02 22:33:51 +01:00
Julius Härtl
6c4a12707d Merge pull request #4490 from nextcloud/fix/4484
fix: Use proper z-index for text menubar
2023-03-01 22:13:22 +01:00
Marcel Klehr
8fb7bb83a9 Merge pull request #4493 from nextcloud/bugfix/noid/duplicate-boards 2023-03-01 18:47:20 +01:00
Julius Härtl
fbb410667a fix: Always return sorted index array to make sure a json array is the result
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-03-01 17:28:13 +01:00
Julius Härtl
f69868ae26 Merge pull request #4492 from nextcloud/fix/undefined-array-index-dashboard 2023-03-01 15:59:33 +01:00
Marcel Klehr
e41627d763 fix(dashboard): Fix undefined array index
fixes #4491

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
2023-03-01 15:43:26 +01:00
Marcel Klehr
4a89db6d67 Merge pull request #4487 from nextcloud/bugfix/3358
fix: Use passed userid when getting attachment folder
2023-02-28 13:28:10 +01:00
Julius Härtl
a2ffdb41af Merge pull request #4488 from nextcloud/bugfix/4483 2023-02-28 09:22:47 +01:00
Julius Härtl
9fe79ed135 fix: Use proper z-index for text menubar
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-27 23:46:29 +01:00
Julius Härtl
a198a4eef4 fix: Avoid mutating the due date when calculating days
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-27 23:13:06 +01:00
Julius Härtl
62752f8b72 fix: Use passed userid when getting attachment folder
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-27 22:56:28 +01:00
Joas Schilling
2c96108b2e Merge pull request #4485 from nextcloud/fix/permission-userid
fix: Pass user id along to properly check permissions in background jobs
2023-02-27 12:48:15 +01:00
Julius Härtl
1beff8945b fix: Pass user id along to properly check permissions in background jobs
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-27 11:25:45 +01:00
Julius Härtl
ff2ad61b6d Merge pull request #4471 from nextcloud/dependabot/composer/vimeo/psalm-5.7.7 2023-02-27 09:29:51 +01:00
Julius Härtl
05941396c0 Merge pull request #4478 from nextcloud/dependabot/npm_and_yarn/main/nextcloud/l10n-2.1.0 2023-02-27 09:23:04 +01:00
Julius Härtl
6d037e7d94 Merge pull request #4477 from nextcloud/dependabot/npm_and_yarn/main/stylelint-webpack-plugin-4.1.0 2023-02-27 09:06:32 +01:00
Julius Härtl
0768955559 Merge pull request #4479 from nextcloud/dependabot/npm_and_yarn/main/cypress-12.7.0 2023-02-27 09:06:17 +01:00
Julius Härtl
faf35a98a3 Merge pull request #4472 from nextcloud/dependabot/github_actions/actions/github-script-6 2023-02-27 08:39:27 +01:00
Julius Härtl
815cc605a8 Merge pull request #4474 from nextcloud/dependabot/github_actions/svenstaro/upload-release-action-2.5.0 2023-02-27 08:39:20 +01:00
Julius Härtl
3d7410c30c Merge pull request #4481 from nextcloud/dependabot/npm_and_yarn/main/nextcloud/vue-7.7.1
bump @nextcloud/vue from 7.5.0 to 7.7.1
2023-02-27 08:38:41 +01:00
Julius Härtl
3921bd7f60 Merge pull request #4480 from nextcloud/dependabot/npm_and_yarn/main/babel/runtime-7.21.0
bump @babel/runtime from 7.20.13 to 7.21.0
2023-02-27 08:38:09 +01:00
dependabot[bot]
91ae931461 bump @nextcloud/vue from 7.5.0 to 7.7.1
---
updated-dependencies:
- dependency-name: "@nextcloud/vue"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 04:24:11 +00:00
dependabot[bot]
fc4ccf4010 bump @babel/runtime from 7.20.13 to 7.21.0
---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 04:01:01 +00:00
dependabot[bot]
b3cc1da02d chore(deps-dev): bump cypress from 12.5.1 to 12.7.0
Bumps [cypress](https://github.com/cypress-io/cypress) from 12.5.1 to 12.7.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v12.5.1...v12.7.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:59:05 +00:00
dependabot[bot]
e4bd1efe00 chore(deps): bump @nextcloud/l10n from 2.0.1 to 2.1.0
Bumps [@nextcloud/l10n](https://github.com/nextcloud/nextcloud-l10n) from 2.0.1 to 2.1.0.
- [Release notes](https://github.com/nextcloud/nextcloud-l10n/releases)
- [Changelog](https://github.com/nextcloud/nextcloud-l10n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nextcloud/nextcloud-l10n/compare/v2.0.1...v2.1.0)

---
updated-dependencies:
- dependency-name: "@nextcloud/l10n"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:58:33 +00:00
dependabot[bot]
45f3a64ae4 chore(deps-dev): bump stylelint-webpack-plugin from 4.0.0 to 4.1.0
Bumps [stylelint-webpack-plugin](https://github.com/webpack-contrib/stylelint-webpack-plugin) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/webpack-contrib/stylelint-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/stylelint-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/stylelint-webpack-plugin/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: stylelint-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:58:04 +00:00
dependabot[bot]
52940ed4a1 chore(deps): bump svenstaro/upload-release-action from 2.4.1 to 2.5.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](cc92c9093e...7319e4733e)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:57:50 +00:00
dependabot[bot]
2ba1412ae1 chore(deps): bump actions/github-script from 5 to 6
Bumps [actions/github-script](https://github.com/actions/github-script) from 5 to 6.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:57:45 +00:00
dependabot[bot]
b42ff90e0c chore(deps-dev): bump vimeo/psalm from 5.6.0 to 5.7.7
Bumps [vimeo/psalm](https://github.com/vimeo/psalm) from 5.6.0 to 5.7.7.
- [Release notes](https://github.com/vimeo/psalm/releases)
- [Commits](https://github.com/vimeo/psalm/compare/5.6.0...5.7.7)

---
updated-dependencies:
- dependency-name: vimeo/psalm
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:56:28 +00:00
Nextcloud bot
de66f47ac9 Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-02-24 02:42:08 +00:00
Julius Härtl
f5b1e89a9c release: Bump version to 1.9.0-beta.1
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-22 17:39:58 +01:00
Nextcloud bot
4e9a00c3a2 Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-02-22 02:34:17 +00:00
Julius Härtl
0af2fd45e7 Merge pull request #4273 from alangecker/live-updates
live updates 🎉
2023-02-21 22:18:45 +01:00
chandi Langecker
b4eece879d test(unit): fix tests, mostly due to missing boardId's, which are now required for event emitting
Signed-off-by: chandi Langecker <git@chandi.it>
2023-02-21 21:53:22 +01:00
chandi Langecker
c03d067464 style(sessionlist): less incisive borders
Signed-off-by: chandi Langecker <git@chandi.it>
2023-02-21 21:53:22 +01:00
chandi Langecker
437f5c9ab5 chore(psalm): adding missing events for annotation
Signed-off-by: chandi Langecker <git@chandi.it>
2023-02-21 21:53:22 +01:00
chandi Langecker
2e6b20d71d live updates: remove deleted cards with loadStacks() and not just append them
Signed-off-by: chandi Langecker <git@chandi.it>
2023-02-21 21:53:22 +01:00
chandi Langecker
41d8867bdd live updates: listen for stack and board changes
Signed-off-by: chandi Langecker <git@chandi.it>
2023-02-21 21:53:22 +01:00
chandi Langecker
322ee92573 live updates for boards
Signed-off-by: chandi Langecker <git@chandi.it>
2023-02-21 21:53:20 +01:00
Nextcloud bot
9674c344ea Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-02-21 02:33:43 +00:00
Julius Härtl
33912454ae Merge pull request #4460 from nextcloud/dependabot/npm_and_yarn/main/nextcloud-vue-collections-0.11.0
bump nextcloud-vue-collections from 0.10.0 to 0.11.0
2023-02-20 19:03:21 +01:00
Julius Härtl
15c1c9ddc4 Merge pull request #4452 from nextcloud/enh/perf 2023-02-20 16:46:02 +01:00
Nextcloud bot
8bb106b327 Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-02-19 02:34:25 +00:00
dependabot[bot]
43c1a3bbc7 bump nextcloud-vue-collections from 0.10.0 to 0.11.0
---
updated-dependencies:
- dependency-name: nextcloud-vue-collections
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-18 04:39:07 +00:00
Nextcloud bot
5e1b6d248c Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-02-18 02:34:45 +00:00
Julius Härtl
28a9c66143 ci: Use faster password hashing for CI
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:23:29 +01:00
Julius Härtl
b4de6a8f96 fix: Chunk in-queries to 1000 items
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:43 +01:00
Julius Härtl
46df19a3a6 fix: Fix tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:28 +01:00
Julius Härtl
b19b7794bc perf: Cache full/partial board data differently
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
29d21e05e8 chore: Remove some unused methods
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
afcd226be8 refactor: Unify board enrichment
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
4b319d8d23 perf: Avoid extra round trips when checking permissions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
8ec8a91cab perf: Group queries for fetching overview cards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
96d1e14390 perf: Cache stacks per request
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
c01e542044 perf: remove duplicate fetching of assignments
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
133e3f3140 tests: Adapt tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
c1e29ab8cb fix: Fix missing getBoardId method on AclEvent
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:27 +01:00
Julius Härtl
af21282468 style: php-cs-fixer
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Julius Härtl
ba3cab1036 perf: Combine fetching acls for boards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Julius Härtl
81c0d96357 perf: Make fetching user details lazy
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Julius Härtl
ea8b7999f7 perf: No need to fetch boards every middleware call
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Julius Härtl
7bfbbee6e8 perf: Enrich calls in combined sql queries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Julius Härtl
23813b7a03 perf: Add mapper methods to get multiple labels/assignments for cards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Julius Härtl
2542b6ed16 ci: Add query count for integration tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2023-02-17 09:16:26 +01:00
Nextcloud bot
0878adb124 Fix(l10n): 🔠 Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2023-02-17 02:33:42 +00:00
83 changed files with 1448 additions and 1469 deletions

View File

@@ -148,7 +148,7 @@ jobs:
tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }}
- name: Attach tarball to github release
uses: svenstaro/upload-release-action@cc92c9093e5f785e23a3d654fe2671640b851b5f # v2
uses: svenstaro/upload-release-action@7319e4733ec7a184d739a6f412c40ffc339b69c7 # v2
id: attach_to_release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -23,7 +23,7 @@ jobs:
# containers: [1, 2, 3]
php-versions: [ '8.0' ]
databases: [ 'sqlite' ]
server-versions: [ 'master' ]
server-versions: [ 'stable26' ]
steps:
- name: Use Node.js ${{ matrix.node-version }}

View File

@@ -28,7 +28,7 @@ jobs:
matrix:
php-versions: ['8.1']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['master']
server-versions: ['stable26']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
@@ -74,11 +74,12 @@ jobs:
uses: shivammathur/setup-php@2.24.0
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql,
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, mysql, pdo_mysql, pgsql, pdo_pgsql, apcu
ini-values:
apc.enable_cli=on
coverage: none
- name: Set up PHPUnit
- name: Set up dependencies
working-directory: apps/${{ env.APP_NAME }}
run: composer i --no-dev
@@ -91,11 +92,63 @@ jobs:
fi
mkdir data
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
./occ config:system:set hashing_default_password --value=true --type=boolean
./occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
./occ config:system:set memcache.distributed --value="\\OC\\Memcache\\APCu"
cat config/config.php
./occ user:list
./occ app:enable --force ${{ env.APP_NAME }}
./occ config:system:set query_log_file --value '/home/runner/work/${{ env.APP_NAME }}/${{ env.APP_NAME }}/query.log'
php -S localhost:8080 &
- name: Run behat
working-directory: apps/${{ env.APP_NAME }}/tests/integration
run: ./run.sh
- name: Query count
if: ${{ matrix.databases == 'mysql' }}
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
let myOutput = ''
let myError = ''
const options = {}
options.listeners = {
stdout: (data) => {
myOutput += data.toString()
},
stderr: (data) => {
myError += data.toString()
}
}
await exec.exec(`/bin/bash -c "cat /home/runner/work/${{ env.APP_NAME }}/${{ env.APP_NAME }}/query.log | wc -l"`, [], options)
msg = myOutput
const queryCount = parseInt(myOutput, 10)
myOutput = ''
await exec.exec('cat', ['/home/runner/work/${{ env.APP_NAME }}/${{ env.APP_NAME }}/apps/${{ env.APP_NAME }}/tests/integration/base-query-count.txt'], options)
const baseCount = parseInt(myOutput, 10)
const absoluteIncrease = queryCount - baseCount
const relativeIncrease = baseCount <= 0 ? 100 : (parseInt((absoluteIncrease / baseCount * 10000), 10) / 100)
if (absoluteIncrease >= 100 || relativeIncrease > 5) {
const comment = `🐢 Performance warning.\nIt looks like the query count of the integration tests increased with this PR.\nDatabase query count is now ` + queryCount + ' was ' + baseCount + ' (+' + relativeIncrease + '%)\nPlease check your code again. If you added a new test this can be expected and the base value in tests/integration/base-query-count.txt can be increased.'
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
})
}
if (queryCount < 100) {
const comment = `🐈 Performance messuring seems broken. Failed to get query count.`
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
})
}

View File

@@ -26,9 +26,9 @@ jobs:
strategy:
fail-fast: false
matrix:
php-versions: ['8.0', '8.1']
php-versions: ['8.0', '8.1', '8.2']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['master']
server-versions: ['stable26']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}

View File

@@ -1,6 +1,42 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.9.0
### Added
- Live updates on board collaboration using notify_push @alangecker [#4273](https://github.com/nextcloud/deck/pull/4273)
- Basic notify_push usage with session handling @alangecker [#3876](https://github.com/nextcloud/deck/pull/3876)
- Use text as editor if available [#4399](https://github.com/nextcloud/deck/pull/4399)
- Improve reference provider and add reference widgets @julien-nc [#4422](https://github.com/nextcloud/deck/pull/4422)
- Tag creation from card view @juliushaertl [#4344](https://github.com/nextcloud/deck/pull/4344)
- Optimize query performance with larger board or card count @[#4452](https://github.com/nextcloud/deck/pull/4452)
- Export Board as CSV @david-loe [#3065](https://github.com/nextcloud/deck/pull/3065)
### Fixed
- fix(cards): Fix card sizing by limiting too wide style rules [#4521](https://github.com/nextcloud/deck/pull/4521)
- fix(references): Mute NoPermissionException as it is expected to happen for references [#4516](https://github.com/nextcloud/deck/pull/4516)
- fix(API): Fix board API details parameter to work as expected [#4519](https://github.com/nextcloud/deck/pull/4519)
- fix(sessions): Do not send close request without token [#4525](https://github.com/nextcloud/deck/pull/4525)
- fix: Avoid mutating the due date when calculating days @juliushaertl [#4488](https://github.com/nextcloud/deck/pull/4488)
- fix: Pass user id along to properly check permissions in background jobs @juliushaertl [#4485](https://github.com/nextcloud/deck/pull/4485)
- fix: Use passed userid when getting attachment folder @juliushaertl [#4487](https://github.com/nextcloud/deck/pull/4487)
- fix: Use proper z-index for text menubar @juliushaertl [#4490](https://github.com/nextcloud/deck/pull/4490)
- fix(dashboard): Fix undefined array index @marcelklehr [#4492](https://github.com/nextcloud/deck/pull/4492)
- fix: Always return sorted index array to make sure a json array is the result @juliushaertl [#4493](https://github.com/nextcloud/deck/pull/4493)
- Fix component renaming so that acl works on shares again @small1 [#4315](https://github.com/nextcloud/deck/pull/4315)
- fix(Sidebar): Only close sidebar on v-click-outside for specific targets @juliushaertl [#4350](https://github.com/nextcloud/deck/pull/4350)
- add basic e2e tests for stack title @shoetten [#4206](https://github.com/nextcloud/deck/pull/4206)
- App metadata: add links to user and developer documentation @p-bo [#4356](https://github.com/nextcloud/deck/pull/4356)
- Update signature of Entity::markFieldUpdated @nickvergessen [#4398](https://github.com/nextcloud/deck/pull/4398)
- Remove updated nightly information @xf- [#4419](https://github.com/nextcloud/deck/pull/4419)
- perf: Register notifier and resource listener lazy @juliushaertl [#4439](https://github.com/nextcloud/deck/pull/4439)
- perf: Lazy load dashboard components @juliushaertl [#4440](https://github.com/nextcloud/deck/pull/4440)
- Optimise upcomming overview creation @Raudius [#3793](https://github.com/nextcloud/deck/pull/3793)
## 1.8.0-beta.1
### Enhancements

View File

@@ -16,7 +16,7 @@
- 🚀 Get your project organized
</description>
<version>1.9.0-beta.1</version>
<version>1.9.0</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<documentation>

View File

@@ -19,7 +19,7 @@
"symfony/event-dispatcher": "^4.0",
"vimeo/psalm": "^5.4",
"php-parallel-lint/php-parallel-lint": "^1.2",
"nextcloud/ocp": "dev-master"
"nextcloud/ocp": "dev-stable26"
},
"config": {
"optimize-autoloader": true,

114
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "22d201a4569de6d4fafbc13277ae91a6",
"content-hash": "7f234626b3fd062832a6387b9434427c",
"packages": [
{
"name": "cogpowered/finediff",
@@ -952,16 +952,16 @@
},
{
"name": "fidry/cpu-core-counter",
"version": "0.4.1",
"version": "0.5.1",
"source": {
"type": "git",
"url": "https://github.com/theofidry/cpu-core-counter.git",
"reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2"
"reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/79261cc280aded96d098e1b0e0ba0c4881b432c2",
"reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2",
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/b58e5a3933e541dc286cc91fc4f3898bbc6f1623",
"reference": "b58e5a3933e541dc286cc91fc4f3898bbc6f1623",
"shasum": ""
},
"require": {
@@ -1001,7 +1001,7 @@
],
"support": {
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
"source": "https://github.com/theofidry/cpu-core-counter/tree/0.4.1"
"source": "https://github.com/theofidry/cpu-core-counter/tree/0.5.1"
},
"funding": [
{
@@ -1009,7 +1009,7 @@
"type": "github"
}
],
"time": "2022-12-16T22:01:02+00:00"
"time": "2022-12-24T12:35:10+00:00"
},
{
"name": "friendsofphp/php-cs-fixer",
@@ -1253,26 +1253,24 @@
},
{
"name": "nextcloud/ocp",
"version": "dev-master",
"version": "dev-stable26",
"source": {
"type": "git",
"url": "https://github.com/nextcloud-deps/ocp.git",
"reference": "5636b942e35ee391b1103150261d83d3d753d657"
"reference": "cd0f1f72b7589f7751a06d379c9e886ecce5b6d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/5636b942e35ee391b1103150261d83d3d753d657",
"reference": "5636b942e35ee391b1103150261d83d3d753d657",
"url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/cd0f1f72b7589f7751a06d379c9e886ecce5b6d6",
"reference": "cd0f1f72b7589f7751a06d379c9e886ecce5b6d6",
"shasum": ""
},
"require": {
"php": "^7.4 || ~8.0 || ~8.1",
"psr/clock": "^1.0",
"psr/container": "^1.1.1",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.1"
},
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
@@ -1292,9 +1290,9 @@
"description": "Composer package containing Nextcloud's public API (classes, interfaces)",
"support": {
"issues": "https://github.com/nextcloud-deps/ocp/issues",
"source": "https://github.com/nextcloud-deps/ocp/tree/master"
"source": "https://github.com/nextcloud-deps/ocp/tree/stable26"
},
"time": "2023-02-08T00:37:37+00:00"
"time": "2023-03-10T00:40:06+00:00"
},
{
"name": "nikic/php-parser",
@@ -2206,54 +2204,6 @@
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/clock.git",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for reading the clock.",
"homepage": "https://github.com/php-fig/clock",
"keywords": [
"clock",
"now",
"psr",
"psr-20",
"time"
],
"support": {
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
"time": "2022-11-25T14:36:26+00:00"
},
{
"name": "psr/container",
"version": "1.1.2",
@@ -3800,26 +3750,25 @@
},
{
"name": "spatie/array-to-xml",
"version": "2.17.1",
"version": "3.1.5",
"source": {
"type": "git",
"url": "https://github.com/spatie/array-to-xml.git",
"reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46"
"reference": "13f76acef5362d15c71ae1ac6350cc3df5e25e43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46",
"reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46",
"url": "https://api.github.com/repos/spatie/array-to-xml/zipball/13f76acef5362d15c71ae1ac6350cc3df5e25e43",
"reference": "13f76acef5362d15c71ae1ac6350cc3df5e25e43",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": "^7.4|^8.0"
"php": "^8.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
"pestphp/pest": "^1.21",
"phpunit/phpunit": "^9.0",
"spatie/pest-plugin-snapshots": "^1.1"
},
"type": "library",
@@ -3848,7 +3797,7 @@
"xml"
],
"support": {
"source": "https://github.com/spatie/array-to-xml/tree/2.17.1"
"source": "https://github.com/spatie/array-to-xml/tree/3.1.5"
},
"funding": [
{
@@ -3860,7 +3809,7 @@
"type": "github"
}
],
"time": "2022-12-26T08:22:07+00:00"
"time": "2022-12-24T13:43:51+00:00"
},
{
"name": "symfony/console",
@@ -5303,16 +5252,16 @@
},
{
"name": "vimeo/psalm",
"version": "5.6.0",
"version": "5.7.7",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
"reference": "e784128902dfe01d489c4123d69918a9f3c1eac5"
"reference": "e028ba46ba0d7f9a78bc3201c251e137383e145f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/e784128902dfe01d489c4123d69918a9f3c1eac5",
"reference": "e784128902dfe01d489c4123d69918a9f3c1eac5",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/e028ba46ba0d7f9a78bc3201c251e137383e145f",
"reference": "e028ba46ba0d7f9a78bc3201c251e137383e145f",
"shasum": ""
},
"require": {
@@ -5331,12 +5280,12 @@
"ext-tokenizer": "*",
"felixfbecker/advanced-json-rpc": "^3.1",
"felixfbecker/language-server-protocol": "^1.5.2",
"fidry/cpu-core-counter": "^0.4.0",
"fidry/cpu-core-counter": "^0.4.1 || ^0.5.1",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^4.13",
"php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0",
"sebastian/diff": "^4.0 || ^5.0",
"spatie/array-to-xml": "^2.17.0",
"spatie/array-to-xml": "^2.17.0 || ^3.0",
"symfony/console": "^4.1.6 || ^5.0 || ^6.0",
"symfony/filesystem": "^5.4 || ^6.0"
},
@@ -5345,13 +5294,13 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4",
"brianium/paratest": "^6.0",
"brianium/paratest": "^6.9",
"ext-curl": "*",
"mockery/mockery": "^1.5",
"nunomaduro/mock-final-classes": "^1.1",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpdoc-parser": "^1.6",
"phpunit/phpunit": "^9.5",
"phpunit/phpunit": "^9.6",
"psalm/plugin-mockery": "^1.1",
"psalm/plugin-phpunit": "^0.18",
"slevomat/coding-standard": "^8.4",
@@ -5397,13 +5346,14 @@
"keywords": [
"code",
"inspection",
"php"
"php",
"static analysis"
],
"support": {
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm/tree/5.6.0"
"source": "https://github.com/vimeo/psalm/tree/5.7.7"
},
"time": "2023-01-23T20:32:47+00:00"
"time": "2023-02-25T01:05:07+00:00"
},
{
"name": "webmozart/assert",

View File

@@ -79,8 +79,12 @@ OC.L10N.register(
"The board \"%s\" has been shared with you by %s." : "Таблото \"%s\" е споделено с вас от%s.",
"{user} has shared {deck-board} with you." : "{user} сподели {deck-board} с Вас.",
"Deck board" : "Deck табло",
"Owned by %1$s" : "Притежаван от %1$s",
"Deck boards, cards and comments" : "Табла, карти и коментари",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "От %1$s, в %2$s/%3$s, притежание на %4$s",
"Card comments" : "Коментари на карти",
"%s on %s" : "%s на %s",
"Deck boards and cards" : "Табла и карти",
"No data was provided to create an attachment." : "Не бяха предоставени данни за създаване на прикачен файл.",
"Finished" : "Готово",
"To review" : "За преглед",
@@ -291,6 +295,7 @@ OC.L10N.register(
"No due" : "Не се дължи",
"Search for {searchQuery} in all boards" : "Търсене на {searchQuery} във всички табла",
"No results found" : "Няма намерени резултати",
"Deck board {name}\n* Last modified on {lastMod}" : "Табло {name}\n* Последна промяна на {lastMod}",
"{stack} in {board}" : "{stack} в {board}",
"Click to expand description" : "Кликване за разширяване на описанието",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Създаден на {created}\n* Последна промяна на {lastMod} \n* {nbAttachments} прикачени файлове \n* {nbComments} коментара",

View File

@@ -77,8 +77,12 @@
"The board \"%s\" has been shared with you by %s." : "Таблото \"%s\" е споделено с вас от%s.",
"{user} has shared {deck-board} with you." : "{user} сподели {deck-board} с Вас.",
"Deck board" : "Deck табло",
"Owned by %1$s" : "Притежаван от %1$s",
"Deck boards, cards and comments" : "Табла, карти и коментари",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "От %1$s, в %2$s/%3$s, притежание на %4$s",
"Card comments" : "Коментари на карти",
"%s on %s" : "%s на %s",
"Deck boards and cards" : "Табла и карти",
"No data was provided to create an attachment." : "Не бяха предоставени данни за създаване на прикачен файл.",
"Finished" : "Готово",
"To review" : "За преглед",
@@ -289,6 +293,7 @@
"No due" : "Не се дължи",
"Search for {searchQuery} in all boards" : "Търсене на {searchQuery} във всички табла",
"No results found" : "Няма намерени резултати",
"Deck board {name}\n* Last modified on {lastMod}" : "Табло {name}\n* Последна промяна на {lastMod}",
"{stack} in {board}" : "{stack} в {board}",
"Click to expand description" : "Кликване за разширяване на описанието",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Създаден на {created}\n* Последна промяна на {lastMod} \n* {nbAttachments} прикачени файлове \n* {nbComments} коментара",

View File

@@ -79,8 +79,11 @@ OC.L10N.register(
"The board \"%s\" has been shared with you by %s." : "Ο πίνακας \"%s\" είναι σε κοινή χρήση μαζί σας από %s.",
"{user} has shared {deck-board} with you." : "Ο/Η διαμοιράστηκε μαζί σας το {deck-board}",
"Deck board" : "Πίνακας του Deck",
"Owned by %1$s" : "Ανήκει στον/στην %1$s",
"Deck boards, cards and comments" : "Πίνακες, κάρτες και σχόλια Deck",
"Card comments" : "Σχόλια καρτέλας",
"%s on %s" : "%s στο %s",
"Deck boards and cards" : "Πίνακες και κάρτες Deck",
"No data was provided to create an attachment." : "Δεν δόθηκαν στοιχεία για δημιουργία συνημμένου.",
"Finished" : "Ολοκληρώθηκε",
"To review" : "Προς επισκόπηση",
@@ -136,6 +139,7 @@ OC.L10N.register(
"Archived cards" : "Αρχειοθετημένες καρτέλες",
"Add list" : "Προσθήκη λίστας",
"List name" : "Όνομα λίστας",
"Active filters" : "Ενεργά φίλτρα",
"Apply filter" : "Εφαρμογή φίλτρου",
"Filter by tag" : "Φίλτρο ανά ετικέτα",
"Filter by assigned user" : "Φίλτρο ανά χρήστη",
@@ -152,6 +156,7 @@ OC.L10N.register(
"Toggle compact mode" : "Εναλλαγή λειτουργίας μικρού μεγέθους",
"Open details" : "Άνοιγμα λεπτομερειών",
"Details" : "Λεπτομέρειες",
"Currently present people" : "Παρόντες αυτή τη στιγμή",
"Loading board" : "Φόρτωση πίνακα",
"No lists available" : "Δεν υπάρχουν διαθέσιμες λίστες",
"Create a new list to add cards to this board" : "Δημιουργήστε νέα λίστα για να προσθέσετε καρτέλες σε αυτό τον πίνακα.",
@@ -175,10 +180,17 @@ OC.L10N.register(
"Owner" : "Κάτοχος",
"Delete" : "Διαγραφή",
"Failed to create share with {displayName}" : "Αποτυχία δημιουργίας κοινής χρήσης με το {displayName}",
"Are you sure you want to transfer the board {title} to {user}?" : "Είστε σίγουροι ότι θέλετε να μεταφέρετε τον πίνακα {title} στον {user}? ",
"Transfer the board." : "Μεταφορά του πίνακα.",
"Transfer" : "Μεταφορά",
"The board has been transferred to {user}" : "Ο πίνακας έχει μεταφερθεί στον/στην {user}",
"Failed to transfer the board to {user}" : "Απέτυχε η μεταφορά του πίνακα στον χρήστη {user}",
"Edit list title" : "Επεξεργασία τίτλου λίστας",
"Archive all cards" : "Αρχειοθέτηση όλων των καρτελών.",
"Unarchive all cards" : "Κατάργηση αρχειοθέτησης όλων των καρτών",
"Delete list" : "Διαγραφή λίστας",
"Archive all cards in this list" : "Αρχειοθέτηση όλων των καρτελών σε αυτή τη λίστα.",
"Unarchive all cards in this list" : "Κατάργηση αρχειοθέτησης όλων των καρτών σε αυτή τη λίστα",
"Add a new card" : "Προσθήκη νέας καρτέλας",
"Card name" : "Όνομα καρτέλας",
"List deleted" : "Η λίστα διαγράφηκε",
@@ -236,7 +248,9 @@ OC.L10N.register(
"Write a description …" : "Γράψτε μια περιγραφή…",
"Choose attachment" : "Επιλογή συνημμένου",
"(group)" : "(ομάδα)",
"Todo items" : "Στοιχεία todo",
"{count} comments, {unread} unread" : "{count} σχόλια, {unread} μη αναγνωσμένα",
"Edit card title" : "Επεξεργασία τίτλου κάρτας",
"Assign to me" : "Ανάθεση σε εμένα",
"Unassign myself" : "Αποδέσμευσή μου",
"Move card" : "Μετακίνηση καρτέλας",
@@ -251,6 +265,7 @@ OC.L10N.register(
"All boards" : "Όλοι οι πίνακες",
"Archived boards" : "Αρχειοθέτηση πινάκων ",
"Shared with you" : "Διαμοιρασμένα μαζί σας",
"Deck settings" : "Ρυθμίσεις Deck",
"Use bigger card view" : "Χρησιμοποιήστε μεγαλύτερη προβολή καρτέλας",
"Show boards in calendar/tasks" : "Εμφάνιση πινάκων στο ημερολόγιο / εργασίες",
"Limit deck usage of groups" : "Περιορίστε τη χρήση της εφαρμογής deck σε ομάδες",
@@ -260,6 +275,7 @@ OC.L10N.register(
"Clone board" : "Κλώνος πίνακα",
"Unarchive board" : "Κατάργηση αρχειοθέτησης πίνακα",
"Archive board" : "Αρχειοθέτηση πίνακα",
"Export board" : "Εξαγωγή πίνακα",
"Turn on due date reminders" : "Ενεργοποιήστε τις υπενθυμίσεις ημερομηνίας προθεσμίας",
"Turn off due date reminders" : "Απενεργοποιήστε τις υπενθυμίσεις ημερομηνίας προθεσμίας",
"Due date reminders" : "Υπενθυμίσεις ημερομηνίας προθεσμίας",
@@ -271,14 +287,21 @@ OC.L10N.register(
"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 including archived cards." : "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον πίνακα {title}; Αυτό θα διαγράψει όλα τα δεδομένα του πίνακα συμπεριλαμβανομένων και των αρχειοθετημένων καρτών.",
"Delete the board?" : "Διαγραφή του πίνακα;",
"Loading filtered view" : "Φόρτωση εμφάνισης με βάση το φίλτρο",
"No due" : "Χωρίς λήξη",
"Search for {searchQuery} in all boards" : "Αναζήτηση για {searchQuery} σε όλους τους πίνακες",
"No results found" : "Δεν βρέθηκαν αποτελέσματα",
"Deck board {name}\n* Last modified on {lastMod}" : "Πίνακας Deck {name}\n* Τελευταία τροποποίηση στις {lastMod}",
"{stack} in {board}" : "{stack} στο {board}",
"Click to expand description" : "Κλικ για επέκταση περιγραφής",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Δημιουργήθηκε στις {created}\n* Τροποποιήθηκε στις {lastMod}\n* {nbAttachments} συνημμένα\n* {nbComments} σχόλια",
"{nbCards} cards" : "{nbCards} κάρτες",
"Click to expand comment" : "Κλικ για επέκταση σχολίου",
"No upcoming cards" : "Δεν υπάρχουν επερχόμενες καρτέλες",
"upcoming cards" : "επερχόμενες καρτέλες",
"Due on {date}" : "Προθεσμία στις {date}",
"Link to a board" : "Σύνδεσμος στον πίνακα",
"Link to a card" : "Σύνδεσμος σε καρτέλα",
"Create a card" : "Δημιουργία καρτέλας",
@@ -291,6 +314,8 @@ OC.L10N.register(
"Share {file} with a Deck card" : "Μοιραστείτε το {file} με μια καρτέλα Deck",
"Share" : "Μοιραστείτε",
"Are you sure you want to transfer the board {title} for {user}?" : "Είστε σίγουροι ότι θέλετε να μεταφέρετε τον πίνακα {title} για {user}? ",
"Transfer the board for {user} successfully" : "Επιτυχής μεταφορά του πίνακα για τον χρήστη {user}",
"Failed to transfer the board for {user}" : "Απέτυχε η μεταφορά του πίνακα για τον χρήστη {user}",
"Add a new list" : "Προσθήκη νέας λίστας",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον πίνακα {title}; Θα διαγραφούν όλα τα δεδομένα."
},

View File

@@ -77,8 +77,11 @@
"The board \"%s\" has been shared with you by %s." : "Ο πίνακας \"%s\" είναι σε κοινή χρήση μαζί σας από %s.",
"{user} has shared {deck-board} with you." : "Ο/Η διαμοιράστηκε μαζί σας το {deck-board}",
"Deck board" : "Πίνακας του Deck",
"Owned by %1$s" : "Ανήκει στον/στην %1$s",
"Deck boards, cards and comments" : "Πίνακες, κάρτες και σχόλια Deck",
"Card comments" : "Σχόλια καρτέλας",
"%s on %s" : "%s στο %s",
"Deck boards and cards" : "Πίνακες και κάρτες Deck",
"No data was provided to create an attachment." : "Δεν δόθηκαν στοιχεία για δημιουργία συνημμένου.",
"Finished" : "Ολοκληρώθηκε",
"To review" : "Προς επισκόπηση",
@@ -134,6 +137,7 @@
"Archived cards" : "Αρχειοθετημένες καρτέλες",
"Add list" : "Προσθήκη λίστας",
"List name" : "Όνομα λίστας",
"Active filters" : "Ενεργά φίλτρα",
"Apply filter" : "Εφαρμογή φίλτρου",
"Filter by tag" : "Φίλτρο ανά ετικέτα",
"Filter by assigned user" : "Φίλτρο ανά χρήστη",
@@ -150,6 +154,7 @@
"Toggle compact mode" : "Εναλλαγή λειτουργίας μικρού μεγέθους",
"Open details" : "Άνοιγμα λεπτομερειών",
"Details" : "Λεπτομέρειες",
"Currently present people" : "Παρόντες αυτή τη στιγμή",
"Loading board" : "Φόρτωση πίνακα",
"No lists available" : "Δεν υπάρχουν διαθέσιμες λίστες",
"Create a new list to add cards to this board" : "Δημιουργήστε νέα λίστα για να προσθέσετε καρτέλες σε αυτό τον πίνακα.",
@@ -173,10 +178,17 @@
"Owner" : "Κάτοχος",
"Delete" : "Διαγραφή",
"Failed to create share with {displayName}" : "Αποτυχία δημιουργίας κοινής χρήσης με το {displayName}",
"Are you sure you want to transfer the board {title} to {user}?" : "Είστε σίγουροι ότι θέλετε να μεταφέρετε τον πίνακα {title} στον {user}? ",
"Transfer the board." : "Μεταφορά του πίνακα.",
"Transfer" : "Μεταφορά",
"The board has been transferred to {user}" : "Ο πίνακας έχει μεταφερθεί στον/στην {user}",
"Failed to transfer the board to {user}" : "Απέτυχε η μεταφορά του πίνακα στον χρήστη {user}",
"Edit list title" : "Επεξεργασία τίτλου λίστας",
"Archive all cards" : "Αρχειοθέτηση όλων των καρτελών.",
"Unarchive all cards" : "Κατάργηση αρχειοθέτησης όλων των καρτών",
"Delete list" : "Διαγραφή λίστας",
"Archive all cards in this list" : "Αρχειοθέτηση όλων των καρτελών σε αυτή τη λίστα.",
"Unarchive all cards in this list" : "Κατάργηση αρχειοθέτησης όλων των καρτών σε αυτή τη λίστα",
"Add a new card" : "Προσθήκη νέας καρτέλας",
"Card name" : "Όνομα καρτέλας",
"List deleted" : "Η λίστα διαγράφηκε",
@@ -234,7 +246,9 @@
"Write a description …" : "Γράψτε μια περιγραφή…",
"Choose attachment" : "Επιλογή συνημμένου",
"(group)" : "(ομάδα)",
"Todo items" : "Στοιχεία todo",
"{count} comments, {unread} unread" : "{count} σχόλια, {unread} μη αναγνωσμένα",
"Edit card title" : "Επεξεργασία τίτλου κάρτας",
"Assign to me" : "Ανάθεση σε εμένα",
"Unassign myself" : "Αποδέσμευσή μου",
"Move card" : "Μετακίνηση καρτέλας",
@@ -249,6 +263,7 @@
"All boards" : "Όλοι οι πίνακες",
"Archived boards" : "Αρχειοθέτηση πινάκων ",
"Shared with you" : "Διαμοιρασμένα μαζί σας",
"Deck settings" : "Ρυθμίσεις Deck",
"Use bigger card view" : "Χρησιμοποιήστε μεγαλύτερη προβολή καρτέλας",
"Show boards in calendar/tasks" : "Εμφάνιση πινάκων στο ημερολόγιο / εργασίες",
"Limit deck usage of groups" : "Περιορίστε τη χρήση της εφαρμογής deck σε ομάδες",
@@ -258,6 +273,7 @@
"Clone board" : "Κλώνος πίνακα",
"Unarchive board" : "Κατάργηση αρχειοθέτησης πίνακα",
"Archive board" : "Αρχειοθέτηση πίνακα",
"Export board" : "Εξαγωγή πίνακα",
"Turn on due date reminders" : "Ενεργοποιήστε τις υπενθυμίσεις ημερομηνίας προθεσμίας",
"Turn off due date reminders" : "Απενεργοποιήστε τις υπενθυμίσεις ημερομηνίας προθεσμίας",
"Due date reminders" : "Υπενθυμίσεις ημερομηνίας προθεσμίας",
@@ -269,14 +285,21 @@
"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 including archived cards." : "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον πίνακα {title}; Αυτό θα διαγράψει όλα τα δεδομένα του πίνακα συμπεριλαμβανομένων και των αρχειοθετημένων καρτών.",
"Delete the board?" : "Διαγραφή του πίνακα;",
"Loading filtered view" : "Φόρτωση εμφάνισης με βάση το φίλτρο",
"No due" : "Χωρίς λήξη",
"Search for {searchQuery} in all boards" : "Αναζήτηση για {searchQuery} σε όλους τους πίνακες",
"No results found" : "Δεν βρέθηκαν αποτελέσματα",
"Deck board {name}\n* Last modified on {lastMod}" : "Πίνακας Deck {name}\n* Τελευταία τροποποίηση στις {lastMod}",
"{stack} in {board}" : "{stack} στο {board}",
"Click to expand description" : "Κλικ για επέκταση περιγραφής",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Δημιουργήθηκε στις {created}\n* Τροποποιήθηκε στις {lastMod}\n* {nbAttachments} συνημμένα\n* {nbComments} σχόλια",
"{nbCards} cards" : "{nbCards} κάρτες",
"Click to expand comment" : "Κλικ για επέκταση σχολίου",
"No upcoming cards" : "Δεν υπάρχουν επερχόμενες καρτέλες",
"upcoming cards" : "επερχόμενες καρτέλες",
"Due on {date}" : "Προθεσμία στις {date}",
"Link to a board" : "Σύνδεσμος στον πίνακα",
"Link to a card" : "Σύνδεσμος σε καρτέλα",
"Create a card" : "Δημιουργία καρτέλας",
@@ -289,6 +312,8 @@
"Share {file} with a Deck card" : "Μοιραστείτε το {file} με μια καρτέλα Deck",
"Share" : "Μοιραστείτε",
"Are you sure you want to transfer the board {title} for {user}?" : "Είστε σίγουροι ότι θέλετε να μεταφέρετε τον πίνακα {title} για {user}? ",
"Transfer the board for {user} successfully" : "Επιτυχής μεταφορά του πίνακα για τον χρήστη {user}",
"Failed to transfer the board for {user}" : "Απέτυχε η μεταφορά του πίνακα για τον χρήστη {user}",
"Add a new list" : "Προσθήκη νέας λίστας",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Είστε βέβαιοι ότι θέλετε να διαγράψετε τον πίνακα {title}; Θα διαγραφούν όλα τα δεδομένα."
},"pluralForm" :"nplurals=2; plural=(n != 1);"

View File

@@ -81,6 +81,7 @@ OC.L10N.register(
"Deck board" : "Tableau",
"Owned by %1$s" : "Assignée à %1$s",
"Deck boards, cards and comments" : "Tableaux, cartes et commentaires",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "De %1$s, dans %2$s / %3$s, appartenant à %4$s",
"Card comments" : "Commentaires de la carte",
"%s on %s" : "%s sur %s",
"Deck boards and cards" : "Tableaux et cartes",

View File

@@ -79,6 +79,7 @@
"Deck board" : "Tableau",
"Owned by %1$s" : "Assignée à %1$s",
"Deck boards, cards and comments" : "Tableaux, cartes et commentaires",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "De %1$s, dans %2$s / %3$s, appartenant à %4$s",
"Card comments" : "Commentaires de la carte",
"%s on %s" : "%s sur %s",
"Deck boards and cards" : "Tableaux et cartes",

View File

@@ -3,6 +3,14 @@ OC.L10N.register(
{
"Personal" : "Անձնական",
"Done" : "Done",
"The file was uploaded" : "Նիշքը վերբերռնված է",
"The uploaded file exceeds the upload_max_filesize directive in php.ini" : "Վերբեռնած նիշքը գերազանցում է upload_max_filesize սահմանված php.ini֊ում",
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "Վերբեռնած նիշքը գերազանցում է MAX_FILE_SIZE, որը սահմանված է HTML ձևաթղթում",
"The file was only partially uploaded" : "Նիշքի մի մասն է վերբեռնված",
"No file was uploaded" : "Ոչ մի նիշք չի վերնբեռնվել",
"Missing a temporary folder" : "Բացակայում է ժամանակավոր պանակը",
"Could not write file to disk" : "Չհաջողվեց գրառել նիշքը սկավառակի վրա",
"A PHP extension stopped the file upload" : "PHP֊ի ընդլայնումն կանգնեցրեց նիշքի վերբեռնումը",
"Cancel" : "ընդհատել",
"Close" : "Փակել",
"Details" : "Մանրամասներ",

View File

@@ -1,6 +1,14 @@
{ "translations": {
"Personal" : "Անձնական",
"Done" : "Done",
"The file was uploaded" : "Նիշքը վերբերռնված է",
"The uploaded file exceeds the upload_max_filesize directive in php.ini" : "Վերբեռնած նիշքը գերազանցում է upload_max_filesize սահմանված php.ini֊ում",
"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form" : "Վերբեռնած նիշքը գերազանցում է MAX_FILE_SIZE, որը սահմանված է HTML ձևաթղթում",
"The file was only partially uploaded" : "Նիշքի մի մասն է վերբեռնված",
"No file was uploaded" : "Ոչ մի նիշք չի վերնբեռնվել",
"Missing a temporary folder" : "Բացակայում է ժամանակավոր պանակը",
"Could not write file to disk" : "Չհաջողվեց գրառել նիշքը սկավառակի վրա",
"A PHP extension stopped the file upload" : "PHP֊ի ընդլայնումն կանգնեցրեց նիշքի վերբեռնումը",
"Cancel" : "ընդհատել",
"Close" : "Փակել",
"Details" : "Մանրամասներ",

View File

@@ -67,6 +67,7 @@ OC.L10N.register(
"Deck" : "Longgok",
"Changes in the <strong>Deck app</strong>" : "Perubahan pada <strong>aplikasi Longgok</strong>",
"A <strong>comment</strong> was created on a card" : "<strong>Komentar</strong> telah dibuat pada suatu kartu",
"Upcoming cards" : "Kartu berikut",
"Personal" : "Personal",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "Kartu \"%s\" pada \"%s\" telah ditugaskan kepada Anda oleh %s.",
"The card \"%s\" on \"%s\" has reached its due date." : "Kartu \"%s\" pada \"%s\" telah melampaui tenggat.",
@@ -93,6 +94,7 @@ OC.L10N.register(
"Could not write file to disk" : "Tidak dapat menulis berkas ke diska",
"A PHP extension stopped the file upload" : "Ekstensi PHP menghentikan proses unggah berkas",
"No file uploaded or file size exceeds maximum of %s" : "Gagal unggah berkas atau ukuran melampaui batas maksimum %s",
"Card not found" : "Kartu tidak ditemukan",
"Invalid date, date format must be YYYY-MM-DD" : "Tanggal salah, format tanggal harus TTTT-BB-HH",
"Personal planning and team project organization" : "Perencanaan pribadi dan pengelolaan proyek tim",
"Add board" : "Tambah papan",
@@ -115,10 +117,12 @@ OC.L10N.register(
"Drop your files to upload" : "Lepas berkas Anda untuk mengunggah",
"Add card" : "Tambah kartu",
"Archived cards" : "Arsip kartu",
"Add list" : "Tambahkan daftar",
"List name" : "Nama daftar",
"Apply filter" : "Terapkan filter",
"Filter by tag" : "Filter dengan tag",
"Filter by assigned user" : "Filter dengan pengguna",
"Unassigned" : "Belum ditugaskan",
"Filter by due date" : "Filter dengan tenggat",
"Overdue" : "Lewat tenggat",
"Next 24 hours" : "Jangka 24 jam",
@@ -213,6 +217,9 @@ OC.L10N.register(
"Board {0} deleted" : "{0} papan terhapus",
"An error occurred" : "Terjadi kesalahan",
"Delete the board?" : "Hapus papan?",
"Click to expand comment" : "Klik untuk membuka komentar",
"No upcoming cards" : "Tidak ada kartu berikut",
"upcoming cards" : "kartu berikut",
"Link to a board" : "Tautan ke papan",
"Link to a card" : "Tautan ke kartu",
"Something went wrong" : "Ada yang salah",

View File

@@ -65,6 +65,7 @@
"Deck" : "Longgok",
"Changes in the <strong>Deck app</strong>" : "Perubahan pada <strong>aplikasi Longgok</strong>",
"A <strong>comment</strong> was created on a card" : "<strong>Komentar</strong> telah dibuat pada suatu kartu",
"Upcoming cards" : "Kartu berikut",
"Personal" : "Personal",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "Kartu \"%s\" pada \"%s\" telah ditugaskan kepada Anda oleh %s.",
"The card \"%s\" on \"%s\" has reached its due date." : "Kartu \"%s\" pada \"%s\" telah melampaui tenggat.",
@@ -91,6 +92,7 @@
"Could not write file to disk" : "Tidak dapat menulis berkas ke diska",
"A PHP extension stopped the file upload" : "Ekstensi PHP menghentikan proses unggah berkas",
"No file uploaded or file size exceeds maximum of %s" : "Gagal unggah berkas atau ukuran melampaui batas maksimum %s",
"Card not found" : "Kartu tidak ditemukan",
"Invalid date, date format must be YYYY-MM-DD" : "Tanggal salah, format tanggal harus TTTT-BB-HH",
"Personal planning and team project organization" : "Perencanaan pribadi dan pengelolaan proyek tim",
"Add board" : "Tambah papan",
@@ -113,10 +115,12 @@
"Drop your files to upload" : "Lepas berkas Anda untuk mengunggah",
"Add card" : "Tambah kartu",
"Archived cards" : "Arsip kartu",
"Add list" : "Tambahkan daftar",
"List name" : "Nama daftar",
"Apply filter" : "Terapkan filter",
"Filter by tag" : "Filter dengan tag",
"Filter by assigned user" : "Filter dengan pengguna",
"Unassigned" : "Belum ditugaskan",
"Filter by due date" : "Filter dengan tenggat",
"Overdue" : "Lewat tenggat",
"Next 24 hours" : "Jangka 24 jam",
@@ -211,6 +215,9 @@
"Board {0} deleted" : "{0} papan terhapus",
"An error occurred" : "Terjadi kesalahan",
"Delete the board?" : "Hapus papan?",
"Click to expand comment" : "Klik untuk membuka komentar",
"No upcoming cards" : "Tidak ada kartu berikut",
"upcoming cards" : "kartu berikut",
"Link to a board" : "Tautan ke papan",
"Link to a card" : "Tautan ke kartu",
"Something went wrong" : "Ada yang salah",

View File

@@ -30,6 +30,8 @@ OC.L10N.register(
"Created" : "Үүсгэсэн",
"Today" : "өнөөдөр",
"Tomorrow" : "маргааш",
"Next week" : "Дараа 7 хоног",
"Next month" : "Дараа сар",
"Save" : "Хадгалах",
"Reply" : "хариулт",
"Update" : "Шинэчлэх",

View File

@@ -28,6 +28,8 @@
"Created" : "Үүсгэсэн",
"Today" : "өнөөдөр",
"Tomorrow" : "маргааш",
"Next week" : "Дараа 7 хоног",
"Next month" : "Дараа сар",
"Save" : "Хадгалах",
"Reply" : "хариулт",
"Update" : "Шинэчлэх",

View File

@@ -78,9 +78,13 @@ OC.L10N.register(
"{user} has mentioned you in a comment on {deck-card}." : "{user} har nevnt deg i en kommentar på {deck-card}.",
"The board \"%s\" has been shared with you by %s." : "Brettet \"%s\" har blitt delt med deg av %s.",
"{user} has shared {deck-board} with you." : "{user} har delt brettet {deck-board} med deg.",
"Deck board" : "Deck tavle",
"Deck board" : "Stokktavle",
"Owned by %1$s" : "Eid av %1$s",
"Deck boards, cards and comments" : "Stokktavler, kort og kommentarer",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "Fra %1$s, under %2$s/%3$s, eid av %4$s",
"Card comments" : "Kommentarer på kortet",
"%s on %s" : "%s på %s",
"Deck boards and cards" : "Stokktavler og kort",
"No data was provided to create an attachment." : "Ingen data for å opprette vedlegg.",
"Finished" : "Fullført",
"To review" : "Til gjennomlesning",
@@ -153,6 +157,7 @@ OC.L10N.register(
"Toggle compact mode" : "Endre kompakt modus",
"Open details" : "Åpne detaljer",
"Details" : "Detaljer",
"Currently present people" : "Tilstedeværende personer for øyeblikket",
"Loading board" : "Laster tavle",
"No lists available" : "Ingen stabler tilgjengelig",
"Create a new list to add cards to this board" : "Lag en ny stabel for å legge til kort til denne tavlen",
@@ -181,9 +186,12 @@ OC.L10N.register(
"Transfer" : "Overfør",
"The board has been transferred to {user}" : "Tavlen har blitt overført til {user}",
"Failed to transfer the board to {user}" : "Klarte ikke overføre tavlen til {user}",
"Edit list title" : "Rediger listetittel",
"Archive all cards" : "Arkiver alle kort",
"Unarchive all cards" : "Fjern alle kort fra arkiv",
"Delete list" : "Slett listen",
"Archive all cards in this list" : "Arkiver alle kort i en stabel",
"Unarchive all cards in this list" : "Fjern alle kortene i denne listen fra arkiv",
"Add a new card" : "Legg til nytt kort",
"Card name" : "Navn på kort",
"List deleted" : "Stabel slettet",
@@ -260,6 +268,7 @@ OC.L10N.register(
"Shared with you" : "Delt med deg",
"Deck settings" : "Innstillinger for Stokk",
"Use bigger card view" : "Bruk større visning på kort",
"Show card ID badge" : "Vis ID-merke til kort",
"Show boards in calendar/tasks" : "Vis tavler i kalender/oppgaver",
"Limit deck usage of groups" : "Begrens stokk-bruk til grupper",
"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." : "Begrensning av tavler vil hindre tilgang til de brukere som ikke er medlem av en gruppe fra å lage egne tavler. Bruker kan arbeide på de tavler som er delt med dem.",
@@ -268,6 +277,7 @@ OC.L10N.register(
"Clone board" : "Klon tavle",
"Unarchive board" : "Aktiver tavle",
"Archive board" : "Arkiver tavle",
"Export board" : "Eksporter tavle",
"Turn on due date reminders" : "Skru på påminnelser for forfallsdato",
"Turn off due date reminders" : "Skru av påminnelser for forfallsdato",
"Due date reminders" : "Påminnelser for forfallsdato",
@@ -278,17 +288,19 @@ OC.L10N.register(
"Board {0} deleted" : "Tavle {0} slettet",
"Only assigned cards" : "Kun tildelte kort",
"No reminder" : "Ingen varsel",
"An error occurred" : "En feil oppstod",
"An error occurred" : "En feil oppsto",
"Are you sure you want to delete the board {title}? This will delete all the data of this board including archived cards." : "Are du sikker på sletting av tavlen {title}? Handlingen vil slette all data i denne tavlen, inkludert arkiverte kort.",
"Delete the board?" : "Slett tavlen?",
"Loading filtered view" : "Laster filtrert visning",
"No due" : "Ingen forfall",
"Search for {searchQuery} in all boards" : "Søk etter {searchQuery} i alle tavler",
"No results found" : "Ingen resultater funnet",
"Deck board {name}\n* Last modified on {lastMod}" : "Stokktavle {name}\n* Sist endret {lastMod}",
"{stack} in {board}" : "{stack} i {board}",
"Click to expand description" : "Klikk for å utvide beskrivelsen",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Opprettet {created}\n* Sist endret {lastMod}\n* {nbAttachments} vedlegg\n* {nbComments} kommentarer",
"{nbCards} cards" : "{nbCards} kort",
"Click to expand comment" : "Klikk for å utvide kommentaren",
"No upcoming cards" : "Ingen kommende kort",
"upcoming cards" : "kommende kort",
"Due on {date}" : "Utløper {date}",

View File

@@ -76,9 +76,13 @@
"{user} has mentioned you in a comment on {deck-card}." : "{user} har nevnt deg i en kommentar på {deck-card}.",
"The board \"%s\" has been shared with you by %s." : "Brettet \"%s\" har blitt delt med deg av %s.",
"{user} has shared {deck-board} with you." : "{user} har delt brettet {deck-board} med deg.",
"Deck board" : "Deck tavle",
"Deck board" : "Stokktavle",
"Owned by %1$s" : "Eid av %1$s",
"Deck boards, cards and comments" : "Stokktavler, kort og kommentarer",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "Fra %1$s, under %2$s/%3$s, eid av %4$s",
"Card comments" : "Kommentarer på kortet",
"%s on %s" : "%s på %s",
"Deck boards and cards" : "Stokktavler og kort",
"No data was provided to create an attachment." : "Ingen data for å opprette vedlegg.",
"Finished" : "Fullført",
"To review" : "Til gjennomlesning",
@@ -151,6 +155,7 @@
"Toggle compact mode" : "Endre kompakt modus",
"Open details" : "Åpne detaljer",
"Details" : "Detaljer",
"Currently present people" : "Tilstedeværende personer for øyeblikket",
"Loading board" : "Laster tavle",
"No lists available" : "Ingen stabler tilgjengelig",
"Create a new list to add cards to this board" : "Lag en ny stabel for å legge til kort til denne tavlen",
@@ -179,9 +184,12 @@
"Transfer" : "Overfør",
"The board has been transferred to {user}" : "Tavlen har blitt overført til {user}",
"Failed to transfer the board to {user}" : "Klarte ikke overføre tavlen til {user}",
"Edit list title" : "Rediger listetittel",
"Archive all cards" : "Arkiver alle kort",
"Unarchive all cards" : "Fjern alle kort fra arkiv",
"Delete list" : "Slett listen",
"Archive all cards in this list" : "Arkiver alle kort i en stabel",
"Unarchive all cards in this list" : "Fjern alle kortene i denne listen fra arkiv",
"Add a new card" : "Legg til nytt kort",
"Card name" : "Navn på kort",
"List deleted" : "Stabel slettet",
@@ -258,6 +266,7 @@
"Shared with you" : "Delt med deg",
"Deck settings" : "Innstillinger for Stokk",
"Use bigger card view" : "Bruk større visning på kort",
"Show card ID badge" : "Vis ID-merke til kort",
"Show boards in calendar/tasks" : "Vis tavler i kalender/oppgaver",
"Limit deck usage of groups" : "Begrens stokk-bruk til grupper",
"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." : "Begrensning av tavler vil hindre tilgang til de brukere som ikke er medlem av en gruppe fra å lage egne tavler. Bruker kan arbeide på de tavler som er delt med dem.",
@@ -266,6 +275,7 @@
"Clone board" : "Klon tavle",
"Unarchive board" : "Aktiver tavle",
"Archive board" : "Arkiver tavle",
"Export board" : "Eksporter tavle",
"Turn on due date reminders" : "Skru på påminnelser for forfallsdato",
"Turn off due date reminders" : "Skru av påminnelser for forfallsdato",
"Due date reminders" : "Påminnelser for forfallsdato",
@@ -276,17 +286,19 @@
"Board {0} deleted" : "Tavle {0} slettet",
"Only assigned cards" : "Kun tildelte kort",
"No reminder" : "Ingen varsel",
"An error occurred" : "En feil oppstod",
"An error occurred" : "En feil oppsto",
"Are you sure you want to delete the board {title}? This will delete all the data of this board including archived cards." : "Are du sikker på sletting av tavlen {title}? Handlingen vil slette all data i denne tavlen, inkludert arkiverte kort.",
"Delete the board?" : "Slett tavlen?",
"Loading filtered view" : "Laster filtrert visning",
"No due" : "Ingen forfall",
"Search for {searchQuery} in all boards" : "Søk etter {searchQuery} i alle tavler",
"No results found" : "Ingen resultater funnet",
"Deck board {name}\n* Last modified on {lastMod}" : "Stokktavle {name}\n* Sist endret {lastMod}",
"{stack} in {board}" : "{stack} i {board}",
"Click to expand description" : "Klikk for å utvide beskrivelsen",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Opprettet {created}\n* Sist endret {lastMod}\n* {nbAttachments} vedlegg\n* {nbComments} kommentarer",
"{nbCards} cards" : "{nbCards} kort",
"Click to expand comment" : "Klikk for å utvide kommentaren",
"No upcoming cards" : "Ingen kommende kort",
"upcoming cards" : "kommende kort",
"Due on {date}" : "Utløper {date}",

View File

@@ -79,8 +79,11 @@ OC.L10N.register(
"The board \"%s\" has been shared with you by %s." : "Вам предоставлен доступ к рабочей доске «%s» пользователем %s.",
"{user} has shared {deck-board} with you." : "{user} предоставил(а) вам доступ к {deck-board}.",
"Deck board" : "Доска",
"Owned by %1$s" : "Владелец: %1$s",
"Deck boards, cards and comments" : "Доски, карточки и комментарии",
"Card comments" : "Комментарии карточки",
"%s on %s" : "%s на %s",
"Deck boards and cards" : "Доски и карточки",
"No data was provided to create an attachment." : "Отсутствуют данные для создания вложения.",
"Finished" : "Завершено",
"To review" : "Проверить",
@@ -181,6 +184,7 @@ OC.L10N.register(
"Transfer" : "Передача",
"The board has been transferred to {user}" : "Доска была передана пользователю {user}",
"Failed to transfer the board to {user}" : "Не удалось передать доску пользователю {user}",
"Edit list title" : "Изменить название списка",
"Archive all cards" : "Переместить все карточки в архив",
"Unarchive all cards" : "Восстановить все карточки из архива",
"Delete list" : "Удалить список",
@@ -271,6 +275,7 @@ OC.L10N.register(
"Clone board" : "Скопировать доску",
"Unarchive board" : "Восстановить доску из архива",
"Archive board" : "Переместить доску в архив",
"Export board" : "Экспортировать доску",
"Turn on due date reminders" : "Включить напоминания о сроке выполнения",
"Turn off due date reminders" : "Отключить напоминания о сроке выполнения",
"Due date reminders" : "Напоминания о сроке выполнения",
@@ -288,11 +293,14 @@ OC.L10N.register(
"No due" : "Без назначенной даты",
"Search for {searchQuery} in all boards" : "Искать {searchQuery} на всех досках",
"No results found" : "Результаты отсутствуют",
"Deck board {name}\n* Last modified on {lastMod}" : "Доска «{name}»\n* Последнее изменение: {lastMod}",
"{stack} in {board}" : "«{stack}» с доски «{board}»",
"Click to expand description" : "Нажмите, чтобы развернуть поле описания",
"{nbCards} cards" : "карточек: {nbCards}",
"Click to expand comment" : "Нажмите, чтобы развернуть комментарии",
"No upcoming cards" : "Отсутствуют карточки, ожидающие выполнения",
"upcoming cards" : "карточки, ожидающие выполнения",
"Due on {date}" : "Дата исполнения: {date}",
"Link to a board" : "Ссылка на доску",
"Link to a card" : "Ссылка на карточку",
"Create a card" : "Создать карточку",

View File

@@ -77,8 +77,11 @@
"The board \"%s\" has been shared with you by %s." : "Вам предоставлен доступ к рабочей доске «%s» пользователем %s.",
"{user} has shared {deck-board} with you." : "{user} предоставил(а) вам доступ к {deck-board}.",
"Deck board" : "Доска",
"Owned by %1$s" : "Владелец: %1$s",
"Deck boards, cards and comments" : "Доски, карточки и комментарии",
"Card comments" : "Комментарии карточки",
"%s on %s" : "%s на %s",
"Deck boards and cards" : "Доски и карточки",
"No data was provided to create an attachment." : "Отсутствуют данные для создания вложения.",
"Finished" : "Завершено",
"To review" : "Проверить",
@@ -179,6 +182,7 @@
"Transfer" : "Передача",
"The board has been transferred to {user}" : "Доска была передана пользователю {user}",
"Failed to transfer the board to {user}" : "Не удалось передать доску пользователю {user}",
"Edit list title" : "Изменить название списка",
"Archive all cards" : "Переместить все карточки в архив",
"Unarchive all cards" : "Восстановить все карточки из архива",
"Delete list" : "Удалить список",
@@ -269,6 +273,7 @@
"Clone board" : "Скопировать доску",
"Unarchive board" : "Восстановить доску из архива",
"Archive board" : "Переместить доску в архив",
"Export board" : "Экспортировать доску",
"Turn on due date reminders" : "Включить напоминания о сроке выполнения",
"Turn off due date reminders" : "Отключить напоминания о сроке выполнения",
"Due date reminders" : "Напоминания о сроке выполнения",
@@ -286,11 +291,14 @@
"No due" : "Без назначенной даты",
"Search for {searchQuery} in all boards" : "Искать {searchQuery} на всех досках",
"No results found" : "Результаты отсутствуют",
"Deck board {name}\n* Last modified on {lastMod}" : "Доска «{name}»\n* Последнее изменение: {lastMod}",
"{stack} in {board}" : "«{stack}» с доски «{board}»",
"Click to expand description" : "Нажмите, чтобы развернуть поле описания",
"{nbCards} cards" : "карточек: {nbCards}",
"Click to expand comment" : "Нажмите, чтобы развернуть комментарии",
"No upcoming cards" : "Отсутствуют карточки, ожидающие выполнения",
"upcoming cards" : "карточки, ожидающие выполнения",
"Due on {date}" : "Дата исполнения: {date}",
"Link to a board" : "Ссылка на доску",
"Link to a card" : "Ссылка на карточку",
"Create a card" : "Создать карточку",

View File

@@ -71,11 +71,20 @@ OC.L10N.register(
"Load more" : "Учитај још",
"Personal" : "Лично",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "Корисник %s Вам је доделио картицу „%s“ са табле „%s“.",
"{user} has assigned the card {deck-card} on {deck-board} to you." : "{user} вам је доделио картицу {deck-card} на {deck-board}.",
"The card \"%s\" on \"%s\" has reached its due date." : "Картици „%s“ на табли „%s“ је истекао рок.",
"The card {deck-card} on {deck-board} has reached its due date." : "Картица {deck-card} на {deck-board} је дошла достигла датум када треба да се реши.",
"%s has mentioned you in a comment on \"%s\"." : "%s Вас је поменуо у коментару на „%s“.",
"{user} has mentioned you in a comment on {deck-card}." : "{user} вас је поменуо у коментару на {deck-card}.",
"The board \"%s\" has been shared with you by %s." : "Корисник „%s“ је поделио са Вама таблу „%s“.",
"{user} has shared {deck-board} with you." : "{user} је са вама поделио {deck-board}.",
"Deck board" : "Табла Шпила",
"Owned by %1$s" : "Власник је %1$s",
"Deck boards, cards and comments" : "Табле шпилова, картице и коментари",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "Од %1$s, у %2$s/%3$s, власник је %4$s",
"Card comments" : "Коментари картица",
"%s on %s" : "%s на %s",
"Deck boards and cards" : "Табле шпилова и картице",
"No data was provided to create an attachment." : "Нису дати подаци за креирање прилога.",
"Finished" : "Завршено",
"To review" : "Треба прегледати",
@@ -97,16 +106,24 @@ OC.L10N.register(
"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",
"This comment has more than %s characters.\nAdded as an attachment to the card with name %s.\nAccessible on URL: %s." : "Овај коментар има више од %s карактера.\nДодат је као прилог картици под именом %s.\nДоступно је на URL адреси: %s.",
"Card not found" : "Картица није нађена",
"Path is already shared with this card" : "Путања се већ дели са овом картицом",
"Invalid date, date format must be 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" : "Шпил је организациони алат у канбан стилу који је намењен личном планирању и организацији пројекта у тимовима интегрисаним са Nextcloud.\n\n\n- 📥 Додајте своје задатке у картице и поређајте их по редоследу\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 \"{card}\" was added to \"{board}\"" : "Картица „{card}” је додта на „{board}",
"Open card" : "Отвори картицу",
"Close" : "Затвори",
"Create card" : "Направите картицу",
"Select a card" : "Изаберите картицу",
@@ -123,6 +140,7 @@ OC.L10N.register(
"Archived cards" : "Архивиране картице",
"Add list" : "Додај списак",
"List name" : "Назив листе",
"Active filters" : "Активни филтери",
"Apply filter" : "Примени филтер",
"Filter by tag" : "Филтрирај по ознаци",
"Filter by assigned user" : "Филтрирај по додељеном кориснику",
@@ -139,6 +157,7 @@ OC.L10N.register(
"Toggle compact mode" : "Укључи/искључи компактни режим",
"Open details" : "Отвори детаље",
"Details" : "Детаљи",
"Currently present people" : "Тренутно присутне особе",
"Loading board" : "Учитавам таблу",
"No lists available" : "Нема доступних спискова",
"Create a new list to add cards to this board" : "Направите нови списак да додате картице на ову таблу",
@@ -151,6 +170,7 @@ OC.L10N.register(
"Undo" : "Опозови",
"Deleted cards" : "Обрисане картице",
"Share board with a user, group or circle …" : "Подели таблу са корисником, групом или кругом…",
"Searching for users, groups and circles …" : "Претрага корисника, група и кругова ...",
"No participants found" : "Нема нађених учесника",
"Board owner" : "Власник табле",
"(Group)" : "(група)",
@@ -161,10 +181,17 @@ OC.L10N.register(
"Owner" : "Власник",
"Delete" : "Избриши",
"Failed to create share with {displayName}" : "Грешка у прављењу дељења са {displayName}",
"Are you sure you want to transfer the board {title} to {user}?" : "Да ли сте сигурни да кориснику {user} пренесете таблу {title}?",
"Transfer the board." : "Пренос табле.",
"Transfer" : "Пренеси",
"The board has been transferred to {user}" : "Табла је пренета кориснику {user}",
"Failed to transfer the board to {user}" : "Није успео пренос табле кориснику {user}",
"Edit list title" : "Уреди наслов листе",
"Archive all cards" : "Архивирај све картице",
"Unarchive all cards" : "Врати све картице из архиве",
"Delete list" : "Обриши списак",
"Archive all cards in this list" : "Архивирај све картице са овог списка",
"Unarchive all cards in this list" : "Враћа из архиве све картице у овој листи",
"Add a new card" : "Додај нову картицу",
"Card name" : "Име картице",
"List deleted" : "Листа обрисана",
@@ -173,8 +200,13 @@ OC.L10N.register(
"title and color value must be provided" : "морају се дати вредности за наслов и боју",
"Board name" : "Име табле",
"Members" : "Чланови",
"Upload new files" : "Отпреми нове фајлове",
"Share from Files" : "Подели из Фајлова",
"Pending share" : "Дељење на чекању",
"Add this attachment" : "Додај овај прилог",
"Show in Files" : "Прикажи у Фајловима",
"Download" : "Преузми",
"Remove attachment" : "Уклони прилог",
"Delete Attachment" : "Обриши прилог",
"Restore Attachment" : "Поврати прилог",
"File to share" : "Фајл за дељење",
@@ -187,6 +219,7 @@ OC.L10N.register(
"Created" : "Направљен",
"The title cannot be empty." : "Наслов не може бити празан.",
"No comments yet. Begin the discussion!" : "Нема још коментара. Започните дискусију!",
"Failed to load comments" : "Није успело учитавање коментара",
"Assign a tag to this card…" : "Додели ознаку овој картици…",
"Assign to users" : "Додели корисницима",
"Assign to users/groups/circles" : "Додели корисницима/групама/круговима",
@@ -197,10 +230,13 @@ OC.L10N.register(
"Select Date" : "Одаберите датум",
"Today" : "Данас",
"Tomorrow" : "сутра",
"Next week" : "Наредне недеље",
"Next month" : "Наредног месеца",
"Save" : "Сачувај",
"The comment cannot be empty." : "Коментар не може да буде празан.",
"The comment cannot be longer than 1000 characters." : "Коментар не може да има преко 1000 карактера.",
"In reply to" : "Као одговор на",
"Cancel reply" : "Откажи одговор",
"Reply" : "Одговори",
"Update" : "Ажурирај",
"Description" : "Опис",
@@ -210,8 +246,12 @@ OC.L10N.register(
"Edit description" : "Измени опис",
"View description" : "Погледај опис",
"Add Attachment" : "Додај прилог",
"Write a description …" : "Напишите опис ...",
"Choose attachment" : "Одабери прилог",
"(group)" : "(група)",
"Todo items" : "Ставке обавеза",
"{count} comments, {unread} unread" : "{count} коментара, {unread} није прочитано",
"Edit card title" : "Уреди наслов картице",
"Assign to me" : "Додели мени",
"Unassign myself" : "Склони са мене",
"Move card" : "Премести картицу",
@@ -226,6 +266,9 @@ OC.L10N.register(
"All boards" : "Све табле",
"Archived boards" : "Архивиране табле",
"Shared with you" : "Дељено са Вама",
"Deck settings" : "Поставке Шпила",
"Use bigger card view" : "Користи већи приказ картице",
"Show card ID badge" : "Прикажи беџ ID картице",
"Show boards in calendar/tasks" : "Прикажи табле у календару/задацима",
"Limit deck usage of groups" : "Ограничи употребу шпила на групе",
"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 апликације ће блокирати кориснике који нису део одабраних група да креирају своје табле. Корисници ће и даље моћи да раде на таблама које су подељене са њима.",
@@ -234,22 +277,47 @@ OC.L10N.register(
"Clone board" : "Клонирај таблу",
"Unarchive board" : "Врати таблу из архиве",
"Archive board" : "Архивирај таблу",
"Export board" : "Извези таблу",
"Turn on due date reminders" : "Укључи подсетнике о року",
"Turn off due date reminders" : "Искључи подсетнике о року",
"Due date reminders" : "Подсетници о року",
"All cards" : "Све картице",
"Assigned cards" : "Додељене картице",
"No notifications" : "Нема обавештења",
"Delete board" : "Избриши таблу",
"Board {0} deleted" : "Табла {0} обрисана",
"Only assigned cards" : "Само додељене картице",
"No reminder" : "Нема подсетника",
"An error occurred" : "Догодила се грешка",
"Are you sure you want to delete the board {title}? This will delete all the data of this board including archived cards." : "Да ли сте сигурни да желите да обришете таблу {title}? Ово ће да обрише све податке на овој табли, укључијући и архивиране картице.",
"Delete the board?" : "Обрисати таблу?",
"Loading filtered view" : "Учитам филтрирани преглед",
"No due" : "Нема рокова",
"Search for {searchQuery} in all boards" : "Тражи се {searchQuery} у свим таблама",
"No results found" : "Нема пронађених резултата",
"Deck board {name}\n* Last modified on {lastMod}" : "Шпил табла {name}\n* Последњи пут измењена дана {lastMod}",
"{stack} in {board}" : "{stack} у {board}",
"Click to expand description" : "Кликните да проширите опис",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Креирано дана {created}\n* Последњи пут измењено дана {lastMod}\n* {nbAttachments} прилога\n* {nbComments} коментара",
"{nbCards} cards" : "{nbCards} картица",
"Click to expand comment" : "Кликните да проширите коментар",
"No upcoming cards" : "Нема предстојећих картица",
"upcoming cards" : "предстојеће картице",
"Due on {date}" : "Рок је {date}",
"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}",
"Error creating the share" : "Грешка при прављењу дељења",
"Share with a Deck card" : "Дели са Шпил картицом",
"Share {file} with a Deck card" : "Дели {file} са Шпил картицом",
"Share" : "Подели",
"Are you sure you want to transfer the board {title} for {user}?" : "Да ли сте сигурни да желите пренети таблу {title} за {user}?",
"Transfer the board for {user} successfully" : "Пренос табле за {user} је успео",
"Failed to transfer the board for {user}" : "Пренос табле за {user} није успео",
"Add a new list" : "Додај нови списак",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Да ли стварно желите да обришете таблу {title}? Овим ћете обрисати све податке са табле."
},

View File

@@ -69,11 +69,20 @@
"Load more" : "Учитај још",
"Personal" : "Лично",
"The card \"%s\" on \"%s\" has been assigned to you by %s." : "Корисник %s Вам је доделио картицу „%s“ са табле „%s“.",
"{user} has assigned the card {deck-card} on {deck-board} to you." : "{user} вам је доделио картицу {deck-card} на {deck-board}.",
"The card \"%s\" on \"%s\" has reached its due date." : "Картици „%s“ на табли „%s“ је истекао рок.",
"The card {deck-card} on {deck-board} has reached its due date." : "Картица {deck-card} на {deck-board} је дошла достигла датум када треба да се реши.",
"%s has mentioned you in a comment on \"%s\"." : "%s Вас је поменуо у коментару на „%s“.",
"{user} has mentioned you in a comment on {deck-card}." : "{user} вас је поменуо у коментару на {deck-card}.",
"The board \"%s\" has been shared with you by %s." : "Корисник „%s“ је поделио са Вама таблу „%s“.",
"{user} has shared {deck-board} with you." : "{user} је са вама поделио {deck-board}.",
"Deck board" : "Табла Шпила",
"Owned by %1$s" : "Власник је %1$s",
"Deck boards, cards and comments" : "Табле шпилова, картице и коментари",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "Од %1$s, у %2$s/%3$s, власник је %4$s",
"Card comments" : "Коментари картица",
"%s on %s" : "%s на %s",
"Deck boards and cards" : "Табле шпилова и картице",
"No data was provided to create an attachment." : "Нису дати подаци за креирање прилога.",
"Finished" : "Завршено",
"To review" : "Треба прегледати",
@@ -95,16 +104,24 @@
"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",
"This comment has more than %s characters.\nAdded as an attachment to the card with name %s.\nAccessible on URL: %s." : "Овај коментар има више од %s карактера.\nДодат је као прилог картици под именом %s.\nДоступно је на URL адреси: %s.",
"Card not found" : "Картица није нађена",
"Path is already shared with this card" : "Путања се већ дели са овом картицом",
"Invalid date, date format must be 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" : "Шпил је организациони алат у канбан стилу који је намењен личном планирању и организацији пројекта у тимовима интегрисаним са Nextcloud.\n\n\n- 📥 Додајте своје задатке у картице и поређајте их по редоследу\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 \"{card}\" was added to \"{board}\"" : "Картица „{card}” је додта на „{board}",
"Open card" : "Отвори картицу",
"Close" : "Затвори",
"Create card" : "Направите картицу",
"Select a card" : "Изаберите картицу",
@@ -121,6 +138,7 @@
"Archived cards" : "Архивиране картице",
"Add list" : "Додај списак",
"List name" : "Назив листе",
"Active filters" : "Активни филтери",
"Apply filter" : "Примени филтер",
"Filter by tag" : "Филтрирај по ознаци",
"Filter by assigned user" : "Филтрирај по додељеном кориснику",
@@ -137,6 +155,7 @@
"Toggle compact mode" : "Укључи/искључи компактни режим",
"Open details" : "Отвори детаље",
"Details" : "Детаљи",
"Currently present people" : "Тренутно присутне особе",
"Loading board" : "Учитавам таблу",
"No lists available" : "Нема доступних спискова",
"Create a new list to add cards to this board" : "Направите нови списак да додате картице на ову таблу",
@@ -149,6 +168,7 @@
"Undo" : "Опозови",
"Deleted cards" : "Обрисане картице",
"Share board with a user, group or circle …" : "Подели таблу са корисником, групом или кругом…",
"Searching for users, groups and circles …" : "Претрага корисника, група и кругова ...",
"No participants found" : "Нема нађених учесника",
"Board owner" : "Власник табле",
"(Group)" : "(група)",
@@ -159,10 +179,17 @@
"Owner" : "Власник",
"Delete" : "Избриши",
"Failed to create share with {displayName}" : "Грешка у прављењу дељења са {displayName}",
"Are you sure you want to transfer the board {title} to {user}?" : "Да ли сте сигурни да кориснику {user} пренесете таблу {title}?",
"Transfer the board." : "Пренос табле.",
"Transfer" : "Пренеси",
"The board has been transferred to {user}" : "Табла је пренета кориснику {user}",
"Failed to transfer the board to {user}" : "Није успео пренос табле кориснику {user}",
"Edit list title" : "Уреди наслов листе",
"Archive all cards" : "Архивирај све картице",
"Unarchive all cards" : "Врати све картице из архиве",
"Delete list" : "Обриши списак",
"Archive all cards in this list" : "Архивирај све картице са овог списка",
"Unarchive all cards in this list" : "Враћа из архиве све картице у овој листи",
"Add a new card" : "Додај нову картицу",
"Card name" : "Име картице",
"List deleted" : "Листа обрисана",
@@ -171,8 +198,13 @@
"title and color value must be provided" : "морају се дати вредности за наслов и боју",
"Board name" : "Име табле",
"Members" : "Чланови",
"Upload new files" : "Отпреми нове фајлове",
"Share from Files" : "Подели из Фајлова",
"Pending share" : "Дељење на чекању",
"Add this attachment" : "Додај овај прилог",
"Show in Files" : "Прикажи у Фајловима",
"Download" : "Преузми",
"Remove attachment" : "Уклони прилог",
"Delete Attachment" : "Обриши прилог",
"Restore Attachment" : "Поврати прилог",
"File to share" : "Фајл за дељење",
@@ -185,6 +217,7 @@
"Created" : "Направљен",
"The title cannot be empty." : "Наслов не може бити празан.",
"No comments yet. Begin the discussion!" : "Нема још коментара. Започните дискусију!",
"Failed to load comments" : "Није успело учитавање коментара",
"Assign a tag to this card…" : "Додели ознаку овој картици…",
"Assign to users" : "Додели корисницима",
"Assign to users/groups/circles" : "Додели корисницима/групама/круговима",
@@ -195,10 +228,13 @@
"Select Date" : "Одаберите датум",
"Today" : "Данас",
"Tomorrow" : "сутра",
"Next week" : "Наредне недеље",
"Next month" : "Наредног месеца",
"Save" : "Сачувај",
"The comment cannot be empty." : "Коментар не може да буде празан.",
"The comment cannot be longer than 1000 characters." : "Коментар не може да има преко 1000 карактера.",
"In reply to" : "Као одговор на",
"Cancel reply" : "Откажи одговор",
"Reply" : "Одговори",
"Update" : "Ажурирај",
"Description" : "Опис",
@@ -208,8 +244,12 @@
"Edit description" : "Измени опис",
"View description" : "Погледај опис",
"Add Attachment" : "Додај прилог",
"Write a description …" : "Напишите опис ...",
"Choose attachment" : "Одабери прилог",
"(group)" : "(група)",
"Todo items" : "Ставке обавеза",
"{count} comments, {unread} unread" : "{count} коментара, {unread} није прочитано",
"Edit card title" : "Уреди наслов картице",
"Assign to me" : "Додели мени",
"Unassign myself" : "Склони са мене",
"Move card" : "Премести картицу",
@@ -224,6 +264,9 @@
"All boards" : "Све табле",
"Archived boards" : "Архивиране табле",
"Shared with you" : "Дељено са Вама",
"Deck settings" : "Поставке Шпила",
"Use bigger card view" : "Користи већи приказ картице",
"Show card ID badge" : "Прикажи беџ ID картице",
"Show boards in calendar/tasks" : "Прикажи табле у календару/задацима",
"Limit deck usage of groups" : "Ограничи употребу шпила на групе",
"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 апликације ће блокирати кориснике који нису део одабраних група да креирају своје табле. Корисници ће и даље моћи да раде на таблама које су подељене са њима.",
@@ -232,22 +275,47 @@
"Clone board" : "Клонирај таблу",
"Unarchive board" : "Врати таблу из архиве",
"Archive board" : "Архивирај таблу",
"Export board" : "Извези таблу",
"Turn on due date reminders" : "Укључи подсетнике о року",
"Turn off due date reminders" : "Искључи подсетнике о року",
"Due date reminders" : "Подсетници о року",
"All cards" : "Све картице",
"Assigned cards" : "Додељене картице",
"No notifications" : "Нема обавештења",
"Delete board" : "Избриши таблу",
"Board {0} deleted" : "Табла {0} обрисана",
"Only assigned cards" : "Само додељене картице",
"No reminder" : "Нема подсетника",
"An error occurred" : "Догодила се грешка",
"Are you sure you want to delete the board {title}? This will delete all the data of this board including archived cards." : "Да ли сте сигурни да желите да обришете таблу {title}? Ово ће да обрише све податке на овој табли, укључијући и архивиране картице.",
"Delete the board?" : "Обрисати таблу?",
"Loading filtered view" : "Учитам филтрирани преглед",
"No due" : "Нема рокова",
"Search for {searchQuery} in all boards" : "Тражи се {searchQuery} у свим таблама",
"No results found" : "Нема пронађених резултата",
"Deck board {name}\n* Last modified on {lastMod}" : "Шпил табла {name}\n* Последњи пут измењена дана {lastMod}",
"{stack} in {board}" : "{stack} у {board}",
"Click to expand description" : "Кликните да проширите опис",
"* Created on {created}\n* Last modified on {lastMod}\n* {nbAttachments} attachments\n* {nbComments} comments" : "* Креирано дана {created}\n* Последњи пут измењено дана {lastMod}\n* {nbAttachments} прилога\n* {nbComments} коментара",
"{nbCards} cards" : "{nbCards} картица",
"Click to expand comment" : "Кликните да проширите коментар",
"No upcoming cards" : "Нема предстојећих картица",
"upcoming cards" : "предстојеће картице",
"Due on {date}" : "Рок је {date}",
"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}",
"Error creating the share" : "Грешка при прављењу дељења",
"Share with a Deck card" : "Дели са Шпил картицом",
"Share {file} with a Deck card" : "Дели {file} са Шпил картицом",
"Share" : "Подели",
"Are you sure you want to transfer the board {title} for {user}?" : "Да ли сте сигурни да желите пренети таблу {title} за {user}?",
"Transfer the board for {user} successfully" : "Пренос табле за {user} је успео",
"Failed to transfer the board for {user}" : "Пренос табле за {user} није успео",
"Add a new list" : "Додај нови списак",
"Are you sure you want to delete the board {title}? This will delete all the data of this board." : "Да ли стварно желите да обришете таблу {title}? Овим ћете обрисати све податке са табле."
},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"

View File

@@ -185,6 +185,7 @@ OC.L10N.register(
"An error occurred" : "Виникла помилка",
"Are you sure you want to delete the board {title}? This will delete all the data of this board including archived cards." : "Ви впевнені, що хочете вилучити дошку {title}? Це призведе до видалення всіх даних цієї дошки, включаючи архівні картки.",
"Delete the board?" : "Вилучити дошку?",
"Due on {date}" : "До {date}",
"Link to a board" : "Прив'язати до дошки",
"Link to a card" : "Прив'язати до картки",
"Message from {author} in {conversationName}" : "Повідомлення від {author} у {conversationName}",

View File

@@ -183,6 +183,7 @@
"An error occurred" : "Виникла помилка",
"Are you sure you want to delete the board {title}? This will delete all the data of this board including archived cards." : "Ви впевнені, що хочете вилучити дошку {title}? Це призведе до видалення всіх даних цієї дошки, включаючи архівні картки.",
"Delete the board?" : "Вилучити дошку?",
"Due on {date}" : "До {date}",
"Link to a board" : "Прив'язати до дошки",
"Link to a card" : "Прив'язати до картки",
"Message from {author} in {conversationName}" : "Повідомлення від {author} у {conversationName}",

View File

@@ -78,7 +78,7 @@ OC.L10N.register(
"{user} has mentioned you in a comment on {deck-card}." : "{user} 在 {deck-card} 的留言中提及您。",
"The board \"%s\" has been shared with you by %s." : "佈告欄「%s」已由 %s 分享給您。",
"{user} has shared {deck-board} with you." : "{user} 已與您分享 {deck-board}。",
"Deck board" : "看板佈告欄",
"Deck board" : "Deck 佈告欄",
"Owned by %1$s" : "由 %1$s 擁有",
"Deck boards, cards and comments" : "Deck 看板、卡片與評論",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "來自 %1$s在 %2$s/%3$s由 %4$s 擁有",

View File

@@ -76,7 +76,7 @@
"{user} has mentioned you in a comment on {deck-card}." : "{user} 在 {deck-card} 的留言中提及您。",
"The board \"%s\" has been shared with you by %s." : "佈告欄「%s」已由 %s 分享給您。",
"{user} has shared {deck-board} with you." : "{user} 已與您分享 {deck-board}。",
"Deck board" : "看板佈告欄",
"Deck board" : "Deck 佈告欄",
"Owned by %1$s" : "由 %1$s 擁有",
"Deck boards, cards and comments" : "Deck 看板、卡片與評論",
"From %1$s, in %2$s/%3$s, owned by %4$s" : "來自 %1$s在 %2$s/%3$s由 %4$s 擁有",

View File

@@ -36,6 +36,7 @@ use OCA\Deck\Db\CardMapper;
use OCA\Deck\Event\AclCreatedEvent;
use OCA\Deck\Event\AclDeletedEvent;
use OCA\Deck\Event\AclUpdatedEvent;
use OCA\Deck\Event\BoardUpdatedEvent;
use OCA\Deck\Event\CardCreatedEvent;
use OCA\Deck\Event\CardDeletedEvent;
use OCA\Deck\Event\CardUpdatedEvent;
@@ -154,6 +155,13 @@ class Application extends App implements IBootstrap {
// Event listening for realtime updates via notify_push
$context->registerEventListener(SessionCreatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(SessionClosedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(BoardUpdatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(CardCreatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(CardUpdatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(CardDeletedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(AclCreatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(AclUpdatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(AclDeletedEvent::class, LiveUpdateListener::class);
$context->registerNotifierService(Notifier::class);
$context->registerEventListener(LoadAdditionalScriptsEvent::class, ResourceAdditionalScriptsListener::class);

View File

@@ -60,12 +60,14 @@ class BoardApiController extends ApiController {
* @NoCSRFRequired
*
* Return all of the boards that the current user has access to.
*
* @param bool $details
* @throws StatusException
*/
public function index($details = null) {
public function index(bool $details = false) {
$modified = $this->request->getHeader('If-Modified-Since');
if ($modified === null || $modified === '') {
$boards = $this->boardService->findAll(0, $details);
$boards = $this->boardService->findAll(0, $details === true);
} else {
$date = Util::parseHTTPDate($modified);
if (!$date) {

View File

@@ -59,7 +59,7 @@ class DeckCalendarBackend {
}
public function getBoards(): array {
return $this->boardService->findAll(-1, null, false);
return $this->boardService->findAll(-1, false, false);
}
public function getBoard(int $id): Board {

View File

@@ -117,7 +117,7 @@ class DeckWidget implements IAPIWidget, IButtonWidget, IIconWidget {
$nowTimestamp = (new Datetime())->getTimestamp();
$sinceTimestamp = $since !== null ? (new Datetime($since))->getTimestamp() : null;
$upcomingCards = array_filter($upcomingCards, static function (array $card) use ($nowTimestamp, $sinceTimestamp) {
if ($card['duedate']) {
if (isset($card['duedate'])) {
$ts = (new Datetime($card['duedate']))->getTimestamp();
return $ts > $nowTimestamp && ($sinceTimestamp === null || $ts > $sinceTimestamp);
}

View File

@@ -52,6 +52,20 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function findIn(array $boardIds, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage')
->from('deck_board_acl')
->where($qb->expr()->in('board_id', $qb->createParameter('boardIds')))
->setMaxResults($limit)
->setFirstResult($offset);
return iterator_to_array($this->chunkQuery($boardIds, function (array $ids) use ($qb) {
$qb->setParameter('boardIds', $ids, IQueryBuilder::PARAM_INT_ARRAY);
return $this->findEntities($qb);
}));
}
/**
* @param numeric $userId
* @param numeric $id

View File

@@ -28,15 +28,13 @@ namespace OCA\Deck\Db;
use OCA\Deck\NotFoundException;
use OCA\Deck\Service\CirclesService;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use PDO;
/** @template-extends QBMapper<Assignment> */
class AssignmentMapper extends QBMapper implements IPermissionMapper {
/** @template-extends DeckMapper<Assignment> */
class AssignmentMapper extends DeckMapper implements IPermissionMapper {
/** @var CardMapper */
private $cardMapper;
@@ -60,7 +58,7 @@ class AssignmentMapper extends QBMapper implements IPermissionMapper {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_assigned_users')
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, PDO::PARAM_INT)));
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
$users = $this->findEntities($qb);
foreach ($users as $user) {
$this->mapParticipant($user);
@@ -68,12 +66,29 @@ class AssignmentMapper extends QBMapper implements IPermissionMapper {
return $users;
}
public function findIn(array $cardIds): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_assigned_users')
->where($qb->expr()->in('card_id', $qb->createParameter('cardIds')));
$users = iterator_to_array($this->chunkQuery($cardIds, function (array $ids) use ($qb) {
$qb->setParameter('cardIds', $ids, IQueryBuilder::PARAM_INT_ARRAY);
return $this->findEntities($qb);
}));
foreach ($users as $user) {
$this->mapParticipant($user);
}
return $users;
}
public function findByParticipant(string $participant, $type = Assignment::TYPE_USER): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_assigned_users')
->where($qb->expr()->eq('participant', $qb->createNamedParameter($participant, PDO::PARAM_STR)))
->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, PDO::PARAM_INT)));
->where($qb->expr()->eq('participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
return $this->findEntities($qb);
}
@@ -132,8 +147,8 @@ class AssignmentMapper extends QBMapper implements IPermissionMapper {
private function getOrigin(Assignment $assignment) {
if ($assignment->getType() === Assignment::TYPE_USER) {
$origin = $this->userManager->get($assignment->getParticipant());
return $origin ? new User($origin) : null;
$origin = $this->userManager->userExists($assignment->getParticipant());
return $origin ? new User($assignment->getParticipant(), $this->userManager) : null;
}
if ($assignment->getType() === Assignment::TYPE_GROUP) {
$origin = $this->groupManager->get($assignment->getParticipant());

View File

@@ -90,9 +90,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
$this->boardCache[$id] = $this->findEntity($qb);
}
// FIXME is this necessary? it was NOT done with the old mapper
// $this->mapOwner($board);
// Add labels
if ($withLabels && $this->boardCache[$id]->getLabels() === null) {
$labels = $this->labelMapper->findAll($id);
@@ -159,7 +156,21 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived, $before, $term);
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived, $before, $term);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived, $before, $term);
$allBoards = array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
$allBoards = array_values(array_unique(array_merge($userBoards, $groupBoards, $circleBoards)));
// Could be moved outside
$acls = $this->aclMapper->findIn(array_map(function ($board) {
return $board->getId();
}, $allBoards));
/* @var Board $entry */
foreach ($allBoards as $entry) {
$boardAcls = array_values(array_filter($acls, function ($acl) use ($entry) {
return $acl->getBoardId() === $entry->getId();
}));
$entry->setAcl($boardAcls);
}
foreach ($allBoards as $board) {
$this->boardCache[$board->getId()] = $board;
}
@@ -259,11 +270,7 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
$entry->setShared(1);
}
$entries = array_merge($entries, $sharedEntries);
/* @var Board $entry */
foreach ($entries as $entry) {
$acl = $this->aclMapper->findAll($entry->id);
$entry->setAcl($acl);
}
return $entries;
}
@@ -336,11 +343,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
foreach ($entries as $entry) {
$entry->setShared(2);
}
/* @var Board $entry */
foreach ($entries as $entry) {
$acl = $this->aclMapper->findAll($entry->id);
$entry->setAcl($acl);
}
return $entries;
}
@@ -397,11 +399,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
foreach ($entries as $entry) {
$entry->setShared(2);
}
/* @var Board $entry */
foreach ($entries as $entry) {
$acl = $this->aclMapper->findAll($entry->id);
$entry->setAcl($acl);
}
return $entries;
}
@@ -455,13 +452,11 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
}
public function mapAcl(Acl &$acl) {
$userManager = $this->userManager;
$groupManager = $this->groupManager;
$acl->resolveRelation('participant', function ($participant) use (&$acl, &$userManager, &$groupManager) {
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
$user = $userManager->get($participant);
if ($user !== null) {
return new User($user);
if ($this->userManager->userExists($acl->getParticipant())) {
return new User($acl->getParticipant(), $this->userManager);
}
$this->logger->debug('User ' . $acl->getId() . ' not found when mapping acl ' . $acl->getParticipant());
return null;
@@ -499,9 +494,8 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
public function mapOwner(Board &$board) {
$userManager = $this->userManager;
$board->resolveRelation('owner', function ($owner) use (&$userManager) {
$user = $userManager->get($owner);
if ($user !== null) {
return new User($user);
if ($this->userManager->userExists($owner)) {
return new User($owner, $userManager);
}
return null;
});

View File

@@ -159,16 +159,17 @@ class Card extends RelationalEntity {
}
public function getDaysUntilDue(): ?int {
$today = new DateTime();
$match_date = $this->getDuedate();
if ($match_date === null) {
if ($this->getDuedate() === null) {
return null;
}
$today = new DateTime();
$today->setTime(0, 0);
$match_date->setTime(0, 0);
$diff = $today->diff($match_date);
$matchDate = DateTime::createFromInterface($this->getDuedate());
$matchDate->setTime(0, 0);
$diff = $today->diff($matchDate);
return (int) $diff->format('%R%a'); // Extract days count in interval
}

View File

@@ -254,13 +254,13 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function findAllWithDue($boardId) {
public function findAllWithDue(array $boardIds) {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
->from('deck_cards', 'c')
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id')
->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->where($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->isNotNull('c.duedate'))
->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
@@ -270,14 +270,14 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function findToMeOrNotAssignedCards($boardId, $username) {
public function findToMeOrNotAssignedCards(array $boardIds, string $username) {
$qb = $this->db->getQueryBuilder();
$qb->select('c.*')
->from('deck_cards', 'c')
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id')
->leftJoin('c', 'deck_assigned_users', 'u', 'c.id = u.card_id')
->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->where($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->orX(
$qb->expr()->eq('u.participant', $qb->createNamedParameter($username, IQueryBuilder::PARAM_STR)),
$qb->expr()->isNull('u.participant'))
@@ -607,9 +607,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function mapOwner(Card &$card) {
$userManager = $this->userManager;
$card->resolveRelation('owner', function ($owner) use (&$userManager) {
$user = $userManager->get($owner);
if ($user !== null) {
return new User($user);
if ($userManager->userExists($owner)) {
return new User($owner, $this->userManager);
}
return null;
});

View File

@@ -23,6 +23,7 @@
namespace OCA\Deck\Db;
use Generator;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -35,7 +36,7 @@ abstract class DeckMapper extends QBMapper {
/**
* @param $id
* @return \OCP\AppFramework\Db\Entity if not found
* @return T
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws \OCP\AppFramework\Db\DoesNotExistException
*/
@@ -47,4 +48,21 @@ abstract class DeckMapper extends QBMapper {
return $this->findEntity($qb);
}
/**
* Helper function to split passed array into chunks of 1000 elements and
* call a given callback for fetching query results
*
* Can be useful to limit to 1000 results per query for oracle compatiblity
* but still iterate over all results
*/
public function chunkQuery(array $ids, callable $callback): Generator {
$limit = 1000;
while (!empty($ids)) {
$slice = array_splice($ids, 0, $limit);
foreach ($callback($slice) as $item) {
yield $item;
}
}
}
}

View File

@@ -79,6 +79,19 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function findAssignedLabelsForCards($cardIds, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('l.*', 'card_id')
->from($this->getTableName(), 'l')
->innerJoin('l', 'deck_assigned_labels', 'al', 'l.id = al.label_id')
->where($qb->expr()->in('card_id', $qb->createNamedParameter($cardIds, IQueryBuilder::PARAM_INT_ARRAY)))
->orderBy('l.id')
->setMaxResults($limit)
->setFirstResult($offset);
return $this->findEntities($qb);
}
/**
* @param numeric $boardId
* @param int|null $limit

View File

@@ -33,7 +33,7 @@ class RelationalObject implements JsonSerializable {
* RelationalObject constructor.
*
* @param $primaryKey string
* @param $object
* @param callable|mixed $object
*/
public function __construct($primaryKey, $object) {
$this->primaryKey = $primaryKey;
@@ -47,16 +47,24 @@ class RelationalObject implements JsonSerializable {
);
}
public function getObject() {
if (is_callable($this->object)) {
$this->object = call_user_func($this->object, $this);
}
return $this->object;
}
/**
* This method should be overwritten if object doesn't implement \JsonSerializable
*
* @throws \Exception
*/
public function getObjectSerialization() {
if ($this->object instanceof JsonSerializable) {
return $this->object->jsonSerialize();
if ($this->getObject() instanceof JsonSerializable) {
return $this->getObject()->jsonSerialize();
} else {
throw new \Exception('jsonSerialize is not implemented on ' . get_class($this));
throw new \Exception('jsonSerialize is not implemented on ' . get_class($this->getObject()));
}
}

View File

@@ -26,16 +26,27 @@ namespace OCA\Deck\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Cache\CappedMemoryCache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\ICache;
use OCP\ICacheFactory;
/** @template-extends DeckMapper<Stack> */
class StackMapper extends DeckMapper implements IPermissionMapper {
private $cardMapper;
private CappedMemoryCache $stackCache;
private CardMapper $cardMapper;
private ICache $cache;
public function __construct(IDBConnection $db, CardMapper $cardMapper) {
public function __construct(
IDBConnection $db,
CardMapper $cardMapper,
ICacheFactory $cacheFactory
) {
parent::__construct($db, 'deck_stacks', Stack::class);
$this->cardMapper = $cardMapper;
$this->stackCache = new CappedMemoryCache();
$this->cache = $cacheFactory->createDistributed('deck-stackMapper');
}
@@ -47,12 +58,17 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
* @throws \OCP\DB\Exception
*/
public function find($id): Stack {
if (isset($this->stackCache[(string)$id])) {
return $this->stackCache[(string)$id];
}
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
return $this->findEntity($qb);
$this->stackCache[(string)$id] = $this->findEntity($qb);
return $this->stackCache[(string)$id];
}
/**
@@ -113,9 +129,16 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function update(Entity $entity): Entity {
$result = parent::update($entity);
$this->stackCache[(string)$entity->getId()] = $result;
return $result;
}
public function delete(Entity $entity): Entity {
// delete cards on stack
$this->cardMapper->deleteByStack($entity->getId());
unset($this->stackCache[(string)$entity->getId()]);
return parent::delete($entity);
}
@@ -142,12 +165,19 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
* @throws \OCP\DB\Exception
*/
public function findBoardId($id): ?int {
$result = $this->cache->get('findBoardId:' . $id);
if ($result !== null) {
return $result !== false ? $result : null;
}
try {
$entity = $this->find($id);
return $entity->getBoardId();
$result = $entity->getBoardId();
} catch (DoesNotExistException $e) {
$result = false;
} catch (MultipleObjectsReturnedException $e) {
}
return null;
$this->cache->set('findBoardId:' . $id, $result);
return $result !== false ? $result : null;
}
}

View File

@@ -23,27 +23,30 @@
namespace OCA\Deck\Db;
use OCP\IUser;
use OCP\IUserManager;
class User extends RelationalObject {
public function __construct(IUser $user) {
$primaryKey = $user->getUID();
parent::__construct($primaryKey, $user);
private IUserManager $userManager;
public function __construct($uid, IUserManager $userManager) {
$this->userManager = $userManager;
parent::__construct($uid, function ($object) {
return $this->userManager->get($object->getPrimaryKey());
});
}
public function getObjectSerialization() {
return [
'uid' => $this->object->getUID(),
'displayname' => $this->object->getDisplayName(),
'type' => 0
'uid' => $this->getObject()->getUID(),
'displayname' => $this->getObject()->getDisplayName(),
'type' => Acl::PERMISSION_TYPE_USER
];
}
public function getUID() {
return $this->object->getUID();
return $this->getPrimaryKey();
}
public function getDisplayName() {
return $this->object->getDisplayName();
return $this->userManager->getDisplayName($this->getPrimaryKey());
}
}

View File

@@ -31,7 +31,7 @@ use OCP\EventDispatcher\Event;
abstract class AAclEvent extends Event {
private $acl;
public function __construct(Acl $acl) {
parent::__construct();
@@ -41,4 +41,8 @@ abstract class AAclEvent extends Event {
public function getAcl(): Acl {
return $this->acl;
}
public function getBoardId(): int {
return $this->acl->getBoardId();
}
}

View File

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

View File

@@ -26,5 +26,17 @@ declare(strict_types=1);
namespace OCA\Deck\Event;
use OCA\Deck\Db\Card;
class CardUpdatedEvent extends ACardEvent {
private $cardBefore;
public function __construct(Card $card, Card $before = null) {
parent::__construct($card);
$this->cardBefore = $before;
}
public function getCardBefore() {
return $this->cardBefore;
}
}

View File

@@ -26,7 +26,12 @@ declare(strict_types=1);
namespace OCA\Deck\Listeners;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\NotifyPushEvents;
use OCA\Deck\Event\AAclEvent;
use OCA\Deck\Event\ACardEvent;
use OCA\Deck\Event\BoardUpdatedEvent;
use OCA\Deck\Event\CardUpdatedEvent;
use OCA\Deck\Event\SessionClosedEvent;
use OCA\Deck\Event\SessionCreatedEvent;
use OCA\Deck\Service\SessionService;
@@ -37,18 +42,20 @@ use OCP\IRequest;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
/** @template-implements IEventListener<Event|SessionCreatedEvent|SessionClosedEvent> */
/** @template-implements IEventListener<Event|SessionCreatedEvent|SessionClosedEvent|AAclEvent|ACardEvent|CardUpdatedEvent|BoardUpdatedEvent> */
class LiveUpdateListener implements IEventListener {
private LoggerInterface $logger;
private SessionService $sessionService;
private IRequest $request;
private StackMapper $stackMapper;
private $queue;
public function __construct(
ContainerInterface $container,
IRequest $request,
LoggerInterface $logger,
SessionService $sessionService
SessionService $sessionService,
StackMapper $stackMapper
) {
try {
$this->queue = $container->get(IQueue::class);
@@ -59,6 +66,7 @@ class LiveUpdateListener implements IEventListener {
$this->logger = $logger;
$this->sessionService = $sessionService;
$this->request = $request;
$this->stackMapper = $stackMapper;
}
public function handle(Event $event): void {
@@ -68,17 +76,37 @@ class LiveUpdateListener implements IEventListener {
}
try {
// the web frontend is adding the Session-ID as a header on every request
// the web frontend is adding the Session-ID as a header
// TODO: verify the token! this currently allows to spoof a token from someone
// else, preventing this person from getting any live updates
// else, preventing this person from getting updates
$causingSessionToken = $this->request->getHeader('x-nc-deck-session');
if (
$event instanceof SessionCreatedEvent ||
$event instanceof SessionClosedEvent
$event instanceof SessionClosedEvent ||
$event instanceof BoardUpdatedEvent ||
$event instanceof AAclEvent
) {
$this->sessionService->notifyAllSessions($this->queue, $event->getBoardId(), NotifyPushEvents::DeckBoardUpdate, [
'id' => $event->getBoardId()
], $causingSessionToken);
} elseif ($event instanceof ACardEvent) {
$boardId = $this->stackMapper->findBoardId($event->getCard()->getStackId());
$this->sessionService->notifyAllSessions($this->queue, $boardId, NotifyPushEvents::DeckCardUpdate, [
'boardId' => $boardId,
'cardId' => $event->getCard()->getId()
], $causingSessionToken);
// if card got moved to a diferent board, we should notify
// also sessions active on the previous board
if ($event instanceof CardUpdatedEvent && $event->getCardBefore()) {
$previousBoardId = $this->stackMapper->findBoardId($event->getCardBefore()->getStackId());
if ($boardId !== $previousBoardId) {
$this->sessionService->notifyAllSessions($this->queue, $previousBoardId, NotifyPushEvents::DeckCardUpdate, [
'boardId' => $boardId,
'cardId' => $event->getCard()->getId()
], $causingSessionToken);
}
}
}
} catch (\Exception $e) {
$this->logger->error('Error when handling live update event', ['exception' => $e]);

View File

@@ -26,4 +26,5 @@ namespace OCA\Deck;
class NotifyPushEvents {
public const DeckBoardUpdate = 'deck_board_update';
public const DeckCardUpdate = 'deck_card_update';
}

View File

@@ -23,6 +23,7 @@
namespace OCA\Deck\Reference;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\BoardService;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\IReferenceProvider;
@@ -67,7 +68,12 @@ class BoardReferenceProvider implements IReferenceProvider {
if ($this->matchReference($referenceText)) {
$boardId = $this->getBoardId($referenceText);
if ($boardId !== null) {
$board = $this->boardService->find($boardId)->jsonSerialize();
try {
$board = $this->boardService->find($boardId)->jsonSerialize();
} catch (NoPermissionException $e) {
// Skip throwing if user has no permissions
return null;
}
$board = $this->sanitizeSerializedBoard($board);
/** @var IReference $reference */
$reference = new Reference($referenceText);

View File

@@ -27,6 +27,7 @@ use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\Label;
use OCA\Deck\Model\CardDetails;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CardService;
use OCA\Deck\Service\StackService;
@@ -121,9 +122,15 @@ class CardReferenceProvider extends ADiscoverableReferenceProvider implements IS
$ids = $this->getBoardCardId($referenceText);
if ($ids !== null) {
[$boardId, $cardId] = $ids;
$card = $this->cardService->find((int) $cardId)->jsonSerialize();
$board = $this->boardService->find((int) $boardId)->jsonSerialize();
$stack = $this->stackService->find((int) $card['stackId'])->jsonSerialize();
try {
$card = $this->cardService->find((int) $cardId)->jsonSerialize();
$board = $this->boardService->find((int) $boardId)->jsonSerialize();
$stack = $this->stackService->find((int) $card['stackId'])->jsonSerialize();
} catch (NoPermissionException $e) {
// Skip throwing if user has no permissions
return null;
}
$card = $this->sanitizeSerializedCard($card);
$board = $this->sanitizeSerializedBoard($board);

View File

@@ -27,6 +27,7 @@ use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\Label;
use OCA\Deck\Model\CardDetails;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CardService;
use OCA\Deck\Service\CommentService;
@@ -85,9 +86,14 @@ class CommentReferenceProvider implements IReferenceProvider {
if ($ids !== null) {
[$boardId, $cardId, $commentId] = $ids;
$card = $this->cardService->find($cardId)->jsonSerialize();
$board = $this->boardService->find($boardId)->jsonSerialize();
$stack = $this->stackService->find((int) $card['stackId'])->jsonSerialize();
try {
$card = $this->cardService->find($cardId)->jsonSerialize();
$board = $this->boardService->find($boardId)->jsonSerialize();
$stack = $this->stackService->find((int) $card['stackId'])->jsonSerialize();
} catch (NoPermissionException $e) {
// Skip throwing if user has no permissions
return null;
}
$card = $this->sanitizeSerializedCard($card);
$board = $this->sanitizeSerializedBoard($board);
$stack = $this->sanitizeSerializedStack($stack);

View File

@@ -56,6 +56,7 @@ use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\LabelMapper;
use OCP\IUserManager;
use OCA\Deck\BadRequestException;
use OCA\Deck\Event\BoardUpdatedEvent;
use OCA\Deck\Validators\BoardServiceValidator;
use OCP\IURLGenerator;
use OCP\Server;
@@ -79,7 +80,8 @@ class BoardService {
private IEventDispatcher $eventDispatcher;
private ChangeHelper $changeHelper;
private CardMapper $cardMapper;
private ?array $boardsCache = null;
private ?array $boardsCacheFull = null;
private ?array $boardsCachePartial = null;
private IURLGenerator $urlGenerator;
private IDBConnection $connection;
private BoardServiceValidator $boardServiceValidator;
@@ -147,96 +149,45 @@ class BoardService {
}
/**
* @return array
* @return Board[]
*/
public function findAll($since = -1, $details = null, $includeArchived = true) {
if ($this->boardsCache) {
return $this->boardsCache;
public function findAll(int $since = -1, bool $fullDetails = false, bool $includeArchived = true): array {
if ($this->boardsCacheFull && $fullDetails) {
return $this->boardsCacheFull;
}
if ($this->boardsCachePartial && !$fullDetails) {
return $this->boardsCachePartial;
}
$complete = $this->getUserBoards($since, $includeArchived);
$result = [];
/** @var Board $item */
foreach ($complete as &$item) {
$this->boardMapper->mapOwner($item);
if ($item->getAcl() !== null) {
foreach ($item->getAcl() as &$acl) {
$this->boardMapper->mapAcl($acl);
}
}
if ($details !== null) {
$this->enrichWithStacks($item);
$this->enrichWithLabels($item);
$this->enrichWithUsers($item);
}
$permissions = $this->permissionService->matchPermissions($item);
$item->setPermissions([
'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ] ?? false,
'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT] ?? false,
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false,
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
$this->enrichWithBoardSettings($item);
$result[$item->getId()] = $item;
}
$this->boardsCache = $result;
return array_values($result);
return $this->enrichBoards($complete, $fullDetails);
}
/**
* @param $boardId
* @return Board
* @throws DoesNotExistException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function find($boardId) {
public function find(int $boardId, bool $fullDetails = true): Board {
$this->boardServiceValidator->check(compact('boardId'));
if ($this->boardsCache && isset($this->boardsCache[$boardId])) {
return $this->boardsCache[$boardId];
if (isset($this->boardsCacheFull[$boardId]) && $fullDetails) {
return $this->boardsCacheFull[$boardId];
}
if (is_numeric($boardId) === false) {
throw new BadRequestException('board id must be a number');
if (isset($this->boardsCachePartial[$boardId]) && !$fullDetails) {
return $this->boardsCachePartial[$boardId];
}
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
/** @var Board $board */
$board = $this->boardMapper->find($boardId, true, true);
$this->boardMapper->mapOwner($board);
if ($board->getAcl() !== null) {
foreach ($board->getAcl() as $acl) {
if ($acl !== null) {
$this->boardMapper->mapAcl($acl);
}
}
}
$permissions = $this->permissionService->matchPermissions($board);
$board->setPermissions([
'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ] ?? false,
'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT] ?? false,
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false,
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
$this->enrichWithUsers($board);
$this->enrichWithBoardSettings($board);
$this->enrichWithActiveSessions($board);
$this->boardsCache[$board->getId()] = $board;
[$board] = $this->enrichBoards([$board], $fullDetails);
return $board;
}
/**
* @return array
*/
private function getBoardPrerequisites() {
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($this->userId)
);
return [
'user' => $this->userId,
'groups' => $groups
];
}
/**
* @param $mapper
* @param $id
@@ -429,6 +380,7 @@ class BoardService {
$this->boardMapper->mapOwner($board);
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_BOARD, $changes, ActivityManager::SUBJECT_BOARD_UPDATE);
$this->changeHelper->boardChanged($board->getId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($board->getId()));
return $board;
}
@@ -456,7 +408,7 @@ class BoardService {
public function enrichWithActiveSessions(Board $board) {
$sessions = $this->sessionMapper->findAllActive($board->getId());
$board->setActiveSessions(array_values(
array_unique(
array_map(function (Session $session) {
@@ -691,6 +643,45 @@ class BoardService {
return $board;
}
/** @param Board[] $boards */
private function enrichBoards(array $boards, bool $fullDetails = true): array {
$result = [];
foreach ($boards as $board) {
// FIXME The enrichment in here could make use of combined queries
$this->boardMapper->mapOwner($board);
if ($board->getAcl() !== null) {
foreach ($board->getAcl() as &$acl) {
$this->boardMapper->mapAcl($acl);
}
}
$permissions = $this->permissionService->matchPermissions($board);
$board->setPermissions([
'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ] ?? false,
'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT] ?? false,
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false,
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false
]);
if ($fullDetails) {
$this->enrichWithStacks($board);
$this->enrichWithLabels($board);
$this->enrichWithUsers($board);
$this->enrichWithBoardSettings($board);
$this->enrichWithActiveSessions($board);
}
// Cache for further usage
if ($fullDetails) {
$this->boardsCacheFull[$board->getId()] = $board;
} else {
$this->boardsCachePartial[$board->getId()] = $board;
}
}
return $boards;
}
private function enrichWithStacks($board, $since = -1) {
$stacks = $this->stackMapper->findAll($board->getId(), null, null, $since);
@@ -713,7 +704,7 @@ class BoardService {
private function enrichWithUsers($board, $since = -1) {
$boardUsers = $this->permissionService->findUsers($board->getId());
if (\count($boardUsers) === 0) {
if ($boardUsers === null || \count($boardUsers) === 0) {
return;
}
$board->setUsers(array_values($boardUsers));
@@ -723,10 +714,6 @@ class BoardService {
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . '#' . $endpoint;
}
private function clearBoardsCache() {
$this->boardsCache = null;
}
/**
* Clean a given board data from the Cache
*/
@@ -735,7 +722,8 @@ class BoardService {
$boardOwnerId = $board->getOwner();
$this->boardMapper->flushCache($boardId, $boardOwnerId);
unset($this->boardsCache[$boardId]);
unset($this->boardsCacheFull[$boardId]);
unset($this->boardsCachePartial[$boardId]);
}
private function enrichWithCards($board) {

View File

@@ -28,11 +28,13 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\CardCreatedEvent;
use OCA\Deck\Event\CardDeletedEvent;
@@ -114,32 +116,52 @@ class CardService {
$this->cardServiceValidator = $cardServiceValidator;
}
public function enrich($card) {
$cardId = $card->getId();
$this->cardMapper->mapOwner($card);
$card->setAssignedUsers($this->assignedUsersMapper->findAll($cardId));
$card->setLabels($this->labelMapper->findAssignedLabelsForCard($cardId));
$card->setAttachmentCount($this->attachmentService->count($cardId));
public function enrichCards($cards) {
$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);
$cardIds = array_map(function (Card $card) use ($user) {
// Everything done in here might be heavy as it is executed for every card
$cardId = $card->getId();
$this->cardMapper->mapOwner($card);
$card->setAttachmentCount($this->attachmentService->count($cardId));
// TODO We should find a better way just to get the comment count so we can save 1-3 queries per card here
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
$lastRead = $countComments > 0 ? $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user) : null;
$countUnreadComments = $lastRead ? $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead) : 0;
$card->setCommentsUnread($countUnreadComments);
$card->setCommentsCount($countComments);
$stack = $this->stackMapper->find($card->getStackId());
$board = $this->boardService->find($stack->getBoardId(), false);
$card->setRelatedStack($stack);
$card->setRelatedBoard($board);
return $card->getId();
}, $cards);
$assignedLabels = $this->labelMapper->findAssignedLabelsForCards($cardIds);
$assignedUsers = $this->assignedUsersMapper->findIn($cardIds);
foreach ($cards as $card) {
$cardLabels = array_values(array_filter($assignedLabels, function (Label $label) use ($card) {
return $label->getCardId() === $card->getId();
}));
$cardAssignedUsers = array_values(array_filter($assignedUsers, function (Assignment $assignment) use ($card) {
return $assignment->getCardId() === $card->getId();
}));
$card->setLabels($cardLabels);
$card->setAssignedUsers($cardAssignedUsers);
}
return $cards;
}
public function fetchDeleted($boardId) {
$this->cardServiceValidator->check(compact('boardId'));
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
$cards = $this->cardMapper->findDeleted($boardId);
foreach ($cards as $card) {
$this->enrich($card);
}
$this->enrichCards($cards);
return $cards;
}
@@ -153,16 +175,17 @@ class CardService {
public function find(int $cardId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$card = $this->cardMapper->find($cardId);
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
[$card] = $this->enrichCards([$card]);
// Attachments are only enriched on individual card fetching
$attachments = $this->attachmentService->findAll($cardId, true);
if ($this->request->getParam('apiVersion') === '1.0') {
$attachments = array_filter($attachments, function ($attachment) {
return $attachment->getType() === 'deck_file';
});
}
$card->setAssignedUsers($assignedUsers);
$card->setAttachments($attachments);
$this->enrich($card);
return $card;
}
@@ -174,9 +197,7 @@ class CardService {
return [];
}
$cards = $this->cardMapper->findCalendarEntries($boardId);
foreach ($cards as $card) {
$this->enrich($card);
}
$this->enrichCards($cards);
return $cards;
}
@@ -332,7 +353,7 @@ class CardService {
}
$this->changeHelper->cardChanged($card->getId(), true);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore()));
return $card;
}
@@ -422,6 +443,8 @@ class CardService {
$result[$card->getOrder()] = $card;
}
$this->changeHelper->cardChanged($id, false);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return array_values($result);
}

View File

@@ -224,7 +224,7 @@ class ConfigService {
}
public function getAttachmentFolder(string $userId = null): string {
if ($this->getUserId() === null) {
if ($userId === null && $this->getUserId() === null) {
throw new NoPermissionException('Must be logged in get the attachment folder');
}

View File

@@ -62,9 +62,8 @@ class DefaultBoardService {
*/
public function checkFirstRun($userId): bool {
$firstRun = $this->config->getUserValue($userId, Application::APP_ID, 'firstRun', 'yes');
$userBoards = $this->boardMapper->findAllByUser($userId);
if ($firstRun === 'yes' && count($userBoards) === 0) {
if ($firstRun === 'yes') {
try {
$this->config->setUserValue($userId, Application::APP_ID, 'firstRun', 'no');
} catch (PreConditionNotMetException $e) {

View File

@@ -28,7 +28,7 @@ declare(strict_types=1);
namespace OCA\Deck\Service;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Model\CardDetails;
use OCP\Comments\ICommentsManager;
@@ -37,6 +37,7 @@ use OCA\Deck\Db\LabelMapper;
use OCP\IUserManager;
class OverviewService {
private CardService $cardService;
private BoardMapper $boardMapper;
private LabelMapper $labelMapper;
private CardMapper $cardMapper;
@@ -46,6 +47,7 @@ class OverviewService {
private AttachmentService $attachmentService;
public function __construct(
CardService $cardService,
BoardMapper $boardMapper,
LabelMapper $labelMapper,
CardMapper $cardMapper,
@@ -54,6 +56,7 @@ class OverviewService {
ICommentsManager $commentsManager,
AttachmentService $attachmentService
) {
$this->cardService = $cardService;
$this->boardMapper = $boardMapper;
$this->labelMapper = $labelMapper;
$this->cardMapper = $cardMapper;
@@ -63,66 +66,43 @@ class OverviewService {
$this->attachmentService = $attachmentService;
}
public function enrich(Card $card, string $userId): void {
$cardId = $card->getId();
$this->cardMapper->mapOwner($card);
$card->setAssignedUsers($this->assignedUsersMapper->findAll($cardId));
$card->setLabels($this->labelMapper->findAssignedLabelsForCard($cardId));
$card->setAttachmentCount($this->attachmentService->count($cardId));
$user = $this->userManager->get($userId);
if ($user !== null) {
$lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user);
$count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead);
$card->setCommentsUnread($count);
}
}
public function findAllWithDue(string $userId): array {
$userBoards = $this->boardMapper->findAllForUser($userId);
$allDueCards = [];
foreach ($userBoards as $userBoard) {
$allDueCards[] = array_map(function ($card) use ($userBoard, $userId) {
$this->enrich($card, $userId);
return (new CardDetails($card, $userBoard))->jsonSerialize();
}, $this->cardMapper->findAllWithDue($userBoard->getId()));
}
return array_merge(...$allDueCards);
}
public function findUpcomingCards(string $userId): array {
$userBoards = $this->boardMapper->findAllForUser($userId);
$boardOwnerIds = array_filter(array_map(function (Board $board) {
return count($board->getAcl()) === 0 ? $board->getId() : null;
}, $userBoards));
$boardSharedIds = array_filter(array_map(function (Board $board) {
return count($board->getAcl()) > 0 ? $board->getId() : null;
}, $userBoards));
$foundCards = array_merge(
// private board: get cards with due date
$this->cardMapper->findAllWithDue($boardOwnerIds),
// shared board: get all my assigned or unassigned cards
$this->cardMapper->findToMeOrNotAssignedCards($boardSharedIds, $userId)
);
$this->cardService->enrichCards($foundCards);
$overview = [];
foreach ($userBoards as $userBoard) {
if (count($userBoard->getAcl()) === 0) {
// private board: get cards with due date
$cards = $this->cardMapper->findAllWithDue($userBoard->getId());
} else {
// shared board: get all my assigned or unassigned cards
$cards = $this->cardMapper->findToMeOrNotAssignedCards($userBoard->getId(), $userId);
foreach ($foundCards as $card) {
$diffDays = $card->getDaysUntilDue();
$key = 'later';
if ($diffDays === null) {
$key = 'nodue';
} elseif ($diffDays < 0) {
$key = 'overdue';
} elseif ($diffDays === 0) {
$key = 'today';
} elseif ($diffDays === 1) {
$key = 'tomorrow';
} elseif ($diffDays <= 7) {
$key = 'nextSevenDays';
}
foreach ($cards as $card) {
$this->enrich($card, $userId);
$diffDays = $card->getDaysUntilDue();
$key = 'later';
if ($diffDays === null) {
$key = 'nodue';
} elseif ($diffDays < 0) {
$key = 'overdue';
} elseif ($diffDays === 0) {
$key = 'today';
} elseif ($diffDays === 1) {
$key = 'tomorrow';
} elseif ($diffDays <= 7) {
$key = 'nextSevenDays';
}
$card = (new CardDetails($card, $userBoard));
$overview[$key][] = $card->jsonSerialize();
}
$card = (new CardDetails($card, $card->getRelatedBoard()));
$overview[$key][] = $card->jsonSerialize();
}
return $overview;
}

View File

@@ -97,21 +97,26 @@ class PermissionService {
* @param $boardId
* @return bool|array
*/
public function getPermissions($boardId) {
if ($cached = $this->permissionCache->get($boardId)) {
public function getPermissions($boardId, ?string $userId = null) {
if ($userId === null) {
$userId = $this->userId;
}
$cacheKey = $boardId . '-' . $userId;
if ($cached = $this->permissionCache->get($cacheKey)) {
return $cached;
}
$owner = $this->userIsBoardOwner($boardId);
$owner = $this->userIsBoardOwner($boardId, $userId);
$acls = $this->aclMapper->findAll($boardId);
$permissions = [
Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ),
Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT),
Acl::PERMISSION_MANAGE => $owner || $this->userCan($acls, Acl::PERMISSION_MANAGE),
Acl::PERMISSION_SHARE => ($owner || $this->userCan($acls, Acl::PERMISSION_SHARE))
&& (!$this->shareManager->sharingDisabledForUser($this->userId))
Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ, $userId),
Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT, $userId),
Acl::PERMISSION_MANAGE => $owner || $this->userCan($acls, Acl::PERMISSION_MANAGE, $userId),
Acl::PERMISSION_SHARE => ($owner || $this->userCan($acls, Acl::PERMISSION_SHARE, $userId))
&& (!$this->shareManager->sharingDisabledForUser($userId))
];
$this->permissionCache->set($boardId, $permissions);
$this->permissionCache->set($cacheKey, $permissions);
return $permissions;
}
@@ -143,7 +148,7 @@ class PermissionService {
* @return bool
* @throws NoPermissionException
*/
public function checkPermission($mapper, $id, $permission, $userId = null) {
public function checkPermission($mapper, $id, $permission, $userId = null): bool {
$boardId = $id;
if ($mapper instanceof IPermissionMapper && !($mapper instanceof BoardMapper)) {
$boardId = $mapper->findBoardId($id);
@@ -153,23 +158,11 @@ class PermissionService {
throw new NoPermissionException('Permission denied');
}
if ($permission === Acl::PERMISSION_SHARE && $this->shareManager->sharingDisabledForUser($this->userId)) {
throw new NoPermissionException('Permission denied');
}
if ($this->userIsBoardOwner($boardId, $userId)) {
$permissions = $this->getPermissions($boardId, $userId);
if ($permissions[$permission] === true) {
return true;
}
try {
$acls = $this->getBoard($boardId)->getAcl() ?? [];
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;
}
} catch (DoesNotExistException | MultipleObjectsReturnedException $e) {
}
// Throw NoPermission to not leak information about existing entries
throw new NoPermissionException('Permission denied');
}
@@ -260,22 +253,20 @@ class PermissionService {
}
$users = [];
$owner = $this->userManager->get($board->getOwner());
if ($owner === null) {
if (!$this->userManager->userExists($board->getOwner())) {
$this->logger->info('No owner found for board ' . $board->getId());
} else {
$users[$owner->getUID()] = new User($owner);
$users[$board->getOwner()] = new User($board->getOwner(), $this->userManager);
}
$acls = $this->aclMapper->findAll($boardId);
/** @var Acl $acl */
foreach ($acls as $acl) {
if ($acl->getType() === Acl::PERMISSION_TYPE_USER) {
$user = $this->userManager->get($acl->getParticipant());
if ($user === null) {
if (!$this->userManager->userExists($acl->getParticipant())) {
$this->logger->info('No user found for acl rule ' . $acl->getId());
continue;
}
$users[$user->getUID()] = new User($user);
$users[$acl->getParticipant()] = new User($acl->getParticipant(), $this->userManager);
}
if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) {
$group = $this->groupManager->get($acl->getParticipant());
@@ -284,7 +275,7 @@ class PermissionService {
continue;
}
foreach ($group->getUsers() as $user) {
$users[$user->getUID()] = new User($user);
$users[$user->getUID()] = new User($user->getUID(), $this->userManager);
}
}
@@ -305,7 +296,7 @@ class PermissionService {
if ($user === null) {
$this->logger->info('No user found for circle member ' . $member->getUserId());
} else {
$users[$member->getUserId()] = new User($user);
$users[$member->getUserId()] = new User($member->getUserId(), $this->userManager);
}
}
} catch (\Exception $e) {

View File

@@ -82,11 +82,7 @@ class SearchService {
}, $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);
return $this->cardService->enrichCards($matchedCards);
}
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
@@ -117,7 +113,8 @@ class SearchService {
$comment = $this->commentsManager->get($cardRow['comment_id']);
unset($cardRow['comment_id']);
$card = Card::fromRow($cardRow);
$self->cardService->enrich($card);
// TODO: Only perform one enrich call here
$self->cardService->enrichCards([$card]);
$user = $this->userManager->get($comment->getActorId());
$displayName = $user ? $user->getDisplayName() : '';
return new CommentSearchResultEntry($comment->getId(), $comment->getMessage(), $displayName, $card, $this->urlGenerator, $this->l10n);

View File

@@ -36,10 +36,12 @@ use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\BoardUpdatedEvent;
use OCA\Deck\Model\CardDetails;
use OCA\Deck\NoPermissionException;
use OCA\Deck\StatusException;
use OCA\Deck\Validators\StackServiceValidator;
use OCP\EventDispatcher\IEventDispatcher;
use Psr\Log\LoggerInterface;
class StackService {
@@ -55,6 +57,7 @@ class StackService {
private ActivityManager $activityManager;
private ChangeHelper $changeHelper;
private LoggerInterface $logger;
private IEventDispatcher $eventDispatcher;
private StackServiceValidator $stackServiceValidator;
public function __construct(
@@ -70,6 +73,7 @@ class StackService {
ActivityManager $activityManager,
ChangeHelper $changeHelper,
LoggerInterface $logger,
IEventDispatcher $eventDispatcher,
StackServiceValidator $stackServiceValidator
) {
$this->stackMapper = $stackMapper;
@@ -84,6 +88,7 @@ class StackService {
$this->activityManager = $activityManager;
$this->changeHelper = $changeHelper;
$this->logger = $logger;
$this->eventDispatcher = $eventDispatcher;
$this->stackServiceValidator = $stackServiceValidator;
}
@@ -94,9 +99,9 @@ class StackService {
return;
}
$this->cardService->enrichCards($cards);
$cards = array_map(
function (Card $card): CardDetails {
$this->cardService->enrich($card);
return new CardDetails($card);
},
$cards
@@ -237,6 +242,7 @@ class StackService {
ActivityManager::DECK_OBJECT_BOARD, $stack, ActivityManager::SUBJECT_STACK_CREATE
);
$this->changeHelper->boardChanged($boardId);
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($boardId));
return $stack;
}
@@ -265,6 +271,7 @@ class StackService {
ActivityManager::DECK_OBJECT_BOARD, $stack, ActivityManager::SUBJECT_STACK_DELETE
);
$this->changeHelper->boardChanged($stack->getBoardId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stack->getBoardId()));
$this->enrichStackWithCards($stack);
return $stack;
@@ -306,6 +313,7 @@ class StackService {
ActivityManager::DECK_OBJECT_BOARD, $changes, ActivityManager::SUBJECT_STACK_UPDATE
);
$this->changeHelper->boardChanged($stack->getBoardId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stack->getBoardId()));
return $stack;
}
@@ -345,6 +353,7 @@ class StackService {
$result[$stack->getOrder()] = $stack;
}
$this->changeHelper->boardChanged($stackToSort->getBoardId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stackToSort->getBoardId()));
return $result;
}

1251
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,109 +1,109 @@
{
"name": "deck",
"description": "",
"version": "1.9.0-beta.1",
"authors": [
{
"name": "Julius Härtl",
"email": "jus@bitgrid.net",
"role": "Developer"
},
{
"name": "Michael Weimann",
"email": "mail@michael-weimann.eu",
"role": "Developer"
}
],
"license": "agpl",
"private": true,
"scripts": {
"build": "NODE_ENV=production webpack --progress --config webpack.js",
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"lint:cypress": "eslint --ext .js cypress",
"stylelint": "stylelint src",
"stylelint:fix": "stylelint src --fix",
"test": "jest",
"test:coverage": "jest --coverage"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.20.13",
"@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.3.0",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/event-bus": "^3.0.2",
"@nextcloud/files": "^2.1.0",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^2.0.1",
"@nextcloud/moment": "^1.2.1",
"@nextcloud/notify_push": "^1.1.3",
"@nextcloud/router": "^2.0.1",
"@nextcloud/vue": "^7.5.0",
"@nextcloud/vue-dashboard": "^2.0.1",
"@nextcloud/vue-richtext": "^2.0.4",
"blueimp-md5": "^2.19.0",
"dompurify": "^2.4.3",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-task-checkbox": "^1.0.6",
"moment": "^2.29.4",
"nextcloud-vue-collections": "^0.10.0",
"p-queue": "^7.3.4",
"url-search-params-polyfill": "^8.1.1",
"vue": "^2.7.14",
"vue-at": "^2.5.1",
"vue-click-outside": "^1.1.0",
"vue-easymde": "^2.0.0",
"vue-infinite-loading": "^2.4.5",
"vue-material-design-icons": "^5.2.0",
"vue-router": "^3.6.5",
"vue-smooth-dnd": "^0.8.1",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": "^16.0.0",
"npm": "^7.0.0 || ^8.0.0"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.3.0",
"@nextcloud/cypress": "^1.0.0-beta.2",
"@nextcloud/eslint-config": "^8.2.1",
"@nextcloud/stylelint-config": "^2.3.0",
"@nextcloud/webpack-vue-config": "^5.4.0",
"@relative-ci/agent": "^4.1.3",
"@vue/test-utils": "^1.3.4",
"@vue/vue2-jest": "^29.2.2",
"cypress": "^12.5.1",
"eslint-plugin-cypress": "^2.12.1",
"eslint-webpack-plugin": "^4.0.0",
"jest": "^29.4.3",
"jest-serializer-vue": "^3.1.0",
"stylelint-webpack-plugin": "^4.0.0",
"vue-template-compiler": "^2.7.14"
},
"jest": {
"moduleFileExtensions": [
"js",
"vue"
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
},
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
]
}
}
"name": "deck",
"description": "",
"version": "1.9.0",
"authors": [
{
"name": "Julius Härtl",
"email": "jus@bitgrid.net",
"role": "Developer"
},
{
"name": "Michael Weimann",
"email": "mail@michael-weimann.eu",
"role": "Developer"
}
],
"license": "agpl",
"private": true,
"scripts": {
"build": "NODE_ENV=production webpack --progress --config webpack.js",
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"lint:cypress": "eslint --ext .js cypress",
"stylelint": "stylelint src",
"stylelint:fix": "stylelint src --fix",
"test": "jest",
"test:coverage": "jest --coverage"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.21.0",
"@nextcloud/auth": "^2.0.0",
"@nextcloud/axios": "^2.3.0",
"@nextcloud/dialogs": "^4.0.1",
"@nextcloud/event-bus": "^3.0.2",
"@nextcloud/files": "^2.1.0",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^2.1.0",
"@nextcloud/moment": "^1.2.1",
"@nextcloud/notify_push": "^1.1.3",
"@nextcloud/router": "^2.0.1",
"@nextcloud/vue": "^7.7.1",
"@nextcloud/vue-dashboard": "^2.0.1",
"@nextcloud/vue-richtext": "^2.0.4",
"blueimp-md5": "^2.19.0",
"dompurify": "^3.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-task-checkbox": "^1.0.6",
"moment": "^2.29.4",
"nextcloud-vue-collections": "^0.11.0",
"p-queue": "^7.3.4",
"url-search-params-polyfill": "^8.1.1",
"vue": "^2.7.14",
"vue-at": "^2.5.1",
"vue-click-outside": "^1.1.0",
"vue-easymde": "^2.0.0",
"vue-infinite-loading": "^2.4.5",
"vue-material-design-icons": "^5.2.0",
"vue-router": "^3.6.5",
"vue-smooth-dnd": "^0.8.1",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": "^16.0.0",
"npm": "^7.0.0 || ^8.0.0"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.3.0",
"@nextcloud/cypress": "^1.0.0-beta.2",
"@nextcloud/eslint-config": "^8.2.1",
"@nextcloud/stylelint-config": "^2.3.0",
"@nextcloud/webpack-vue-config": "^5.4.0",
"@relative-ci/agent": "^4.1.3",
"@vue/test-utils": "^1.3.4",
"@vue/vue2-jest": "^29.2.2",
"cypress": "^12.7.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-webpack-plugin": "^4.0.0",
"jest": "^29.4.3",
"jest-serializer-vue": "^3.1.0",
"stylelint-webpack-plugin": "^4.1.0",
"vue-template-compiler": "^2.7.14"
},
"jest": {
"moduleFileExtensions": [
"js",
"vue"
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
},
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
]
}
}

View File

@@ -95,8 +95,7 @@ export default {
.avatar-wrapper {
background-color: #b9b9b9;
border-radius: 50%;
border-width: 2px;
border-style: solid;
border: 1px solid var(--color-border-dark);
width: var(--size);
height: var(--size);
text-align: center;

View File

@@ -57,7 +57,10 @@
@drag-start="draggingStack = true"
@drag-end="draggingStack = false"
@drop="onDropStack">
<Draggable v-for="stack in stacksByBoard" :key="stack.id" data-click-closes-sidebar="true">
<Draggable v-for="stack in stacksByBoard"
:key="stack.id"
data-click-closes-sidebar="true"
class="stack-draggable-wrapper">
<Stack :stack="stack" :dragging="draggingStack" data-click-closes-sidebar="true" />
</Draggable>
</Container>
@@ -223,7 +226,7 @@ export default {
align-items: stretch;
height: 100%;
&:deep(.smooth-dnd-draggable-wrapper) {
&:deep(.stack-draggable-wrapper.smooth-dnd-draggable-wrapper) {
display: flex;
height: auto;

View File

@@ -150,13 +150,13 @@
import ClickOutside from 'vue-click-outside'
import { mapGetters, mapState } from 'vuex'
import { Container, Draggable } from 'vue-smooth-dnd'
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
import { NcActions, NcActionButton, NcModal } from '@nextcloud/vue'
import { showError, showUndo } from '@nextcloud/dialogs'
import CardItem from '../cards/CardItem.vue'
import '@nextcloud/dialogs/styles/toast.scss'
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
import '@nextcloud/dialogs/dist/index.css'
export default {
name: 'Stack',

View File

@@ -417,6 +417,7 @@ h5 {
.app-sidebar__tab .description__text .text-menubar {
top: -10px !important;
z-index: 100;
}
.modal__card .description__text .text-menubar {

View File

@@ -89,13 +89,14 @@
<script>
import { NcModal, NcActions, NcActionButton, NcMultiselect } from '@nextcloud/vue'
import { mapGetters, mapState } from 'vuex'
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
import CardBulletedIcon from 'vue-material-design-icons/CardBulleted.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { showUndo } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/styles/toast.scss'
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
import CardBulletedIcon from 'vue-material-design-icons/CardBulleted.vue'
import '@nextcloud/dialogs/dist/index.css'
export default {
name: 'CardMenu',

View File

@@ -52,6 +52,18 @@ hasPush = listen('deck_board_update', (name, body) => {
store.dispatch('refreshBoard', currentBoardId)
})
listen('deck_card_update', (name, body) => {
// ignore update events which we have triggered ourselves
if (isOurSessionToken(body._causingSessionToken)) return
// only handle update events for the currently open board
const currentBoardId = store.state.currentBoard?.id
if (body.boardId !== currentBoardId) return
store.dispatch('loadStacks', currentBoardId)
})
/**
* is the notify_push app active and can
* provide us with real time updates?
@@ -131,7 +143,12 @@ export function createSession(boardId) {
async close() {
clearInterval(interval)
document.removeEventListener('visibilitychange', visibilitychangeListener)
await sessionApi.closeSession(boardId, await tokenPromise)
if (token) {
await sessionApi.closeSession(boardId, token)
tokenPromise = null
token = null
delete axios.defaults.headers['x-nc-deck-session']
}
},
}
}

View File

@@ -273,6 +273,17 @@ export default {
addNewCard(state, card) {
state.cards.push(card)
},
setCards(state, cards) {
const deletedCards = state.cards.filter(_card => {
return cards.findIndex(c => _card.id === c.id) === -1
})
for (const card of deletedCards) {
this.commit('deleteCard', card)
}
for (const card of cards) {
this.commit('addCard', card)
}
},
},
actions: {
async addCard({ commit }, card) {

View File

@@ -333,10 +333,15 @@ export default new Vuex.Store({
commit('setAssignableUsers', board.users)
},
async refreshBoard({ commit }, boardId) {
async refreshBoard({ commit, dispatch }, boardId) {
const board = await apiClient.loadById(boardId)
const etagHasChanged = board.ETag !== this.state.currentBoard.ETag
commit('setCurrentBoard', board)
commit('setAssignableUsers', board.users)
if (etagHasChanged) {
dispatch('loadStacks', boardId)
}
},
toggleShowArchived({ commit }) {

View File

@@ -84,14 +84,16 @@ export default {
call = 'loadArchivedStacks'
}
const stacks = await apiClient[call](boardId)
const cards = []
for (const i in stacks) {
const stack = stacks[i]
for (const j in stack.cards) {
commit('addCard', stack.cards[j])
cards.push(stack.cards[j])
}
delete stack.cards
commit('addStack', stack)
}
commit('setCards', cards)
},
createStack({ commit }, stack) {
stack.boardId = this.state.currentBoard.id

View File

@@ -0,0 +1 @@
61324

View File

@@ -40,7 +40,6 @@ class ServerContext implements Context {
}
public function getCookieJar(): CookieJar {
echo $this->currentUser;
return $this->cookieJar;
}

View File

@@ -27,16 +27,16 @@ if [ -z "$EXECUTOR_NUMBER" ]; then
fi
PORT=$((9090 + $EXECUTOR_NUMBER))
echo $PORT
php -S localhost:$PORT -t $OC_PATH &
php -q -S localhost:$PORT -t $OC_PATH &
PHPPID=$!
echo $PHPPID
export TEST_SERVER_URL="http://localhost:$PORT/ocs/"
vendor/bin/behat $SCENARIO_TO_RUN
vendor/bin/behat --colors $SCENARIO_TO_RUN
RESULT=$?
kill $PHPPID
kill -9 $PHPPID
echo "runsh: Exit code: $RESULT"
exit $RESULT

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Db;
use OCP\IUser;
use OCP\IUserManager;
class UserTest extends \Test\TestCase {
public function testGroupObjectSerialize() {
@@ -35,7 +36,11 @@ class UserTest extends \Test\TestCase {
$user->expects($this->any())
->method('getDisplayName')
->willReturn('myuser displayname');
$userRelationalObject = new User($user);
$userManager = $this->createMock(IUserManager::class);
$userManager->expects($this->any())
->method('get')
->willReturn($user);
$userRelationalObject = new User('myuser', $userManager);
$expected = [
'uid' => 'myuser',
'displayname' => 'myuser displayname',
@@ -53,7 +58,11 @@ class UserTest extends \Test\TestCase {
$user->expects($this->any())
->method('getDisplayName')
->willReturn('myuser displayname');
$userRelationalObject = new User($user);
$userManager = $this->createMock(IUserManager::class);
$userManager->expects($this->any())
->method('get')
->willReturn($user);
$userRelationalObject = new User('myuser', $userManager);
$expected = [
'uid' => 'myuser',
'displayname' => 'myuser displayname',

View File

@@ -37,6 +37,7 @@ use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use PHPUnit\Framework\MockObject\MockObject;
@@ -219,8 +220,9 @@ class NotificationHelperTest extends \Test\TestCase {
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
]);
$userManager = $this->createMock(IUserManager::class);
$card->setAssignedUsers([
new User($users[0])
new User($users[0]->getUID(), $userManager)
]);
$this->cardMapper->expects($this->once())
->method('findBoardId')
@@ -308,8 +310,9 @@ class NotificationHelperTest extends \Test\TestCase {
'title' => 'MyCardTitle',
'duedate' => '2020-12-24'
]);
$userManager = $this->createMock(IUserManager::class);
$card->setAssignedUsers([
new User($users[0])
new User($users[0]->getUID(), $userManager)
]);
$this->cardMapper->expects($this->once())
->method('findBoardId')

View File

@@ -219,6 +219,7 @@ class BoardServiceTest extends TestCase {
public function testUpdate() {
$board = new Board();
$board->setId(123);
$board->setTitle('MyBoard');
$board->setOwner('admin');
$board->setColor('00ff00');

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
@@ -155,7 +156,8 @@ class CardServiceTest extends TestCase {
->method('getNumberOfCommentsForObject')
->willReturn(0);
$boardMock = $this->createMock(Board::class);
$stackMock = $this->createMock(Stack::class);
$stackMock = new Stack();
$stackMock->setBoardId(1234);
$this->stackMapper->expects($this->any())
->method('find')
->willReturn($stackMock);
@@ -168,13 +170,21 @@ class CardServiceTest extends TestCase {
->method('find')
->with(123)
->willReturn($card);
$a1 = new Assignment();
$a1->setCardId(1337);
$a1->setType(0);
$a1->setParticipant('user1');
$a2 = new Assignment();
$a2->setCardId(1337);
$a2->setType(0);
$a2->setParticipant('user2');
$this->assignedUsersMapper->expects($this->any())
->method('findAll')
->with(1337)
->willReturn(['user1', 'user2']);
->method('findIn')
->with([1337])
->willReturn([$a1, $a2]);
$cardExpected = new Card();
$cardExpected->setId(1337);
$cardExpected->setAssignedUsers(['user1', 'user2']);
$cardExpected->setAssignedUsers([$a1, $a2]);
$cardExpected->setRelatedBoard($boardMock);
$cardExpected->setRelatedStack($stackMock);
$cardExpected->setLabels([]);

View File

@@ -83,10 +83,6 @@ class DefaultBoardServiceTest extends TestCase {
->method('getUserValue')
->willReturn('yes');
$this->boardMapper->expects($this->once())
->method('findAllByUser')
->willReturn($userBoards);
$this->config->expects($this->once())
->method('setUserValue');
@@ -107,10 +103,6 @@ class DefaultBoardServiceTest extends TestCase {
->method('getUserValue')
->willReturn('no');
$this->boardMapper->expects($this->once())
->method('findAllByUser')
->willReturn($userBoards);
$result = $this->service->checkFirstRun($this->userId);
$this->assertEquals($result, false);
}

View File

@@ -30,6 +30,9 @@ use OCP\IUserManager;
use OCP\Server;
use PHPUnit\Framework\MockObject\MockObject;
/**
* @group DB
*/
class TrelloJsonServiceTest extends \Test\TestCase {
private TrelloJsonService $service;
/** @var IURLGenerator|MockObject */
@@ -57,7 +60,7 @@ class TrelloJsonServiceTest extends \Test\TestCase {
}
public function testValidateUsersWithInvalidUser() {
$this->expectErrorMessage('Trello user trello_user not found in property "members" of json data');
$this->expectExceptionMessage('Trello user trello_user not found in property "members" of json data');
$importService = $this->createMock(BoardImportService::class);
$importService
->method('getConfig')

View File

@@ -236,6 +236,11 @@ class PermissionServiceTest extends \Test\TestCase {
$board->setAcl($this->getAcls($boardId));
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
$this->aclMapper->expects($this->any())
->method('findAll')
->with($boardId)
->willReturn($this->getAcls($boardId));
$this->shareManager->expects($this->any())
->method('sharingDisabledForUser')
->willReturn(false);
@@ -262,12 +267,17 @@ class PermissionServiceTest extends \Test\TestCase {
$this->boardMapper->expects($this->any())->method('find')->willReturn($board);
}
$this->aclMapper->expects($this->any())
->method('findAll')
->with($boardId)
->willReturn($this->getAcls($boardId));
if ($result) {
$actual = $this->service->checkPermission($mapper, 1234, $permission);
$actual = $this->service->checkPermission($mapper, $boardId, $permission);
$this->assertTrue($actual);
} else {
$this->expectException(NoPermissionException::class);
$this->service->checkPermission($mapper, 1234, $permission);
$this->service->checkPermission($mapper, $boardId, $permission);
}
}
@@ -340,7 +350,7 @@ class PermissionServiceTest extends \Test\TestCase {
$aclGroup->setParticipant('group1');
$board = $this->createMock(Board::class);
$board->expects($this->once())
$board->expects($this->any())
->method('__call')
->with('getOwner', [])
->willReturn('user1');
@@ -352,8 +362,8 @@ class PermissionServiceTest extends \Test\TestCase {
->method('find')
->with(123)
->willReturn($board);
$this->userManager->expects($this->exactly(2))
->method('get')
$this->userManager->expects($this->any())
->method('userExists')
->withConsecutive(['user1'], ['user2'])
->willReturnOnConsecutiveCalls($user1, $user2);
@@ -367,9 +377,9 @@ class PermissionServiceTest extends \Test\TestCase {
->willReturn($group);
$users = $this->service->findUsers(123);
$this->assertEquals([
'user1' => new User($user1),
'user2' => new User($user2),
'user3' => new User($user3),
'user1' => new User($user1->getUID(), $this->userManager),
'user2' => new User($user2->getUID(), $this->userManager),
'user3' => new User($user3->getUID(), $this->userManager),
], $users);
}
}

View File

@@ -34,6 +34,7 @@ use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Validators\StackServiceValidator;
use OCP\EventDispatcher\IEventDispatcher;
use Psr\Log\LoggerInterface;
use \Test\TestCase;
@@ -71,6 +72,8 @@ class StackServiceTest extends TestCase {
private $changeHelper;
/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
private $logger;
/** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
private $eventDispatcher;
/** @var StackServiceValidator|\PHPUnit\Framework\MockObject\MockObject */
private $stackServiceValidator;
@@ -88,6 +91,7 @@ class StackServiceTest extends TestCase {
$this->activityManager = $this->createMock(ActivityManager::class);
$this->changeHelper = $this->createMock(ChangeHelper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->stackServiceValidator = $this->createMock(StackServiceValidator::class);
$this->stackService = new StackService(
@@ -103,6 +107,7 @@ class StackServiceTest extends TestCase {
$this->activityManager,
$this->changeHelper,
$this->logger,
$this->eventDispatcher,
$this->stackServiceValidator
);
}
@@ -110,10 +115,12 @@ class StackServiceTest extends TestCase {
public function testFindAll() {
$this->permissionService->expects($this->once())->method('checkPermission');
$this->stackMapper->expects($this->once())->method('findAll')->willReturn($this->getStacks());
$this->cardService->expects($this->atLeastOnce())->method('enrich')->will(
$this->cardService->expects($this->atLeastOnce())->method('enrichCards')->will(
$this->returnCallback(
function ($card) {
$card->setLabels($this->getLabels()[$card->getId()]);
function ($cards) {
foreach ($cards as $card) {
$card->setLabels($this->getLabels()[$card->getId()]);
}
}
)
);
@@ -196,6 +203,7 @@ class StackServiceTest extends TestCase {
$this->permissionService->expects($this->once())->method('checkPermission');
$stackToBeDeleted = new Stack();
$stackToBeDeleted->setId(1);
$stackToBeDeleted->setBoardId(1);
$this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted);
$this->stackMapper->expects($this->once())->method('update')->willReturn($stackToBeDeleted);
$this->cardMapper->expects($this->once())->method('findAll')->willReturn([]);
@@ -244,6 +252,7 @@ class StackServiceTest extends TestCase {
private function createStack($id, $order) {
$stack = new Stack();
$stack->setId($id);
$stack->setBoardId(1);
$stack->setOrder($order);
return $stack;
}

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Controller;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Board;
use OCP\IUser;
class BoardControllerTest extends \Test\TestCase {
@@ -88,11 +89,12 @@ class BoardControllerTest extends \Test\TestCase {
}
public function testRead() {
$board = new Board();
$this->boardService->expects($this->once())
->method('find')
->with(123)
->willReturn(1);
$this->assertEquals(1, $this->controller->read(123));
->willReturn($board);
$this->assertEquals($board, $this->controller->read(123));
}
public function testCreate() {