diff --git a/lib/Controller/AttachmentApiController.php b/lib/Controller/AttachmentApiController.php index e2c044e1a..cf027c064 100644 --- a/lib/Controller/AttachmentApiController.php +++ b/lib/Controller/AttachmentApiController.php @@ -45,7 +45,7 @@ class AttachmentApiController extends ApiController { * */ public function getAll() { - $attachment = $this->attachmentService->findAll($this->request->getParam('cardId')); + $attachment = $this->attachmentService->findAll($this->request->getParam('cardId'), true); return new DataResponse($attachment, HTTP::STATUS_OK); } diff --git a/lib/Controller/AttachmentController.php b/lib/Controller/AttachmentController.php index ea31f194a..d595dc2b8 100644 --- a/lib/Controller/AttachmentController.php +++ b/lib/Controller/AttachmentController.php @@ -42,7 +42,7 @@ class AttachmentController extends Controller { * @NoAdminRequired */ public function getAll($cardId) { - return $this->attachmentService->findAll($cardId); + return $this->attachmentService->findAll($cardId, true); } /** diff --git a/lib/Db/AttachmentMapper.php b/lib/Db/AttachmentMapper.php index 2ccae3be8..00a7c61d7 100644 --- a/lib/Db/AttachmentMapper.php +++ b/lib/Db/AttachmentMapper.php @@ -81,6 +81,22 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper { return $this->mapRowToEntity($row); } + public function findByData($cardId, $data) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_attachment') + ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('data', $qb->createNamedParameter($data, IQueryBuilder::PARAM_STR))); + $cursor = $qb->execute(); + $row = $cursor->fetch(PDO::FETCH_ASSOC); + if($row === false) { + $cursor->closeCursor(); + throw new DoesNotExistException('Did expect one result but found none when executing' . $qb); + } + $cursor->closeCursor(); + return $this->mapRowToEntity($row); + } + /** * Find all attachments for a card * diff --git a/lib/Exceptions/ConflictException.php b/lib/Exceptions/ConflictException.php new file mode 100644 index 000000000..53c33601c --- /dev/null +++ b/lib/Exceptions/ConflictException.php @@ -0,0 +1,44 @@ + + * + * @author Jakob Röhrl + * + * @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 . + * + */ + +namespace OCA\Deck\Exceptions; + +use OCA\Deck\StatusException; + +class ConflictException extends StatusException { + + private $data; + + public function __construct($message, $data = null) { + parent::__construct($message); + $this->data = $data; + } + + public function getStatus() { + return 409; + } + + public function getData() { + return $this->data; + } +} \ No newline at end of file diff --git a/lib/Middleware/ExceptionMiddleware.php b/lib/Middleware/ExceptionMiddleware.php index a4f0610ee..416ef105a 100644 --- a/lib/Middleware/ExceptionMiddleware.php +++ b/lib/Middleware/ExceptionMiddleware.php @@ -25,6 +25,7 @@ namespace OCA\Deck\Middleware; use OCA\Deck\Controller\PageController; use OCA\Deck\StatusException; +use OCA\Deck\Exceptions\ConflictException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Middleware; use OCP\AppFramework\Http\JSONResponse; @@ -63,6 +64,17 @@ class ExceptionMiddleware extends Middleware { * @throws \Exception */ public function afterException($controller, $methodName, \Exception $exception) { + if ($exception instanceof ConflictException) { + if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) { + $this->logger->logException($exception); + } + return new JSONResponse([ + 'status' => $exception->getStatus(), + 'message' => $exception->getMessage(), + 'data' => $exception->getData(), + ], $exception->getStatus()); + } + if ($exception instanceof StatusException) { if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) { $this->logger->logException($exception); diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index ec03db12c..1e4216591 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -25,7 +25,9 @@ namespace OCA\Deck\Service; use OC\Security\CSP\ContentSecurityPolicyManager; use OCA\Deck\Db\Attachment; +use OCA\Deck\Db\AttachmentMapper; use OCA\Deck\StatusException; +use OCA\Deck\Exceptions\ConflictException; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; use OCP\AppFramework\Http\FileDisplayResponse; @@ -43,6 +45,7 @@ use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; + class FileService implements IAttachmentService { private $l10n; @@ -51,6 +54,7 @@ class FileService implements IAttachmentService { private $logger; private $rootFolder; private $config; + private $attachmentMapper; public function __construct( IL10N $l10n, @@ -58,7 +62,8 @@ class FileService implements IAttachmentService { IRequest $request, ILogger $logger, IRootFolder $rootFolder, - IConfig $config + IConfig $config, + AttachmentMapper $attachmentMapper ) { $this->l10n = $l10n; $this->appData = $appData; @@ -66,6 +71,7 @@ class FileService implements IAttachmentService { $this->logger = $logger; $this->rootFolder = $rootFolder; $this->config = $config; + $this->attachmentMapper = $attachmentMapper; } /** @@ -146,13 +152,15 @@ class FileService implements IAttachmentService { * @param Attachment $attachment * @throws NotPermittedException * @throws StatusException + * @throws ConflictException */ public function create(Attachment $attachment) { $file = $this->getUploadedFile(); $folder = $this->getFolder($attachment); $fileName = $file['name']; if ($folder->fileExists($fileName)) { - throw new StatusException('File already exists.'); + $attachment = $this->attachmentMapper->findByData($attachment->getCardId(), $fileName); + throw new ConflictException('File already exists.', $attachment); } $target = $folder->newFile($fileName); diff --git a/package-lock.json b/package-lock.json index eeda19e46..7b326bd34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3442,6 +3442,22 @@ "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-1.0.0.tgz", "integrity": "sha512-f+sKpdLZXkODV+OY39K1M+Spmd4RgxmtEXmNn4Bviv4R7uBFHXuw+JX9ZdfDeOryfHjJ/TRQxQEp0GMpBwZFUw==" }, + "@nextcloud/dialogs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-1.2.1.tgz", + "integrity": "sha512-v+nnlqdOUpqumD51Fkjo1V9W/xImW7GcY29Iq1ErSDCmkhrS4pk9YDYrZO86teey+pT5nZ0gdGAtiU+LNFQgzw==", + "requires": { + "core-js": "3.6.4", + "toastify-js": "^1.6.2" + }, + "dependencies": { + "core-js": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", + "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==" + } + } + }, "@nextcloud/event-bus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.1.2.tgz", @@ -3469,6 +3485,21 @@ } } }, + "@nextcloud/files": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-1.0.0.tgz", + "integrity": "sha512-HJF+eavX8BymQ83jGkluNyQ8zrbFfuiQwunSe140sbQ042pjyljSUACf/WyvVAeqaCj7cIeYMPQBtvykom0+cg==", + "requires": { + "core-js": "3.5.0" + }, + "dependencies": { + "core-js": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.5.0.tgz", + "integrity": "sha512-Ifh3kj78gzQ7NAoJXeTu+XwzDld0QRIwjBLRqAMhuLhP3d2Av5wmgE9ycfnvK6NAEjTkQ1sDPeoEZAWO3Hx1Uw==" + } + } + }, "@nextcloud/l10n": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-1.1.0.tgz", @@ -4307,9 +4338,9 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "micromatch": { "version": "3.1.10", @@ -6979,9 +7010,9 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" } } }, @@ -7384,9 +7415,9 @@ }, "dependencies": { "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "acorn-jsx": { @@ -7985,9 +8016,9 @@ }, "dependencies": { "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "eslint-visitor-keys": { @@ -12620,9 +12651,9 @@ }, "dependencies": { "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true } } @@ -13321,7 +13352,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -13394,9 +13425,9 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" } } }, @@ -13930,7 +13961,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, @@ -13947,7 +13978,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -14128,7 +14159,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { @@ -14912,9 +14943,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true } } @@ -15299,9 +15330,9 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "micromatch": { "version": "3.1.10", @@ -16025,9 +16056,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "micromatch": { @@ -16951,7 +16982,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -17520,9 +17551,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "micromatch": { @@ -17828,6 +17859,11 @@ } } }, + "toastify-js": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.7.0.tgz", + "integrity": "sha512-GmPy4zJ/ulCfmCHlfCtgcB+K2xhx2AXW3T/ZZOSjyjaIGevhz+uvR8HSCTay/wBq4tt2mUnBqlObP1sSWGlsnQ==" + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", diff --git a/package.json b/package.json index f6f2c22d8..6ff7e4314 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "@nextcloud/auth": "^1.2.1", "@nextcloud/axios": "^1.3.1", "@nextcloud/l10n": "^1.1.0", + "@nextcloud/dialogs": "^1.2.1", + "@nextcloud/files": "^1.0.0", "@nextcloud/moment": "^1.1.0", "@nextcloud/router": "^1.0.0", "@nextcloud/vue": "^1.4.0", diff --git a/src/components/card/CardSidebarTabAttachments.vue b/src/components/card/CardSidebarTabAttachments.vue index 100716150..5b2160e98 100644 --- a/src/components/card/CardSidebarTabAttachments.vue +++ b/src/components/card/CardSidebarTabAttachments.vue @@ -22,30 +22,263 @@ - diff --git a/src/components/cards/CardBadges.vue b/src/components/cards/CardBadges.vue index 72f7315fc..1dc509a88 100644 --- a/src/components/cards/CardBadges.vue +++ b/src/components/cards/CardBadges.vue @@ -34,7 +34,9 @@ {{ checkListCheckedCount }}/{{ checkListCount }} -
+
+ {{ card.attachmentCount }} +
@@ -99,9 +101,10 @@ export default { .icon { opacity: 0.5; - padding: 12px 3px; + padding: 12px 14px; margin-right: 10px; background-position: left; + background-size: 16px; span { margin-left: 18px; } diff --git a/src/services/AttachmentApi.js b/src/services/AttachmentApi.js new file mode 100644 index 000000000..2849c913f --- /dev/null +++ b/src/services/AttachmentApi.js @@ -0,0 +1,81 @@ +/* + * @copyright Copyright (c) 2020 Jakob Röhrl + * + * @author Jakob Röhrl + * + * @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 . + * + */ + +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +export class AttachmentApi { + + url(url) { + return generateUrl(`/apps/deck${url}`) + } + + async fetchAttachments(cardId) { + const response = await axios({ + method: 'GET', + url: this.url(`/cards/${cardId}/attachments`), + }) + return response.data + } + + async createAttachment({ cardId, formData }) { + const response = await axios({ + method: 'POST', + url: this.url(`/cards/${cardId}/attachment`), + data: formData, + }) + return response.data + } + + async updateAttachment({ cardId, attachmentId, formData }) { + const response = await axios({ + method: 'POST', + url: this.url(`/cards/${cardId}/attachment/${attachmentId}`), + data: formData, + }) + return response.data + } + + async deleteAttachment(attachment) { + await axios({ + method: 'DELETE', + url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.id}`), + }) + } + + async restoreAttachment(attachment) { + const response = await axios({ + method: 'GET', + url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.id}/restore`), + }) + return response.data + } + + async displayAttachment(attachment) { + const response = await axios({ + method: 'GET', + url: this.url(`/cards/${attachment.cardId}/attachment/${attachment.id}`), + }) + return response.data + } + +} diff --git a/src/store/attachment.js b/src/store/attachment.js new file mode 100644 index 000000000..966958256 --- /dev/null +++ b/src/store/attachment.js @@ -0,0 +1,110 @@ +/* + * @copyright Copyright (c) 2020 Jakob Röhrl + * + * @author Jakob Röhrl + * + * @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 . + * + */ + +import { AttachmentApi } from './../services/AttachmentApi' +import Vue from 'vue' + +const apiClient = new AttachmentApi() + +export default { + state: { + attachments: {}, + }, + getters: { + attachmentsByCard: state => (cardId) => { + if (typeof state.attachments[cardId] === 'undefined') { + return [] + } + return state.attachments[cardId] + }, + + }, + mutations: { + createAttachment(state, { cardId, attachment }) { + if (typeof state.attachments[cardId] === 'undefined') { + Vue.set(state.attachments, cardId, [attachment]) + } else { + state.attachments[cardId].push(attachment) + } + }, + + createAttachments(state, { cardId, attachments }) { + if (typeof state.attachments[cardId] === 'undefined') { + Vue.set(state.attachments, cardId, attachments) + } else { + state.attachments[cardId].push(attachments) + } + }, + + updateAttachment(state, { cardId, attachment }) { + const existingIndex = state.attachments[attachment.cardId].findIndex(a => a.id === attachment.id) + if (existingIndex !== -1) { + Vue.set(state.attachments[cardId], existingIndex, attachment) + } + }, + + deleteAttachment(state, deletedAttachment) { + const existingIndex = state.attachments[deletedAttachment.cardId].findIndex(a => a.id === deletedAttachment.id) + if (existingIndex !== -1) { + state.attachments[deletedAttachment.cardId][existingIndex].deletedAt = -1 + } + }, + + restoreAttachment(state, restoredAttachment) { + const existingIndex = state.attachments[restoredAttachment.cardId].findIndex(a => a.id === restoredAttachment.id) + if (existingIndex !== -1) { + state.attachments[restoredAttachment.cardId][existingIndex].deletedAt = 0 + } + }, + + }, + actions: { + async fetchAttachments({ commit }, cardId) { + const attachments = await apiClient.fetchAttachments(cardId) + commit('createAttachments', { cardId, attachments }) + }, + + async createAttachment({ commit }, { cardId, formData }) { + const attachment = await apiClient.createAttachment({ cardId, formData }) + commit('createAttachment', { cardId, attachment }) + commit('cardIncreaseAttachmentCount', cardId) + }, + + async updateAttachment({ commit }, { cardId, attachmentId, formData }) { + const attachment = await apiClient.updateAttachment({ cardId, attachmentId, formData }) + commit('updateAttachment', { cardId, attachment }) + }, + + async deleteAttachment({ commit }, attachment) { + await apiClient.deleteAttachment(attachment) + commit('deleteAttachment', attachment) + commit('cardDecreaseAttachmentCount', attachment.cardId) + }, + + async restoreAttachment({ commit }, attachment) { + const restoredAttachment = await apiClient.restoreAttachment(attachment) + commit('restoreAttachment', restoredAttachment) + commit('cardIncreaseAttachmentCount', attachment.cardId) + }, + + }, +} diff --git a/src/store/card.js b/src/store/card.js index 24e800f5e..65fffffc1 100644 --- a/src/store/card.js +++ b/src/store/card.js @@ -140,6 +140,20 @@ export default { Vue.set(state.cards[existingIndex], property, card[property]) } }, + cardIncreaseAttachmentCount(state, cardId) { + const existingIndex = state.cards.findIndex(_card => _card.id === cardId) + if (existingIndex !== -1) { + const existingCard = state.cards.find(_card => _card.id === cardId) + Vue.set(state.cards, existingCard.attachmentCount, existingCard.attachmentCount++) + } + }, + cardDecreaseAttachmentCount(state, cardId) { + const existingIndex = state.cards.findIndex(_card => _card.id === cardId) + if (existingIndex !== -1) { + const existingCard = state.cards.find(_card => _card.id === cardId) + Vue.set(state.cards, existingCard.attachmentCount, existingCard.attachmentCount--) + } + }, }, actions: { async addCard({ commit }, card) { diff --git a/src/store/main.js b/src/store/main.js index 56079ff0b..1fef80971 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -30,6 +30,7 @@ import stack from './stack' import card from './card' import comment from './comment' import trashbin from './trashbin' +import attachment from './attachment' Vue.use(Vuex) @@ -48,6 +49,7 @@ export default new Vuex.Store({ card, comment, trashbin, + attachment, }, strict: debug, state: { diff --git a/tests/unit/Service/FileServiceTest.php b/tests/unit/Service/FileServiceTest.php index bd3a4e238..72dd57dd6 100644 --- a/tests/unit/Service/FileServiceTest.php +++ b/tests/unit/Service/FileServiceTest.php @@ -24,28 +24,15 @@ namespace OCA\Deck\Service; -use OCA\Deck\AppInfo\Application; -use OCA\Deck\Db\AssignedUsers; -use OCA\Deck\Db\AssignedUsersMapper; use OCA\Deck\Db\Attachment; use OCA\Deck\Db\AttachmentMapper; -use OCA\Deck\Db\Card; -use OCA\Deck\Db\CardMapper; -use OCA\Deck\Db\StackMapper; -use OCA\Deck\InvalidAttachmentType; -use OCA\Deck\NotFoundException; -use OCA\Deck\StatusException; use OCP\AppFramework\Http\ContentSecurityPolicy; -use OCP\AppFramework\Http\FileDisplayResponse; -use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\StreamResponse; -use OCP\AppFramework\IAppContainer; use OCP\Files\Folder; use OCP\Files\IAppData; use OCP\Files\IRootFolder; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; -use OCP\ICacheFactory; use OCP\IConfig; use OCP\IL10N; use OCP\ILogger; @@ -69,6 +56,8 @@ class FileServiceTest extends TestCase { private $rootFolder; /** @var IConfig */ private $config; + /** @var AttachmentMapper|MockObject */ + private $attachmentMapper; public function setUp(): void { parent::setUp(); @@ -78,7 +67,8 @@ class FileServiceTest extends TestCase { $this->logger = $this->createMock(ILogger::class); $this->rootFolder = $this->createMock(IRootFolder::class); $this->config = $this->createMock(IConfig::class); - $this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config); + $this->attachmentMapper = $this->createMock(AttachmentMapper::class); + $this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger, $this->rootFolder, $this->config, $this->attachmentMapper); } public function mockGetFolder($cardId) {