Compare commits
129 Commits
techdept/m
...
v1.5.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -3,10 +3,6 @@ module.exports = {
|
||||
'@nextcloud',
|
||||
],
|
||||
rules: {
|
||||
'jsdoc/require-param-description': ['off'],
|
||||
'jsdoc/require-param-type': ['off'],
|
||||
'jsdoc/check-param-names': ['off'],
|
||||
'jsdoc/no-undefined-types': ['off'],
|
||||
'jsdoc/require-property-description' : ['off']
|
||||
'valid-jsdoc': ['off'],
|
||||
},
|
||||
}
|
||||
|
||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -39,13 +39,3 @@ updates:
|
||||
versions:
|
||||
- "< 16"
|
||||
- ">= 15.a"
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "03:00"
|
||||
timezone: Europe/Paris
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- juliushaertl
|
||||
|
||||
25
.github/stale.yml
vendored
Normal file
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.
|
||||
6
.github/workflows/appbuild.yml
vendored
6
.github/workflows/appbuild.yml
vendored
@@ -12,15 +12,15 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.5.1
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: composer
|
||||
|
||||
151
.github/workflows/appstore-build-publish.yml
vendored
Normal file
151
.github/workflows/appstore-build-publish.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Build and publish app release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
PHP_VERSION: 7.4
|
||||
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Only allowed to be run on nextcloud-releases repositories
|
||||
if: ${{ github.repository_owner == 'nextcloud-releases' }}
|
||||
|
||||
steps:
|
||||
- name: Check actor permission
|
||||
uses: skjnldsv/check-actor-permission@v2
|
||||
with:
|
||||
require: write
|
||||
|
||||
- name: Set app env
|
||||
run: |
|
||||
# Split and keep last
|
||||
echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
|
||||
echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ env.APP_NAME }}
|
||||
|
||||
- name: Get appinfo data
|
||||
id: appinfo
|
||||
uses: skjnldsv/xpath-action@master
|
||||
with:
|
||||
filename: ${{ env.APP_NAME }}/appinfo/info.xml
|
||||
expression: "//info//dependencies//nextcloud/@min-version"
|
||||
|
||||
- name: Read package.json node and npm engines version
|
||||
uses: skjnldsv/read-package-engines-version-actions@v1.2
|
||||
id: versions
|
||||
# Continue if no package.json
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ env.APP_NAME }}
|
||||
fallbackNode: "^12"
|
||||
fallbackNpm: "^6"
|
||||
|
||||
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
|
||||
# Skip if no package.json
|
||||
if: ${{ steps.versions.outputs.nodeVersion }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ steps.versions.outputs.nodeVersion }}
|
||||
|
||||
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
|
||||
# Skip if no package.json
|
||||
if: ${{ steps.versions.outputs.npmVersion }}
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Set up php ${{ env.PHP_VERSION }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ env.PHP_VERSION }}
|
||||
coverage: none
|
||||
|
||||
- name: Check composer.json
|
||||
id: check_composer
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "${{ env.APP_NAME }}/composer.json"
|
||||
|
||||
- name: Install composer dependencies
|
||||
if: steps.check_composer.outputs.files_exists == 'true'
|
||||
run: |
|
||||
cd ${{ env.APP_NAME }}
|
||||
composer install --no-dev
|
||||
|
||||
- name: Build ${{ env.APP_NAME }}
|
||||
# Skip if no package.json
|
||||
if: ${{ steps.versions.outputs.nodeVersion }}
|
||||
run: |
|
||||
cd ${{ env.APP_NAME }}
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Install Krankerl
|
||||
run: |
|
||||
wget https://github.com/ChristophWurst/krankerl/releases/download/v0.13.0/krankerl_0.13.0_amd64.deb
|
||||
sudo dpkg -i krankerl_0.13.0_amd64.deb
|
||||
|
||||
- name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }}
|
||||
# Try krankerl, fallback to makefile
|
||||
run: |
|
||||
cd ${{ env.APP_NAME }}
|
||||
krankerl package || make appstore
|
||||
|
||||
- name: Checkout server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
|
||||
continue-on-error: true
|
||||
id: server-checkout
|
||||
run: |
|
||||
NCVERSION=${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}
|
||||
wget --quiet https://download.nextcloud.com/server/releases/latest-$NCVERSION.zip
|
||||
unzip latest-$NCVERSION.zip
|
||||
|
||||
- name: Checkout server master fallback
|
||||
uses: actions/checkout@v3
|
||||
if: ${{ steps.server-checkout.outcome != 'success' }}
|
||||
with:
|
||||
repository: nextcloud/server
|
||||
path: nextcloud
|
||||
|
||||
- name: Sign app
|
||||
run: |
|
||||
# Extracting release
|
||||
cd ${{ env.APP_NAME }}/build/artifacts
|
||||
tar -xvf ${{ env.APP_NAME }}.tar.gz
|
||||
cd ../../../
|
||||
# Setting up keys
|
||||
echo "${{ secrets.APP_PRIVATE_KEY }}" > ${{ env.APP_NAME }}.key
|
||||
wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt"
|
||||
# Signing
|
||||
php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}
|
||||
# Rebuilding archive
|
||||
cd ${{ env.APP_NAME }}/build/artifacts
|
||||
tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }}
|
||||
|
||||
- name: Attach tarball to github release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
id: attach_to_release
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz
|
||||
asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Upload app to Nextcloud appstore
|
||||
uses: nextcloud-releases/nextcloud-appstore-push-action@v1
|
||||
with:
|
||||
app_name: ${{ env.APP_NAME }}
|
||||
appstore_token: ${{ secrets.APPSTORE_TOKEN }}
|
||||
download_url: ${{ steps.attach_to_release.outputs.browser_download_url }}
|
||||
app_private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
46
.github/workflows/command-rebase.yml
vendored
46
.github/workflows/command-rebase.yml
vendored
@@ -1,46 +0,0 @@
|
||||
# This workflow is provided via the organization template repository
|
||||
#
|
||||
# https://github.com/nextcloud/.github
|
||||
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
|
||||
|
||||
name: Rebase command
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: created
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# On pull requests and if the comment starts with `/rebase`
|
||||
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase')
|
||||
|
||||
steps:
|
||||
- name: Add reaction on start
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
repository: ${{ github.event.repository.full_name }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reaction-type: "+1"
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Add reaction on failure
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
if: failure()
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
repository: ${{ github.event.repository.full_name }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reaction-type: "-1"
|
||||
29
.github/workflows/dependabot-approve-merge.yml
vendored
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
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 }}
|
||||
8
.github/workflows/integration.yml
vendored
8
.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@v2.4.0
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: nextcloud/server
|
||||
ref: ${{ matrix.server-versions }}
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
|
||||
- name: Checkout app
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpunit
|
||||
|
||||
26
.github/workflows/lint.yml
vendored
26
.github/workflows/lint.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0']
|
||||
php-versions: ['7.2', '7.3', '7.4']
|
||||
|
||||
name: php${{ matrix.php-versions }} lint
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up php${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
coverage: none
|
||||
@@ -31,9 +31,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@master
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: 7.4
|
||||
coverage: none
|
||||
@@ -50,9 +50,9 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.5.1
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
@@ -67,16 +67,16 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-versions: [14.x]
|
||||
|
||||
name: stylelint node${{ matrix.node-version }}
|
||||
name: stylelint node${{ matrix.node-versions }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.5.1
|
||||
- name: Set up node ${{ matrix.node-versions }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-versions: ${{ matrix.node-versions }}
|
||||
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
|
||||
6
.github/workflows/nightly.yml
vendored
6
.github/workflows/nightly.yml
vendored
@@ -17,15 +17,15 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.5.1
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@v1
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: composer
|
||||
|
||||
4
.github/workflows/nodejs.yml
vendored
4
.github/workflows/nodejs.yml
vendored
@@ -12,9 +12,9 @@ jobs:
|
||||
node-version: [14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.5.1
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
|
||||
10
.github/workflows/phpunit.yml
vendored
10
.github/workflows/phpunit.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0']
|
||||
php-versions: ['7.3', '7.4']
|
||||
databases: ['sqlite', 'mysql', 'pgsql']
|
||||
server-versions: ['master']
|
||||
server-versions: ['stable22']
|
||||
|
||||
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: nextcloud/server
|
||||
ref: ${{ matrix.server-versions }}
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
|
||||
- name: Checkout app
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpunit
|
||||
|
||||
6
.github/workflows/static-analysis.yml
vendored
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@v2.4.0
|
||||
uses: actions/checkout@master
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@2.17.0
|
||||
uses: shivammathur/setup-php@master
|
||||
with:
|
||||
php-version: 7.4
|
||||
tools: composer:v1
|
||||
|
||||
143
CHANGELOG.md
143
CHANGELOG.md
@@ -1,38 +1,91 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 1.6.0-beta1
|
||||
|
||||
### Added
|
||||
|
||||
- #3177 Use async import for vue component on collections entrypoint @juliushaertl
|
||||
- #2791 Open description links in new tab @fm-sys
|
||||
- #3344 Improve combined search @eneiluj
|
||||
- #3362 Improve search performance @eneiluj
|
||||
- #2710 Due date shortcuts in the datepicker @jakobroehrl
|
||||
## 1.5.6
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3161 Reduce duplicate queries when fetching user boards an permissions @juliushaertl
|
||||
- #3151 Always log generic exceptions @juliushaertl
|
||||
- #3217 Move circle checks to a unified service and improve member checks @juliushaertl
|
||||
- #3225 Check for null value to avoid TypeError in the group manager @juliushaertl
|
||||
- #3263 Defer obtaining the user session in the config service @juliushaertl
|
||||
- #3294 Fix print style issues @weeman1337
|
||||
- #3299 Return false instead of throwing when getting calendar setting @juliushaertl
|
||||
- #3298 Delete file shares through attachments API @juliushaertl
|
||||
- #3343 Fix search pagination cursor @eneiluj
|
||||
- #3326 add autofocus on board edit @weeman1337
|
||||
- #3323 Extend drag-and-drop zone in card sidebar @old-green-frog
|
||||
- #3364 Fix optional parameter order @juliushaertl
|
||||
- #3324 Fix menu button position in card modal @valerydmitrieva
|
||||
- #3391 Use displayname instead of uid for mentions (reopened against master) @kffl
|
||||
- #3316 Additional check for stacks @juliushaertl
|
||||
- #3357 Revert "Fix search pagination cursor" @juliushaertl
|
||||
- #3327 Do not show both bullets and checkboxes for checklists @Themanwhosmellslikesugar
|
||||
- #3375 Show absolute dates when printing @weeman1337
|
||||
- #3376 Print assignee names @weeman1337
|
||||
- #3384 Keep exceptions http response generic @juliushaertl
|
||||
- Allow to download an attachment without navigating to the files app [#3441](https://api.github.com/repos/nextcloud/deck/pulls/3441)
|
||||
- Fix CalDAV blocking and modernize circles API usage [#3527](https://api.github.com/repos/nextcloud/deck/pulls/3527)
|
||||
- CardApiController: Fix order of optional parameters [#3521](https://api.github.com/repos/nextcloud/deck/pulls/3521)
|
||||
- Fix cursor generation if no results are found [#3460](https://api.github.com/repos/nextcloud/deck/pulls/3460)
|
||||
- Exclude deleted boards in the selection for target [#3524](https://api.github.com/repos/nextcloud/deck/pulls/3524)
|
||||
- Generate fixed link for activity emails [#3627](https://api.github.com/repos/nextcloud/deck/pulls/3627)
|
||||
- Make insert attachment buttom easy to click [#3615](https://api.github.com/repos/nextcloud/deck/pulls/3615)
|
||||
- Fix confusion between stackId and boardId in StackService [#3544](https://api.github.com/repos/nextcloud/deck/pulls/3544)
|
||||
|
||||
## 1.5.5
|
||||
|
||||
- Fix release asset build
|
||||
|
||||
## 1.5.4
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3378 Fix menu button position in card modal
|
||||
- #3392 Use displayname instead of uid for mentions (reopened against master)
|
||||
- #3361 Improve combined search @eneiluj
|
||||
- #3381 Extend drag-and-drop zone in card sidebar @Artem4590
|
||||
- #3366 Fix optional parameter order
|
||||
- #3407 Keep exceptions http response generic
|
||||
|
||||
|
||||
## 1.5.3
|
||||
|
||||
### Fied
|
||||
|
||||
- #3317 Additional check for stacks
|
||||
|
||||
|
||||
## 1.5.2
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3300 Fix print style issues
|
||||
- #3303 Delete file shares through attachments API
|
||||
- #3306 Return false instead of throwing when getting calendar setting
|
||||
|
||||
## 1.5.1 - 2021-09-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3224 Move circle checks to a unified service and improve member checks
|
||||
- #3231 Check for null value to avoid TypeError in the group manager
|
||||
- #3264 Defer obtaining the user session in the config service
|
||||
|
||||
|
||||
## 1.5.0 - 2021-07-09
|
||||
|
||||
### Added
|
||||
|
||||
* Nextcloud 22 compatibility
|
||||
* [#3105](https://github.com/nextcloud/deck/pull/3105) Compatibility with Cirlces changes in 22
|
||||
* [#3147](https://github.com/nextcloud/deck/pull/3147) Add card button to the dashboard widget @jakobroehrl
|
||||
* [#2854](https://github.com/nextcloud/deck/pull/2854) Add card button in card overview @jakobroehrl
|
||||
* [#3078](https://github.com/nextcloud/deck/pull/3078) Show on shared boards unassigned cards to all users @jakobroehrl
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#2935](https://github.com/nextcloud/deck/pull/2935) Rich object string parameters for notifications @nickvergessen
|
||||
* [#2950](https://github.com/nextcloud/deck/pull/2950) Remove notification on unshare and add type hints
|
||||
* [#2983](https://github.com/nextcloud/deck/pull/2983) Fix codemirror description width
|
||||
* [#2989](https://github.com/nextcloud/deck/pull/2989) Fix unified comments search with postgres
|
||||
* [#3005](https://github.com/nextcloud/deck/pull/3005) Do not query the lookupserver when looking for sharees
|
||||
* [#3011](https://github.com/nextcloud/deck/pull/3011) L10n: Spelling unification @Valdnet
|
||||
* [#3014](https://github.com/nextcloud/deck/pull/3014) Proper error handling when fetching comments fails
|
||||
* [#3016](https://github.com/nextcloud/deck/pull/3016) Allow searching for filters without a query to match all that have a given filter set
|
||||
* [#3021](https://github.com/nextcloud/deck/pull/3021) L10n: Add word "Card" @Valdnet
|
||||
* [#3025](https://github.com/nextcloud/deck/pull/3025) Show comment counter and highlight if unread comments are available
|
||||
* [#3036](https://github.com/nextcloud/deck/pull/3036) Add link to migration tool for Trello @maxammann
|
||||
* [#3037](https://github.com/nextcloud/deck/pull/3037) Catch any error during circle detail fetching
|
||||
* [#3038](https://github.com/nextcloud/deck/pull/3038) Get attachment from the user node instead of the share source
|
||||
* [#3092](https://github.com/nextcloud/deck/pull/3092) Refactor update to have proper order of optional parameters
|
||||
* [#3113](https://github.com/nextcloud/deck/pull/3113) Use new viewer syntax with destructuring object @azul
|
||||
* [#3142](https://github.com/nextcloud/deck/pull/3142) Always pass user id in share provider
|
||||
* [#3152](https://github.com/nextcloud/deck/pull/3152) Only offer stack creation in emptycontent with proper permissions
|
||||
* [#3165](https://github.com/nextcloud/deck/pull/3165) Always log generic exceptions
|
||||
* [#3168](https://github.com/nextcloud/deck/pull/3168) Reduce duplicate queries when fetching user boards an permissions
|
||||
|
||||
|
||||
|
||||
## 1.4.0 - 2021-04-13
|
||||
@@ -69,15 +122,15 @@ All notable changes to this project will be documented in this file.
|
||||
## 1.3.0-beta2
|
||||
|
||||
### Fixed
|
||||
* [#2700](https://github.com/nextcloud/deck/pull/2700) Attempt to copy file on dropping it to deck @juliushaertl
|
||||
* [#2701](https://github.com/nextcloud/deck/pull/2701) Fix uploading files by drag and drop @juliushaertl
|
||||
* [#2700](https://github.com/nextcloud/deck/pull/2700) Attempt to copy file on dropping it to deck
|
||||
* [#2701](https://github.com/nextcloud/deck/pull/2701) Fix uploading files by drag and drop
|
||||
* [#2707](https://github.com/nextcloud/deck/pull/2707) L10n: Change to a capital letter @Valdnet
|
||||
* [#2712](https://github.com/nextcloud/deck/pull/2712) Docs: Fix table in section "GET /api/v1.0/config" @das-g
|
||||
* [#2716](https://github.com/nextcloud/deck/pull/2716) Remove repair step which is no longer needed as we cleanup properly @juliushaertl
|
||||
* [#2716](https://github.com/nextcloud/deck/pull/2716) Remove repair step which is no longer needed as we cleanup properly
|
||||
* [#2723](https://github.com/nextcloud/deck/pull/2723) Pad random color with leading zeroes @PVince81
|
||||
* [#2729](https://github.com/nextcloud/deck/pull/2729) Remove invalid activity parameters @nickvergessen
|
||||
* [#2750](https://github.com/nextcloud/deck/pull/2750) Fix deck activity emails not being translated @nickvergessen
|
||||
* [#2751](https://github.com/nextcloud/deck/pull/2751) Properly set author for activity events that are triggered by cron @juliushaertl
|
||||
* [#2751](https://github.com/nextcloud/deck/pull/2751) Properly set author for activity events that are triggered by cron
|
||||
|
||||
|
||||
## 1.2.2 - 2020-11-24
|
||||
@@ -186,31 +239,31 @@ All notable changes to this project will be documented in this file.
|
||||
### Fixed
|
||||
|
||||
|
||||
* [#2116](https://github.com/nextcloud/deck/pull/2116) Fix navigation layout issues @juliushaertl
|
||||
* [#2118](https://github.com/nextcloud/deck/pull/2118) Use proper parameter when handling attachments @juliushaertl
|
||||
* [#2116](https://github.com/nextcloud/deck/pull/2116) Fix navigation layout issues
|
||||
* [#2118](https://github.com/nextcloud/deck/pull/2118) Use proper parameter when handling attachments
|
||||
|
||||
## 1.0.4 - 2020-06-26
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#2062](https://github.com/nextcloud/deck/pull/2062) Fix saving card description after toggling checkboxes @juliushaertl
|
||||
* [#2062](https://github.com/nextcloud/deck/pull/2062) Fix saving card description after toggling checkboxes
|
||||
* [#2065](https://github.com/nextcloud/deck/pull/2065) Adding CSS rule for Markdown Blockquotes @reox
|
||||
* [#2059](https://github.com/nextcloud/deck/pull/2059) Fix fetching attachments on card change @juliushaertl
|
||||
* [#2060](https://github.com/nextcloud/deck/pull/2060) Use mixing for relative date in card sidebar @juliushaertl
|
||||
* [#2059](https://github.com/nextcloud/deck/pull/2059) Fix fetching attachments on card change
|
||||
* [#2060](https://github.com/nextcloud/deck/pull/2060) Use mixing for relative date in card sidebar
|
||||
|
||||
|
||||
## 1.0.3 - 2020-06-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#2019](https://github.com/nextcloud/deck/pull/2019) Remove old global css rule @juliushaertl
|
||||
* [#2020](https://github.com/nextcloud/deck/pull/2020) Fix navigation issue with leftover nodes @juliushaertl
|
||||
* [#2021](https://github.com/nextcloud/deck/pull/2021) Fix description issues @juliushaertl
|
||||
* [#2022](https://github.com/nextcloud/deck/pull/2022) Fix replyto issues with the comments API @juliushaertl
|
||||
* [#2027](https://github.com/nextcloud/deck/pull/2027) Allow to unassign current user from card @juliushaertl
|
||||
* [#2019](https://github.com/nextcloud/deck/pull/2019) Remove old global css rule
|
||||
* [#2020](https://github.com/nextcloud/deck/pull/2020) Fix navigation issue with leftover nodes
|
||||
* [#2021](https://github.com/nextcloud/deck/pull/2021) Fix description issues
|
||||
* [#2022](https://github.com/nextcloud/deck/pull/2022) Fix replyto issues with the comments API
|
||||
* [#2027](https://github.com/nextcloud/deck/pull/2027) Allow to unassign current user from card
|
||||
* [#2029](https://github.com/nextcloud/deck/pull/2029) Fix wording : stack -> list @cloud2018
|
||||
* [#2032](https://github.com/nextcloud/deck/pull/2032) Force order by id as second sorting key @juliushaertl
|
||||
* [#2045](https://github.com/nextcloud/deck/pull/2045) Improve label styling @juliushaertl
|
||||
* [#2032](https://github.com/nextcloud/deck/pull/2032) Force order by id as second sorting key
|
||||
* [#2045](https://github.com/nextcloud/deck/pull/2045) Improve label styling
|
||||
* [#2010](https://github.com/nextcloud/deck/pull/2010) User documentation fixes @Nyco
|
||||
* [#1998](https://github.com/nextcloud/deck/pull/1998) Add Checklist explaination to the doc @4rnoP
|
||||
|
||||
|
||||
@@ -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.7.0-alpha1</version>
|
||||
<version>1.5.6</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Julius Härtl</author>
|
||||
<namespace>Deck</namespace>
|
||||
@@ -31,10 +31,11 @@
|
||||
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
|
||||
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
|
||||
<dependencies>
|
||||
<php min-version="7.3"/>
|
||||
<database min-version="9.4">pgsql</database>
|
||||
<database>sqlite</database>
|
||||
<database min-version="8.0">mysql</database>
|
||||
<nextcloud min-version="24" max-version="24"/>
|
||||
<database min-version="5.5">mysql</database>
|
||||
<nextcloud min-version="22" max-version="22"/>
|
||||
</dependencies>
|
||||
<background-jobs>
|
||||
<job>OCA\Deck\Cron\DeleteCron</job>
|
||||
@@ -43,7 +44,6 @@
|
||||
</background-jobs>
|
||||
<commands>
|
||||
<command>OCA\Deck\Command\UserExport</command>
|
||||
<command>OCA\Deck\Command\BoardImport</command>
|
||||
</commands>
|
||||
<activity>
|
||||
<settings>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
['name' => 'page#redirectToCard', 'url' => '/card/{cardId}', 'verb' => 'GET'],
|
||||
|
||||
// boards
|
||||
['name' => 'board#index', 'url' => '/boards', 'verb' => 'GET'],
|
||||
@@ -90,10 +91,6 @@ return [
|
||||
['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
|
||||
['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
|
||||
|
||||
['name' => 'board_import_api#getAllowedSystems', 'url' => '/api/v{apiVersion}/boards/import/getSystems','verb' => 'GET'],
|
||||
['name' => 'board_import_api#getConfigSchema', 'url' => '/api/v{apiVersion}/boards/import/config/schema/{name}','verb' => 'GET'],
|
||||
['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST'],
|
||||
|
||||
|
||||
['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'],
|
||||
['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'],
|
||||
|
||||
@@ -9,27 +9,20 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"cogpowered/finediff": "0.3.*",
|
||||
"justinrainbow/json-schema": "^5.2"
|
||||
"cogpowered/finediff": "0.3.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-master",
|
||||
"christophwurst/nextcloud": "^21@dev",
|
||||
"phpunit/phpunit": "^9",
|
||||
"nextcloud/coding-standard": "^1.0.0",
|
||||
"nextcloud/coding-standard": "^0.5.0",
|
||||
"symfony/event-dispatcher": "^4.0",
|
||||
"vimeo/psalm": "^4.3",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"classmap-authoritative": true,
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true
|
||||
},
|
||||
"platform": {
|
||||
"php": "7.4"
|
||||
}
|
||||
"classmap-authoritative": true
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "find . -name \\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
|
||||
|
||||
1469
composer.lock
generated
1469
composer.lock
generated
File diff suppressed because it is too large
Load Diff
68
docs/API.md
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,8 +14,6 @@ Overall, Deck is easy to use. You can create boards, add users, share the Deck,
|
||||
3. [Handle cards options](#3-handle-cards-options)
|
||||
4. [Archive old tasks](#4-archive-old-tasks)
|
||||
5. [Manage your board](#5-manage-your-board)
|
||||
6. [Import boards](#6-import-boards)
|
||||
7. [Search](#7-search)
|
||||
|
||||
### 1. Create my first board
|
||||
In this example, we're going to create a board and share it with an other nextcloud user.
|
||||
@@ -71,80 +69,14 @@ The **sharing tab** allows you to add users or even groups to your boards.
|
||||
**Deleted objects** allows you to return previously deleted stacks or cards.
|
||||
The **Timeline** allows you to see everything that happened in your boards. Everything!
|
||||
|
||||
### 6. Import boards
|
||||
|
||||
Importing can be done using the API or the `occ` `deck:import` command.
|
||||
|
||||
Comments with more than 1000 characters are placed as attached files to the card.
|
||||
|
||||
It is possible to import from the following sources:
|
||||
|
||||
#### Trello JSON
|
||||
|
||||
Steps:
|
||||
* Create the data file
|
||||
* Access Trello
|
||||
* go to the board you want to export
|
||||
* Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON
|
||||
* Create the configuration file
|
||||
* Execute the import informing the import file path, data file and source as `Trello JSON`
|
||||
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON`
|
||||
|
||||
Example configuration file:
|
||||
```json
|
||||
{
|
||||
"owner": "admin",
|
||||
"color": "0800fd",
|
||||
"uidRelation": {
|
||||
"johndoe": "johndoe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Limitations**:
|
||||
|
||||
Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has.
|
||||
|
||||
#### Trello API
|
||||
|
||||
Import using API is recommended for boards with more than 1000 actions.
|
||||
|
||||
Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello.
|
||||
|
||||
* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization)
|
||||
* Get the ID of the board you want to import by making a request to:
|
||||
https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name
|
||||
|
||||
This ID you will use in the configuration file in the `board` property
|
||||
* Create the configuration file
|
||||
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON`
|
||||
|
||||
Example configuration file:
|
||||
```json
|
||||
{
|
||||
"owner": "admin",
|
||||
"color": "0800fd",
|
||||
"api": {
|
||||
"key": "0cc175b9c0f1b6a831c399e269772661",
|
||||
"token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33"
|
||||
},
|
||||
"board": "8277e0910d750195b4487976",
|
||||
"uidRelation": {
|
||||
"johndoe": "johndoe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Search
|
||||
## Search
|
||||
|
||||
Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls.
|
||||
This search allows advanced filtering of cards across all board of the logged in user.
|
||||
|
||||
For example the search `project tag:ToDo assigned:alice assigned:bob` will return all cards where the card title or description contains project **and** the tag ToDo is set **and** the user alice is assigned **and** the user bob is assigned.
|
||||
|
||||
#### Supported search filters
|
||||
### Supported search filters
|
||||
|
||||
| Filter | Operators | Query |
|
||||
| ----------- | ----------------- | ------------------------------------------------------------ |
|
||||
|
||||
@@ -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]
|
||||
@@ -35,6 +35,7 @@ use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserManager;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCA\Deck\Service\CardService;
|
||||
|
||||
class DeckProvider implements IProvider {
|
||||
|
||||
@@ -52,8 +53,10 @@ class DeckProvider implements IProvider {
|
||||
private $l10nFactory;
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
/** @var CardService */
|
||||
private $cardService;
|
||||
|
||||
public function __construct(IURLGenerator $urlGenerator, ActivityManager $activityManager, IUserManager $userManager, ICommentsManager $commentsManager, IFactory $l10n, IConfig $config, $userId) {
|
||||
public function __construct(IURLGenerator $urlGenerator, ActivityManager $activityManager, IUserManager $userManager, ICommentsManager $commentsManager, IFactory $l10n, IConfig $config, $userId, CardService $cardService) {
|
||||
$this->userId = $userId;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->activityManager = $activityManager;
|
||||
@@ -61,6 +64,7 @@ class DeckProvider implements IProvider {
|
||||
$this->userManager = $userManager;
|
||||
$this->l10nFactory = $l10n;
|
||||
$this->config = $config;
|
||||
$this->cardService = $cardService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +135,7 @@ class DeckProvider implements IProvider {
|
||||
|
||||
if (array_key_exists('board', $subjectParams)) {
|
||||
$archivedParam = $subjectParams['card']['archived'] ? 'archived/' : '';
|
||||
$card['link'] = $this->deckUrl('/board/' . $subjectParams['board']['id'] . '/' . $archivedParam . 'card/' . $event->getObjectId());
|
||||
$card['link'] = $this->cardService->getRedirectUrlForCard($event->getObjectId());
|
||||
}
|
||||
$params['card'] = $card;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,20 @@ use OCP\IInitialStateService;
|
||||
use OCP\IRequest;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCP\IURLGenerator;
|
||||
use \OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Service\CardService;
|
||||
|
||||
class PageController extends Controller {
|
||||
private $permissionService;
|
||||
private $initialState;
|
||||
private $configService;
|
||||
private $eventDispatcher;
|
||||
private $cardMapper;
|
||||
private $urlGenerator;
|
||||
private $cardService;
|
||||
|
||||
public function __construct(
|
||||
$AppName,
|
||||
@@ -47,7 +55,10 @@ class PageController extends Controller {
|
||||
PermissionService $permissionService,
|
||||
IInitialStateService $initialStateService,
|
||||
ConfigService $configService,
|
||||
IEventDispatcher $eventDispatcher
|
||||
IEventDispatcher $eventDispatcher,
|
||||
CardMapper $cardMapper,
|
||||
IURLGenerator $urlGenerator,
|
||||
CardService $cardService
|
||||
) {
|
||||
parent::__construct($AppName, $request);
|
||||
|
||||
@@ -55,6 +66,9 @@ class PageController extends Controller {
|
||||
$this->initialState = $initialStateService;
|
||||
$this->configService = $configService;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cardService = $cardService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,4 +99,17 @@ class PageController extends Controller {
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function redirectToCard($cardId): RedirectResponse {
|
||||
try {
|
||||
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
|
||||
return new RedirectResponse($this->cardService->getCardUrl($cardId));
|
||||
} catch (\Exception $e) {
|
||||
return new RedirectResponse($this->urlGenerator->linkToRouteAbsolute('deck.page.index'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
namespace OCA\Deck\Cron;
|
||||
|
||||
use OCP\BackgroundJob\Job;
|
||||
use OC\BackgroundJob\Job;
|
||||
use OCA\Deck\Activity\ActivityManager;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
|
||||
|
||||
@@ -24,14 +24,13 @@
|
||||
|
||||
namespace OCA\Deck\Cron;
|
||||
|
||||
use OCP\BackgroundJob\TimedJob;
|
||||
use OC\BackgroundJob\Job;
|
||||
use OCA\Deck\Db\AttachmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\InvalidAttachmentType;
|
||||
use OCA\Deck\Service\AttachmentService;
|
||||
use OCP\BackgroundJob\IJob;
|
||||
|
||||
class DeleteCron extends TimedJob {
|
||||
class DeleteCron extends Job {
|
||||
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
@@ -44,9 +43,6 @@ class DeleteCron extends TimedJob {
|
||||
$this->boardMapper = $boardMapper;
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->attachmentMapper = $attachmentMapper;
|
||||
|
||||
$this->setInterval(60 * 60 * 24);
|
||||
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
namespace OCA\Deck\Cron;
|
||||
|
||||
use OCP\BackgroundJob\Job;
|
||||
use OC\BackgroundJob\Job;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Notification\NotificationHelper;
|
||||
|
||||
@@ -25,46 +25,23 @@ namespace OCA\Deck\Db;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class AclMapper extends QBMapper implements IPermissionMapper {
|
||||
class AclMapper extends DeckMapper implements IPermissionMapper {
|
||||
public function __construct(IDBConnection $db) {
|
||||
parent::__construct($db, 'deck_board_acl', Acl::class);
|
||||
}
|
||||
|
||||
public function find($id): Acl {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($id)));
|
||||
|
||||
return $this->findEntity($query);
|
||||
}
|
||||
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($query->expr()->eq('board_id', $query->createNamedParameter($boardId)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($query);
|
||||
$sql = 'SELECT id, board_id, type, participant, permission_edit, permission_share, permission_manage FROM `*PREFIX*deck_board_acl` WHERE `board_id` = ? ';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
}
|
||||
|
||||
public function isOwner($userId, $aclId): bool {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('owner')
|
||||
->from('deck_boards', 'b')
|
||||
->innerJoin('b', $this->getTableName(), 'a', $query->expr()->eq('b.id', 'a.board_id'))
|
||||
->where($query->expr()->eq('a.id', $query->createNamedParameter($aclId)));
|
||||
|
||||
$cursor = $query->execute();
|
||||
$row = $cursor->fetch();
|
||||
$cursor->closeCursor();
|
||||
|
||||
return is_array($row) && $row['owner'] === $userId;
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_board_acl` WHERE id = ?)';
|
||||
$stmt = $this->execute($sql, [$aclId]);
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
}
|
||||
|
||||
public function findBoardId($id): ?int {
|
||||
@@ -76,13 +53,8 @@ class AclMapper extends QBMapper implements IPermissionMapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function findByParticipant(int $type, string $participant): array {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($query->expr()->eq('type', $query->createNamedParameter($type)))
|
||||
->andWhere($query->expr()->eq('type', $query->createNamedParameter($participant)));
|
||||
|
||||
return $this->findEntities($query);
|
||||
public function findByParticipant($type, $participant): array {
|
||||
$sql = 'SELECT * from *PREFIX*deck_board_acl WHERE type = ? AND participant = ?';
|
||||
return $this->findEntities($sql, [$type, $participant]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,8 @@ class Board extends RelationalEntity {
|
||||
protected $owner;
|
||||
protected $color;
|
||||
protected $archived = false;
|
||||
/** @var Label[]|null */
|
||||
protected $labels = null;
|
||||
/** @var Acl[]|null */
|
||||
protected $acl = null;
|
||||
protected $labels = [];
|
||||
protected $acl = [];
|
||||
protected $permissions = [];
|
||||
protected $users = [];
|
||||
protected $shared;
|
||||
@@ -63,10 +61,6 @@ class Board extends RelationalEntity {
|
||||
if ($this->shared === -1) {
|
||||
unset($json['shared']);
|
||||
}
|
||||
// FIXME: Ideally the API responses should follow the internal data structure and return null if the labels/acls have not been fetched from the db
|
||||
// however this would be a breaking change for consumers of the API
|
||||
$json['acl'] = $this->acl ?? [];
|
||||
$json['labels'] = $this->labels ?? [];
|
||||
return $json;
|
||||
}
|
||||
|
||||
@@ -74,27 +68,21 @@ class Board extends RelationalEntity {
|
||||
* @param Label[] $labels
|
||||
*/
|
||||
public function setLabels($labels) {
|
||||
$this->labels = $labels;
|
||||
foreach ($labels as $l) {
|
||||
$this->labels[] = $l;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Acl[] $acl
|
||||
*/
|
||||
public function setAcl($acl) {
|
||||
$this->acl = $acl;
|
||||
foreach ($acl as $a) {
|
||||
$this->acl[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
public function getETag() {
|
||||
return md5((string)$this->getLastModified());
|
||||
}
|
||||
|
||||
/** @returns Acl[]|null */
|
||||
public function getAcl(): ?array {
|
||||
return $this->acl;
|
||||
}
|
||||
|
||||
/** @returns Label[]|null */
|
||||
public function getLabels(): ?array {
|
||||
return $this->labels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,12 @@ namespace OCA\Deck\Db;
|
||||
use OC\Cache\CappedMemoryCache;
|
||||
use OCA\Deck\Service\CirclesService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IGroupManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
class BoardMapper extends DeckMapper implements IPermissionMapper {
|
||||
private $labelMapper;
|
||||
private $aclMapper;
|
||||
private $stackMapper;
|
||||
@@ -44,8 +42,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
/** @var CappedMemoryCache */
|
||||
private $userBoardCache;
|
||||
/** @var CappedMemoryCache */
|
||||
private $boardCache;
|
||||
|
||||
public function __construct(
|
||||
IDBConnection $db,
|
||||
@@ -67,7 +63,6 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
$this->logger = $logger;
|
||||
|
||||
$this->userBoardCache = new CappedMemoryCache();
|
||||
$this->boardCache = new CappedMemoryCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,52 +70,40 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
* @param $id
|
||||
* @param bool $withLabels
|
||||
* @param bool $withAcl
|
||||
* @return Board
|
||||
* @return \OCP\AppFramework\Db\Entity
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function find($id, $withLabels = false, $withAcl = false): Board {
|
||||
if (!isset($this->boardCache[$id])) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('deck_boards')
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
||||
->orderBy('id');
|
||||
$this->boardCache[$id] = $this->findEntity($qb);
|
||||
}
|
||||
|
||||
// FIXME is this necessary? it was NOT done with the old mapper
|
||||
// $this->mapOwner($board);
|
||||
public function find($id, $withLabels = false, $withAcl = false) {
|
||||
$sql = 'SELECT id, title, owner, color, archived, deleted_at, last_modified FROM `*PREFIX*deck_boards` ' .
|
||||
'WHERE `id` = ?';
|
||||
$board = $this->findEntity($sql, [$id]);
|
||||
|
||||
// Add labels
|
||||
if ($withLabels && $this->boardCache[$id]->getLabels() === null) {
|
||||
if ($withLabels) {
|
||||
$labels = $this->labelMapper->findAll($id);
|
||||
$this->boardCache[$id]->setLabels($labels);
|
||||
$board->setLabels($labels);
|
||||
}
|
||||
|
||||
// Add acl
|
||||
if ($withAcl && $this->boardCache[$id]->getAcl() === null) {
|
||||
if ($withAcl) {
|
||||
$acl = $this->aclMapper->findAll($id);
|
||||
$this->boardCache[$id]->setAcl($acl);
|
||||
$board->setAcl($acl);
|
||||
}
|
||||
|
||||
return $this->boardCache[$id];
|
||||
return $board;
|
||||
}
|
||||
|
||||
public function findAllForUser(string $userId, ?int $since = null, bool $includeArchived = true, ?int $before = null,
|
||||
?string $term = null): array {
|
||||
$useCache = ($since === -1 && $includeArchived === true && $before === null && $term === null);
|
||||
public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array {
|
||||
$useCache = ($since === -1 && $includeArchived === true);
|
||||
if (!isset($this->userBoardCache[$userId]) || !$useCache) {
|
||||
$groups = $this->groupManager->getUserGroupIds(
|
||||
$this->userManager->get($userId)
|
||||
);
|
||||
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived, $before, $term);
|
||||
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived, $before, $term);
|
||||
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived, $before, $term);
|
||||
$userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived);
|
||||
$groupBoards = $this->findAllByGroups($userId, $groups, null, null, $since, $includeArchived);
|
||||
$circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived);
|
||||
$allBoards = array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
|
||||
foreach ($allBoards as $board) {
|
||||
$this->boardCache[$board->getId()] = $board;
|
||||
}
|
||||
if ($useCache) {
|
||||
$this->userBoardCache[$userId] = $allBoards;
|
||||
}
|
||||
@@ -137,91 +120,19 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
* @param null $offset
|
||||
* @return array
|
||||
*/
|
||||
public function findAllByUser(string $userId, ?int $limit = null, ?int $offset = null, ?int $since = null,
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
|
||||
// FIXME this used to be a UNION to get boards owned by $userId and the user shares in one single query
|
||||
// Is it possible with the query builder?
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
|
||||
// this does not work in MySQL/PostgreSQL
|
||||
//->selectAlias('0', 'shared')
|
||||
->from('deck_boards', 'b')
|
||||
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
public function findAllByUser($userId, $limit = null, $offset = null, $since = -1, $includeArchived = true) {
|
||||
// FIXME: One moving to QBMapper we should allow filtering the boards probably by method chaining for additional where clauses
|
||||
$sql = 'SELECT id, title, owner, color, archived, deleted_at, 0 as shared, last_modified FROM `*PREFIX*deck_boards` WHERE owner = ? AND last_modified > ?';
|
||||
if (!$includeArchived) {
|
||||
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$sql .= ' AND NOT archived AND deleted_at = 0';
|
||||
}
|
||||
if ($since !== null) {
|
||||
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($before !== null) {
|
||||
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($term !== null) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->iLike(
|
||||
'title',
|
||||
$qb->createNamedParameter(
|
||||
'%' . $this->db->escapeLikeParameter($term) . '%',
|
||||
IQueryBuilder::PARAM_STR
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
$qb->orderBy('b.id');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
$entries = $this->findEntities($qb);
|
||||
foreach ($entries as $entry) {
|
||||
$entry->setShared(0);
|
||||
}
|
||||
|
||||
// shared with user
|
||||
$qb->resetQueryParts();
|
||||
$qb->select('b.id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
|
||||
//->selectAlias('1', 'shared')
|
||||
->from('deck_boards', 'b')
|
||||
->innerJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'))
|
||||
->where($qb->expr()->eq('acl.participant', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
|
||||
->andWhere($qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_USER, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->neq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
$sql .= ' UNION ' .
|
||||
'SELECT boards.id, title, owner, color, archived, deleted_at, 1 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
|
||||
'JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE acl.participant=? AND acl.type=? AND boards.owner != ? AND last_modified > ?';
|
||||
if (!$includeArchived) {
|
||||
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
$sql .= ' AND NOT archived AND deleted_at = 0';
|
||||
}
|
||||
if ($since !== null) {
|
||||
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($before !== null) {
|
||||
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($term !== null) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->iLike(
|
||||
'title',
|
||||
$qb->createNamedParameter(
|
||||
'%' . $this->db->escapeLikeParameter($term) . '%',
|
||||
IQueryBuilder::PARAM_STR
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
$qb->orderBy('b.id');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
$sharedEntries = $this->findEntities($qb);
|
||||
foreach ($sharedEntries as $entry) {
|
||||
$entry->setShared(1);
|
||||
}
|
||||
$entries = array_merge($entries, $sharedEntries);
|
||||
$entries = $this->findEntities($sql, [$userId, $since, $userId, Acl::PERMISSION_TYPE_USER, $userId, $since], $limit, $offset);
|
||||
/* @var Board $entry */
|
||||
foreach ($entries as $entry) {
|
||||
$acl = $this->aclMapper->findAll($entry->id);
|
||||
@@ -230,19 +141,9 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function findAllByOwner(string $userId, ?int $limit = null, ?int $offset = null) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('deck_boards')
|
||||
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
|
||||
->orderBy('id');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
return $this->findEntities($qb);
|
||||
public function findAllByOwner(string $userId, int $limit = null, int $offset = null) {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_boards` WHERE owner = ?';
|
||||
return $this->findEntities($sql, [$userId], $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,57 +155,23 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
* @param null $offset
|
||||
* @return array
|
||||
*/
|
||||
public function findAllByGroups(string $userId, array $groups, ?int $limit = null, ?int $offset = null, ?int $since = null,
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
|
||||
public function findAllByGroups($userId, $groups, $limit = null, $offset = null, $since = -1,$includeArchived = true) {
|
||||
if (count($groups) <= 0) {
|
||||
return [];
|
||||
}
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('b.id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
|
||||
//->selectAlias('2', 'shared')
|
||||
->from('deck_boards', 'b')
|
||||
->innerJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'))
|
||||
->where($qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_GROUP, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->neq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
$or = $qb->expr()->orx();
|
||||
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
|
||||
'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND (';
|
||||
for ($i = 0, $iMax = count($groups); $i < $iMax; $i++) {
|
||||
$or->add(
|
||||
$qb->expr()->eq('acl.participant', $qb->createNamedParameter($groups[$i], IQueryBuilder::PARAM_STR))
|
||||
);
|
||||
$sql .= 'acl.participant = ? ';
|
||||
if (count($groups) > 1 && $i < count($groups) - 1) {
|
||||
$sql .= ' OR ';
|
||||
}
|
||||
}
|
||||
$qb->andWhere($or);
|
||||
$sql .= ')';
|
||||
if (!$includeArchived) {
|
||||
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($since !== null) {
|
||||
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($before !== null) {
|
||||
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($term !== null) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->iLike(
|
||||
'title',
|
||||
$qb->createNamedParameter(
|
||||
'%' . $this->db->escapeLikeParameter($term) . '%',
|
||||
IQueryBuilder::PARAM_STR
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
$qb->orderBy('b.id');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
$entries = $this->findEntities($qb);
|
||||
foreach ($entries as $entry) {
|
||||
$entry->setShared(2);
|
||||
$sql .= ' AND NOT archived AND deleted_at = 0';
|
||||
}
|
||||
$entries = $this->findEntities($sql, array_merge([$userId, Acl::PERMISSION_TYPE_GROUP], $groups), $limit, $offset);
|
||||
/* @var Board $entry */
|
||||
foreach ($entries as $entry) {
|
||||
$acl = $this->aclMapper->findAll($entry->id);
|
||||
@@ -313,59 +180,25 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function findAllByCircles(string $userId, ?int $limit = null, ?int $offset = null, ?int $since = null,
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
|
||||
public function findAllByCircles($userId, $limit = null, $offset = null, $since = -1,$includeArchived = true) {
|
||||
$circles = $this->circlesService->getUserCircles($userId);
|
||||
if (count($circles) === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('b.id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
|
||||
//->selectAlias('2', 'shared')
|
||||
->from('deck_boards', 'b')
|
||||
->innerJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'))
|
||||
->where($qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_CIRCLE, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->neq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
$or = $qb->expr()->orx();
|
||||
$sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared, last_modified FROM `*PREFIX*deck_boards` as boards ' .
|
||||
'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND (';
|
||||
for ($i = 0, $iMax = count($circles); $i < $iMax; $i++) {
|
||||
$or->add(
|
||||
$qb->expr()->eq('acl.participant', $qb->createNamedParameter($circles[$i], IQueryBuilder::PARAM_STR))
|
||||
);
|
||||
$sql .= 'acl.participant = ? ';
|
||||
if (count($circles) > 1 && $i < count($circles) - 1) {
|
||||
$sql .= ' OR ';
|
||||
}
|
||||
}
|
||||
$qb->andWhere($or);
|
||||
$sql .= ')';
|
||||
if (!$includeArchived) {
|
||||
$qb->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($since !== null) {
|
||||
$qb->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($before !== null) {
|
||||
$qb->andWhere($qb->expr()->lt('last_modified', $qb->createNamedParameter($before, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
if ($term !== null) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->iLike(
|
||||
'title',
|
||||
$qb->createNamedParameter(
|
||||
'%' . $this->db->escapeLikeParameter($term) . '%',
|
||||
IQueryBuilder::PARAM_STR
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
$qb->orderBy('b.id');
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
$entries = $this->findEntities($qb);
|
||||
foreach ($entries as $entry) {
|
||||
$entry->setShared(2);
|
||||
$sql .= ' AND NOT archived AND deleted_at = 0';
|
||||
}
|
||||
$entries = $this->findEntities($sql, array_merge([$userId, Acl::PERMISSION_TYPE_CIRCLE], $circles), $limit, $offset);
|
||||
/* @var Board $entry */
|
||||
foreach ($entries as $entry) {
|
||||
$acl = $this->aclMapper->findAll($entry->id);
|
||||
@@ -374,26 +207,21 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function findAll(): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id')
|
||||
->from('deck_boards');
|
||||
return $this->findEntities($qb);
|
||||
public function findAll() {
|
||||
$sql = 'SELECT id from *PREFIX*deck_boards;';
|
||||
return $this->findEntities($sql);
|
||||
}
|
||||
|
||||
public function findToDelete() {
|
||||
// add buffer of 5 min
|
||||
$timeLimit = time() - (60 * 5);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id', 'title', 'owner', 'color', 'archived', 'deleted_at', 'last_modified')
|
||||
->from('deck_boards')
|
||||
->where($qb->expr()->gt('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($timeLimit, IQueryBuilder::PARAM_INT)));
|
||||
return $this->findEntities($qb);
|
||||
$sql = 'SELECT id, title, owner, color, archived, deleted_at, last_modified FROM `*PREFIX*deck_boards` ' .
|
||||
'WHERE `deleted_at` > 0 AND `deleted_at` < ?';
|
||||
return $this->findEntities($sql, [$timeLimit]);
|
||||
}
|
||||
|
||||
public function delete(/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
|
||||
\OCP\AppFramework\Db\Entity $entity): \OCP\AppFramework\Db\Entity {
|
||||
\OCP\AppFramework\Db\Entity $entity) {
|
||||
// delete acl
|
||||
$acl = $this->aclMapper->findAll($entity->getId());
|
||||
foreach ($acl as $item) {
|
||||
|
||||
@@ -30,8 +30,6 @@ use OCA\Deck\Search\Query\SearchQuery;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
@@ -48,8 +46,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
private $groupManager;
|
||||
/** @var IManager */
|
||||
private $notificationManager;
|
||||
/** @var ICache */
|
||||
private $cache;
|
||||
private $databaseType;
|
||||
private $database4ByteSupport;
|
||||
|
||||
@@ -59,7 +55,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
IManager $notificationManager,
|
||||
ICacheFactory $cacheFactory,
|
||||
$databaseType = 'sqlite3',
|
||||
$database4ByteSupport = true
|
||||
) {
|
||||
@@ -68,7 +63,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
$this->userManager = $userManager;
|
||||
$this->groupManager = $groupManager;
|
||||
$this->notificationManager = $notificationManager;
|
||||
$this->cache = $cacheFactory->createDistributed('deck-cardMapper');
|
||||
$this->databaseType = $databaseType;
|
||||
$this->database4ByteSupport = $database4ByteSupport;
|
||||
}
|
||||
@@ -81,9 +75,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
$description = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $entity->getDescription());
|
||||
$entity->setDescription($description);
|
||||
}
|
||||
$entity = parent::insert($entity);
|
||||
$this->cache->remove('findBoardId:' . $entity->getId());
|
||||
return $entity;
|
||||
return parent::insert($entity);
|
||||
}
|
||||
|
||||
public function update(Entity $entity, $updateModified = true): Entity {
|
||||
@@ -115,10 +107,6 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
// Invalidate cache when the card may be moved to a different board
|
||||
if (isset($updatedFields['stackId'])) {
|
||||
$this->cache->remove('findBoardId:' . $entity->getId());
|
||||
}
|
||||
return parent::update($entity);
|
||||
}
|
||||
|
||||
@@ -265,7 +253,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
public function findOverdue() {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id', 'title', 'duedate', 'notified')
|
||||
$qb->select('id','title','duedate','notified')
|
||||
->from('deck_cards')
|
||||
->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()')))
|
||||
->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
@@ -276,7 +264,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
public function findUnexposedDescriptionChances() {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id', 'title', 'duedate', 'notified', 'description_prev', 'last_editor', 'description')
|
||||
$qb->select('id','title','duedate','notified','description_prev','last_editor','description')
|
||||
->from('deck_cards')
|
||||
->where($qb->expr()->isNotNull('last_editor'))
|
||||
->andWhere($qb->expr()->isNotNull('description_prev'));
|
||||
@@ -491,8 +479,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
}
|
||||
return $qb->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
|
||||
$qb = $this->queryCardsByBoards($boardIds)
|
||||
@@ -518,8 +506,9 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
}
|
||||
|
||||
public function delete(Entity $entity): Entity {
|
||||
// delete assigned labels
|
||||
$this->labelMapper->deleteLabelAssignmentsForCard($entity->getId());
|
||||
$this->cache->remove('findBoardId:' . $entity->getId());
|
||||
// delete card
|
||||
return parent::delete($entity);
|
||||
}
|
||||
|
||||
@@ -558,22 +547,11 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
}
|
||||
|
||||
public function findBoardId($id): ?int {
|
||||
$result = $this->cache->get('findBoardId:' . $id);
|
||||
if ($result === null) {
|
||||
try {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('board_id')
|
||||
->from('deck_stacks', 's')
|
||||
->innerJoin('s', 'deck_cards', 'c', 'c.stack_id = s.id')
|
||||
->where($qb->expr()->eq('c.id', $qb->createNamedParameter($id)));
|
||||
$queryResult = $qb->executeQuery();
|
||||
$result = $queryResult->fetchOne();
|
||||
} catch (\Exception $e) {
|
||||
$result = false;
|
||||
}
|
||||
$this->cache->set('findBoardId:' . $id, $result);
|
||||
}
|
||||
return $result !== false ? $result : null;
|
||||
$sql = 'SELECT id FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id IN (SELECT stack_id FROM `*PREFIX*deck_cards` WHERE id = ?))';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $id, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchColumn() ?? null;
|
||||
}
|
||||
|
||||
public function mapOwner(Card &$card) {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
use OCP\IUserManager;
|
||||
use OCA\Deck\BadRequestException;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class BoardService {
|
||||
private $boardMapper;
|
||||
@@ -68,8 +69,8 @@ class BoardService {
|
||||
private $activityManager;
|
||||
private $eventDispatcher;
|
||||
private $changeHelper;
|
||||
|
||||
private $boardsCache = null;
|
||||
private $urlGenerator;
|
||||
|
||||
|
||||
public function __construct(
|
||||
@@ -87,6 +88,7 @@ class BoardService {
|
||||
ActivityManager $activityManager,
|
||||
IEventDispatcher $eventDispatcher,
|
||||
ChangeHelper $changeHelper,
|
||||
IURLGenerator $urlGenerator,
|
||||
$userId
|
||||
) {
|
||||
$this->boardMapper = $boardMapper;
|
||||
@@ -104,6 +106,7 @@ class BoardService {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->changeHelper = $changeHelper;
|
||||
$this->userId = $userId;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,9 +121,8 @@ class BoardService {
|
||||
/**
|
||||
* Get all boards that are shared with a user, their groups or circles
|
||||
*/
|
||||
public function getUserBoards(?int $since = null, bool $includeArchived = true, ?int $before = null,
|
||||
?string $term = null): array {
|
||||
return $this->boardMapper->findAllForUser($this->userId, $since, $includeArchived, $before, $term);
|
||||
public function getUserBoards(int $since = -1, bool $includeArchived = true): array {
|
||||
return $this->boardMapper->findAllForUser($this->userId, $since, $includeArchived);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,11 +181,9 @@ class BoardService {
|
||||
/** @var Board $board */
|
||||
$board = $this->boardMapper->find($boardId, true, true);
|
||||
$this->boardMapper->mapOwner($board);
|
||||
if ($board->getAcl() !== null) {
|
||||
foreach ($board->getAcl() as $acl) {
|
||||
if ($acl !== null) {
|
||||
$this->boardMapper->mapAcl($acl);
|
||||
}
|
||||
foreach ($board->getAcl() as &$acl) {
|
||||
if ($acl !== null) {
|
||||
$this->boardMapper->mapAcl($acl);
|
||||
}
|
||||
}
|
||||
$permissions = $this->permissionService->matchPermissions($board);
|
||||
@@ -697,4 +697,8 @@ class BoardService {
|
||||
}
|
||||
$board->setUsers(array_values($boardUsers));
|
||||
}
|
||||
|
||||
public function getBoardUrl($endpoint) {
|
||||
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . '#' . $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ use OCA\Deck\BadRequestException;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class CardService {
|
||||
private $cardMapper;
|
||||
@@ -63,6 +64,7 @@ class CardService {
|
||||
private $changeHelper;
|
||||
private $eventDispatcher;
|
||||
private $userManager;
|
||||
private $urlGenerator;
|
||||
|
||||
public function __construct(
|
||||
CardMapper $cardMapper,
|
||||
@@ -79,6 +81,7 @@ class CardService {
|
||||
IUserManager $userManager,
|
||||
ChangeHelper $changeHelper,
|
||||
IEventDispatcher $eventDispatcher,
|
||||
IURLGenerator $urlGenerator,
|
||||
$userId
|
||||
) {
|
||||
$this->cardMapper = $cardMapper;
|
||||
@@ -96,6 +99,7 @@ class CardService {
|
||||
$this->changeHelper = $changeHelper;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->currentUser = $userId;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
public function enrich($card) {
|
||||
@@ -602,4 +606,14 @@ class CardService {
|
||||
|
||||
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
|
||||
}
|
||||
|
||||
public function getCardUrl($cardId) {
|
||||
$boardId = $this->cardMapper->findBoardId($cardId);
|
||||
|
||||
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . "#/board/$boardId/card/$cardId";
|
||||
}
|
||||
|
||||
public function getRedirectUrlForCard($cardId) {
|
||||
return $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . "card/$cardId";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ class OverviewService {
|
||||
private function findAllBoardsFromUser(string $userId): array {
|
||||
$userInfo = $this->getBoardPrerequisites($userId);
|
||||
$userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null);
|
||||
$groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'], null, null);
|
||||
$groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null);
|
||||
$circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null);
|
||||
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -500,7 +500,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
);
|
||||
}
|
||||
|
||||
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
|
||||
$qb->orderBy('s.id');
|
||||
|
||||
@@ -11,6 +11,4 @@ pages:
|
||||
- Nextcloud API: API-Nextcloud.md
|
||||
- Developer documentation:
|
||||
- Data structure: structure.md
|
||||
- Import documentation:
|
||||
- Implement import: implement-import.md
|
||||
- Class diagram: import-class-diagram.md
|
||||
|
||||
|
||||
15325
package-lock.json
generated
15325
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deck",
|
||||
"description": "",
|
||||
"version": "1.7.0-beta1",
|
||||
"version": "1.5.6",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Julius Härtl",
|
||||
@@ -29,25 +29,24 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@babel/runtime": "^7.14.6",
|
||||
"@juliushaertl/vue-richtext": "^1.0.1",
|
||||
"@nextcloud/auth": "^1.3.0",
|
||||
"@nextcloud/axios": "^1.9.0",
|
||||
"@nextcloud/axios": "^1.6.0",
|
||||
"@nextcloud/dialogs": "^3.1.2",
|
||||
"@nextcloud/event-bus": "^2.1.1",
|
||||
"@nextcloud/files": "^2.1.0",
|
||||
"@nextcloud/initial-state": "^1.2.1",
|
||||
"@nextcloud/event-bus": "^2.0.0",
|
||||
"@nextcloud/files": "^2.0.0",
|
||||
"@nextcloud/initial-state": "^1.2.0",
|
||||
"@nextcloud/l10n": "^1.4.1",
|
||||
"@nextcloud/moment": "^1.1.1",
|
||||
"@nextcloud/router": "^2.0.0",
|
||||
"@nextcloud/vue": "^5.0.0",
|
||||
"@nextcloud/vue": "^3.10.1",
|
||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"dompurify": "^2.3.6",
|
||||
"blueimp-md5": "^2.18.0",
|
||||
"dompurify": "^2.2.9",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it": "^12.0.6",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"markdown-it-link-attributes": "^4.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"nextcloud-vue-collections": "^0.9.0",
|
||||
"p-queue": "^6.6.2",
|
||||
@@ -55,9 +54,9 @@
|
||||
"vue": "^2.6.14",
|
||||
"vue-at": "^2.5.0-beta.2",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-easymde": "^2.0.0",
|
||||
"vue-easymde": "^1.4.0",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-router": "^3.5.1",
|
||||
"vue-smooth-dnd": "^0.8.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
@@ -66,18 +65,18 @@
|
||||
"extends @nextcloud/browserslist-config"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^14.0.0",
|
||||
"npm": "^7.0.0"
|
||||
"node": ">=14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/browserslist-config": "^2.2.0",
|
||||
"@nextcloud/eslint-config": "^6.1.2",
|
||||
"@nextcloud/stylelint-config": "^2.1.2",
|
||||
"@nextcloud/webpack-vue-config": "^5.0.0",
|
||||
"@relative-ci/agent": "^3.1.1",
|
||||
"@vue/test-utils": "^1.3.0",
|
||||
"jest": "^27.5.1",
|
||||
"@nextcloud/babel-config": "^1.0.0-beta.1",
|
||||
"@nextcloud/browserslist-config": "^2.1.0",
|
||||
"@nextcloud/eslint-config": "^5.1.0",
|
||||
"@nextcloud/stylelint-config": "^1.0.0-beta.0",
|
||||
"@nextcloud/webpack-vue-config": "^4.0.3",
|
||||
"@relative-ci/agent": "^2.0.0",
|
||||
"@vue/test-utils": "^1.2.1",
|
||||
"jest": "^27.0.4",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"vue-jest": "^3.0.7"
|
||||
},
|
||||
@@ -97,4 +96,4 @@
|
||||
"<rootDir>/node_modules/jest-serializer-vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/App.vue
19
src/App.vue
@@ -59,11 +59,6 @@ export default {
|
||||
Content,
|
||||
AppContent,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
boardApi,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addButton: {
|
||||
@@ -88,6 +83,7 @@ export default {
|
||||
navShown: state => state.navShown,
|
||||
sidebarShownState: state => state.sidebarShown,
|
||||
currentBoard: state => state.currentBoard,
|
||||
cardDetailsInModal: state => state.cardDetailsInModal,
|
||||
}),
|
||||
// TODO: properly handle sidebar showing for route subview and board sidebar
|
||||
sidebarRouterView() {
|
||||
@@ -97,14 +93,6 @@ export default {
|
||||
sidebarShown() {
|
||||
return this.sidebarRouterView || this.sidebarShownState
|
||||
},
|
||||
cardDetailsInModal: {
|
||||
get() {
|
||||
return this.$store.getters.config('cardDetailsInModal')
|
||||
},
|
||||
set(newValue) {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('loadBoards')
|
||||
@@ -124,6 +112,11 @@ export default {
|
||||
this.$router.push({ name: 'board' })
|
||||
},
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
boardApi,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -76,108 +76,110 @@
|
||||
<ActionButton v-else icon="icon-filter" />
|
||||
</Actions>
|
||||
|
||||
<div v-if="filterVisible" class="filter">
|
||||
<h3>{{ t('deck', 'Filter by tag') }}</h3>
|
||||
<div v-for="label in labelsSorted" :key="label.id" class="filter--item">
|
||||
<input
|
||||
:id="label.id"
|
||||
v-model="filter.tags"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="label.id"
|
||||
@change="setFilter">
|
||||
<label :for="label.id"><span class="label" :style="labelStyle(label)">{{ label.title }}</span></label>
|
||||
</div>
|
||||
<template>
|
||||
<div v-if="filterVisible" class="filter">
|
||||
<h3>{{ t('deck', 'Filter by tag') }}</h3>
|
||||
<div v-for="label in labelsSorted" :key="label.id" class="filter--item">
|
||||
<input
|
||||
:id="label.id"
|
||||
v-model="filter.tags"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="label.id"
|
||||
@change="setFilter">
|
||||
<label :for="label.id"><span class="label" :style="labelStyle(label)">{{ label.title }}</span></label>
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="unassigned"
|
||||
v-model="filter.unassigned"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
value="unassigned"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
|
||||
</div>
|
||||
<div v-for="user in board.users" :key="user.uid" class="filter--item">
|
||||
<input
|
||||
:id="user.uid"
|
||||
v-model="filter.users"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="user.uid"
|
||||
@change="setFilter">
|
||||
<label :for="user.uid"><Avatar :user="user.uid" :size="24" :disable-menu="true" /> {{ user.displayname }}</label>
|
||||
</div>
|
||||
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="unassigned"
|
||||
v-model="filter.unassigned"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
value="unassigned"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
|
||||
</div>
|
||||
<div v-for="user in board.users" :key="user.uid" class="filter--item">
|
||||
<input
|
||||
:id="user.uid"
|
||||
v-model="filter.users"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="user.uid"
|
||||
@change="setFilter">
|
||||
<label :for="user.uid"><Avatar :user="user.uid" :size="24" :disable-menu="true" /> {{ user.displayname }}</label>
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by due date') }}</h3>
|
||||
<h3>{{ t('deck', 'Filter by due date') }}</h3>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="overdue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="overdue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="overdue">{{ t('deck', 'Overdue') }}</label>
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="overdue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="overdue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="overdue">{{ t('deck', 'Overdue') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueToday"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueToday"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueToday">{{ t('deck', 'Next 24 hours') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueWeek"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueWeek"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueWeek">{{ t('deck', 'Next 7 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueMonth"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueMonth"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueMonth">{{ t('deck', 'Next 30 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="noDue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="noDue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="noDue">{{ t('deck', 'No due date') }}</label>
|
||||
</div>
|
||||
|
||||
<Button :disabled="!isFilterActive" @click="clearFilter">
|
||||
{{ t('deck', 'Clear filter') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueToday"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueToday"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueToday">{{ t('deck', 'Next 24 hours') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueWeek"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueWeek"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueWeek">{{ t('deck', 'Next 7 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueMonth"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueMonth"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueMonth">{{ t('deck', 'Next 30 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="noDue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="noDue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="noDue">{{ t('deck', 'No due date') }}</label>
|
||||
</div>
|
||||
|
||||
<Button :disabled="!isFilterActive" @click="clearFilter">
|
||||
{{ t('deck', 'Clear filter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<Actions>
|
||||
@@ -239,7 +241,6 @@ export default {
|
||||
isAddStackVisible: false,
|
||||
filter: { tags: [], users: [], due: '', unassigned: false },
|
||||
showAddCardModal: false,
|
||||
defaultPageTitle: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -267,17 +268,11 @@ export default {
|
||||
return [...this.board.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.setPageTitle('')
|
||||
},
|
||||
watch: {
|
||||
board(current, previous) {
|
||||
if (current?.id !== previous?.id) {
|
||||
this.clearFilter()
|
||||
}
|
||||
if (current) {
|
||||
this.setPageTitle(current.title)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -337,22 +332,6 @@ export default {
|
||||
clickHideAddCardModel() {
|
||||
this.showAddCardModal = false
|
||||
},
|
||||
setPageTitle(title) {
|
||||
if (this.defaultPageTitle === false) {
|
||||
this.defaultPageTitle = window.document.title
|
||||
if (this.defaultPageTitle.indexOf(' - Deck - ') !== -1) {
|
||||
this.defaultPageTitle = this.defaultPageTitle.substring(this.defaultPageTitle.indexOf(' - Deck - ') + 3)
|
||||
}
|
||||
if (this.defaultPageTitle.indexOf('Deck - ') !== 0) {
|
||||
this.defaultPageTitle = 'Deck - ' + this.defaultPageTitle
|
||||
}
|
||||
}
|
||||
let newTitle = this.defaultPageTitle
|
||||
if (title !== '') {
|
||||
newTitle = `${title} - ${newTitle}`
|
||||
}
|
||||
window.document.title = newTitle
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -162,6 +162,7 @@ export default {
|
||||
]),
|
||||
...mapState({
|
||||
showArchived: state => state.showArchived,
|
||||
cardDetailsInModal: state => state.cardDetailsInModal,
|
||||
}),
|
||||
cardsByStack() {
|
||||
return this.$store.getters.cardsByStack(this.stack.id).filter((card) => {
|
||||
@@ -174,14 +175,6 @@ export default {
|
||||
dragHandleSelector() {
|
||||
return this.canEdit ? null : '.no-drag'
|
||||
},
|
||||
cardDetailsInModal: {
|
||||
get() {
|
||||
return this.$store.getters.config('cardDetailsInModal')
|
||||
},
|
||||
set(newValue) {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -43,22 +43,24 @@
|
||||
|
||||
<li v-if="addLabel" class="editing">
|
||||
<!-- New Tag -->
|
||||
<form class="label-form" @submit.prevent="clickAddLabel">
|
||||
<ColorPicker class="color-picker-wrapper" :value="'#' + addLabelObj.color" @input="updateColor">
|
||||
<div :style="{ backgroundColor: '#' + addLabelObj.color }" class="color0 icon-colorpicker" />
|
||||
</ColorPicker>
|
||||
<input v-model="addLabelObj.title" type="text">
|
||||
<input v-tooltip="{content: missingDataLabel, show: !addLabelObjValidated, trigger: 'manual' }"
|
||||
:disabled="!addLabelObjValidated"
|
||||
type="submit"
|
||||
value=""
|
||||
class="icon-confirm">
|
||||
<Actions>
|
||||
<ActionButton icon="icon-close" @click="addLabel=false">
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</form>
|
||||
<template>
|
||||
<form class="label-form" @submit.prevent="clickAddLabel">
|
||||
<ColorPicker class="color-picker-wrapper" :value="'#' + addLabelObj.color" @input="updateColor">
|
||||
<div :style="{ backgroundColor: '#' + addLabelObj.color }" class="color0 icon-colorpicker" />
|
||||
</ColorPicker>
|
||||
<input v-model="addLabelObj.title" type="text">
|
||||
<input v-tooltip="{content: missingDataLabel, show: !addLabelObjValidated, trigger: 'manual' }"
|
||||
:disabled="!addLabelObjValidated"
|
||||
type="submit"
|
||||
value=""
|
||||
class="icon-confirm">
|
||||
<Actions>
|
||||
<ActionButton icon="icon-close" @click="addLabel=false">
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</form>
|
||||
</template>
|
||||
</li>
|
||||
<button v-if="canManage && !isArchived" @click="clickShowAddLabel()">
|
||||
<span class="icon-add" />{{ t('deck', 'Add a new tag') }}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<Actions v-if="selectable">
|
||||
<ActionButton icon="icon-confirm" @click="$emit('select-attachment', attachment)">
|
||||
<ActionButton icon="icon-confirm" @click="$emit('selectAttachment', attachment)">
|
||||
{{ t('deck', 'Add this attachment') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
@@ -89,10 +89,10 @@
|
||||
{{ t('deck', 'Remove attachment') }}
|
||||
</ActionButton>
|
||||
|
||||
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('delete-attachment', attachment)">
|
||||
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
|
||||
{{ t('deck', 'Delete Attachment') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-else-if="!attachment.extendedData.fileid" icon="icon-history" @click="$emit('restore-attachment', attachment)">
|
||||
<ActionButton v-else-if="!attachment.extendedData.fileid" icon="icon-history" @click="$emit('restoreAttachment', attachment)">
|
||||
{{ t('deck', 'Restore Attachment') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
@@ -222,7 +222,7 @@ export default {
|
||||
},
|
||||
shareFromFiles() {
|
||||
picker.pick()
|
||||
.then(async (path) => {
|
||||
.then(async(path) => {
|
||||
console.debug(`path ${path} selected for sharing`)
|
||||
if (!path.startsWith('/')) {
|
||||
throw new Error(t('files', 'Invalid path selected'))
|
||||
|
||||
@@ -25,14 +25,13 @@
|
||||
:active="tabId"
|
||||
:title="title"
|
||||
:subtitle="subtitle"
|
||||
:subtitle-tooltip="subtitleTooltip"
|
||||
:title-editable="titleEditable"
|
||||
@update:titleEditable="handleUpdateTitleEditable"
|
||||
@update:title="handleUpdateTitle"
|
||||
@submit-title="handleSubmitTitle"
|
||||
@close="closeSidebar">
|
||||
<template #secondary-actions>
|
||||
<ActionButton v-if="cardDetailsInModal" icon="icon-menu-sidebar" @click.stop="closeModal()">
|
||||
<ActionButton v-if="cardDetailsInModal" icon="icon-menu-sidebar" @click.stop="showModal()">
|
||||
{{ t('deck', 'Open in sidebar view') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-else icon="icon-external" @click.stop="showModal()">
|
||||
@@ -89,10 +88,8 @@ import CardSidebarTabAttachments from './CardSidebarTabAttachments'
|
||||
import CardSidebarTabComments from './CardSidebarTabComments'
|
||||
import CardSidebarTabActivity from './CardSidebarTabActivity'
|
||||
import relativeDate from '../../mixins/relativeDate'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { getLocale } from '@nextcloud/l10n'
|
||||
|
||||
const capabilities = window.OC.getCapabilities()
|
||||
|
||||
@@ -129,12 +126,12 @@ export default {
|
||||
titleEditable: false,
|
||||
titleEditing: '',
|
||||
hasActivity: capabilities && capabilities.activity,
|
||||
locale: getLocale(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentBoard: state => state.currentBoard,
|
||||
cardDetailsInModal: state => state.cardDetailsInModal,
|
||||
}),
|
||||
...mapGetters(['canEdit', 'assignables', 'cardActions', 'stackById']),
|
||||
title() {
|
||||
@@ -146,9 +143,6 @@ export default {
|
||||
subtitle() {
|
||||
return t('deck', 'Modified') + ': ' + this.relativeDate(this.currentCard.lastModified * 1000) + ' ' + t('deck', 'Created') + ': ' + this.relativeDate(this.currentCard.createdAt * 1000)
|
||||
},
|
||||
subtitleTooltip() {
|
||||
return t('deck', 'Modified') + ': ' + this.formatDate(this.currentCard.lastModified) + '\n' + t('deck', 'Created') + ': ' + this.formatDate(this.currentCard.createdAt)
|
||||
},
|
||||
cardRichObject() {
|
||||
return {
|
||||
id: '' + this.currentCard.id,
|
||||
@@ -158,14 +152,6 @@ export default {
|
||||
link: window.location.protocol + '//' + window.location.host + generateUrl('/apps/deck/') + `#/board/${this.currentBoard.id}/card/${this.currentCard.id}`,
|
||||
}
|
||||
},
|
||||
cardDetailsInModal: {
|
||||
get() {
|
||||
return this.$store.getters.config('cardDetailsInModal')
|
||||
},
|
||||
set(newValue) {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleUpdateTitleEditable(value) {
|
||||
@@ -191,13 +177,7 @@ export default {
|
||||
},
|
||||
|
||||
showModal() {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: true })
|
||||
},
|
||||
closeModal() {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: false })
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
return moment.unix(timestamp).locale(this.locale).format('LLLL')
|
||||
this.$store.dispatch('setCardDetailsInModal', true)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<AttachmentList
|
||||
:card-id="card.id"
|
||||
:removable="true"
|
||||
@delete-attachment="deleteAttachment"
|
||||
@restore-attachment="restoreAttachment" />
|
||||
@deleteAttachment="deleteAttachment"
|
||||
@restoreAttachment="restoreAttachment" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
:lang="lang"
|
||||
:formatter="format"
|
||||
:disabled="saving || !canEdit"
|
||||
:shortcuts="shortcuts"
|
||||
confirm />
|
||||
<Actions v-if="canEdit">
|
||||
<ActionButton v-if="copiedCard.duedate" icon="icon-delete" @click="removeDue()">
|
||||
@@ -177,53 +176,12 @@ export default {
|
||||
stringify: this.stringify,
|
||||
parse: this.parse,
|
||||
},
|
||||
shortcuts: [
|
||||
{
|
||||
text: t('deck', 'Today'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate())
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('deck', 'Tomorrow'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 1)
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('deck', 'Next week'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 7)
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
{
|
||||
text: t('deck', 'Next month'),
|
||||
onClick() {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 30)
|
||||
date.setHours(23)
|
||||
date.setMinutes(59)
|
||||
return date
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentBoard: state => state.currentBoard,
|
||||
cardDetailsInModal: state => state.cardDetailsInModal,
|
||||
}),
|
||||
...mapGetters(['canEdit', 'assignables']),
|
||||
formatedAssignables() {
|
||||
@@ -249,14 +207,6 @@ export default {
|
||||
return assignable
|
||||
})
|
||||
},
|
||||
cardDetailsInModal: {
|
||||
get() {
|
||||
return this.$store.getters.config('cardDetailsInModal')
|
||||
},
|
||||
set(newValue) {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
||||
},
|
||||
},
|
||||
duedate: {
|
||||
get() {
|
||||
return this.card.duedate ? new Date(this.card.duedate) : null
|
||||
@@ -366,14 +316,6 @@ export default {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.section-wrapper::v-deep .mx-datepicker-main.mx-datepicker-popup {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.section-wrapper::v-deep .mx-datepicker-main.mx-datepicker-popup.mx-datepicker-sidebar {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.section-wrapper {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -19,45 +19,47 @@
|
||||
</div>
|
||||
</div>
|
||||
<li v-else class="comment">
|
||||
<div class="comment--header">
|
||||
<Avatar :user="comment.actorId" />
|
||||
<span class="has-tooltip username">
|
||||
{{ comment.actorDisplayName }}
|
||||
</span>
|
||||
<Actions v-show="!edit" :force-menu="true">
|
||||
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
|
||||
{{ t('deck', 'Reply') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canEdit"
|
||||
icon="icon-rename"
|
||||
:close-after-click="true"
|
||||
@click="showUpdateForm()">
|
||||
{{ t('deck', 'Update') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canEdit"
|
||||
icon="icon-delete"
|
||||
:close-after-click="true"
|
||||
@click="deleteComment()">
|
||||
{{ t('deck', 'Delete') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="edit">
|
||||
<ActionButton icon="icon-close" @click="hideUpdateForm" />
|
||||
</Actions>
|
||||
<div class="spacer" />
|
||||
<div class="timestamp">
|
||||
{{ relativeDate(comment.creationDateTime) }}
|
||||
<template>
|
||||
<div class="comment--header">
|
||||
<Avatar :user="comment.actorId" />
|
||||
<span class="has-tooltip username">
|
||||
{{ comment.actorDisplayName }}
|
||||
</span>
|
||||
<Actions v-show="!edit" :force-menu="true">
|
||||
<ActionButton icon="icon-reply" :close-after-click="true" @click="replyTo()">
|
||||
{{ t('deck', 'Reply') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canEdit"
|
||||
icon="icon-rename"
|
||||
:close-after-click="true"
|
||||
@click="showUpdateForm()">
|
||||
{{ t('deck', 'Update') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canEdit"
|
||||
icon="icon-delete"
|
||||
:close-after-click="true"
|
||||
@click="deleteComment()">
|
||||
{{ t('deck', 'Delete') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="edit">
|
||||
<ActionButton icon="icon-close" @click="hideUpdateForm" />
|
||||
</Actions>
|
||||
<div class="spacer" />
|
||||
<div class="timestamp">
|
||||
{{ relativeDate(comment.creationDateTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CommentItem v-if="comment.replyTo" :reply="true" :comment="comment.replyTo" />
|
||||
<div v-show="!edit" ref="richTextElement">
|
||||
<RichText
|
||||
class="comment--content"
|
||||
:text="richText(comment)"
|
||||
:arguments="richArgs(comment)"
|
||||
:autolink="true" />
|
||||
</div>
|
||||
<CommentForm v-if="edit" v-model="commentMsg" @submit="updateComment" />
|
||||
<CommentItem v-if="comment.replyTo" :reply="true" :comment="comment.replyTo" />
|
||||
<div v-show="!edit" ref="richTextElement">
|
||||
<RichText
|
||||
class="comment--content"
|
||||
:text="richText(comment)"
|
||||
:arguments="richArgs(comment)"
|
||||
:autolink="true" />
|
||||
</div>
|
||||
<CommentForm v-if="edit" v-model="commentMsg" @submit="updateComment" />
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
ref="markdownEditor"
|
||||
v-model="description"
|
||||
:configs="mdeConfig"
|
||||
@update:modelValue="updateDescription"
|
||||
@input="updateDescription"
|
||||
@blur="saveDescription" />
|
||||
|
||||
<Modal v-if="modalShow" :title="t('deck', 'Choose attachment')" @close="modalShow=false">
|
||||
@@ -66,7 +66,7 @@
|
||||
<AttachmentList
|
||||
:card-id="card.id"
|
||||
:selectable="true"
|
||||
@select-attachment="addAttachment" />
|
||||
@selectAttachment="addAttachment" />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
@@ -75,7 +75,6 @@
|
||||
<script>
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import MarkdownItTaskLists from 'markdown-it-task-lists'
|
||||
import MarkdownItLinkAttributes from 'markdown-it-link-attributes'
|
||||
import AttachmentList from './AttachmentList'
|
||||
import { Actions, ActionButton, Modal } from '@nextcloud/vue'
|
||||
import { formatFileSize } from '@nextcloud/files'
|
||||
@@ -87,13 +86,6 @@ const markdownIt = new MarkdownIt({
|
||||
})
|
||||
markdownIt.use(MarkdownItTaskLists, { enabled: true, label: true, labelAfter: true })
|
||||
|
||||
markdownIt.use(MarkdownItLinkAttributes, {
|
||||
attrs: {
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'Description',
|
||||
components: {
|
||||
@@ -132,6 +124,7 @@ export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
currentBoard: state => state.currentBoard,
|
||||
cardDetailsInModal: state => state.cardDetailsInModal,
|
||||
}),
|
||||
...mapGetters(['canEdit']),
|
||||
attachments() {
|
||||
@@ -230,7 +223,7 @@ export default {
|
||||
updateDescription() {
|
||||
this.descriptionLastEdit = Date.now()
|
||||
clearTimeout(this.descriptionSaveTimeout)
|
||||
this.descriptionSaveTimeout = setTimeout(async () => {
|
||||
this.descriptionSaveTimeout = setTimeout(async() => {
|
||||
await this.saveDescription()
|
||||
}, 2500)
|
||||
},
|
||||
@@ -250,8 +243,6 @@ export default {
|
||||
|
||||
&::v-deep .attachment-list {
|
||||
flex-shrink: 1;
|
||||
overflow: scroll;
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +253,6 @@ export default {
|
||||
|
||||
#description-preview {
|
||||
min-height: 100px;
|
||||
width: auto;
|
||||
overflow-x: auto;
|
||||
|
||||
&::v-deep {
|
||||
@import './../../css/markdown';
|
||||
@@ -334,15 +323,4 @@ h5 {
|
||||
#app-sidebar .app-sidebar-header__desc h4 {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.vue-easymde .cm-s-easymde .cm-link {
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
.vue-easymde .cm-s-easymde .cm-string.cm-url,
|
||||
.vue-easymde .cm-s-easymde .cm-formatting.cm-link,
|
||||
.vue-easymde .cm-s-easymde .cm-formatting.cm-url,
|
||||
.vue-easymde .cm-s-easymde .cm-formatting.cm-image {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,19 +53,6 @@
|
||||
<PopoverMenu :menu="popover" />
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="avatar-print-list">
|
||||
<div v-for="user in avatarUsers" :key="user.id" class="avatar-print-list-item">
|
||||
<Avatar
|
||||
class="avatar-print-list-avatar"
|
||||
:user="user.participant.uid"
|
||||
:display-name="user.participant.displayname"
|
||||
:disable-menu="true"
|
||||
:is-no-user="user.type !== 0"
|
||||
:size="24" />
|
||||
{{ user.participant.displayname }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,15 +116,6 @@ export default {
|
||||
}),
|
||||
]
|
||||
},
|
||||
avatarUsers() {
|
||||
if (!this.users) {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.users.filter((user) => {
|
||||
return [0, 1, 7].includes(user.type)
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
togglePopover() {
|
||||
@@ -198,26 +176,4 @@ export default {
|
||||
display: block;
|
||||
margin: 40px -6px;
|
||||
}
|
||||
|
||||
.avatar-print-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.avatar-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.avatar-print-list-item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.avatar-print-list {
|
||||
display: block;
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<AvatarList :users="card.assignedUsers" />
|
||||
|
||||
<CardMenu class="card-menu" :card="card" />
|
||||
<CardMenu :card="card" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -150,15 +150,4 @@ export default {
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.badges {
|
||||
align-items: flex-start;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,11 +66,7 @@
|
||||
:placeholder="t('deck', 'Select a list')"
|
||||
:options="stacksFromBoard"
|
||||
:max-height="100"
|
||||
label="title">
|
||||
<span slot="noOptions">
|
||||
{{ t('deck', 'List is empty') }}
|
||||
</span>
|
||||
</Multiselect>
|
||||
label="title" />
|
||||
|
||||
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="moveCard">
|
||||
{{ t('deck', 'Move card') }}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<template>
|
||||
<div v-if="card" class="duedate">
|
||||
<transition name="zoom">
|
||||
<div v-if="card.duedate" :class="dueIcon" :title="absoluteDate">
|
||||
<div v-if="card.duedate" :class="dueIcon">
|
||||
<span>{{ relativeDate }}</span>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -62,14 +62,14 @@ export default {
|
||||
}
|
||||
return moment(this.card.duedate).fromNow()
|
||||
},
|
||||
absoluteDate() {
|
||||
return moment(this.card.duedate).format('L')
|
||||
dueDateTooltip() {
|
||||
return moment(this.card.duedate).format('LLLL')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" coped>
|
||||
.icon.due {
|
||||
background-position: 4px center;
|
||||
border-radius: 3px;
|
||||
@@ -105,7 +105,6 @@ export default {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
&::before,
|
||||
span {
|
||||
margin-left: 20px;
|
||||
white-space: nowrap;
|
||||
@@ -113,18 +112,4 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.icon.due {
|
||||
background-color: transparent !important;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: attr(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,10 +144,10 @@ export default {
|
||||
},
|
||||
cardDetailsInModal: {
|
||||
get() {
|
||||
return this.$store.getters.config('cardDetailsInModal')
|
||||
return this.$store.getters.cardDetailsInModal
|
||||
},
|
||||
set(newValue) {
|
||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
||||
this.$store.dispatch('setCardDetailsInModal', newValue)
|
||||
},
|
||||
},
|
||||
configCalendar: {
|
||||
|
||||
@@ -39,9 +39,6 @@
|
||||
<script>
|
||||
import { ColorPicker, ActionButton, Actions, AppNavigationItem } from '@nextcloud/vue'
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function randomColor() {
|
||||
let randomHexColor = ((1 << 24) * Math.random() | 0).toString(16)
|
||||
while (randomHexColor.length < 6) {
|
||||
|
||||
@@ -125,10 +125,7 @@
|
||||
<div :style="{ backgroundColor: getColor }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
|
||||
</ColorPicker>
|
||||
<form @submit.prevent.stop="applyEdit">
|
||||
<input v-model="editTitle"
|
||||
v-focus
|
||||
type="text"
|
||||
required>
|
||||
<input v-model="editTitle" type="text" required>
|
||||
<input type="submit" value="" class="icon-confirm">
|
||||
<Actions><ActionButton icon="icon-close" @click.stop.prevent="cancelEdit" /></Actions>
|
||||
</form>
|
||||
|
||||
@@ -63,15 +63,10 @@ import { Actions, ActionButton } from '@nextcloud/vue'
|
||||
|
||||
const createCancelToken = () => axios.CancelToken.source()
|
||||
|
||||
/**
|
||||
* @param root0
|
||||
* @param root0.query
|
||||
* @param root0.cursor
|
||||
*/
|
||||
function search({ query, cursor }) {
|
||||
const cancelToken = createCancelToken()
|
||||
|
||||
const request = async () => axios.get(generateOcsUrl('apps/deck/api/v1.0/search'), {
|
||||
const request = async() => axios.get(generateOcsUrl('apps/deck/api/v1.0/search'), {
|
||||
cancelToken: cancelToken.token,
|
||||
params: {
|
||||
term: query,
|
||||
|
||||
@@ -21,11 +21,6 @@ ul {
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
|
||||
.task-list-item {
|
||||
margin-left: -20px;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -72,7 +67,7 @@ img {
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
margin: 0px 3px 0px 0px;
|
||||
margin: 0px 10px 0px 0px;
|
||||
line-height: 10px;
|
||||
font-size: 10px;
|
||||
display: inline-block;
|
||||
|
||||
@@ -25,8 +25,9 @@ const buildSelector = (selector, propsData = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const container = document.createElement('div')
|
||||
document.getElementById('body-user').append(container)
|
||||
const ComponentVM = new Vue({
|
||||
render: (h) => h(selector, propsData),
|
||||
const View = Vue.extend(selector)
|
||||
const ComponentVM = new View({
|
||||
propsData,
|
||||
}).$mount(container)
|
||||
ComponentVM.$root.$on('close', () => {
|
||||
ComponentVM.$el.remove()
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
import BoardSelector from './BoardSelector'
|
||||
import CardSelector from './CardSelector'
|
||||
import './../css/collections.css'
|
||||
import FileSharingPicker from './views/FileSharingPicker'
|
||||
import { buildSelector } from './helpers/selector'
|
||||
@@ -43,19 +45,13 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
window.OCP.Collaboration.registerType('deck', {
|
||||
action: () => {
|
||||
const BoardSelector = () => import('./BoardSelector')
|
||||
buildSelector(BoardSelector)
|
||||
},
|
||||
action: () => buildSelector(BoardSelector),
|
||||
typeString: t('deck', 'Link to a board'),
|
||||
typeIconClass: 'icon-deck',
|
||||
})
|
||||
|
||||
window.OCP.Collaboration.registerType('deck-card', {
|
||||
action: () => {
|
||||
const CardSelector = () => import('./CardSelector')
|
||||
buildSelector(CardSelector)
|
||||
},
|
||||
action: () => buildSelector(CardSelector),
|
||||
typeString: t('deck', 'Link to a card'),
|
||||
typeIconClass: 'icon-deck',
|
||||
})
|
||||
|
||||
@@ -49,15 +49,10 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
const shortenedMessage = shortenedMessageCandidate === '' ? message.substr(0, 255) : shortenedMessageCandidate
|
||||
try {
|
||||
await buildSelector(CardCreateDialog, {
|
||||
props: {
|
||||
title: shortenedMessage,
|
||||
description: message + '\n\n' + '['
|
||||
+ t('deck', 'Message from {author} in {conversationName}', {
|
||||
author: actorDisplayName,
|
||||
conversationName,
|
||||
})
|
||||
+ '](' + window.location.protocol + '//' + window.location.host + generateUrl('/call/' + conversationToken) + ')',
|
||||
},
|
||||
title: shortenedMessage,
|
||||
description: message + '\n\n' + '['
|
||||
+ t('deck', 'Message from {author} in {conversationName}', { author: actorDisplayName, conversationName })
|
||||
+ '](' + generateUrl('/call/' + conversationToken) + ')',
|
||||
})
|
||||
} catch (e) {
|
||||
console.debug('Card creation dialog was canceled')
|
||||
|
||||
@@ -109,7 +109,7 @@ if (!window.OCA.Deck) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} CardRichObject
|
||||
* @typedef {Object} CardRichObject
|
||||
* @property {string} id
|
||||
* @property {string} name
|
||||
* @property {string} boardname
|
||||
@@ -124,14 +124,13 @@ if (!window.OCA.Deck) {
|
||||
|
||||
/**
|
||||
* Frontend message API for adding actions to talk messages.
|
||||
*
|
||||
* @param {*} Object the wrapping object.
|
||||
* @param {string} label the action label.
|
||||
* @param {String} label the action label.
|
||||
* @param {registerActionCallback} callback the callback function. This function will receive
|
||||
* the card as a parameter and be triggered by a click on the
|
||||
* action. The card parameter will be of the format of a rich object string
|
||||
* type "deck-card"
|
||||
* @param {string} icon the action label. E.g. "icon-reply"
|
||||
* @param {String} icon the action label. E.g. "icon-reply"
|
||||
*/
|
||||
window.OCA.Deck.registerCardAction = ({ label, callback, icon }) => {
|
||||
const cardAction = {
|
||||
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
bodyFormData.append('cardId', this.cardId)
|
||||
bodyFormData.append('type', type)
|
||||
bodyFormData.append('file', file)
|
||||
await queue.add(async () => {
|
||||
await queue.add(async() => {
|
||||
try {
|
||||
await this.$store.dispatch('createAttachment', {
|
||||
cardId: this.cardId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
/**
|
||||
* Board model
|
||||
*
|
||||
* @typedef {object} Board
|
||||
* @property {string} title
|
||||
* @typedef {Object} Board
|
||||
* @property {String} title
|
||||
* @property {boolean} archived
|
||||
* @property {number} shared 1 (shared) or 0 (not shared)
|
||||
*/
|
||||
@@ -32,8 +32,8 @@
|
||||
/**
|
||||
* Stack model
|
||||
*
|
||||
* @typedef {object} Stack
|
||||
* @property {string} title
|
||||
* @typedef {Object} Stack
|
||||
* @property {String} title
|
||||
* @property {number} boardId
|
||||
* @property {number} order
|
||||
*/
|
||||
@@ -41,15 +41,15 @@
|
||||
/**
|
||||
* Card model
|
||||
*
|
||||
* @typedef {object} Card
|
||||
* @property {string} title
|
||||
* @typedef {Object} Card
|
||||
* @property {String} title
|
||||
* @property {boolean} archived
|
||||
* @property {number} order
|
||||
*/
|
||||
/**
|
||||
* Label model
|
||||
*
|
||||
* @typedef {object} Label
|
||||
* @property {string} title
|
||||
* @property {string} color
|
||||
* @typedef {Object} Label
|
||||
* @property {String} title
|
||||
* @property {String} color
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,7 @@ export class BoardApi {
|
||||
* Updates a board.
|
||||
*
|
||||
* @param {Board} board the board object to update
|
||||
* @return {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateBoard(board) {
|
||||
return axios.put(this.url(`/boards/${board.id}`), board)
|
||||
@@ -57,13 +57,13 @@ export class BoardApi {
|
||||
|
||||
/**
|
||||
* Creates a new board.
|
||||
*
|
||||
* @typedef {object} BoardCreateObject
|
||||
* @typedef {Object} BoardCreateObject
|
||||
* @property {string} title
|
||||
* @property {string} color
|
||||
*
|
||||
* @param {BoardCreateObject} boardData The board data to send.
|
||||
* color the hexadecimal color value formated /[0-9A-F]{6}/i
|
||||
* @return {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createBoard(boardData) {
|
||||
return axios.post(this.url('/boards'), boardData)
|
||||
|
||||
@@ -78,7 +78,7 @@ export class StackApi {
|
||||
|
||||
/**
|
||||
* @param {Stack} stack stack object to create
|
||||
* @return {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createStack(stack) {
|
||||
return axios.post(this.url('/stacks'), stack)
|
||||
|
||||
@@ -62,6 +62,7 @@ export default new Vuex.Store({
|
||||
showArchived: false,
|
||||
navShown: localStorage.getItem('deck.navShown') === 'true',
|
||||
compactMode: localStorage.getItem('deck.compactMode') === 'true',
|
||||
cardDetailsInModal: localStorage.getItem('deck.cardDetailsInModal') === 'true',
|
||||
sidebarShown: false,
|
||||
currentBoard: null,
|
||||
currentCard: null,
|
||||
@@ -78,6 +79,9 @@ export default new Vuex.Store({
|
||||
config: state => (key) => {
|
||||
return state.config[key]
|
||||
},
|
||||
cardDetailsInModal: state => {
|
||||
return state.cardDetailsInModal
|
||||
},
|
||||
getSearchQuery: state => {
|
||||
return state.searchQuery
|
||||
},
|
||||
@@ -229,6 +233,10 @@ export default new Vuex.Store({
|
||||
state.compactMode = !state.compactMode
|
||||
localStorage.setItem('deck.compactMode', state.compactMode)
|
||||
},
|
||||
setCardDetailsInModal(state) {
|
||||
state.cardDetailsInModal = !state.cardDetailsInModal
|
||||
localStorage.setItem('deck.cardDetailsInModal', state.cardDetailsInModal)
|
||||
},
|
||||
setBoards(state, boards) {
|
||||
state.boards = boards
|
||||
},
|
||||
@@ -345,7 +353,6 @@ export default new Vuex.Store({
|
||||
},
|
||||
|
||||
/**
|
||||
* @param commit.commit
|
||||
* @param commit
|
||||
* @param state
|
||||
* @param {Board} board
|
||||
@@ -359,7 +366,6 @@ export default new Vuex.Store({
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @param commit.commit
|
||||
* @param commit
|
||||
* @param state
|
||||
* @param {Board} board
|
||||
@@ -375,10 +381,9 @@ export default new Vuex.Store({
|
||||
/**
|
||||
* Updates a board API side.
|
||||
*
|
||||
* @param commit.commit
|
||||
* @param commit
|
||||
* @param board The board to update.
|
||||
* @return {Promise<void>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateBoard({ commit }, board) {
|
||||
const storedBoard = await apiClient.updateBoard(board)
|
||||
@@ -433,6 +438,9 @@ export default new Vuex.Store({
|
||||
toggleCompactMode({ commit }) {
|
||||
commit('toggleCompactMode')
|
||||
},
|
||||
setCardDetailsInModal({ commit }, show) {
|
||||
commit('setCardDetailsInModal', show)
|
||||
},
|
||||
setCurrentBoard({ commit }, board) {
|
||||
commit('setCurrentBoard', board)
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
import CardSelector from '../CardSelector'
|
||||
import { createShare } from '../services/SharingApi'
|
||||
|
||||
export default {
|
||||
@@ -33,12 +34,12 @@ export default {
|
||||
container.id = 'deck-board-select'
|
||||
const body = document.getElementById('body-user')
|
||||
body.append(container)
|
||||
const CardSelector = () => import('./../CardSelector')
|
||||
const ComponentVM = new Vue({
|
||||
render: (h) => h(CardSelector, {
|
||||
const SelectorView = Vue.extend(CardSelector)
|
||||
const ComponentVM = new SelectorView({
|
||||
propsData: {
|
||||
title: t('deck', 'Share {file} with a Deck card', { file: decodeURIComponent(self.fileInfo.name) }),
|
||||
action: t('deck', 'Share'),
|
||||
}),
|
||||
},
|
||||
})
|
||||
ComponentVM.$mount(container)
|
||||
ComponentVM.$root.$on('close', () => {
|
||||
@@ -46,7 +47,7 @@ export default {
|
||||
ComponentVM.$destroy()
|
||||
reject(new Error('Canceled'))
|
||||
})
|
||||
ComponentVM.$root.$on('select', async (id) => {
|
||||
ComponentVM.$root.$on('select', async(id) => {
|
||||
const result = await createShare({
|
||||
path: self.fileInfo.path + '/' + self.fileInfo.name,
|
||||
shareType: 12,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"owner": "admin",
|
||||
"color": "0800fd",
|
||||
"uidRelation": {
|
||||
"johndoe": "johndoe"
|
||||
}
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
{
|
||||
"id": "fakeboardidhash",
|
||||
"name": "Test Board Name",
|
||||
"desc": "",
|
||||
"descData": null,
|
||||
"closed": false,
|
||||
"dateClosed": null,
|
||||
"idOrganization": null,
|
||||
"shortLink": "qwerty",
|
||||
"powerUps": [],
|
||||
"dateLastActivity": "2021-07-10T17:01:58.633Z",
|
||||
"idTags": [],
|
||||
"datePluginDisable": null,
|
||||
"creationMethod": null,
|
||||
"idBoardSource": null,
|
||||
"idMemberCreator": "fakeidmemberhash",
|
||||
"idEnterprise": null,
|
||||
"pinned": false,
|
||||
"starred": false,
|
||||
"url": "https://trello.com/b/qwerty/fakeboardurl",
|
||||
"prefs": {
|
||||
"permissionLevel": "private",
|
||||
"hideVotes": false,
|
||||
"voting": "disabled",
|
||||
"comments": "members",
|
||||
"invitations": "members",
|
||||
"selfJoin": false,
|
||||
"cardCovers": true,
|
||||
"isTemplate": false,
|
||||
"cardAging": "regular",
|
||||
"calendarFeedEnabled": false,
|
||||
"background": "blue",
|
||||
"backgroundImage": null,
|
||||
"backgroundImageScaled": null,
|
||||
"backgroundTile": false,
|
||||
"backgroundBrightness": "dark",
|
||||
"backgroundColor": "#0079BF",
|
||||
"backgroundBottomColor": "#0079BF",
|
||||
"backgroundTopColor": "#0079BF",
|
||||
"canBePublic": true,
|
||||
"canBeEnterprise": true,
|
||||
"canBeOrg": true,
|
||||
"canBePrivate": true,
|
||||
"canInvite": true
|
||||
},
|
||||
"shortUrl": "https://trello.com/b/qwerty",
|
||||
"premiumFeatures": [],
|
||||
"enterpriseOwned": false,
|
||||
"ixUpdate": "67",
|
||||
"limits": {
|
||||
"attachments": {
|
||||
"perBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 36000,
|
||||
"warnAt": 32400
|
||||
},
|
||||
"perCard": {
|
||||
"status": "ok",
|
||||
"disableAt": 1000,
|
||||
"warnAt": 900
|
||||
}
|
||||
},
|
||||
"boards": {
|
||||
"totalMembersPerBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 1600,
|
||||
"warnAt": 1440
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
"openPerBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 5000,
|
||||
"warnAt": 4500
|
||||
},
|
||||
"openPerList": {
|
||||
"status": "ok",
|
||||
"disableAt": 5000,
|
||||
"warnAt": 4500
|
||||
},
|
||||
"totalPerBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 2000000,
|
||||
"warnAt": 1800000
|
||||
},
|
||||
"totalPerList": {
|
||||
"status": "ok",
|
||||
"disableAt": 1000000,
|
||||
"warnAt": 900000
|
||||
}
|
||||
},
|
||||
"checklists": {
|
||||
"perBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 2000000,
|
||||
"warnAt": 1800000
|
||||
},
|
||||
"perCard": {
|
||||
"status": "ok",
|
||||
"disableAt": 500,
|
||||
"warnAt": 450
|
||||
}
|
||||
},
|
||||
"checkItems": {
|
||||
"perChecklist": {
|
||||
"status": "ok",
|
||||
"disableAt": 200,
|
||||
"warnAt": 180
|
||||
}
|
||||
},
|
||||
"customFields": {
|
||||
"perBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 50,
|
||||
"warnAt": 45
|
||||
}
|
||||
},
|
||||
"customFieldOptions": {
|
||||
"perField": {
|
||||
"status": "ok",
|
||||
"disableAt": 50,
|
||||
"warnAt": 45
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"perBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 1000,
|
||||
"warnAt": 900
|
||||
}
|
||||
},
|
||||
"lists": {
|
||||
"openPerBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 500,
|
||||
"warnAt": 450
|
||||
},
|
||||
"totalPerBoard": {
|
||||
"status": "ok",
|
||||
"disableAt": 3000,
|
||||
"warnAt": 2700
|
||||
}
|
||||
},
|
||||
"stickers": {
|
||||
"perCard": {
|
||||
"status": "ok",
|
||||
"disableAt": 70,
|
||||
"warnAt": 63
|
||||
}
|
||||
},
|
||||
"reactions": {
|
||||
"perAction": {
|
||||
"status": "ok",
|
||||
"disableAt": 1000,
|
||||
"warnAt": 900
|
||||
},
|
||||
"uniquePerAction": {
|
||||
"status": "ok",
|
||||
"disableAt": 17,
|
||||
"warnAt": 16
|
||||
}
|
||||
}
|
||||
},
|
||||
"subscribed": false,
|
||||
"templateGallery": null,
|
||||
"dateLastView": "2021-07-10T17:01:58.665Z",
|
||||
"labelNames": {
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"red": "",
|
||||
"purple": "",
|
||||
"blue": "",
|
||||
"sky": "",
|
||||
"lime": "",
|
||||
"pink": "",
|
||||
"black": ""
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"id": "60e9d2869efe2e1141be2798",
|
||||
"idMemberCreator": "fakeidmemberhash",
|
||||
"data": {
|
||||
"idMember": "fakeidmemberhash",
|
||||
"deactivated": false,
|
||||
"card": {
|
||||
"id": "hashcard7",
|
||||
"name": "Name Card 7",
|
||||
"idShort": 7,
|
||||
"shortLink": "fakeshortlinkcard7"
|
||||
},
|
||||
"board": {
|
||||
"id": "fakeboardidhash",
|
||||
"name": "Test Board Name",
|
||||
"shortLink": "qwerty"
|
||||
},
|
||||
"member": {
|
||||
"id": "fakeidmemberhash",
|
||||
"name": "John Doe"
|
||||
}
|
||||
},
|
||||
"type": "removeMemberFromCard",
|
||||
"date": "2021-07-10T17:01:58.636Z",
|
||||
"appCreator": null,
|
||||
"limits": {},
|
||||
"member": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
},
|
||||
"memberCreator": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "60e9d1832ff82d10c0cea6ba",
|
||||
"idMemberCreator": "fakeidmemberhash",
|
||||
"data": {
|
||||
"idMember": "fakeidmemberhash",
|
||||
"card": {
|
||||
"id": "hashcard7",
|
||||
"name": "Name Card 7",
|
||||
"idShort": 7,
|
||||
"shortLink": "fakeshortlinkcard7"
|
||||
},
|
||||
"board": {
|
||||
"id": "fakeboardidhash",
|
||||
"name": "Test Board Name",
|
||||
"shortLink": "qwerty"
|
||||
},
|
||||
"member": {
|
||||
"id": "fakeidmemberhash",
|
||||
"name": "John Doe"
|
||||
}
|
||||
},
|
||||
"type": "addMemberToCard",
|
||||
"date": "2021-07-10T16:57:39.999Z",
|
||||
"appCreator": null,
|
||||
"limits": {},
|
||||
"member": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
},
|
||||
"memberCreator": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "59bbfc4bf36aa0270d6bfd43",
|
||||
"idMemberCreator": "fakeidmemberhash",
|
||||
"data": {
|
||||
"board": {
|
||||
"shortLink": "qwerty",
|
||||
"name": "Test Board Name",
|
||||
"id": "fakeboardidhash"
|
||||
},
|
||||
"list": {
|
||||
"name": "TODO",
|
||||
"id": "hashlisttodo"
|
||||
},
|
||||
"card": {
|
||||
"shortLink": "fakeshortlinkcard7",
|
||||
"idShort": 7,
|
||||
"name": "Name Card 7",
|
||||
"id": "hashcard7"
|
||||
}
|
||||
},
|
||||
"type": "createCard",
|
||||
"date": "2017-09-15T16:14:03.187Z",
|
||||
"appCreator": null,
|
||||
"limits": {},
|
||||
"memberCreator": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "59bbfb8e4a6f8ca35be9b82a",
|
||||
"idMemberCreator": "fakeidmemberhash",
|
||||
"data": {
|
||||
"board": {
|
||||
"shortLink": "qwerty",
|
||||
"name": "Test Board Name",
|
||||
"id": "fakeboardidhash"
|
||||
},
|
||||
"list": {
|
||||
"name": "TODO",
|
||||
"id": "hashlisttodo"
|
||||
}
|
||||
},
|
||||
"type": "createList",
|
||||
"date": "2017-09-15T16:10:54.714Z",
|
||||
"appCreator": null,
|
||||
"limits": {},
|
||||
"memberCreator": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "59bbfb88973b76e586edec5e",
|
||||
"idMemberCreator": "fakeidmemberhash",
|
||||
"data": {
|
||||
"board": {
|
||||
"shortLink": "qwerty",
|
||||
"name": "Test Board Name",
|
||||
"id": "fakeboardidhash"
|
||||
}
|
||||
},
|
||||
"type": "createBoard",
|
||||
"date": "2017-09-15T16:10:48.069Z",
|
||||
"appCreator": null,
|
||||
"limits": {},
|
||||
"memberCreator": {
|
||||
"id": "fakeidmemberhash",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idMemberReferrer": null,
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"id": "hashcard7",
|
||||
"address": null,
|
||||
"checkItemStates": null,
|
||||
"closed": false,
|
||||
"coordinates": null,
|
||||
"creationMethod": null,
|
||||
"dateLastActivity": "2021-07-10T17:01:58.633Z",
|
||||
"desc": "",
|
||||
"descData": null,
|
||||
"dueReminder": null,
|
||||
"idBoard": "fakeboardidhash",
|
||||
"idLabels": [],
|
||||
"idList": "hashlisttodo",
|
||||
"idMembersVoted": [],
|
||||
"idShort": 7,
|
||||
"idAttachmentCover": null,
|
||||
"locationName": null,
|
||||
"manualCoverAttachment": false,
|
||||
"name": "Name Card 7",
|
||||
"pos": 65535,
|
||||
"shortLink": "fakeshortlinkcard7",
|
||||
"isTemplate": false,
|
||||
"cardRole": null,
|
||||
"badges": {
|
||||
"attachmentsByType": {
|
||||
"trello": {
|
||||
"board": 0,
|
||||
"card": 0
|
||||
}
|
||||
},
|
||||
"location": false,
|
||||
"votes": 0,
|
||||
"viewingMemberVoted": false,
|
||||
"subscribed": false,
|
||||
"fogbugz": "",
|
||||
"checkItems": 0,
|
||||
"checkItemsChecked": 0,
|
||||
"checkItemsEarliestDue": null,
|
||||
"comments": 0,
|
||||
"attachments": 0,
|
||||
"description": false,
|
||||
"due": null,
|
||||
"dueComplete": false,
|
||||
"start": null
|
||||
},
|
||||
"dueComplete": false,
|
||||
"due": null,
|
||||
"email": "johndoe+card7@boards.trello.com",
|
||||
"idChecklists": [],
|
||||
"idMembers": [],
|
||||
"labels": [],
|
||||
"limits": {
|
||||
"attachments": {
|
||||
"perCard": {
|
||||
"status": "ok",
|
||||
"disableAt": 1000,
|
||||
"warnAt": 900
|
||||
}
|
||||
},
|
||||
"checklists": {
|
||||
"perCard": {
|
||||
"status": "ok",
|
||||
"disableAt": 500,
|
||||
"warnAt": 450
|
||||
}
|
||||
},
|
||||
"stickers": {
|
||||
"perCard": {
|
||||
"status": "ok",
|
||||
"disableAt": 70,
|
||||
"warnAt": 63
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortUrl": "https://trello.com/c/fakeshortlinkcard7",
|
||||
"start": null,
|
||||
"subscribed": false,
|
||||
"url": "https://trello.com/c/fakeshortlinkcard7/7-name-card-7",
|
||||
"cover": {
|
||||
"idAttachment": null,
|
||||
"color": null,
|
||||
"idUploadedBackground": null,
|
||||
"size": "normal",
|
||||
"brightness": "dark",
|
||||
"idPlugin": null
|
||||
},
|
||||
"attachments": [],
|
||||
"pluginData": [],
|
||||
"customFieldItems": []
|
||||
}
|
||||
],
|
||||
"labels": [
|
||||
{
|
||||
"id": "59bbfb881314a339999eb855",
|
||||
"idBoard": "fakeboardidhash",
|
||||
"name": "",
|
||||
"color": "yellow"
|
||||
}
|
||||
],
|
||||
"lists": [
|
||||
{
|
||||
"id": "hashlisttodo",
|
||||
"name": "TODO",
|
||||
"closed": false,
|
||||
"pos": 65535,
|
||||
"softLimit": null,
|
||||
"creationMethod": null,
|
||||
"idBoard": "fakeboardidhash",
|
||||
"limits": {
|
||||
"cards": {
|
||||
"openPerList": {
|
||||
"status": "ok",
|
||||
"disableAt": 5000,
|
||||
"warnAt": 4500
|
||||
},
|
||||
"totalPerList": {
|
||||
"status": "ok",
|
||||
"disableAt": 1000000,
|
||||
"warnAt": 900000
|
||||
}
|
||||
}
|
||||
},
|
||||
"subscribed": false
|
||||
}
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"id": "fakeidmemberhash",
|
||||
"bio": "",
|
||||
"bioData": {
|
||||
"emoji": {}
|
||||
},
|
||||
"confirmed": true,
|
||||
"memberType": "normal",
|
||||
"username": "johndoe",
|
||||
"activityBlocked": false,
|
||||
"avatarHash": "fakeavatarhash",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"fullName": "John Doe",
|
||||
"idEnterprise": null,
|
||||
"idEnterprisesDeactivated": [],
|
||||
"idMemberReferrer": null,
|
||||
"idPremOrgsAdmin": [],
|
||||
"initials": "JD",
|
||||
"nonPublic": {
|
||||
"fullName": "John Doe",
|
||||
"initials": "JD",
|
||||
"avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash",
|
||||
"avatarHash": "fakeavatarhash"
|
||||
},
|
||||
"nonPublicAvailable": true,
|
||||
"products": [],
|
||||
"url": "https://trello.com/johndoe",
|
||||
"status": "disconnected"
|
||||
}
|
||||
],
|
||||
"checklists": [],
|
||||
"customFields": [],
|
||||
"memberships": [
|
||||
{
|
||||
"id": "59bbfb88973b76e586edec5d",
|
||||
"idMember": "fakeidmemberhash",
|
||||
"memberType": "admin",
|
||||
"unconfirmed": false,
|
||||
"deactivated": false
|
||||
}
|
||||
],
|
||||
"pluginData": []
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class BoardDatabaseTest extends \Test\TestCase {
|
||||
$board->setOwner(self::TEST_USER1);
|
||||
$board->setColor('000000');
|
||||
$board->setLabels([]);
|
||||
$created = $this->boardService->create('Test', self::TEST_USER1, '000000');
|
||||
$created = $this->boardService->create('Test', self::TEST_USER1, '000000');
|
||||
$id = $created->getId();
|
||||
$actual = $this->boardService->find($id);
|
||||
$this->assertEquals($actual->getTitle(), $board->getTitle());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="4.16.1@aa7e400908833b10c0333861f86cd48c510b60eb">
|
||||
<files psalm-version="4.7.3@38c452ae584467e939d55377aaf83b5a26f19dd1">
|
||||
<file src="lib/Activity/ActivityManager.php">
|
||||
<TypeDoesNotContainType occurrences="1">
|
||||
<code>$message !== null</code>
|
||||
@@ -10,17 +10,6 @@
|
||||
<code>(int)$subjectParams['comment']</code>
|
||||
</InvalidScalarArgument>
|
||||
</file>
|
||||
<file src="lib/AppInfo/Application.php">
|
||||
<InvalidArgument occurrences="7">
|
||||
<code>registerEventListener</code>
|
||||
<code>registerEventListener</code>
|
||||
<code>registerEventListener</code>
|
||||
<code>registerEventListener</code>
|
||||
<code>registerEventListener</code>
|
||||
<code>registerEventListener</code>
|
||||
<code>registerEventListener</code>
|
||||
</InvalidArgument>
|
||||
</file>
|
||||
<file src="lib/Command/UserExport.php">
|
||||
<ImplementedReturnTypeMismatch occurrences="1">
|
||||
<code>void</code>
|
||||
@@ -116,14 +105,6 @@
|
||||
<ParamNameMismatch occurrences="1">
|
||||
<code>$boardId</code>
|
||||
</ParamNameMismatch>
|
||||
<TypeDoesNotContainType occurrences="6">
|
||||
<code>$limit !== null</code>
|
||||
<code>$limit !== null</code>
|
||||
<code>$limit !== null</code>
|
||||
<code>$offset !== null</code>
|
||||
<code>$offset !== null</code>
|
||||
<code>$offset !== null</code>
|
||||
</TypeDoesNotContainType>
|
||||
</file>
|
||||
<file src="lib/Db/Card.php">
|
||||
<UndefinedClass occurrences="2">
|
||||
@@ -132,6 +113,7 @@
|
||||
</UndefinedClass>
|
||||
</file>
|
||||
<file src="lib/Db/CardMapper.php">
|
||||
<InvalidArgument occurrences="1"/>
|
||||
<InvalidScalarArgument occurrences="1">
|
||||
<code>$entity->getId()</code>
|
||||
</InvalidScalarArgument>
|
||||
@@ -269,10 +251,11 @@
|
||||
</RedundantCondition>
|
||||
</file>
|
||||
<file src="lib/Service/FilesAppService.php">
|
||||
<MissingDependency occurrences="3">
|
||||
<MissingDependency occurrences="4">
|
||||
<code>$this->rootFolder</code>
|
||||
<code>$this->rootFolder</code>
|
||||
<code>IRootFolder</code>
|
||||
<code>ShareNotFound</code>
|
||||
</MissingDependency>
|
||||
</file>
|
||||
<file src="lib/Service/PermissionService.php">
|
||||
@@ -286,6 +269,27 @@
|
||||
<code>BadRquestException</code>
|
||||
</UndefinedClass>
|
||||
</file>
|
||||
<file src="lib/Sharing/DeckShareProvider.php">
|
||||
<InvalidReturnStatement occurrences="1">
|
||||
<code>$shares</code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType occurrences="1">
|
||||
<code>getSharesInFolder</code>
|
||||
</InvalidReturnType>
|
||||
<InvalidThrow occurrences="1">
|
||||
<code>throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this card'), 403);</code>
|
||||
</InvalidThrow>
|
||||
<MissingDependency occurrences="8">
|
||||
<code>GenericShareException</code>
|
||||
<code>GenericShareException</code>
|
||||
<code>ShareNotFound</code>
|
||||
<code>ShareNotFound</code>
|
||||
<code>ShareNotFound</code>
|
||||
<code>ShareNotFound</code>
|
||||
<code>ShareNotFound</code>
|
||||
<code>ShareNotFound</code>
|
||||
</MissingDependency>
|
||||
</file>
|
||||
<file src="lib/Sharing/Listener.php">
|
||||
<InvalidArgument occurrences="1">
|
||||
<code>[self::class, 'listenPreShare']</code>
|
||||
|
||||
@@ -38,6 +38,7 @@ use OCP\L10N\IFactory;
|
||||
use OCP\RichObjectStrings\IValidator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit_Framework_MockObject_MockObject as MockObject;
|
||||
use OCA\Deck\Service\CardService;
|
||||
|
||||
class DeckProviderTest extends TestCase {
|
||||
|
||||
@@ -56,6 +57,9 @@ class DeckProviderTest extends TestCase {
|
||||
/** @var ICommentsManager|MockObject */
|
||||
private $commentsManager;
|
||||
|
||||
/** @var CardService|MockObject */
|
||||
private $cardService;
|
||||
|
||||
/** @var string */
|
||||
private $userId = 'admin';
|
||||
|
||||
@@ -67,7 +71,9 @@ class DeckProviderTest extends TestCase {
|
||||
$this->commentsManager = $this->createMock(ICommentsManager::class);
|
||||
$this->l10nFactory = $this->createMock(IFactory::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->provider = new DeckProvider($this->urlGenerator, $this->activityManager, $this->userManager, $this->commentsManager, $this->l10nFactory, $this->config, $this->userId);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->cardService = $this->createMock(CardService::class);
|
||||
$this->provider = new DeckProvider($this->urlGenerator, $this->activityManager, $this->userManager, $this->commentsManager, $this->l10nFactory, $this->config, $this->userId, $this->cardService);
|
||||
}
|
||||
|
||||
private function mockEvent($objectType, $objectId, $objectName, $subject, $subjectParameters = []) {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio>
|
||||
*
|
||||
* @author Vitor Mattos <vitor@php.rio>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Deck\Command;
|
||||
|
||||
use OCA\Deck\Service\Importer\BoardImportCommandService;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class BoardImportTest extends \Test\TestCase {
|
||||
/** @var BoardImportCommandService */
|
||||
private $boardImportCommandService;
|
||||
/** @var BoardImport */
|
||||
private $boardImport;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->boardImportCommandService = $this->createMock(BoardImportCommandService::class);
|
||||
$this->boardImport = new BoardImport(
|
||||
$this->boardImportCommandService
|
||||
);
|
||||
$questionHelper = new QuestionHelper();
|
||||
$this->boardImport->setHelperSet(
|
||||
new HelperSet([
|
||||
$questionHelper
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public function testExecuteWithSuccess() {
|
||||
$input = $this->createMock(InputInterface::class);
|
||||
$input
|
||||
->method('getOption')
|
||||
->withConsecutive(
|
||||
['system'],
|
||||
['config']
|
||||
)
|
||||
->will($this->returnValueMap([
|
||||
['system', 'trelloJson'],
|
||||
['config', null]
|
||||
]));
|
||||
|
||||
$output = $this->createMock(OutputInterface::class);
|
||||
|
||||
$output
|
||||
->expects($this->once())
|
||||
->method('writeLn')
|
||||
->with('Done!');
|
||||
|
||||
$actual = $this->invokePrivate($this->boardImport, 'interact', [$input, $output]);
|
||||
$this->assertNull($actual);
|
||||
$actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]);
|
||||
$this->assertEquals(0, $actual);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,10 @@ class AclMapperTest extends MapperTestUtility {
|
||||
$this->boardMapper->insert($this->getBoard('MyBoard 3', 'user3'))
|
||||
];
|
||||
$this->acls = [
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[1]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user2', true, false, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user3', true, true, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[2]->getId()))
|
||||
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[1]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user','user2', true, false, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user','user3', true, true, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[2]->getId()))
|
||||
];
|
||||
|
||||
foreach ($this->acls as $acl) {
|
||||
|
||||
@@ -76,10 +76,10 @@ class BoardMapperTest extends MapperTestUtility {
|
||||
$this->boardMapper->insert($this->getBoard('MyBoard 3', 'user3'))
|
||||
];
|
||||
$this->acls = [
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[1]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user2', true, false, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user3', true, true, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user', 'user1', false, false, false, $this->boards[2]->getId()))
|
||||
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[1]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user','user2', true, false, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user','user3', true, true, false, $this->boards[0]->getId())),
|
||||
$this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[2]->getId()))
|
||||
];
|
||||
|
||||
foreach ($this->acls as $acl) {
|
||||
@@ -163,7 +163,6 @@ class BoardMapperTest extends MapperTestUtility {
|
||||
$actual = $this->boardMapper->find($this->boards[0]->getId(), true, false);
|
||||
/** @var Board $expected */
|
||||
$expected = $this->boards[0];
|
||||
$expected->setLabels([]);
|
||||
$this->assertEquals($expected->getLabels(), $actual->getLabels());
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user