From 136ba01628f62bb0d955c314494c7eb06b228dd8 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Jul 2026 13:59:23 +0100 Subject: [PATCH 1/2] The No-Due column on the overview page is a killer for load time I have hundreds of cards that don't have dates on them because that feature isn't used, so the page load time takes 5-6 seconds. Attempting to add a flag to deactivate loading them optionally, so I can skip that list. Just doing this in the frontend wasn't sufficient as the query was taking approximately 30 seconds to run on my dev environment with the NOT NULL filter it is down to approx 700ms Signed-off-by: Rob Emery --- docs/API.md | 2 ++ lib/Controller/OverviewApiController.php | 3 ++- lib/Dashboard/DeckWidgetUpcoming.php | 2 +- lib/Db/CardMapper.php | 6 +++++- lib/Service/ConfigService.php | 26 ++++++++++++++++++++++- lib/Service/OverviewService.php | 6 +++--- src/App.vue | 8 +++++++ src/components/DeckAppSettings.vue | 10 +++++++++ src/components/overview/Overview.vue | 27 +++++++++++++++++------- src/services/OverviewApi.js | 5 ++++- src/stores/dashboard.js | 4 ++-- src/stores/overview.js | 4 ++-- 12 files changed, 83 insertions(+), 20 deletions(-) diff --git a/docs/API.md b/docs/API.md index c3d40d265d..ed67000632 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1128,6 +1128,7 @@ Deck stores user and app configuration values globally and per board. The GET en | --- | --- | | 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) | +| hideNoDueOnOverview | Determines if the No Due Date column should be displayed | | cardIdBadge | Determines if the ID badges are displayed on cards (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)| @@ -1173,6 +1174,7 @@ Deck stores user and app configuration values globally and per board. The GET en | notify-due | `off`, `assigned` or `all` | | calendar | Boolean | | cardDetailsInModal | Boolean | +| hideNoDueOnOverview | Boolean | | cardIdBadge | Boolean | #### Example request diff --git a/lib/Controller/OverviewApiController.php b/lib/Controller/OverviewApiController.php index 9374425317..4777297df9 100644 --- a/lib/Controller/OverviewApiController.php +++ b/lib/Controller/OverviewApiController.php @@ -27,6 +27,7 @@ public function __construct( #[NoAdminRequired] public function upcomingCards(): DataResponse { - return new DataResponse($this->dashboardService->findUpcomingCards($this->userId)); + $hideNoDue = ($this->request->getParam('hideNoDueOnOverview') == "true"); + return new DataResponse($this->dashboardService->findUpcomingCards($this->userId, $hideNoDue)); } } diff --git a/lib/Dashboard/DeckWidgetUpcoming.php b/lib/Dashboard/DeckWidgetUpcoming.php index 4e46b435cd..76bd65eea7 100644 --- a/lib/Dashboard/DeckWidgetUpcoming.php +++ b/lib/Dashboard/DeckWidgetUpcoming.php @@ -96,7 +96,7 @@ public function load(): void { * @inheritDoc */ public function getItems(string $userId, ?string $since = null, int $limit = 7): array { - $upcomingCards = $this->dashboardService->findUpcomingCards($userId); + $upcomingCards = $this->dashboardService->findUpcomingCards($userId, false); $nowTimestamp = (new Datetime())->getTimestamp(); $sinceTimestamp = $since !== null ? (new Datetime($since))->getTimestamp() : null; $upcomingCards = array_filter($upcomingCards, static function (array $card) use ($nowTimestamp, $sinceTimestamp) { diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 47f3e58af3..db998ed047 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -328,9 +328,10 @@ public function findAllWithDue(array $boardIds): array { /** * @param int[] $boardIds + * @param bool $filterNoDue * @return Card[] */ - public function findToMeOrNotAssignedCards(array $boardIds, string $username): array { + public function findToMeOrNotAssignedCards(array $boardIds, string $username, bool $filterNoDue): array { $qb = $this->db->getQueryBuilder(); $qb->select('c.*') ->from('deck_cards', 'c') @@ -349,6 +350,9 @@ public function findToMeOrNotAssignedCards(array $boardIds, string $username): a ->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('b.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + if($filterNoDue) { + $qb = $qb->andWhere($qb->expr()->isNotNull('duedate')); + } return $this->findEntities($qb); } diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index b4afb8fc45..e870a290b0 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -52,7 +52,8 @@ public function getAll(): array { $data = [ 'calendar' => $this->isCalendarEnabled(), 'cardDetailsInModal' => $this->isCardDetailsInModal(), - 'cardIdBadge' => $this->isCardIdBadgeEnabled() + 'cardIdBadge' => $this->isCardIdBadgeEnabled(), + 'hideNoDueOnOverview' => $this->isHideNoDueOnOverviewEnabled() ]; if ($this->groupManager->isAdmin($userId)) { $data['groupLimit'] = $this->get('groupLimit'); @@ -85,6 +86,11 @@ public function get(string $key) { return false; } return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', true); + case 'hideNoDueOnOverview': + if ($this->getUserId() === null) { + return false; + } + return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'hideNoDueOnOverview', true); case 'cardIdBadge': if ($this->getUserId() === null) { return false; @@ -123,6 +129,20 @@ public function isCardDetailsInModal(?int $boardId = null): bool { return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'board:' . $boardId . ':cardDetailsInModal', $defaultState); } + public function isHideNoDueOnOverviewEnabled(?int $boardId = null): bool { + $userId = $this->getUserId(); + if ($userId === null) { + return false; + } + + $defaultState = (bool)$this->config->getUserValue($userId, Application::APP_ID, 'hideNoDueOnOverview', false); + if ($boardId === null) { + return $defaultState; + } + + return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'board:' . $boardId . ':hideNoDueOnOverview', $defaultState); + } + public function isCardIdBadgeEnabled(): bool { $userId = $this->getUserId(); if ($userId === null) { @@ -177,6 +197,10 @@ public function set($key, $value) { $this->config->setUserValue($userId, Application::APP_ID, 'cardDetailsInModal', (string)$value); $result = $value; break; + case 'hideNoDueOnOverview': + $this->config->setUserValue($userId, Application::APP_ID, 'hideNoDueOnOverview', (string)$value); + $result = $value; + break; case 'cardIdBadge': $this->config->setUserValue($userId, Application::APP_ID, 'cardIdBadge', (string)$value); $result = $value; diff --git a/lib/Service/OverviewService.php b/lib/Service/OverviewService.php index bacd0b43ae..d5a92cee98 100644 --- a/lib/Service/OverviewService.php +++ b/lib/Service/OverviewService.php @@ -23,7 +23,7 @@ public function __construct( ) { } - public function findUpcomingCards(string $userId): array { + public function findUpcomingCards(string $userId, bool $filterNoDue): array { $userBoards = $this->boardMapper->findAllForUser($userId); $boardOwnerIds = array_filter(array_map(function (Board $board) { @@ -35,9 +35,9 @@ public function findUpcomingCards(string $userId): array { $foundCards = array_merge( // private board: get all my assigned or unassigned cards - $this->cardMapper->findToMeOrNotAssignedCards($boardOwnerIds, $userId), + $this->cardMapper->findToMeOrNotAssignedCards($boardOwnerIds, $userId, $filterNoDue), // shared board: get all my assigned or unassigned cards - $this->cardMapper->findToMeOrNotAssignedCards($boardSharedIds, $userId) + $this->cardMapper->findToMeOrNotAssignedCards($boardSharedIds, $userId, $filterNoDue) ); $this->cardService->enrichCards($foundCards); diff --git a/src/App.vue b/src/App.vue index 881f75ced9..aba4793edd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -98,6 +98,14 @@ export default { this.$store.dispatch('setConfig', { cardDetailsInModal: newValue }) }, }, + hideNoDueOnOverview: { + get() { + return this.$store.getters.config('hideNoDueOnOverview') + }, + set(newValue) { + this.$store.dispatch('setConfig', { hideNoDueOnOverview: newValue }) + }, + }, }, created() { const initialState = loadState('deck', 'initialBoards', null) diff --git a/src/components/DeckAppSettings.vue b/src/components/DeckAppSettings.vue index 3935dbff5d..f336e16ef5 100644 --- a/src/components/DeckAppSettings.vue +++ b/src/components/DeckAppSettings.vue @@ -12,6 +12,8 @@ + @@ -119,6 +121,14 @@ export default { this.$store.dispatch('setConfig', { cardDetailsInModal: newValue }) }, }, + hideNoDueOnOverview: { + get() { + return this.$store.getters.config('hideNoDueOnOverview') + }, + set(newValue) { + this.$store.dispatch('setConfig', { hideNoDueOnOverview: newValue }) + }, + }, cardIdBadge: { get() { return this.$store.getters.config('cardIdBadge') diff --git a/src/components/overview/Overview.vue b/src/components/overview/Overview.vue index 74d10b7bf7..7bd5d26db7 100644 --- a/src/components/overview/Overview.vue +++ b/src/components/overview/Overview.vue @@ -75,11 +75,6 @@ const COLUMN_PROPS_LIST = [ title: 'Later', filter: 'later', }, - { - title: 'No due', - filter: 'nodue', - sort: false, - }, ] export default { @@ -96,9 +91,17 @@ export default { }, }, data() { + const dynamicList = [...COLUMN_PROPS_LIST]; + if(!this.$store.getters.config('hideNoDueOnOverview')) { + dynamicList.push({ + title: 'No due', + filter: 'nodue', + sort: false, + }); + } return { loading: true, - columnPropsList: COLUMN_PROPS_LIST, + columnPropsList: dynamicList, } }, computed: { @@ -113,7 +116,15 @@ export default { return '' } }, - ...mapState(useOverviewStore, ['assignedCards']), + hideNoDueOnOverview: { + get() { + return this.$store.getters.config('hideNoDueOnOverview') + }, + set(newValue) { + this.$store.dispatch('setConfig', { hideNoDueOnOverview: newValue }) + }, + }, + ...mapGetters(['assignedCardsDashboard']), }, watch: { '$route.params.filter'() { @@ -129,7 +140,7 @@ export default { this.loading = true try { if (this.filter === FILTER_UPCOMING) { - await this.loadUpcoming() + await this.loadUpcoming(this.$store.getters.config('hideNoDueOnOverview')) } } catch (e) { console.error(e) diff --git a/src/services/OverviewApi.js b/src/services/OverviewApi.js index 91aeecfce6..e6e6169a9f 100644 --- a/src/services/OverviewApi.js +++ b/src/services/OverviewApi.js @@ -12,9 +12,12 @@ export class OverviewApi { return generateOcsUrl(`apps/deck/api/v1.0/${url}`) } - get(filter) { + get(filter, hideNoDueOnOverview) { return axios.get(this.url(`overview/${filter}`), { headers: { 'OCS-APIRequest': 'true' }, + params: { + hideNoDueOnOverview: hideNoDueOnOverview, + } }) .then( (response) => Promise.resolve(response.data.ocs.data), diff --git a/src/stores/dashboard.js b/src/stores/dashboard.js index 9e417a29ab..143dd43399 100644 --- a/src/stores/dashboard.js +++ b/src/stores/dashboard.js @@ -13,8 +13,8 @@ export const useDashboardStore = defineStore('dashboard', { assignedCards: [], }), actions: { - async loadUpcoming() { - const upcommingCards = await apiClient.get('upcoming') + async loadUpcoming(hideNoDueOnOverview) { + const upcommingCards = await apiClient.get('upcoming', hideNoDueOnOverview) this.assignedCards = upcommingCards }, }, diff --git a/src/stores/overview.js b/src/stores/overview.js index 843c05a04c..2206b875d6 100644 --- a/src/stores/overview.js +++ b/src/stores/overview.js @@ -14,13 +14,13 @@ export const useOverviewStore = defineStore('overview', { loading: false, }), actions: { - async loadUpcoming() { + async loadUpcoming(hideNoDueOnOverview) { if (this.loading) { return this.loading } const promise = (async () => { this.$vuex.commit('setCurrentBoard', null) - const assignedCards = await overviewApi.get('upcoming') + const assignedCards = await overviewApi.get('upcoming', hideNoDueOnOverview) const assignedCardsFlat = Object.values(assignedCards).flat() for (const i in assignedCardsFlat) { this.$vuex.commit('addCard', assignedCardsFlat[i]) From d70e5b392df494875b3687c4b645ad583e291fc6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 12 Jul 2026 15:34:46 +0100 Subject: [PATCH 2/2] Fixing merge-mistake I think Signed-off-by: Rob Emery --- src/components/overview/Overview.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/overview/Overview.vue b/src/components/overview/Overview.vue index 7bd5d26db7..da568f771a 100644 --- a/src/components/overview/Overview.vue +++ b/src/components/overview/Overview.vue @@ -47,6 +47,7 @@ import CardItem from '../cards/CardItem.vue' import GlobalSearchResults from '../search/GlobalSearchResults.vue' import { useOverviewStore } from '../../stores/overview.js' import { mapActions, mapState } from 'pinia' +import { mapGetters } from 'vuex' const FILTER_UPCOMING = 'upcoming' @@ -124,7 +125,7 @@ export default { this.$store.dispatch('setConfig', { hideNoDueOnOverview: newValue }) }, }, - ...mapGetters(['assignedCardsDashboard']), + ...mapState(useOverviewStore, ['assignedCards']), }, watch: { '$route.params.filter'() {