diff --git a/appinfo/routes.php b/appinfo/routes.php index 4ab64f84a..6330381cf 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -66,6 +66,7 @@ return [ ['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'], ['name' => 'card#unassignUser', 'url' => '/cards/{cardId}/unassign', 'verb' => 'PUT'], + // attachments ['name' => 'attachment#getAll', 'url' => '/cards/{cardId}/attachments', 'verb' => 'GET'], ['name' => 'attachment#create', 'url' => '/cards/{cardId}/attachment', 'verb' => 'POST'], ['name' => 'attachment#display', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'GET'], @@ -110,6 +111,8 @@ return [ ['name' => 'card_api#reorder', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder', 'verb' => 'PUT'], ['name' => 'card_api#delete', 'url' => '/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'DELETE'], + ['name' => 'card_api#findAllWithDue', 'url' => '/api/v1.0/dashboard/due', 'verb' => 'GET'], + ['name' => 'label_api#get', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'GET'], ['name' => 'label_api#create', 'url' => '/api/v1.0/boards/{boardId}/labels', 'verb' => 'POST'], ['name' => 'label_api#update', 'url' => '/api/v1.0/boards/{boardId}/labels/{labelId}', 'verb' => 'PUT'], @@ -131,5 +134,9 @@ return [ ['name' => 'comments_api#create', 'url' => '/api/v1.0/cards/{cardId}/comments', 'verb' => 'POST'], ['name' => 'comments_api#update', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'], ['name' => 'comments_api#delete', 'url' => '/api/v1.0/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'], + + // dashboard + ['name' => 'overview_api#findAllWithDue', 'url' => '/api/v1.0/overview/due', 'verb' => 'GET'], + ['name' => 'overview_api#findAssignedCards', 'url' => '/api/v1.0/overview/assigned', 'verb' => 'GET'], ] ]; diff --git a/lib/Controller/OverviewApiController.php b/lib/Controller/OverviewApiController.php new file mode 100644 index 000000000..7635dc6c6 --- /dev/null +++ b/lib/Controller/OverviewApiController.php @@ -0,0 +1,61 @@ + + * + * @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\Controller; + +use OCA\Deck\Service\OverviewService; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +class OverviewApiController extends OCSController { + + /** @var OverviewService */ + private $dashboardService; + + /** @var string */ + private $userId; + + public function __construct($appName, IRequest $request, OverviewService $dashboardService, $userId) { + parent::__construct($appName, $request); + $this->dashboardService = $dashboardService; + $this->userId = $userId; + } + + /** + * @NoAdminRequired + */ + public function findAllWithDue(): DataResponse { + return new DataResponse($this->dashboardService->findAllWithDue($this->userId)); + } + + /** + * @NoAdminRequired + */ + public function findAssignedCards(): DataResponse { + return new DataResponse($this->dashboardService->findAssignedCards($this->userId)); + } +} diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index ad2c97724..bbbcdb560 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -141,6 +141,23 @@ class CardMapper extends DeckMapper implements IPermissionMapper { return $this->findEntities($sql, [$stackId], $limit, $offset); } + public function findAllWithDue($boardId) { + $sql = 'SELECT c.* FROM `*PREFIX*deck_cards` c + INNER JOIN `*PREFIX*deck_stacks` s ON s.id = c.stack_id + INNER JOIN `*PREFIX*deck_boards` b ON b.id = s.board_id + WHERE `s`.`board_id` = ? AND duedate IS NOT NULL AND NOT c.archived AND c.deleted_at = 0 AND s.deleted_at = 0 AND NOT b.archived AND b.deleted_at = 0'; + return $this->findEntities($sql, [$boardId]); + } + + public function findAssignedCards($boardId, $username) { + $sql = 'SELECT c.* FROM `*PREFIX*deck_cards` c + INNER JOIN `*PREFIX*deck_stacks` s ON s.id = c.stack_id + INNER JOIN `*PREFIX*deck_boards` b ON b.id = s.board_id + INNER JOIN `*PREFIX*deck_assigned_users` u ON c.id = card_id + WHERE `s`.`board_id` = ? AND participant = ? AND NOT c.archived AND c.deleted_at = 0 AND s.deleted_at = 0 AND NOT b.archived AND b.deleted_at = 0'; + return $this->findEntities($sql, [$boardId, $username]); + } + public function findOverdue() { $sql = 'SELECT id,title,duedate,notified from `*PREFIX*deck_cards` WHERE duedate < NOW() AND NOT archived AND deleted_at = 0'; return $this->findEntities($sql); diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 50ea6fa49..9d4867705 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -564,4 +564,28 @@ class CardService { '\OCA\Deck\Card::onUpdate', new FTSEvent(null, ['id' => $cardId, 'card' => $card]) ); } + + /** + * + * @return array + * @throws \OCA\Deck\NoPermissionException + * @throws BadRequestException + */ + public function findAllWithDue($userId) { + $cards = $this->cardMapper->findAllWithDue($userId); + + return $cards; + } + + /** + * + * @return array + * @throws \OCA\Deck\NoPermissionException + * @throws BadRequestException + */ + public function findAssignedCards($userId) { + $cards = $this->cardMapper->findAssignedCards($userId); + + return $cards; + } } diff --git a/lib/Service/OverviewService.php b/lib/Service/OverviewService.php new file mode 100644 index 000000000..fc0168c82 --- /dev/null +++ b/lib/Service/OverviewService.php @@ -0,0 +1,143 @@ + + * + * @author Julius Härtl + * @author Maxence Lange + * + * @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\Service; + +use OCA\Deck\Db\AssignedUsersMapper; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCP\Comments\ICommentsManager; +use OCP\IGroupManager; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\LabelMapper; +use OCP\IUserManager; + +class OverviewService { + + /** @var BoardMapper */ + private $boardMapper; + /** @var LabelMapper */ + private $labelMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var AssignedUsersMapper */ + private $assignedUsersMapper; + /** @var IUserManager */ + private $userManager; + /** @var IGroupManager */ + private $groupManager; + /** @var ICommentsManager */ + private $commentsManager; + /** @var AttachmentService */ + private $attachmentService; + + public function __construct( + BoardMapper $boardMapper, + LabelMapper $labelMapper, + CardMapper $cardMapper, + AssignedUsersMapper $assignedUsersMapper, + IUserManager $userManager, + IGroupManager $groupManager, + ICommentsManager $commentsManager, + AttachmentService $attachmentService + ) { + $this->boardMapper = $boardMapper; + $this->labelMapper = $labelMapper; + $this->cardMapper = $cardMapper; + $this->assignedUsersMapper = $assignedUsersMapper; + $this->userManager = $userManager; + $this->groupManager = $groupManager; + $this->commentsManager = $commentsManager; + $this->attachmentService = $attachmentService; + } + + public function enrich(Card $card, string $userId): void { + $cardId = $card->getId(); + + $this->cardMapper->mapOwner($card); + $card->setAssignedUsers($this->assignedUsersMapper->find($cardId)); + $card->setLabels($this->labelMapper->findAssignedLabelsForCard($cardId)); + $card->setAttachmentCount($this->attachmentService->count($cardId)); + + $user = $this->userManager->get($userId); + if ($user !== null) { + $lastRead = $this->commentsManager->getReadMark('deckCard', (string)$card->getId(), $user); + $count = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId(), $lastRead); + $card->setCommentsUnread($count); + } + } + + public function findAllWithDue(string $userId): array { + $userBoards = $this->findAllBoardsFromUser($userId); + $allDueCards = []; + foreach ($userBoards as $userBoard) { + $service = $this; + $allDueCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) { + $service->enrich($card, $userId); + $cardData = $card->jsonSerialize(); + $cardData['boardId'] = $userBoard->getId(); + return $cardData; + }, $this->cardMapper->findAllWithDue($userBoard->getId())); + } + return $allDueCards; + } + + public function findAssignedCards(string $userId): array { + $userBoards = $this->findAllBoardsFromUser($userId); + $allAssignedCards = []; + foreach ($userBoards as $userBoard) { + $service = $this; + $allAssignedCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) { + $service->enrich($card, $userId); + $cardData = $card->jsonSerialize(); + $cardData['boardId'] = $userBoard->getId(); + return $cardData; + }, $this->cardMapper->findAssignedCards($userBoard->getId(), $userId)); + } + return $allAssignedCards; + } + + // FIXME: This is duplicate code with the board service + + private function findAllBoardsFromUser(string $userId): array { + $userInfo = $this->getBoardPrerequisites($userId); + $userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null); + $groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null); + $circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null); + return array_merge($userBoards, $groupBoards, $circleBoards); + } + + private function getBoardPrerequisites($userId): array { + $user = $this->userManager->get($userId); + $groups = $user !== null ? $this->groupManager->getUserGroupIds($user) : []; + return [ + 'user' => $userId, + 'groups' => $groups + ]; + } +} diff --git a/src/components/Controls.vue b/src/components/Controls.vue index 6a4fbb309..3017f9a96 100644 --- a/src/components/Controls.vue +++ b/src/components/Controls.vue @@ -30,6 +30,9 @@ ({{ t('deck', 'Archived cards') }})

+
@import '../../css/animations.scss'; - - $board-spacing: 15px; - $stack-spacing: 10px; - $stack-width: 300px; + @import '../../css/variables.scss'; form { text-align: center; diff --git a/src/components/board/Stack.vue b/src/components/board/Stack.vue index 8f2b35186..27fe84eec 100644 --- a/src/components/board/Stack.vue +++ b/src/components/board/Stack.vue @@ -253,8 +253,7 @@ export default { diff --git a/src/css/variables.scss b/src/css/variables.scss new file mode 100644 index 000000000..ff339e192 --- /dev/null +++ b/src/css/variables.scss @@ -0,0 +1,5 @@ +$card-spacing: 10px; +$card-padding: 10px; +$stack-spacing: 10px; +$stack-width: 260px; +$board-spacing: 15px; diff --git a/src/router.js b/src/router.js index 3d42dd0aa..f5fce9f51 100644 --- a/src/router.js +++ b/src/router.js @@ -29,6 +29,7 @@ import Board from './components/board/Board' import Sidebar from './components/Sidebar' import BoardSidebar from './components/board/BoardSidebar' import CardSidebar from './components/card/CardSidebar' +import Overview from './components/overview/Overview' Vue.use(Router) @@ -41,6 +42,20 @@ export default new Router({ name: 'main', component: Boards, }, + { + path: '/overview/:filter', + name: 'overview', + components: { + default: Overview, + }, + props: { + default: (route) => { + return { + filter: route.params.filter, + } + }, + }, + }, { path: '/board', name: 'boards', diff --git a/src/services/OverviewApi.js b/src/services/OverviewApi.js new file mode 100644 index 000000000..8fac07ff9 --- /dev/null +++ b/src/services/OverviewApi.js @@ -0,0 +1,56 @@ +/* + * @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 { generateOcsUrl } from '@nextcloud/router' + +export class OverviewApi { + + url(url) { + return generateOcsUrl(`apps/deck/api/v1.0`) + url + } + + findAllWithDue(data) { + return axios.get(this.url(`overview/due`), { + headers: { 'OCS-APIRequest': 'true' }, + }) + .then( + (response) => Promise.resolve(response.data.ocs.data), + (err) => Promise.reject(err) + ) + .catch((err) => Promise.reject(err) + ) + } + + findMyAssignedCards(data) { + return axios.get(this.url(`overview/assigned`), { + headers: { 'OCS-APIRequest': 'true' }, + }) + .then( + (response) => Promise.resolve(response.data.ocs.data), + (err) => Promise.reject(err) + ) + .catch((err) => Promise.reject(err) + ) + } + +} diff --git a/src/store/card.js b/src/store/card.js index d3061cb18..4f4f4c987 100644 --- a/src/store/card.js +++ b/src/store/card.js @@ -96,9 +96,6 @@ export default { }, }, mutations: { - clearCards(state) { - state.cards = [] - }, addCard(state, card) { card.labels = card.labels || [] card.assignedUsers = card.assignedUsers || [] diff --git a/src/store/main.js b/src/store/main.js index 6189fd934..d66149cf5 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -26,12 +26,13 @@ import Vue from 'vue' import Vuex from 'vuex' import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -import { BoardApi } from './../services/BoardApi' +import { BoardApi } from '../services/BoardApi' import stack from './stack' import card from './card' import comment from './comment' import trashbin from './trashbin' import attachment from './attachment' +import overview from './overview' import debounce from 'lodash/debounce' Vue.use(Vuex) @@ -51,6 +52,7 @@ export default new Vuex.Store({ comment, trashbin, attachment, + overview, }, strict: debug, state: { diff --git a/src/store/overview.js b/src/store/overview.js new file mode 100644 index 000000000..438ee8e59 --- /dev/null +++ b/src/store/overview.js @@ -0,0 +1,71 @@ +/* + * @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 Vue from 'vue' +import Vuex from 'vuex' +import { OverviewApi } from '../services/OverviewApi' +Vue.use(Vuex) + +const apiClient = new OverviewApi() +export default { + state: { + withDue: [], + assignedCards: [], + }, + getters: { + withDueDashboard: state => { + return state.withDue + }, + assignedCardsDashboard: state => { + return state.assignedCards + }, + }, + mutations: { + setWithDueDashboard(state, withDue) { + state.withDue = withDue + }, + setAssignedCards(state, assignedCards) { + state.assignedCards = assignedCards + }, + }, + actions: { + async loadDueDashboard({ commit }) { + commit('setCurrentBoard', null) + const cardsWithDueDate = await apiClient.findAllWithDue() + const withDueFlat = cardsWithDueDate.flat() + for (const i in withDueFlat) { + commit('addCard', withDueFlat[i]) + } + commit('setWithDueDashboard', withDueFlat) + }, + + async loadAssignDashboard({ commit }) { + commit('setCurrentBoard', null) + const assignedCards = await apiClient.findMyAssignedCards() + const assignedCardsFlat = assignedCards.flat() + for (const i in assignedCardsFlat) { + commit('addCard', assignedCardsFlat[i]) + } + commit('setAssignedCards', assignedCardsFlat) + }, + }, +} diff --git a/src/store/stack.js b/src/store/stack.js index afaa1a496..3f428e71f 100644 --- a/src/store/stack.js +++ b/src/store/stack.js @@ -76,7 +76,6 @@ export default { }) }, async loadStacks({ commit }, boardId) { - commit('clearCards') let call = 'loadStacks' if (this.state.showArchived === true) { call = 'loadArchivedStacks'