Compare commits

..

129 Commits

Author SHA1 Message Date
Julius Härtl
c99b4bf4ab Merge pull request #3629 from nextcloud/release/1.5.6
Release 1.5.6
2022-03-10 09:54:21 +01:00
Julius Härtl
296e7abf14 Bump version to 1.5.6
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-03-10 09:30:38 +01:00
Julius Härtl
c6ef07ceb0 Merge pull request #3627 from nextcloud/backport/3611/stable22
[stable22] Generate fixed link for activity emails
2022-03-09 20:20:26 +01:00
Luka Trovic
4e937bd03a 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:32 +00:00
Julius Härtl
84943c5889 Create appstore-build-publish.yml 2022-03-04 13:58:23 +01:00
Julius Härtl
4ed71b7a63 Merge pull request #3615 from nextcloud/backport/3612/stable22
[stable22] Make insert attachment buttom easy to click
2022-03-02 21:05:00 +01:00
Luka Trovic
4ab8078103 fix: make insert attachment buttom easy to click
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-03-02 16:24:22 +00:00
Nextcloud bot
6b75a1ab1b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-19 02:38:02 +00:00
Nextcloud bot
76e3a3a25e [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-17 02:38:53 +00:00
Nextcloud bot
9503de4716 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-08 02:38:07 +00:00
Nextcloud bot
3340ccec6f [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-06 02:37:01 +00:00
Nextcloud bot
a90e099113 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-05 02:37:29 +00:00
Nextcloud bot
aa69495075 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-02-01 02:37:27 +00:00
Nextcloud bot
11eb779beb [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-31 02:37:27 +00:00
Nextcloud bot
836e2b33df [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-24 02:37:12 +00:00
Nextcloud bot
c63b72bf2e [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-19 02:38:15 +00:00
Julien Veyssier
c20008dacc Merge pull request #3544 from nextcloud/backport/3541/stable22
[stable22] Fix confusion between stackId and boardId in StackService
2022-01-18 16:45:52 +01:00
Julien Veyssier
edacda1377 fix confusion between stackId and boardId in StackService::update()
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
2022-01-18 15:23:26 +00:00
Nextcloud bot
2f0ad2d639 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-18 02:56:05 +00:00
Nextcloud bot
745d76c74b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-13 02:37:11 +00:00
Julius Härtl
3b7f49de55 Merge pull request #3527 from nextcloud/backport/3500/stable22 2022-01-12 13:45:45 +01:00
Julius Härtl
e336bd98b2 Merge pull request #3524 from nextcloud/backport/3502/stable22 2022-01-12 08:39:40 +01:00
Julius Härtl
f62e87cffa Update psalm baseline
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-01-12 08:38:51 +01:00
Nextcloud bot
d4d716b1f5 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-12 02:37:26 +00:00
Julius Härtl
6e89dd48bc Move any circles API usage to internal service
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-01-11 09:31:34 +01:00
Julius Härtl
2bbd572cb5 Avoid blocking calendar access if something goes wrong while fetching deck entries
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2022-01-11 09:29:29 +01:00
Luka Trovic
17543eaf94 exclude deleted boards in the selection for target
Signed-off-by: Luka Trovic <luka@nextcloud.com>
2022-01-11 08:00:48 +00:00
Nextcloud bot
4f4e9fa5c7 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-11 02:37:41 +00:00
Julius Härtl
d0060604a6 Merge pull request #3521 from nextcloud/backport/3512/stable22 2022-01-10 21:08:16 +01:00
Simon Spannagel
9f8df2bbd4 CardApiController: Fix order of optional parameters
Signed-off-by: Simon Spannagel <simonspa@kth.se>
2022-01-10 11:02:10 +00:00
Nextcloud bot
139cca1ae8 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-10 02:37:43 +00:00
Nextcloud bot
32054adcb5 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-08 02:36:42 +00:00
Nextcloud bot
764dd1e995 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-06 02:38:51 +00:00
Nextcloud bot
b65fde069f [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-05 02:38:16 +00:00
Nextcloud bot
bec68eb5c3 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-04 02:38:10 +00:00
Nextcloud bot
8dca76f9f0 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-03 02:38:06 +00:00
Nextcloud bot
13ed30e2bc [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-02 02:37:46 +00:00
Nextcloud bot
a7ff79605d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-01 02:37:48 +00:00
Nextcloud bot
b5bebac3c2 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-31 02:38:05 +00:00
Nextcloud bot
756d78f78a [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-24 02:39:53 +00:00
Nextcloud bot
7e55cb18b3 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-22 02:38:53 +00:00
Nextcloud bot
b834ef664a [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-20 02:40:45 +00:00
Nextcloud bot
1ee0e16f9e [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-18 02:37:55 +00:00
Nextcloud bot
63434d7b63 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-17 02:40:02 +00:00
Nextcloud bot
d9f993dc5b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-16 02:38:16 +00:00
Nextcloud bot
9090c13417 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-12 02:38:56 +00:00
Nextcloud bot
0630dafef9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-09 02:40:05 +00:00
Nextcloud bot
150ab7b83a [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-05 02:43:14 +00:00
Nextcloud bot
11cc569914 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-03 02:40:13 +00:00
Nextcloud bot
eaf58e0fc9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-02 02:40:11 +00:00
Julius Härtl
da994c5ecd Merge pull request #3460 from nextcloud/backport/3459/stable22 2021-11-30 16:32:06 +01:00
Julius Härtl
cef4cc6a4d Fix cursor generation if no results are found
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-30 12:22:08 +00:00
Nextcloud bot
d761323887 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-27 02:37:10 +00:00
Nextcloud bot
0857270a48 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-26 02:57:41 +00:00
Nextcloud bot
df178369c0 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-25 02:36:05 +00:00
Nextcloud bot
8c3edf077d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-24 02:37:00 +00:00
Jonas
ac477a58c3 Merge pull request #3441 from nextcloud/backport/3428/stable22
[stable22] Allow to download an attachment without navigating to the files app
2021-11-22 19:11:19 +01:00
Julius Härtl
504f977739 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:38 +00:00
Nextcloud bot
00e813295b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-20 02:53:16 +00:00
Nextcloud bot
300d300c45 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-19 02:53:35 +00:00
Nextcloud bot
1514f9a737 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-15 02:35:08 +00:00
Nextcloud bot
f4fe271b39 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-14 02:36:21 +00:00
Nextcloud bot
1c01881eb7 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-13 02:36:43 +00:00
Julius Härtl
af1564d8e5 Bump version to 1.5.5
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-10 20:12:55 +01:00
Julius Härtl
35515ce157 Bump version to 1.5.4
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-10 16:16:20 +01:00
Nextcloud bot
fd62ab7a59 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-10 02:36:04 +00:00
Nextcloud bot
1557797926 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-09 04:07:36 +00:00
Nextcloud bot
90a8b479f6 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-08 02:49:15 +00:00
Nextcloud bot
c2fae6b2d7 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-07 02:32:58 +00:00
Nextcloud bot
89a3f4fc26 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-06 02:33:23 +00:00
Julius Härtl
7aa94c74d7 Merge pull request #3407 from nextcloud/backport/3384/stable22
[stable22] Keep exceptions http response generic
2021-11-05 19:53:06 +01:00
Julius Härtl
33993418ac Keep exceptions http response generic and return the request ID for further tracing
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-11-05 17:02:27 +00:00
Nextcloud bot
75a2d9d54c [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-30 02:33:52 +00:00
Nextcloud bot
b1a3c6b237 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-29 02:33:14 +00:00
Julius Härtl
437704abb3 Merge pull request #3392 from nextcloud/backport/3391/stable22 2021-10-27 14:16:01 +02:00
Paweł Kuffel
0e09548e69 use displayname instead of uid for mentions
Signed-off-by: Paweł Kuffel <pawel@kuffel.io>
2021-10-27 11:59:19 +00:00
Julius Härtl
37834cb926 Merge pull request #3378 from nextcloud/backport/3324/stable22 2021-10-22 20:24:40 +02:00
Nextcloud bot
7dd3bb49f4 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-22 02:32:43 +00:00
Julius Härtl
fdf1eaeaed Merge pull request #3381 from Artem4590/backport/3323/stable22 2021-10-21 15:49:43 +02:00
Nextcloud bot
4cc88c8b64 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-21 02:34:56 +00:00
Nextcloud bot
db331ecb72 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-20 02:53:53 +00:00
Artem Lavrukhin
c2757bec7d [stable22] Extend drag-and-drop zone in card sidebar
Signed-off-by: Artem Lavrukhin <lavryha4590@gmail.com>
2021-10-15 16:09:10 +03:00
Lera Dmitrieva
e074eac092 Fix menu button position in card modal
Signed-off-by: Lera Dmitrieva <dmit.valerya@yandex.ru>
2021-10-12 10:25:11 +00:00
Nextcloud bot
d5166c74e5 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-07 02:36:02 +00:00
Julius Härtl
226e3c8212 Merge pull request #3366 from nextcloud/backport/3364/stable22 2021-10-06 11:40:20 +02:00
Julius Härtl
a680915a89 Fix optional parameter order
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-10-06 09:24:12 +00:00
Julius Härtl
b8ff06e5d2 Merge pull request #3361 from nextcloud/enh/stable22-paginated-search-for-boards-and-cards 2021-10-05 08:24:06 +02:00
Julien Veyssier
5a108b64b0 use distinct pagination cursor for cards and boards, use cursor and limit in SearchService::searchBoards()
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
2021-10-04 17:38:07 +02:00
Nextcloud bot
00630587af [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-04 02:37:16 +00:00
Nextcloud bot
b02cf925a1 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-10-02 02:37:09 +00:00
Julius Härtl
aaa26575dd Bump version to 1.5.3
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-14 21:08:10 +02:00
Julius Härtl
aebb3cef03 Merge pull request #3317 from nextcloud/backport/3316/stable22
[stable22] Additional check for stacks
2021-09-14 21:06:56 +02:00
Julius Härtl
18a7ae3a1a Additional check for stacks
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-14 19:01:19 +00:00
Nextcloud bot
6c84c67970 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-14 02:42:00 +00:00
Nextcloud bot
d449acde30 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-13 02:40:06 +00:00
Nextcloud bot
c1e700cefa [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-12 02:40:07 +00:00
Julius Härtl
30df03948c Bump version to 1.5.2
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-09 14:18:07 +02:00
Nextcloud bot
15ea081daa [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-09 02:38:56 +00:00
Julius Härtl
0f71741c46 Merge pull request #3306 from nextcloud/backport/3299/stable22
[stable22] Return false instead of throwing when getting calendar setting
2021-09-08 18:28:57 +02:00
Julius Härtl
585f0999a3 Merge pull request #3303 from nextcloud/backport/3298/stable22 2021-09-08 18:14:35 +02:00
Julius Härtl
d51f645299 Return false instead of throwing when getting calendar integration setting
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-08 16:00:28 +00:00
Julius Härtl
ebbe4eb802 Delete file shares through attachments API
Previously the file was deleted in the file structure of the user is not
expected as the file might not only be related to the card.

Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-08 17:57:01 +02:00
Nextcloud bot
d6c94f44d9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-08 02:38:21 +00:00
Julius Härtl
86345a6338 Merge pull request #3300 from nextcloud/backport/3294/stable22
[stable22] Fix print style issues
2021-09-07 13:14:33 +02:00
Michael Weimann
2c77d8a589 fix print style issues
Signed-off-by: Michael Weimann <mail@michael-weimann.eu>
2021-09-06 13:58:34 +00:00
Nextcloud bot
16cbbd4805 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-06 02:38:53 +00:00
Nextcloud bot
8aa77679c8 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-09-05 02:38:20 +00:00
Julius Härtl
e6cff5bbb6 Bump version to 1.5.1
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-09-03 15:03:27 +02:00
Nextcloud bot
86ed24744c [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-08-31 02:37:47 +00:00
Julius Härtl
01ed3625dc Merge pull request #3264 from nextcloud/backport/3263/stable22
[stable22] Defer obtaining the user session in the config service
2021-08-20 19:07:13 +02:00
Julius Härtl
cc9dea1f2b Defer obtaining the user session in the config service which might be injected before login has hapened
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-08-20 16:02:48 +00:00
Nextcloud bot
b16ade905c [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-08-18 02:37:01 +00:00
Nextcloud bot
ee1bba7d99 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-08-10 02:44:35 +00:00
Nextcloud bot
3407097e95 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-08-06 02:38:51 +00:00
Julius Härtl
47ac3e6082 Merge pull request #3231 from nextcloud/backport/3225/stable22
[stable22] Check for null value to avoid TypeError in the group manager
2021-08-05 09:04:16 +02:00
Julius Härtl
75110bed47 Check for null value to avoid TypeError in the group manager
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-08-04 16:41:31 +00:00
Julius Härtl
74f0106718 Merge pull request #3224 from nextcloud/backport/3217/stable22
[stable22] Move circle checks to a unified service and improve member checks
2021-08-03 11:41:03 +02:00
Julius Härtl
958d50d9b7 Move circle checks to a unified service and improve member checks
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-08-03 07:43:30 +00:00
Julius Härtl
5f4aa017b6 Pin mariadb to 10.5 for tests
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-08-03 09:42:43 +02:00
Nextcloud bot
92c2a58f50 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-08-01 02:12:52 +00:00
Nextcloud bot
75bf0dffe6 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-07-15 10:52:50 +00:00
Julius Härtl
45a10f0840 Bump version to 1.5.0
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-09 11:55:34 +02:00
Julius Härtl
fbf3b1cd19 Merge pull request #3168 from nextcloud/backport/3161/stable22
[stable22] Reduce duplicate queries when fetching user boards an permissions
2021-07-06 07:54:35 +02:00
Julius Härtl
0cc4151929 Reduce duplicate queries when fetching user boards an permissions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-05 18:27:49 +00:00
Julius Härtl
e261ade1bb Merge pull request #3165 from nextcloud/backport/3151/stable22
[stable22] Always log generic exceptions
2021-07-05 16:21:15 +02:00
Julius Härtl
5d7e54d419 Always log generic exceptions
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2021-07-02 15:55:44 +00:00
Julius Härtl
3499858295 Merge remote-tracking branch 'origin/master' into stable22 2021-06-25 15:39:38 +02:00
Julius Härtl
1615e218bd Merge pull request #3148 from nextcloud/update-stable22-target-versions
Update stable22 target versions
2021-06-25 09:07:38 +02:00
Joas Schilling
9a3b859780 Update stable22 target versions
Signed-off-by: Joas Schilling <coding@schilljs.com>
2021-06-24 14:07:30 +02:00
109 changed files with 9539 additions and 12481 deletions

View File

@@ -3,10 +3,6 @@ module.exports = {
'@nextcloud',
],
rules: {
'jsdoc/require-param-description': ['off'],
'jsdoc/require-param-type': ['off'],
'jsdoc/check-param-names': ['off'],
'jsdoc/no-undefined-types': ['off'],
'jsdoc/require-property-description' : ['off']
'valid-jsdoc': ['off'],
},
}

View File

@@ -39,13 +39,3 @@ updates:
versions:
- "< 16"
- ">= 15.a"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: saturday
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
reviewers:
- juliushaertl

25
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "1. to develop"
- "2. developing"
- "3. to review"
- "discussion"
- "bounty"
- "bug"
- "enhancement"
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.

View File

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

View File

@@ -0,0 +1,151 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Build and publish app release
on:
release:
types: [published]
env:
PHP_VERSION: 7.4
jobs:
build_and_publish:
runs-on: ubuntu-latest
# Only allowed to be run on nextcloud-releases repositories
if: ${{ github.repository_owner == 'nextcloud-releases' }}
steps:
- name: Check actor permission
uses: skjnldsv/check-actor-permission@v2
with:
require: write
- name: Set app env
run: |
# Split and keep last
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v3
with:
path: ${{ env.APP_NAME }}
- name: Get appinfo data
id: appinfo
uses: skjnldsv/xpath-action@master
with:
filename: ${{ env.APP_NAME }}/appinfo/info.xml
expression: "//info//dependencies//nextcloud/@min-version"
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@v1.2
id: versions
# Continue if no package.json
continue-on-error: true
with:
path: ${{ env.APP_NAME }}
fallbackNode: "^12"
fallbackNpm: "^6"
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
# Skip if no package.json
if: ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@v3
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
# Skip if no package.json
if: ${{ steps.versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
- name: Set up php ${{ env.PHP_VERSION }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
coverage: none
- name: Check composer.json
id: check_composer
uses: andstor/file-existence-action@v1
with:
files: "${{ env.APP_NAME }}/composer.json"
- name: Install composer dependencies
if: steps.check_composer.outputs.files_exists == 'true'
run: |
cd ${{ env.APP_NAME }}
composer install --no-dev
- name: Build ${{ env.APP_NAME }}
# Skip if no package.json
if: ${{ steps.versions.outputs.nodeVersion }}
run: |
cd ${{ env.APP_NAME }}
npm ci
npm run build
- name: Install Krankerl
run: |
wget https://github.com/ChristophWurst/krankerl/releases/download/v0.13.0/krankerl_0.13.0_amd64.deb
sudo dpkg -i krankerl_0.13.0_amd64.deb
- name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }}
# Try krankerl, fallback to makefile
run: |
cd ${{ env.APP_NAME }}
krankerl package || make appstore
- name: Checkout server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
continue-on-error: true
id: server-checkout
run: |
NCVERSION=${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
wget --quiet https://download.nextcloud.com/server/releases/latest-$NCVERSION.zip
unzip latest-$NCVERSION.zip
- name: Checkout server master fallback
uses: actions/checkout@v3
if: ${{ steps.server-checkout.outcome != 'success' }}
with:
repository: nextcloud/server
path: nextcloud
- name: Sign app
run: |
# Extracting release
cd ${{ env.APP_NAME }}/build/artifacts
tar -xvf ${{ env.APP_NAME }}.tar.gz
cd ../../../
# Setting up keys
echo "${{ secrets.APP_PRIVATE_KEY }}" > ${{ env.APP_NAME }}.key
wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt"
# Signing
php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}
# Rebuilding archive
cd ${{ env.APP_NAME }}/build/artifacts
tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }}
- name: Attach tarball to github release
uses: svenstaro/upload-release-action@v2
id: attach_to_release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz
asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz
tag: ${{ github.ref }}
overwrite: true
- name: Upload app to Nextcloud appstore
uses: nextcloud-releases/nextcloud-appstore-push-action@v1
with:
app_name: ${{ env.APP_NAME }}
appstore_token: ${{ secrets.APPSTORE_TOKEN }}
download_url: ${{ steps.attach_to_release.outputs.browser_download_url }}
app_private_key: ${{ secrets.APP_PRIVATE_KEY }}

View File

@@ -1,46 +0,0 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Rebase command
on:
issue_comment:
types: created
jobs:
rebase:
runs-on: ubuntu-latest
# On pull requests and if the comment starts with `/rebase`
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase')
steps:
- name: Add reaction on start
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "+1"
- name: Checkout the latest code
uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
token: ${{ secrets.COMMAND_BOT_PAT }}
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
- name: Add reaction on failure
uses: peter-evans/create-or-update-comment@v1
if: failure()
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "-1"

View File

@@ -1,29 +0,0 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Dependabot
on:
pull_request_target:
branches:
- master
- stable*
jobs:
auto-approve-merge:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
# Github actions bot approve
- uses: hmarr/auto-approve-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Nextcloud bot approve and merge request
- uses: ahmadnassri/action-dependabot-auto-merge@v2
with:
target: minor
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}

View File

@@ -1,20 +0,0 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Pull request checks
on: pull_request
jobs:
commit-message-check:
name: Block fixup and squash commits
runs-on: ubuntu-latest
steps:
- name: Run check
uses: xt0rted/block-autosquash-commits-action@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -19,7 +19,7 @@ jobs:
matrix:
php-versions: ['7.4']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['master']
server-versions: ['stable22']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v2.4.0
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
@@ -56,12 +56,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@v2.4.0
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.17.0
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit

View File

@@ -13,13 +13,13 @@ jobs:
strategy:
matrix:
php-versions: ['7.4', '8.0']
php-versions: ['7.2', '7.3', '7.4']
name: php${{ matrix.php-versions }} lint
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v2
- name: Set up php${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.17.0
uses: shivammathur/setup-php@v1
with:
php-version: ${{ matrix.php-versions }}
coverage: none
@@ -31,9 +31,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.4.0
uses: actions/checkout@master
- name: Set up php
uses: shivammathur/setup-php@2.17.0
uses: shivammathur/setup-php@master
with:
php-version: 7.4
coverage: none
@@ -50,9 +50,9 @@ jobs:
node-version: [14.x]
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v2
- name: Use node ${{ matrix.node-version }}
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Set up npm7
@@ -67,16 +67,16 @@ jobs:
strategy:
matrix:
node-version: [14.x]
node-versions: [14.x]
name: stylelint node${{ matrix.node-version }}
name: stylelint node${{ matrix.node-versions }}
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v2
- name: Set up node ${{ matrix.node-version }}
uses: actions/setup-node@v2.5.1
- name: Set up node ${{ matrix.node-versions }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
node-versions: ${{ matrix.node-versions }}
- name: Set up npm7
run: npm i -g npm@7

View File

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

View File

@@ -12,9 +12,9 @@ jobs:
node-version: [14.x]
steps:
- uses: actions/checkout@v2.4.0
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.5.1
uses: actions/setup-node@v1
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']
php-versions: ['7.3', '7.4']
databases: ['sqlite', 'mysql', 'pgsql']
server-versions: ['master']
server-versions: ['stable22']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
@@ -44,7 +44,7 @@ jobs:
steps:
- name: Checkout server
uses: actions/checkout@v2.4.0
uses: actions/checkout@v2
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@v2.4.0
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@2.17.0
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit

View File

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

View File

@@ -1,38 +1,91 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.6.0-beta1
### Added
- #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
- #3362 Improve search performance @eneiluj
- #2710 Due date shortcuts in the datepicker @jakobroehrl
## 1.5.6
### Fixed
- #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
- #3225 Check for null value to avoid TypeError in the group manager @juliushaertl
- #3263 Defer obtaining the user session in the config service @juliushaertl
- #3294 Fix print style issues @weeman1337
- #3299 Return false instead of throwing when getting calendar setting @juliushaertl
- #3298 Delete file shares through attachments API @juliushaertl
- #3343 Fix search pagination cursor @eneiluj
- #3326 add autofocus on board edit @weeman1337
- #3323 Extend drag-and-drop zone in card sidebar @old-green-frog
- #3364 Fix optional parameter order @juliushaertl
- #3324 Fix menu button position in card modal @valerydmitrieva
- #3391 Use displayname instead of uid for mentions (reopened against master) @kffl
- #3316 Additional check for stacks @juliushaertl
- #3357 Revert "Fix search pagination cursor" @juliushaertl
- #3327 Do not show both bullets and checkboxes for checklists @Themanwhosmellslikesugar
- #3375 Show absolute dates when printing @weeman1337
- #3376 Print assignee names @weeman1337
- #3384 Keep exceptions http response generic @juliushaertl
- Allow to download an attachment without navigating to the files app [#3441](https://api.github.com/repos/nextcloud/deck/pulls/3441)
- Fix CalDAV blocking and modernize circles API usage [#3527](https://api.github.com/repos/nextcloud/deck/pulls/3527)
- CardApiController: Fix order of optional parameters [#3521](https://api.github.com/repos/nextcloud/deck/pulls/3521)
- Fix cursor generation if no results are found [#3460](https://api.github.com/repos/nextcloud/deck/pulls/3460)
- Exclude deleted boards in the selection for target [#3524](https://api.github.com/repos/nextcloud/deck/pulls/3524)
- Generate fixed link for activity emails [#3627](https://api.github.com/repos/nextcloud/deck/pulls/3627)
- Make insert attachment buttom easy to click [#3615](https://api.github.com/repos/nextcloud/deck/pulls/3615)
- Fix confusion between stackId and boardId in StackService [#3544](https://api.github.com/repos/nextcloud/deck/pulls/3544)
## 1.5.5
- Fix release asset build
## 1.5.4
### Fixed
- #3378 Fix menu button position in card modal
- #3392 Use displayname instead of uid for mentions (reopened against master)
- #3361 Improve combined search @eneiluj
- #3381 Extend drag-and-drop zone in card sidebar @Artem4590
- #3366 Fix optional parameter order
- #3407 Keep exceptions http response generic
## 1.5.3
### Fied
- #3317 Additional check for stacks
## 1.5.2
### Fixed
- #3300 Fix print style issues
- #3303 Delete file shares through attachments API
- #3306 Return false instead of throwing when getting calendar setting
## 1.5.1 - 2021-09-03
### Fixed
- #3224 Move circle checks to a unified service and improve member checks
- #3231 Check for null value to avoid TypeError in the group manager
- #3264 Defer obtaining the user session in the config service
## 1.5.0 - 2021-07-09
### Added
* Nextcloud 22 compatibility
* [#3105](https://github.com/nextcloud/deck/pull/3105) Compatibility with Cirlces changes in 22
* [#3147](https://github.com/nextcloud/deck/pull/3147) Add card button to the dashboard widget @jakobroehrl
* [#2854](https://github.com/nextcloud/deck/pull/2854) Add card button in card overview @jakobroehrl
* [#3078](https://github.com/nextcloud/deck/pull/3078) Show on shared boards unassigned cards to all users @jakobroehrl
### Fixed
* [#2935](https://github.com/nextcloud/deck/pull/2935) Rich object string parameters for notifications @nickvergessen
* [#2950](https://github.com/nextcloud/deck/pull/2950) Remove notification on unshare and add type hints
* [#2983](https://github.com/nextcloud/deck/pull/2983) Fix codemirror description width
* [#2989](https://github.com/nextcloud/deck/pull/2989) Fix unified comments search with postgres
* [#3005](https://github.com/nextcloud/deck/pull/3005) Do not query the lookupserver when looking for sharees
* [#3011](https://github.com/nextcloud/deck/pull/3011) L10n: Spelling unification @Valdnet
* [#3014](https://github.com/nextcloud/deck/pull/3014) Proper error handling when fetching comments fails
* [#3016](https://github.com/nextcloud/deck/pull/3016) Allow searching for filters without a query to match all that have a given filter set
* [#3021](https://github.com/nextcloud/deck/pull/3021) L10n: Add word "Card" @Valdnet
* [#3025](https://github.com/nextcloud/deck/pull/3025) Show comment counter and highlight if unread comments are available
* [#3036](https://github.com/nextcloud/deck/pull/3036) Add link to migration tool for Trello @maxammann
* [#3037](https://github.com/nextcloud/deck/pull/3037) Catch any error during circle detail fetching
* [#3038](https://github.com/nextcloud/deck/pull/3038) Get attachment from the user node instead of the share source
* [#3092](https://github.com/nextcloud/deck/pull/3092) Refactor update to have proper order of optional parameters
* [#3113](https://github.com/nextcloud/deck/pull/3113) Use new viewer syntax with destructuring object @azul
* [#3142](https://github.com/nextcloud/deck/pull/3142) Always pass user id in share provider
* [#3152](https://github.com/nextcloud/deck/pull/3152) Only offer stack creation in emptycontent with proper permissions
* [#3165](https://github.com/nextcloud/deck/pull/3165) Always log generic exceptions
* [#3168](https://github.com/nextcloud/deck/pull/3168) Reduce duplicate queries when fetching user boards an permissions
## 1.4.0 - 2021-04-13
@@ -69,15 +122,15 @@ All notable changes to this project will be documented in this file.
## 1.3.0-beta2
### Fixed
* [#2700](https://github.com/nextcloud/deck/pull/2700) Attempt to copy file on dropping it to deck @juliushaertl
* [#2701](https://github.com/nextcloud/deck/pull/2701) Fix uploading files by drag and drop @juliushaertl
* [#2700](https://github.com/nextcloud/deck/pull/2700) Attempt to copy file on dropping it to deck
* [#2701](https://github.com/nextcloud/deck/pull/2701) Fix uploading files by drag and drop
* [#2707](https://github.com/nextcloud/deck/pull/2707) L10n: Change to a capital letter @Valdnet
* [#2712](https://github.com/nextcloud/deck/pull/2712) Docs: Fix table in section "GET /api/v1.0/config" @das-g
* [#2716](https://github.com/nextcloud/deck/pull/2716) Remove repair step which is no longer needed as we cleanup properly @juliushaertl
* [#2716](https://github.com/nextcloud/deck/pull/2716) Remove repair step which is no longer needed as we cleanup properly
* [#2723](https://github.com/nextcloud/deck/pull/2723) Pad random color with leading zeroes @PVince81
* [#2729](https://github.com/nextcloud/deck/pull/2729) Remove invalid activity parameters @nickvergessen
* [#2750](https://github.com/nextcloud/deck/pull/2750) Fix deck activity emails not being translated @nickvergessen
* [#2751](https://github.com/nextcloud/deck/pull/2751) Properly set author for activity events that are triggered by cron @juliushaertl
* [#2751](https://github.com/nextcloud/deck/pull/2751) Properly set author for activity events that are triggered by cron
## 1.2.2 - 2020-11-24
@@ -186,31 +239,31 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#2116](https://github.com/nextcloud/deck/pull/2116) Fix navigation layout issues @juliushaertl
* [#2118](https://github.com/nextcloud/deck/pull/2118) Use proper parameter when handling attachments @juliushaertl
* [#2116](https://github.com/nextcloud/deck/pull/2116) Fix navigation layout issues
* [#2118](https://github.com/nextcloud/deck/pull/2118) Use proper parameter when handling attachments
## 1.0.4 - 2020-06-26
### Fixed
* [#2062](https://github.com/nextcloud/deck/pull/2062) Fix saving card description after toggling checkboxes @juliushaertl
* [#2062](https://github.com/nextcloud/deck/pull/2062) Fix saving card description after toggling checkboxes
* [#2065](https://github.com/nextcloud/deck/pull/2065) Adding CSS rule for Markdown Blockquotes @reox
* [#2059](https://github.com/nextcloud/deck/pull/2059) Fix fetching attachments on card change @juliushaertl
* [#2060](https://github.com/nextcloud/deck/pull/2060) Use mixing for relative date in card sidebar @juliushaertl
* [#2059](https://github.com/nextcloud/deck/pull/2059) Fix fetching attachments on card change
* [#2060](https://github.com/nextcloud/deck/pull/2060) Use mixing for relative date in card sidebar
## 1.0.3 - 2020-06-19
### Fixed
* [#2019](https://github.com/nextcloud/deck/pull/2019) Remove old global css rule @juliushaertl
* [#2020](https://github.com/nextcloud/deck/pull/2020) Fix navigation issue with leftover nodes @juliushaertl
* [#2021](https://github.com/nextcloud/deck/pull/2021) Fix description issues @juliushaertl
* [#2022](https://github.com/nextcloud/deck/pull/2022) Fix replyto issues with the comments API @juliushaertl
* [#2027](https://github.com/nextcloud/deck/pull/2027) Allow to unassign current user from card @juliushaertl
* [#2019](https://github.com/nextcloud/deck/pull/2019) Remove old global css rule
* [#2020](https://github.com/nextcloud/deck/pull/2020) Fix navigation issue with leftover nodes
* [#2021](https://github.com/nextcloud/deck/pull/2021) Fix description issues
* [#2022](https://github.com/nextcloud/deck/pull/2022) Fix replyto issues with the comments API
* [#2027](https://github.com/nextcloud/deck/pull/2027) Allow to unassign current user from card
* [#2029](https://github.com/nextcloud/deck/pull/2029) Fix wording : stack -> list @cloud2018
* [#2032](https://github.com/nextcloud/deck/pull/2032) Force order by id as second sorting key @juliushaertl
* [#2045](https://github.com/nextcloud/deck/pull/2045) Improve label styling @juliushaertl
* [#2032](https://github.com/nextcloud/deck/pull/2032) Force order by id as second sorting key
* [#2045](https://github.com/nextcloud/deck/pull/2045) Improve label styling
* [#2010](https://github.com/nextcloud/deck/pull/2010) User documentation fixes @Nyco
* [#1998](https://github.com/nextcloud/deck/pull/1998) Add Checklist explaination to the doc @4rnoP

View File

@@ -25,8 +25,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-alpha1</version>
<version>1.5.6</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="22" max-version="22"/>
</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>
</commands>
<activity>
<settings>

View File

@@ -25,6 +25,7 @@
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#redirectToCard', 'url' => '/card/{cardId}', 'verb' => 'GET'],
// boards
['name' => 'board#index', 'url' => '/boards', 'verb' => 'GET'],
@@ -90,10 +91,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,20 @@
}
],
"require": {
"cogpowered/finediff": "0.3.*",
"justinrainbow/json-schema": "^5.2"
"cogpowered/finediff": "0.3.*"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^21@dev",
"phpunit/phpunit": "^9",
"nextcloud/coding-standard": "^1.0.0",
"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"
}
"classmap-authoritative": true
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",

1469
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,8 +14,6 @@ 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)
### 1. Create my first board
In this example, we're going to create a board and share it with an other nextcloud user.
@@ -71,80 +69,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

@@ -35,6 +35,7 @@ use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCA\Deck\Service\CardService;
class DeckProvider implements IProvider {
@@ -52,8 +53,10 @@ class DeckProvider implements IProvider {
private $l10nFactory;
/** @var IConfig */
private $config;
/** @var CardService */
private $cardService;
public function __construct(IURLGenerator $urlGenerator, ActivityManager $activityManager, IUserManager $userManager, ICommentsManager $commentsManager, IFactory $l10n, IConfig $config, $userId) {
public function __construct(IURLGenerator $urlGenerator, ActivityManager $activityManager, IUserManager $userManager, ICommentsManager $commentsManager, IFactory $l10n, IConfig $config, $userId, CardService $cardService) {
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
$this->activityManager = $activityManager;
@@ -61,6 +64,7 @@ class DeckProvider implements IProvider {
$this->userManager = $userManager;
$this->l10nFactory = $l10n;
$this->config = $config;
$this->cardService = $cardService;
}
/**
@@ -131,7 +135,7 @@ class DeckProvider implements IProvider {
if (array_key_exists('board', $subjectParams)) {
$archivedParam = $subjectParams['card']['archived'] ? 'archived/' : '';
$card['link'] = $this->deckUrl('/board/' . $subjectParams['board']['id'] . '/' . $archivedParam . 'card/' . $event->getObjectId());
$card['link'] = $this->cardService->getRedirectUrlForCard($event->getObjectId());
}
$params['card'] = $card;
}

View File

@@ -100,9 +100,6 @@ class ResourceProvider implements IProvider {
if ($board->getOwner() === $user->getUID()) {
return true;
}
if ($board->getAcl() === null) {
return false;
}
return $this->permissionService->userCan($board->getAcl(), Acl::PERMISSION_READ, $user->getUID());
}

View File

@@ -127,9 +127,6 @@ class ResourceProviderCard implements IProvider {
if ($board->getOwner() === $user->getUID()) {
return true;
}
if ($board->getAcl() === null) {
return false;
}
return $this->permissionService->userCan($board->getAcl(), Acl::PERMISSION_READ, $user->getUID());
}

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

@@ -34,12 +34,20 @@ use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Controller;
use OCA\Deck\Db\CardMapper;
use OCP\IURLGenerator;
use \OCP\AppFramework\Http\RedirectResponse;
use OCA\Deck\Db\Acl;
use OCA\Deck\Service\CardService;
class PageController extends Controller {
private $permissionService;
private $initialState;
private $configService;
private $eventDispatcher;
private $cardMapper;
private $urlGenerator;
private $cardService;
public function __construct(
$AppName,
@@ -47,7 +55,10 @@ class PageController extends Controller {
PermissionService $permissionService,
IInitialStateService $initialStateService,
ConfigService $configService,
IEventDispatcher $eventDispatcher
IEventDispatcher $eventDispatcher,
CardMapper $cardMapper,
IURLGenerator $urlGenerator,
CardService $cardService
) {
parent::__construct($AppName, $request);
@@ -55,6 +66,9 @@ class PageController extends Controller {
$this->initialState = $initialStateService;
$this->configService = $configService;
$this->eventDispatcher = $eventDispatcher;
$this->cardMapper = $cardMapper;
$this->urlGenerator = $urlGenerator;
$this->cardService = $cardService;
}
/**
@@ -85,4 +99,17 @@ class PageController extends Controller {
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function redirectToCard($cardId): RedirectResponse {
try {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
return new RedirectResponse($this->cardService->getCardUrl($cardId));
} catch (\Exception $e) {
return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('deck.page.index'));
}
}
}

View File

@@ -24,7 +24,7 @@
namespace OCA\Deck\Cron;
use OCP\BackgroundJob\Job;
use OC\BackgroundJob\Job;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\CardMapper;

View File

@@ -24,14 +24,13 @@
namespace OCA\Deck\Cron;
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;
@@ -44,9 +43,6 @@ class DeleteCron extends TimedJob {
$this->boardMapper = $boardMapper;
$this->attachmentService = $attachmentService;
$this->attachmentMapper = $attachmentMapper;
$this->setInterval(60 * 60 * 24);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
/**

View File

@@ -23,7 +23,7 @@
namespace OCA\Deck\Cron;
use OCP\BackgroundJob\Job;
use OC\BackgroundJob\Job;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Notification\NotificationHelper;

View File

@@ -25,46 +25,23 @@ namespace OCA\Deck\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
class AclMapper extends QBMapper implements IPermissionMapper {
class AclMapper extends DeckMapper implements IPermissionMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'deck_board_acl', Acl::class);
}
public function find($id): Acl {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('id', $query->createNamedParameter($id)));
return $this->findEntity($query);
}
public function findAll($boardId, $limit = null, $offset = null) {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('board_id', $query->createNamedParameter($boardId)))
->setMaxResults($limit)
->setFirstResult($offset);
return $this->findEntities($query);
$sql = 'SELECT id, board_id, type, participant, permission_edit, permission_share, permission_manage FROM `*PREFIX*deck_board_acl` WHERE `board_id` = ? ';
return $this->findEntities($sql, [$boardId], $limit, $offset);
}
public function isOwner($userId, $aclId): bool {
$query = $this->db->getQueryBuilder();
$query->select('owner')
->from('deck_boards', 'b')
->innerJoin('b', $this->getTableName(), 'a', $query->expr()->eq('b.id', 'a.board_id'))
->where($query->expr()->eq('a.id', $query->createNamedParameter($aclId)));
$cursor = $query->execute();
$row = $cursor->fetch();
$cursor->closeCursor();
return is_array($row) && $row['owner'] === $userId;
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_board_acl` WHERE id = ?)';
$stmt = $this->execute($sql, [$aclId]);
$row = $stmt->fetch();
return ($row['owner'] === $userId);
}
public function findBoardId($id): ?int {
@@ -76,13 +53,8 @@ class AclMapper extends QBMapper implements IPermissionMapper {
return null;
}
public function findByParticipant(int $type, string $participant): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('type', $query->createNamedParameter($type)))
->andWhere($query->expr()->eq('type', $query->createNamedParameter($participant)));
return $this->findEntities($query);
public function findByParticipant($type, $participant): array {
$sql = 'SELECT * from *PREFIX*deck_board_acl WHERE type = ? AND participant = ?';
return $this->findEntities($sql, [$type, $participant]);
}
}

View File

@@ -28,10 +28,8 @@ class Board extends RelationalEntity {
protected $owner;
protected $color;
protected $archived = false;
/** @var Label[]|null */
protected $labels = null;
/** @var Acl[]|null */
protected $acl = null;
protected $labels = [];
protected $acl = [];
protected $permissions = [];
protected $users = [];
protected $shared;
@@ -63,10 +61,6 @@ class Board extends RelationalEntity {
if ($this->shared === -1) {
unset($json['shared']);
}
// FIXME: Ideally the API responses should follow the internal data structure and return null if the labels/acls have not been fetched from the db
// however this would be a breaking change for consumers of the API
$json['acl'] = $this->acl ?? [];
$json['labels'] = $this->labels ?? [];
return $json;
}
@@ -74,27 +68,21 @@ class Board extends RelationalEntity {
* @param Label[] $labels
*/
public function setLabels($labels) {
$this->labels = $labels;
foreach ($labels as $l) {
$this->labels[] = $l;
}
}
/**
* @param Acl[] $acl
*/
public function setAcl($acl) {
$this->acl = $acl;
foreach ($acl as $a) {
$this->acl[] = $a;
}
}
public function getETag() {
return md5((string)$this->getLastModified());
}
/** @returns Acl[]|null */
public function getAcl(): ?array {
return $this->acl;
}
/** @returns Label[]|null */
public function getLabels(): ?array {
return $this->labels;
}
}

View File

@@ -26,14 +26,12 @@ namespace OCA\Deck\Db;
use OC\Cache\CappedMemoryCache;
use OCA\Deck\Service\CirclesService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUserManager;
use OCP\IGroupManager;
use Psr\Log\LoggerInterface;
class BoardMapper extends QBMapper implements IPermissionMapper {
class BoardMapper extends DeckMapper implements IPermissionMapper {
private $labelMapper;
private $aclMapper;
private $stackMapper;
@@ -44,8 +42,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
/** @var CappedMemoryCache */
private $userBoardCache;
/** @var CappedMemoryCache */
private $boardCache;
public function __construct(
IDBConnection $db,
@@ -67,7 +63,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
$this->logger = $logger;
$this->userBoardCache = new CappedMemoryCache();
$this->boardCache = new CappedMemoryCache();
}
@@ -75,52 +70,40 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
* @param $id
* @param bool $withLabels
* @param bool $withAcl
* @return Board
* @return \OCP\AppFramework\Db\Entity
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function find($id, $withLabels = false, $withAcl = false): Board {
if (!isset($this->boardCache[$id])) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_boards')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->orderBy('id');
$this->boardCache[$id] = $this->findEntity($qb);
}
// FIXME is this necessary? it was NOT done with the old mapper
// $this->mapOwner($board);
public function find($id, $withLabels = false, $withAcl = false) {
$sql = 'SELECT id, title, owner, color, archived, deleted_at, last_modified FROM `*PREFIX*deck_boards` ' .
'WHERE `id` = ?';
$board = $this->findEntity($sql, [$id]);
// Add labels
if ($withLabels && $this->boardCache[$id]->getLabels() === null) {
if ($withLabels) {
$labels = $this->labelMapper->findAll($id);
$this->boardCache[$id]->setLabels($labels);
$board->setLabels($labels);
}
// Add acl
if ($withAcl && $this->boardCache[$id]->getAcl() === null) {
if ($withAcl) {
$acl = $this->aclMapper->findAll($id);
$this->boardCache[$id]->setAcl($acl);
$board->setAcl($acl);
}
return $this->boardCache[$id];
return $board;
}
public function findAllForUser(string $userId, ?int $since = null, bool $includeArchived = true, ?int $before = null,
?string $term = null): array {
$useCache = ($since === -1 && $includeArchived === true && $before === null && $term === null);
public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array {
$useCache = ($since === -1 && $includeArchived === true);
if (!isset($this->userBoardCache[$userId]) || !$useCache) {
$groups = $this->groupManager->getUserGroupIds(
$this->userManager->get($userId)
);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived, $before, $term);
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived, $before, $term);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived, $before, $term);
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived);
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
$allBoards = array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
foreach ($allBoards as $board) {
$this->boardCache[$board->getId()] = $board;
}
if ($useCache) {
$this->userBoardCache[$userId] = $allBoards;
}
@@ -137,91 +120,19 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
* @param null $offset
* @return array
*/
public function findAllByUser(string $userId, ?int $limit = null, ?int $offset = null, ?int $since = null,
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
// FIXME this used to be a UNION to get boards owned by $userId and the user shares in one single query
// Is it possible with the query builder?
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
// this does not work in MySQL/PostgreSQL
//->selectAlias('0', 'shared')
->from('deck_boards', 'b')
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
public function findAllByUser($userId, $limit = null, $offset = null, $since = -1, $includeArchived = true) {
// FIXME: One moving to QBMapper we should allow filtering the boards probably by method chaining for additional where clauses
$sql = 'SELECT id, title, owner, color, archived, deleted_at, 0 as shared, last_modified FROM `*PREFIX*deck_boards` WHERE owner = ? AND last_modified > ?';
if (!$includeArchived) {
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$sql .= ' AND NOT archived AND deleted_at = 0';
}
if ($since !== null) {
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
}
if ($before !== null) {
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
}
if ($term !== null) {
$qb->andWhere(
$qb->expr()->iLike(
'title',
$qb->createNamedParameter(
'%' . $this->db->escapeLikeParameter($term) . '%',
IQueryBuilder::PARAM_STR
)
)
);
}
$qb->orderBy('b.id');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$entries = $this->findEntities($qb);
foreach ($entries as $entry) {
$entry->setShared(0);
}
// shared with user
$qb->resetQueryParts();
$qb->select('b.id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
//->selectAlias('1', 'shared')
->from('deck_boards', 'b')
->innerJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'))
->where($qb->expr()->eq('acl.participant', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_USER, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->neq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
$sql .= ' UNION ' .
'SELECT boards.id, title, owner, color, archived, deleted_at, 1 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
'JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE acl.participant=? AND acl.type=? AND boards.owner != ? AND last_modified > ?';
if (!$includeArchived) {
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$sql .= ' AND NOT archived AND deleted_at = 0';
}
if ($since !== null) {
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
}
if ($before !== null) {
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
}
if ($term !== null) {
$qb->andWhere(
$qb->expr()->iLike(
'title',
$qb->createNamedParameter(
'%' . $this->db->escapeLikeParameter($term) . '%',
IQueryBuilder::PARAM_STR
)
)
);
}
$qb->orderBy('b.id');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$sharedEntries = $this->findEntities($qb);
foreach ($sharedEntries as $entry) {
$entry->setShared(1);
}
$entries = array_merge($entries, $sharedEntries);
$entries = $this->findEntities($sql, [$userId, $since, $userId, Acl::PERMISSION_TYPE_USER, $userId, $since], $limit, $offset);
/* @var Board $entry */
foreach ($entries as $entry) {
$acl = $this->aclMapper->findAll($entry->id);
@@ -230,19 +141,9 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
return $entries;
}
public function findAllByOwner(string $userId, ?int $limit = null, ?int $offset = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_boards')
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
->orderBy('id');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
return $this->findEntities($qb);
public function findAllByOwner(string $userId, int $limit = null, int $offset = null) {
$sql = 'SELECT * FROM `*PREFIX*deck_boards` WHERE owner = ?';
return $this->findEntities($sql, [$userId], $limit, $offset);
}
/**
@@ -254,57 +155,23 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
* @param null $offset
* @return array
*/
public function findAllByGroups(string $userId, array $groups, ?int $limit = null, ?int $offset = null, ?int $since = null,
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
public function findAllByGroups($userId, $groups, $limit = null, $offset = null, $since = -1,$includeArchived = true) {
if (count($groups) <= 0) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('b.id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
//->selectAlias('2', 'shared')
->from('deck_boards', 'b')
->innerJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'))
->where($qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_GROUP, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->neq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
$or = $qb->expr()->orx();
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND (';
for ($i = 0, $iMax = count($groups); $i < $iMax; $i++) {
$or->add(
$qb->expr()->eq('acl.participant', $qb->createNamedParameter($groups[$i], IQueryBuilder::PARAM_STR))
);
$sql .= 'acl.participant = ? ';
if (count($groups) > 1 && $i < count($groups) - 1) {
$sql .= ' OR ';
}
}
$qb->andWhere($or);
$sql .= ')';
if (!$includeArchived) {
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
}
if ($since !== null) {
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
}
if ($before !== null) {
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
}
if ($term !== null) {
$qb->andWhere(
$qb->expr()->iLike(
'title',
$qb->createNamedParameter(
'%' . $this->db->escapeLikeParameter($term) . '%',
IQueryBuilder::PARAM_STR
)
)
);
}
$qb->orderBy('b.id');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$entries = $this->findEntities($qb);
foreach ($entries as $entry) {
$entry->setShared(2);
$sql .= ' AND NOT archived AND deleted_at = 0';
}
$entries = $this->findEntities($sql, array_merge([$userId, Acl::PERMISSION_TYPE_GROUP], $groups), $limit, $offset);
/* @var Board $entry */
foreach ($entries as $entry) {
$acl = $this->aclMapper->findAll($entry->id);
@@ -313,59 +180,25 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
return $entries;
}
public function findAllByCircles(string $userId, ?int $limit = null, ?int $offset = null, ?int $since = null,
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
public function findAllByCircles($userId, $limit = null, $offset = null, $since = -1,$includeArchived = true) {
$circles = $this->circlesService->getUserCircles($userId);
if (count($circles) === 0) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('b.id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
//->selectAlias('2', 'shared')
->from('deck_boards', 'b')
->innerJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'))
->where($qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_CIRCLE, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->neq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
$or = $qb->expr()->orx();
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND (';
for ($i = 0, $iMax = count($circles); $i < $iMax; $i++) {
$or->add(
$qb->expr()->eq('acl.participant', $qb->createNamedParameter($circles[$i], IQueryBuilder::PARAM_STR))
);
$sql .= 'acl.participant = ? ';
if (count($circles) > 1 && $i < count($circles) - 1) {
$sql .= ' OR ';
}
}
$qb->andWhere($or);
$sql .= ')';
if (!$includeArchived) {
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
}
if ($since !== null) {
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
}
if ($before !== null) {
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
}
if ($term !== null) {
$qb->andWhere(
$qb->expr()->iLike(
'title',
$qb->createNamedParameter(
'%' . $this->db->escapeLikeParameter($term) . '%',
IQueryBuilder::PARAM_STR
)
)
);
}
$qb->orderBy('b.id');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$entries = $this->findEntities($qb);
foreach ($entries as $entry) {
$entry->setShared(2);
$sql .= ' AND NOT archived AND deleted_at = 0';
}
$entries = $this->findEntities($sql, array_merge([$userId, Acl::PERMISSION_TYPE_CIRCLE], $circles), $limit, $offset);
/* @var Board $entry */
foreach ($entries as $entry) {
$acl = $this->aclMapper->findAll($entry->id);
@@ -374,26 +207,21 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
return $entries;
}
public function findAll(): array {
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from('deck_boards');
return $this->findEntities($qb);
public function findAll() {
$sql = 'SELECT id from *PREFIX*deck_boards;';
return $this->findEntities($sql);
}
public function findToDelete() {
// add buffer of 5 min
$timeLimit = time() - (60 * 5);
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
->from('deck_boards')
->where($qb->expr()->gt('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($timeLimit, IQueryBuilder::PARAM_INT)));
return $this->findEntities($qb);
$sql = 'SELECT id, title, owner, color, archived, deleted_at, last_modified FROM `*PREFIX*deck_boards` ' .
'WHERE `deleted_at` > 0 AND `deleted_at` < ?';
return $this->findEntities($sql, [$timeLimit]);
}
public function delete(/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
\OCP\AppFramework\Db\Entity $entity): \OCP\AppFramework\Db\Entity {
\OCP\AppFramework\Db\Entity $entity) {
// delete acl
$acl = $this->aclMapper->findAll($entity->getId());
foreach ($acl as $item) {

View File

@@ -30,8 +30,6 @@ use OCA\Deck\Search\Query\SearchQuery;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUser;
@@ -48,8 +46,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
private $groupManager;
/** @var IManager */
private $notificationManager;
/** @var ICache */
private $cache;
private $databaseType;
private $database4ByteSupport;
@@ -59,7 +55,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
IUserManager $userManager,
IGroupManager $groupManager,
IManager $notificationManager,
ICacheFactory $cacheFactory,
$databaseType = 'sqlite3',
$database4ByteSupport = true
) {
@@ -68,7 +63,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->notificationManager = $notificationManager;
$this->cache = $cacheFactory->createDistributed('deck-cardMapper');
$this->databaseType = $databaseType;
$this->database4ByteSupport = $database4ByteSupport;
}
@@ -81,9 +75,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
$description = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $entity->getDescription());
$entity->setDescription($description);
}
$entity = parent::insert($entity);
$this->cache->remove('findBoardId:' . $entity->getId());
return $entity;
return parent::insert($entity);
}
public function update(Entity $entity, $updateModified = true): Entity {
@@ -115,10 +107,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
} catch (Exception $e) {
}
}
// Invalidate cache when the card may be moved to a different board
if (isset($updatedFields['stackId'])) {
$this->cache->remove('findBoardId:' . $entity->getId());
}
return parent::update($entity);
}
@@ -265,7 +253,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 +264,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'));
@@ -491,8 +479,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
}
return $qb->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATE);
}
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoards($boardIds)
@@ -518,8 +506,9 @@ class CardMapper extends QBMapper implements IPermissionMapper {
}
public function delete(Entity $entity): Entity {
// delete assigned labels
$this->labelMapper->deleteLabelAssignmentsForCard($entity->getId());
$this->cache->remove('findBoardId:' . $entity->getId());
// delete card
return parent::delete($entity);
}
@@ -558,22 +547,11 @@ class CardMapper extends QBMapper implements IPermissionMapper {
}
public function findBoardId($id): ?int {
$result = $this->cache->get('findBoardId:' . $id);
if ($result === null) {
try {
$qb = $this->db->getQueryBuilder();
$qb->select('board_id')
->from('deck_stacks', 's')
->innerJoin('s', 'deck_cards', 'c', 'c.stack_id = s.id')
->where($qb->expr()->eq('c.id', $qb->createNamedParameter($id)));
$queryResult = $qb->executeQuery();
$result = $queryResult->fetchOne();
} catch (\Exception $e) {
$result = false;
}
$this->cache->set('findBoardId:' . $id, $result);
}
return $result !== false ? $result : null;
$sql = 'SELECT id 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, $id, \PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchColumn() ?? null;
}
public function mapOwner(Card &$card) {

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

@@ -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

@@ -50,6 +50,7 @@ use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\LabelMapper;
use OCP\IUserManager;
use OCA\Deck\BadRequestException;
use OCP\IURLGenerator;
class BoardService {
private $boardMapper;
@@ -68,8 +69,8 @@ class BoardService {
private $activityManager;
private $eventDispatcher;
private $changeHelper;
private $boardsCache = null;
private $urlGenerator;
public function __construct(
@@ -87,6 +88,7 @@ class BoardService {
ActivityManager $activityManager,
IEventDispatcher $eventDispatcher,
ChangeHelper $changeHelper,
IURLGenerator $urlGenerator,
$userId
) {
$this->boardMapper = $boardMapper;
@@ -104,6 +106,7 @@ class BoardService {
$this->eventDispatcher = $eventDispatcher;
$this->changeHelper = $changeHelper;
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
}
/**
@@ -118,9 +121,8 @@ class BoardService {
/**
* Get all boards that are shared with a user, their groups or circles
*/
public function getUserBoards(?int $since = null, bool $includeArchived = true, ?int $before = null,
?string $term = null): array {
return $this->boardMapper->findAllForUser($this->userId, $since, $includeArchived, $before, $term);
public function getUserBoards(int $since = -1, bool $includeArchived = true): array {
return $this->boardMapper->findAllForUser($this->userId, $since, $includeArchived);
}
/**
@@ -179,11 +181,9 @@ class BoardService {
/** @var Board $board */
$board = $this->boardMapper->find($boardId, true, true);
$this->boardMapper->mapOwner($board);
if ($board->getAcl() !== null) {
foreach ($board->getAcl() as $acl) {
if ($acl !== null) {
$this->boardMapper->mapAcl($acl);
}
foreach ($board->getAcl() as &$acl) {
if ($acl !== null) {
$this->boardMapper->mapAcl($acl);
}
}
$permissions = $this->permissionService->matchPermissions($board);
@@ -697,4 +697,8 @@ class BoardService {
}
$board->setUsers(array_values($boardUsers));
}
public function getBoardUrl($endpoint) {
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . '#' . $endpoint;
}
}

View File

@@ -46,6 +46,7 @@ use OCA\Deck\BadRequestException;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUserManager;
use OCP\IURLGenerator;
class CardService {
private $cardMapper;
@@ -63,6 +64,7 @@ class CardService {
private $changeHelper;
private $eventDispatcher;
private $userManager;
private $urlGenerator;
public function __construct(
CardMapper $cardMapper,
@@ -79,6 +81,7 @@ class CardService {
IUserManager $userManager,
ChangeHelper $changeHelper,
IEventDispatcher $eventDispatcher,
IURLGenerator $urlGenerator,
$userId
) {
$this->cardMapper = $cardMapper;
@@ -96,6 +99,7 @@ class CardService {
$this->changeHelper = $changeHelper;
$this->eventDispatcher = $eventDispatcher;
$this->currentUser = $userId;
$this->urlGenerator = $urlGenerator;
}
public function enrich($card) {
@@ -602,4 +606,14 @@ class CardService {
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
}
public function getCardUrl($cardId) {
$boardId = $this->cardMapper->findBoardId($cardId);
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . "#/board/$boardId/card/$cardId";
}
public function getRedirectUrlForCard($cardId) {
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . "card/$cardId";
}
}

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

@@ -59,7 +59,7 @@ class FullTextSearchService {
/** @var CardMapper */
private $cardMapper;
public function __construct(
BoardMapper $boardMapper, StackMapper $stackMapper, CardMapper $cardMapper
) {
@@ -187,6 +187,6 @@ class FullTextSearchService {
* @return Board[]
*/
private function getBoardsFromUser(string $userId): array {
return $this->boardMapper->findAllByUser($userId, null, null, null);
return $this->boardMapper->findAllByUser($userId, null, null, -1);
}
}

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

@@ -117,7 +117,7 @@ class PermissionService {
*/
public function matchPermissions(Board $board) {
$owner = $this->userIsBoardOwner($board->getId());
$acls = $board->getAcl() ?? [];
$acls = $board->getAcl();
return [
Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ),
Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT),
@@ -155,7 +155,7 @@ class PermissionService {
}
try {
$acls = $this->getBoard($boardId)->getAcl() ?? [];
$acls = $this->getBoard($boardId)->getAcl();
$result = $this->userCan($acls, $permission, $userId);
if ($result) {
return true;

View File

@@ -90,19 +90,25 @@ class SearchService {
}
public function searchBoards(string $term, ?int $limit, ?int $cursor): array {
$boards = $this->boardService->getUserBoards(null, true, $cursor, mb_strtolower($term));
$boards = $this->boardService->getUserBoards();
// get boards that have a lastmodified date which is lower than the cursor
// and which match the search term
$filteredBoards = array_filter($boards, static function (Board $board) use ($term, $cursor) {
return (
($cursor === null || $board->getLastModified() < $cursor)
&& mb_stripos(mb_strtolower($board->getTitle()), mb_strtolower($term)) > -1
);
});
// sort the boards, recently modified first
usort($boards, function ($boardA, $boardB) {
usort($filteredBoards, function ($boardA, $boardB) {
$ta = $boardA->getLastModified();
$tb = $boardB->getLastModified();
return $ta === $tb
? 0
: ($ta > $tb ? -1 : 1);
});
// limit the number of results
return array_slice($boards, 0, $limit);
return array_slice($filteredBoards, 0, $limit);
}
public function searchComments(string $term, ?int $limit = null, ?int $cursor = null): array {

View File

@@ -500,7 +500,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');

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

15325
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-beta1",
"version": "1.5.6",
"authors": [
{
"name": "Julius Härtl",
@@ -29,25 +29,24 @@
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"@babel/runtime": "^7.17.2",
"@babel/runtime": "^7.14.6",
"@juliushaertl/vue-richtext": "^1.0.1",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.9.0",
"@nextcloud/axios": "^1.6.0",
"@nextcloud/dialogs": "^3.1.2",
"@nextcloud/event-bus": "^2.1.1",
"@nextcloud/files": "^2.1.0",
"@nextcloud/initial-state": "^1.2.1",
"@nextcloud/event-bus": "^2.0.0",
"@nextcloud/files": "^2.0.0",
"@nextcloud/initial-state": "^1.2.0",
"@nextcloud/l10n": "^1.4.1",
"@nextcloud/moment": "^1.1.1",
"@nextcloud/router": "^2.0.0",
"@nextcloud/vue": "^5.0.0",
"@nextcloud/vue": "^3.10.1",
"@nextcloud/vue-dashboard": "^2.0.1",
"blueimp-md5": "^2.19.0",
"dompurify": "^2.3.6",
"blueimp-md5": "^2.18.0",
"dompurify": "^2.2.9",
"lodash": "^4.17.21",
"markdown-it": "^12.3.2",
"markdown-it": "^12.0.6",
"markdown-it-task-lists": "^2.1.1",
"markdown-it-link-attributes": "^4.0.0",
"moment": "^2.29.1",
"nextcloud-vue-collections": "^0.9.0",
"p-queue": "^6.6.2",
@@ -55,9 +54,9 @@
"vue": "^2.6.14",
"vue-at": "^2.5.0-beta.2",
"vue-click-outside": "^1.1.0",
"vue-easymde": "^2.0.0",
"vue-easymde": "^1.4.0",
"vue-infinite-loading": "^2.4.5",
"vue-router": "^3.5.3",
"vue-router": "^3.5.1",
"vue-smooth-dnd": "^0.8.1",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
@@ -66,18 +65,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.1",
"@vue/test-utils": "^1.3.0",
"jest": "^27.5.1",
"@nextcloud/babel-config": "^1.0.0-beta.1",
"@nextcloud/browserslist-config": "^2.1.0",
"@nextcloud/eslint-config": "^5.1.0",
"@nextcloud/stylelint-config": "^1.0.0-beta.0",
"@nextcloud/webpack-vue-config": "^4.0.3",
"@relative-ci/agent": "^2.0.0",
"@vue/test-utils": "^1.2.1",
"jest": "^27.0.4",
"jest-serializer-vue": "^2.0.2",
"vue-jest": "^3.0.7"
},
@@ -97,4 +96,4 @@
"<rootDir>/node_modules/jest-serializer-vue"
]
}
}
}

View File

@@ -59,11 +59,6 @@ export default {
Content,
AppContent,
},
provide() {
return {
boardApi,
}
},
data() {
return {
addButton: {
@@ -88,6 +83,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() {
@@ -97,14 +93,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')
@@ -124,6 +112,11 @@ export default {
this.$router.push({ name: 'board' })
},
},
provide() {
return {
boardApi,
}
},
}
</script>

View File

@@ -76,108 +76,110 @@
<ActionButton v-else icon="icon-filter" />
</Actions>
<div v-if="filterVisible" class="filter">
<h3>{{ t('deck', 'Filter by tag') }}</h3>
<div v-for="label in labelsSorted" :key="label.id" class="filter--item">
<input
:id="label.id"
v-model="filter.tags"
type="checkbox"
class="checkbox"
:value="label.id"
@change="setFilter">
<label :for="label.id"><span class="label" :style="labelStyle(label)">{{ label.title }}</span></label>
</div>
<template>
<div v-if="filterVisible" class="filter">
<h3>{{ t('deck', 'Filter by tag') }}</h3>
<div v-for="label in labelsSorted" :key="label.id" class="filter--item">
<input
:id="label.id"
v-model="filter.tags"
type="checkbox"
class="checkbox"
:value="label.id"
@change="setFilter">
<label :for="label.id"><span class="label" :style="labelStyle(label)">{{ label.title }}</span></label>
</div>
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
<div class="filter--item">
<input
id="unassigned"
v-model="filter.unassigned"
type="checkbox"
class="checkbox"
value="unassigned"
@change="setFilter"
@click="beforeSetFilter">
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
</div>
<div v-for="user in board.users" :key="user.uid" class="filter--item">
<input
:id="user.uid"
v-model="filter.users"
type="checkbox"
class="checkbox"
:value="user.uid"
@change="setFilter">
<label :for="user.uid"><Avatar :user="user.uid" :size="24" :disable-menu="true" /> {{ user.displayname }}</label>
</div>
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
<div class="filter--item">
<input
id="unassigned"
v-model="filter.unassigned"
type="checkbox"
class="checkbox"
value="unassigned"
@change="setFilter"
@click="beforeSetFilter">
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
</div>
<div v-for="user in board.users" :key="user.uid" class="filter--item">
<input
:id="user.uid"
v-model="filter.users"
type="checkbox"
class="checkbox"
:value="user.uid"
@change="setFilter">
<label :for="user.uid"><Avatar :user="user.uid" :size="24" :disable-menu="true" /> {{ user.displayname }}</label>
</div>
<h3>{{ t('deck', 'Filter by due date') }}</h3>
<h3>{{ t('deck', 'Filter by due date') }}</h3>
<div class="filter--item">
<input
id="overdue"
v-model="filter.due"
type="radio"
class="radio"
value="overdue"
@change="setFilter"
@click="beforeSetFilter">
<label for="overdue">{{ t('deck', 'Overdue') }}</label>
<div class="filter--item">
<input
id="overdue"
v-model="filter.due"
type="radio"
class="radio"
value="overdue"
@change="setFilter"
@click="beforeSetFilter">
<label for="overdue">{{ t('deck', 'Overdue') }}</label>
</div>
<div class="filter--item">
<input
id="dueToday"
v-model="filter.due"
type="radio"
class="radio"
value="dueToday"
@change="setFilter"
@click="beforeSetFilter">
<label for="dueToday">{{ t('deck', 'Next 24 hours') }}</label>
</div>
<div class="filter--item">
<input
id="dueWeek"
v-model="filter.due"
type="radio"
class="radio"
value="dueWeek"
@change="setFilter"
@click="beforeSetFilter">
<label for="dueWeek">{{ t('deck', 'Next 7 days') }}</label>
</div>
<div class="filter--item">
<input
id="dueMonth"
v-model="filter.due"
type="radio"
class="radio"
value="dueMonth"
@change="setFilter"
@click="beforeSetFilter">
<label for="dueMonth">{{ t('deck', 'Next 30 days') }}</label>
</div>
<div class="filter--item">
<input
id="noDue"
v-model="filter.due"
type="radio"
class="radio"
value="noDue"
@change="setFilter"
@click="beforeSetFilter">
<label for="noDue">{{ t('deck', 'No due date') }}</label>
</div>
<Button :disabled="!isFilterActive" @click="clearFilter">
{{ t('deck', 'Clear filter') }}
</Button>
</div>
<div class="filter--item">
<input
id="dueToday"
v-model="filter.due"
type="radio"
class="radio"
value="dueToday"
@change="setFilter"
@click="beforeSetFilter">
<label for="dueToday">{{ t('deck', 'Next 24 hours') }}</label>
</div>
<div class="filter--item">
<input
id="dueWeek"
v-model="filter.due"
type="radio"
class="radio"
value="dueWeek"
@change="setFilter"
@click="beforeSetFilter">
<label for="dueWeek">{{ t('deck', 'Next 7 days') }}</label>
</div>
<div class="filter--item">
<input
id="dueMonth"
v-model="filter.due"
type="radio"
class="radio"
value="dueMonth"
@change="setFilter"
@click="beforeSetFilter">
<label for="dueMonth">{{ t('deck', 'Next 30 days') }}</label>
</div>
<div class="filter--item">
<input
id="noDue"
v-model="filter.due"
type="radio"
class="radio"
value="noDue"
@change="setFilter"
@click="beforeSetFilter">
<label for="noDue">{{ t('deck', 'No due date') }}</label>
</div>
<Button :disabled="!isFilterActive" @click="clearFilter">
{{ t('deck', 'Clear filter') }}
</Button>
</div>
</template>
</Popover>
<Actions>
@@ -239,7 +241,6 @@ export default {
isAddStackVisible: false,
filter: { tags: [], users: [], due: '', unassigned: false },
showAddCardModal: false,
defaultPageTitle: false,
}
},
@@ -267,17 +268,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 +332,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

@@ -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

@@ -43,22 +43,24 @@
<li v-if="addLabel" class="editing">
<!-- New Tag -->
<form class="label-form" @submit.prevent="clickAddLabel">
<ColorPicker class="color-picker-wrapper" :value="'#' + addLabelObj.color" @input="updateColor">
<div :style="{ backgroundColor: '#' + addLabelObj.color }" class="color0 icon-colorpicker" />
</ColorPicker>
<input v-model="addLabelObj.title" type="text">
<input v-tooltip="{content: missingDataLabel, show: !addLabelObjValidated, trigger: 'manual' }"
:disabled="!addLabelObjValidated"
type="submit"
value=""
class="icon-confirm">
<Actions>
<ActionButton icon="icon-close" @click="addLabel=false">
{{ t('deck', 'Cancel') }}
</ActionButton>
</Actions>
</form>
<template>
<form class="label-form" @submit.prevent="clickAddLabel">
<ColorPicker class="color-picker-wrapper" :value="'#' + addLabelObj.color" @input="updateColor">
<div :style="{ backgroundColor: '#' + addLabelObj.color }" class="color0 icon-colorpicker" />
</ColorPicker>
<input v-model="addLabelObj.title" type="text">
<input v-tooltip="{content: missingDataLabel, show: !addLabelObjValidated, trigger: 'manual' }"
:disabled="!addLabelObjValidated"
type="submit"
value=""
class="icon-confirm">
<Actions>
<ActionButton icon="icon-close" @click="addLabel=false">
{{ t('deck', 'Cancel') }}
</ActionButton>
</Actions>
</form>
</template>
</li>
<button v-if="canManage && !isArchived" @click="clickShowAddLabel()">
<span class="icon-add" />{{ t('deck', 'Add a new tag') }}

View File

@@ -71,7 +71,7 @@
</a>
</div>
<Actions v-if="selectable">
<ActionButton icon="icon-confirm" @click="$emit('select-attachment', attachment)">
<ActionButton icon="icon-confirm" @click="$emit('selectAttachment', attachment)">
{{ t('deck', 'Add this attachment') }}
</ActionButton>
</Actions>
@@ -89,10 +89,10 @@
{{ t('deck', 'Remove attachment') }}
</ActionButton>
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('delete-attachment', attachment)">
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
{{ t('deck', 'Delete Attachment') }}
</ActionButton>
<ActionButton v-else-if="!attachment.extendedData.fileid" icon="icon-history" @click="$emit('restore-attachment', attachment)">
<ActionButton v-else-if="!attachment.extendedData.fileid" icon="icon-history" @click="$emit('restoreAttachment', attachment)">
{{ t('deck', 'Restore Attachment') }}
</ActionButton>
</Actions>
@@ -222,7 +222,7 @@ export default {
},
shareFromFiles() {
picker.pick()
.then(async (path) => {
.then(async(path) => {
console.debug(`path ${path} selected for sharing`)
if (!path.startsWith('/')) {
throw new Error(t('files', 'Invalid path selected'))

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)
},
},
}

View File

@@ -24,8 +24,8 @@
<AttachmentList
:card-id="card.id"
:removable="true"
@delete-attachment="deleteAttachment"
@restore-attachment="restoreAttachment" />
@deleteAttachment="deleteAttachment"
@restoreAttachment="restoreAttachment" />
</template>
<script>

View File

@@ -101,7 +101,6 @@
:lang="lang"
:formatter="format"
:disabled="saving || !canEdit"
:shortcuts="shortcuts"
confirm />
<Actions v-if="canEdit">
<ActionButton v-if="copiedCard.duedate" icon="icon-delete" @click="removeDue()">
@@ -177,53 +176,12 @@ export default {
stringify: this.stringify,
parse: this.parse,
},
shortcuts: [
{
text: t('deck', 'Today'),
onClick() {
const date = new Date()
date.setDate(date.getDate())
date.setHours(23)
date.setMinutes(59)
return date
},
},
{
text: t('deck', 'Tomorrow'),
onClick() {
const date = new Date()
date.setDate(date.getDate() + 1)
date.setHours(23)
date.setMinutes(59)
return date
},
},
{
text: t('deck', 'Next week'),
onClick() {
const date = new Date()
date.setDate(date.getDate() + 7)
date.setHours(23)
date.setMinutes(59)
return date
},
},
{
text: t('deck', 'Next month'),
onClick() {
const date = new Date()
date.setDate(date.getDate() + 30)
date.setHours(23)
date.setMinutes(59)
return date
},
},
],
}
},
computed: {
...mapState({
currentBoard: state => state.currentBoard,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
...mapGetters(['canEdit', 'assignables']),
formatedAssignables() {
@@ -249,14 +207,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
@@ -366,14 +316,6 @@ export default {
</script>
<style lang="scss" scoped>
.section-wrapper::v-deep .mx-datepicker-main.mx-datepicker-popup {
left: 0 !important;
}
.section-wrapper::v-deep .mx-datepicker-main.mx-datepicker-popup.mx-datepicker-sidebar {
padding: 0 !important;
}
.section-wrapper {
display: flex;
max-width: 100%;

View File

@@ -19,45 +19,47 @@
</div>
</div>
<li v-else class="comment">
<div class="comment--header">
<Avatar :user="comment.actorId" />
<span class="has-tooltip username">
{{ comment.actorDisplayName }}
</span>
<Actions v-show="!edit" :force-menu="true">
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
{{ t('deck', 'Reply') }}
</ActionButton>
<ActionButton v-if="canEdit"
icon="icon-rename"
:close-after-click="true"
@click="showUpdateForm()">
{{ t('deck', 'Update') }}
</ActionButton>
<ActionButton v-if="canEdit"
icon="icon-delete"
:close-after-click="true"
@click="deleteComment()">
{{ t('deck', 'Delete') }}
</ActionButton>
</Actions>
<Actions v-if="edit">
<ActionButton icon="icon-close" @click="hideUpdateForm" />
</Actions>
<div class="spacer" />
<div class="timestamp">
{{ relativeDate(comment.creationDateTime) }}
<template>
<div class="comment--header">
<Avatar :user="comment.actorId" />
<span class="has-tooltip username">
{{ comment.actorDisplayName }}
</span>
<Actions v-show="!edit" :force-menu="true">
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
{{ t('deck', 'Reply') }}
</ActionButton>
<ActionButton v-if="canEdit"
icon="icon-rename"
:close-after-click="true"
@click="showUpdateForm()">
{{ t('deck', 'Update') }}
</ActionButton>
<ActionButton v-if="canEdit"
icon="icon-delete"
:close-after-click="true"
@click="deleteComment()">
{{ t('deck', 'Delete') }}
</ActionButton>
</Actions>
<Actions v-if="edit">
<ActionButton icon="icon-close" @click="hideUpdateForm" />
</Actions>
<div class="spacer" />
<div class="timestamp">
{{ relativeDate(comment.creationDateTime) }}
</div>
</div>
</div>
<CommentItem v-if="comment.replyTo" :reply="true" :comment="comment.replyTo" />
<div v-show="!edit" ref="richTextElement">
<RichText
class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
</div>
<CommentForm v-if="edit" v-model="commentMsg" @submit="updateComment" />
<CommentItem v-if="comment.replyTo" :reply="true" :comment="comment.replyTo" />
<div v-show="!edit" ref="richTextElement">
<RichText
class="comment--content"
:text="richText(comment)"
:arguments="richArgs(comment)"
:autolink="true" />
</div>
<CommentForm v-if="edit" v-model="commentMsg" @submit="updateComment" />
</template>
</li>
</template>

View File

@@ -57,7 +57,7 @@
ref="markdownEditor"
v-model="description"
:configs="mdeConfig"
@update:modelValue="updateDescription"
@input="updateDescription"
@blur="saveDescription" />
<Modal v-if="modalShow" :title="t('deck', 'Choose attachment')" @close="modalShow=false">
@@ -66,7 +66,7 @@
<AttachmentList
:card-id="card.id"
:selectable="true"
@select-attachment="addAttachment" />
@selectAttachment="addAttachment" />
</div>
</Modal>
</div>
@@ -75,7 +75,6 @@
<script>
import MarkdownIt from 'markdown-it'
import MarkdownItTaskLists from 'markdown-it-task-lists'
import MarkdownItLinkAttributes from 'markdown-it-link-attributes'
import AttachmentList from './AttachmentList'
import { Actions, ActionButton, Modal } from '@nextcloud/vue'
import { formatFileSize } from '@nextcloud/files'
@@ -87,13 +86,6 @@ const markdownIt = new MarkdownIt({
})
markdownIt.use(MarkdownItTaskLists, { enabled: true, label: true, labelAfter: true })
markdownIt.use(MarkdownItLinkAttributes, {
attrs: {
target: '_blank',
rel: 'noreferrer noopener',
},
})
export default {
name: 'Description',
components: {
@@ -132,6 +124,7 @@ export default {
computed: {
...mapState({
currentBoard: state => state.currentBoard,
cardDetailsInModal: state => state.cardDetailsInModal,
}),
...mapGetters(['canEdit']),
attachments() {
@@ -230,7 +223,7 @@ export default {
updateDescription() {
this.descriptionLastEdit = Date.now()
clearTimeout(this.descriptionSaveTimeout)
this.descriptionSaveTimeout = setTimeout(async () => {
this.descriptionSaveTimeout = setTimeout(async() => {
await this.saveDescription()
}, 2500)
},
@@ -250,8 +243,6 @@ export default {
&::v-deep .attachment-list {
flex-shrink: 1;
overflow: scroll;
max-height: 50vh;
}
}
@@ -262,8 +253,6 @@ export default {
#description-preview {
min-height: 100px;
width: auto;
overflow-x: auto;
&::v-deep {
@import './../../css/markdown';
@@ -334,15 +323,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

@@ -53,19 +53,6 @@
<PopoverMenu :menu="popover" />
<slot />
</div>
<div class="avatar-print-list">
<div v-for="user in avatarUsers" :key="user.id" class="avatar-print-list-item">
<Avatar
class="avatar-print-list-avatar"
:user="user.participant.uid"
:display-name="user.participant.displayname"
:disable-menu="true"
:is-no-user="user.type !== 0"
:size="24" />
{{ user.participant.displayname }}
</div>
</div>
</div>
</template>
@@ -129,15 +116,6 @@ export default {
}),
]
},
avatarUsers() {
if (!this.users) {
return []
}
return this.users.filter((user) => {
return [0, 1, 7].includes(user.type)
})
},
},
methods: {
togglePopover() {
@@ -198,26 +176,4 @@ export default {
display: block;
margin: 40px -6px;
}
.avatar-print-list {
display: none;
}
@media print {
.avatar-list {
display: none;
}
.avatar-print-list-item {
align-items: center;
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.avatar-print-list {
display: block;
padding-top: 5px;
}
}
</style>

View File

@@ -41,7 +41,7 @@
<AvatarList :users="card.assignedUsers" />
<CardMenu class="card-menu" :card="card" />
<CardMenu :card="card" />
</div>
</template>
<script>
@@ -150,15 +150,4 @@ export default {
.fade-enter, .fade-leave-to {
opacity: 0;
}
@media print {
.badges {
align-items: flex-start;
max-height: none !important;
}
.card-menu {
display: none;
}
}
</style>

View File

@@ -66,11 +66,7 @@
:placeholder="t('deck', 'Select a list')"
:options="stacksFromBoard"
:max-height="100"
label="title">
<span slot="noOptions">
{{ t('deck', 'List is empty') }}
</span>
</Multiselect>
label="title" />
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="moveCard">
{{ t('deck', 'Move card') }}

View File

@@ -23,7 +23,7 @@
<template>
<div v-if="card" class="duedate">
<transition name="zoom">
<div v-if="card.duedate" :class="dueIcon" :title="absoluteDate">
<div v-if="card.duedate" :class="dueIcon">
<span>{{ relativeDate }}</span>
</div>
</transition>
@@ -62,14 +62,14 @@ export default {
}
return moment(this.card.duedate).fromNow()
},
absoluteDate() {
return moment(this.card.duedate).format('L')
dueDateTooltip() {
return moment(this.card.duedate).format('LLLL')
},
},
}
</script>
<style lang="scss" scoped>
<style lang="scss" coped>
.icon.due {
background-position: 4px center;
border-radius: 3px;
@@ -105,7 +105,6 @@ export default {
padding: 3px 4px;
}
&::before,
span {
margin-left: 20px;
white-space: nowrap;
@@ -113,18 +112,4 @@ export default {
overflow: hidden;
}
}
@media print {
.icon.due {
background-color: transparent !important;
span {
display: none;
}
&::before {
content: attr(title);
}
}
}
</style>

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

@@ -39,9 +39,6 @@
<script>
import { ColorPicker, ActionButton, Actions, AppNavigationItem } from '@nextcloud/vue'
/**
*
*/
function randomColor() {
let randomHexColor = ((1 << 24) * Math.random() | 0).toString(16)
while (randomHexColor.length < 6) {

View File

@@ -125,10 +125,7 @@
<div :style="{ backgroundColor: getColor }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
</ColorPicker>
<form @submit.prevent.stop="applyEdit">
<input v-model="editTitle"
v-focus
type="text"
required>
<input v-model="editTitle" type="text" required>
<input type="submit" value="" class="icon-confirm">
<Actions><ActionButton icon="icon-close" @click.stop.prevent="cancelEdit" /></Actions>
</form>

View File

@@ -63,15 +63,10 @@ import { Actions, ActionButton } from '@nextcloud/vue'
const createCancelToken = () => axios.CancelToken.source()
/**
* @param root0
* @param root0.query
* @param root0.cursor
*/
function search({ query, cursor }) {
const cancelToken = createCancelToken()
const request = async () => axios.get(generateOcsUrl('apps/deck/api/v1.0/search'), {
const request = async() => axios.get(generateOcsUrl('apps/deck/api/v1.0/search'), {
cancelToken: cancelToken.token,
params: {
term: query,

View File

@@ -21,11 +21,6 @@ ul {
ul {
list-style-type: disc;
.task-list-item {
margin-left: -20px;
list-style-type: none;
}
}
h1 {
@@ -72,7 +67,7 @@ img {
}
input[type=checkbox] {
margin: 0px 3px 0px 0px;
margin: 0px 10px 0px 0px;
line-height: 10px;
font-size: 10px;
display: inline-block;

View File

@@ -25,8 +25,9 @@ const buildSelector = (selector, propsData = {}) => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
document.getElementById('body-user').append(container)
const ComponentVM = new Vue({
render: (h) => h(selector, propsData),
const View = Vue.extend(selector)
const ComponentVM = new View({
propsData,
}).$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()

View File

@@ -22,6 +22,8 @@
import Vue from 'vue'
import BoardSelector from './BoardSelector'
import CardSelector from './CardSelector'
import './../css/collections.css'
import FileSharingPicker from './views/FileSharingPicker'
import { buildSelector } from './helpers/selector'
@@ -43,19 +45,13 @@ window.addEventListener('DOMContentLoaded', () => {
}
window.OCP.Collaboration.registerType('deck', {
action: () => {
const BoardSelector = () => import('./BoardSelector')
buildSelector(BoardSelector)
},
action: () => buildSelector(BoardSelector),
typeString: t('deck', 'Link to a board'),
typeIconClass: 'icon-deck',
})
window.OCP.Collaboration.registerType('deck-card', {
action: () => {
const CardSelector = () => import('./CardSelector')
buildSelector(CardSelector)
},
action: () => buildSelector(CardSelector),
typeString: t('deck', 'Link to a card'),
typeIconClass: 'icon-deck',
})

View File

@@ -49,15 +49,10 @@ window.addEventListener('DOMContentLoaded', () => {
const shortenedMessage = shortenedMessageCandidate === '' ? message.substr(0, 255) : shortenedMessageCandidate
try {
await buildSelector(CardCreateDialog, {
props: {
title: shortenedMessage,
description: message + '\n\n' + '['
+ t('deck', 'Message from {author} in {conversationName}', {
author: actorDisplayName,
conversationName,
})
+ '](' + window.location.protocol + '//' + window.location.host + generateUrl('/call/' + conversationToken) + ')',
},
title: shortenedMessage,
description: message + '\n\n' + '['
+ t('deck', 'Message from {author} in {conversationName}', { author: actorDisplayName, conversationName })
+ '](' + generateUrl('/call/' + conversationToken) + ')',
})
} catch (e) {
console.debug('Card creation dialog was canceled')

View File

@@ -109,7 +109,7 @@ if (!window.OCA.Deck) {
}
/**
* @typedef {object} CardRichObject
* @typedef {Object} CardRichObject
* @property {string} id
* @property {string} name
* @property {string} boardname
@@ -124,14 +124,13 @@ if (!window.OCA.Deck) {
/**
* Frontend message API for adding actions to talk messages.
*
* @param {*} Object the wrapping object.
* @param {string} label the action label.
* @param {String} label the action label.
* @param {registerActionCallback} callback the callback function. This function will receive
* the card as a parameter and be triggered by a click on the
* action. The card parameter will be of the format of a rich object string
* type "deck-card"
* @param {string} icon the action label. E.g. "icon-reply"
* @param {String} icon the action label. E.g. "icon-reply"
*/
window.OCA.Deck.registerCardAction = ({ label, callback, icon }) => {
const cardAction = {

View File

@@ -47,7 +47,7 @@ export default {
bodyFormData.append('cardId', this.cardId)
bodyFormData.append('type', type)
bodyFormData.append('file', file)
await queue.add(async () => {
await queue.add(async() => {
try {
await this.$store.dispatch('createAttachment', {
cardId: this.cardId,

View File

@@ -1,4 +1,4 @@
/*
/**
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>

View File

@@ -23,8 +23,8 @@
/**
* Board model
*
* @typedef {object} Board
* @property {string} title
* @typedef {Object} Board
* @property {String} title
* @property {boolean} archived
* @property {number} shared 1 (shared) or 0 (not shared)
*/
@@ -32,8 +32,8 @@
/**
* Stack model
*
* @typedef {object} Stack
* @property {string} title
* @typedef {Object} Stack
* @property {String} title
* @property {number} boardId
* @property {number} order
*/
@@ -41,15 +41,15 @@
/**
* Card model
*
* @typedef {object} Card
* @property {string} title
* @typedef {Object} Card
* @property {String} title
* @property {boolean} archived
* @property {number} order
*/
/**
* Label model
*
* @typedef {object} Label
* @property {string} title
* @property {string} color
* @typedef {Object} Label
* @property {String} title
* @property {String} color
*/

View File

@@ -38,7 +38,7 @@ export class BoardApi {
* Updates a board.
*
* @param {Board} board the board object to update
* @return {Promise}
* @returns {Promise}
*/
updateBoard(board) {
return axios.put(this.url(`/boards/${board.id}`), board)
@@ -57,13 +57,13 @@ export class BoardApi {
/**
* Creates a new board.
*
* @typedef {object} BoardCreateObject
* @typedef {Object} BoardCreateObject
* @property {string} title
* @property {string} color
*
* @param {BoardCreateObject} boardData The board data to send.
* color the hexadecimal color value formated /[0-9A-F]{6}/i
* @return {Promise}
* @returns {Promise}
*/
createBoard(boardData) {
return axios.post(this.url('/boards'), boardData)

View File

@@ -78,7 +78,7 @@ export class StackApi {
/**
* @param {Stack} stack stack object to create
* @return {Promise}
* @returns {Promise}
*/
createStack(stack) {
return axios.post(this.url('/stacks'), stack)

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
},
@@ -345,7 +353,6 @@ export default new Vuex.Store({
},
/**
* @param commit.commit
* @param commit
* @param state
* @param {Board} board
@@ -359,7 +366,6 @@ export default new Vuex.Store({
})
},
/**
* @param commit.commit
* @param commit
* @param state
* @param {Board} board
@@ -375,10 +381,9 @@ export default new Vuex.Store({
/**
* Updates a board API side.
*
* @param commit.commit
* @param commit
* @param board The board to update.
* @return {Promise<void>}
* @returns {Promise<void>}
*/
async updateBoard({ commit }, board) {
const storedBoard = await apiClient.updateBoard(board)
@@ -433,6 +438,9 @@ export default new Vuex.Store({
toggleCompactMode({ commit }) {
commit('toggleCompactMode')
},
setCardDetailsInModal({ commit }, show) {
commit('setCardDetailsInModal', show)
},
setCurrentBoard({ commit }, board) {
commit('setCurrentBoard', board)
},

View File

@@ -21,6 +21,7 @@
*/
import Vue from 'vue'
import CardSelector from '../CardSelector'
import { createShare } from '../services/SharingApi'
export default {
@@ -33,12 +34,12 @@ export default {
container.id = 'deck-board-select'
const body = document.getElementById('body-user')
body.append(container)
const CardSelector = () => import('./../CardSelector')
const ComponentVM = new Vue({
render: (h) => h(CardSelector, {
const SelectorView = Vue.extend(CardSelector)
const ComponentVM = new SelectorView({
propsData: {
title: t('deck', 'Share {file} with a Deck card', { file: decodeURIComponent(self.fileInfo.name) }),
action: t('deck', 'Share'),
}),
},
})
ComponentVM.$mount(container)
ComponentVM.$root.$on('close', () => {
@@ -46,7 +47,7 @@ export default {
ComponentVM.$destroy()
reject(new Error('Canceled'))
})
ComponentVM.$root.$on('select', async (id) => {
ComponentVM.$root.$on('select', async(id) => {
const result = await createShare({
path: self.fileInfo.path + '/' + self.fileInfo.name,
shareType: 12,

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

@@ -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,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.16.1@aa7e400908833b10c0333861f86cd48c510b60eb">
<files psalm-version="4.7.3@38c452ae584467e939d55377aaf83b5a26f19dd1">
<file src="lib/Activity/ActivityManager.php">
<TypeDoesNotContainType occurrences="1">
<code>$message !== null</code>
@@ -10,17 +10,6 @@
<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>
@@ -116,14 +105,6 @@
<ParamNameMismatch occurrences="1">
<code>$boardId</code>
</ParamNameMismatch>
<TypeDoesNotContainType occurrences="6">
<code>$limit !== null</code>
<code>$limit !== null</code>
<code>$limit !== null</code>
<code>$offset !== null</code>
<code>$offset !== null</code>
<code>$offset !== null</code>
</TypeDoesNotContainType>
</file>
<file src="lib/Db/Card.php">
<UndefinedClass occurrences="2">
@@ -132,6 +113,7 @@
</UndefinedClass>
</file>
<file src="lib/Db/CardMapper.php">
<InvalidArgument occurrences="1"/>
<InvalidScalarArgument occurrences="1">
<code>$entity-&gt;getId()</code>
</InvalidScalarArgument>
@@ -269,10 +251,11 @@
</RedundantCondition>
</file>
<file src="lib/Service/FilesAppService.php">
<MissingDependency occurrences="3">
<MissingDependency occurrences="4">
<code>$this-&gt;rootFolder</code>
<code>$this-&gt;rootFolder</code>
<code>IRootFolder</code>
<code>ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Service/PermissionService.php">
@@ -286,6 +269,27 @@
<code>BadRquestException</code>
</UndefinedClass>
</file>
<file src="lib/Sharing/DeckShareProvider.php">
<InvalidReturnStatement occurrences="1">
<code>$shares</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>getSharesInFolder</code>
</InvalidReturnType>
<InvalidThrow occurrences="1">
<code>throw new GenericShareException('Already shared', $this-&gt;l-&gt;t('Path is already shared with this card'), 403);</code>
</InvalidThrow>
<MissingDependency occurrences="8">
<code>GenericShareException</code>
<code>GenericShareException</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
<code>ShareNotFound</code>
</MissingDependency>
</file>
<file src="lib/Sharing/Listener.php">
<InvalidArgument occurrences="1">
<code>[self::class, 'listenPreShare']</code>

View File

@@ -38,6 +38,7 @@ use OCP\L10N\IFactory;
use OCP\RichObjectStrings\IValidator;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
use OCA\Deck\Service\CardService;
class DeckProviderTest extends TestCase {
@@ -56,6 +57,9 @@ class DeckProviderTest extends TestCase {
/** @var ICommentsManager|MockObject */
private $commentsManager;
/** @var CardService|MockObject */
private $cardService;
/** @var string */
private $userId = 'admin';
@@ -67,7 +71,9 @@ class DeckProviderTest extends TestCase {
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->config = $this->createMock(IConfig::class);
$this->provider = new DeckProvider($this->urlGenerator, $this->activityManager, $this->userManager, $this->commentsManager, $this->l10nFactory, $this->config, $this->userId);
$this->config = $this->createMock(IConfig::class);
$this->cardService = $this->createMock(CardService::class);
$this->provider = new DeckProvider($this->urlGenerator, $this->activityManager, $this->userManager, $this->commentsManager, $this->l10nFactory, $this->config, $this->userId, $this->cardService);
}
private function mockEvent($objectType, $objectId, $objectName, $subject, $subjectParameters = []) {

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

@@ -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) {
@@ -163,7 +163,6 @@ class BoardMapperTest extends MapperTestUtility {
$actual = $this->boardMapper->find($this->boards[0]->getId(), true, false);
/** @var Board $expected */
$expected = $this->boards[0];
$expected->setLabels([]);
$this->assertEquals($expected->getLabels(), $actual->getLabels());
}

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