From e444694dc09e030d5f717400818e21617617849d Mon Sep 17 00:00:00 2001 From: Theo <36564257+theoholl@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:41:40 +0000 Subject: [PATCH 01/24] Adds settings for creating new cards at the top or bottom of stacks Signed-off-by: Theo <36564257+theoholl@users.noreply.github.com> --- lib/Service/ConfigService.php | 21 ++++++++++++++++++++- src/components/DeckAppSettings.vue | 10 ++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index b4afb8fc45..0b9dde777b 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(), + 'stackAddCardAtTop' => $this->isStackAddCardAtTopEnabled() ]; if ($this->groupManager->isAdmin($userId)) { $data['groupLimit'] = $this->get('groupLimit'); @@ -90,6 +91,11 @@ public function get(string $key) { return false; } return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardIdBadge', false); + case 'stackAddCardAtTop': + if ($this->getUserId() === null) { + return false; + } + return $this->isStackAddCardAtTopEnabled(); } return false; } @@ -134,6 +140,15 @@ public function isCardIdBadgeEnabled(): bool { return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'cardIdBadge', $defaultState); } + public function isStackAddCardAtTopEnabled(): bool { + $userId = $this->getUserId(); + if ($userId === null) { + return false; + } + + return (bool)$this->config->getUserValue($userId, Application::APP_ID, 'stackAddCardAtTop', true); + } + public function ensureFederationEnabled() { if (!$this->get('federationEnabled')) { throw new FederationDisabledException(); @@ -181,6 +196,10 @@ public function set($key, $value) { $this->config->setUserValue($userId, Application::APP_ID, 'cardIdBadge', (string)$value); $result = $value; break; + case 'stackAddCardAtTop': + $this->config->setUserValue($userId, Application::APP_ID, 'stackAddCardAtTop', (string)$value); + $result = $value; + break; case 'board': // extra check that user only send one of the allowed board settings and not something random $parts = explode(':', $key, 3); diff --git a/src/components/DeckAppSettings.vue b/src/components/DeckAppSettings.vue index 3935dbff5d..8045bcc2df 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 }) }, }, + stackAddCardAtTop: { + get() { + return this.$store.getters.config('stackAddCardAtTop') !== false + }, + set(newValue) { + this.$store.dispatch('setConfig', { stackAddCardAtTop: newValue }) + }, + }, cardIdBadge: { get() { return this.$store.getters.config('cardIdBadge') From e2b6939ecc406e64d3617f8d98ac7164b25b6d2d Mon Sep 17 00:00:00 2001 From: Theo <36564257+theoholl@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:01:20 +0000 Subject: [PATCH 02/24] Add a button for adding cards to the top or bottom of a stack Signed-off-by: Theo <36564257+theoholl@users.noreply.github.com> --- src/components/board/Stack.vue | 91 ++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/src/components/board/Stack.vue b/src/components/board/Stack.vue index 0531548e68..2a4c9f315c 100644 --- a/src/components/board/Stack.vue +++ b/src/components/board/Stack.vue @@ -69,14 +69,6 @@ {{ t('deck', 'Delete list') }} - - - {{ t('deck', 'Add card') }} - - - @@ -101,6 +93,39 @@ + +
+ + {{ t('deck', '+ Add card') }} + +
+ + + +
+
+
+ -
-
+ + {{ t('deck', '+ Add card') }} + + { this.$refs.newCardInput.focus() this.animate = false - this.$refs.card[(this.$refs.card.length - 1)].scrollIntoView() + if (this.$refs.card && this.$refs.card.length > 0) { + const index = addCardAtTop ? 0 : this.$refs.card.length - 1 + this.$refs.card[index].scrollIntoView() + } }) if (!this.cardDetailsInModal) { this.$router.push({ name: 'card', params: { cardId: newCard.id } }) @@ -512,10 +566,17 @@ export default { flex-shrink: 0; z-index: 100; display: flex; - padding-bottom: $stack-gap; background-color: var(--color-main-background); position: relative; + &--top { + padding-top: $stack-gap; + } + + &--bottom { + padding-bottom: $stack-gap; + } + // Smooth fade out of the cards at the top &:before { content: ''; From 8e6289010ccf813aca7f3c16310b8d4657c4ae4b Mon Sep 17 00:00:00 2001 From: Theo <36564257+theoholl@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:12:22 +0000 Subject: [PATCH 03/24] Move 'Add card' button at bottom of stack right underneath last card but make it stick in visible area on overflow Signed-off-by: Theo <36564257+theoholl@users.noreply.github.com> --- src/components/board/Stack.vue | 77 ++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/src/components/board/Stack.vue b/src/components/board/Stack.vue index 2a4c9f315c..f954fcf82b 100644 --- a/src/components/board/Stack.vue +++ b/src/components/board/Stack.vue @@ -4,8 +4,12 @@ --> @@ -162,11 +138,11 @@ import { mapGetters, mapState } from 'vuex' import { Container, Draggable } from 'vue-smooth-dnd' import ArchiveIcon from 'vue-material-design-icons/ArchiveOutline.vue' import CheckCircleOutline from 'vue-material-design-icons/CheckCircleOutline.vue' -import PlusIcon from 'vue-material-design-icons/Plus.vue' -import { NcActions, NcActionButton, NcButton, NcModal } from '@nextcloud/vue' -import { showError, showUndo } from '@nextcloud/dialogs' +import { NcActions, NcActionButton, NcModal } from '@nextcloud/vue' +import { showUndo } from '@nextcloud/dialogs' import CardItem from '../cards/CardItem.vue' +import StackCardAdd from './StackCardAdd.vue' import '@nextcloud/dialogs/style.css' import { mapActions } from 'pinia' @@ -178,14 +154,13 @@ export default { components: { NcActions, NcActionButton, - NcButton, CardItem, + StackCardAdd, Container, Draggable, NcModal, ArchiveIcon, CheckCircleOutline, - PlusIcon, }, directives: { ClickOutside, @@ -205,9 +180,6 @@ export default { editing: false, draggingCard: false, copiedStack: '', - newCardTitle: '', - showAddCard: false, - stateCardCreating: false, animate: false, modalArchivAllCardsShow: false, stackTransfer: { @@ -239,14 +211,6 @@ export default { dragHandleSelector() { return this.canEdit && !this.showArchived ? null : '.no-drag' }, - cardDetailsInModal: { - get() { - return this.$store.getters.config('cardDetailsInModal') - }, - set(newValue) { - this.$store.dispatch('setConfig', { cardDetailsInModal: newValue }) - }, - }, stackAddCardAtTop() { return this.$store.getters.config('stackAddCardAtTop') === true }, @@ -254,18 +218,6 @@ export default { return this.canEdit && !this.showArchived && !this.isArchived }, }, - watch: { - showAddCard(newValue) { - if (!newValue) { - this.$store.dispatch('toggleShortcutLock', false) - } else { - this.$nextTick(() => { - this.$refs.newCardInput.focus() - }) - } - }, - }, - mounted() { this.setupAutoscrollOnDrag() }, @@ -273,16 +225,6 @@ export default { methods: { ...mapActions(useTrashbinStore, ['stackUndoDelete']), ...mapActions(useStackStore, ['setDoneStack', 'deleteStack', 'updateStack']), - stopCardCreation(e) { - // For some reason the submit event triggers a MouseEvent that is bubbling to the outside - // so we have to ignore it - e.stopPropagation() - if (this.$refs.newCardInput && this.$refs.newCardInput.parentElement === e.target.parentElement) { - return false - } - this.showAddCard = false - return false - }, async onDropCard(stackId, event) { const { addedIndex, removedIndex, payload } = event const card = Object.assign({}, payload) @@ -343,41 +285,12 @@ export default { cancelEdit() { this.editing = false }, - async clickAddCard() { - this.stateCardCreating = true - try { - const addCardAtTop = this.stackAddCardAtTop - this.animate = true - const newCard = await this.$store.dispatch('addCard', { - title: this.newCardTitle, - stackId: this.stack.id, - boardId: this.stack.boardId, - // Without an order the API appends the card to the end of the stack - ...(addCardAtTop ? { order: 0 } : {}), - }) - if (addCardAtTop) { - // Creating a card does not move the existing cards down, so reorder - await this.$store.dispatch('reorderCard', { ...newCard, order: 0 }) - } - this.newCardTitle = '' - this.showAddCard = true - this.$nextTick(() => { - this.$refs.newCardInput.focus() - this.animate = false - // Refs of a v-for are registered in creation order, not in list order - this.$refs.card?.find((card) => card.id === newCard.id)?.scrollIntoView() - }) - if (!this.cardDetailsInModal) { - this.$router.push({ name: 'card', params: { cardId: newCard.id } }) - } - } catch (e) { - showError('Could not create card: ' + e.response.data.message) - } finally { - this.stateCardCreating = false - } - }, - onCreateCardFocus() { - this.$store.dispatch('toggleShortcutLock', true) + handleCardCreated(newCard) { + this.$nextTick(() => { + this.animate = false + // Refs of a v-for are registered in creation order, not in list order + this.$refs.card?.find((card) => card.id === newCard.id)?.scrollIntoView() + }) }, setupAutoscrollOnDrag() { let timer @@ -436,18 +349,6 @@ export default { } &.stack--add-card-at-top { - .stack__header { - order: 1; - } - - .stack__card-add { - order: 2; - } - - .stack__cards-list { - order: 3; - } - &:after { content: ''; display: block; @@ -573,84 +474,6 @@ export default { } } - .stack__card-add { - flex-shrink: 0; - z-index: 100; - display: flex; - background-color: var(--color-main-background); - position: relative; - - .stack--add-card-at-top & { - padding-top: $stack-gap; - - &:after { - content: ''; - display: block; - position: absolute; - width: 100%; - height: $stack-gap; - bottom: 0; - z-index: 99; - pointer-events: none; - background-image: linear-gradient(180deg, var(--color-main-background) 0%, transparent 100%); - transform: translateY(100%); - } - } - - .stack--add-card-at-bottom & { - padding-bottom: $stack-gap; - } - - // Smooth fade out of the cards at the top - &:before { - content: ''; - display: block; - position: absolute; - width: 100%; - height: $stack-gap; - z-index: 99; - transition: bottom var(--animation-slow); - background-image: linear-gradient(0deg, var(--color-main-background) 0%, transparent 100%); - transform: translateY(-100%); - } - - :deep(.stack__card-add-button.button-vue) { - --button-size: var(--stack-card-add-control-height); - color: var(--color-text-maxcontrast); - - &:hover:not(:disabled), - &:focus-visible { - color: var(--color-main-text); - } - } - - form { - display: flex; - width: 100%; - height: var(--stack-card-add-control-height); - box-sizing: border-box; - border: 2px solid var(--color-border-maxcontrast); - border-radius: var(--border-radius-large); - overflow: hidden; - padding: 2px; - } - - &.icon-loading-small:after, - &.icon-loading-small-dark:after { - margin-inline-start: calc(50% - 25px); - } - - input[type=text] { - flex-grow: 1; - padding-inline-end: 16px; - } - - input { - border: none; - margin: 0; - } - } - .modal__content { width: 25vw; min-width: 250px; diff --git a/src/components/board/StackCardAdd.vue b/src/components/board/StackCardAdd.vue new file mode 100644 index 0000000000..b235cd036e --- /dev/null +++ b/src/components/board/StackCardAdd.vue @@ -0,0 +1,205 @@ + + + + + + +