Compare commits
219 Commits
eslint-war
...
backport/3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45473df9f4 | ||
|
|
6832d41dee | ||
|
|
ab6d12cc5f | ||
|
|
a8db7a90f1 | ||
|
|
19b4d0ac21 | ||
|
|
1dec7be713 | ||
|
|
92a0ef904f | ||
|
|
f389f834f0 | ||
|
|
c035a6be73 | ||
|
|
d1498486eb | ||
|
|
408a19ba9a | ||
|
|
9523aa9eeb | ||
|
|
3d74323be4 | ||
|
|
3d14e3f916 | ||
|
|
cce5bd8a7d | ||
|
|
62ccab6e96 | ||
|
|
9f8e7258b5 | ||
|
|
2ca00fed08 | ||
|
|
22cb32e197 | ||
|
|
e1391efbef | ||
|
|
4633ddc153 | ||
|
|
a96ccc3bfd | ||
|
|
14bcab8c8a | ||
|
|
40f0f3d194 | ||
|
|
32e5f6dd6b | ||
|
|
5f2235a91f | ||
|
|
deca12ddd3 | ||
|
|
31d6794490 | ||
|
|
90938d5f58 | ||
|
|
0c7768b36d | ||
|
|
d14c7810ee | ||
|
|
de5153337d | ||
|
|
bd405122e2 | ||
|
|
e7d6233796 | ||
|
|
25bb72c3dc | ||
|
|
487c626a73 | ||
|
|
6b0182ea70 | ||
|
|
ca6fe65580 | ||
|
|
498ed9dfff | ||
|
|
32a2a4ebf5 | ||
|
|
4bd1b70a5e | ||
|
|
71b78e8ec3 | ||
|
|
2d6fc18218 | ||
|
|
b7554a06d2 | ||
|
|
35a8fee3b3 | ||
|
|
4fe94e65ad | ||
|
|
80fa93716b | ||
|
|
0145593cba | ||
|
|
af712ddb56 | ||
|
|
f0deb93cb7 | ||
|
|
781cfc11c7 | ||
|
|
97e45ebc69 | ||
|
|
c4bc945b1e | ||
|
|
2bd12ed7b5 | ||
|
|
658d358e70 | ||
|
|
3e6a80eb37 | ||
|
|
5408d4f9c5 | ||
|
|
6e5fd9e25a | ||
|
|
9fbdafbe73 | ||
|
|
9403fb1759 | ||
|
|
691abb02d8 | ||
|
|
3bb99a9001 | ||
|
|
74e0149a6d | ||
|
|
c5d83e662c | ||
|
|
376c7c7d07 | ||
|
|
a52664e9e4 | ||
|
|
f573abe279 | ||
|
|
975af7c056 | ||
|
|
f2456d796c | ||
|
|
c7c9302109 | ||
|
|
989db4702c | ||
|
|
9de5f12f4b | ||
|
|
9338ebb561 | ||
|
|
0d9cdc5f1e | ||
|
|
f66c71ee55 | ||
|
|
d742bc097b | ||
|
|
53386a7f1a | ||
|
|
cfbc18deb9 | ||
|
|
bdf4631504 | ||
|
|
a0f93a81d2 | ||
|
|
d9b086f146 | ||
|
|
b8b3ac3516 | ||
|
|
118959795f | ||
|
|
c93aaeb9bf | ||
|
|
f42c82274f | ||
|
|
fff9c4f7a2 | ||
|
|
3eb5ccbbe2 | ||
|
|
cc61f3d4e0 | ||
|
|
209adc1e7a | ||
|
|
e26f06be8f | ||
|
|
c99b4bf4ab | ||
|
|
296e7abf14 | ||
|
|
c6ef07ceb0 | ||
|
|
4e937bd03a | ||
|
|
84943c5889 | ||
|
|
4ed71b7a63 | ||
|
|
4ab8078103 | ||
|
|
6b75a1ab1b | ||
|
|
76e3a3a25e | ||
|
|
9503de4716 | ||
|
|
3340ccec6f | ||
|
|
a90e099113 | ||
|
|
aa69495075 | ||
|
|
11eb779beb | ||
|
|
836e2b33df | ||
|
|
c63b72bf2e | ||
|
|
c20008dacc | ||
|
|
edacda1377 | ||
|
|
2f0ad2d639 | ||
|
|
745d76c74b | ||
|
|
3b7f49de55 | ||
|
|
e336bd98b2 | ||
|
|
f62e87cffa | ||
|
|
d4d716b1f5 | ||
|
|
6e89dd48bc | ||
|
|
2bbd572cb5 | ||
|
|
17543eaf94 | ||
|
|
4f4e9fa5c7 | ||
|
|
d0060604a6 | ||
|
|
9f8df2bbd4 | ||
|
|
139cca1ae8 | ||
|
|
32054adcb5 | ||
|
|
764dd1e995 | ||
|
|
b65fde069f | ||
|
|
bec68eb5c3 | ||
|
|
8dca76f9f0 | ||
|
|
13ed30e2bc | ||
|
|
a7ff79605d | ||
|
|
b5bebac3c2 | ||
|
|
756d78f78a | ||
|
|
7e55cb18b3 | ||
|
|
b834ef664a | ||
|
|
1ee0e16f9e | ||
|
|
63434d7b63 | ||
|
|
d9f993dc5b | ||
|
|
9090c13417 | ||
|
|
0630dafef9 | ||
|
|
150ab7b83a | ||
|
|
11cc569914 | ||
|
|
eaf58e0fc9 | ||
|
|
da994c5ecd | ||
|
|
cef4cc6a4d | ||
|
|
d761323887 | ||
|
|
0857270a48 | ||
|
|
df178369c0 | ||
|
|
8c3edf077d | ||
|
|
ac477a58c3 | ||
|
|
504f977739 | ||
|
|
00e813295b | ||
|
|
300d300c45 | ||
|
|
1514f9a737 | ||
|
|
f4fe271b39 | ||
|
|
1c01881eb7 | ||
|
|
af1564d8e5 | ||
|
|
35515ce157 | ||
|
|
fd62ab7a59 | ||
|
|
1557797926 | ||
|
|
90a8b479f6 | ||
|
|
c2fae6b2d7 | ||
|
|
89a3f4fc26 | ||
|
|
7aa94c74d7 | ||
|
|
33993418ac | ||
|
|
75a2d9d54c | ||
|
|
b1a3c6b237 | ||
|
|
437704abb3 | ||
|
|
0e09548e69 | ||
|
|
37834cb926 | ||
|
|
7dd3bb49f4 | ||
|
|
fdf1eaeaed | ||
|
|
4cc88c8b64 | ||
|
|
db331ecb72 | ||
|
|
c2757bec7d | ||
|
|
e074eac092 | ||
|
|
d5166c74e5 | ||
|
|
226e3c8212 | ||
|
|
a680915a89 | ||
|
|
b8ff06e5d2 | ||
|
|
5a108b64b0 | ||
|
|
00630587af | ||
|
|
b02cf925a1 | ||
|
|
aaa26575dd | ||
|
|
aebb3cef03 | ||
|
|
18a7ae3a1a | ||
|
|
6c84c67970 | ||
|
|
d449acde30 | ||
|
|
c1e700cefa | ||
|
|
30df03948c | ||
|
|
15ea081daa | ||
|
|
0f71741c46 | ||
|
|
585f0999a3 | ||
|
|
d51f645299 | ||
|
|
ebbe4eb802 | ||
|
|
d6c94f44d9 | ||
|
|
86345a6338 | ||
|
|
2c77d8a589 | ||
|
|
16cbbd4805 | ||
|
|
8aa77679c8 | ||
|
|
e6cff5bbb6 | ||
|
|
86ed24744c | ||
|
|
01ed3625dc | ||
|
|
cc9dea1f2b | ||
|
|
b16ade905c | ||
|
|
ee1bba7d99 | ||
|
|
3407097e95 | ||
|
|
47ac3e6082 | ||
|
|
75110bed47 | ||
|
|
74f0106718 | ||
|
|
958d50d9b7 | ||
|
|
5f4aa017b6 | ||
|
|
92c2a58f50 | ||
|
|
75bf0dffe6 | ||
|
|
45a10f0840 | ||
|
|
fbf3b1cd19 | ||
|
|
0cc4151929 | ||
|
|
e261ade1bb | ||
|
|
5d7e54d419 | ||
|
|
3499858295 | ||
|
|
1615e218bd | ||
|
|
9a3b859780 |
@@ -1,13 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'@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'],
|
||||
},
|
||||
}
|
||||
|
||||
38
.github/dependabot.yml
vendored
@@ -11,6 +11,19 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- juliushaertl
|
||||
- jakobroehrl
|
||||
#- package-ecosystem: npm
|
||||
# directory: "/"
|
||||
# target-branch: "stable1.1"
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: saturday
|
||||
# time: "03:00"
|
||||
# timezone: Europe/Paris
|
||||
# open-pull-requests-limit: 10
|
||||
# reviewers:
|
||||
# - juliushaertl
|
||||
# - jakobroehrl
|
||||
- package-ecosystem: composer
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -21,23 +34,8 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- juliushaertl
|
||||
- package-ecosystem: composer
|
||||
directory: "/tests/integration"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- juliushaertl
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- juliushaertl
|
||||
ignore:
|
||||
- dependency-name: christophwurst/nextcloud
|
||||
versions:
|
||||
- "< 16"
|
||||
- ">= 15.a"
|
||||
|
||||
25
.github/stale.yml
vendored
Normal 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.
|
||||
8
.github/workflows/appbuild.yml
vendored
@@ -12,15 +12,15 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
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.18.1
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: composer
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uname -a
|
||||
RUST_BACKTRACE=1 krankerl --version
|
||||
RUST_BACKTRACE=1 krankerl package
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Deck app tarball
|
||||
path: build/artifacts/deck.tar.gz
|
||||
|
||||
2
.github/workflows/appstore-build-publish.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Set up php ${{ env.PHP_VERSION }}
|
||||
uses: shivammathur/setup-php@2.18.1
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ env.PHP_VERSION }}
|
||||
coverage: none
|
||||
|
||||
49
.github/workflows/command-rebase.yml
vendored
@@ -1,49 +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@v2
|
||||
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@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Fix permissions
|
||||
run: git config --global --add safe.directory /github/workspace
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Add reaction on failure
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
if: failure()
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
repository: ${{ github.event.repository.full_name }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reaction-type: "-1"
|
||||
112
.github/workflows/cypress.yml
vendored
@@ -1,112 +0,0 @@
|
||||
name: Cypress
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable*
|
||||
|
||||
env:
|
||||
APP_NAME: deck
|
||||
CYPRESS_baseUrl: http://localhost:8081/index.php
|
||||
|
||||
jobs:
|
||||
cypress:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
# containers: [1, 2, 3]
|
||||
php-versions: [ '7.4' ]
|
||||
databases: [ 'sqlite' ]
|
||||
server-versions: [ 'master' ]
|
||||
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: nextcloud/server
|
||||
ref: ${{ matrix.server-versions }}
|
||||
|
||||
- name: Checkout submodules
|
||||
shell: bash
|
||||
run: |
|
||||
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
|
||||
git submodule sync --recursive
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
|
||||
- name: Checkout ${{ env.APP_NAME }}
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, zip, gd, apcu
|
||||
ini-values:
|
||||
apc.enable_cli=on
|
||||
coverage: none
|
||||
|
||||
- name: Set up Nextcloud
|
||||
env:
|
||||
DB_PORT: 4444
|
||||
PHP_CLI_SERVER_WORKERS: 10
|
||||
run: |
|
||||
mkdir data
|
||||
php occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
|
||||
php occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
|
||||
php occ config:system:set debug --value=true --type=boolean
|
||||
php -f index.php
|
||||
php -S 0.0.0.0:8081 &
|
||||
export OC_PASS=1234561
|
||||
php occ user:add --password-from-env user1
|
||||
php occ user:add --password-from-env user2
|
||||
php occ app:enable deck
|
||||
php occ app:list
|
||||
cd apps/deck
|
||||
composer install
|
||||
npm ci
|
||||
npm run build
|
||||
cd ../../
|
||||
curl -v http://localhost:8081/index.php/login
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
record: true
|
||||
parallel: false
|
||||
wait-on: '${{ env.CYPRESS_baseUrl }}'
|
||||
working-directory: 'apps/${{ env.APP_NAME }}'
|
||||
config: defaultCommandTimeout=10000,video=false
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
npm_package_name: ${{ env.APP_NAME }}
|
||||
|
||||
- name: Upload test failure screenshots
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Upload screenshots
|
||||
path: apps/${{ env.APP_NAME }}/cypress/screenshots/
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload nextcloud logs
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Upload nextcloud log
|
||||
path: data/nextcloud.log
|
||||
retention-days: 5
|
||||
29
.github/workflows/dependabot-approve-merge.yml
vendored
@@ -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 }}
|
||||
20
.github/workflows/fixup.yml
vendored
@@ -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 }}
|
||||
9
.github/workflows/integration.yml
vendored
@@ -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@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: nextcloud/server
|
||||
ref: ${{ matrix.server-versions }}
|
||||
@@ -54,15 +54,14 @@ jobs:
|
||||
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
|
||||
git submodule sync --recursive
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
cd build/integration && composer require --dev phpunit/phpunit:~8
|
||||
|
||||
- name: Checkout app
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.18.1
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpunit
|
||||
|
||||
26
.github/workflows/lint.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0', '8.1']
|
||||
php-versions: ['7.3', '7.4']
|
||||
|
||||
name: php${{ matrix.php-versions }} lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up php${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.18.1
|
||||
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@v3
|
||||
uses: actions/checkout@master
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@2.18.1
|
||||
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@v3
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
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@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
- 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
|
||||
|
||||
6
.github/workflows/nightly.yml
vendored
@@ -17,15 +17,15 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
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.18.1
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: composer
|
||||
|
||||
4
.github/workflows/nodejs.yml
vendored
@@ -12,9 +12,9 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
|
||||
10
.github/workflows/phpunit.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0', '8.1']
|
||||
php-versions: ['7.3', '7.4']
|
||||
databases: ['sqlite', 'mysql', 'pgsql']
|
||||
server-versions: ['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@v3
|
||||
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@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.18.1
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpunit
|
||||
|
||||
6
.github/workflows/static-analysis.yml
vendored
@@ -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@v3
|
||||
uses: actions/checkout@master
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@2.18.1
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: 7.4
|
||||
tools: composer:v1
|
||||
|
||||
198
CHANGELOG.md
@@ -1,91 +1,115 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## 1.7.0
|
||||
## 1.5.7
|
||||
|
||||
### Added
|
||||
|
||||
- Transfer ownership @matchish @luka-nextcloud @juliushaertl [#2496](https://github.com/nextcloud/deck/pull/2496)
|
||||
- Import from trello via CLI @vitormattos [#3182](https://github.com/nextcloud/deck/pull/3182)
|
||||
- Add app config to toggle the default calendar setting as an admin @juliushaertl [#3528](https://github.com/nextcloud/deck/pull/3528)
|
||||
- Show board name in browser title @luka-nextcloud [#3499](https://github.com/nextcloud/deck/pull/3499)
|
||||
- Move DeleteCron to be time insensitive @juliushaertl [#3599](https://github.com/nextcloud/deck/pull/3599)
|
||||
- 🚸 Shows error on board fetchData @vinicius73 [#3653](https://github.com/nextcloud/deck/pull/3653)
|
||||
- Add support for PHP 8.1 @juliushaertl [#3601](https://github.com/nextcloud/deck/pull/3601)
|
||||
- Nextcloud 24 compatibility
|
||||
- Transfer ownership @juliushaertl [#3665](https://github.com/nextcloud/deck/pull/3665)
|
||||
|
||||
### Fixed
|
||||
|
||||
- CardApiController: Fix order of optional parameters @simonspa [#3512](https://github.com/nextcloud/deck/pull/3512)
|
||||
- Exclude deleted boards in the selection for target @luka-nextcloud [#3502](https://github.com/nextcloud/deck/pull/3502)
|
||||
- Fix CalDAV blocking and modernize circles API usage @juliushaertl [#3500](https://github.com/nextcloud/deck/pull/3500)
|
||||
- Timestamps on created and modified at values @luka-nextcloud [#3532](https://github.com/nextcloud/deck/pull/3532)
|
||||
- return the selector for collections @dartcafe [#3552](https://github.com/nextcloud/deck/pull/3552)
|
||||
- Generate fixed link for activity emails @luka-nextcloud [#3611](https://github.com/nextcloud/deck/pull/3611)
|
||||
- 🐛 Fix missing files sidebar @vinicius73 [#3635](https://github.com/nextcloud/deck/pull/3635)
|
||||
- Handle description shortening more gracefully @juliushaertl [#3650](https://github.com/nextcloud/deck/pull/3650)
|
||||
- Sort boards non case sensitive @Ben-Ro [#3560](https://github.com/nextcloud/deck/pull/3560)
|
||||
- Remove unused argument from transfer ownership @juliushaertl [#3712](https://github.com/nextcloud/deck/pull/3712)
|
||||
- Fix: Check all circle shares for permissions @bink [#3625](https://github.com/nextcloud/deck/pull/3625)
|
||||
- Extend API changelog @juliushaertl [#3522](https://github.com/nextcloud/deck/pull/3522)
|
||||
- Fix talk integration @nickvergessen [#3529](https://github.com/nextcloud/deck/pull/3529)
|
||||
- Fix confusion between stackId and boardId in StackService @eneiluj [#3541](https://github.com/nextcloud/deck/pull/3541)
|
||||
- Add horizontal scrollbar into the large table inside description @luka-nextcloud [#3531](https://github.com/nextcloud/deck/pull/3531)
|
||||
- Make links in markdown note bolder @luka-nextcloud [#3530](https://github.com/nextcloud/deck/pull/3530)
|
||||
- Update master php testing versions @nickvergessen [#3561](https://github.com/nextcloud/deck/pull/3561)
|
||||
- Update master php enviroment @nickvergessen [#3582](https://github.com/nextcloud/deck/pull/3582)
|
||||
- Make insert attachment buttom easy to click @luka-nextcloud [#3612](https://github.com/nextcloud/deck/pull/3612)
|
||||
- Remove extra bullet @elitejake [#3613](https://github.com/nextcloud/deck/pull/3613)
|
||||
- l10n: Delete space @Valdnet [#3666](https://github.com/nextcloud/deck/pull/3666)
|
||||
- Update master php testing versions @nickvergessen [#3688](https://github.com/nextcloud/deck/pull/3688)
|
||||
- Fix wording to represent the code behavior @q-wertz [#3685](https://github.com/nextcloud/deck/pull/3685)
|
||||
- Fix cron jobs @nickvergessen [#3689](https://github.com/nextcloud/deck/pull/3689)
|
||||
- Update master php testing versions @nickvergessen [#3695](https://github.com/nextcloud/deck/pull/3695)
|
||||
- Optimise queries when preparing card related notifications @Raudius [#3690](https://github.com/nextcloud/deck/pull/3690)
|
||||
- Properly check for the stack AND setting board permissions @juliushaertl [#3670](https://github.com/nextcloud/deck/pull/3670)
|
||||
- Replace deprecated String.prototype.substr() @CommanderRoot [#3669](https://github.com/nextcloud/deck/pull/3669)
|
||||
- Dependency updates
|
||||
- Show cards after moving into another list [#3736](https://github.com/nextcloud/deck/pull/3736)
|
||||
- Fix paramter replacements when creating deck cards from talk messages @nickvergessen [#3683](https://github.com/nextcloud/deck/pull/3683)
|
||||
- Fix hidden attachment icon on archived cards [#3733](https://github.com/nextcloud/deck/pull/3733)
|
||||
- Adapt the card modal to upstream changes [#3764](https://github.com/nextcloud/deck/pull/3764)
|
||||
- Fix text selection in dark mode and modal view [#3765](https://github.com/nextcloud/deck/pull/3765)
|
||||
- Add missing indices [#3754](https://github.com/nextcloud/deck/pull/3754)
|
||||
- Fix: Check all circle shares for permissions [#3721](https://github.com/nextcloud/deck/pull/3721)
|
||||
- Add a missing translation - not found in transifex [#3707](https://github.com/nextcloud/deck/pull/3707)
|
||||
- 🐛 Fix missing files sidebar [#3642](https://github.com/nextcloud/deck/pull/3642)
|
||||
- [stable23] Use explicit cast to make use of index [#3731](https://github.com/nextcloud/deck/pull/3731)
|
||||
- Fix hidden attachment icon on archived cards [#3735](https://github.com/nextcloud/deck/pull/3735)
|
||||
- [stable23] Sort boards non case sensitive [#3739](https://github.com/nextcloud/deck/pull/3739)
|
||||
- add autofocus on board edit #3326 @juliushaertl [#3743](https://github.com/nextcloud/deck/pull/3743)
|
||||
- Fix paramter replacements when creating deck cards from talk messages @juliushaertl [#3742](https://github.com/nextcloud/deck/pull/3742)
|
||||
- Fix text selection in dark mode and modal view [#3767](https://github.com/nextcloud/deck/pull/3767)
|
||||
|
||||
### Other
|
||||
|
||||
- Properly check for the stack AND setting board permissions [#3714](https://github.com/nextcloud/deck/pull/3714)
|
||||
- Add missing indices [#3756](https://github.com/nextcloud/deck/pull/3756)
|
||||
|
||||
|
||||
## 1.6.0-beta1
|
||||
## 1.5.6
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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
|
||||
|
||||
- #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
|
||||
* 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
|
||||
|
||||
- #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
|
||||
* [#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
|
||||
@@ -122,15 +146,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
|
||||
@@ -239,31 +263,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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.8.0-beta.0</version>
|
||||
<version>1.5.7</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="25" max-version="25"/>
|
||||
<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>
|
||||
<command>OCA\Deck\Command\TransferOwnership</command>
|
||||
</commands>
|
||||
<activity>
|
||||
|
||||
@@ -92,10 +92,6 @@ return [
|
||||
['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
|
||||
['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
|
||||
|
||||
['name' => 'board_import_api#getAllowedSystems', 'url' => '/api/v{apiVersion}/boards/import/getSystems','verb' => 'GET'],
|
||||
['name' => 'board_import_api#getConfigSchema', 'url' => '/api/v{apiVersion}/boards/import/config/schema/{name}','verb' => 'GET'],
|
||||
['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST'],
|
||||
|
||||
|
||||
['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'],
|
||||
['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'],
|
||||
|
||||
@@ -8,28 +8,26 @@
|
||||
"email": "jus@bitgrid.net"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.3"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"cogpowered/finediff": "0.3.*",
|
||||
"justinrainbow/json-schema": "^5.2"
|
||||
"cogpowered/finediff": "0.3.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-master",
|
||||
"christophwurst/nextcloud": "dev-master",
|
||||
"phpunit/phpunit": "^9",
|
||||
"nextcloud/coding-standard": "^1.0.0",
|
||||
"christophwurst/nextcloud": "^22@dev",
|
||||
"phpunit/phpunit": "^8",
|
||||
"nextcloud/coding-standard": "^0.5.0",
|
||||
"symfony/event-dispatcher": "^4.0",
|
||||
"vimeo/psalm": "^4.3",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"classmap-authoritative": true,
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true
|
||||
},
|
||||
"platform": {
|
||||
"php": "7.4"
|
||||
}
|
||||
"classmap-authoritative": true
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
|
||||
|
||||
1538
composer.lock
generated
16
css/deck.css
@@ -1,16 +0,0 @@
|
||||
.icon-deck {
|
||||
background-image: url(../img/deck-dark.svg);
|
||||
}
|
||||
|
||||
.icon-deck-white, .icon-deck.icon-white {
|
||||
background-image: url(../img/deck.svg);
|
||||
}
|
||||
|
||||
body[data-theme-dark] .icon-deck {
|
||||
background-image: url(../img/deck.svg);
|
||||
}
|
||||
|
||||
body[data-theme-dark] .icon-deck-white,
|
||||
body[data-theme-dark] .icon-deck.icon-white {
|
||||
background-image: url(../img/deck-dark.svg);
|
||||
}
|
||||
1
css/deck.scss
Normal file
@@ -0,0 +1 @@
|
||||
@include icon-black-white('deck', 'deck', 1);
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
/*
|
||||
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Artem Anufrij <artem.anufrij@live.de>
|
||||
* @author Marin Treselj <marin@pixelipo.com>
|
||||
* @author Oskar Kurz <oskar.kurz@gmail.com>
|
||||
* @author Ryan Fletcher <ryan.fletcher@codepassion.ca>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
@@ -20,26 +23,6 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Deck\Model;
|
||||
|
||||
use OCA\Deck\Db\Board;
|
||||
|
||||
class BoardSummary extends Board {
|
||||
private Board $board;
|
||||
|
||||
public function __construct(Board $board) {
|
||||
parent::__construct();
|
||||
$this->board = $board;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'title' => $this->getTitle()
|
||||
];
|
||||
}
|
||||
|
||||
public function __call($name, $arguments) {
|
||||
return $this->board->__call($name, $arguments);
|
||||
}
|
||||
}
|
||||
@import 'icons';
|
||||
@import 'print';
|
||||
41
css/icons.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Custom icons
|
||||
*/
|
||||
@include icon-black-white('deck', 'deck', 1);
|
||||
@include icon-black-white('archive', 'deck', 1);
|
||||
@include icon-black-white('circles', 'deck', 1);
|
||||
@include icon-black-white('clone', 'deck', 1);
|
||||
@include icon-black-white('filter', 'deck', 1);
|
||||
@include icon-black-white('filter_set', 'deck', 1);
|
||||
@include icon-black-white('attach', 'deck', 1);
|
||||
@include icon-black-white('reply', 'deck', 1);
|
||||
@include icon-black-white('notifications-dark', 'deck', 1);
|
||||
@include icon-black-white('description', 'deck', 1);
|
||||
|
||||
.icon-toggle-compact-collapsed {
|
||||
@include icon-color('toggle-view-expand', 'deck', $color-black);
|
||||
}
|
||||
|
||||
.icon-toggle-compact-expanded {
|
||||
@include icon-color('toggle-view-collapse', 'deck', $color-black);
|
||||
}
|
||||
.icon-activity {
|
||||
@include icon-color('activity-dark', 'activity', $color-black);
|
||||
}
|
||||
.icon-comment--unread {
|
||||
@include icon-color('comment', 'actions', $color-primary, 1, true);
|
||||
}
|
||||
|
||||
.avatardiv.circles {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.icon-circles {
|
||||
opacity: 1;
|
||||
background-size: 20px;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.icon-colorpicker {
|
||||
background-image: url('../img/color_picker.svg');
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://nextcloud.local/index.php",
|
||||
"projectId": "1s7wkc",
|
||||
"viewportWidth": 1280,
|
||||
"viewportHeight": 720,
|
||||
"experimentalSessionAndOrigin": true
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { randHash } from "../utils"
|
||||
const randUser = randHash()
|
||||
|
||||
describe('Board', function () {
|
||||
const password = 'pass123'
|
||||
|
||||
before(function () {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it('Can create a board', function () {
|
||||
let board = 'Test'
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '/index.php/apps/deck/boards',
|
||||
}).as('createBoardRequest')
|
||||
|
||||
// Click "Add board"
|
||||
cy.openLeftSidebar()
|
||||
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
|
||||
.eq(3).find('a').first().click({force: true})
|
||||
|
||||
// Type the board title
|
||||
cy.get('.board-create form input[type=text]')
|
||||
.type(board, {force: true})
|
||||
|
||||
// Submit
|
||||
cy.get('.board-create form input[type=submit]')
|
||||
.first().click({force: true})
|
||||
|
||||
cy.wait('@createBoardRequest').its('response.statusCode').should('equal', 200)
|
||||
|
||||
cy.get('.app-navigation__list .app-navigation-entry__children .app-navigation-entry')
|
||||
.contains(board).should('be.visible')
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { randHash } from '../utils'
|
||||
const randUser = randHash()
|
||||
|
||||
describe('Card', function () {
|
||||
const board = 'TestBoard'
|
||||
const list = 'TestList'
|
||||
const password = 'pass123'
|
||||
|
||||
before(function () {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
cy.deckCreateBoard({ user: randUser, password }, board)
|
||||
cy.deckCreateList({ user: randUser, password }, list)
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it('Can add a card', function () {
|
||||
let card = 'Card 1'
|
||||
|
||||
cy.openLeftSidebar()
|
||||
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
|
||||
.eq(3).find('a.app-navigation-entry-link')
|
||||
.first().click({force: true})
|
||||
|
||||
cy.get('.board .stack').eq(0).within(() => {
|
||||
cy.get('button.action-item.action-item--single.icon-add')
|
||||
.first().click()
|
||||
|
||||
cy.get('.stack__card-add form input#new-stack-input-main')
|
||||
.type(card)
|
||||
cy.get('.stack__card-add form input[type=submit]')
|
||||
.first().click()
|
||||
cy.get('.card').first().contains(card).should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { randHash } from '../utils'
|
||||
const randUser = randHash()
|
||||
|
||||
describe('Deck dashboard', function() {
|
||||
const password = 'pass123'
|
||||
|
||||
before(function () {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it('Can show the right title on the dashboard', function() {
|
||||
cy.get('.board-title h2')
|
||||
.should('have.length', 1).first()
|
||||
.should('have.text', 'Upcoming cards')
|
||||
})
|
||||
|
||||
it('Can see the default "Personal Board" created for user by default', function () {
|
||||
const defaultBoard = 'Personal'
|
||||
|
||||
cy.openLeftSidebar()
|
||||
cy.get('.app-navigation__list .app-navigation-entry')
|
||||
.eq(1)
|
||||
.find('ul.app-navigation-entry__children li.app-navigation-entry')
|
||||
.first()
|
||||
.contains(defaultBoard)
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
import { randHash } from "../utils";
|
||||
const randUser = randHash();
|
||||
|
||||
describe("Stack", function () {
|
||||
const board = "TestBoard";
|
||||
const password = "pass123";
|
||||
const stack = "List 1";
|
||||
|
||||
before(function () {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
cy.deckCreateBoard({ user: randUser, password }, board)
|
||||
})
|
||||
|
||||
beforeEach(function () {
|
||||
cy.logout()
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it("Can create a stack", function () {
|
||||
cy.openLeftSidebar()
|
||||
cy.get("#app-navigation-vue .app-navigation__list .app-navigation-entry")
|
||||
.eq(3)
|
||||
.find("a.app-navigation-entry-link")
|
||||
.first()
|
||||
.click({ force: true })
|
||||
|
||||
cy.get("#stack-add button").first().click()
|
||||
cy.get("#stack-add form input#new-stack-input-main").type(stack)
|
||||
cy.get("#stack-add form input[type=submit]").first().click()
|
||||
|
||||
cy.get(".board .stack").eq(0).contains(stack).should("be.visible")
|
||||
})
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
const url = Cypress.config("baseUrl").replace(/\/index.php\/?$/g, "");
|
||||
Cypress.env("baseUrl", url);
|
||||
|
||||
Cypress.Commands.add("login", (user, password, route = "/apps/deck/") => {
|
||||
let session = `${user}-${Date.now()}`;
|
||||
cy.session(session, function () {
|
||||
cy.visit(route);
|
||||
cy.get("input[name=user]").type(user);
|
||||
cy.get("input[name=password]").type(password);
|
||||
cy.get(".submit-wrapper input[type=submit]").click();
|
||||
cy.url().should("include", route);
|
||||
});
|
||||
// in case the session already existed but we are on a different route...
|
||||
cy.visit(route);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("logout", (route = "/") => {
|
||||
cy.session("_guest", function () {});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("nextcloudCreateUser", (user, password) => {
|
||||
cy.clearCookies();
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${Cypress.env("baseUrl")}/ocs/v1.php/cloud/users?format=json`,
|
||||
form: true,
|
||||
body: {
|
||||
userid: user,
|
||||
password: password,
|
||||
},
|
||||
auth: { user: "admin", pass: "admin" },
|
||||
headers: {
|
||||
"OCS-ApiRequest": "true",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}).then((response) => {
|
||||
cy.log(`Created user ${user}`, response.status);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("nextcloudUpdateUser", (user, password, key, value) => {
|
||||
cy.request({
|
||||
method: "PUT",
|
||||
url: `${Cypress.env("baseUrl")}/ocs/v2.php/cloud/users/${user}`,
|
||||
form: true,
|
||||
body: { key, value },
|
||||
auth: { user, pass: password },
|
||||
headers: {
|
||||
"OCS-ApiRequest": "true",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
}).then((response) => {
|
||||
cy.log(`Updated user ${user} ${key} to ${value}`, response.status);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openLeftSidebar", () => {
|
||||
cy.get(".app-navigation button.app-navigation-toggle").click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("deckCreateBoard", ({ user, password }, title) => {
|
||||
cy.login(user, password);
|
||||
|
||||
cy.get(".app-navigation button.app-navigation-toggle").click();
|
||||
cy.get("#app-navigation-vue .app-navigation__list .app-navigation-entry")
|
||||
.eq(3)
|
||||
.find("a")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
cy.get(".board-create form input[type=text]").type(title, { force: true });
|
||||
|
||||
cy.get(".board-create form input[type=submit]")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
Cypress.Commands.add("deckCreateList", ({ user, password }, title) => {
|
||||
cy.login(user, password);
|
||||
|
||||
cy.get(".app-navigation button.app-navigation-toggle").click();
|
||||
cy.get("#app-navigation-vue .app-navigation__list .app-navigation-entry")
|
||||
.eq(3)
|
||||
.find("a.app-navigation-entry-link")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
cy.get("#stack-add button").first().click();
|
||||
cy.get("#stack-add form input#new-stack-input-main").type(title);
|
||||
cy.get("#stack-add form input[type=submit]").first().click();
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -1 +0,0 @@
|
||||
export const randHash = () => Math.random().toString(36).replace(/[^a-z]+/g, '').slice(0, 10)
|
||||
68
docs/API.md
@@ -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
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ Overall, Deck is easy to use. You can create boards, add users, share the Deck,
|
||||
3. [Handle cards options](#3-handle-cards-options)
|
||||
4. [Archive old tasks](#4-archive-old-tasks)
|
||||
5. [Manage your board](#5-manage-your-board)
|
||||
6. [Import boards](#6-import-boards)
|
||||
7. [Search](#7-search)
|
||||
8. [New owner for the deck entities](#8-new-owner-for-the-deck-entities)
|
||||
6. [New owner for the deck entities](#8-new-owner-for-the-deck-entities)
|
||||
|
||||
### 1. Create my first board
|
||||
In this example, we're going to create a board and share it with an other nextcloud user.
|
||||
@@ -72,80 +70,14 @@ The **sharing tab** allows you to add users or even groups to your boards.
|
||||
**Deleted objects** allows you to return previously deleted stacks or cards.
|
||||
The **Timeline** allows you to see everything that happened in your boards. Everything!
|
||||
|
||||
### 6. Import boards
|
||||
|
||||
Importing can be done using the API or the `occ` `deck:import` command.
|
||||
|
||||
Comments with more than 1000 characters are placed as attached files to the card.
|
||||
|
||||
It is possible to import from the following sources:
|
||||
|
||||
#### Trello JSON
|
||||
|
||||
Steps:
|
||||
* Create the data file
|
||||
* Access Trello
|
||||
* go to the board you want to export
|
||||
* Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON
|
||||
* Create the configuration file
|
||||
* Execute the import informing the import file path, data file and source as `Trello JSON`
|
||||
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON`
|
||||
|
||||
Example configuration file:
|
||||
```json
|
||||
{
|
||||
"owner": "admin",
|
||||
"color": "0800fd",
|
||||
"uidRelation": {
|
||||
"johndoe": "johndoe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Limitations**:
|
||||
|
||||
Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has.
|
||||
|
||||
#### Trello API
|
||||
|
||||
Import using API is recommended for boards with more than 1000 actions.
|
||||
|
||||
Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello.
|
||||
|
||||
* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization)
|
||||
* Get the ID of the board you want to import by making a request to:
|
||||
https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name
|
||||
|
||||
This ID you will use in the configuration file in the `board` property
|
||||
* Create the configuration file
|
||||
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON`
|
||||
|
||||
Example configuration file:
|
||||
```json
|
||||
{
|
||||
"owner": "admin",
|
||||
"color": "0800fd",
|
||||
"api": {
|
||||
"key": "0cc175b9c0f1b6a831c399e269772661",
|
||||
"token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33"
|
||||
},
|
||||
"board": "8277e0910d750195b4487976",
|
||||
"uidRelation": {
|
||||
"johndoe": "johndoe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Search
|
||||
## Search
|
||||
|
||||
Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls.
|
||||
This search allows advanced filtering of cards across all board of the logged in user.
|
||||
|
||||
For example the search `project tag:ToDo assigned:alice assigned:bob` will return all cards where the card title or description contains project **and** the tag ToDo is set **and** the user alice is assigned **and** the user bob is assigned.
|
||||
|
||||
#### Supported search filters
|
||||
### Supported search filters
|
||||
|
||||
| Filter | Operators | Query |
|
||||
| ----------- | ----------------- | ------------------------------------------------------------ |
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||

|
||||
@@ -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->A2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>A1->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->A3 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>A2->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->A7 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>A3->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->A9 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>A3->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->A10 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>A3->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->A5 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>A4->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->A6 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>A5->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->A3 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>A6->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->A3 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>A7->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"><<abstract>></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->A8 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>A7->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->A3 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>A9->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->A8 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>A9->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->A11 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>A9->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 |
@@ -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]
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0" viewbox="0 0 32 32">
|
||||
<path d="m16 1-10 18h11l-1 12 10-18h-11z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 205 B |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0" viewBox="0 0 32 32">
|
||||
<path d="m16 1-10 18h11l-1 12 10-18h-11z" fill="#FFF"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 217 B |
1
img/archive-white.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g transform="translate(0 -1036.362)" fill="#fff"><path d="M1.93 1041.296c-.185 0-.336.138-.336.31v9.842c0 .172.15.313.336.313h12.517c.185 0 .333-.14.333-.313v-9.842c0-.172-.148-.31-.333-.31H1.93zm4.124 1.507h4.223c.39 0 .705.314.705.704v.43c0 .39-.315.705-.705.705H6.054a.703.703 0 0 1-.705-.705v-.43c0-.39.314-.704.705-.704z"/><rect width="15.742" height="2.296" x=".136" y="1037.543" ry="0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 488 B |
1
img/archive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g transform="translate(0 -1036.362)"><path d="M1.93 1041.296c-.185 0-.336.138-.336.31v9.842c0 .172.15.313.336.313h12.517c.185 0 .333-.14.333-.313v-9.842c0-.172-.148-.31-.333-.31H1.93zm4.124 1.507h4.223c.39 0 .705.314.705.704v.43c0 .39-.315.705-.705.705H6.054a.703.703 0 0 1-.705-.705v-.43c0-.39.314-.704.705-.704z"/><rect width="15.742" height="2.296" x=".136" y="1037.543" ry=".42"/></g></svg>
|
||||
|
After Width: | Height: | Size: 478 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" width="512" height="512"><g fill="#000"><path d="M54.319 37.839C54.762 35.918 55 33.96 55 32c0-9.095-4.631-17.377-12.389-22.153a1 1 0 1 0-1.049 1.703C48.724 15.96 53 23.604 53 32c0 1.726-.2 3.451-.573 5.147A6.992 6.992 0 0 0 51 37c-3.86 0-7 3.141-7 7s3.14 7 7 7 7-3.141 7-7a7.006 7.006 0 0 0-3.681-6.161zM38.171 54.182A23.867 23.867 0 0 1 29 56a24.047 24.047 0 0 1-17.017-7.092A6.974 6.974 0 0 0 14 44c0-3.859-3.14-7-7-7s-7 3.141-7 7 3.14 7 7 7a6.952 6.952 0 0 0 3.381-.875C15.26 55.136 21.994 58 29 58c3.435 0 6.778-.663 9.936-1.971.51-.211.753-.796.542-1.307a1.001 1.001 0 0 0-1.307-.54zM4 31.213a1 1 0 0 0 1.068-.927c.712-10.089 7.586-18.52 17.22-21.314C23.142 11.874 25.825 14 29 14c3.86 0 7-3.141 7-7s-3.14-7-7-7c-3.851 0-6.985 3.127-6.999 6.975C11.42 9.922 3.851 19.12 3.073 30.146A.999.999 0 0 0 4 31.213z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 885 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" width="512" height="512"><g fill="#fff"><path d="M54.319 37.839C54.762 35.918 55 33.96 55 32c0-9.095-4.631-17.377-12.389-22.153a1 1 0 1 0-1.049 1.703C48.724 15.96 53 23.604 53 32c0 1.726-.2 3.451-.573 5.147A6.992 6.992 0 0 0 51 37c-3.86 0-7 3.141-7 7s3.14 7 7 7 7-3.141 7-7a7.006 7.006 0 0 0-3.681-6.161zM38.171 54.182A23.867 23.867 0 0 1 29 56a24.047 24.047 0 0 1-17.017-7.092A6.974 6.974 0 0 0 14 44c0-3.859-3.14-7-7-7s-7 3.141-7 7 3.14 7 7 7a6.952 6.952 0 0 0 3.381-.875C15.26 55.136 21.994 58 29 58c3.435 0 6.778-.663 9.936-1.971.51-.211.753-.796.542-1.307a1.001 1.001 0 0 0-1.307-.54zM4 31.213a1 1 0 0 0 1.068-.927c.712-10.089 7.586-18.52 17.22-21.314C23.142 11.874 25.825 14 29 14c3.86 0 7-3.141 7-7s-3.14-7-7-7c-3.851 0-6.985 3.127-6.999 6.975C11.42 9.922 3.851 19.12 3.073 30.146A.999.999 0 0 0 4 31.213z"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" width="512" height="512"><g fill="#000"><path d="M54.319 37.839C54.762 35.918 55 33.96 55 32c0-9.095-4.631-17.377-12.389-22.153a1 1 0 1 0-1.049 1.703C48.724 15.96 53 23.604 53 32c0 1.726-.2 3.451-.573 5.147A6.992 6.992 0 0 0 51 37c-3.86 0-7 3.141-7 7s3.14 7 7 7 7-3.141 7-7a7.006 7.006 0 0 0-3.681-6.161zM38.171 54.182A23.867 23.867 0 0 1 29 56a24.047 24.047 0 0 1-17.017-7.092A6.974 6.974 0 0 0 14 44c0-3.859-3.14-7-7-7s-7 3.141-7 7 3.14 7 7 7a6.952 6.952 0 0 0 3.381-.875C15.26 55.136 21.994 58 29 58c3.435 0 6.778-.663 9.936-1.971.51-.211.753-.796.542-1.307a1.001 1.001 0 0 0-1.307-.54zM4 31.213a1 1 0 0 0 1.068-.927c.712-10.089 7.586-18.52 17.22-21.314C23.142 11.874 25.825 14 29 14c3.86 0 7-3.141 7-7s-3.14-7-7-7c-3.851 0-6.985 3.127-6.999 6.975C11.42 9.922 3.851 19.12 3.073 30.146A.999.999 0 0 0 4 31.213z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 885 B After Width: | Height: | Size: 885 B |
1
img/clone.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.8 13.8H2.2V4.2h9.6m1.2 0c0-.67-.53-1.2-1.2-1.2H2.2C1.53 3 1 3.53 1 4.2v9.6c0 .67.53 1.2 1.2 1.2h9.6c.67 0 1.2-.53 1.2-1.2"/><path d="m4.2 1c-0.67 0-1.2 0.54-1.2 1.2h10.8v10.8c0.67 0 1.2-0.53 1.2-1.2v-9.6c0-0.67-0.53-1.2-1.2-1.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
1
img/reply.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M15 15s-.4-7.8-7-10V1L1 8l7 7v-4c5.1 0 7 4 7 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -31,6 +31,7 @@ use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Db\AclMapper;
|
||||
use OCA\Deck\Db\Assignment;
|
||||
use OCA\Deck\Db\Attachment;
|
||||
use OCA\Deck\Db\AttachmentMapper;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
@@ -49,15 +50,12 @@ use OCP\L10N\IFactory;
|
||||
|
||||
class ActivityManager {
|
||||
public const DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED = 'DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED';
|
||||
|
||||
public const SUBJECT_PARAMS_MAX_LENGTH = 4000;
|
||||
public const SHORTENED_DESCRIPTION_MAX_LENGTH = 2000;
|
||||
|
||||
private $manager;
|
||||
private $userId;
|
||||
private $permissionService;
|
||||
private $boardMapper;
|
||||
private $cardMapper;
|
||||
private $attachmentMapper;
|
||||
private $aclMapper;
|
||||
private $stackMapper;
|
||||
private $l10nFactory;
|
||||
@@ -112,6 +110,7 @@ class ActivityManager {
|
||||
BoardMapper $boardMapper,
|
||||
CardMapper $cardMapper,
|
||||
StackMapper $stackMapper,
|
||||
AttachmentMapper $attachmentMapper,
|
||||
AclMapper $aclMapper,
|
||||
IFactory $l10nFactory,
|
||||
$userId
|
||||
@@ -121,6 +120,7 @@ class ActivityManager {
|
||||
$this->boardMapper = $boardMapper;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->stackMapper = $stackMapper;
|
||||
$this->attachmentMapper = $attachmentMapper;
|
||||
$this->aclMapper = $aclMapper;
|
||||
$this->l10nFactory = $l10nFactory;
|
||||
$this->userId = $userId;
|
||||
@@ -249,6 +249,19 @@ class ActivityManager {
|
||||
try {
|
||||
$event = $this->createEvent($objectType, $entity, $subject, $additionalParams, $author);
|
||||
if ($event !== null) {
|
||||
$json = json_encode($event->getSubjectParameters());
|
||||
if (mb_strlen($json) > 4000) {
|
||||
$params = json_decode(json_encode($event->getSubjectParameters()), true);
|
||||
|
||||
$newContent = $params['after'];
|
||||
unset($params['before'], $params['after'], $params['card']['description']);
|
||||
|
||||
$params['after'] = mb_substr($newContent, 0, 2000);
|
||||
if (mb_strlen($newContent) > 2000) {
|
||||
$params['after'] .= '...';
|
||||
}
|
||||
$event->setSubject($event->getSubject(), $params);
|
||||
}
|
||||
$this->sendToUsers($event);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
@@ -397,31 +410,12 @@ class ActivityManager {
|
||||
|
||||
$subjectParams['author'] = $author === null ? $this->userId : $author;
|
||||
|
||||
$subjectParams = array_merge($subjectParams, $additionalParams);
|
||||
$json = json_encode($subjectParams);
|
||||
if (mb_strlen($json) > self::SUBJECT_PARAMS_MAX_LENGTH) {
|
||||
$params = json_decode(json_encode($subjectParams), true);
|
||||
|
||||
if ($subject === self::SUBJECT_CARD_UPDATE_DESCRIPTION && isset($params['after'])) {
|
||||
$newContent = $params['after'];
|
||||
unset($params['before'], $params['after'], $params['card']['description']);
|
||||
|
||||
$params['after'] = mb_substr($newContent, 0, self::SHORTENED_DESCRIPTION_MAX_LENGTH);
|
||||
if (mb_strlen($newContent) > self::SHORTENED_DESCRIPTION_MAX_LENGTH) {
|
||||
$params['after'] .= '...';
|
||||
}
|
||||
$subjectParams = $params;
|
||||
} else {
|
||||
throw new \Exception('Subject parameters too long');
|
||||
}
|
||||
}
|
||||
|
||||
$event = $this->manager->generateEvent();
|
||||
$event->setApp('deck')
|
||||
->setType($eventType)
|
||||
->setAuthor($subjectParams['author'])
|
||||
->setObject($objectType, (int)$object->getId(), $object->getTitle())
|
||||
->setSubject($subject, $subjectParams)
|
||||
->setSubject($subject, array_merge($subjectParams, $additionalParams))
|
||||
->setTimestamp(time());
|
||||
|
||||
if ($message !== null) {
|
||||
|
||||
@@ -69,7 +69,15 @@ class ChangeSet implements \JsonSerializable {
|
||||
return $this->after;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
*
|
||||
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||
* which is a value of any type other than a resource.
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function jsonSerialize() {
|
||||
return [
|
||||
'before' => $this->getBefore(),
|
||||
'after' => $this->getAfter(),
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace OCA\Deck\Cache;
|
||||
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
|
||||
class AttachmentCacheHelper {
|
||||
/** @var ICache */
|
||||
private $cache;
|
||||
|
||||
public function __construct(ICacheFactory $cacheFactory) {
|
||||
$this->cache = $cacheFactory->createDistributed('deck-attachments');
|
||||
}
|
||||
|
||||
public function getAttachmentCount(int $cardId): ?int {
|
||||
return $this->cache->get('count-' . $cardId);
|
||||
}
|
||||
|
||||
public function setAttachmentCount(int $cardId, int $count): void {
|
||||
$this->cache->set('count-' . $cardId, $count);
|
||||
}
|
||||
|
||||
public function clearAttachmentCount(int $cardId): void {
|
||||
$this->cache->remove('count-' . $cardId);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\StackMapper;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\Service\BoardService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
@@ -102,9 +101,7 @@ class UserExport extends Command {
|
||||
$fullCard = $this->cardMapper->find($card->getId());
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$fullCard->setAssignedUsers($assignedUsers);
|
||||
|
||||
$cardDetails = new CardDetails($fullCard, $fullBoard);
|
||||
$data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = $cardDetails->jsonSerialize();
|
||||
$data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = (array)$fullCard->jsonSerialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ declare(strict_types=1);
|
||||
namespace OCA\Deck\Controller;
|
||||
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\Service\SearchService;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
@@ -51,12 +50,9 @@ class SearchController extends OCSController {
|
||||
public function search(string $term, ?int $limit = null, ?int $cursor = null): DataResponse {
|
||||
$cards = $this->searchService->searchCards($term, $limit, $cursor);
|
||||
return new DataResponse(array_map(function (Card $card) {
|
||||
$board = $card->getRelatedBoard();
|
||||
$json = (new CardDetails($card, $board))->jsonSerialize();
|
||||
|
||||
$json['relatedBoard'] = $board;
|
||||
$json = $card->jsonSerialize();
|
||||
$json['relatedStack'] = $card->getRelatedStack();
|
||||
|
||||
$json['relatedBoard'] = $card->getRelatedBoard();
|
||||
return $json;
|
||||
}, $cards));
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
|
||||
namespace OCA\Deck\Cron;
|
||||
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\Job;
|
||||
use OC\BackgroundJob\Job;
|
||||
use OCA\Deck\Activity\ActivityManager;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
|
||||
@@ -36,8 +35,7 @@ class CardDescriptionActivity extends Job {
|
||||
/** @var CardMapper */
|
||||
private $cardMapper;
|
||||
|
||||
public function __construct(ITimeFactory $time, ActivityManager $activityManager, CardMapper $cardMapper) {
|
||||
parent::__construct($time);
|
||||
public function __construct(ActivityManager $activityManager, CardMapper $cardMapper) {
|
||||
$this->activityManager = $activityManager;
|
||||
$this->cardMapper = $cardMapper;
|
||||
}
|
||||
|
||||
@@ -24,15 +24,13 @@
|
||||
|
||||
namespace OCA\Deck\Cron;
|
||||
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
use OC\BackgroundJob\Job;
|
||||
use OCA\Deck\Db\AttachmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\InvalidAttachmentType;
|
||||
use OCA\Deck\Service\AttachmentService;
|
||||
use OCP\BackgroundJob\IJob;
|
||||
|
||||
class DeleteCron extends TimedJob {
|
||||
class DeleteCron extends Job {
|
||||
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
@@ -41,14 +39,10 @@ class DeleteCron extends TimedJob {
|
||||
/** @var AttachmentMapper */
|
||||
private $attachmentMapper;
|
||||
|
||||
public function __construct(ITimeFactory $time, BoardMapper $boardMapper, AttachmentService $attachmentService, AttachmentMapper $attachmentMapper) {
|
||||
parent::__construct($time);
|
||||
public function __construct(BoardMapper $boardMapper, AttachmentService $attachmentService, AttachmentMapper $attachmentMapper) {
|
||||
$this->boardMapper = $boardMapper;
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->attachmentMapper = $attachmentMapper;
|
||||
|
||||
$this->setInterval(60 * 60 * 24);
|
||||
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
|
||||
namespace OCA\Deck\Cron;
|
||||
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\Job;
|
||||
use OC\BackgroundJob\Job;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Notification\NotificationHelper;
|
||||
@@ -41,12 +40,10 @@ class ScheduledNotifications extends Job {
|
||||
protected $logger;
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
CardMapper $cardMapper,
|
||||
NotificationHelper $notificationHelper,
|
||||
ILogger $logger
|
||||
) {
|
||||
parent::__construct($time);
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->notificationHelper = $notificationHelper;
|
||||
$this->logger = $logger;
|
||||
|
||||
@@ -33,45 +33,18 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
|
||||
parent::__construct($db, 'deck_board_acl', Acl::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Acl[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage')
|
||||
->from('deck_board_acl')
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $userId
|
||||
* @param numeric $aclId
|
||||
* @return bool
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function isOwner($userId, $aclId): bool {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('acl.id')
|
||||
->from($this->getTableName(), 'acl')
|
||||
->innerJoin('acl', 'deck_boards', 'b', 'acl.board_id = b.id')
|
||||
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
|
||||
->andWhere($qb->expr()->eq('acl.id', $qb->createNamedParameter($aclId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return count($qb->executeQuery()->fetchAll()) > 0;
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return int|null
|
||||
*/
|
||||
public function findBoardId($id): ?int {
|
||||
try {
|
||||
$entity = $this->find($id);
|
||||
@@ -81,21 +54,9 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $type
|
||||
* @param string $participant
|
||||
* @return Acl[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findByParticipant($type, $participant): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return $this->findEntities($qb);
|
||||
$sql = 'SELECT * from *PREFIX*deck_board_acl WHERE type = ? AND participant = ?';
|
||||
return $this->findEntities($sql, [$type, $participant]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,6 +55,9 @@ class AssignmentMapper extends QBMapper implements IPermissionMapper {
|
||||
$this->circleService = $circleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Assignment[]
|
||||
*/
|
||||
public function findAll(int $cardId): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
|
||||
@@ -30,6 +30,7 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
use PDO;
|
||||
|
||||
class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
private $cardMapper;
|
||||
@@ -51,53 +52,70 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Attachment
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws \OCP\DB\Exception
|
||||
* @param $id
|
||||
* @return Entity|Attachment
|
||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
*/
|
||||
public function find($id) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->from('deck_attachment')
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
$cursor = $qb->execute();
|
||||
$row = $cursor->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row === false) {
|
||||
$cursor->closeCursor();
|
||||
throw new DoesNotExistException('Did expect one result but found none when executing' . $qb);
|
||||
}
|
||||
|
||||
$row2 = $cursor->fetch();
|
||||
$cursor->closeCursor();
|
||||
if ($row2 !== false) {
|
||||
throw new MultipleObjectsReturnedException('Did not expect more than one result when executing' . $qb);
|
||||
}
|
||||
|
||||
return $this->mapRowToEntity($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $cardId
|
||||
* @param string $data
|
||||
* @return Attachment
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findByData($cardId, $data) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->from('deck_attachment')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('data', $qb->createNamedParameter($data, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
$cursor = $qb->execute();
|
||||
$row = $cursor->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row === false) {
|
||||
$cursor->closeCursor();
|
||||
throw new DoesNotExistException('Did expect one result but found none when executing' . $qb);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
return $this->mapRowToEntity($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all attachments for a card
|
||||
*
|
||||
* @param $cardId
|
||||
* @return Entity[]
|
||||
* @throws \OCP\DB\Exception
|
||||
* @return array
|
||||
*/
|
||||
public function findAll($cardId) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->from('deck_attachment')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
|
||||
return $this->findEntities($qb);
|
||||
$entities = [];
|
||||
$cursor = $qb->execute();
|
||||
while ($row = $cursor->fetch()) {
|
||||
$entities[] = $this->mapRowToEntity($row);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +128,7 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
$timeLimit = time() - (60 * 5);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->from('deck_attachment')
|
||||
->where($qb->expr()->gt('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
if ($withOffset) {
|
||||
$qb
|
||||
@@ -121,7 +139,13 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
->andWhere($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
|
||||
return $this->findEntities($qb);
|
||||
$entities = [];
|
||||
$cursor = $qb->execute();
|
||||
while ($row = $cursor->fetch()) {
|
||||
$entities[] = $this->mapRowToEntity($row);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
return $entities;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,23 +23,13 @@
|
||||
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
/**
|
||||
* @method int getId()
|
||||
* @method string getTitle()
|
||||
* @method int getShared()
|
||||
* @method bool getArchived()
|
||||
* @method int getDeletedAt()
|
||||
* @method int getLastModified()
|
||||
*/
|
||||
class Board extends RelationalEntity {
|
||||
protected $title;
|
||||
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;
|
||||
@@ -66,15 +56,11 @@ class Board extends RelationalEntity {
|
||||
$this->shared = -1;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
public function jsonSerialize() {
|
||||
$json = parent::jsonSerialize();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -82,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,13 @@ 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 +43,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
/** @var CappedMemoryCache */
|
||||
private $userBoardCache;
|
||||
/** @var CappedMemoryCache */
|
||||
private $boardCache;
|
||||
|
||||
public function __construct(
|
||||
IDBConnection $db,
|
||||
@@ -67,7 +64,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
$this->logger = $logger;
|
||||
|
||||
$this->userBoardCache = new CappedMemoryCache();
|
||||
$this->boardCache = new CappedMemoryCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,52 +71,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 +121,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 +142,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 +156,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 +181,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 +208,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) {
|
||||
@@ -494,11 +323,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
* Reset cache for a given board or a given user
|
||||
*/
|
||||
public function flushCache(?int $boardId = null, ?string $userId = null) {
|
||||
if ($boardId) {
|
||||
unset($this->boardCache[$boardId]);
|
||||
} else {
|
||||
$this->boardCache = null;
|
||||
}
|
||||
if ($userId) {
|
||||
unset($this->userBoardCache[$userId]);
|
||||
} else {
|
||||
|
||||
@@ -27,44 +27,6 @@ use DateTime;
|
||||
use DateTimeZone;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
||||
/**
|
||||
* @method string getTitle()
|
||||
* @method string getDescription()
|
||||
* @method string getDescriptionPrev()
|
||||
* @method int getStackId()
|
||||
* @method int getOrder()
|
||||
* @method int getLastModified()
|
||||
* @method int getCreatedAt()
|
||||
* @method bool getArchived()
|
||||
* @method bool getNotified()
|
||||
*
|
||||
* @method void setLabels(Label[] $labels)
|
||||
* @method null|Label[] getLabels()
|
||||
*
|
||||
* @method void setAssignedUsers(Assignment[] $users)
|
||||
* @method null|User[] getAssignedUsers()
|
||||
*
|
||||
* @method void setAttachments(Attachment[] $attachments)
|
||||
* @method null|Attachment[] getAttachments()
|
||||
*
|
||||
* @method void setAttachmentCount(int $count)
|
||||
* @method null|int getAttachmentCount()
|
||||
*
|
||||
* @method void setCommentsUnread(int $count)
|
||||
* @method null|int getCommentsUnread()
|
||||
*
|
||||
* @method void setCommentsCount(int $count)
|
||||
* @method null|int getCommentsCount()
|
||||
*
|
||||
* @method void setOwner(string $user)
|
||||
* @method null|string getOwner()
|
||||
*
|
||||
* @method void setRelatedStack(Stack $stack)
|
||||
* @method null|Stack getRelatedStack()
|
||||
*
|
||||
* @method void setRelatedBoard(Board $board)
|
||||
* @method null|Board getRelatedBoard()
|
||||
*/
|
||||
class Card extends RelationalEntity {
|
||||
public const TITLE_MAX_LENGTH = 255;
|
||||
|
||||
@@ -88,7 +50,7 @@ class Card extends RelationalEntity {
|
||||
protected $deletedAt = 0;
|
||||
protected $commentsUnread = 0;
|
||||
protected $commentsCount = 0;
|
||||
|
||||
|
||||
protected $relatedStack = null;
|
||||
protected $relatedBoard = null;
|
||||
|
||||
@@ -108,7 +70,6 @@ class Card extends RelationalEntity {
|
||||
$this->addType('archived', 'boolean');
|
||||
$this->addType('notified', 'boolean');
|
||||
$this->addType('deletedAt', 'integer');
|
||||
$this->addType('duedate', 'string');
|
||||
$this->addRelation('labels');
|
||||
$this->addRelation('assignedUsers');
|
||||
$this->addRelation('attachments');
|
||||
@@ -117,7 +78,7 @@ class Card extends RelationalEntity {
|
||||
$this->addRelation('commentsUnread');
|
||||
$this->addRelation('commentsCount');
|
||||
$this->addResolvable('owner');
|
||||
|
||||
|
||||
$this->addRelation('relatedStack');
|
||||
$this->addRelation('relatedBoard');
|
||||
}
|
||||
@@ -126,18 +87,49 @@ class Card extends RelationalEntity {
|
||||
$this->databaseType = $type;
|
||||
}
|
||||
|
||||
public function getDueDateTime(): ?DateTime {
|
||||
return $this->duedate ? new DateTime($this->duedate) : null;
|
||||
public function getDuedate($isoFormat = false) {
|
||||
if ($this->duedate === null) {
|
||||
return null;
|
||||
}
|
||||
$dt = new DateTime($this->duedate);
|
||||
if (!$isoFormat && $this->databaseType === 'mysql') {
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
return $dt->format('c');
|
||||
}
|
||||
|
||||
public function getDuedate($isoFormat = false): ?string {
|
||||
$dt = $this->getDueDateTime();
|
||||
$format = 'c';
|
||||
if (!$isoFormat && $this->databaseType === 'mysql') {
|
||||
$format = 'Y-m-d H:i:s';
|
||||
}
|
||||
public function jsonSerialize() {
|
||||
$json = parent::jsonSerialize();
|
||||
$json['overdue'] = self::DUEDATE_FUTURE;
|
||||
$due = strtotime($this->duedate);
|
||||
|
||||
return $dt ? $dt->format($format) : null;
|
||||
$today = new DateTime();
|
||||
$today->setTime(0, 0);
|
||||
|
||||
$match_date = new DateTime($this->duedate);
|
||||
|
||||
$match_date->setTime(0, 0);
|
||||
|
||||
$diff = $today->diff($match_date);
|
||||
$diffDays = (integer) $diff->format('%R%a'); // Extract days count in interval
|
||||
|
||||
if ($due !== false) {
|
||||
if ($diffDays === 1) {
|
||||
$json['overdue'] = self::DUEDATE_NEXT;
|
||||
}
|
||||
if ($diffDays === 0) {
|
||||
$json['overdue'] = self::DUEDATE_NOW;
|
||||
}
|
||||
if ($diffDays < 0) {
|
||||
$json['overdue'] = self::DUEDATE_OVERDUE;
|
||||
}
|
||||
}
|
||||
$json['duedate'] = $this->getDuedate(true);
|
||||
unset($json['notified']);
|
||||
unset($json['descriptionPrev']);
|
||||
unset($json['relatedStack']);
|
||||
unset($json['relatedBoard']);
|
||||
return $json;
|
||||
}
|
||||
|
||||
public function getCalendarObject(): VCalendar {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -226,21 +214,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function findAllByBoardId(int $boardId, ?int $limit = null, ?int $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('c.*')
|
||||
->from('deck_cards', 'c')
|
||||
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
|
||||
->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id')
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset)
|
||||
->orderBy('c.lastmodified')
|
||||
->addOrderBy('c.id');
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function findAllWithDue($boardId) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('c.*')
|
||||
@@ -280,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)))
|
||||
@@ -291,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'));
|
||||
@@ -506,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)
|
||||
@@ -533,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);
|
||||
}
|
||||
|
||||
@@ -566,29 +540,18 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
public function isOwner($userId, $cardId): bool {
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id IN (SELECT stack_id FROM `*PREFIX*deck_cards` WHERE id = ?))';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT, 0);
|
||||
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -23,15 +23,17 @@
|
||||
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\AppFramework\Db\Mapper;
|
||||
|
||||
/**
|
||||
* Class DeckMapper
|
||||
*
|
||||
* @package OCA\Deck\Db
|
||||
* @deprecated use QBMapper
|
||||
*
|
||||
* TODO: Move to QBMapper once Nextcloud 14 is a minimum requirement
|
||||
*/
|
||||
class DeckMapper extends QBMapper {
|
||||
class DeckMapper extends Mapper {
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
@@ -40,11 +42,11 @@ class DeckMapper extends QBMapper {
|
||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||
*/
|
||||
public function find($id) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
$sql = 'SELECT * FROM `' . $this->tableName . '` ' . 'WHERE `id` = ?';
|
||||
return $this->findEntity($sql, [$id]);
|
||||
}
|
||||
|
||||
return $this->findEntity($qb);
|
||||
protected function execute($sql, array $params = [], $limit = null, $offset = null) {
|
||||
return parent::execute($sql, $params, $limit, $offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace OCA\Deck\Db;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class LabelMapper extends DeckMapper implements IPermissionMapper {
|
||||
@@ -34,105 +33,41 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
|
||||
parent::__construct($db, 'deck_labels', Label::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Label[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($boardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
return $this->findEntities($qb);
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_labels` WHERE `board_id` = ? ORDER BY `id`';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function delete(Entity $entity): Entity {
|
||||
public function delete(\OCP\AppFramework\Db\Entity $entity) {
|
||||
// delete assigned labels
|
||||
$this->deleteLabelAssignments($entity->getId());
|
||||
// delete label
|
||||
return parent::delete($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $cardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Label[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAssignedLabelsForCard($cardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('l.*', 'card_id')
|
||||
->from($this->getTableName(), 'l')
|
||||
->innerJoin('l', 'deck_assigned_labels', 'al', 'l.id = al.label_id')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->orderBy('l.id')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
public function findAssignedLabelsForCard($cardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT l.*,card_id FROM `*PREFIX*deck_assigned_labels` as al INNER JOIN *PREFIX*deck_labels as l ON l.id = al.label_id WHERE `card_id` = ? ORDER BY l.id';
|
||||
return $this->findEntities($sql, [$cardId], $limit, $offset);
|
||||
}
|
||||
public function findAssignedLabelsForBoard($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT c.id as card_id, l.id as id, l.title as title, l.color as color FROM `*PREFIX*deck_cards` as c ' .
|
||||
' INNER JOIN `*PREFIX*deck_assigned_labels` as al ON al.card_id = c.id INNER JOIN `*PREFIX*deck_labels` as l ON al.label_id = l.id WHERE board_id=? ORDER BY l.id';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Label[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAssignedLabelsForBoard($boardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('l.id as id', 'l.title as title', 'l.color as color')
|
||||
->selectAlias('c.id', 'card_id')
|
||||
->from($this->getTableName(), 'l')
|
||||
->innerJoin('l', 'deck_assigned_labels', 'al', 'al.label_id = l.id')
|
||||
->innerJoin('l', 'deck_cards', 'c', 'al.card_id = c.id')
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->orderBy('l.id')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function insert(Entity $entity): Entity {
|
||||
public function insert(Entity $entity) {
|
||||
$entity->setLastModified(time());
|
||||
return parent::insert($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @param bool $updateModified
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function update(Entity $entity, $updateModified = true): Entity {
|
||||
public function update(Entity $entity, $updateModified = true) {
|
||||
if ($updateModified) {
|
||||
$entity->setLastModified(time());
|
||||
}
|
||||
return parent::update($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @return array
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
|
||||
public function getAssignedLabelsForBoard($boardId) {
|
||||
$labels = $this->findAssignedLabelsForBoard($boardId);
|
||||
$result = [];
|
||||
@@ -145,51 +80,27 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $labelId
|
||||
* @return void
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function deleteLabelAssignments($labelId) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete('deck_assigned_labels')
|
||||
->where($qb->expr()->eq('label_id', $qb->createNamedParameter($labelId, IQueryBuilder::PARAM_INT)));
|
||||
$qb->executeStatement();
|
||||
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE label_id = ?';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $labelId, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $cardId
|
||||
* @return void
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function deleteLabelAssignmentsForCard($cardId) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete('deck_assigned_labels')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
|
||||
$qb->executeStatement();
|
||||
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE card_id = ?';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param numeric $labelId
|
||||
* @return bool
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function isOwner($userId, $labelId): bool {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('l.id')
|
||||
->from($this->getTableName(), 'l')
|
||||
->innerJoin('l', 'deck_boards', 'b', 'l.board_id = b.id')
|
||||
->where($qb->expr()->eq('l.id', $qb->createNamedParameter($labelId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return count($qb->executeQuery()->fetchAll()) > 0;
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_labels` WHERE id = ?)';
|
||||
$stmt = $this->execute($sql, [$labelId]);
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return int|null
|
||||
*/
|
||||
public function findBoardId($id): ?int {
|
||||
try {
|
||||
$entity = $this->find($id);
|
||||
|
||||
@@ -63,7 +63,7 @@ class RelationalEntity extends Entity implements \JsonSerializable {
|
||||
* @return array serialized data
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function jsonSerialize(): array {
|
||||
public function jsonSerialize() {
|
||||
$properties = get_object_vars($this);
|
||||
$reflection = new \ReflectionClass($this);
|
||||
$json = [];
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class RelationalObject implements JsonSerializable {
|
||||
class RelationalObject implements \JsonSerializable {
|
||||
protected $primaryKey;
|
||||
protected $object;
|
||||
|
||||
@@ -40,7 +38,7 @@ class RelationalObject implements JsonSerializable {
|
||||
$this->object = $object;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
public function jsonSerialize() {
|
||||
return array_merge(
|
||||
['primaryKey' => $this->primaryKey],
|
||||
$this->getObjectSerialization()
|
||||
@@ -53,8 +51,8 @@ class RelationalObject implements JsonSerializable {
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getObjectSerialization() {
|
||||
if ($this->object instanceof JsonSerializable) {
|
||||
return $this->object->jsonSerialize();
|
||||
if ($this->object instanceof \JsonSerializable) {
|
||||
$this->object->jsonSerialize();
|
||||
} else {
|
||||
throw new \Exception('jsonSerialize is not implemented on ' . get_class($this));
|
||||
}
|
||||
|
||||
@@ -25,13 +25,6 @@ namespace OCA\Deck\Db;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
||||
/**
|
||||
* @method int getId()
|
||||
* @method int getBoardId()
|
||||
* @method int getDeletedAt()
|
||||
* @method int getLastModified()
|
||||
* @method int getOrder()
|
||||
*/
|
||||
class Stack extends RelationalEntity {
|
||||
protected $title;
|
||||
protected $boardId;
|
||||
@@ -52,7 +45,7 @@ class Stack extends RelationalEntity {
|
||||
$this->cards = $cards;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
public function jsonSerialize() {
|
||||
$json = parent::jsonSerialize();
|
||||
if (empty($this->cards)) {
|
||||
unset($json['cards']);
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace OCA\Deck\Db;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class StackMapper extends DeckMapper implements IPermissionMapper {
|
||||
@@ -39,112 +38,43 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
|
||||
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return Stack
|
||||
* @throws DoesNotExistException
|
||||
* @param $id
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws \OCP\DB\Exception
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function find($id): Stack {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` ' .
|
||||
'WHERE `id` = ?';
|
||||
return $this->findEntity($sql, [$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $cardId
|
||||
* @return Stack|null
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findStackFromCardId($cardId): ?Stack {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('s.*')
|
||||
->from($this->getTableName(), 's')
|
||||
->innerJoin('s', 'deck_cards', 'c', 's.id = c.stack_id')
|
||||
->where($qb->expr()->eq('c.id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
try {
|
||||
return $this->findEntity($qb);
|
||||
} catch (MultipleObjectsReturnedException|DoesNotExistException $e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` WHERE `board_id` = ? AND deleted_at = 0 ORDER BY `order`, `id`';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Stack[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($boardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Stack[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findDeleted($boardId, $limit = null, $offset = null) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->neq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` s
|
||||
WHERE `s`.`board_id` = ? AND NOT s.deleted_at = 0';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function delete(Entity $entity): Entity {
|
||||
|
||||
public function delete(Entity $entity) {
|
||||
// delete cards on stack
|
||||
$this->cardMapper->deleteByStack($entity->getId());
|
||||
return parent::delete($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $userId
|
||||
* @param numeric $stackId
|
||||
* @return bool
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function isOwner($userId, $stackId): bool {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('s.id')
|
||||
->from($this->getTableName(), 's')
|
||||
->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id')
|
||||
->where($qb->expr()->eq('s.id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return count($qb->executeQuery()->fetchAll()) > 0;
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id = ?)';
|
||||
$stmt = $this->execute($sql, [$stackId]);
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return int|null
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findBoardId($id): ?int {
|
||||
try {
|
||||
$entity = $this->find($id);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
*
|
||||
* @author Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Deck\Model;
|
||||
|
||||
use DateTime;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\Card;
|
||||
|
||||
class CardDetails extends Card {
|
||||
private Card $card;
|
||||
private ?Board $board;
|
||||
|
||||
public function __construct(Card $card, ?Board $board = null) {
|
||||
parent::__construct();
|
||||
$this->card = $card;
|
||||
$this->board = $board;
|
||||
}
|
||||
|
||||
public function setBoard(?Board $board): void {
|
||||
$this->board = $board;
|
||||
}
|
||||
|
||||
public function jsonSerialize(array $extras = []): array {
|
||||
$array = $this->card->jsonSerialize();
|
||||
unset($array['notified'], $array['descriptionPrev'], $array['relatedStack'], $array['relatedBoard']);
|
||||
|
||||
$array['overdue'] = $this->getDueStatus();
|
||||
$this->appendBoardDetails($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
private function getDueStatus(): int {
|
||||
$today = new DateTime();
|
||||
$today->setTime(0, 0);
|
||||
|
||||
$match_date = $this->card->getDueDateTime();
|
||||
if (!$match_date) {
|
||||
return Card::DUEDATE_FUTURE;
|
||||
}
|
||||
$match_date->setTime(0, 0);
|
||||
|
||||
$diff = $today->diff($match_date);
|
||||
$diffDays = (int) $diff->format('%R%a'); // Extract days count in interval
|
||||
|
||||
|
||||
if ($diffDays === 1) {
|
||||
return Card::DUEDATE_NEXT;
|
||||
}
|
||||
if ($diffDays === 0) {
|
||||
return Card::DUEDATE_NOW;
|
||||
}
|
||||
if ($diffDays < 0) {
|
||||
return Card::DUEDATE_OVERDUE;
|
||||
}
|
||||
|
||||
return Card::DUEDATE_FUTURE;
|
||||
}
|
||||
|
||||
private function appendBoardDetails(&$array): void {
|
||||
if (!$this->board) {
|
||||
return;
|
||||
}
|
||||
|
||||
$array['boardId'] = $this->board->id;
|
||||
$array['board'] = (new BoardSummary($this->board))->jsonSerialize();
|
||||
}
|
||||
|
||||
public function __call($name, $arguments) {
|
||||
return $this->card->__call($name, $arguments);
|
||||
}
|
||||
}
|
||||
@@ -101,12 +101,15 @@ class Notifier implements INotifier {
|
||||
switch ($notification->getSubject()) {
|
||||
case 'card-assigned':
|
||||
$cardId = $notification->getObjectId();
|
||||
$stack = $this->stackMapper->findStackFromCardId($cardId);
|
||||
$boardId = $stack ? $stack->getBoardId() : null;
|
||||
$boardId = $this->cardMapper->findBoardId($cardId);
|
||||
if (!$boardId) {
|
||||
throw new AlreadyProcessedException();
|
||||
}
|
||||
|
||||
$card = $this->cardMapper->find($cardId);
|
||||
$stackId = $card->getStackId();
|
||||
$stack = $this->stackMapper->find($stackId);
|
||||
|
||||
$initiator = $this->userManager->get($params[2]);
|
||||
if ($initiator !== null) {
|
||||
$dn = $initiator->getDisplayName();
|
||||
@@ -144,12 +147,15 @@ class Notifier implements INotifier {
|
||||
break;
|
||||
case 'card-overdue':
|
||||
$cardId = $notification->getObjectId();
|
||||
$stack = $this->stackMapper->findStackFromCardId($cardId);
|
||||
$boardId = $stack ? $stack->getBoardId() : null;
|
||||
$boardId = $this->cardMapper->findBoardId($cardId);
|
||||
if (!$boardId) {
|
||||
throw new AlreadyProcessedException();
|
||||
}
|
||||
|
||||
$card = $this->cardMapper->find($cardId);
|
||||
$stackId = $card->getStackId();
|
||||
$stack = $this->stackMapper->find($stackId);
|
||||
|
||||
$notification->setParsedSubject(
|
||||
(string) $l->t('The card "%s" on "%s" has reached its due date.', $params)
|
||||
);
|
||||
@@ -176,12 +182,15 @@ class Notifier implements INotifier {
|
||||
break;
|
||||
case 'card-comment-mentioned':
|
||||
$cardId = $notification->getObjectId();
|
||||
$stack = $this->stackMapper->findStackFromCardId($cardId);
|
||||
$boardId = $stack ? $stack->getBoardId() : null;
|
||||
$boardId = $this->cardMapper->findBoardId($cardId);
|
||||
if (!$boardId) {
|
||||
throw new AlreadyProcessedException();
|
||||
}
|
||||
|
||||
$card = $this->cardMapper->find($cardId);
|
||||
$stackId = $card->getStackId();
|
||||
$stack = $this->stackMapper->find($stackId);
|
||||
|
||||
$initiator = $this->userManager->get($params[2]);
|
||||
if ($initiator !== null) {
|
||||
$dn = $initiator->getDisplayName();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,11 @@ use OCA\Deck\Db\ChangeHelper;
|
||||
use OCA\Deck\InvalidAttachmentType;
|
||||
use OCA\Deck\NoPermissionException;
|
||||
use OCA\Deck\NotFoundException;
|
||||
use OCA\Deck\Cache\AttachmentCacheHelper;
|
||||
use OCA\Deck\StatusException;
|
||||
use OCP\AppFramework\Db\IMapperException;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IL10N;
|
||||
|
||||
class AttachmentService {
|
||||
@@ -48,10 +49,9 @@ class AttachmentService {
|
||||
|
||||
/** @var IAttachmentService[] */
|
||||
private $services = [];
|
||||
/** @var Application */
|
||||
private $application;
|
||||
/** @var AttachmentCacheHelper */
|
||||
private $attachmentCacheHelper;
|
||||
/** @var ICache */
|
||||
private $cache;
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
/** @var ActivityManager */
|
||||
@@ -59,13 +59,13 @@ class AttachmentService {
|
||||
/** @var ChangeHelper */
|
||||
private $changeHelper;
|
||||
|
||||
public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ChangeHelper $changeHelper, PermissionService $permissionService, Application $application, AttachmentCacheHelper $attachmentCacheHelper, $userId, IL10N $l10n, ActivityManager $activityManager) {
|
||||
public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ChangeHelper $changeHelper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId, IL10N $l10n, ActivityManager $activityManager) {
|
||||
$this->attachmentMapper = $attachmentMapper;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->userId = $userId;
|
||||
$this->application = $application;
|
||||
$this->attachmentCacheHelper = $attachmentCacheHelper;
|
||||
$this->cache = $cacheFactory->createDistributed('deck-card-attachments-');
|
||||
$this->l10n = $l10n;
|
||||
$this->activityManager = $activityManager;
|
||||
$this->changeHelper = $changeHelper;
|
||||
@@ -139,16 +139,14 @@ class AttachmentService {
|
||||
* @param $cardId
|
||||
* @return int|mixed
|
||||
* @throws BadRequestException
|
||||
* @throws InvalidAttachmentType
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function count($cardId) {
|
||||
if (is_numeric($cardId) === false) {
|
||||
throw new BadRequestException('card id must be a number');
|
||||
}
|
||||
|
||||
$count = $this->attachmentCacheHelper->getAttachmentCount((int)$cardId);
|
||||
if ($count === null) {
|
||||
$count = $this->cache->get('card-' . $cardId);
|
||||
if (!$count) {
|
||||
$count = count($this->attachmentMapper->findAll($cardId));
|
||||
|
||||
foreach (array_keys($this->services) as $attachmentType) {
|
||||
@@ -158,7 +156,7 @@ class AttachmentService {
|
||||
}
|
||||
}
|
||||
|
||||
$this->attachmentCacheHelper->setAttachmentCount((int)$cardId, $count);
|
||||
$this->cache->set('card-' . $cardId, $count);
|
||||
}
|
||||
|
||||
return $count;
|
||||
@@ -188,7 +186,7 @@ class AttachmentService {
|
||||
|
||||
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
|
||||
|
||||
$this->attachmentCacheHelper->clearAttachmentCount((int)$cardId);
|
||||
$this->cache->clear('card-' . $cardId);
|
||||
$attachment = new Attachment();
|
||||
$attachment->setCardId($cardId);
|
||||
$attachment->setType($type);
|
||||
@@ -300,7 +298,7 @@ class AttachmentService {
|
||||
}
|
||||
|
||||
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
||||
$this->attachmentCacheHelper->clearAttachmentCount($cardId);
|
||||
$this->cache->clear('card-' . $attachment->getCardId());
|
||||
|
||||
$attachment->setData($data);
|
||||
try {
|
||||
@@ -358,7 +356,7 @@ class AttachmentService {
|
||||
}
|
||||
}
|
||||
|
||||
$this->attachmentCacheHelper->clearAttachmentCount($cardId);
|
||||
$this->cache->clear('card-' . $attachment->getCardId());
|
||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
||||
return $attachment;
|
||||
@@ -372,7 +370,7 @@ class AttachmentService {
|
||||
}
|
||||
|
||||
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
||||
$this->attachmentCacheHelper->clearAttachmentCount($cardId);
|
||||
$this->cache->clear('card-' . $attachment->getCardId());
|
||||
|
||||
try {
|
||||
$service = $this->getService($attachment->getType());
|
||||
|
||||
@@ -127,9 +127,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,11 +187,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);
|
||||
@@ -625,9 +622,11 @@ class BoardService {
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
$delete = $this->aclMapper->delete($acl);
|
||||
|
||||
$this->eventDispatcher->dispatchTyped(new AclDeletedEvent($acl));
|
||||
return (bool) $this->aclMapper->delete($acl);
|
||||
|
||||
return $delete;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -114,7 +114,7 @@ class CardService {
|
||||
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
|
||||
$card->setCommentsUnread($countUnreadComments);
|
||||
$card->setCommentsCount($countComments);
|
||||
|
||||
|
||||
$stack = $this->stackMapper->find($card->getStackId());
|
||||
$board = $this->boardService->find($stack->getBoardId());
|
||||
$card->setRelatedStack($stack);
|
||||
@@ -224,7 +224,7 @@ class CardService {
|
||||
$card->setDescription($description);
|
||||
$card->setDuedate($duedate);
|
||||
$card = $this->cardMapper->insert($card);
|
||||
|
||||
|
||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE);
|
||||
$this->changeHelper->cardChanged($card->getId(), false);
|
||||
$this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card));
|
||||
@@ -253,7 +253,7 @@ class CardService {
|
||||
$card = $this->cardMapper->find($id);
|
||||
$card->setDeletedAt(time());
|
||||
$this->cardMapper->update($card);
|
||||
|
||||
|
||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
|
||||
$this->notificationHelper->markDuedateAsRead($card);
|
||||
$this->changeHelper->cardChanged($card->getId(), false);
|
||||
@@ -338,7 +338,7 @@ class CardService {
|
||||
$resetDuedateNotification = false;
|
||||
if (
|
||||
$card->getDuedate() === null ||
|
||||
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate() ?? ''))
|
||||
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate()))
|
||||
) {
|
||||
$card->setNotified(false);
|
||||
$resetDuedateNotification = true;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,10 +91,12 @@ class LabelService {
|
||||
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE);
|
||||
|
||||
$boardLabels = $this->labelMapper->findAll($boardId);
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
if (\is_array($boardLabels)) {
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +163,15 @@ class LabelService {
|
||||
$label = $this->find($id);
|
||||
|
||||
$boardLabels = $this->labelMapper->findAll($label->getBoardId());
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getId() === $label->getId()) {
|
||||
continue;
|
||||
}
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
if (\is_array($boardLabels)) {
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getId() === $label->getId()) {
|
||||
continue;
|
||||
}
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ namespace OCA\Deck\Service;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\IGroupManager;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
use OCP\IUserManager;
|
||||
@@ -96,42 +96,49 @@ class OverviewService {
|
||||
$userBoards = $this->findAllBoardsFromUser($userId);
|
||||
$allDueCards = [];
|
||||
foreach ($userBoards as $userBoard) {
|
||||
$allDueCards[] = array_map(function ($card) use ($userBoard, $userId) {
|
||||
$this->enrich($card, $userId);
|
||||
return (new CardDetails($card, $userBoard))->jsonSerialize();
|
||||
$service = $this;
|
||||
$allDueCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) {
|
||||
$service->enrich($card, $userId);
|
||||
$cardData = $card->jsonSerialize();
|
||||
$cardData['boardId'] = $userBoard->getId();
|
||||
return $cardData;
|
||||
}, $this->cardMapper->findAllWithDue($userBoard->getId()));
|
||||
}
|
||||
return array_merge(...$allDueCards);
|
||||
return $allDueCards;
|
||||
}
|
||||
|
||||
public function findUpcomingCards(string $userId): array {
|
||||
$userBoards = $this->findAllBoardsFromUser($userId);
|
||||
$foundCards = [];
|
||||
$findCards = [];
|
||||
foreach ($userBoards as $userBoard) {
|
||||
$service = $this;
|
||||
|
||||
if (count($userBoard->getAcl()) === 0) {
|
||||
// private board: get cards with due date
|
||||
$cards = $this->cardMapper->findAllWithDue($userBoard->getId());
|
||||
$findCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) {
|
||||
$service->enrich($card, $userId);
|
||||
$cardData = $card->jsonSerialize();
|
||||
$cardData['boardId'] = $userBoard->getId();
|
||||
return $cardData;
|
||||
}, $this->cardMapper->findAllWithDue($userBoard->getId()));
|
||||
} else {
|
||||
// shared board: get all my assigned or unassigned cards
|
||||
$cards = $this->cardMapper->findToMeOrNotAssignedCards($userBoard->getId(), $userId);
|
||||
$findCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) {
|
||||
$service->enrich($card, $userId);
|
||||
$cardData = $card->jsonSerialize();
|
||||
$cardData['boardId'] = $userBoard->getId();
|
||||
return $cardData;
|
||||
}, $this->cardMapper->findToMeOrNotAssignedCards($userBoard->getId(), $userId));
|
||||
}
|
||||
|
||||
$foundCards[] = array_map(
|
||||
function (Card $card) use ($userBoard, $userId) {
|
||||
$this->enrich($card, $userId);
|
||||
return (new CardDetails($card, $userBoard))->jsonSerialize();
|
||||
},
|
||||
$cards
|
||||
);
|
||||
}
|
||||
return array_merge(...$foundCards);
|
||||
return $findCards;
|
||||
}
|
||||
|
||||
// FIXME: This is duplicate code with the board service
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,13 +30,11 @@ use OCA\Deck\BadRequestException;
|
||||
use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\ChangeHelper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
use OCA\Deck\Db\Stack;
|
||||
use OCA\Deck\Db\StackMapper;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\NoPermissionException;
|
||||
use OCA\Deck\StatusException;
|
||||
|
||||
@@ -86,13 +84,9 @@ class StackService {
|
||||
return;
|
||||
}
|
||||
|
||||
$cards = array_map(
|
||||
function (Card $card): CardDetails {
|
||||
$this->cardService->enrich($card);
|
||||
return new CardDetails($card);
|
||||
},
|
||||
$cards
|
||||
);
|
||||
foreach ($cards as $card) {
|
||||
$this->cardService->enrich($card);
|
||||
}
|
||||
|
||||
$stack->setCards($cards);
|
||||
}
|
||||
@@ -118,18 +112,12 @@ class StackService {
|
||||
|
||||
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_READ);
|
||||
$stack = $this->stackMapper->find($stackId);
|
||||
|
||||
$cards = array_map(
|
||||
function (Card $card): CardDetails {
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$card->setAssignedUsers($assignedUsers);
|
||||
$card->setAttachmentCount($this->attachmentService->count($card->getId()));
|
||||
|
||||
return new CardDetails($card);
|
||||
},
|
||||
$this->cardMapper->findAll($stackId)
|
||||
);
|
||||
|
||||
$cards = $this->cardMapper->findAll($stackId);
|
||||
foreach ($cards as $cardIndex => $card) {
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$card->setAssignedUsers($assignedUsers);
|
||||
$card->setAttachmentCount($this->attachmentService->count($card->getId()));
|
||||
}
|
||||
$stack->setCards($cards);
|
||||
|
||||
return $stack;
|
||||
|
||||
@@ -26,8 +26,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\Deck\Sharing;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use OC\Files\Cache\Cache;
|
||||
use OCA\Deck\Cache\AttachmentCacheHelper;
|
||||
use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
@@ -46,6 +46,7 @@ use OCP\Files\IMimeTypeLoader;
|
||||
use OCP\Files\Node;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IL10N;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use OCP\Share\Exceptions\GenericShareException;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use OCP\Share\IManager;
|
||||
@@ -69,8 +70,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
private $dbConnection;
|
||||
/** @var IManager */
|
||||
private $shareManager;
|
||||
/** @var AttachmentCacheHelper */
|
||||
private $attachmentCacheHelper;
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
/** @var CardMapper */
|
||||
@@ -79,25 +78,14 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
private $permissionService;
|
||||
/** @var ITimeFactory */
|
||||
private $timeFactory;
|
||||
/** @var IL10N */
|
||||
private $l;
|
||||
|
||||
public function __construct(
|
||||
IDBConnection $connection,
|
||||
IManager $shareManager,
|
||||
BoardMapper $boardMapper,
|
||||
CardMapper $cardMapper,
|
||||
PermissionService $permissionService,
|
||||
AttachmentCacheHelper $attachmentCacheHelper,
|
||||
IL10N $l
|
||||
) {
|
||||
public function __construct(IDBConnection $connection, IManager $shareManager, ISecureRandom $secureRandom, BoardMapper $boardMapper, CardMapper $cardMapper, PermissionService $permissionService, IL10N $l) {
|
||||
$this->dbConnection = $connection;
|
||||
$this->shareManager = $shareManager;
|
||||
$this->boardMapper = $boardMapper;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->attachmentCacheHelper = $attachmentCacheHelper;
|
||||
$this->permissionService = $permissionService;
|
||||
|
||||
$this->l = $l;
|
||||
$this->timeFactory = \OC::$server->get(ITimeFactory::class);
|
||||
}
|
||||
@@ -165,8 +153,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
);
|
||||
$data = $this->getRawShare($shareId);
|
||||
|
||||
$this->attachmentCacheHelper->clearAttachmentCount((int)$cardId);
|
||||
|
||||
return $this->createShareObject($data);
|
||||
}
|
||||
|
||||
@@ -354,8 +340,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$qb->orWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())));
|
||||
|
||||
$qb->execute();
|
||||
|
||||
$this->attachmentCacheHelper->clearAttachmentCount((int)$share->getSharedWith());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,7 +501,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
);
|
||||
}
|
||||
|
||||
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
|
||||
$qb->orderBy('s.id');
|
||||
@@ -727,6 +711,13 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
}
|
||||
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
// Avoid using implicit cast in order to make use of the index in the join on MySQL/MariaDB
|
||||
// FIXME: Once >= Nextcloud 24 this can be dropped due to https://github.com/nextcloud/server/pull/30471
|
||||
if ($this->dbConnection->getDatabasePlatform() instanceof MySQLPlatform) {
|
||||
$cardIdExpression = $qb->createFunction('CAST(dc.id as CHAR)');
|
||||
} else {
|
||||
$cardIdExpression = $qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR);
|
||||
}
|
||||
$qb->select('s.*',
|
||||
'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
|
||||
'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
|
||||
@@ -737,7 +728,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
->orderBy('s.id')
|
||||
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
|
||||
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
|
||||
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR), 's.share_with'))
|
||||
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($cardIdExpression, 's.share_with'))
|
||||
->leftJoin('dc', 'deck_stacks', 'ds', $qb->expr()->eq('dc.stack_id', 'ds.id'))
|
||||
->leftJoin('ds', 'deck_boards', 'db', $qb->expr()->eq('ds.board_id', 'db.id'));
|
||||
|
||||
@@ -798,6 +789,13 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$shares = [];
|
||||
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
// Avoid using implicit cast in order to make use of the index in the join on MySQL/MariaDB
|
||||
// FIXME: Once >= Nextcloud 24 this can be dropped due to https://github.com/nextcloud/server/pull/30471
|
||||
if ($this->dbConnection->getDatabasePlatform() instanceof MySQLPlatform) {
|
||||
$cardIdExpression = $qb->createFunction('CAST(dc.id as CHAR)');
|
||||
} else {
|
||||
$cardIdExpression = $qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR);
|
||||
}
|
||||
$qb->select('s.*',
|
||||
'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
|
||||
'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
|
||||
@@ -808,7 +806,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
->orderBy('s.id')
|
||||
->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
|
||||
->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
|
||||
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($qb->expr()->castColumn('dc.id', IQueryBuilder::PARAM_STR), 's.share_with'));
|
||||
->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq($cardIdExpression, 's.share_with'));
|
||||
|
||||
if ($limit !== -1) {
|
||||
$qb->setMaxResults($limit);
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace OCA\Deck;
|
||||
*/
|
||||
class StatusException extends \Exception {
|
||||
public function __construct($message) {
|
||||
parent::__construct($message ?? '');
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
public function getStatus() {
|
||||
|
||||