Compare commits

..

116 Commits

Author SHA1 Message Date
Luka Trovic
0a3f064c69 fix: feedback
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-04-20 14:24:31 +00:00
Luka Trovic
15db16fe7e fix: show card after moving into another list
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-04-20 14:24:31 +00:00
Julius Härtl
558ac704f5 Merge pull request #3497 from nextcloud/bugfix/noid/share-query-opt
[stable23] Use explicit cast to make use of index
2022-04-19 21:43:16 +02:00
Nextcloud bot
0c98a04699 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-04-17 02:38:10 +00:00
Nextcloud bot
4f9b71dcad [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-04-14 02:42:23 +00:00
Nextcloud bot
b2367314fc [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-04-13 02:40:14 +00:00
Julius Härtl
c04e5c1681 Use explicit cast to make use of index
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-04-12 10:13:22 +02:00
Julius Härtl
541eeb0b06 Merge pull request #3664 from nextcloud/backport/stable23/2496
[stable23] Transfer ownership
2022-04-11 18:29:14 +02:00
Julius Härtl
1bd62d7fa2 Merge pull request #3713 from nextcloud/backport/3670/stable23
[stable23] Properly check for the stack AND setting board permissions
2022-04-11 18:28:14 +02:00
Julius Härtl
03a9915dd5 Merge pull request #3717 from nextcloud/backport/3625/stable23
[stable23] Fix: Check all circle shares for permissions
2022-04-11 18:27:51 +02:00
Bink
b6cce55325 Fix: Check all circle shares for permissions instead of returning after the first 2022-04-11 14:45:58 +00:00
Julius Härtl
47345e10f1 Properly check for the stack AND setting board permissions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-04-11 14:35:10 +00:00
Julius Härtl
39784dc940 Remove unused argument
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-04-11 16:25:54 +02:00
Julius Härtl
1cdea9dba0 Merge pull request #3706 from nextcloud/backport/3501/stable23
[stable23] Add a missing translation - not found in transifex
2022-04-11 09:53:30 +02:00
Nextcloud bot
49591d6699 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-04-09 02:39:15 +00:00
Luka Trovic
101e15c1b7 add a missing translation - not found in transifex
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-04-08 19:35:33 +00:00
Nextcloud bot
4d9743f8d3 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-04-06 02:39:52 +00:00
Nextcloud bot
51433ac4a9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-04-05 02:39:55 +00:00
Nextcloud bot
2cde9ac16c [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-31 02:41:41 +00:00
Nextcloud bot
fb2a38e684 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-30 02:39:31 +00:00
Nextcloud bot
ff4f33264b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-29 02:39:53 +00:00
Nextcloud bot
879e936728 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-28 02:39:00 +00:00
Nextcloud bot
c47bfd21eb [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-25 03:07:27 +00:00
Nextcloud bot
c59b6f8882 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-24 02:38:34 +00:00
Julius Härtl
4dabc22f27 lint: fix eslint
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 16:03:22 +01:00
Julius Härtl
490cfb2396 Fix tests and move to 7.3 as a min php version
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 16:03:22 +01:00
Julius Härtl
f840bbba11 PHP 7.2 compatbility
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 16:03:22 +01:00
Julius Härtl
cffcd4fd66 Adjust documentaion wording
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:40 +01:00
Julius Härtl
a3336cb0a0 Handle board exceptions more gracefully
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:40 +01:00
Julius Härtl
379f1144b3 Cover case where the owner is preserved
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:40 +01:00
Luka Trovic
eb69512b5f fix: feedback
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-22 14:22:40 +01:00
Luka Trovic
f46c31f120 feat: add api endpoint and UI to transfer a board to a different user
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-22 14:22:40 +01:00
Julius Härtl
b75fb76c08 fix: test cases using generator
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:39 +01:00
Julius Härtl
3d925fb145 Reuse single board transfer for all user boards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:39 +01:00
Julius Härtl
7786c86f47 fix: Properly handle limited scope for remapping users
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:39 +01:00
Julius Härtl
7f3a318fc5 cleanup test cases
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:39 +01:00
Julius Härtl
525a14c428 Allow transfer of single boards
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:39 +01:00
Julius Härtl
48ec97386c fix: Psalm
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:22:33 +01:00
Julius Härtl
26c76fbb46 fix: unit tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:15:15 +01:00
Luka Trovic
c95c96fb40 feat: add integration test for transferring board ownership with data
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-22 14:15:15 +01:00
Luka Trovic
981fc8e16f fix: integration tests
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-22 14:15:15 +01:00
Luka Trovic
b7f3c2d140 fix: unit test & psalm static code analysis issues
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-22 14:15:15 +01:00
Max
b01d472745 fix: queries with the new base mapper in BoardMapper
Signed-off-by: Max <max@nextcloud.com>
2022-03-22 14:15:15 +01:00
Max
05dbf67531 fix: Assignment is the new AssignedUsers
Signed-off-by: Max <max@nextcloud.com>
2022-03-22 14:15:14 +01:00
Julius Härtl
a8c22482f6 Make queries work with the new base mapper
Signed-off-by: Julius Härtl <jus@bitgrid.net>

fix: conflicts
2022-03-22 14:15:14 +01:00
Julius Härtl
c0886cfc7a Just cleanup old ACL rules, there are none for the board owner so nothing to cleanup or persist there
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:15:14 +01:00
Julius Härtl
7c68414435 Use proper description of what gets transferred
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:15:14 +01:00
Julius Härtl
da745f9306 Fix card mapper query for transfer
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-22 14:15:14 +01:00
Sergey Shliakhov
16413735d2 Fix coding styles
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Signed-off-by: Max <max@nextcloud.com>
2022-03-22 14:15:14 +01:00
Sergey Shliakhov
e70e7128c0 Transfer deck ownership even if target user already participant of a board
https://github.com/nextcloud/deck/pull/1955#issuecomment-640392715
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>
2022-03-22 14:15:14 +01:00
Sergey Shliakhov
36a9d2e95c Check type before transfer card participants ownership
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>

temp
2022-03-22 14:15:14 +01:00
Sergey Shliakhov
ce85b4378f Fix wrong class name
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>
2022-03-22 14:15:14 +01:00
Sergey Shliakhov
d52697823d Fix code style
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>
2022-03-22 14:15:13 +01:00
Sergey Shliakhov
53d462b3da Add tests
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>
2022-03-22 14:15:13 +01:00
Sergey Shliakhov
fc9fa5dc25 Update docs
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>

fix: conflicts
2022-03-22 14:15:11 +01:00
Sergey Shliakhov
eb8a321637 Add deck:transfer-ownership command
Signed-off-by: Sergey Shliakhov <husband.sergey@gmail.com>
2022-03-22 14:14:46 +01:00
Julius Härtl
ce77725ef0 Merge pull request #3663 from nextcloud/backport/3560/stable23
[stable23] Sort boards non case sensitive
2022-03-22 09:53:25 +01:00
ben
a88e028793 fixes nextcloud/deck#3410
Signed-off-by: ben <git@rott.io>
2022-03-22 08:43:03 +00:00
Nextcloud bot
0655ab8d59 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-18 02:38:31 +00:00
Julius Härtl
2c5b8c5fba Merge pull request #3641 from nextcloud/backport/3635/stable23
[stable23] 🐛 Fix missing files sidebar
2022-03-17 08:19:44 +01:00
Vinicius Reis
1057dc7039 fix style-lint
Signed-off-by: Vinicius Reis <vinicius.reis@nextcloud.com>
2022-03-17 00:02:32 -03:00
Nextcloud bot
8274d090c3 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-03-17 02:41:19 +00:00
Vinicius Reis
c213727180 🐛 Fix missing files sidebar
Signed-off-by: Vinicius Reis <vinicius.reis@nextcloud.com>
2022-03-14 14:10:53 +00:00
Julius Härtl
a5565d192d Merge pull request #3630 from nextcloud/release/1.6.1
Release 1.6.1
2022-03-10 09:54:27 +01:00
Julius Härtl
d25c8f6945 Bump version to 1.6.1
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-10 09:35:57 +01:00
Julius Härtl
e31ff9f247 Merge pull request #3626 from nextcloud/backport/3611/stable23
[stable23] Generate fixed link for activity emails
2022-03-09 20:19:59 +01:00
Luka Trovic
2d8bb1e654 fix: generate fixed link for activity emails
Signed-off-by: Luka Trovic <luka@nextcloud.com>

fix: generate fixed link for activity emails

Signed-off-by: Luka Trovic <luka@nextcloud.com>

Fix tests

Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-09 17:24:18 +00:00
Julius Härtl
18babccfe1 Create appstore-build-publish.yml 2022-03-04 13:58:09 +01:00
Julius Härtl
4646499917 Merge pull request #3618 from nextcloud/backport/3552/stable23
[stable23] return the selector for collections
2022-03-04 13:55:39 +01:00
dartcafe
da9c18b4cb return the selector for collections
Signed-off-by: dartcafe <github@dartcafe.de>
2022-03-04 08:53:40 +00:00
Julius Härtl
94134fc7d6 Merge pull request #3614 from nextcloud/backport/3612/stable23
[stable23] Make insert attachment buttom easy to click
2022-03-02 21:04:30 +01:00
Luka Trovic
113cc5c578 fix: make insert attachment buttom easy to click
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-02 16:24:21 +00:00
Nextcloud bot
0d12c2a4ec [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-19 02:38:00 +00:00
Nextcloud bot
3d1ed51ea9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-17 02:38:51 +00:00
Nextcloud bot
31152d9763 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-08 02:38:05 +00:00
Nextcloud bot
044afef32d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-06 02:36:59 +00:00
Nextcloud bot
1e35c7b836 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-05 02:37:27 +00:00
Nextcloud bot
0a43156a91 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-01 02:37:25 +00:00
Nextcloud bot
fd9ad30e3e [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-31 02:37:25 +00:00
Nextcloud bot
c5fc9f6833 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-24 02:37:10 +00:00
Nextcloud bot
298d72c651 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-19 02:38:12 +00:00
Julien Veyssier
64e231630f Merge pull request #3543 from nextcloud/backport/3541/stable23
[stable23] Fix confusion between stackId and boardId in StackService
2022-01-18 16:37:37 +01:00
Julien Veyssier
314fdc43ad fix confusion between stackId and boardId in StackService::update()
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
2022-01-18 15:23:22 +00:00
Nextcloud bot
6962187497 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-18 02:56:03 +00:00
Julius Härtl
0f9fdbd82a Merge pull request #3537 from nextcloud/backport/3529/stable23
[stable23] Fix talk integration
2022-01-14 23:05:23 +01:00
Joas Schilling
c6f8b22837 Fix talk integration
title and description where not populated and also opposed to documentation the link is not absolute

Signed-off-by: Joas Schilling <coding@schilljs.com>
2022-01-14 21:11:14 +00:00
Nextcloud bot
a3cdd5e19d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-13 02:37:09 +00:00
Julius Härtl
239bb5e4cb Merge pull request #3523 from nextcloud/backport/3502/stable23 2022-01-12 13:45:53 +01:00
Nextcloud bot
2a121536de [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-12 02:37:24 +00:00
Julius Härtl
1f645a3059 Merge pull request #3526 from nextcloud/backport/3500/stable23
[stable23] Fix CalDAV blocking and modernize circles API usage
2022-01-11 19:06:10 +01:00
Julius Härtl
92efa7f5ec Move any circles API usage to internal service
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-01-11 08:27:16 +00:00
Julius Härtl
70664c1853 Avoid blocking calendar access if something goes wrong while fetching deck entries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-01-11 08:27:15 +00:00
Luka Trovic
8ac2bbbbbc exclude deleted boards in the selection for target
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-01-11 08:00:46 +00:00
Nextcloud bot
7560619939 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-11 02:37:39 +00:00
Julius Härtl
7ce68f74c4 Merge pull request #3520 from nextcloud/backport/3512/stable23 2022-01-10 21:08:19 +01:00
Simon Spannagel
fa493ddd9a CardApiController: Fix order of optional parameters
Signed-off-by: Simon Spannagel <simonspa@kth.se>
2022-01-10 11:02:06 +00:00
Julius Härtl
d4c9147419 Bump version to 1.6.0
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-30 16:35:01 +01:00
Julius Härtl
21b81ff56c Merge pull request #3462 from nextcloud/backport/3459/stable23 2021-11-30 16:31:59 +01:00
Julius Härtl
4645c5077d Merge pull request #3463 from nextcloud/backport/3458/stable23 2021-11-30 16:31:52 +01:00
Julius Härtl
9a7bcafa6c Properly handle setters now that there is a default value of null with unset acl/labels
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-30 12:24:00 +00:00
Julius Härtl
53ebe480cf Fix cursor generation if no results are found
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-30 12:22:22 +00:00
Julius Härtl
e4db5e4d28 Bump version to 1.6.0-beta3
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-24 16:07:12 +01:00
Julius Härtl
2b8dee5081 Merge pull request #3449 from nextcloud/backport/3444/stable23 2021-11-24 15:57:59 +01:00
Julius Härtl
a8d41797ef Keep API results the same as before
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-24 14:06:49 +00:00
Julius Härtl
bc9fe51036 Avoid fetching board details multiple times
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-24 14:06:49 +00:00
Julius Härtl
d16799948f Cache card to board id relation
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-24 14:06:49 +00:00
Julien Veyssier
764dd947d4 Merge pull request #3446 from nextcloud/backport/3365/stable23
[stable23] Switch to QBMapper in BoardMapper
2021-11-24 10:53:51 +01:00
Julius Härtl
ea35f64d2a Update psalm baseline
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-24 08:41:08 +00:00
Julien Veyssier
a2205db748 switch to QBMapper in BoardMapper
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
2021-11-24 08:41:08 +00:00
Jonas
a80d87a3e9 Merge pull request #3440 from nextcloud/backport/3428/stable23
[stable23] Allow to download an attachment without navigating to the files app
2021-11-22 19:10:45 +01:00
Julius Härtl
12ed617a56 Allow to download an attachment without navigating to the files app
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-22 17:40:34 +00:00
Julius Härtl
ffccbf73fd Bump version to 1.6.0-beta2
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-19 17:15:00 +01:00
Julius Härtl
8514e91edc Merge pull request #3433 from nextcloud/backport/3432/stable23
[stable23] Fix event name for updating the description
2021-11-19 12:13:51 +01:00
Julius Härtl
8926363092 Fix event name for updating the description
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-19 09:14:44 +00:00
Joas Schilling
2be4fff781 Merge pull request #3418 from nextcloud/update-stable23-target-versions
Update stable23 target versions
2021-11-11 22:53:17 +01:00
Joas Schilling
8f52667d2c Update stable23 target versions
Signed-off-by: Joas Schilling <coding@schilljs.com>
2021-11-11 09:27:35 +01:00
103 changed files with 5105 additions and 9270 deletions

View File

@@ -11,6 +11,19 @@ updates:
open-pull-requests-limit: 10
reviewers:
- juliushaertl
- jakobroehrl
#- package-ecosystem: npm
# directory: "/"
# target-branch: "stable1.1"
# schedule:
# interval: weekly
# day: saturday
# time: "03:00"
# timezone: Europe/Paris
# open-pull-requests-limit: 10
# reviewers:
# - juliushaertl
# - jakobroehrl
- package-ecosystem: composer
directory: "/"
schedule:
@@ -21,16 +34,11 @@ updates:
open-pull-requests-limit: 10
reviewers:
- juliushaertl
- package-ecosystem: composer
directory: "/tests/integration"
schedule:
interval: weekly
day: saturday
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
reviewers:
- juliushaertl
ignore:
- dependency-name: christophwurst/nextcloud
versions:
- "< 16"
- ">= 15.a"
- package-ecosystem: github-actions
directory: "/"
schedule:

View File

@@ -12,15 +12,15 @@ jobs:
node-version: [14.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.4.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2.4.1
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7
run: npm i -g npm@7
- name: Setup PHP
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: '7.4'
tools: composer
@@ -33,7 +33,7 @@ jobs:
uname -a
RUST_BACKTRACE=1 krankerl --version
RUST_BACKTRACE=1 krankerl package
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v2
with:
name: Deck app tarball
path: build/artifacts/deck.tar.gz

View File

@@ -66,7 +66,7 @@ jobs:
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
- name: Set up php ${{ env.PHP_VERSION }}
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
coverage: none

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Add reaction on start
uses: peter-evans/create-or-update-comment@v2
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
@@ -26,7 +26,7 @@ jobs:
reaction-type: "+1"
- name: Checkout the latest code
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
token: ${{ secrets.COMMAND_BOT_PAT }}
@@ -37,7 +37,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
- name: Add reaction on failure
uses: peter-evans/create-or-update-comment@v2
uses: peter-evans/create-or-update-comment@v1
if: failure()
with:
token: ${{ secrets.COMMAND_BOT_PAT }}

View File

@@ -25,5 +25,5 @@ jobs:
# Nextcloud bot approve and merge request
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
target: minor
target: patch
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}

View File

@@ -19,7 +19,7 @@ jobs:
matrix:
php-versions: ['7.4']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['stable24']
server-versions: ['stable23']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
@@ -54,15 +54,14 @@ jobs:
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
cd build/integration && composer require --dev phpunit/phpunit:~8
- name: Checkout app
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit

View File

@@ -13,13 +13,13 @@ jobs:
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.1']
php-versions: ['7.3', '7.4']
name: php${{ matrix.php-versions }} lint
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.4.0
- name: Set up php${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: ${{ matrix.php-versions }}
coverage: none
@@ -31,9 +31,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
- name: Set up php
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: 7.4
coverage: none
@@ -50,9 +50,9 @@ jobs:
node-version: [14.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.4.0
- name: Use node ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2.4.1
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7
@@ -71,10 +71,10 @@ jobs:
name: stylelint node${{ matrix.node-version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.4.0
- name: Set up node ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2.4.1
with:
node-version: ${{ matrix.node-version }}

View File

@@ -17,15 +17,15 @@ jobs:
node-version: [14.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.4.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2.4.1
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7
run: npm i -g npm@7
- name: Setup PHP
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: '7.4'
tools: composer

View File

@@ -12,9 +12,9 @@ jobs:
node-version: [14.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2.4.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v2.4.1
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7

View File

@@ -18,9 +18,9 @@ jobs:
strategy:
fail-fast: false
matrix:
php-versions: ['7.4', '8.0', '8.1']
php-versions: ['7.3', '7.4']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['stable24']
server-versions: ['stable23']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
@@ -44,7 +44,7 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
@@ -57,12 +57,12 @@ jobs:
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit

View File

@@ -12,13 +12,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ocp-version: [ 'dev-stable24' ]
ocp-version: [ 'dev-stable23' ]
name: Nextcloud ${{ matrix.ocp-version }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2.4.0
- name: Set up php
uses: shivammathur/setup-php@2.18.0
uses: shivammathur/setup-php@2.15.0
with:
php-version: 7.4
tools: composer:v1

View File

@@ -1,56 +1,26 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.7.0-beta.1
### Added
- Transfer ownership @matchish @luka-nextcloud @juliushaertl [#2496](https://github.com/nextcloud/deck/pull/2496)
- Import from trello via CLI @vitormattos [#3182](https://github.com/nextcloud/deck/pull/3182)
- Add app config to toggle the default calendar setting as an admin @juliushaertl [#3528](https://github.com/nextcloud/deck/pull/3528)
- Show board name in browser title @luka-nextcloud [#3499](https://github.com/nextcloud/deck/pull/3499)
- Move DeleteCron to be time insensitive @juliushaertl [#3599](https://github.com/nextcloud/deck/pull/3599)
- 🚸 Shows error on board fetchData @vinicius73 [#3653](https://github.com/nextcloud/deck/pull/3653)
- Add support for PHP 8.1 @juliushaertl [#3601](https://github.com/nextcloud/deck/pull/3601)
- Nextcloud 24 compatibility
## 1.6.1
### Fixed
- CardApiController: Fix order of optional parameters @simonspa [#3512](https://github.com/nextcloud/deck/pull/3512)
- Exclude deleted boards in the selection for target @luka-nextcloud [#3502](https://github.com/nextcloud/deck/pull/3502)
- Fix CalDAV blocking and modernize circles API usage @juliushaertl [#3500](https://github.com/nextcloud/deck/pull/3500)
- Timestamps on created and modified at values @luka-nextcloud [#3532](https://github.com/nextcloud/deck/pull/3532)
- return the selector for collections @dartcafe [#3552](https://github.com/nextcloud/deck/pull/3552)
- Generate fixed link for activity emails @luka-nextcloud [#3611](https://github.com/nextcloud/deck/pull/3611)
- 🐛 Fix missing files sidebar @vinicius73 [#3635](https://github.com/nextcloud/deck/pull/3635)
- Handle description shortening more gracefully @juliushaertl [#3650](https://github.com/nextcloud/deck/pull/3650)
- Sort boards non case sensitive @Ben-Ro [#3560](https://github.com/nextcloud/deck/pull/3560)
- Remove unused argument from transfer ownership @juliushaertl [#3712](https://github.com/nextcloud/deck/pull/3712)
- Fix: Check all circle shares for permissions @bink [#3625](https://github.com/nextcloud/deck/pull/3625)
- Extend API changelog @juliushaertl [#3522](https://github.com/nextcloud/deck/pull/3522)
- Fix talk integration @nickvergessen [#3529](https://github.com/nextcloud/deck/pull/3529)
- Fix confusion between stackId and boardId in StackService @eneiluj [#3541](https://github.com/nextcloud/deck/pull/3541)
- Add horizontal scrollbar into the large table inside description @luka-nextcloud [#3531](https://github.com/nextcloud/deck/pull/3531)
- Make links in markdown note bolder @luka-nextcloud [#3530](https://github.com/nextcloud/deck/pull/3530)
- Update master php testing versions @nickvergessen [#3561](https://github.com/nextcloud/deck/pull/3561)
- Update master php enviroment @nickvergessen [#3582](https://github.com/nextcloud/deck/pull/3582)
- Make insert attachment buttom easy to click @luka-nextcloud [#3612](https://github.com/nextcloud/deck/pull/3612)
- Remove extra bullet @elitejake [#3613](https://github.com/nextcloud/deck/pull/3613)
- l10n: Delete space @Valdnet [#3666](https://github.com/nextcloud/deck/pull/3666)
- Update master php testing versions @nickvergessen [#3688](https://github.com/nextcloud/deck/pull/3688)
- Fix wording to represent the code behavior @q-wertz [#3685](https://github.com/nextcloud/deck/pull/3685)
- Fix cron jobs @nickvergessen [#3689](https://github.com/nextcloud/deck/pull/3689)
- Update master php testing versions @nickvergessen [#3695](https://github.com/nextcloud/deck/pull/3695)
- Optimise queries when preparing card related notifications @Raudius [#3690](https://github.com/nextcloud/deck/pull/3690)
- Properly check for the stack AND setting board permissions @juliushaertl [#3670](https://github.com/nextcloud/deck/pull/3670)
- Replace deprecated String.prototype.substr() @CommanderRoot [#3669](https://github.com/nextcloud/deck/pull/3669)
- Dependency updates
- Exclude deleted boards in the selection for target [#3523](https://api.github.com/repos/nextcloud/deck/pulls/3523)
- CardApiController: Fix order of optional parameters [#3520](https://api.github.com/repos/nextcloud/deck/pulls/3520)
- Fix cursor generation if no results are found [#3462](https://api.github.com/repos/nextcloud/deck/pulls/3462)
- Fix CalDAV blocking and modernize circles API usage [#3526](https://api.github.com/repos/nextcloud/deck/pulls/3526)
- Fix overview card listing [#3463](https://api.github.com/repos/nextcloud/deck/pulls/3463)
- Generate fixed link for activity emails [#3626](https://api.github.com/repos/nextcloud/deck/pulls/3626)
- return the selector for collections [#3618](https://api.github.com/repos/nextcloud/deck/pulls/3618)
- Fix confusion between stackId and boardId in StackService [#3543](https://api.github.com/repos/nextcloud/deck/pulls/3543)
- Fix talk integration [#3537](https://api.github.com/repos/nextcloud/deck/pulls/3537)
- Make insert attachment buttom easy to click [#3614](https://api.github.com/repos/nextcloud/deck/pulls/3614)
## 1.6.0-beta1
## 1.6.0
### Added
- #3449 Cache most frequent queries
- #3177 Use async import for vue component on collections entrypoint @juliushaertl
- #2791 Open description links in new tab @fm-sys
- #3344 Improve combined search @eneiluj
@@ -59,6 +29,11 @@ All notable changes to this project will be documented in this file.
### Fixed
- #3446 Switch to QBMapper in BoardMapper
- #3433 Fix event name for updating the description
- #3463 Fix overview card listing
- #3440 Allow to download an attachment without navigating to the files app
- #3462 Fix cursor generation if no results are found
- #3161 Reduce duplicate queries when fetching user boards an permissions @juliushaertl
- #3151 Always log generic exceptions @juliushaertl
- #3217 Move circle checks to a unified service and improve member checks @juliushaertl

View File

@@ -26,7 +26,7 @@ Deck is a kanban style organization tool aimed at personal planning and project
- [trello-to-deck](https://github.com/maxammann/trello-to-deck) - Migrates cards from Trello
- [mail2deck](https://github.com/newroco/mail2deck) - Provides an "email in" solution
- [A-deck](https://github.com/leoossa/A-deck) - Chrome Extension that allows to create new card in selected stack based on current tab
-
## Installation/Update
This app is supposed to work on the two latest Nextcloud versions.

View File

@@ -7,16 +7,16 @@
- 📥 Add your tasks to cards and put them in order
- 📄 Write down additional notes in Markdown
- 📄 Write down additional notes in markdown
- 🔖 Assign labels for even better organization
- 👥 Share with your team, friends or family
- 📎 Attach files and embed them in your Markdown description
- 📎 Attach files and embed them in your markdown description
- 💬 Discuss with your team using comments
- ⚡ Keep track of changes in the activity stream
- 🚀 Get your project organized
</description>
<version>1.7.0-beta.1</version>
<version>1.6.1</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
@@ -31,10 +31,11 @@
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
<dependencies>
<php min-version="7.3"/>
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="8.0">mysql</database>
<nextcloud min-version="24" max-version="24"/>
<database min-version="5.5">mysql</database>
<nextcloud min-version="23" max-version="23"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
@@ -43,7 +44,6 @@
</background-jobs>
<commands>
<command>OCA\Deck\Command\UserExport</command>
<command>OCA\Deck\Command\BoardImport</command>
<command>OCA\Deck\Command\TransferOwnership</command>
</commands>
<activity>

View File

@@ -92,10 +92,6 @@ return [
['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
['name' => 'board_import_api#getAllowedSystems', 'url' => '/api/v{apiVersion}/boards/import/getSystems','verb' => 'GET'],
['name' => 'board_import_api#getConfigSchema', 'url' => '/api/v{apiVersion}/boards/import/config/schema/{name}','verb' => 'GET'],
['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST'],
['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'],
['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'],

View File

@@ -9,27 +9,23 @@
}
],
"require": {
"cogpowered/finediff": "0.3.*",
"justinrainbow/json-schema": "^5.2"
"cogpowered/finediff": "0.3.*"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "dev-master",
"phpunit/phpunit": "^9",
"nextcloud/coding-standard": "^1.0.0",
"christophwurst/nextcloud": "^22@dev",
"phpunit/phpunit": "^8",
"nextcloud/coding-standard": "^0.5.0",
"symfony/event-dispatcher": "^4.0",
"vimeo/psalm": "^4.3",
"php-parallel-lint/php-parallel-lint": "^1.2"
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true,
"allow-plugins": {
"composer/package-versions-deprecated": true
},
"platform": {
"php": "7.4"
}
"php": "7.3"
},
"optimize-autoloader": true,
"classmap-authoritative": true
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",

1448
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -96,27 +96,10 @@ If available the ETag will also be part of JSON response objects as shown below
# Changelog
## API version 1.0
- Deck >=1.0.0: The maximum length of the card title has been extended from 100 to 255 characters
- Deck >=1.0.0: The API will now return a 400 Bad request response if the length limitation of a board, stack or card title is exceeded
## API version 1.1
This API version has become available with **Deck 1.3.0**.
## 1.0.0 (unreleased)
- The maximum length of the card title has been extended from 100 to 255 characters
- The API will now return a 400 Bad request response if the length limitation of a board, stack or card title is exceeded
- The attachments API endpoints will return other attachment types than deck_file
- Prior to Deck version v1.3.0 (API v1.0), attachments were stored within deck. For this type of attachments `deck_file` was used as the default type of attachments
- Starting with Deck version 1.3.0 (API v1.1) files are stored within the users regular Nextcloud files and the type `file` has been introduced for that
## API version 1.2 (unreleased)
- Endpoints for the new import functionality have been added:
- [GET /boards/import/getSystems - Import a board](#get-boardsimportgetsystems-import-a-board)
- [GET /boards/import/config/system/{schema} - Import a board](#get-boardsimportconfigsystemschema-import-a-board)
- [POST /boards/import - Import a board](#post-boardsimport-import-a-board)
# Endpoints
@@ -944,8 +927,7 @@ The request can fail with a bad request response for the following reasons:
| type | String | The type of the attachement |
| file | Binary | File data to add as an attachment |
- Prior to Deck version v1.3.0 (API v1.0), attachments were stored within deck. For this type of attachments `deck_file` was used as the default type of attachments
- Starting with Deck version 1.3.0 (API v1.1) files are stored within the users regular Nextcloud files and the type `file` has been introduced for that
For now only `deck_file` is supported as an attachment type.
#### Response
@@ -1006,49 +988,6 @@ For now only `deck_file` is supported as an attachment type.
##### 200 Success
### GET /boards/import/getSystems - Import a board
#### Request parameters
| Parameter | Type | Description |
| ------------ | ------- | --------------------------------------------- |
| system | Integer | The system name. Example: trello |
#### Response
Make a request to see the json schema of system
```json
{
}
```
### GET /boards/import/config/system/{schema} - Import a board
#### Request parameters
#### Response
```json
[
"trello"
]
```
### POST /boards/import - Import a board
#### Request parameters
| Parameter | Type | Description |
| ------------ | ------- | --------------------------------------------- |
| system | string | The allowed name of system to import from |
| config | Object | The config object (JSON) |
| data | Object | The data object to import (JSON) |
#### Response
##### 200 Success
# OCS API
The following endpoints are available through the Nextcloud OCS endpoint, which is available at `/ocs/v2.php/apps/deck/api/v1.0/`.
@@ -1065,7 +1004,6 @@ Deck stores user and app configuration values globally and per board. The GET en
| Config key | Description |
| --- | --- |
| calendar | Determines if the calendar/tasks integration through the CalDAV backend is enabled for the user (boolean) |
| cardDetailsInModal | Determines if the bigger view is used (boolean) |
| groupLimit | Determines if creating new boards is limited to certain groups of the instance. The resulting output is an array of group objects with the id and the displayname (Admin only)|
```
@@ -1078,7 +1016,6 @@ Deck stores user and app configuration values globally and per board. The GET en
},
"data": {
"calendar": true,
"cardDetailsInModal": true,
"groupLimit": [
{
"id": "admin",
@@ -1108,7 +1045,6 @@ Deck stores user and app configuration values globally and per board. The GET en
| --- | ----- |
| notify-due | `off`, `assigned` or `all` |
| calendar | Boolean |
| cardDetailsInModal | Boolean |
#### Example request

View File

@@ -14,9 +14,7 @@ Overall, Deck is easy to use. You can create boards, add users, share the Deck,
3. [Handle cards options](#3-handle-cards-options)
4. [Archive old tasks](#4-archive-old-tasks)
5. [Manage your board](#5-manage-your-board)
6. [Import boards](#6-import-boards)
7. [Search](#7-search)
8. [New owner for the deck entities](#8-new-owner-for-the-deck-entities)
6. [New owner for the deck entities](#8-new-owner-for-the-deck-entities)
### 1. Create my first board
In this example, we're going to create a board and share it with an other nextcloud user.
@@ -72,80 +70,14 @@ The **sharing tab** allows you to add users or even groups to your boards.
**Deleted objects** allows you to return previously deleted stacks or cards.
The **Timeline** allows you to see everything that happened in your boards. Everything!
### 6. Import boards
Importing can be done using the API or the `occ` `deck:import` command.
Comments with more than 1000 characters are placed as attached files to the card.
It is possible to import from the following sources:
#### Trello JSON
Steps:
* Create the data file
* Access Trello
* go to the board you want to export
* Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON
* Create the configuration file
* Execute the import informing the import file path, data file and source as `Trello JSON`
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON`
Example configuration file:
```json
{
"owner": "admin",
"color": "0800fd",
"uidRelation": {
"johndoe": "johndoe"
}
}
```
**Limitations**:
Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has.
#### Trello API
Import using API is recommended for boards with more than 1000 actions.
Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello.
* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization)
* Get the ID of the board you want to import by making a request to:
https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name
This ID you will use in the configuration file in the `board` property
* Create the configuration file
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON`
Example configuration file:
```json
{
"owner": "admin",
"color": "0800fd",
"api": {
"key": "0cc175b9c0f1b6a831c399e269772661",
"token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33"
},
"board": "8277e0910d750195b4487976",
"uidRelation": {
"johndoe": "johndoe"
}
}
```
### 7. Search
## Search
Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls.
This search allows advanced filtering of cards across all board of the logged in user.
For example the search `project tag:ToDo assigned:alice assigned:bob` will return all cards where the card title or description contains project **and** the tag ToDo is set **and** the user alice is assigned **and** the user bob is assigned.
#### Supported search filters
### Supported search filters
| Filter | Operators | Query |
| ----------- | ----------------- | ------------------------------------------------------------ |

View File

@@ -1,32 +0,0 @@
## Implement import
* Create a new importer class extending `ABoardImportService`
* Create a listener for event `BoardImportGetAllowedEvent` to enable your importer.
> You can read more about listeners on [Nextcloud](https://docs.nextcloud.com/server/latest/developer_manual/basics/events.html?highlight=event#writing-a-listener) doc.
Example:
```php
class YourCustomImporterListener {
public function handle(Event $event): void {
if (!($event instanceof BoardImportGetAllowedEvent)) {
return;
}
$event->getService()->addAllowedImportSystem([
'name' => YourCustomImporterService::$name,
'class' => YourCustomImporterService::class,
'internalName' => 'YourCustomImporter'
]);
}
}
```
* Register your listener on your `Application` class like this:
```php
$dispatcher = $this->getContainer()->query(IEventDispatcher::class);
$dispatcher->registerEventListener(
BoardImportGetAllowedEvent::class,
YourCustomImporterListener::class
);
```
* Use the `lib/Service/Importer/Systems/TrelloJsonService.php` class as inspiration

View File

@@ -1,7 +0,0 @@
## Import class diagram
Importing boards to the Deck implements the class diagram below.
> **NOTE**: When making any changes to the structure of the classes or implementing import from other sources, edit the `BoardImport.yuml` file
![Screenshot](resources/BoardImport.svg)

View File

@@ -1,214 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: G Pages: 1 -->
<svg width="417pt" height="830pt"
viewBox="0.00 0.00 417.01 830.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 826)">
<title>G</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-826 413.012,-826 413.012,4 -4,4"/>
<!-- A0 -->
<g id="node1" class="node">
<title>A0</title>
<polygon fill="#fff8dc" stroke="#000000" points="165.909,-822 70.091,-822 70.091,-766 171.909,-766 171.909,-816 165.909,-822"/>
<polyline fill="none" stroke="#000000" points="165.909,-822 165.909,-816 "/>
<polyline fill="none" stroke="#000000" points="171.909,-816 165.909,-816 "/>
<text text-anchor="middle" x="121" y="-809" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Classes used on</text>
<text text-anchor="middle" x="121" y="-797" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">board import.</text>
<text text-anchor="middle" x="121" y="-785" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Methods just to</text>
<text text-anchor="middle" x="121" y="-773" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">illustrate.</text>
</g>
<!-- A1 -->
<g id="node2" class="node">
<title>A1</title>
<polygon fill="none" stroke="#000000" points="108.7773,-680 23.2227,-680 23.2227,-644 108.7773,-644 108.7773,-680"/>
<text text-anchor="middle" x="66" y="-659" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ApiController</text>
</g>
<!-- A2 -->
<g id="node3" class="node">
<title>A2</title>
<polygon fill="none" stroke="#000000" points="0,-514 0,-546 132,-546 132,-514 0,-514"/>
<text text-anchor="start" x="9.607" y="-527" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">BoardImportApiController</text>
<polygon fill="none" stroke="#000000" points="0,-458 0,-514 132,-514 132,-458 0,-458"/>
<text text-anchor="start" x="45.8645" y="-495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+import()</text>
<text text-anchor="start" x="16.1335" y="-483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+getAllowedSystems()</text>
<text text-anchor="start" x="20.0185" y="-471" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+getConfigSchema()</text>
</g>
<!-- A1&#45;&gt;A2 -->
<g id="edge1" class="edge">
<title>A1&#45;&gt;A2</title>
<path fill="none" stroke="#000000" d="M66,-633.6693C66,-609.4424 66,-574.1663 66,-546.2238"/>
<polygon fill="#000000" stroke="#000000" points="66,-643.957 61.5001,-633.9569 66,-638.957 66.0001,-633.957 66.0001,-633.957 66.0001,-633.957 66,-638.957 70.5001,-633.957 66,-643.957 66,-643.957"/>
</g>
<!-- A3 -->
<g id="node4" class="node">
<title>A3</title>
<polygon fill="none" stroke="#000000" points="92,-364 92,-396 200,-396 200,-364 92,-364"/>
<text text-anchor="start" x="101.828" y="-377" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">BoardImportService</text>
<polygon fill="none" stroke="#000000" points="92,-284 92,-364 200,-364 200,-284 92,-284"/>
<text text-anchor="start" x="125.8645" y="-345" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+import()</text>
<text text-anchor="start" x="118.9105" y="-333" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+bootstrap()</text>
<text text-anchor="start" x="105.857" y="-321" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+validateSystem()</text>
<text text-anchor="start" x="108.218" y="-309" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#validateConfig()</text>
<text text-anchor="start" x="112.107" y="-297" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#validateData()</text>
</g>
<!-- A2&#45;&gt;A3 -->
<g id="edge2" class="edge">
<title>A2&#45;&gt;A3</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M87.8604,-457.7328C95.8577,-441.5382 105.0823,-422.8583 113.7939,-405.2174"/>
<polygon fill="#000000" stroke="#000000" points="118.2935,-396.1057 117.9004,-407.0646 116.0795,-400.5889 113.8656,-405.072 113.8656,-405.072 113.8656,-405.072 116.0795,-400.5889 109.8308,-403.0795 118.2935,-396.1057 118.2935,-396.1057"/>
<text text-anchor="middle" x="88.3076" y="-434.7378" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">uses</text>
</g>
<!-- A7 -->
<g id="node8" class="node">
<title>A7</title>
<polygon fill="none" stroke="#000000" points="37,-196 37,-228 129,-228 129,-196 37,-196"/>
<text text-anchor="start" x="46.612" y="-209" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">TrelloApiService</text>
<polygon fill="none" stroke="#000000" points="37,-164 37,-196 129,-196 129,-164 37,-164"/>
<text text-anchor="start" x="53.9655" y="-177" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+name:string</text>
</g>
<!-- A3&#45;&gt;A7 -->
<g id="edge6" class="edge">
<title>A3&#45;&gt;A7</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M114.8609,-283.9135C107.8316,-268.5143 100.7854,-252.0928 95.0404,-237.6613"/>
<polygon fill="#000000" stroke="#000000" points="91.2872,-228.0253 99.1098,-235.7102 93.1019,-232.6844 94.9167,-237.3434 94.9167,-237.3434 94.9167,-237.3434 93.1019,-232.6844 90.7235,-238.9767 91.2872,-228.0253 91.2872,-228.0253"/>
<text text-anchor="middle" x="99.6759" y="-267.8975" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">uses</text>
</g>
<!-- A9 -->
<g id="node10" class="node">
<title>A9</title>
<polygon fill="none" stroke="#000000" points="148,-202 148,-234 273,-234 273,-202 148,-202"/>
<text text-anchor="start" x="170.7765" y="-215" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">TrelloJsonService</text>
<polygon fill="none" stroke="#000000" points="148,-158 148,-202 273,-202 273,-158 148,-158"/>
<text text-anchor="start" x="181.4655" y="-183" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+name:string</text>
<text text-anchor="start" x="157.981" y="-171" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#needValidateData:true</text>
</g>
<!-- A3&#45;&gt;A9 -->
<g id="edge9" class="edge">
<title>A3&#45;&gt;A9</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M164.3261,-283.9135C170.0039,-270.5688 176.3462,-256.4563 182.4816,-243.5365"/>
<polygon fill="#000000" stroke="#000000" points="186.9002,-234.3677 186.6126,-245.3298 184.7295,-238.872 182.5588,-243.3762 182.5588,-243.3762 182.5588,-243.3762 184.7295,-238.872 178.505,-241.4226 186.9002,-234.3677 186.9002,-234.3677"/>
<text text-anchor="middle" x="163.6874" y="-260.9237" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">uses</text>
</g>
<!-- A10 -->
<g id="node11" class="node">
<title>A10</title>
<polygon fill="#fff8dc" stroke="#000000" points="317.7872,-362 218.2128,-362 218.2128,-318 323.7872,-318 323.7872,-356 317.7872,-362"/>
<polyline fill="none" stroke="#000000" points="317.7872,-362 317.7872,-356 "/>
<polyline fill="none" stroke="#000000" points="323.7872,-356 317.7872,-356 "/>
<text text-anchor="middle" x="271" y="-349" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">validateSystem is</text>
<text text-anchor="middle" x="271" y="-337" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">public because is</text>
<text text-anchor="middle" x="271" y="-325" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">used on Api.</text>
</g>
<!-- A3&#45;&gt;A10 -->
<g id="edge11" class="edge">
<title>A3&#45;&gt;A10</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M200.1992,-340C206.1915,-340 212.1837,-340 218.176,-340"/>
</g>
<!-- A4 -->
<g id="node5" class="node">
<title>A4</title>
<polygon fill="none" stroke="#000000" points="264.1131,-812 189.8869,-812 189.8869,-776 264.1131,-776 264.1131,-812"/>
<text text-anchor="middle" x="227" y="-791" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">Command</text>
</g>
<!-- A5 -->
<g id="node6" class="node">
<title>A5</title>
<polygon fill="none" stroke="#000000" points="148,-684 148,-716 307,-716 307,-684 148,-684"/>
<text text-anchor="start" x="199.9955" y="-697" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">BoardImport</text>
<polygon fill="none" stroke="#000000" points="148,-652 148,-684 307,-684 307,-652 148,-652"/>
<text text-anchor="start" x="157.907" y="-665" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+boardImportCommandService</text>
<polygon fill="none" stroke="#000000" points="148,-608 148,-652 307,-652 307,-608 148,-608"/>
<text text-anchor="start" x="200.8305" y="-633" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#configure()</text>
<text text-anchor="start" x="177.76" y="-621" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#execute(input,output)</text>
</g>
<!-- A4&#45;&gt;A5 -->
<g id="edge3" class="edge">
<title>A4&#45;&gt;A5</title>
<path fill="none" stroke="#000000" d="M227,-765.6356C227,-751.1554 227,-733.0451 227,-716.0324"/>
<polygon fill="#000000" stroke="#000000" points="227,-775.9227 222.5001,-765.9227 227,-770.9227 227.0001,-765.9227 227.0001,-765.9227 227.0001,-765.9227 227,-770.9227 231.5001,-765.9228 227,-775.9227 227,-775.9227"/>
</g>
<!-- A6 -->
<g id="node7" class="node">
<title>A6</title>
<polygon fill="none" stroke="#000000" points="150,-526 150,-558 304,-558 304,-526 150,-526"/>
<text text-anchor="start" x="159.7715" y="-539" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">BoardImportCommandService</text>
<polygon fill="none" stroke="#000000" points="150,-446 150,-526 304,-526 304,-446 150,-446"/>
<text text-anchor="start" x="199.9105" y="-507" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+bootstrap()</text>
<text text-anchor="start" x="206.8645" y="-495" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+import()</text>
<text text-anchor="start" x="186.857" y="-483" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+validateSystem()</text>
<text text-anchor="start" x="189.218" y="-471" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#validateConfig()</text>
<text text-anchor="start" x="193.107" y="-459" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#validateData()</text>
</g>
<!-- A5&#45;&gt;A6 -->
<g id="edge4" class="edge">
<title>A5&#45;&gt;A6</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M227,-607.8313C227,-595.0442 227,-581.2707 227,-568.0248"/>
<polygon fill="#000000" stroke="#000000" points="227,-558.0234 231.5001,-568.0234 227,-563.0234 227.0001,-568.0234 227.0001,-568.0234 227.0001,-568.0234 227,-563.0234 222.5001,-568.0235 227,-558.0234 227,-558.0234"/>
<text text-anchor="middle" x="218.5476" y="-586.7051" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">uses</text>
</g>
<!-- A6&#45;&gt;A3 -->
<g id="edge5" class="edge">
<title>A6&#45;&gt;A3</title>
<path fill="none" stroke="#000000" d="M198.8975,-445.7949C192.3634,-432.7268 185.3528,-418.7057 178.6417,-405.2834"/>
<polygon fill="#000000" stroke="#000000" points="174.0529,-396.1057 182.55,-403.0375 176.289,-400.5779 178.5251,-405.05 178.5251,-405.05 178.5251,-405.05 176.289,-400.5779 174.5001,-407.0625 174.0529,-396.1057 174.0529,-396.1057"/>
</g>
<!-- A7&#45;&gt;A3 -->
<g id="edge7" class="edge">
<title>A7&#45;&gt;A3</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M102.735,-228.0253C109.5347,-241.763 117.1224,-258.3431 124.0627,-274.4849"/>
<polygon fill="#000000" stroke="#000000" points="128.0634,-283.9135 120.0148,-276.4657 126.1104,-279.3107 124.1573,-274.7079 124.1573,-274.7079 124.1573,-274.7079 126.1104,-279.3107 128.2998,-272.9502 128.0634,-283.9135 128.0634,-283.9135"/>
<text text-anchor="middle" x="118.307" y="-237.5757" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">uses</text>
</g>
<!-- A8 -->
<g id="node9" class="node">
<title>A8</title>
<polygon fill="none" stroke="#000000" points="80,-64 80,-108 213,-108 213,-64 80,-64"/>
<text text-anchor="start" x="117.04" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">&lt;&lt;abstract&gt;&gt;</text>
<text text-anchor="start" x="98.9935" y="-77" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">ABoardImportService</text>
<polygon fill="none" stroke="#000000" points="80,-32 80,-64 213,-64 213,-32 80,-32"/>
<text text-anchor="start" x="92.036" y="-45" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">#needValidateData:false</text>
<polygon fill="none" stroke="#000000" points="80,0 80,-32 213,-32 213,0 80,0"/>
<text text-anchor="start" x="89.677" y="-13" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">+needValidateData():bool</text>
</g>
<!-- A7&#45;&gt;A8 -->
<g id="edge8" class="edge">
<title>A7&#45;&gt;A8</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M97.2957,-163.778C103.3956,-150.029 110.7371,-133.4813 117.8485,-117.4527"/>
<polygon fill="none" stroke="#000000" points="121.1416,-118.6605 121.9978,-108.1003 114.743,-115.8216 121.1416,-118.6605"/>
<text text-anchor="middle" x="96.9205" y="-140.7815" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">implements</text>
</g>
<!-- A9&#45;&gt;A3 -->
<g id="edge10" class="edge">
<title>A9&#45;&gt;A3</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M198.9952,-234.3677C194.0646,-246.7117 188.0483,-260.7568 181.8434,-274.4849"/>
<polygon fill="#000000" stroke="#000000" points="177.5286,-283.9135 177.598,-272.9478 179.6093,-279.367 181.6899,-274.8204 181.6899,-274.8204 181.6899,-274.8204 179.6093,-279.367 185.7818,-276.693 177.5286,-283.9135 177.5286,-283.9135"/>
<text text-anchor="middle" x="200.0654" y="-251.3391" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">uses</text>
</g>
<!-- A9&#45;&gt;A8 -->
<g id="edge13" class="edge">
<title>A9&#45;&gt;A8</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M192.8492,-157.9466C187.2535,-145.5313 180.8796,-131.389 174.6742,-117.6209"/>
<polygon fill="none" stroke="#000000" points="177.7167,-115.8534 170.4168,-108.1747 171.3349,-118.7297 177.7167,-115.8534"/>
<text text-anchor="middle" x="177.6953" y="-141.8944" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">implements</text>
</g>
<!-- A11 -->
<g id="node12" class="node">
<title>A11</title>
<polygon fill="#fff8dc" stroke="#000000" points="403.024,-224 290.976,-224 290.976,-168 409.024,-168 409.024,-218 403.024,-224"/>
<polyline fill="none" stroke="#000000" points="403.024,-224 403.024,-218 "/>
<polyline fill="none" stroke="#000000" points="409.024,-218 403.024,-218 "/>
<text text-anchor="middle" x="350" y="-211" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">To create an import</text>
<text text-anchor="middle" x="350" y="-199" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">to another system,</text>
<text text-anchor="middle" x="350" y="-187" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">create another class</text>
<text text-anchor="middle" x="350" y="-175" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">similar to this.</text>
</g>
<!-- A9&#45;&gt;A11 -->
<g id="edge12" class="edge">
<title>A9&#45;&gt;A11</title>
<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M272.6172,-196C278.6627,-196 284.7083,-196 290.7538,-196"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,24 +0,0 @@
// Created using [yUML](https://github.com/jaime-olivares/vscode-yuml)
// {type:class}
// {direction:topDown}
// {generate:true}
[note: Classes used on board import. Methods just to illustrate. {bg:cornsilk}]
[ApiController]<-[BoardImportApiController|+import();+getAllowedSystems();+getConfigSchema()]
[BoardImportApiController]uses-.->[BoardImportService|+import();+bootstrap();+validateSystem();#validateConfig();#validateData();]
[Command]<-[BoardImport|+boardImportCommandService|#configure();#execute(input,output)]
[BoardImport]uses-.->[BoardImportCommandService|+bootstrap();+import();+validateSystem();#validateConfig();#validateData()]
[BoardImportCommandService]->[BoardImportService]
[BoardImportService]uses-.->[TrelloApiService|+name:string]
[TrelloApiService]uses-.->[BoardImportService]
[TrelloApiService]implements-.-^[<<abstract>> ABoardImportService|#needValidateData:false|+needValidateData():bool]
[BoardImportService]uses-.->[TrelloJsonService|+name:string;#needValidateData:true]
[TrelloJsonService]uses-.->[BoardImportService]
[BoardImportService]-[note: validateSystem is public because is used on Api. {bg:cornsilk}]
[TrelloJsonService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}]
[TrelloJsonService]implements-.-^[<<abstract>> ABoardImportService]

View File

@@ -100,10 +100,12 @@ 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" : "Deck е инструмент за организация в стил kanban, насочен към лично планиране и организация на проекти за екипи, интегрирани с Nextcloud.\n\n\n- 📥 Добавете задачите си към карти и ги подредете\n- 📄 Запишете допълнителни бележки в markdown формат\n- Присвояване на етикети за още по-добра организация\n- 👥 Споделете с вашия екип, приятели или семейство\n- 📎Прикачете файлове и ги вградете във вашето описание за маркиране\n- 💬Обсъдете с вашия екип, като използвате коментари\n- ⚡ Проследявайте промените в потока от дейности\n- 🚀 Организирайте проекта си",
"Card details" : "Подробности за картата",
"Add board" : "Добави табло",
"Select the board to link to a project" : "Изберете таблото, което да свържете към проект",
@@ -168,8 +170,14 @@ OC.L10N.register(
"Can edit" : "Може да редактира",
"Can share" : "Може да споделя",
"Can manage" : "Може да управлява",
"Owner" : "Собственик",
"Delete" : "Изтриване",
"Failed to create share with {displayName}" : "Създаването на споделяне с {displayName} не бе успешно",
"Are you sure you want to transfer the board {title} for {user}?" : "Сигурни ли сте че искате да прехвърлите таблото {title} на {user}?",
"Transfer the board." : "Прехвърлете таблото.",
"Transfer" : "Прехвърляне",
"Transfer the board for {user} successfully" : "Успешно прехвърляне на таблото към {user} ",
"Failed to transfer the board for {user}" : "Неуспешно прехвърляне на таблото към {user}",
"Add a new list" : "Добавяне на нов списък",
"Archive all cards" : "Архивира всички карти",
"Delete list" : "Изтрива списък",
@@ -239,6 +247,7 @@ OC.L10N.register(
"Archive card" : "Архивиране на карта",
"Delete card" : "Изтриване на карта",
"Move card to another board" : "Преместване на картата на друго табло",
"List is empty" : "Списъкът е празен",
"Card deleted" : "Картата е изтрита",
"seconds ago" : "преди секунди",
"All boards" : "Всички табла",
@@ -284,6 +293,10 @@ OC.L10N.register(
"Share {file} with a Deck card" : "Споделяне {file} с Deck карта",
"Share" : "Споделяне",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck е инструмент за организация в стил kanban, насочен към лично планиране и организация на проекти за екипи, интегрирани с Nextcloud.\n\n\n- 📥 Добавете задачите си към карти и ги подредете\n- 📄 Запишете допълнителни бележки в markdown формат\n- Присвояване на етикети за още по-добра организация\n- 👥 Споделете с вашия екип, приятели или семейство\n- 📎Прикачете файлове и ги вградете във вашето описание за маркиране\n- 💬Обсъдете с вашия екип, като използвате коментари\n- ⚡ Проследявайте промените в потока от дейности\n- 🚀 Организирайте проекта си",
"This week" : "Тази седмица"
"Creating the new card…" : "Създаване на новата карта ...",
"\"{card}\" was added to \"{board}\"" : " \"{card}\" беше добавен към \"{board}\"",
"(circle)" : "(кръг)",
"This week" : "Тази седмица",
"Are you sure you want to transfer the board {title} for {user} ?" : "Сигурни ли сте че искате да прехвърлите таблото {title} на {user}?"
},
"nplurals=2; plural=(n != 1);");

View File

@@ -98,10 +98,12 @@
"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" : "Deck е инструмент за организация в стил kanban, насочен към лично планиране и организация на проекти за екипи, интегрирани с Nextcloud.\n\n\n- 📥 Добавете задачите си към карти и ги подредете\n- 📄 Запишете допълнителни бележки в markdown формат\n- Присвояване на етикети за още по-добра организация\n- 👥 Споделете с вашия екип, приятели или семейство\n- 📎Прикачете файлове и ги вградете във вашето описание за маркиране\n- 💬Обсъдете с вашия екип, като използвате коментари\n- ⚡ Проследявайте промените в потока от дейности\n- 🚀 Организирайте проекта си",
"Card details" : "Подробности за картата",
"Add board" : "Добави табло",
"Select the board to link to a project" : "Изберете таблото, което да свържете към проект",
@@ -166,8 +168,14 @@
"Can edit" : "Може да редактира",
"Can share" : "Може да споделя",
"Can manage" : "Може да управлява",
"Owner" : "Собственик",
"Delete" : "Изтриване",
"Failed to create share with {displayName}" : "Създаването на споделяне с {displayName} не бе успешно",
"Are you sure you want to transfer the board {title} for {user}?" : "Сигурни ли сте че искате да прехвърлите таблото {title} на {user}?",
"Transfer the board." : "Прехвърлете таблото.",
"Transfer" : "Прехвърляне",
"Transfer the board for {user} successfully" : "Успешно прехвърляне на таблото към {user} ",
"Failed to transfer the board for {user}" : "Неуспешно прехвърляне на таблото към {user}",
"Add a new list" : "Добавяне на нов списък",
"Archive all cards" : "Архивира всички карти",
"Delete list" : "Изтрива списък",
@@ -237,6 +245,7 @@
"Archive card" : "Архивиране на карта",
"Delete card" : "Изтриване на карта",
"Move card to another board" : "Преместване на картата на друго табло",
"List is empty" : "Списъкът е празен",
"Card deleted" : "Картата е изтрита",
"seconds ago" : "преди секунди",
"All boards" : "Всички табла",
@@ -282,6 +291,10 @@
"Share {file} with a Deck card" : "Споделяне {file} с Deck карта",
"Share" : "Споделяне",
"Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.\n\n\n- 📥 Add your tasks to cards and put them in order\n- 📄 Write down additional notes in markdown\n- 🔖 Assign labels for even better organization\n- 👥 Share with your team, friends or family\n- 📎 Attach files and embed them in your markdown description\n- 💬 Discuss with your team using comments\n- ⚡ Keep track of changes in the activity stream\n- 🚀 Get your project organized" : "Deck е инструмент за организация в стил kanban, насочен към лично планиране и организация на проекти за екипи, интегрирани с Nextcloud.\n\n\n- 📥 Добавете задачите си към карти и ги подредете\n- 📄 Запишете допълнителни бележки в markdown формат\n- Присвояване на етикети за още по-добра организация\n- 👥 Споделете с вашия екип, приятели или семейство\n- 📎Прикачете файлове и ги вградете във вашето описание за маркиране\n- 💬Обсъдете с вашия екип, като използвате коментари\n- ⚡ Проследявайте промените в потока от дейности\n- 🚀 Организирайте проекта си",
"This week" : "Тази седмица"
"Creating the new card…" : "Създаване на новата карта ...",
"\"{card}\" was added to \"{board}\"" : " \"{card}\" беше добавен към \"{board}\"",
"(circle)" : "(кръг)",
"This week" : "Тази седмица",
"Are you sure you want to transfer the board {title} for {user} ?" : "Сигурни ли сте че искате да прехвърлите таблото {title} на {user}?"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

View File

@@ -31,6 +31,7 @@ use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
@@ -49,15 +50,12 @@ use OCP\L10N\IFactory;
class ActivityManager {
public const DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED = 'DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED';
public const SUBJECT_PARAMS_MAX_LENGTH = 4000;
public const SHORTENED_DESCRIPTION_MAX_LENGTH = 2000;
private $manager;
private $userId;
private $permissionService;
private $boardMapper;
private $cardMapper;
private $attachmentMapper;
private $aclMapper;
private $stackMapper;
private $l10nFactory;
@@ -112,6 +110,7 @@ class ActivityManager {
BoardMapper $boardMapper,
CardMapper $cardMapper,
StackMapper $stackMapper,
AttachmentMapper $attachmentMapper,
AclMapper $aclMapper,
IFactory $l10nFactory,
$userId
@@ -121,6 +120,7 @@ class ActivityManager {
$this->boardMapper = $boardMapper;
$this->cardMapper = $cardMapper;
$this->stackMapper = $stackMapper;
$this->attachmentMapper = $attachmentMapper;
$this->aclMapper = $aclMapper;
$this->l10nFactory = $l10nFactory;
$this->userId = $userId;
@@ -249,6 +249,19 @@ class ActivityManager {
try {
$event = $this->createEvent($objectType, $entity, $subject, $additionalParams, $author);
if ($event !== null) {
$json = json_encode($event->getSubjectParameters());
if (mb_strlen($json) > 4000) {
$params = json_decode(json_encode($event->getSubjectParameters()), true);
$newContent = $params['after'];
unset($params['before'], $params['after'], $params['card']['description']);
$params['after'] = mb_substr($newContent, 0, 2000);
if (mb_strlen($newContent) > 2000) {
$params['after'] .= '...';
}
$event->setSubject($event->getSubject(), $params);
}
$this->sendToUsers($event);
}
} catch (\Exception $e) {
@@ -397,31 +410,12 @@ class ActivityManager {
$subjectParams['author'] = $author === null ? $this->userId : $author;
$subjectParams = array_merge($subjectParams, $additionalParams);
$json = json_encode($subjectParams);
if (mb_strlen($json) > self::SUBJECT_PARAMS_MAX_LENGTH) {
$params = json_decode(json_encode($subjectParams), true);
if ($subject === self::SUBJECT_CARD_UPDATE_DESCRIPTION && isset($params['after'])) {
$newContent = $params['after'];
unset($params['before'], $params['after'], $params['card']['description']);
$params['after'] = mb_substr($newContent, 0, self::SHORTENED_DESCRIPTION_MAX_LENGTH);
if (mb_strlen($newContent) > self::SHORTENED_DESCRIPTION_MAX_LENGTH) {
$params['after'] .= '...';
}
$subjectParams = $params;
} else {
throw new \Exception('Subject parameters too long');
}
}
$event = $this->manager->generateEvent();
$event->setApp('deck')
->setType($eventType)
->setAuthor($subjectParams['author'])
->setObject($objectType, (int)$object->getId(), $object->getTitle())
->setSubject($subject, $subjectParams)
->setSubject($subject, array_merge($subjectParams, $additionalParams))
->setTimestamp(time());
if ($message !== null) {

View File

@@ -69,7 +69,15 @@ class ChangeSet implements \JsonSerializable {
return $this->after;
}
public function jsonSerialize(): array {
/**
* Specify data which should be serialized to JSON
*
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize() {
return [
'before' => $this->getBefore(),
'after' => $this->getAfter(),

View File

@@ -1,92 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Command;
use OCA\Deck\Service\Importer\BoardImportCommandService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class BoardImport extends Command {
/** @var BoardImportCommandService */
private $boardImportCommandService;
public function __construct(
BoardImportCommandService $boardImportCommandService
) {
$this->boardImportCommandService = $boardImportCommandService;
parent::__construct();
}
/**
* @return void
*/
protected function configure() {
$allowedSystems = $this->boardImportCommandService->getAllowedImportSystems();
$names = array_column($allowedSystems, 'name');
$this
->setName('deck:import')
->setDescription('Import data')
->addOption(
'system',
null,
InputOption::VALUE_REQUIRED,
'Source system for import. Available options: ' . implode(', ', $names) . '.',
null
)
->addOption(
'config',
null,
InputOption::VALUE_REQUIRED,
'Configuration json file.',
'config.json'
)
->addOption(
'data',
null,
InputOption::VALUE_OPTIONAL,
'Data file to import.',
'data.json'
)
;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$this
->boardImportCommandService
->setInput($input)
->setOutput($output)
->setCommand($this)
->import();
$output->writeln('Done!');
return 0;
}
}

View File

@@ -1,85 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Controller;
use OCA\Deck\Service\Importer\BoardImportService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
class BoardImportApiController extends OCSController {
/** @var BoardImportService */
private $boardImportService;
/** @var string */
private $userId;
public function __construct(
string $appName,
IRequest $request,
BoardImportService $boardImportService,
string $userId
) {
parent::__construct($appName, $request);
$this->boardImportService = $boardImportService;
$this->userId = $userId;
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function import(string $system, array $config, array $data): DataResponse {
$this->boardImportService->setSystem($system);
$config = json_decode(json_encode($config));
$config->owner = $this->userId;
$this->boardImportService->setConfigInstance($config);
$this->boardImportService->setData(json_decode(json_encode($data)));
$this->boardImportService->import();
return new DataResponse($this->boardImportService->getBoard(), Http::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function getAllowedSystems(): DataResponse {
$allowedSystems = $this->boardImportService->getAllowedImportSystems();
return new DataResponse($allowedSystems, Http::STATUS_OK);
}
/**
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*/
public function getConfigSchema(string $name): DataResponse {
$this->boardImportService->setSystem($name);
$this->boardImportService->validateSystem();
$jsonSchemaPath = json_decode(file_get_contents($this->boardImportService->getJsonSchemaPath()));
return new DataResponse($jsonSchemaPath, Http::STATUS_OK);
}
}

View File

@@ -24,8 +24,7 @@
namespace OCA\Deck\Cron;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\Job;
use OC\BackgroundJob\Job;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\CardMapper;
@@ -36,8 +35,7 @@ class CardDescriptionActivity extends Job {
/** @var CardMapper */
private $cardMapper;
public function __construct(ITimeFactory $time, ActivityManager $activityManager, CardMapper $cardMapper) {
parent::__construct($time);
public function __construct(ActivityManager $activityManager, CardMapper $cardMapper) {
$this->activityManager = $activityManager;
$this->cardMapper = $cardMapper;
}

View File

@@ -24,15 +24,13 @@
namespace OCA\Deck\Cron;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OC\BackgroundJob\Job;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\Service\AttachmentService;
use OCP\BackgroundJob\IJob;
class DeleteCron extends TimedJob {
class DeleteCron extends Job {
/** @var BoardMapper */
private $boardMapper;
@@ -41,14 +39,10 @@ class DeleteCron extends TimedJob {
/** @var AttachmentMapper */
private $attachmentMapper;
public function __construct(ITimeFactory $time, BoardMapper $boardMapper, AttachmentService $attachmentService, AttachmentMapper $attachmentMapper) {
parent::__construct($time);
public function __construct(BoardMapper $boardMapper, AttachmentService $attachmentService, AttachmentMapper $attachmentMapper) {
$this->boardMapper = $boardMapper;
$this->attachmentService = $attachmentService;
$this->attachmentMapper = $attachmentMapper;
$this->setInterval(60 * 60 * 24);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
/**

View File

@@ -23,8 +23,7 @@
namespace OCA\Deck\Cron;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\Job;
use OC\BackgroundJob\Job;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Notification\NotificationHelper;
@@ -41,12 +40,10 @@ class ScheduledNotifications extends Job {
protected $logger;
public function __construct(
ITimeFactory $time,
CardMapper $cardMapper,
NotificationHelper $notificationHelper,
ILogger $logger
) {
parent::__construct($time);
$this->cardMapper = $cardMapper;
$this->notificationHelper = $notificationHelper;
$this->logger = $logger;

View File

@@ -58,7 +58,7 @@ class Board extends RelationalEntity {
$this->shared = -1;
}
public function jsonSerialize(): array {
public function jsonSerialize() {
$json = parent::jsonSerialize();
if ($this->shared === -1) {
unset($json['shared']);

View File

@@ -50,7 +50,7 @@ class Card extends RelationalEntity {
protected $deletedAt = 0;
protected $commentsUnread = 0;
protected $commentsCount = 0;
protected $relatedStack = null;
protected $relatedBoard = null;
@@ -78,7 +78,7 @@ class Card extends RelationalEntity {
$this->addRelation('commentsUnread');
$this->addRelation('commentsCount');
$this->addResolvable('owner');
$this->addRelation('relatedStack');
$this->addRelation('relatedBoard');
}
@@ -98,21 +98,22 @@ class Card extends RelationalEntity {
return $dt->format('c');
}
public function jsonSerialize(): array {
public function jsonSerialize() {
$json = parent::jsonSerialize();
$json['overdue'] = self::DUEDATE_FUTURE;
$due = $this->duedate ? strtotime($this->duedate) : false;
$due = strtotime($this->duedate);
$today = new DateTime();
$today->setTime(0, 0);
$match_date = new DateTime($this->duedate);
$match_date->setTime(0, 0);
$diff = $today->diff($match_date);
$diffDays = (integer) $diff->format('%R%a'); // Extract days count in interval
if ($due !== false) {
$today = new DateTime();
$today->setTime(0, 0);
$match_date = new DateTime($this->duedate);
$match_date->setTime(0, 0);
$diff = $today->diff($match_date);
$diffDays = (integer) $diff->format('%R%a'); // Extract days count in interval
if ($diffDays === 1) {
$json['overdue'] = self::DUEDATE_NEXT;
}

View File

@@ -265,7 +265,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function findOverdue() {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'duedate', 'notified')
$qb->select('id','title','duedate','notified')
->from('deck_cards')
->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()')))
->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
@@ -276,7 +276,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function findUnexposedDescriptionChances() {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'duedate', 'notified', 'description_prev', 'last_editor', 'description')
$qb->select('id','title','duedate','notified','description_prev','last_editor','description')
->from('deck_cards')
->where($qb->expr()->isNotNull('last_editor'))
->andWhere($qb->expr()->isNotNull('description_prev'));
@@ -551,7 +551,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
public function isOwner($userId, $cardId): bool {
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id IN (SELECT stack_id FROM `*PREFIX*deck_cards` WHERE id = ?))';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT, 0);
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch();
return ($row['owner'] === $userId);

View File

@@ -83,14 +83,14 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
public function deleteLabelAssignments($labelId) {
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE label_id = ?';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1, $labelId, \PDO::PARAM_INT, 0);
$stmt->bindParam(1, $labelId, \PDO::PARAM_INT);
$stmt->execute();
}
public function deleteLabelAssignmentsForCard($cardId) {
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE card_id = ?';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT, 0);
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT);
$stmt->execute();
}

View File

@@ -63,7 +63,7 @@ class RelationalEntity extends Entity implements \JsonSerializable {
* @return array serialized data
* @throws \ReflectionException
*/
public function jsonSerialize(): array {
public function jsonSerialize() {
$properties = get_object_vars($this);
$reflection = new \ReflectionClass($this);
$json = [];

View File

@@ -23,9 +23,7 @@
namespace OCA\Deck\Db;
use JsonSerializable;
class RelationalObject implements JsonSerializable {
class RelationalObject implements \JsonSerializable {
protected $primaryKey;
protected $object;
@@ -40,7 +38,7 @@ class RelationalObject implements JsonSerializable {
$this->object = $object;
}
public function jsonSerialize(): array {
public function jsonSerialize() {
return array_merge(
['primaryKey' => $this->primaryKey],
$this->getObjectSerialization()
@@ -53,8 +51,8 @@ class RelationalObject implements JsonSerializable {
* @throws \Exception
*/
public function getObjectSerialization() {
if ($this->object instanceof JsonSerializable) {
return $this->object->jsonSerialize();
if ($this->object instanceof \JsonSerializable) {
$this->object->jsonSerialize();
} else {
throw new \Exception('jsonSerialize is not implemented on ' . get_class($this));
}

View File

@@ -25,13 +25,6 @@ namespace OCA\Deck\Db;
use Sabre\VObject\Component\VCalendar;
/**
* @method int getId()
* @method int getBoardId()
* @method int getDeletedAt()
* @method int getLastModified()
* @method int getOrder()
*/
class Stack extends RelationalEntity {
protected $title;
protected $boardId;
@@ -52,7 +45,7 @@ class Stack extends RelationalEntity {
$this->cards = $cards;
}
public function jsonSerialize(): array {
public function jsonSerialize() {
$json = parent::jsonSerialize();
if (empty($this->cards)) {
unset($json['cards']);

View File

@@ -48,25 +48,6 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntity($sql, [$id]);
}
/**
* @param $cardId
* @return Stack|null
*/
public function findStackFromCardId($cardId): ?Stack {
$sql = <<<SQL
SELECT s.*
FROM `*PREFIX*deck_stacks` as `s`
INNER JOIN `*PREFIX*deck_cards` as `c` ON s.id = c.stack_id
WHERE c.id = ?
SQL;
try {
return $this->findEntity($sql, [$cardId]);
} catch (MultipleObjectsReturnedException|DoesNotExistException $e) {
}
return null;
}
public function findAll($boardId, $limit = null, $offset = null) {
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` WHERE `board_id` = ? AND deleted_at = 0 ORDER BY `order`, `id`';

View File

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

View File

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

View File

@@ -1,99 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Your name <your@email.com>
*
* @author Your name <your@email.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Migration;
use Closure;
use Doctrine\DBAL\Schema\SchemaException;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version10800Date20220422061816 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @throws SchemaException
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();
$indexAdded = $this->addIndex($schema,
'deck_boards',
'idx_owner_modified',
[ 'owner', 'last_modified' ]
);
$indexAdded = $this->addIndex($schema,
'deck_board_acl',
'idx_participant_type',
[ 'participant', 'type']
) || $indexAdded;
$indexAdded = $this->addIndex($schema,
'deck_cards',
'idx_due_notified_archived_deleted', [
'duedate', 'notified', 'archived', 'deleted_at'
],
) || $indexAdded;
$indexAdded = $this->addIndex($schema,
'deck_cards',
'idx_last_editor', [
'last_editor', 'description_prev'
], [],
// Adding a partial index on the description_prev as it is only used for a NULL check
['lengths' => [null, 1]]
) || $indexAdded;
$indexAdded = $this->addIndex($schema,
'deck_attachment',
'idx_cardid_deletedat',
[ 'card_id', 'deleted_at']
) || $indexAdded;
$indexAdded = $this->addIndex($schema,
'deck_assigned_users',
'idx_card_participant',
[ 'card_id', 'participant']
) || $indexAdded;
return $indexAdded ? $schema : null;
}
private function addIndex(ISchemaWrapper $schema, string $table, string $indexName, array $columns, array $flags = [], array $options = []): bool {
$table = $schema->getTable($table);
if (!$table->hasIndex($indexName)) {
$table->addIndex($columns, $indexName, $flags, $options);
return true;
}
return false;
}
}

View File

@@ -101,12 +101,15 @@ class Notifier implements INotifier {
switch ($notification->getSubject()) {
case 'card-assigned':
$cardId = $notification->getObjectId();
$stack = $this->stackMapper->findStackFromCardId($cardId);
$boardId = $stack ? $stack->getBoardId() : null;
$boardId = $this->cardMapper->findBoardId($cardId);
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$initiator = $this->userManager->get($params[2]);
if ($initiator !== null) {
$dn = $initiator->getDisplayName();
@@ -144,12 +147,15 @@ class Notifier implements INotifier {
break;
case 'card-overdue':
$cardId = $notification->getObjectId();
$stack = $this->stackMapper->findStackFromCardId($cardId);
$boardId = $stack ? $stack->getBoardId() : null;
$boardId = $this->cardMapper->findBoardId($cardId);
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$notification->setParsedSubject(
(string) $l->t('The card "%s" on "%s" has reached its due date.', $params)
);
@@ -176,12 +182,15 @@ class Notifier implements INotifier {
break;
case 'card-comment-mentioned':
$cardId = $notification->getObjectId();
$stack = $this->stackMapper->findStackFromCardId($cardId);
$boardId = $stack ? $stack->getBoardId() : null;
$boardId = $this->cardMapper->findBoardId($cardId);
if (!$boardId) {
throw new AlreadyProcessedException();
}
$card = $this->cardMapper->find($cardId);
$stackId = $card->getStackId();
$stack = $this->stackMapper->find($stackId);
$initiator = $this->userManager->get($params[2]);
if ($initiator !== null) {
$dn = $initiator->getDisplayName();

View File

@@ -33,6 +33,6 @@ use OCP\Search\SearchResultEntry;
class CardSearchResultEntry extends SearchResultEntry {
public function __construct(Board $board, Stack $stack, Card $card, $urlGenerator) {
parent::__construct('', $card->getTitle(), $board->getTitle() . ' » ' . $stack->getTitle(), $urlGenerator->linkToRouteAbsolute('deck.page.index') . '#/board/' . $board->getId() . '/card/' . $card->getId(), 'icon-deck');
parent::__construct('', $card->getTitle(), $board->getTitle() . ' » ' . $stack->getTitle() , $urlGenerator->linkToRouteAbsolute('deck.page.index') . '#/board/' . $board->getId() . '/card/' . $card->getId(), 'icon-deck');
}
}

View File

@@ -114,7 +114,7 @@ class CardService {
$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);
@@ -224,7 +224,7 @@ class CardService {
$card->setDescription($description);
$card->setDuedate($duedate);
$card = $this->cardMapper->insert($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE);
$this->changeHelper->cardChanged($card->getId(), false);
$this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card));
@@ -253,7 +253,7 @@ class CardService {
$card = $this->cardMapper->find($id);
$card->setDeletedAt(time());
$this->cardMapper->update($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
$this->notificationHelper->markDuedateAsRead($card);
$this->changeHelper->cardChanged($card->getId(), false);
@@ -338,7 +338,7 @@ class CardService {
$resetDuedateNotification = false;
if (
$card->getDuedate() === null ||
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate() ?? ''))
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate()))
) {
$card->setNotified(false);
$resetDuedateNotification = true;

View File

@@ -66,8 +66,7 @@ class ConfigService {
}
$data = [
'calendar' => $this->isCalendarEnabled(),
'cardDetailsInModal' => $this->isCardDetailsInModal(),
'calendar' => $this->isCalendarEnabled()
];
if ($this->groupManager->isAdmin($this->getUserId())) {
$data['groupLimit'] = $this->get('groupLimit');
@@ -89,11 +88,6 @@ class ConfigService {
return false;
}
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'calendar', true);
case 'cardDetailsInModal':
if ($this->getUserId() === null) {
return false;
}
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', true);
}
}
@@ -102,8 +96,7 @@ class ConfigService {
return false;
}
$appConfigState = $this->config->getAppValue(Application::APP_ID, 'calendar', 'yes') === 'yes';
$defaultState = (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'calendar', $appConfigState);
$defaultState = (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'calendar', true);
if ($boardId === null) {
return $defaultState;
}
@@ -111,19 +104,6 @@ class ConfigService {
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'board:' . $boardId . ':calendar', $defaultState);
}
public function isCardDetailsInModal(int $boardId = null): bool {
if ($this->getUserId() === null) {
return false;
}
$defaultState = (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', true);
if ($boardId === null) {
return $defaultState;
}
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'board:' . $boardId . ':cardDetailsInModal', $defaultState);
}
public function set($key, $value) {
if ($this->getUserId() === null) {
throw new NoPermissionException('Must be logged in to set user config');
@@ -142,10 +122,6 @@ class ConfigService {
$this->config->setUserValue($this->getUserId(), Application::APP_ID, 'calendar', (string)$value);
$result = $value;
break;
case 'cardDetailsInModal':
$this->config->setUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', (string)$value);
$result = $value;
break;
case 'board':
[$boardId, $boardConfigKey] = explode(':', $key);
if ($boardConfigKey === 'notify-due' && !in_array($value, [self::SETTING_BOARD_NOTIFICATION_DUE_ALL, self::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED, self::SETTING_BOARD_NOTIFICATION_DUE_OFF], true)) {

View File

@@ -86,7 +86,7 @@ class FileService implements IAttachmentService {
* @return ISimpleFolder
* @throws NotPermittedException
*/
public function getFolder(Attachment $attachment) {
private function getFolder(Attachment $attachment) {
$folderName = 'file-card-' . (int)$attachment->getCardId();
try {
$folder = $this->appData->getFolder($folderName);

View File

@@ -1,136 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCP\AppFramework\Db\Entity;
use OCP\Comments\IComment;
abstract class ABoardImportService {
/** @var string */
public static $name = '';
/** @var BoardImportService */
private $boardImportService;
/** @var bool */
protected $needValidateData = true;
/** @var Stack[] */
protected $stacks = [];
/** @var Label[] */
protected $labels = [];
/** @var Card[] */
protected $cards = [];
/** @var Acl[] */
protected $acls = [];
/** @var IComment[][] */
protected $comments = [];
/** @var Assignment[] */
protected $assignments = [];
/** @var string[][] */
protected $labelCardAssignments = [];
/**
* Configure import service
*
* @return void
*/
abstract public function bootstrap(): void;
abstract public function getBoard(): ?Board;
/**
* @return Acl[]
*/
abstract public function getAclList(): array;
/**
* @return Stack[]
*/
abstract public function getStacks(): array;
/**
* @return Card[]
*/
abstract public function getCards(): array;
abstract public function getCardAssignments(): array;
abstract public function getCardLabelAssignment(): array;
/**
* @return IComment[][]|array
*/
abstract public function getComments(): array;
/** @return Label[] */
abstract public function getLabels(): array;
abstract public function validateUsers(): void;
abstract public function getJsonSchemaPath(): string;
public function updateStack(string $id, Stack $stack): void {
$this->stacks[$id] = $stack;
}
public function updateCard(string $id, Card $card): void {
$this->cards[$id] = $card;
}
public function updateLabel(string $code, Label $label): void {
$this->labels[$code] = $label;
}
public function updateAcl(string $code, Acl $acl): void {
$this->acls[$code] = $acl;
}
public function updateComment(string $cardId, string $commentId, IComment $comment): void {
$this->comments[$cardId][$commentId] = $comment;
}
public function updateCardAssignment(string $cardId, string $assignmentId, Entity $assignment): void {
$this->assignments[$cardId][$assignmentId] = $assignment;
}
public function updateCardLabelsAssignment(string $cardId, string $assignmentId, string $assignment): void {
$this->labelCardAssignments[$cardId][$assignmentId] = $assignment;
}
public function setImportService(BoardImportService $service): void {
$this->boardImportService = $service;
}
public function getImportService(): BoardImportService {
return $this->boardImportService;
}
public function needValidateData(): bool {
return $this->needValidateData;
}
}

View File

@@ -1,199 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer;
use OCA\Deck\Exceptions\ConflictException;
use OCA\Deck\NotFoundException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
class BoardImportCommandService extends BoardImportService {
/**
* @var Command
* @psalm-suppress PropertyNotSetInConstructor
*/
private $command;
/**
* @var InputInterface
* @psalm-suppress PropertyNotSetInConstructor
*/
private $input;
/**
* @var OutputInterface
* @psalm-suppress PropertyNotSetInConstructor
*/
private $output;
public function setCommand(Command $command): self {
$this->command = $command;
return $this;
}
public function getCommand(): Command {
return $this->command;
}
public function setInput(InputInterface $input): self {
$this->input = $input;
return $this;
}
public function getInput(): InputInterface {
return $this->input;
}
public function setOutput(OutputInterface $output): self {
$this->output = $output;
return $this;
}
public function getOutput(): OutputInterface {
return $this->output;
}
protected function validateConfig(): void {
try {
$config = $this->getInput()->getOption('config');
if (is_string($config)) {
if (!is_file($config)) {
throw new NotFoundException('It\'s not a valid config file.');
}
$config = json_decode(file_get_contents($config));
if (!$config instanceof \stdClass) {
throw new NotFoundException('Failed to parse JSON.');
}
$this->setConfigInstance($config);
}
parent::validateConfig();
return;
} catch (NotFoundException $e) {
$this->getOutput()->writeln('<error>' . $e->getMessage() . '</error>');
$helper = $this->getCommand()->getHelper('question');
$question = new Question(
"<info>You can get more info on https://deck.readthedocs.io/en/latest/User_documentation_en/#6-import-boards</info>\n" .
'Please inform a valid config json file: ',
'config.json'
);
$question->setValidator(function (string $answer) {
if (!is_file($answer)) {
throw new \RuntimeException(
'config file not found'
);
}
return $answer;
});
$configFile = $helper->ask($this->getInput(), $this->getOutput(), $question);
$this->getInput()->setOption('config', $configFile);
} catch (ConflictException $e) {
$this->getOutput()->writeln('<error>Invalid config file</error>');
$this->getOutput()->writeln(array_map(function (array $v): string {
return $v['message'];
}, $e->getData()));
$this->getOutput()->writeln('Valid schema:');
$this->getOutput()->writeln(print_r(file_get_contents($this->getJsonSchemaPath()), true));
$this->getInput()->setOption('config', '');
}
$this->validateConfig();
}
public function validateSystem(): void {
try {
parent::validateSystem();
return;
} catch (\Throwable $th) {
}
$helper = $this->getCommand()->getHelper('question');
$allowedSystems = $this->getAllowedImportSystems();
$names = array_column($allowedSystems, 'name');
$question = new ChoiceQuestion(
'Please inform a source system',
$names,
0
);
$question->setErrorMessage('System %s is invalid.');
$selectedName = $helper->ask($this->getInput(), $this->getOutput(), $question);
$className = $allowedSystems[array_flip($names)[$selectedName]]['internalName'];
$this->setSystem($className);
return;
}
protected function validateData(): void {
if (!$this->getImportSystem()->needValidateData()) {
return;
}
$data = $this->getInput()->getOption('data');
if (is_string($data)) {
$data = json_decode(file_get_contents($data));
if ($data instanceof \stdClass) {
$this->setData($data);
return;
}
}
$helper = $this->getCommand()->getHelper('question');
$question = new Question(
'Please provide a valid data json file: ',
'data.json'
);
$question->setValidator(function (string $answer) {
if (!is_file($answer)) {
throw new \RuntimeException(
'Data file not found'
);
}
return $answer;
});
$data = $helper->ask($this->getInput(), $this->getOutput(), $question);
$this->getInput()->setOption('data', $data);
$this->validateData();
}
public function bootstrap(): void {
$this->setSystem($this->getInput()->getOption('system'));
parent::bootstrap();
}
public function import(): void {
$this->getOutput()->writeln('Starting import...');
$this->bootstrap();
$this->getOutput()->writeln('Importing board...');
$this->importBoard();
$this->getOutput()->writeln('Assign users to board...');
$this->importAcl();
$this->getOutput()->writeln('Importing labels...');
$this->importLabels();
$this->getOutput()->writeln('Importing stacks...');
$this->importStacks();
$this->getOutput()->writeln('Importing cards...');
$this->importCards();
$this->getOutput()->writeln('Assign cards to labels...');
$this->assignCardsToLabels();
$this->getOutput()->writeln('Importing comments...');
$this->importComments();
$this->getOutput()->writeln('Importing participants...');
$this->importCardAssignments();
}
}

View File

@@ -1,449 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Validator;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\BoardImportGetAllowedEvent;
use OCA\Deck\Exceptions\ConflictException;
use OCA\Deck\NotFoundException;
use OCA\Deck\Service\FileService;
use OCA\Deck\Service\Importer\Systems\TrelloApiService;
use OCA\Deck\Service\Importer\Systems\TrelloJsonService;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException as CommentNotFoundException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUserManager;
class BoardImportService {
/** @var IUserManager */
private $userManager;
/** @var BoardMapper */
private $boardMapper;
/** @var AclMapper */
private $aclMapper;
/** @var LabelMapper */
private $labelMapper;
/** @var StackMapper */
private $stackMapper;
/** @var CardMapper */
private $cardMapper;
/** @var AssignmentMapper */
private $assignmentMapper;
/** @var AttachmentMapper */
private $attachmentMapper;
/** @var ICommentsManager */
private $commentsManager;
/** @var IEventDispatcher */
private $eventDispatcher;
/** @var string */
private $system = '';
/** @var null|ABoardImportService */
private $systemInstance;
/** @var array */
private $allowedSystems = [];
/**
* Data object created from config JSON
*
* @var \stdClass
* @psalm-suppress PropertyNotSetInConstructor
*/
public $config;
/**
* Data object created from JSON of origin system
*
* @var \stdClass
* @psalm-suppress PropertyNotSetInConstructor
*/
private $data;
/**
* @var Board
*/
private $board;
public function __construct(
IUserManager $userManager,
BoardMapper $boardMapper,
AclMapper $aclMapper,
LabelMapper $labelMapper,
StackMapper $stackMapper,
AssignmentMapper $assignmentMapper,
AttachmentMapper $attachmentMapper,
CardMapper $cardMapper,
ICommentsManager $commentsManager,
IEventDispatcher $eventDispatcher
) {
$this->userManager = $userManager;
$this->boardMapper = $boardMapper;
$this->aclMapper = $aclMapper;
$this->labelMapper = $labelMapper;
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
$this->assignmentMapper = $assignmentMapper;
$this->attachmentMapper = $attachmentMapper;
$this->commentsManager = $commentsManager;
$this->eventDispatcher = $eventDispatcher;
$this->board = new Board();
$this->disableCommentsEvents();
}
private function disableCommentsEvents(): void {
if (defined('PHPUNIT_RUN')) {
return;
}
$propertyEventHandlers = new \ReflectionProperty($this->commentsManager, 'eventHandlers');
$propertyEventHandlers->setAccessible(true);
$propertyEventHandlers->setValue($this->commentsManager, []);
$propertyEventHandlerClosures = new \ReflectionProperty($this->commentsManager, 'eventHandlerClosures');
$propertyEventHandlerClosures->setAccessible(true);
$propertyEventHandlerClosures->setValue($this->commentsManager, []);
}
public function import(): void {
$this->bootstrap();
try {
$this->importBoard();
$this->importAcl();
$this->importLabels();
$this->importStacks();
$this->importCards();
$this->assignCardsToLabels();
$this->importComments();
$this->importCardAssignments();
} catch (\Throwable $th) {
throw new BadRequestException($th->getMessage());
}
}
public function validateSystem(): void {
$allowedSystems = $this->getAllowedImportSystems();
$allowedSystems = array_column($allowedSystems, 'internalName');
if (!in_array($this->getSystem(), $allowedSystems)) {
throw new NotFoundException('Invalid system');
}
}
/**
* @param mixed $system
* @return self
*/
public function setSystem($system): self {
$this->system = $system;
return $this;
}
public function getSystem(): string {
return $this->system;
}
public function addAllowedImportSystem($system): self {
$this->allowedSystems[] = $system;
return $this;
}
public function getAllowedImportSystems(): array {
if (!$this->allowedSystems) {
$this->addAllowedImportSystem([
'name' => TrelloApiService::$name,
'class' => TrelloApiService::class,
'internalName' => 'TrelloApi'
]);
$this->addAllowedImportSystem([
'name' => TrelloJsonService::$name,
'class' => TrelloJsonService::class,
'internalName' => 'TrelloJson'
]);
}
$this->eventDispatcher->dispatchTyped(new BoardImportGetAllowedEvent($this));
return $this->allowedSystems;
}
public function getImportSystem(): ABoardImportService {
if (!$this->getSystem()) {
throw new NotFoundException('System to import not found');
}
if (!is_object($this->systemInstance)) {
$systemClass = 'OCA\\Deck\\Service\\Importer\\Systems\\' . ucfirst($this->getSystem()) . 'Service';
$this->systemInstance = \OC::$server->get($systemClass);
$this->systemInstance->setImportService($this);
}
return $this->systemInstance;
}
public function setImportSystem(ABoardImportService $instance): void {
$this->systemInstance = $instance;
}
public function importBoard(): void {
$board = $this->getImportSystem()->getBoard();
if ($board) {
$this->boardMapper->insert($board);
$this->board = $board;
}
}
public function getBoard(bool $reset = false): Board {
if ($reset) {
$this->board = new Board();
}
return $this->board;
}
public function importAcl(): void {
$aclList = $this->getImportSystem()->getAclList();
foreach ($aclList as $code => $acl) {
$this->aclMapper->insert($acl);
$this->getImportSystem()->updateAcl($code, $acl);
}
$this->getBoard()->setAcl($aclList);
}
public function importLabels(): void {
$labels = $this->getImportSystem()->getLabels();
foreach ($labels as $code => $label) {
$this->labelMapper->insert($label);
$this->getImportSystem()->updateLabel($code, $label);
}
$this->getBoard()->setLabels($labels);
}
public function importStacks(): void {
$stacks = $this->getImportSystem()->getStacks();
foreach ($stacks as $code => $stack) {
$this->stackMapper->insert($stack);
$this->getImportSystem()->updateStack($code, $stack);
}
$this->getBoard()->setStacks(array_values($stacks));
}
public function importCards(): void {
$cards = $this->getImportSystem()->getCards();
foreach ($cards as $code => $card) {
$createdAt = $card->getCreatedAt();
$lastModified = $card->getLastModified();
$this->cardMapper->insert($card);
$updateDate = false;
if ($createdAt && $createdAt !== $card->getCreatedAt()) {
$card->setCreatedAt($createdAt);
$updateDate = true;
}
if ($lastModified && $lastModified !== $card->getLastModified()) {
$card->setLastModified($lastModified);
$updateDate = true;
}
if ($updateDate) {
$this->cardMapper->update($card, false);
}
$this->getImportSystem()->updateCard($code, $card);
}
}
/**
* @param mixed $cardId
* @param mixed $labelId
* @return self
*/
public function assignCardToLabel($cardId, $labelId): self {
$this->cardMapper->assignLabel(
$cardId,
$labelId
);
return $this;
}
public function assignCardsToLabels(): void {
$data = $this->getImportSystem()->getCardLabelAssignment();
foreach ($data as $cardId => $assignemnt) {
foreach ($assignemnt as $assignmentId => $labelId) {
$this->assignCardToLabel(
$cardId,
$labelId
);
$this->getImportSystem()->updateCardLabelsAssignment($cardId, $assignmentId, $labelId);
}
}
}
public function importComments(): void {
$allComments = $this->getImportSystem()->getComments();
foreach ($allComments as $cardId => $comments) {
foreach ($comments as $commentId => $comment) {
$this->insertComment($cardId, $comment);
$this->getImportSystem()->updateComment($cardId, $commentId, $comment);
}
}
}
private function insertComment(string $cardId, IComment $comment): void {
$comment->setObject('deckCard', $cardId);
$comment->setVerb('comment');
// Check if parent is a comment on the same card
if ($comment->getParentId() !== '0') {
try {
$parent = $this->commentsManager->get($comment->getParentId());
if ($parent->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $parent->getObjectId() !== $cardId) {
throw new CommentNotFoundException();
}
} catch (CommentNotFoundException $e) {
throw new BadRequestException('Invalid parent id: The parent comment was not found or belongs to a different card');
}
}
try {
$this->commentsManager->save($comment);
} catch (\InvalidArgumentException $e) {
throw new BadRequestException('Invalid input values');
} catch (CommentNotFoundException $e) {
throw new NotFoundException('Could not create comment.');
}
}
public function importCardAssignments(): void {
$allAssignments = $this->getImportSystem()->getCardAssignments();
foreach ($allAssignments as $cardId => $assignments) {
foreach ($assignments as $assignmentId => $assignment) {
$this->assignmentMapper->insert($assignment);
$this->getImportSystem()->updateCardAssignment($cardId, $assignmentId, $assignment);
}
}
}
public function insertAttachment(Attachment $attachment, string $content): Attachment {
$service = \OC::$server->get(FileService::class);
$folder = $service->getFolder($attachment);
if ($folder->fileExists($attachment->getData())) {
$attachment = $this->attachmentMapper->findByData($attachment->getCardId(), $attachment->getData());
throw new ConflictException('File already exists.', $attachment);
}
$target = $folder->newFile($attachment->getData());
$target->putContent($content);
$attachment = $this->attachmentMapper->insert($attachment);
$service->extendData($attachment);
return $attachment;
}
public function setData(\stdClass $data): void {
$this->data = $data;
}
public function getData(): \stdClass {
return $this->data;
}
/**
* Define a config
*
* @param string $configName
* @param mixed $value
* @return void
*/
public function setConfig(string $configName, $value): void {
if (empty((array) $this->config)) {
$this->setConfigInstance(new \stdClass);
}
$this->config->$configName = $value;
}
/**
* Get a config
*
* @param string $configName config name
* @return mixed
*/
public function getConfig(string $configName) {
if (!property_exists($this->config, $configName)) {
return;
}
return $this->config->$configName;
}
/**
* @param \stdClass $config
* @return self
*/
public function setConfigInstance($config): self {
$this->config = $config;
return $this;
}
public function getConfigInstance(): \stdClass {
return $this->config;
}
protected function validateConfig(): void {
$config = $this->getConfigInstance();
$schemaPath = $this->getJsonSchemaPath();
$validator = new Validator();
$newConfig = clone $config;
$validator->validate(
$newConfig,
(object)['$ref' => 'file://' . realpath($schemaPath)],
Constraint::CHECK_MODE_APPLY_DEFAULTS
);
if (!$validator->isValid()) {
throw new ConflictException('Invalid config file', $validator->getErrors());
}
$this->setConfigInstance($newConfig);
$this->validateOwner();
}
public function getJsonSchemaPath(): string {
return $this->getImportSystem()->getJsonSchemaPath();
}
public function validateOwner(): void {
$owner = $this->userManager->get($this->getConfig('owner'));
if (!$owner) {
throw new \LogicException('Owner "' . $this->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.');
}
$this->setConfig('owner', $owner);
}
protected function validateData(): void {
}
public function bootstrap(): void {
$this->validateSystem();
$this->validateConfig();
$this->validateData();
$this->getImportSystem()->bootstrap();
}
}

View File

@@ -1,214 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer\Systems;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class TrelloApiService extends TrelloJsonService {
/** @var string */
public static $name = 'Trello API';
protected $needValidateData = false;
/** @var IClient */
private $httpClient;
/** @var LoggerInterface */
protected $logger;
/** @var string */
private $baseApiUrl = 'https://api.trello.com/1';
/** @var ?\stdClass[] */
private $boards;
public function __construct(
IUserManager $userManager,
IURLGenerator $urlGenerator,
IL10N $l10n,
LoggerInterface $logger,
IClientService $httpClientService
) {
parent::__construct($userManager, $urlGenerator, $l10n);
$this->logger = $logger;
$this->httpClient = $httpClientService->newClient();
}
public function bootstrap(): void {
$this->populateBoard();
$this->populateMembers();
$this->populateLabels();
$this->populateLists();
$this->populateCheckLists();
$this->populateCards();
$this->populateActions();
parent::bootstrap();
}
public function getJsonSchemaPath(): string {
return implode(DIRECTORY_SEPARATOR, [
__DIR__,
'..',
'fixtures',
'config-trelloApi-schema.json',
]);
}
private function populateActions(): void {
$data = $this->getImportService()->getData();
$data->actions = $this->doRequest(
'/boards/' . $data->id . '/actions',
[
'filter' => 'commentCard,createCard',
'fields=memberCreator,type,data,date',
'memberCreator_fields' => 'username',
'limit' => 1000
]
);
}
private function populateCards(): void {
$data = $this->getImportService()->getData();
$data->cards = $this->doRequest(
'/boards/' . $data->id . '/cards',
[
'fields' => 'id,idMembers,dateLastActivity,closed,idChecklists,name,idList,pos,desc,due,labels',
'attachments' => true,
'attachment_fields' => 'name,url,date',
'limit' => 1000
]
);
}
private function populateCheckLists(): void {
$data = $this->getImportService()->getData();
$data->checklists = $this->doRequest(
'/boards/' . $data->id . '/checkLists',
[
'fields' => 'id,idCard,name',
'checkItem_fields' => 'id,state,name',
'limit' => 1000
]
);
}
private function populateLists(): void {
$data = $this->getImportService()->getData();
$data->lists = $this->doRequest(
'/boards/' . $data->id . '/lists',
[
'fields' => 'id,name,closed',
'limit' => 1000
]
);
}
private function populateLabels(): void {
$data = $this->getImportService()->getData();
$data->labels = $this->doRequest(
'/boards/' . $data->id . '/labels',
[
'fields' => 'id,color,name',
'limit' => 1000
]
);
}
private function populateMembers(): void {
$data = $this->getImportService()->getData();
$data->members = $this->doRequest(
'/boards/' . $data->id . '/members',
[
'fields' => 'username',
'limit' => 1000
]
);
}
private function populateBoard(): void {
$toImport = $this->getImportService()->getConfig('board');
$board = $this->doRequest(
'/boards/' . $toImport,
['fields' => 'id,name']
);
if ($board instanceof \stdClass) {
$this->getImportService()->setData($board);
return;
}
throw new \Exception('Invalid board id to import');
}
/**
* @return array|\stdClass
*/
private function doRequest(string $path = '', array $queryString = []) {
$target = $this->baseApiUrl . $path;
try {
$result = $this->httpClient
->get($target, $this->getQueryString($queryString))
->getBody();
if (is_string($result)) {
$data = json_decode($result);
if (is_array($data)) {
$data = array_merge(
$data,
$this->paginate($path, $queryString, $data)
);
}
return $data;
}
throw new \Exception('Invalid return of api');
} catch (\Throwable $e) {
$this->logger->critical(
$e->getMessage(),
['app' => 'deck']
);
throw new \Exception($e->getMessage());
}
}
private function paginate(string $path = '', array $queryString = [], array $data = []): array {
if (empty($queryString['limit'])) {
return [];
}
if (count($data) < $queryString['limit']) {
return [];
}
$queryString['before'] = end($data)->id;
$return = $this->doRequest($path, $queryString);
if (is_array($return)) {
return $return;
}
throw new \Exception('Invalid return of api');
}
private function getQueryString(array $params = []): array {
$apiSettings = $this->getImportService()->getConfig('api');
$params['key'] = $apiSettings->key;
$params['token'] = $apiSettings->token;
return [
'query' => $params
];
}
}

View File

@@ -1,400 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer\Systems;
use OC\Comments\Comment;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Service\Importer\ABoardImportService;
use OCP\Comments\IComment;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
class TrelloJsonService extends ABoardImportService {
/** @var string */
public static $name = 'Trello JSON';
/** @var IUserManager */
private $userManager;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IL10N */
private $l10n;
/** @var IUser[] */
private $members = [];
public function __construct(
IUserManager $userManager,
IURLGenerator $urlGenerator,
IL10N $l10n
) {
$this->userManager = $userManager;
$this->urlGenerator = $urlGenerator;
$this->l10n = $l10n;
}
public function bootstrap(): void {
$this->validateUsers();
}
public function getJsonSchemaPath(): string {
return implode(DIRECTORY_SEPARATOR, [
__DIR__,
'..',
'fixtures',
'config-trelloJson-schema.json',
]);
}
public function validateUsers(): void {
if (empty($this->getImportService()->getConfig('uidRelation'))) {
return;
}
foreach ($this->getImportService()->getConfig('uidRelation') as $trelloUid => $nextcloudUid) {
$user = array_filter($this->getImportService()->getData()->members, function (\stdClass $u) use ($trelloUid) {
return $u->username === $trelloUid;
});
if (!$user) {
throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data');
}
if (!is_string($nextcloudUid) && !is_numeric($nextcloudUid)) {
throw new \LogicException('User on setting uidRelation is invalid');
}
$nextcloudUid = (string) $nextcloudUid;
$this->getImportService()->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid);
if (!$this->getImportService()->getConfig('uidRelation')->$trelloUid) {
throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid);
}
$user = current($user);
$this->members[$user->id] = $this->getImportService()->getConfig('uidRelation')->$trelloUid;
}
}
public function getCardAssignments(): array {
$assignments = [];
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
foreach ($trelloCard->idMembers as $idMember) {
if (empty($this->members[$idMember])) {
continue;
}
$assignment = new Assignment();
$assignment->setCardId($this->cards[$trelloCard->id]->getId());
$assignment->setParticipant($this->members[$idMember]->getUID());
$assignment->setType(Assignment::TYPE_USER);
$assignments[$trelloCard->id][] = $assignment;
}
}
return $assignments;
}
public function getComments(): array {
$comments = [];
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
$values = array_filter(
$this->getImportService()->getData()->actions,
function (\stdClass $a) use ($trelloCard) {
return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id;
}
);
$keys = array_map(function (\stdClass $c): string {
return $c->id;
}, $values);
$trelloComments = array_combine($keys, $values);
$trelloComments = $this->sortComments($trelloComments);
foreach ($trelloComments as $commentId => $trelloComment) {
$cardId = $this->cards[$trelloCard->id]->getId();
$comment = new Comment();
if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) {
$actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID();
} else {
$actor = $this->getImportService()->getConfig('owner')->getUID();
}
$message = $this->replaceUsernames($trelloComment->data->text);
if (mb_strlen($message, 'UTF-8') > IComment::MAX_MESSAGE_LENGTH) {
$attachment = new Attachment();
$attachment->setCardId($cardId);
$attachment->setType('deck_file');
$attachment->setCreatedBy($actor);
$attachment->setLastModified(time());
$attachment->setCreatedAt(time());
$attachment->setData('comment_' . $commentId . '.md');
$attachment = $this->getImportService()->insertAttachment($attachment, $message);
$urlToDownloadAttachment = $this->urlGenerator->linkToRouteAbsolute(
'deck.attachment.display',
[
'cardId' => $cardId,
'attachmentId' => $attachment->getId()
]
);
$message = $this->l10n->t(
"This comment has more than %s characters.\n" .
"Added as an attachment to the card with name %s.\n" .
"Accessible on URL: %s.",
[
IComment::MAX_MESSAGE_LENGTH,
'comment_' . $commentId . '.md',
$urlToDownloadAttachment
]
);
}
$comment
->setActor('users', $actor)
->setMessage($message)
->setCreationDateTime(
\DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date)
);
$comments[$cardId][$commentId] = $comment;
}
}
return $comments;
}
private function sortComments(array $comments): array {
$comparison = function (\stdClass $a, \stdClass $b): int {
if ($a->date == $b->date) {
return 0;
}
return ($a->date < $b->date) ? -1 : 1;
};
usort($comments, $comparison);
return $comments;
}
public function getCardLabelAssignment(): array {
$cardsLabels = [];
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
foreach ($trelloCard->labels as $label) {
$cardId = $this->cards[$trelloCard->id]->getId();
$labelId = $this->labels[$label->id]->getId();
$cardsLabels[$cardId][] = $labelId;
}
}
return $cardsLabels;
}
public function getBoard(): Board {
$board = $this->getImportService()->getBoard();
if (empty($this->getImportService()->getData()->name)) {
throw new BadRequestException('Invalid name of board');
}
$board->setTitle($this->getImportService()->getData()->name);
$board->setOwner($this->getImportService()->getConfig('owner')->getUID());
$board->setColor($this->getImportService()->getConfig('color'));
return $board;
}
/**
* @return Label[]
*/
public function getLabels(): array {
foreach ($this->getImportService()->getData()->labels as $trelloLabel) {
$label = new Label();
if (empty($trelloLabel->name)) {
$label->setTitle('Unnamed ' . $trelloLabel->color . ' label');
} else {
$label->setTitle($trelloLabel->name);
}
$label->setColor($this->translateColor($trelloLabel->color));
$label->setBoardId($this->getImportService()->getBoard()->getId());
$this->labels[$trelloLabel->id] = $label;
}
return $this->labels;
}
/**
* @return Stack[]
*/
public function getStacks(): array {
$return = [];
foreach ($this->getImportService()->getData()->lists as $order => $list) {
$stack = new Stack();
if ($list->closed) {
$stack->setDeletedAt(time());
}
$stack->setTitle($list->name);
$stack->setBoardId($this->getImportService()->getBoard()->getId());
$stack->setOrder($order + 1);
$return[$list->id] = $stack;
}
return $return;
}
/**
* @return Card[]
*/
public function getCards(): array {
$checklists = [];
foreach ($this->getImportService()->getData()->checklists as $checklist) {
$checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist);
}
$this->getImportService()->getData()->checklists = $checklists;
$cards = [];
foreach ($this->getImportService()->getData()->cards as $trelloCard) {
$card = new Card();
$lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity);
$card->setLastModified($lastModified->format('Y-m-d H:i:s'));
if ($trelloCard->closed) {
$card->setArchived(true);
}
if ((count($trelloCard->idChecklists) !== 0)) {
foreach ($this->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) {
$trelloCard->desc .= "\n" . $checklist;
}
}
$this->appendAttachmentsToDescription($trelloCard);
$card->setTitle($trelloCard->name);
$card->setStackId($this->stacks[$trelloCard->idList]->getId());
$cardsOnStack = $this->stacks[$trelloCard->idList]->getCards();
$cardsOnStack[] = $card;
$this->stacks[$trelloCard->idList]->setCards($cardsOnStack);
$card->setType('plain');
$card->setOrder($trelloCard->pos);
$card->setOwner($this->getImportService()->getConfig('owner')->getUID());
$lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity);
$card->setLastModified($lastModified->format('U'));
$createCardDate = array_filter(
$this->getImportService()->getData()->actions,
function (\stdClass $a) use ($trelloCard) {
return $a->type === 'createCard' && $a->data->card->id === $trelloCard->id;
}
);
$createCardDate = current($createCardDate);
$createCardDate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $createCardDate->date);
if ($createCardDate) {
$card->setCreatedAt($createCardDate->format('U'));
} else {
$card->setCreatedAt($lastModified->format('U'));
}
$card->setDescription($trelloCard->desc);
if ($trelloCard->due) {
$duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due)
->format('Y-m-d H:i:s');
$card->setDuedate($duedate);
}
$cards[$trelloCard->id] = $card;
}
return $cards;
}
/**
* @return Acl[]
*/
public function getAclList(): array {
$return = [];
foreach ($this->members as $member) {
if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) {
continue;
}
$acl = new Acl();
$acl->setBoardId($this->getImportService()->getBoard()->getId());
$acl->setType(Acl::PERMISSION_TYPE_USER);
$acl->setParticipant($member->getUID());
$acl->setPermissionEdit(false);
$acl->setPermissionShare(false);
$acl->setPermissionManage(false);
$return[] = $acl;
}
return $return;
}
private function translateColor(string $color): string {
switch ($color) {
case 'red':
return 'ff0000';
case 'yellow':
return 'ffff00';
case 'orange':
return 'ff6600';
case 'green':
return '00ff00';
case 'purple':
return '9900ff';
case 'blue':
return '0000ff';
case 'sky':
return '00ccff';
case 'lime':
return '00ff99';
case 'pink':
return 'ff66cc';
case 'black':
return '000000';
default:
return 'ffffff';
}
}
private function replaceUsernames(string $text): string {
foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) {
$text = str_replace($trello, $nextcloud->getUID(), $text);
}
return $text;
}
private function checklistItem(\stdClass $item): string {
if (($item->state == 'incomplete')) {
$string_start = '- [ ]';
} else {
$string_start = '- [x]';
}
$check_item_string = $string_start . ' ' . $item->name . "\n";
return $check_item_string;
}
private function formulateChecklistText(\stdClass $checklist): string {
$checklist_string = "\n\n## {$checklist->name}\n";
foreach ($checklist->checkItems as $item) {
$checklist_item_string = $this->checklistItem($item);
$checklist_string = $checklist_string . "\n" . $checklist_item_string;
}
return $checklist_string;
}
private function appendAttachmentsToDescription(\stdClass $trelloCard): void {
if (empty($trelloCard->attachments)) {
return;
}
$trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n";
$trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n";
$trelloCard->desc .= "|---|---\n";
foreach ($trelloCard->attachments as $attachment) {
$name = mb_strlen($attachment->name, 'UTF-8') ? $attachment->name : $attachment->url;
$trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n";
}
}
}

View File

@@ -1,41 +0,0 @@
{
"type": "object",
"properties": {
"api": {
"type": "object",
"properties": {
"key": {
"type": "string",
"pattern": "^[0-9a-fA-F]{32}$"
},
"token": {
"type": "string",
"pattern": "^[0-9a-fA-F]{64}$"
}
}
},
"board": {
"type": "string",
"pattern": "^\\w{1,}$"
},
"uidRelation": {
"type": "object",
"comment": "Relationship between Trello and Nextcloud usernames",
"example": {
"johndoe": "admin"
}
},
"owner": {
"type": "string",
"required": true,
"comment": "Nextcloud owner username"
},
"color": {
"type": "string",
"required": true,
"pattern": "^[0-9a-fA-F]{6}$",
"comment": "Default color for the board. If you don't inform, the default color will be used.",
"default": "0800fd"
}
}
}

View File

@@ -1,24 +0,0 @@
{
"type": "object",
"properties": {
"uidRelation": {
"type": "object",
"comment": "Relationship between Trello and Nextcloud usernames",
"example": {
"johndoe": "admin"
}
},
"owner": {
"type": "string",
"required": true,
"comment": "Nextcloud owner username"
},
"color": {
"type": "string",
"required": true,
"pattern": "^[0-9a-fA-F]{6}$",
"comment": "Default color for the board. If you don't inform, the default color will be used.",
"default": "0800fd"
}
}
}

View File

@@ -138,7 +138,7 @@ class OverviewService {
private function findAllBoardsFromUser(string $userId): array {
$userInfo = $this->getBoardPrerequisites($userId);
$userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null);
$groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'], null, null);
$groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null);
$circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null);
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
}

View File

@@ -181,7 +181,6 @@ class StackService {
if (array_key_exists($card->id, $labels)) {
$cards[$cardIndex]->setLabels($labels[$card->id]);
}
$cards[$cardIndex]->setAttachmentCount($this->attachmentService->count($card->getId()));
}
$stacks[$stackIndex]->setCards($cards);
}

View File

@@ -26,6 +26,7 @@ declare(strict_types=1);
namespace OCA\Deck\Sharing;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use OC\Files\Cache\Cache;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Board;
@@ -500,7 +501,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
);
}
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
$qb->orderBy('s.id');
@@ -710,6 +711,13 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
}
$qb = $this->dbConnection->getQueryBuilder();
// Avoid using implicit cast in order to make use of the index in the join on MySQL/MariaDB
// FIXME: Once >= Nextcloud 24 this can be dropped due to https://github.com/nextcloud/server/pull/30471
if ($this->dbConnection->getDatabasePlatform() instanceof MySQLPlatform) {
$cardIdExpression = $qb->createFunction('CAST(dc.id as CHAR)');
} else {
$cardIdExpression = $qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR);
}
$qb->select('s.*',
'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
@@ -720,7 +728,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->orderBy('s.id')
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR), 's.share_with'))
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($cardIdExpression, 's.share_with'))
->leftJoin('dc', 'deck_stacks', 'ds', $qb->expr()->eq('dc.stack_id', 'ds.id'))
->leftJoin('ds', 'deck_boards', 'db', $qb->expr()->eq('ds.board_id', 'db.id'));
@@ -781,6 +789,13 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
$shares = [];
$qb = $this->dbConnection->getQueryBuilder();
// Avoid using implicit cast in order to make use of the index in the join on MySQL/MariaDB
// FIXME: Once >= Nextcloud 24 this can be dropped due to https://github.com/nextcloud/server/pull/30471
if ($this->dbConnection->getDatabasePlatform() instanceof MySQLPlatform) {
$cardIdExpression = $qb->createFunction('CAST(dc.id as CHAR)');
} else {
$cardIdExpression = $qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR);
}
$qb->select('s.*',
'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
@@ -791,7 +806,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
->orderBy('s.id')
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR), 's.share_with'));
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($cardIdExpression, 's.share_with'));
if ($limit !== -1) {
$qb->setMaxResults($limit);

View File

@@ -31,7 +31,7 @@ namespace OCA\Deck;
*/
class StatusException extends \Exception {
public function __construct($message) {
parent::__construct($message ?? '');
parent::__construct($message);
}
public function getStatus() {

View File

@@ -11,6 +11,4 @@ pages:
- Nextcloud API: API-Nextcloud.md
- Developer documentation:
- Data structure: structure.md
- Import documentation:
- Implement import: implement-import.md
- Class diagram: import-class-diagram.md

8460
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "deck",
"description": "",
"version": "1.7.0-beta.1",
"version": "1.6.1",
"authors": [
{
"name": "Julius Härtl",
@@ -29,26 +29,26 @@
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.17.9",
"@babel/runtime": "^7.16.0",
"@juliushaertl/vue-richtext": "^1.0.1",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.9.0",
"@nextcloud/axios": "^1.7.0",
"@nextcloud/dialogs": "^3.1.2",
"@nextcloud/event-bus": "^2.1.1",
"@nextcloud/files": "^2.1.0",
"@nextcloud/initial-state": "^1.2.1",
"@nextcloud/l10n": "^1.4.1",
"@nextcloud/moment": "^1.2.0",
"@nextcloud/moment": "^1.1.1",
"@nextcloud/router": "^2.0.0",
"@nextcloud/vue": "^5.3.1",
"@nextcloud/vue": "^4.2.0",
"@nextcloud/vue-dashboard": "^2.0.1",
"blueimp-md5": "^2.19.0",
"dompurify": "^2.3.6",
"dompurify": "^2.3.3",
"lodash": "^4.17.21",
"markdown-it": "^12.3.2",
"markdown-it": "^12.2.0",
"markdown-it-task-lists": "^2.1.1",
"markdown-it-link-attributes": "^4.0.0",
"moment": "^2.29.2",
"markdown-it-link-attributes": "^3.0.0",
"moment": "^2.29.1",
"nextcloud-vue-collections": "^0.9.0",
"p-queue": "^6.6.2",
"url-search-params-polyfill": "^8.1.1",
@@ -66,18 +66,18 @@
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": "^14.0.0",
"npm": "^7.0.0"
"node": ">=14.0.0",
"npm": ">=7.0.0"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.2.0",
"@nextcloud/eslint-config": "^6.1.2",
"@nextcloud/stylelint-config": "^2.1.2",
"@nextcloud/webpack-vue-config": "^5.0.0",
"@relative-ci/agent": "^3.1.2",
"@vue/test-utils": "^1.3.0",
"jest": "^27.5.1",
"@nextcloud/eslint-config": "^6.1.0",
"@nextcloud/stylelint-config": "^1.0.0-beta.0",
"@nextcloud/webpack-vue-config": "^4.1.2",
"@relative-ci/agent": "^3.0.0",
"@vue/test-utils": "^1.2.2",
"jest": "^27.3.1",
"jest-serializer-vue": "^2.0.2",
"vue-jest": "^3.0.7"
},
@@ -97,4 +97,4 @@
"<rootDir>/node_modules/jest-serializer-vue"
]
}
}
}

View File

@@ -39,6 +39,7 @@
<referencedClass name="OC\*" />
<referencedClass name="OC" />
<referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" />
<referencedClass name="Doctrine\DBAL\Platforms\MySQLPlatform" />
</errorLevel>
</UndefinedClass>
<UndefinedDocblockClass>
@@ -48,6 +49,8 @@
<referencedClass name="Doctrine\DBAL\Schema\SchemaException" />
<referencedClass name="Doctrine\DBAL\Driver\Statement" />
<referencedClass name="Doctrine\DBAL\Schema\Table" />
<referencedClass name="Doctrine\DBAL\Platforms\AbstractPlatform" />
<referencedClass name="Doctrine\DBAL\Platforms\MySQLPlatform" />
<referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" />
</errorLevel>
</UndefinedDocblockClass>

View File

@@ -31,7 +31,6 @@
v-if="cardDetailsInModal && $route.params.cardId"
:clear-view-delay="0"
:title="t('deck', 'Card details')"
size="large"
@close="hideModal()">
<div class="modal__content modal__card">
<router-view name="sidebar" />
@@ -89,6 +88,7 @@ export default {
navShown: state => state.navShown,
sidebarShownState: state => state.sidebarShown,
currentBoard: state => state.currentBoard,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
// TODO: properly handle sidebar showing for route subview and board sidebar
sidebarRouterView() {
@@ -98,14 +98,6 @@ export default {
sidebarShown() {
return this.sidebarRouterView || this.sidebarShownState
},
cardDetailsInModal: {
get() {
return this.$store.getters.config('cardDetailsInModal')
},
set(newValue) {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
},
},
},
created() {
this.$store.dispatch('loadBoards')
@@ -155,6 +147,14 @@ export default {
}
}
}
.modal__card {
min-width: 320px;
width: 50vw;
max-width: 800px;
min-height: 200px;
height: 80vh;
}
</style>
<style lang="scss">

View File

@@ -75,7 +75,7 @@ export default {
const subject = this.activity.subject_rich[0]
const parameters = JSON.parse(JSON.stringify(this.activity.subject_rich[1]))
if (parameters.after && typeof parameters.after.id === 'string' && parameters.after.id.startsWith('dt:')) {
const dateTime = parameters.after.id.slice(3)
const dateTime = parameters.after.id.substr(3)
parameters.after.name = moment(dateTime).format('L LTS')
}

View File

@@ -239,7 +239,6 @@ export default {
isAddStackVisible: false,
filter: { tags: [], users: [], due: '', unassigned: false },
showAddCardModal: false,
defaultPageTitle: false,
}
},
@@ -267,17 +266,11 @@ export default {
return [...this.board.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
},
},
beforeDestroy() {
this.setPageTitle('')
},
watch: {
board(current, previous) {
if (current?.id !== previous?.id) {
this.clearFilter()
}
if (current) {
this.setPageTitle(current.title)
}
},
},
methods: {
@@ -337,22 +330,6 @@ export default {
clickHideAddCardModel() {
this.showAddCardModal = false
},
setPageTitle(title) {
if (this.defaultPageTitle === false) {
this.defaultPageTitle = window.document.title
if (this.defaultPageTitle.indexOf(' - Deck - ') !== -1) {
this.defaultPageTitle = this.defaultPageTitle.substring(this.defaultPageTitle.indexOf(' - Deck - ') + 3)
}
if (this.defaultPageTitle.indexOf('Deck - ') !== 0) {
this.defaultPageTitle = 'Deck - ' + this.defaultPageTitle
}
}
let newTitle = this.defaultPageTitle
if (title !== '') {
newTitle = `${title} - ${newTitle}`
}
window.document.title = newTitle
},
},
}
</script>

View File

@@ -45,6 +45,7 @@ export default {
#app-sidebar .icon-close {
z-index: 100;
}
.app-deck .app-sidebar {
z-index: 20000 !important;
}

View File

@@ -77,7 +77,6 @@ import Controls from '../Controls'
import Stack from './Stack'
import { EmptyContent } from '@nextcloud/vue'
import GlobalSearchResults from '../search/GlobalSearchResults'
import { showError } from '../../helpers/errors'
export default {
name: 'Board',
@@ -140,7 +139,6 @@ export default {
await this.$store.dispatch('loadStacks', this.id)
} catch (e) {
console.error(e)
showError(e)
}
this.loading = false
},

View File

@@ -200,7 +200,7 @@ export default {
},
clickTransferOwner(newOwner) {
OC.dialogs.confirmDestructive(
t('deck', 'Are you sure you want to transfer the board {title} for {user}?', { title: this.board.title, user: newOwner }),
t('deck', 'Are you sure you want to transfer the board {title} for {user} ?', { title: this.board.title, user: newOwner }),
t('deck', 'Transfer the board.'),
{
type: OC.dialogs.YES_NO_BUTTONS,
@@ -214,7 +214,7 @@ export default {
this.isLoading = true
await this.$store.dispatch('transferOwnership', {
boardId: this.board.id,
newOwner
newOwner,
})
const successMessage = t('deck', 'Transfer the board for {user} successfully', { user: newOwner })
showSuccess(successMessage)

View File

@@ -162,6 +162,7 @@ export default {
]),
...mapState({
showArchived: state => state.showArchived,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
cardsByStack() {
return this.$store.getters.cardsByStack(this.stack.id).filter((card) => {
@@ -174,14 +175,6 @@ export default {
dragHandleSelector() {
return this.canEdit ? null : '.no-drag'
},
cardDetailsInModal: {
get() {
return this.$store.getters.config('cardDetailsInModal')
},
set(newValue) {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
},
},
},
methods: {

View File

@@ -25,14 +25,13 @@
:active="tabId"
:title="title"
:subtitle="subtitle"
:subtitle-tooltip="subtitleTooltip"
:title-editable="titleEditable"
@update:titleEditable="handleUpdateTitleEditable"
@update:title="handleUpdateTitle"
@submit-title="handleSubmitTitle"
@close="closeSidebar">
<template #secondary-actions>
<ActionButton v-if="cardDetailsInModal" icon="icon-menu-sidebar" @click.stop="closeModal()">
<ActionButton v-if="cardDetailsInModal" icon="icon-menu-sidebar" @click.stop="showModal()">
{{ t('deck', 'Open in sidebar view') }}
</ActionButton>
<ActionButton v-else icon="icon-external" @click.stop="showModal()">
@@ -89,10 +88,8 @@ import CardSidebarTabAttachments from './CardSidebarTabAttachments'
import CardSidebarTabComments from './CardSidebarTabComments'
import CardSidebarTabActivity from './CardSidebarTabActivity'
import relativeDate from '../../mixins/relativeDate'
import moment from '@nextcloud/moment'
import { showError } from '@nextcloud/dialogs'
import { getLocale } from '@nextcloud/l10n'
const capabilities = window.OC.getCapabilities()
@@ -129,12 +126,12 @@ export default {
titleEditable: false,
titleEditing: '',
hasActivity: capabilities && capabilities.activity,
locale: getLocale(),
}
},
computed: {
...mapState({
currentBoard: state => state.currentBoard,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
...mapGetters(['canEdit', 'assignables', 'cardActions', 'stackById']),
title() {
@@ -146,9 +143,6 @@ export default {
subtitle() {
return t('deck', 'Modified') + ': ' + this.relativeDate(this.currentCard.lastModified * 1000) + ' ' + t('deck', 'Created') + ': ' + this.relativeDate(this.currentCard.createdAt * 1000)
},
subtitleTooltip() {
return t('deck', 'Modified') + ': ' + this.formatDate(this.currentCard.lastModified) + '\n' + t('deck', 'Created') + ': ' + this.formatDate(this.currentCard.createdAt)
},
cardRichObject() {
return {
id: '' + this.currentCard.id,
@@ -158,14 +152,6 @@ export default {
link: window.location.protocol + '//' + window.location.host + generateUrl('/apps/deck/') + `#/board/${this.currentBoard.id}/card/${this.currentCard.id}`,
}
},
cardDetailsInModal: {
get() {
return this.$store.getters.config('cardDetailsInModal')
},
set(newValue) {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
},
},
},
methods: {
handleUpdateTitleEditable(value) {
@@ -191,13 +177,7 @@ export default {
},
showModal() {
this.$store.dispatch('setConfig', { cardDetailsInModal: true })
},
closeModal() {
this.$store.dispatch('setConfig', { cardDetailsInModal: false })
},
formatDate(timestamp) {
return moment.unix(timestamp).locale(this.locale).format('LLLL')
this.$store.dispatch('setCardDetailsInModal', true)
},
},
}
@@ -225,8 +205,6 @@ export default {
max-width: calc(100% - #{$modal-padding*2});
padding: 0 14px;
max-height: 100%;
overflow: initial;
&::v-deep {
.app-sidebar-header {
position: sticky;
@@ -242,10 +220,6 @@ export default {
background-color: var(--color-main-background);
}
.app-sidebar__tab {
overflow: initial;
}
#emptycontent, .emptycontent {
margin-top: 88px;
}

View File

@@ -224,6 +224,7 @@ export default {
computed: {
...mapState({
currentBoard: state => state.currentBoard,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
...mapGetters(['canEdit', 'assignables']),
formatedAssignables() {
@@ -249,14 +250,6 @@ export default {
return assignable
})
},
cardDetailsInModal: {
get() {
return this.$store.getters.config('cardDetailsInModal')
},
set(newValue) {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
},
},
duedate: {
get() {
return this.card.duedate ? new Date(this.card.duedate) : null

View File

@@ -132,6 +132,7 @@ export default {
computed: {
...mapState({
currentBoard: state => state.currentBoard,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
...mapGetters(['canEdit']),
attachments() {
@@ -260,8 +261,6 @@ export default {
#description-preview {
min-height: 100px;
width: auto;
overflow-x: auto;
&::v-deep {
@import './../../css/markdown';
@@ -332,15 +331,4 @@ h5 {
#app-sidebar .app-sidebar-header__desc h4 {
font-size: 12px !important;
}
.vue-easymde .cm-s-easymde .cm-link {
color: var(--color-main-text);
}
.vue-easymde .cm-s-easymde .cm-string.cm-url,
.vue-easymde .cm-s-easymde .cm-formatting.cm-link,
.vue-easymde .cm-s-easymde .cm-formatting.cm-url,
.vue-easymde .cm-s-easymde .cm-formatting.cm-image {
color: var(--color-text-maxcontrast);
}
</style>

View File

@@ -136,10 +136,6 @@ export default {
activeBoards() {
return this.$store.getters.boards.filter((item) => item.deletedAt === 0 && item.archived === false)
},
boardId() {
return this.card?.boardId ? this.card.boardId : this.$route.params.id
}
},
methods: {
openCard() {

View File

@@ -144,10 +144,10 @@ export default {
},
cardDetailsInModal: {
get() {
return this.$store.getters.config('cardDetailsInModal')
return this.$store.getters.cardDetailsInModal
},
set(newValue) {
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
this.$store.dispatch('setCardDetailsInModal', newValue)
},
},
configCalendar: {

View File

@@ -53,8 +53,8 @@
</div>
<div class="dashboard-column">
<h3>{{ t('deck', 'Next 7 days') }}</h3>
<div v-for="card in cardsByDueDate.nextSevenDays" :key="card.id">
<h3>{{ t('deck', 'This week') }}</h3>
<div v-for="card in cardsByDueDate.thisWeek" :key="card.id">
<CardItem :id="card.id" />
</div>
</div>
@@ -160,7 +160,7 @@ export default {
overdue: [],
today: [],
tomorrow: [],
nextSevenDays: [],
thisWeek: [],
later: [],
}
dataset.forEach(card => {
@@ -180,7 +180,7 @@ export default {
all.tomorrow.push(card)
}
if (hours >= (48 - currentHour) && hours < (24 * 7)) {
all.nextSevenDays.push(card)
all.thisWeek.push(card)
}
if (hours >= (24 * 7)) {
all.later.push(card)

View File

@@ -1,27 +0,0 @@
import { showError as errorDialog } from '@nextcloud/dialogs'
const showAxiosError = err => {
const response = err?.response || {}
const message = response?.data.message
if (message) {
errorDialog(message)
return
}
errorDialog(err.message)
}
const showError = err => {
// axios error
if (err.response) {
showAxiosError(err)
return
}
errorDialog(err.message)
}
export {
showError,
}

View File

@@ -26,8 +26,8 @@ import { generateUrl } from '@nextcloud/router'
subscribe('calendar:handle-todo-click', ({ calendarId, taskId }) => {
const deckAppPrefix = 'app-generated--deck--board-'
if (calendarId.startsWith(deckAppPrefix)) {
const board = calendarId.slice(deckAppPrefix.length)
const card = taskId.slice('card-'.length).replace('.ics', '')
const board = calendarId.substr(deckAppPrefix.length)
const card = taskId.substr('card-'.length).replace('.ics', '')
console.debug('[deck] Clicked task matches deck calendar pattern')
window.location = generateUrl(`apps/deck/#/board/${board}/card/${card}`)
}

View File

@@ -44,33 +44,14 @@ window.addEventListener('DOMContentLoaded', () => {
window.OCA.Talk.registerMessageAction({
label: t('deck', 'Create a card'),
icon: 'icon-deck',
async callback({ message: { message, messageParameters, actorDisplayName }, metadata: { name: conversationName, token: conversationToken } }) {
const parsedMessage = message.replace(/{[a-z0-9-_]+}/gi, function(parameter) {
const parameterName = parameter.substr(1, parameter.length - 2)
if (messageParameters[parameterName]) {
if (messageParameters[parameterName].type === 'file' && messageParameters[parameterName].path) {
return messageParameters[parameterName].path
}
if (messageParameters[parameterName].type === 'user' || messageParameters[parameterName].type === 'call') {
return '@' + messageParameters[parameterName].name
}
if (messageParameters[parameterName].name) {
return messageParameters[parameterName].name
}
}
// Do not replace so insert with curly braces again
return parameter
})
const shortenedMessageCandidate = parsedMessage.replace(/^(.{255}[^\s]*).*/, '$1')
const shortenedMessage = shortenedMessageCandidate === '' ? parsedMessage.substr(0, 255) : shortenedMessageCandidate
async callback({ message: { message, actorDisplayName }, metadata: { name: conversationName, token: conversationToken } }) {
const shortenedMessageCandidate = message.replace(/^(.{255}[^\s]*).*/, '$1')
const shortenedMessage = shortenedMessageCandidate === '' ? message.substr(0, 255) : shortenedMessageCandidate
try {
await buildSelector(CardCreateDialog, {
props: {
title: shortenedMessage,
description: parsedMessage + '\n\n' + '['
description: message + '\n\n' + '['
+ t('deck', 'Message from {author} in {conversationName}', {
author: actorDisplayName,
conversationName,

View File

@@ -92,7 +92,7 @@ export default {
const filterOutQuotes = (q) => {
if (q[0] === '"' && q[q.length - 1] === '"') {
return q.slice(1, -1)
return q.substr(1, q.length - 2)
}
return q
}
@@ -153,7 +153,7 @@ export default {
const comparator = query[0] + (query[1] === '=' ? '=' : '')
const isValidComparator = ['<', '<=', '>', '>='].indexOf(comparator) !== -1
const parsedCardDate = moment(card.duedate)
const parsedDate = moment(query.slice(isValidComparator ? comparator.length : 0))
const parsedDate = moment(query.substr(isValidComparator ? comparator.length : 0))
switch (comparator) {
case '<':
hasMatch = hasMatch && parsedCardDate.isBefore(parsedDate)

View File

@@ -62,6 +62,7 @@ export default new Vuex.Store({
showArchived: false,
navShown: localStorage.getItem('deck.navShown') === 'true',
compactMode: localStorage.getItem('deck.compactMode') === 'true',
cardDetailsInModal: localStorage.getItem('deck.cardDetailsInModal') === 'true',
sidebarShown: false,
currentBoard: null,
currentCard: null,
@@ -78,6 +79,9 @@ export default new Vuex.Store({
config: state => (key) => {
return state.config[key]
},
cardDetailsInModal: state => {
return state.cardDetailsInModal
},
getSearchQuery: state => {
return state.searchQuery
},
@@ -229,6 +233,10 @@ export default new Vuex.Store({
state.compactMode = !state.compactMode
localStorage.setItem('deck.compactMode', state.compactMode)
},
setCardDetailsInModal(state) {
state.cardDetailsInModal = !state.cardDetailsInModal
localStorage.setItem('deck.cardDetailsInModal', state.cardDetailsInModal)
},
setBoards(state, boards) {
state.boards = boards
},
@@ -433,6 +441,9 @@ export default new Vuex.Store({
toggleCompactMode({ commit }) {
commit('toggleCompactMode')
},
setCardDetailsInModal({ commit }, show) {
commit('setCardDetailsInModal', show)
},
setCurrentBoard({ commit }, board) {
commit('setCurrentBoard', board)
},

View File

@@ -1,7 +0,0 @@
{
"owner": "admin",
"color": "0800fd",
"uidRelation": {
"johndoe": "johndoe"
}
}

View File

@@ -1,582 +0,0 @@
{
"id": "fakeboardidhash",
"name": "Test Board Name",
"desc": "",
"descData": null,
"closed": false,
"dateClosed": null,
"idOrganization": null,
"shortLink": "qwerty",
"powerUps": [],
"dateLastActivity": "2021-07-10T17:01:58.633Z",
"idTags": [],
"datePluginDisable": null,
"creationMethod": null,
"idBoardSource": null,
"idMemberCreator": "fakeidmemberhash",
"idEnterprise": null,
"pinned": false,
"starred": false,
"url": "https://trello.com/b/qwerty/fakeboardurl",
"prefs": {
"permissionLevel": "private",
"hideVotes": false,
"voting": "disabled",
"comments": "members",
"invitations": "members",
"selfJoin": false,
"cardCovers": true,
"isTemplate": false,
"cardAging": "regular",
"calendarFeedEnabled": false,
"background": "blue",
"backgroundImage": null,
"backgroundImageScaled": null,
"backgroundTile": false,
"backgroundBrightness": "dark",
"backgroundColor": "#0079BF",
"backgroundBottomColor": "#0079BF",
"backgroundTopColor": "#0079BF",
"canBePublic": true,
"canBeEnterprise": true,
"canBeOrg": true,
"canBePrivate": true,
"canInvite": true
},
"shortUrl": "https://trello.com/b/qwerty",
"premiumFeatures": [],
"enterpriseOwned": false,
"ixUpdate": "67",
"limits": {
"attachments": {
"perBoard": {
"status": "ok",
"disableAt": 36000,
"warnAt": 32400
},
"perCard": {
"status": "ok",
"disableAt": 1000,
"warnAt": 900
}
},
"boards": {
"totalMembersPerBoard": {
"status": "ok",
"disableAt": 1600,
"warnAt": 1440
}
},
"cards": {
"openPerBoard": {
"status": "ok",
"disableAt": 5000,
"warnAt": 4500
},
"openPerList": {
"status": "ok",
"disableAt": 5000,
"warnAt": 4500
},
"totalPerBoard": {
"status": "ok",
"disableAt": 2000000,
"warnAt": 1800000
},
"totalPerList": {
"status": "ok",
"disableAt": 1000000,
"warnAt": 900000
}
},
"checklists": {
"perBoard": {
"status": "ok",
"disableAt": 2000000,
"warnAt": 1800000
},
"perCard": {
"status": "ok",
"disableAt": 500,
"warnAt": 450
}
},
"checkItems": {
"perChecklist": {
"status": "ok",
"disableAt": 200,
"warnAt": 180
}
},
"customFields": {
"perBoard": {
"status": "ok",
"disableAt": 50,
"warnAt": 45
}
},
"customFieldOptions": {
"perField": {
"status": "ok",
"disableAt": 50,
"warnAt": 45
}
},
"labels": {
"perBoard": {
"status": "ok",
"disableAt": 1000,
"warnAt": 900
}
},
"lists": {
"openPerBoard": {
"status": "ok",
"disableAt": 500,
"warnAt": 450
},
"totalPerBoard": {
"status": "ok",
"disableAt": 3000,
"warnAt": 2700
}
},
"stickers": {
"perCard": {
"status": "ok",
"disableAt": 70,
"warnAt": 63
}
},
"reactions": {
"perAction": {
"status": "ok",
"disableAt": 1000,
"warnAt": 900
},
"uniquePerAction": {
"status": "ok",
"disableAt": 17,
"warnAt": 16
}
}
},
"subscribed": false,
"templateGallery": null,
"dateLastView": "2021-07-10T17:01:58.665Z",
"labelNames": {
"green": "",
"yellow": "",
"orange": "",
"red": "",
"purple": "",
"blue": "",
"sky": "",
"lime": "",
"pink": "",
"black": ""
},
"actions": [
{
"id": "60e9d2869efe2e1141be2798",
"idMemberCreator": "fakeidmemberhash",
"data": {
"idMember": "fakeidmemberhash",
"deactivated": false,
"card": {
"id": "hashcard7",
"name": "Name Card 7",
"idShort": 7,
"shortLink": "fakeshortlinkcard7"
},
"board": {
"id": "fakeboardidhash",
"name": "Test Board Name",
"shortLink": "qwerty"
},
"member": {
"id": "fakeidmemberhash",
"name": "John Doe"
}
},
"type": "removeMemberFromCard",
"date": "2021-07-10T17:01:58.636Z",
"appCreator": null,
"limits": {},
"member": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
},
"memberCreator": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
}
},
{
"id": "60e9d1832ff82d10c0cea6ba",
"idMemberCreator": "fakeidmemberhash",
"data": {
"idMember": "fakeidmemberhash",
"card": {
"id": "hashcard7",
"name": "Name Card 7",
"idShort": 7,
"shortLink": "fakeshortlinkcard7"
},
"board": {
"id": "fakeboardidhash",
"name": "Test Board Name",
"shortLink": "qwerty"
},
"member": {
"id": "fakeidmemberhash",
"name": "John Doe"
}
},
"type": "addMemberToCard",
"date": "2021-07-10T16:57:39.999Z",
"appCreator": null,
"limits": {},
"member": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
},
"memberCreator": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
}
},
{
"id": "59bbfc4bf36aa0270d6bfd43",
"idMemberCreator": "fakeidmemberhash",
"data": {
"board": {
"shortLink": "qwerty",
"name": "Test Board Name",
"id": "fakeboardidhash"
},
"list": {
"name": "TODO",
"id": "hashlisttodo"
},
"card": {
"shortLink": "fakeshortlinkcard7",
"idShort": 7,
"name": "Name Card 7",
"id": "hashcard7"
}
},
"type": "createCard",
"date": "2017-09-15T16:14:03.187Z",
"appCreator": null,
"limits": {},
"memberCreator": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
}
},
{
"id": "59bbfb8e4a6f8ca35be9b82a",
"idMemberCreator": "fakeidmemberhash",
"data": {
"board": {
"shortLink": "qwerty",
"name": "Test Board Name",
"id": "fakeboardidhash"
},
"list": {
"name": "TODO",
"id": "hashlisttodo"
}
},
"type": "createList",
"date": "2017-09-15T16:10:54.714Z",
"appCreator": null,
"limits": {},
"memberCreator": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
}
},
{
"id": "59bbfb88973b76e586edec5e",
"idMemberCreator": "fakeidmemberhash",
"data": {
"board": {
"shortLink": "qwerty",
"name": "Test Board Name",
"id": "fakeboardidhash"
}
},
"type": "createBoard",
"date": "2017-09-15T16:10:48.069Z",
"appCreator": null,
"limits": {},
"memberCreator": {
"id": "fakeidmemberhash",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idMemberReferrer": null,
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true
}
}
],
"cards": [
{
"id": "hashcard7",
"address": null,
"checkItemStates": null,
"closed": false,
"coordinates": null,
"creationMethod": null,
"dateLastActivity": "2021-07-10T17:01:58.633Z",
"desc": "",
"descData": null,
"dueReminder": null,
"idBoard": "fakeboardidhash",
"idLabels": [],
"idList": "hashlisttodo",
"idMembersVoted": [],
"idShort": 7,
"idAttachmentCover": null,
"locationName": null,
"manualCoverAttachment": false,
"name": "Name Card 7",
"pos": 65535,
"shortLink": "fakeshortlinkcard7",
"isTemplate": false,
"cardRole": null,
"badges": {
"attachmentsByType": {
"trello": {
"board": 0,
"card": 0
}
},
"location": false,
"votes": 0,
"viewingMemberVoted": false,
"subscribed": false,
"fogbugz": "",
"checkItems": 0,
"checkItemsChecked": 0,
"checkItemsEarliestDue": null,
"comments": 0,
"attachments": 0,
"description": false,
"due": null,
"dueComplete": false,
"start": null
},
"dueComplete": false,
"due": null,
"email": "johndoe+card7@boards.trello.com",
"idChecklists": [],
"idMembers": [],
"labels": [],
"limits": {
"attachments": {
"perCard": {
"status": "ok",
"disableAt": 1000,
"warnAt": 900
}
},
"checklists": {
"perCard": {
"status": "ok",
"disableAt": 500,
"warnAt": 450
}
},
"stickers": {
"perCard": {
"status": "ok",
"disableAt": 70,
"warnAt": 63
}
}
},
"shortUrl": "https://trello.com/c/fakeshortlinkcard7",
"start": null,
"subscribed": false,
"url": "https://trello.com/c/fakeshortlinkcard7/7-name-card-7",
"cover": {
"idAttachment": null,
"color": null,
"idUploadedBackground": null,
"size": "normal",
"brightness": "dark",
"idPlugin": null
},
"attachments": [],
"pluginData": [],
"customFieldItems": []
}
],
"labels": [
{
"id": "59bbfb881314a339999eb855",
"idBoard": "fakeboardidhash",
"name": "",
"color": "yellow"
}
],
"lists": [
{
"id": "hashlisttodo",
"name": "TODO",
"closed": false,
"pos": 65535,
"softLimit": null,
"creationMethod": null,
"idBoard": "fakeboardidhash",
"limits": {
"cards": {
"openPerList": {
"status": "ok",
"disableAt": 5000,
"warnAt": 4500
},
"totalPerList": {
"status": "ok",
"disableAt": 1000000,
"warnAt": 900000
}
}
},
"subscribed": false
}
],
"members": [
{
"id": "fakeidmemberhash",
"bio": "",
"bioData": {
"emoji": {}
},
"confirmed": true,
"memberType": "normal",
"username": "johndoe",
"activityBlocked": false,
"avatarHash": "fakeavatarhash",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"fullName": "John Doe",
"idEnterprise": null,
"idEnterprisesDeactivated": [],
"idMemberReferrer": null,
"idPremOrgsAdmin": [],
"initials": "JD",
"nonPublic": {
"fullName": "John Doe",
"initials": "JD",
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
"avatarHash": "fakeavatarhash"
},
"nonPublicAvailable": true,
"products": [],
"url": "https://trello.com/johndoe",
"status": "disconnected"
}
],
"checklists": [],
"customFields": [],
"memberships": [
{
"id": "59bbfb88973b76e586edec5d",
"idMember": "fakeidmemberhash",
"memberType": "admin",
"unconfirmed": false,
"deactivated": false
}
],
"pluginData": []
}

View File

@@ -1,11 +1,11 @@
{
"require-dev": {
"phpunit/phpunit": "~9",
"behat/behat": "~3.10.0",
"guzzlehttp/guzzle": "7.4.2",
"phpunit/phpunit": "~6.5",
"behat/behat": "~3.8.0",
"guzzlehttp/guzzle": "6.5.2",
"jarnaiz/behat-junit-formatter": "^1.3",
"sabre/dav": "4.3.1",
"symfony/event-dispatcher": "~5.4"
"sabre/dav": "3.2.3",
"symfony/event-dispatcher": "~4.4"
},
"autoload": {
"psr-0": {

View File

@@ -70,7 +70,7 @@ class BoardDatabaseTest extends \Test\TestCase {
$board->setOwner(self::TEST_USER1);
$board->setColor('000000');
$board->setLabels([]);
$created = $this->boardService->create('Test', self::TEST_USER1, '000000');
$created = $this->boardService->create('Test', self::TEST_USER1, '000000');
$id = $created->getId();
$actual = $this->boardService->find($id);
$this->assertEquals($actual->getTitle(), $board->getTitle());

View File

@@ -1,16 +1,15 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="bootstrap.php" colors="true" convertDeprecationsToExceptions="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">./../lib</directory>
</include>
</coverage>
<testsuites>
<testsuite name="integration-database">
<directory>./integration/database</directory>
</testsuite>
<testsuite name="integration-app">
<directory>./integration/app</directory>
</testsuite>
</testsuites>
<phpunit bootstrap="bootstrap.php" colors="true">
<testsuites>
<testsuite name="integration-database">
<directory>./integration/database</directory>
</testsuite>
<testsuite name="integration-app">
<directory>./integration/app</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./../lib</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,13 +1,12 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="../../../tests/bootstrap.php" colors="true" convertDeprecationsToExceptions="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">./../lib</directory>
</include>
</coverage>
<testsuites>
<testsuite name="unit">
<directory>./unit</directory>
</testsuite>
</testsuites>
<phpunit bootstrap="../../../tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="unit">
<directory>./unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./../lib</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.22.0@fc2c6ab4d5fa5d644d8617089f012f3bb84b8703">
<files psalm-version="4.16.1@aa7e400908833b10c0333861f86cd48c510b60eb">
<file src="lib/Activity/ActivityManager.php">
<TypeDoesNotContainType occurrences="1">
<code>$message !== null</code>
@@ -10,6 +10,17 @@
<code>(int)$subjectParams['comment']</code>
</InvalidScalarArgument>
</file>
<file src="lib/AppInfo/Application.php">
<InvalidArgument occurrences="7">
<code>registerEventListener</code>
<code>registerEventListener</code>
<code>registerEventListener</code>
<code>registerEventListener</code>
<code>registerEventListener</code>
<code>registerEventListener</code>
<code>registerEventListener</code>
</InvalidArgument>
</file>
<file src="lib/Command/UserExport.php">
<ImplementedReturnTypeMismatch occurrences="1">
<code>void</code>
@@ -38,6 +49,11 @@
<code>$this-&gt;userId</code>
</UndefinedThisPropertyFetch>
</file>
<file src="lib/Controller/BoardController.php">
<UndefinedDocblockClass occurrences="1">
<code>\OCP\Deck\DB\Board</code>
</UndefinedDocblockClass>
</file>
<file src="lib/Controller/CommentsApiController.php">
<InvalidScalarArgument occurrences="6">
<code>$cardId</code>
@@ -91,6 +107,11 @@
<code>$cardId</code>
</ParamNameMismatch>
</file>
<file src="lib/Db/AttachmentMapper.php">
<UndefinedVariable occurrences="1">
<code>$query</code>
</UndefinedVariable>
</file>
<file src="lib/Db/BoardMapper.php">
<ParamNameMismatch occurrences="1">
<code>$boardId</code>
@@ -163,11 +184,6 @@
<code>$stackId</code>
</ParamNameMismatch>
</file>
<file src="lib/Migration/Version10800Date20220422061816.php">
<MoreSpecificImplementedParamType occurrences="1">
<code>$schemaClosure</code>
</MoreSpecificImplementedParamType>
</file>
<file src="lib/Notification/Notifier.php">
<RedundantCast occurrences="4">
<code>(string) $l-&gt;t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])</code>

View File

@@ -26,6 +26,7 @@ namespace OCA\Deck\Activity;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
@@ -40,7 +41,7 @@ use OCP\IL10N;
use OCP\IUser;
use OCP\L10N\IFactory;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
class ActivityManagerTest extends TestCase {
@@ -56,6 +57,8 @@ class ActivityManagerTest extends TestCase {
private $cardMapper;
/** @var StackMapper|MockObject */
private $stackMapper;
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var AclMapper|MockObject */
private $aclMapper;
/** @var IFactory|MockObject */
@@ -71,6 +74,7 @@ class ActivityManagerTest extends TestCase {
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->stackMapper = $this->createMock(StackMapper::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->aclMapper = $this->createMock(AclMapper::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->l10n = $this->createMock(IL10N::class);
@@ -80,6 +84,7 @@ class ActivityManagerTest extends TestCase {
$this->boardMapper,
$this->cardMapper,
$this->stackMapper,
$this->attachmentMapper,
$this->aclMapper,
$this->l10nFactory,
$this->userId
@@ -88,7 +93,7 @@ class ActivityManagerTest extends TestCase {
public function testGetActivityFormatOwn() {
$managerClass = new \ReflectionClass(ActivityManager::class);
$this->l10n->expects(self::any())
$this->l10n->expects($this->any())
->method('t')
->will($this->returnCallback(function ($s) {
return $s;
@@ -103,41 +108,36 @@ class ActivityManagerTest extends TestCase {
if ($format !== '') {
$this->assertStringContainsString('{user}', $format);
} else {
self::addWarning('No activity string found for '. $constant);
/** @noinspection ForgottenDebugOutputInspection */
print_r('No activity string found for '. $constant . PHP_EOL);
}
$format = $this->activityManager->getActivityFormat('cz', $value, [], true);
if ($format !== '') {
$this->assertStringStartsWith('You', $format);
} else {
self::addWarning('No own activity string found for '. $constant);
/** @noinspection ForgottenDebugOutputInspection */
print_r('No own activity string found for '. $constant . PHP_EOL);
}
}
}
}
private function expectEventCreation($subject, $subjectParams) {
$event = $this->createMock(IEvent::class);
$this->manager->expects(self::once())
->method('generateEvent')
->willReturn($event);
$event->expects(self::once())->method('setApp')->willReturn($event);
$event->expects(self::once())->method('setType')->willReturn($event);
$event->expects(self::once())->method('setAuthor')->willReturn($event);
$event->expects(self::once())->method('setObject')->willReturn($event);
$event->expects(self::once())->method('setSubject')->with($subject, $subjectParams)->willReturn($event);
$event->expects(self::once())->method('setTimestamp')->willReturn($event);
return $event;
}
public function testCreateEvent() {
$board = new Board();
$board->setTitle('');
$this->boardMapper->expects(self::once())
$this->boardMapper->expects($this->once())
->method('find')
->willReturn($board);
$event = $this->expectEventCreation(ActivityManager::SUBJECT_BOARD_CREATE, [
'author' => 'admin'
]);
$event = $this->createMock(IEvent::class);
$this->manager->expects($this->once())
->method('generateEvent')
->willReturn($event);
$event->expects($this->once())->method('setApp')->willReturn($event);
$event->expects($this->once())->method('setType')->willReturn($event);
$event->expects($this->once())->method('setAuthor')->willReturn($event);
$event->expects($this->once())->method('setObject')->willReturn($event);
$event->expects($this->once())->method('setSubject')->willReturn($event);
$event->expects($this->once())->method('setTimestamp')->willReturn($event);
$actual = $this->invokePrivate($this->activityManager, 'createEvent', [
ActivityManager::DECK_OBJECT_BOARD,
$board,
@@ -146,133 +146,6 @@ class ActivityManagerTest extends TestCase {
$this->assertEquals($event, $actual);
}
public function testCreateEventDescription() {
$board = new Board();
$board->setTitle('');
$this->boardMapper->expects(self::once())
->method('find')
->willReturn($board);
$card = Card::fromRow([
'id' => 123,
'title' => 'My card',
'description' => str_repeat('A', 1000),
]);
$this->cardMapper->expects(self::any())
->method('find')
->willReturn($card);
$stack = Stack::fromRow([]);
$this->stackMapper->expects(self::any())
->method('find')
->willReturn($stack);
$expectedCard = $card->jsonSerialize();
unset($expectedCard['description']);
$event = $this->expectEventCreation(ActivityManager::SUBJECT_CARD_UPDATE_DESCRIPTION, [
'card' => $expectedCard,
'stack' => $stack->jsonSerialize(),
'board' => $board->jsonSerialize(),
'diff' => true,
'author' => 'admin',
'after' => str_repeat('C', 2000),
]);
$actual = $this->invokePrivate($this->activityManager, 'createEvent', [
ActivityManager::DECK_OBJECT_CARD,
$card,
ActivityManager::SUBJECT_CARD_UPDATE_DESCRIPTION,
[
'before' => str_repeat('B', 2000),
'after' => str_repeat('C', 2000)
],
]);
$this->assertEquals($event, $actual);
}
public function testCreateEventLongDescription() {
$board = new Board();
$board->setTitle('');
$this->boardMapper->expects(self::once())
->method('find')
->willReturn($board);
$card = new Card();
$card->setDescription(str_repeat('A', 5000));
$card->setTitle('My card');
$card->setId(123);
$this->cardMapper->expects(self::any())
->method('find')
->willReturn($card);
$stack = new Stack();
$this->stackMapper->expects(self::any())
->method('find')
->willReturn($stack);
$expectedCard = $card->jsonSerialize();
unset($expectedCard['description']);
$event = $this->expectEventCreation(ActivityManager::SUBJECT_CARD_UPDATE_DESCRIPTION, [
'card' => $expectedCard,
'stack' => $stack->jsonSerialize(),
'board' => $board->jsonSerialize(),
'diff' => true,
'author' => 'admin',
'after' => str_repeat('C', 2000) . '...',
]);
$actual = $this->invokePrivate($this->activityManager, 'createEvent', [
ActivityManager::DECK_OBJECT_CARD,
$card,
ActivityManager::SUBJECT_CARD_UPDATE_DESCRIPTION,
[
'before' => str_repeat('B', 5000),
'after' => str_repeat('C', 5000)
],
]);
$this->assertEquals($event, $actual);
}
public function testCreateEventLabel() {
$board = Board::fromRow([
'title' => 'My board'
]);
$this->boardMapper->expects(self::once())
->method('find')
->willReturn($board);
$card = Card::fromParams([]);
$card->setDescription(str_repeat('A', 5000));
$card->setTitle('My card');
$card->setId(123);
$this->cardMapper->expects(self::any())
->method('find')
->willReturn($card);
$stack = Stack::fromParams([]);
$this->stackMapper->expects(self::any())
->method('find')
->willReturn($stack);
$event = $this->expectEventCreation(ActivityManager::SUBJECT_CARD_UPDATE_TITLE, [
'card' => [
'id' => 123,
'title' => 'My card',
'archived' => false,
],
'stack' => $stack,
'board' => $board,
'author' => 'admin',
]);
$actual = $this->invokePrivate($this->activityManager, 'createEvent', [
ActivityManager::DECK_OBJECT_CARD,
$card,
ActivityManager::SUBJECT_CARD_UPDATE_TITLE
]);
$this->assertEquals($event, $actual);
}
public function dataSendToUsers() {
return [
[ActivityManager::DECK_OBJECT_BOARD],
@@ -282,7 +155,7 @@ class ActivityManagerTest extends TestCase {
private function mockUser($uid) {
$user = $this->createMock(IUser::class);
$user->expects(self::any())
$user->expects($this->any())
->method('getUID')
->willReturn($uid);
return $user;
@@ -296,21 +169,15 @@ class ActivityManagerTest extends TestCase {
$this->mockUser('user2'),
];
$event = $this->createMock(IEvent::class);
$event->expects(self::once())
$event->expects($this->once())
->method('getObjectType')
->willReturn($objectType);
$event->expects(self::once())
$event->expects($this->once())
->method('getObjectId')
->willReturn(1);
$event->expects(self::exactly(2))
$event->expects($this->exactly(2))
->method('setAffectedUser')
->withConsecutive(
['user1'],
['user2'],
)
->willReturnSelf();
->withConsecutive(['user1'], ['user2']);
$mapper = null;
switch ($objectType) {
case ActivityManager::DECK_OBJECT_BOARD:
@@ -320,14 +187,13 @@ class ActivityManagerTest extends TestCase {
$mapper = $this->cardMapper;
break;
}
$mapper->expects(self::once())
$mapper->expects($this->once())
->method('findBoardId')
->willReturn(123);
$this->permissionService->expects(self::once())
$this->permissionService->expects($this->once())
->method('findUsers')
->willReturn($users);
$this->manager->expects(self::exactly(2))
$this->manager->expects($this->exactly(2))
->method('publish')
->with($event);
$this->invokePrivate($this->activityManager, 'sendToUsers', [$event]);
@@ -371,14 +237,14 @@ class ActivityManagerTest extends TestCase {
$card->setId(3);
$expected = null;
if ($objectType === ActivityManager::DECK_OBJECT_BOARD) {
$this->boardMapper->expects(self::once())
$this->boardMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($board);
$expected = $board;
}
if ($objectType === ActivityManager::DECK_OBJECT_CARD) {
$this->cardMapper->expects(self::once())
$this->cardMapper->expects($this->once())
->method('find')
->with(3)
->willReturn($card);
@@ -394,11 +260,11 @@ class ActivityManagerTest extends TestCase {
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->stackMapper->expects(self::once())
$this->stackMapper->expects($this->once())
->method('find')
->with(123)
->willReturn($stack);
$this->boardMapper->expects(self::once())->method('find')
$this->boardMapper->expects($this->once())->method('find')
->with(999)
->willReturn($board);
$this->assertEquals([
@@ -417,15 +283,15 @@ class ActivityManagerTest extends TestCase {
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->cardMapper->expects(self::once())
$this->cardMapper->expects($this->once())
->method('find')
->with(555)
->willReturn($card);
$this->stackMapper->expects(self::once())
$this->stackMapper->expects($this->once())
->method('find')
->with(123)
->willReturn($stack);
$this->boardMapper->expects(self::once())->method('find')
$this->boardMapper->expects($this->once())->method('find')
->with(999)
->willReturn($board);
$this->assertEquals([
@@ -451,15 +317,15 @@ class ActivityManagerTest extends TestCase {
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->cardMapper->expects(self::once())
$this->cardMapper->expects($this->once())
->method('find')
->with(555)
->willReturn($card);
$this->stackMapper->expects(self::once())
$this->stackMapper->expects($this->once())
->method('find')
->with(123)
->willReturn($stack);
$this->boardMapper->expects(self::once())->method('find')
$this->boardMapper->expects($this->once())->method('find')
->with(999)
->willReturn($board);
$this->assertEquals([

View File

@@ -1,77 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Command;
use OCA\Deck\Service\Importer\BoardImportCommandService;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class BoardImportTest extends \Test\TestCase {
/** @var BoardImportCommandService */
private $boardImportCommandService;
/** @var BoardImport */
private $boardImport;
public function setUp(): void {
parent::setUp();
$this->boardImportCommandService = $this->createMock(BoardImportCommandService::class);
$this->boardImport = new BoardImport(
$this->boardImportCommandService
);
$questionHelper = new QuestionHelper();
$this->boardImport->setHelperSet(
new HelperSet([
$questionHelper
])
);
}
public function testExecuteWithSuccess() {
$input = $this->createMock(InputInterface::class);
$input
->method('getOption')
->withConsecutive(
['system'],
['config']
)
->will($this->returnValueMap([
['system', 'trelloJson'],
['config', null]
]));
$output = $this->createMock(OutputInterface::class);
$output
->expects($this->once())
->method('writeLn')
->with('Done!');
$actual = $this->invokePrivate($this->boardImport, 'interact', [$input, $output]);
$this->assertNull($actual);
$actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]);
$this->assertEquals(0, $actual);
}
}

View File

@@ -30,30 +30,24 @@ use OCA\Deck\Db\BoardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\Service\AttachmentService;
use OCA\Deck\Service\IAttachmentService;
use OCP\AppFramework\Utility\ITimeFactory;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class DeleteCronTest extends TestCase {
class DeleteCronTest extends \Test\TestCase {
/** @var ITimeFactory|MockObject */
private $timeFactory;
/** @var BoardMapper|MockObject */
/** @var BoardMapper|\PHPUnit\Framework\MockObject\MockObject */
protected $boardMapper;
/** @var AttachmentService|MockObject */
/** @var AttachmentService|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentService;
/** @var AttachmentMapper|MockObject */
/** @var AttachmentMapper|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentMapper;
/** @var DeleteCron */
protected $deleteCron;
public function setUp(): void {
parent::setUp();
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->deleteCron = new DeleteCron($this->timeFactory, $this->boardMapper, $this->attachmentService, $this->attachmentMapper);
$this->deleteCron = new DeleteCron($this->boardMapper, $this->attachmentService, $this->attachmentMapper);
}
protected function getBoard($id) {

View File

@@ -26,34 +26,28 @@ namespace OCA\Deck\Cron;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Notification\NotificationHelper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ILogger;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ScheduledNoificationsTest extends TestCase {
class ScheduledNoificationsTest extends \Test\TestCase {
/** @var ITimeFactory|MockObject */
protected $timeFactory;
/** @var CardMapper|MockObject */
/** @var CardMapper|\PHPUnit\Framework\MockObject\MockObject */
protected $cardMapper;
/** @var NotificationHelper|MockObject */
/** @var NotificationHelper|\PHPUnit\Framework\MockObject\MockObject */
protected $notificationHelper;
/** @var ILogger|MockObject */
/** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
protected $logger;
/** @var ScheduledNotifications */
protected $scheduledNotifications;
public function setUp(): void {
parent::setUp();
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->notificationHelper = $this->createMock(NotificationHelper::class);
$this->logger = $this->createMock(ILogger::class);
$this->scheduledNotifications = new ScheduledNotifications($this->timeFactory, $this->cardMapper, $this->notificationHelper, $this->logger);
$this->scheduledNotifications = new ScheduledNotifications($this->cardMapper, $this->notificationHelper, $this->logger);
}
public function testScheduledCron() {
public function testDeleteCron() {
$c1 = new Card();
$c2 = new Card();
$cards = [$c1, $c2];

View File

@@ -67,10 +67,10 @@ class AclMapperTest extends MapperTestUtility {
$this->boardMapper->insert($this->getBoard('MyBoard 3', 'user3'))
];
$this->acls = [
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[1]->getId())),
$this->aclMapper->insert($this->getAcl('user', 'user2', true, false, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user', 'user3', true, true, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[2]->getId()))
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[1]->getId())),
$this->aclMapper->insert($this->getAcl('user','user2', true, false, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user','user3', true, true, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[2]->getId()))
];
foreach ($this->acls as $acl) {

View File

@@ -76,10 +76,10 @@ class BoardMapperTest extends MapperTestUtility {
$this->boardMapper->insert($this->getBoard('MyBoard 3', 'user3'))
];
$this->acls = [
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[1]->getId())),
$this->aclMapper->insert($this->getAcl('user', 'user2', true, false, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user', 'user3', true, true, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[2]->getId()))
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[1]->getId())),
$this->aclMapper->insert($this->getAcl('user','user2', true, false, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user','user3', true, true, false, $this->boards[0]->getId())),
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[2]->getId()))
];
foreach ($this->acls as $acl) {

View File

@@ -124,7 +124,7 @@ class NotificationHelperTest extends \Test\TestCase {
->withConsecutive(
[$param1[0], $param2, $param3, $DUE_ASSIGNED],
[$param1[1], $param2, $param3, $DUE_ASSIGNED],
[$param1[2], $param2, $param3, $DUE_ASSIGNED],
[$param1[2], $param2, $param3, $DUE_ASSIGNED]
)
->willReturn(ConfigService::SETTING_BOARD_NOTIFICATION_DUE_ALL);

View File

@@ -25,7 +25,6 @@ namespace OCA\Deck\Notification;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCP\IL10N;
use OCP\IURLGenerator;
@@ -104,9 +103,9 @@ class NotifierTest extends \Test\TestCase {
$notification->expects($this->once())
->method('getObjectId')
->willReturn('123');
$this->stackMapper->expects($this->once())
->method('findStackFromCardId')
->willReturn($this->buildMockStack());
$this->cardMapper->expects($this->once())
->method('findBoardId')
->willReturn(999);
$expectedMessage = 'The card "Card title" on "Board title" has reached its due date.';
$notification->expects($this->once())
->method('setParsedSubject')
@@ -147,9 +146,9 @@ class NotifierTest extends \Test\TestCase {
$notification->expects($this->once())
->method('getObjectId')
->willReturn('123');
$this->stackMapper->expects($this->once())
->method('findStackFromCardId')
->willReturn($this->buildMockStack());
$this->cardMapper->expects($this->once())
->method('findBoardId')
->willReturn(999);
$expectedMessage = 'admin has mentioned you in a comment on "Card title".';
$notification->expects($this->once())
->method('setParsedSubject')
@@ -184,9 +183,9 @@ class NotifierTest extends \Test\TestCase {
/** @dataProvider dataPrepareCardAssigned */
public function testPrepareCardAssigned($withUserFound = true) {
$this->stackMapper->expects($this->once())
->method('findStackFromCardId')
->willReturn($this->buildMockStack(123));
$this->cardMapper->expects($this->once())
->method('findBoardId')
->willReturn(123);
/** @var INotification $notification */
$notification = $this->createMock(INotification::class);
@@ -339,17 +338,4 @@ class NotifierTest extends \Test\TestCase {
$this->assertEquals($notification, $actualNotification);
}
/**
* @param int $boardId
* @return Stack|MockObject
*/
private function buildMockStack(int $boardId = 999) {
$mockStack = $this->getMockBuilder(Stack::class)
->addMethods(['getBoardId'])
->getMock();
$mockStack->method('getBoardId')->willReturn($boardId);
return $mockStack;
}
}

View File

@@ -205,9 +205,9 @@ class AttachmentServiceTest extends TestCase {
public function testFindAll() {
$this->mockPermission(Acl::PERMISSION_READ);
$attachments = [
$this->createAttachment('deck_file', 'file1'),
$this->createAttachment('deck_file', 'file2'),
$this->createAttachment('deck_file_invalid', 'file3'),
$this->createAttachment('deck_file','file1'),
$this->createAttachment('deck_file','file2'),
$this->createAttachment('deck_file_invalid','file3'),
];
$this->attachmentMapper->expects($this->once())
->method('findAll')
@@ -218,11 +218,11 @@ class AttachmentServiceTest extends TestCase {
->method('extendData')
->withConsecutive(
[$attachments[0]],
[$attachments[1]],
[$attachments[1]]
)
->willReturnOnConsecutiveCalls(
$attachments[0],
$attachments[1],
$attachments[1]
);
$this->assertEquals($attachments, $this->attachmentService->findAll(123, false));
@@ -231,14 +231,14 @@ class AttachmentServiceTest extends TestCase {
public function testFindAllWithDeleted() {
$this->mockPermission(Acl::PERMISSION_READ);
$attachments = [
$this->createAttachment('deck_file', 'file1'),
$this->createAttachment('deck_file', 'file2'),
$this->createAttachment('deck_file_invalid', 'file3'),
$this->createAttachment('deck_file','file1'),
$this->createAttachment('deck_file','file2'),
$this->createAttachment('deck_file_invalid','file3'),
];
$attachmentsDeleted = [
$this->createAttachment('deck_file', 'file4'),
$this->createAttachment('deck_file', 'file5'),
$this->createAttachment('deck_file_invalid', 'file6'),
$this->createAttachment('deck_file','file4'),
$this->createAttachment('deck_file','file5'),
$this->createAttachment('deck_file_invalid','file6'),
];
$this->attachmentMapper->expects($this->once())
->method('findAll')

View File

@@ -263,6 +263,9 @@ class BoardServiceTest extends TestCase {
->willReturn([
'admin' => 'admin',
]);
$this->boardMapper->expects($this->once())
->method('find')
->willReturn(new Board());
$this->assertEquals($acl, $this->service->addAcl(
123, 'user', 'admin', true, true, true
));
@@ -352,6 +355,9 @@ class BoardServiceTest extends TestCase {
$acl->resolveRelation('participant', function ($participant) use (&$user) {
return null;
});
$this->boardMapper->expects($this->once())
->method('find')
->willReturn(new Board());
$this->permissionService->expects($this->any())
->method('findUsers')
->willReturn([

View File

@@ -1,199 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
*
* @author Vitor Mattos <vitor@php.rio>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Service\Importer;
use OC\Comments\Comment;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\BoardImportGetAllowedEvent;
use OCA\Deck\Service\Importer\Systems\TrelloJsonService;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
class BoardImportServiceTest extends \Test\TestCase {
/** @var IDBConnection|MockObject */
protected $dbConn;
/** @var IUserManager|MockObject */
private $userManager;
/** @var BoardMapper|MockObject */
private $boardMapper;
/** @var AclMapper|MockObject */
private $aclMapper;
/** @var LabelMapper|MockObject */
private $labelMapper;
/** @var StackMapper|MockObject */
private $stackMapper;
/** @var CardMapper|MockObject */
private $cardMapper;
/** @var AssignmentMapper|MockObject */
private $assignmentMapper;
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var ICommentsManager|MockObject */
private $commentsManager;
/** @var IEventDispatcher|MockObject */
private $eventDispatcher;
/** @var TrelloJsonService|MockObject */
private $trelloJsonService;
/** @var BoardImportService|MockObject */
private $boardImportService;
public function setUp(): void {
$this->userManager = $this->createMock(IUserManager::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->aclMapper = $this->createMock(AclMapper::class);
$this->labelMapper = $this->createMock(LabelMapper::class);
$this->stackMapper = $this->createMock(StackMapper::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->assignmentMapper = $this->createMock(AssignmentMapper::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->boardImportService = new BoardImportService(
$this->userManager,
$this->boardMapper,
$this->aclMapper,
$this->labelMapper,
$this->stackMapper,
$this->assignmentMapper,
$this->attachmentMapper,
$this->cardMapper,
$this->commentsManager,
$this->eventDispatcher
);
$this->boardImportService->setSystem('trelloJson');
$this->eventDispatcher
->method('dispatchTyped')
->willReturnCallback(function (BoardImportGetAllowedEvent $event) {
$event->getService()->addAllowedImportSystem([
'name' => TrelloJsonService::$name,
'class' => TrelloJsonService::class,
'internalName' => 'trelloJson'
]);
});
$data = json_decode(file_get_contents(__DIR__ . '/../../../data/data-trelloJson.json'));
$this->boardImportService->setData($data);
$configFile = __DIR__ . '/../../../data/config-trelloJson.json';
$configInstance = json_decode(file_get_contents($configFile));
$this->boardImportService->setConfigInstance($configInstance);
$this->trelloJsonService = $this->createMock(TrelloJsonService::class);
$this->trelloJsonService
->method('getJsonSchemaPath')
->willReturn($configFile);
$this->boardImportService->setImportSystem($this->trelloJsonService);
$owner = $this->createMock(IUser::class);
$owner
->method('getUID')
->willReturn('admin');
$johndoe = $this->createMock(IUser::class);
$johndoe
->method('getUID')
->willReturn('johndoe');
$this->userManager
->method('get')
->withConsecutive(
['admin'],
['johndoe']
)
->willReturnonConsecutiveCalls(
$owner,
$johndoe
);
}
public function testImportSuccess() {
$this->boardMapper
->expects($this->once())
->method('insert');
$this->trelloJsonService
->method('getAclList')
->willReturn([new Acl()]);
$this->aclMapper
->expects($this->once())
->method('insert');
$this->trelloJsonService
->method('getLabels')
->willReturn([new Label()]);
$this->labelMapper
->expects($this->once())
->method('insert');
$this->trelloJsonService
->method('getStacks')
->willReturn([new Stack()]);
$this->stackMapper
->expects($this->once())
->method('insert');
$this->trelloJsonService
->method('getCards')
->willReturn([new Card()]);
$this->cardMapper
->expects($this->any())
->method('insert');
$this->trelloJsonService
->method('getComments')
->willReturn([
'fakecardid' => [new Comment()]
]);
$this->commentsManager
->expects($this->once())
->method('save');
$this->trelloJsonService
->method('getCardAssignments')
->willReturn([
'fakecardid' => [new Assignment()]
]);
$this->assignmentMapper
->expects($this->once())
->method('insert');
$actual = $this->boardImportService->import();
$this->assertNull($actual);
}
}

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