From f6088ace996071e2cbb3b9476212e4bf4a4d3189 Mon Sep 17 00:00:00 2001
From: Simon Kern <14980558+skjerns@users.noreply.github.com>
Date: Fri, 30 Oct 2026 06:25:54 +0100
Subject: [PATCH 1/4] feat(filter): support filtering cards by creator in store
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Simon Kern <14980558+skjerns@users.noreply.github.com>
---
src/helpers/cardFilters.js | 17 ++++++++++++++++
src/helpers/cardFilters.spec.js | 36 +++++++++++++++++++++++++++++++++
src/stores/board.js | 4 ++--
src/stores/card.js | 7 ++++++-
4 files changed, 61 insertions(+), 3 deletions(-)
create mode 100644 src/helpers/cardFilters.js
create mode 100644 src/helpers/cardFilters.spec.js
diff --git a/src/helpers/cardFilters.js b/src/helpers/cardFilters.js
new file mode 100644
index 0000000000..ca628813cd
--- /dev/null
+++ b/src/helpers/cardFilters.js
@@ -0,0 +1,17 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Check whether a card was created by one of the given users.
+ * The card owner is either a user id or a user object.
+ *
+ * @param {object} card the card to check
+ * @param {string[]} uids the user ids to match against
+ * @return {boolean}
+ */
+export function cardCreatedByAny(card, uids) {
+ const ownerUid = card.owner?.uid ?? card.owner
+ return uids.includes(ownerUid)
+}
diff --git a/src/helpers/cardFilters.spec.js b/src/helpers/cardFilters.spec.js
new file mode 100644
index 0000000000..f8c760056a
--- /dev/null
+++ b/src/helpers/cardFilters.spec.js
@@ -0,0 +1,36 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { describe, expect, it } from '@jest/globals'
+import { cardCreatedByAny } from './cardFilters.js'
+
+describe('cardCreatedByAny', () => {
+ it('matches an owner given as user id', () => {
+ expect(cardCreatedByAny({ owner: 'alice' }, ['alice'])).toBe(true)
+ })
+
+ it('matches an owner given as user object', () => {
+ expect(cardCreatedByAny({ owner: { uid: 'alice', displayname: 'Alice' } }, ['alice'])).toBe(true)
+ })
+
+ it('matches if the owner is any of several users', () => {
+ expect(cardCreatedByAny({ owner: 'bob' }, ['alice', 'bob'])).toBe(true)
+ expect(cardCreatedByAny({ owner: { uid: 'bob' } }, ['alice', 'bob'])).toBe(true)
+ })
+
+ it('does not match other owners', () => {
+ expect(cardCreatedByAny({ owner: 'carol' }, ['alice', 'bob'])).toBe(false)
+ expect(cardCreatedByAny({ owner: { uid: 'carol' } }, ['alice', 'bob'])).toBe(false)
+ })
+
+ it('does not match a card without owner', () => {
+ expect(cardCreatedByAny({ owner: null }, ['alice'])).toBe(false)
+ expect(cardCreatedByAny({}, ['alice'])).toBe(false)
+ })
+
+ it('does not match when no users are given', () => {
+ expect(cardCreatedByAny({ owner: 'alice' }, [])).toBe(false)
+ })
+})
diff --git a/src/stores/board.js b/src/stores/board.js
index d0f2d2b25e..651fc006e0 100644
--- a/src/stores/board.js
+++ b/src/stores/board.js
@@ -25,7 +25,7 @@ export const useBoardStore = defineStore('board', {
showArchived: false,
viewModeByBoard: {},
assignableUsers: [],
- filter: { tags: [], users: [], due: '', unassigned: false, completed: 'both' },
+ filter: { tags: [], users: [], creators: [], due: '', unassigned: false, completed: 'both' },
boardFilter: BOARD_FILTERS.ALL,
boards: loadState('deck', 'initialBoards', {}),
}),
@@ -145,7 +145,7 @@ export const useBoardStore = defineStore('board', {
localStorage.setItem(`deck.viewMode.${this.currentBoard.id}`, mode)
},
async loadBoardById(boardId) {
- this.filter = { tags: [], users: [], due: '', unassigned: false, completed: 'both' }
+ this.filter = { tags: [], users: [], creators: [], due: '', unassigned: false, completed: 'both' }
this.setCurrentBoard(null)
const board = await apiClient.loadById(boardId)
this.setCurrentBoard(board)
diff --git a/src/stores/card.js b/src/stores/card.js
index 666b042a0e..38a090e766 100644
--- a/src/stores/card.js
+++ b/src/stores/card.js
@@ -5,6 +5,7 @@
import moment from 'moment'
import { defineStore } from 'pinia'
+import { cardCreatedByAny } from '../helpers/cardFilters.js'
import { CardApi } from '../services/CardApi.js'
import { useBoardStore } from './board.js'
import { useSettingsStore } from './settings.js'
@@ -20,7 +21,7 @@ export const useCardStore = defineStore('card', {
getters: {
cardsByStack(state) {
return (id) => state.cards.filter((card) => {
- const { tags, users, due, unassigned, completed } = useBoardStore().filter
+ const { tags, users, creators, due, unassigned, completed } = useBoardStore().filter
if (completed === 'open' && card.done !== null) {
return false
}
@@ -49,6 +50,10 @@ export const useCardStore = defineStore('card', {
}
}
+ if (creators.length > 0 && !cardCreatedByAny(card, creators)) {
+ return false
+ }
+
if (unassigned && card.assignedUsers.length > 0) {
return false
}
From 26f5681d22a72e2ed77092b1042d31a2fc9c3d96 Mon Sep 17 00:00:00 2001
From: Simon Kern <14980558+skjerns@users.noreply.github.com>
Date: Fri, 30 Oct 2026 06:25:54 +0100
Subject: [PATCH 2/4] feat(filter): add "Filter by creator" section to board
filter
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Simon Kern <14980558+skjerns@users.noreply.github.com>
---
src/components/Controls.vue | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/src/components/Controls.vue b/src/components/Controls.vue
index 2c09e1fbe7..b0e4febb9a 100644
--- a/src/components/Controls.vue
+++ b/src/components/Controls.vue
@@ -146,6 +146,22 @@
:hideStatus="true" /> {{ user.displayname }}
+
{{ t('deck', 'Filter by creator') }}
+
+
+
+
+
{{ t('deck', 'Filter by status') }}
Date: Mon, 9 Nov 2026 19:44:21 +0100
Subject: [PATCH 3/4] fix(filter): list all card creators in creator filter
Include creators who no longer have access to the board, and skip removed users.
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Simon Kern <14980558+skjerns@users.noreply.github.com>
---
src/components/Controls.vue | 15 +++++++++++++--
src/helpers/cardFilters.js | 20 ++++++++++++++++++++
src/helpers/cardFilters.spec.js | 33 ++++++++++++++++++++++++++++++++-
3 files changed, 65 insertions(+), 3 deletions(-)
diff --git a/src/components/Controls.vue b/src/components/Controls.vue
index b0e4febb9a..570f02996d 100644
--- a/src/components/Controls.vue
+++ b/src/components/Controls.vue
@@ -147,7 +147,7 @@
{{ t('deck', 'Filter by creator') }}
-
+
state.showCardCover,
}),
- ...mapState(useCardStore, ['cardById']),
+ ...mapState(useCardStore, ['cardById', 'cards']),
+ ...mapState(useStackStore, ['stacksByBoard']),
+ cardCreators() {
+ if (!this.board) {
+ return []
+ }
+ const stackIds = new Set(this.stacksByBoard(this.board.id).map((stack) => stack.id))
+ const boardCards = this.cards.filter((card) => stackIds.has(card.stackId))
+ return cardCreatorUsers(boardCards, this.board.users)
+ },
+
...mapWritableState(useSettingsStore, ['searchQuery']),
pageTitle() {
if (!this.board?.title) {
diff --git a/src/helpers/cardFilters.js b/src/helpers/cardFilters.js
index ca628813cd..b6dcc2bf20 100644
--- a/src/helpers/cardFilters.js
+++ b/src/helpers/cardFilters.js
@@ -15,3 +15,23 @@ export function cardCreatedByAny(card, uids) {
const ownerUid = card.owner?.uid ?? card.owner
return uids.includes(ownerUid)
}
+
+/**
+ * Collect the users that can be picked in the "Filter by creator" list:
+ * the creators of the given cards merged with the given board users,
+ * de-duplicated by uid and sorted by display name. Users without a
+ * display name (e.g. removed accounts) are skipped.
+ *
+ * @param {object[]} cards the cards of the board
+ * @param {object[]} boardUsers the users the board is shared with
+ * @return {object[]}
+ */
+export function cardCreatorUsers(cards, boardUsers = []) {
+ const users = new Map()
+ for (const user of [...boardUsers, ...cards.map((card) => card.owner)]) {
+ if (user?.uid && user.displayname && !users.has(user.uid)) {
+ users.set(user.uid, user)
+ }
+ }
+ return [...users.values()].sort((a, b) => a.displayname.localeCompare(b.displayname))
+}
diff --git a/src/helpers/cardFilters.spec.js b/src/helpers/cardFilters.spec.js
index f8c760056a..777ddf0a4a 100644
--- a/src/helpers/cardFilters.spec.js
+++ b/src/helpers/cardFilters.spec.js
@@ -4,7 +4,7 @@
*/
import { describe, expect, it } from '@jest/globals'
-import { cardCreatedByAny } from './cardFilters.js'
+import { cardCreatedByAny, cardCreatorUsers } from './cardFilters.js'
describe('cardCreatedByAny', () => {
it('matches an owner given as user id', () => {
@@ -34,3 +34,34 @@ describe('cardCreatedByAny', () => {
expect(cardCreatedByAny({ owner: 'alice' }, [])).toBe(false)
})
})
+
+describe('cardCreatorUsers', () => {
+ const alice = { uid: 'alice', displayname: 'Alice' }
+ const bob = { uid: 'bob', displayname: 'Bob' }
+ const carol = { uid: 'carol', displayname: 'Carol' }
+
+ it('includes creators that are no longer board users', () => {
+ const cards = [{ owner: carol }, { owner: alice }]
+ expect(cardCreatorUsers(cards, [alice, bob])).toEqual([alice, bob, carol])
+ })
+
+ it('de-duplicates users by uid', () => {
+ const cards = [{ owner: { ...alice } }, { owner: alice }, { owner: bob }]
+ expect(cardCreatorUsers(cards, [alice])).toEqual([alice, bob])
+ })
+
+ it('sorts users by display name', () => {
+ const cards = [{ owner: carol }, { owner: bob }, { owner: alice }]
+ expect(cardCreatorUsers(cards).map((user) => user.uid)).toEqual(['alice', 'bob', 'carol'])
+ })
+
+ it('skips removed users without display name', () => {
+ const cards = [{ owner: { uid: 'removed', displayname: null } }, { owner: alice }]
+ expect(cardCreatorUsers(cards, [])).toEqual([alice])
+ })
+
+ it('skips cards without owner object', () => {
+ const cards = [{ owner: null }, {}, { owner: 'bob' }]
+ expect(cardCreatorUsers(cards, [alice])).toEqual([alice])
+ })
+})
From 9b7d03f3c9243b912d5c9d78cfd7c8aa0c118212 Mon Sep 17 00:00:00 2001
From: Simon Kern <14980558+skjerns@users.noreply.github.com>
Date: Tue, 10 Nov 2026 17:18:04 +0100
Subject: [PATCH 4/4] feat(filter): group deleted accounts as "Deleted users"
in creator filter
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Simon Kern <14980558+skjerns@users.noreply.github.com>
---
src/components/Controls.vue | 34 +++++++++++++++++++++++----
src/helpers/cardFilters.js | 40 ++++++++++++++++++++++++++++----
src/helpers/cardFilters.spec.js | 41 ++++++++++++++++++++++++++++++++-
src/stores/card.js | 2 +-
4 files changed, 107 insertions(+), 10 deletions(-)
diff --git a/src/components/Controls.vue b/src/components/Controls.vue
index 570f02996d..829bb64c83 100644
--- a/src/components/Controls.vue
+++ b/src/components/Controls.vue
@@ -161,6 +161,16 @@
:disableMenu="true"
:hideStatus="true" /> {{ user.displayname }}
+
+
+
+
{{ t('deck', 'Filter by status') }}
@@ -335,6 +345,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { NcActionButton, NcActions, NcActionSeparator, NcAvatar, NcButton, NcModal, NcPopover, NcTextField } from '@nextcloud/vue'
import { vOnClickOutside } from '@vueuse/components'
import { mapActions, mapState, mapWritableState } from 'pinia'
+import AccountOffIcon from 'vue-material-design-icons/AccountOffOutline.vue'
import ArchiveIcon from 'vue-material-design-icons/ArchiveOutline.vue'
import ArrowCollapseVerticalIcon from 'vue-material-design-icons/ArrowCollapseVertical.vue'
import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertical.vue'
@@ -346,7 +357,7 @@ import TableColumnPlusAfter from 'vue-material-design-icons/TableColumnPlusAfter
import ViewColumnIcon from 'vue-material-design-icons/ViewColumn.vue'
import CreateNewCardCustomPicker from '../views/CreateNewCardCustomPicker.vue'
import SessionList from './SessionList.vue'
-import { cardCreatorUsers } from '../helpers/cardFilters.js'
+import { cardCreatorUsers, DELETED_CREATORS, isCreatorDeleted } from '../helpers/cardFilters.js'
import labelStyle from '../mixins/labelStyle.js'
import { isNotifyPushEnabled } from '../sessions.js'
import { useBoardStore } from '../stores/board.js'
@@ -358,6 +369,7 @@ export default {
name: 'Controls',
components: {
CreateNewCardCustomPicker,
+ AccountOffIcon,
NcModal,
NcActions,
NcActionButton,
@@ -415,6 +427,7 @@ export default {
data() {
return {
+ DELETED_CREATORS,
newStackTitle: '',
stack: '',
isAddStackVisible: false,
@@ -442,13 +455,20 @@ export default {
...mapState(useCardStore, ['cardById', 'cards']),
...mapState(useStackStore, ['stacksByBoard']),
- cardCreators() {
+ boardCards() {
if (!this.board) {
return []
}
const stackIds = new Set(this.stacksByBoard(this.board.id).map((stack) => stack.id))
- const boardCards = this.cards.filter((card) => stackIds.has(card.stackId))
- return cardCreatorUsers(boardCards, this.board.users)
+ return this.cards.filter((card) => stackIds.has(card.stackId))
+ },
+
+ cardCreators() {
+ return cardCreatorUsers(this.boardCards, this.board?.users)
+ },
+
+ hasDeletedCreators() {
+ return this.boardCards.some((card) => isCreatorDeleted(card, this.board.users))
},
...mapWritableState(useSettingsStore, ['searchQuery']),
@@ -753,6 +773,12 @@ export default {
margin-bottom: 2px;
margin-inline-end: 3px;
}
+ .filter--deleted-icon {
+ display: inline-flex;
+ vertical-align: middle;
+ margin-bottom: 2px;
+ margin-inline-end: 3px;
+ }
.label {
padding: 5px;
border-radius: 3px;
diff --git a/src/helpers/cardFilters.js b/src/helpers/cardFilters.js
index b6dcc2bf20..9247f0e3de 100644
--- a/src/helpers/cardFilters.js
+++ b/src/helpers/cardFilters.js
@@ -3,15 +3,47 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+/**
+ * Filter value standing for all cards created by deleted accounts.
+ * Contains a colon, which is not allowed in Nextcloud user ids.
+ */
+export const DELETED_CREATORS = 'deck:deleted-users'
+
+/**
+ * Check whether a card was created by an account that no longer exists.
+ * The server sends the owner as a user object only for existing accounts,
+ * deleted ones come as plain uid. A plain uid of a board user (e.g. a card
+ * just created in this session) does not count as deleted.
+ *
+ * @param {object} card the card to check
+ * @param {object[]} boardUsers the users with access to the board
+ * @return {boolean}
+ */
+export function isCreatorDeleted(card, boardUsers = []) {
+ const owner = card.owner
+ if (!owner) {
+ return false
+ }
+ if (typeof owner === 'string') {
+ return !boardUsers.some((user) => user.uid === owner)
+ }
+ return !owner.displayname
+}
+
/**
* Check whether a card was created by one of the given users.
- * The card owner is either a user id or a user object.
+ * The card owner is either a user id or a user object. The value
+ * DELETED_CREATORS matches cards of all deleted accounts.
*
* @param {object} card the card to check
* @param {string[]} uids the user ids to match against
+ * @param {object[]} boardUsers the users with access to the board, needed for DELETED_CREATORS
* @return {boolean}
*/
-export function cardCreatedByAny(card, uids) {
+export function cardCreatedByAny(card, uids, boardUsers = []) {
+ if (uids.includes(DELETED_CREATORS) && isCreatorDeleted(card, boardUsers)) {
+ return true
+ }
const ownerUid = card.owner?.uid ?? card.owner
return uids.includes(ownerUid)
}
@@ -19,8 +51,8 @@ export function cardCreatedByAny(card, uids) {
/**
* Collect the users that can be picked in the "Filter by creator" list:
* the creators of the given cards merged with the given board users,
- * de-duplicated by uid and sorted by display name. Users without a
- * display name (e.g. removed accounts) are skipped.
+ * de-duplicated by uid and sorted by display name. Deleted accounts are
+ * not listed, see isCreatorDeleted() for grouping them.
*
* @param {object[]} cards the cards of the board
* @param {object[]} boardUsers the users the board is shared with
diff --git a/src/helpers/cardFilters.spec.js b/src/helpers/cardFilters.spec.js
index 777ddf0a4a..e7cae3aa3b 100644
--- a/src/helpers/cardFilters.spec.js
+++ b/src/helpers/cardFilters.spec.js
@@ -4,7 +4,7 @@
*/
import { describe, expect, it } from '@jest/globals'
-import { cardCreatedByAny, cardCreatorUsers } from './cardFilters.js'
+import { cardCreatedByAny, cardCreatorUsers, DELETED_CREATORS, isCreatorDeleted } from './cardFilters.js'
describe('cardCreatedByAny', () => {
it('matches an owner given as user id', () => {
@@ -65,3 +65,42 @@ describe('cardCreatorUsers', () => {
expect(cardCreatorUsers(cards, [alice])).toEqual([alice])
})
})
+
+describe('deleted creators', () => {
+ const boardUsers = [{ uid: 'alice', displayname: 'Alice' }]
+
+ it('treats a plain uid owner outside the board users as deleted', () => {
+ expect(isCreatorDeleted({ owner: 'gone' }, boardUsers)).toBe(true)
+ })
+
+ it('does not treat a plain uid owner of a board user as deleted', () => {
+ expect(isCreatorDeleted({ owner: 'alice' }, boardUsers)).toBe(false)
+ })
+
+ it('does not treat resolved owners as deleted, even without board access', () => {
+ expect(isCreatorDeleted({ owner: { uid: 'bob', displayname: 'Bob' } }, boardUsers)).toBe(false)
+ })
+
+ it('treats a resolved owner without display name as deleted', () => {
+ expect(isCreatorDeleted({ owner: { uid: 'gone', displayname: null } }, boardUsers)).toBe(true)
+ })
+
+ it('does not treat cards without owner as deleted', () => {
+ expect(isCreatorDeleted({ owner: null }, boardUsers)).toBe(false)
+ expect(isCreatorDeleted({}, boardUsers)).toBe(false)
+ })
+
+ it('matches all deleted creators with the DELETED_CREATORS value', () => {
+ expect(cardCreatedByAny({ owner: 'gone' }, [DELETED_CREATORS], boardUsers)).toBe(true)
+ expect(cardCreatedByAny({ owner: 'other-gone' }, [DELETED_CREATORS], boardUsers)).toBe(true)
+ expect(cardCreatedByAny({ owner: 'alice' }, [DELETED_CREATORS], boardUsers)).toBe(false)
+ expect(cardCreatedByAny({ owner: { uid: 'bob', displayname: 'Bob' } }, [DELETED_CREATORS], boardUsers)).toBe(false)
+ })
+
+ it('combines DELETED_CREATORS with selected users', () => {
+ const uids = [DELETED_CREATORS, 'bob']
+ expect(cardCreatedByAny({ owner: 'gone' }, uids, boardUsers)).toBe(true)
+ expect(cardCreatedByAny({ owner: { uid: 'bob', displayname: 'Bob' } }, uids, boardUsers)).toBe(true)
+ expect(cardCreatedByAny({ owner: 'alice' }, uids, boardUsers)).toBe(false)
+ })
+})
diff --git a/src/stores/card.js b/src/stores/card.js
index 38a090e766..3c16e14bf5 100644
--- a/src/stores/card.js
+++ b/src/stores/card.js
@@ -50,7 +50,7 @@ export const useCardStore = defineStore('card', {
}
}
- if (creators.length > 0 && !cardCreatedByAny(card, creators)) {
+ if (creators.length > 0 && !cardCreatedByAny(card, creators, useBoardStore().currentBoard?.users)) {
return false
}