Merge pull request #5358 from nextcloud/backport/3758/stable28

This commit is contained in:
Julius Härtl
2023-12-07 18:04:57 +01:00
committed by GitHub
11 changed files with 646 additions and 21 deletions

View File

@@ -40,12 +40,14 @@
<router-view name="sidebar" :visible="!cardDetailsInModal || !$route.params.cardId" /> <router-view name="sidebar" :visible="!cardDetailsInModal || !$route.params.cardId" />
</div> </div>
<KeyboardShortcuts />
</NcContent> </NcContent>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import AppNavigation from './components/navigation/AppNavigation.vue' import AppNavigation from './components/navigation/AppNavigation.vue'
import KeyboardShortcuts from './components/KeyboardShortcuts.vue'
import { NcModal, NcContent, NcAppContent } from '@nextcloud/vue' import { NcModal, NcContent, NcAppContent } from '@nextcloud/vue'
import { BoardApi } from './services/BoardApi.js' import { BoardApi } from './services/BoardApi.js'
import { emit, subscribe } from '@nextcloud/event-bus' import { emit, subscribe } from '@nextcloud/event-bus'
@@ -60,6 +62,7 @@ export default {
NcModal, NcModal,
NcContent, NcContent,
NcAppContent, NcAppContent,
KeyboardShortcuts,
}, },
provide() { provide() {
return { return {

View File

@@ -22,6 +22,9 @@
<template> <template>
<div class="controls"> <div class="controls">
<NcModal v-if="showAddCardModal" class="card-selector" @close="clickHideAddCardModel">
<CreateNewCardCustomPicker show-created-notice @cancel="clickHideAddCardModel" />
</NcModal>
<div v-if="overviewName" class="board-title"> <div v-if="overviewName" class="board-title">
<div class="board-bullet icon-calendar-dark" /> <div class="board-bullet icon-calendar-dark" />
<h2 dir="auto"> <h2 dir="auto">
@@ -32,9 +35,6 @@
{{ t('deck', 'Add card') }} {{ t('deck', 'Add card') }}
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
<NcModal v-if="showAddCardModal" class="card-selector" @close="clickHideAddCardModel">
<CreateNewCardCustomPicker show-created-notice @cancel="clickHideAddCardModel" />
</NcModal>
</div> </div>
<div v-else-if="board" class="board-title"> <div v-else-if="board" class="board-title">
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" /> <div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
@@ -49,9 +49,14 @@
<SessionList v-if="isNotifyPushEnabled && presentUsers.length" <SessionList v-if="isNotifyPushEnabled && presentUsers.length"
:sessions="presentUsers" /> :sessions="presentUsers" />
<div v-if="searchQuery || true" class="deck-search"> <div v-if="searchQuery || true" class="deck-search">
<input type="search" <input id="deck-search-input"
ref="search"
:tabindex="0"
type="search"
class="icon-search" class="icon-search"
:value="searchQuery" :value="searchQuery"
@focus="$store.dispatch('toggleShortcutLock', true)"
@blur="$store.dispatch('toggleShortcutLock', false)"
@input="$store.commit('setSearchQuery', $event.target.value)"> @input="$store.commit('setSearchQuery', $event.target.value)">
</div> </div>
<div v-if="board && canManage && !showArchived && !board.archived" <div v-if="board && canManage && !showArchived && !board.archived"
@@ -70,7 +75,9 @@
type="text" type="text"
class="no-close" class="no-close"
:placeholder="t('deck', 'List name')" :placeholder="t('deck', 'List name')"
required> required
@focus="$store.dispatch('toggleShortcutLock', true)"
@blur="$store.dispatch('toggleShortcutLock', false)">
<input v-tooltip="t('deck', 'Add list')" <input v-tooltip="t('deck', 'Add list')"
class="icon-confirm" class="icon-confirm"
type="submit" type="submit"
@@ -88,6 +95,7 @@
@hide="filterVisible=false"> @hide="filterVisible=false">
<!-- We cannot use NcActions here are the popover trigger does not update on reactive icons --> <!-- We cannot use NcActions here are the popover trigger does not update on reactive icons -->
<NcButton slot="trigger" <NcButton slot="trigger"
ref="filterPopover"
:title="t('deck', 'Apply filter')" :title="t('deck', 'Apply filter')"
class="filter-button" class="filter-button"
:type="isFilterActive ? 'primary' : 'tertiary'"> :type="isFilterActive ? 'primary' : 'tertiary'">
@@ -233,6 +241,7 @@
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { NcActions, NcActionButton, NcAvatar, NcButton, NcPopover, NcModal } from '@nextcloud/vue' import { NcActions, NcActionButton, NcAvatar, NcButton, NcPopover, NcModal } from '@nextcloud/vue'
import labelStyle from '../mixins/labelStyle.js' import labelStyle from '../mixins/labelStyle.js'
import ArchiveIcon from 'vue-material-design-icons/Archive.vue' import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
@@ -244,6 +253,7 @@ import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertic
import SessionList from './SessionList.vue' import SessionList from './SessionList.vue'
import { isNotifyPushEnabled } from '../sessions.js' import { isNotifyPushEnabled } from '../sessions.js'
import CreateNewCardCustomPicker from '../views/CreateNewCardCustomPicker.vue' import CreateNewCardCustomPicker from '../views/CreateNewCardCustomPicker.vue'
import { getCurrentUser } from '@nextcloud/auth'
export default { export default {
name: 'Controls', name: 'Controls',
@@ -327,7 +337,18 @@ export default {
} }
}, },
}, },
beforeMount() {
subscribe('deck:board:show-new-card', this.clickShowAddCardModel)
subscribe('deck:board:toggle-filter-popover', this.triggerOpenFilters)
subscribe('deck:board:clear-filter', this.triggerClearFilter)
subscribe('deck:board:toggle-filter-by-me', this.triggerFilterByMe)
},
beforeDestroy() { beforeDestroy() {
unsubscribe('deck:board:show-new-card', this.clickShowAddCardModel)
unsubscribe('deck:board:toggle-filter-popover', this.triggerOpenFilters)
unsubscribe('deck:board:clear-filter', this.triggerClearFilter)
unsubscribe('deck:board:toggle-filter-by-me', this.triggerFilterByMe)
this.setPageTitle('') this.setPageTitle('')
}, },
methods: { methods: {
@@ -406,6 +427,23 @@ export default {
} }
window.document.title = newTitle window.document.title = newTitle
}, },
triggerOpenFilters() {
this.$refs.filterPopover.$el.click()
},
triggerOpenSearch() {
this.$refs.search.focus()
},
triggerClearFilter() {
this.clearFilter()
},
triggerFilterByMe() {
if (this.isFilterActive) {
this.clearFilter()
} else {
this.filter.users = [getCurrentUser().uid]
this.setFilter()
}
},
}, },
} }
</script> </script>

View File

@@ -0,0 +1,279 @@
<template>
<!-- :style="{top:cardTop, left:cardLeft}" -->
<div v-if="card && selector"
ref="shortcutModal"
v-click-outside="close"
class="keyboard-shortcuts__modal"
tabindex="0"
@keydown.esc="close">
<CardItem :card="card" />
<DueDateSelector v-if="selector === 'due-date'" :card="card" :can-edit="true" />
<TagSelector v-if="selector === 'tag'" :card="card" :can-edit="true" />
<AssignmentSelector v-if="selector === 'assignment'" :card="card" :can-edit="true" />
</div>
</template>
<script>
import DueDateSelector from './card/DueDateSelector.vue'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { mapState } from 'vuex'
import TagSelector from './card/TagSelector.vue'
import AssignmentSelector from './card/AssignmentSelector.vue'
import CardItem from './cards/CardItem.vue'
export default {
name: 'KeyboardShortcuts',
components: {
DueDateSelector,
TagSelector,
AssignmentSelector,
CardItem,
},
data() {
return {
card: null,
cardTop: null,
cardLeft: null,
selector: null,
}
},
computed: {
...mapState({
board: state => state.currentBoard,
}),
},
created() {
document.addEventListener('keydown', this.onKeydown)
subscribe('deck:card:show-assignment-selector', this.handleShowAssignemnt)
subscribe('deck:card:show-due-date-selector', this.handleShowDueDate)
subscribe('deck:card:show-label-selector', this.handleShowLabel)
},
destroyed() {
document.removeEventListener('keydown', this.onKeydown)
unsubscribe('deck:card:show-assignment-selector', this.handleShowAssignemnt)
unsubscribe('deck:card:show-due-date-selector', this.handleShowDueDate)
unsubscribe('deck:card:show-label-selector', this.handleShowLabel)
},
methods: {
onKeydown(key) {
if (OCP.Accessibility.disableKeyboardShortcuts()) {
return
}
if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(key.target.tagName) || key.target.isContentEditable) {
return
}
// Global shortcuts (not board specific)
if ((key.metaKey || key.ctrlKey) && key.code === 'KeyF') {
const searchInput = document.getElementById('deck-search-input')
if (searchInput === document.activeElement) {
return false
}
document.getElementById('deck-search-input').focus()
key.preventDefault()
return true
}
if (key.code === 'Minus') {
emit('deck:global:toggle-help-dialog')
return
}
if (this.$store.state.shortcutLock || key.shiftKey || key.ctrlKey || key.altKey || key.metaKey) {
return
}
if (this.$route.name === 'card' && key.code === 'Escape') {
this.$router.push({ name: 'board' })
return
}
// Board specific shortcuts
if (!this.board) {
return
}
switch (key.code) {
case 'KeyN':
emit('deck:board:show-new-card', this.board.id)
break
case 'KeyF':
emit('deck:board:toggle-filter-popover', this.board.id)
break
case 'KeyX':
emit('deck:board:clear-filter', this.board.id)
break
case 'KeyQ':
emit('deck:board:toggle-filter-by-me', this.board.id)
break
case 'ArrowDown':
this.keyboardFocusDown()
break
case 'ArrowUp':
this.keyboardFocusUp()
break
case 'ArrowLeft':
this.keyboardFocusLeft()
break
case 'ArrowRight':
this.keyboardFocusRight()
break
default:
return
}
key.preventDefault()
},
keyboardFocusDown() {
const activeCard = document.activeElement.closest('.card')
const cards = document.querySelectorAll('.card')
const stacks = document.querySelectorAll('.stack')
const index = Array.from(cards).findIndex(card => card === activeCard)
if (index === -1) {
cards[0]?.focus()
return
}
const currentStack = Array.from(stacks).find(stack => stack.contains(document.activeElement))
const currentStackCards = currentStack.querySelectorAll('.card')
const currentStackIndex = Array.from(currentStackCards).findIndex(card => card === document.activeElement)
if (currentStackIndex === currentStackCards.length - 1) {
return
}
cards[index + 1]?.focus()
cards[index + 1]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
},
keyboardFocusUp() {
const activeCard = document.activeElement.closest('.card')
const cards = document.querySelectorAll('.card')
const stacks = document.querySelectorAll('.stack')
const index = Array.from(cards).findIndex(card => card === activeCard)
if (index === -1) {
cards[0]?.focus()
return
}
const currentStack = Array.from(stacks).find(stack => stack.contains(document.activeElement))
const currentStackCards = currentStack.querySelectorAll('.card')
const currentStackIndex = Array.from(currentStackCards).findIndex(card => card === document.activeElement)
if (currentStackIndex === 0) {
return
}
cards[index - 1]?.focus()
cards[index - 1]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
},
keyboardFocusLeft() {
const activeCard = document.activeElement.closest('.card')
const stacks = document.querySelectorAll('.stack')
const currentStackIndex = Array.from(stacks).findIndex(stack => stack.contains(activeCard))
if (!currentStackIndex === 0) {
return
}
const nextStack = stacks[currentStackIndex - 1] ?? stacks[0]
const currentCardTopOffset = document.activeElement.getBoundingClientRect().top
// iterate over all next stack cards and remember the one with the smallest offset
const nextStackCards = nextStack.querySelectorAll('.card')
let nextCard = null
for (const card of nextStackCards) {
const cardTopOffset = card.getBoundingClientRect().bottom
if (cardTopOffset < currentCardTopOffset) {
continue
}
nextCard = card
break
}
if (!nextCard) {
nextCard = nextStackCards[nextStackCards.length - 1]
}
nextCard?.focus()
nextCard?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
},
keyboardFocusRight() {
const activeCard = document.activeElement.closest('.card')
const stacks = document.querySelectorAll('.stack')
const currentStackIndex = Array.from(stacks).findIndex(stack => stack.contains(activeCard))
if (currentStackIndex === stacks.length - 1) {
return
}
const nextStack = stacks[currentStackIndex + 1]
const currentCardTopOffset = document.activeElement.getBoundingClientRect().top
// iterate over all next stack cards and remember the one with the smallest offset
const nextStackCards = nextStack.querySelectorAll('.card')
let nextCard = null
for (const card of nextStackCards) {
const cardTopOffset = card.getBoundingClientRect().bottom
if (cardTopOffset < currentCardTopOffset) {
continue
}
nextCard = card
break
}
if (!nextCard) {
nextCard = nextStackCards[nextStackCards.length - 1]
}
nextCard?.focus()
nextCard?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
},
handleShowDueDate({ card, element }) {
// this.cardTop = element.getBoundingClientRect().top + 'px'
// this.cardLeft = element.getBoundingClientRect().left + 'px'
this.card = card
this.selector = 'due-date'
this.$refs.shortcutModal?.focus()
},
handleShowAssignemnt({ card, element }) {
// this.cardTop = element.getBoundingClientRect().top + 'px'
// this.cardLeft = element.getBoundingClientRect().left + 'px'
this.card = card
this.selector = 'assignment'
this.$refs.shortcutModal?.focus()
},
handleShowLabel({ card, element }) {
// this.cardTop = element.getBoundingClientRect().top + 'px'
// this.cardLeft = element.getBoundingClientRect().left + 'px'
this.card = card
this.selector = 'tag'
this.$refs.shortcutModal?.focus()
},
close() {
this.card = null
this.selector = null
},
},
}
</script>
<style lang="scss" scoped>
.keyboard-shortcuts__modal {
position: fixed;
z-index: 9999;
box-shadow: 0 0 100px 30px rgba(0, 0, 0, 0.5);
max-width: 500px;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
background-color: var(--color-background-dark);
border-radius: var(--border-radius-rounded);
padding: 24px 32px;
width: 100%;
border: 2px solid var(--color-border-maxcontrast);
}
</style>

View File

@@ -21,7 +21,7 @@
--> -->
<template> <template>
<div class="board-wrapper"> <div class="board-wrapper" :tabindex="-1">
<Controls :board="board" /> <Controls :board="board" />
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@@ -86,7 +86,6 @@
</template> </template>
<script> <script>
import { Container, Draggable } from 'vue-smooth-dnd' import { Container, Draggable } from 'vue-smooth-dnd'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import Controls from '../Controls.vue' import Controls from '../Controls.vue'

View File

@@ -26,10 +26,14 @@
<template> <template>
<AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card"> <AttachmentDragAndDrop v-if="card" :card-id="card.id" class="drop-upload--card">
<div :class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'card__editable': canEdit, 'card__archived': card.archived }" <div :ref="`card${card.id}`"
:class="{'compact': compactMode, 'current-card': currentCard, 'has-labels': card.labels && card.labels.length > 0, 'card__editable': canEdit, 'card__archived': card.archived }"
tag="div" tag="div"
:tabindex="0"
class="card" class="card"
@click="openCard"> @click="openCard"
@keyup.self="handleCardKeyboardShortcut"
@mouseenter="focus(card.id)">
<div v-if="standalone" class="card-related"> <div v-if="standalone" class="card-related">
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" dir="auto" /> <div :style="{backgroundColor: '#' + board.color}" class="board-bullet" dir="auto" />
{{ board.title }} » {{ stack.title }} {{ board.title }} » {{ stack.title }}
@@ -47,7 +51,8 @@
tabindex="0" tabindex="0"
contenteditable="true" contenteditable="true"
role="textbox" role="textbox"
@blur="blurTitle" @focus="onTitleFocus"
@blur="onTitleBlur"
@click.stop @click.stop
@keyup.esc="cancelEdit" @keyup.esc="cancelEdit"
@keyup.stop>{{ card.title }}</span> @keyup.stop>{{ card.title }}</span>
@@ -92,6 +97,7 @@ import AttachmentDragAndDrop from '../AttachmentDragAndDrop.vue'
import CardMenu from './CardMenu.vue' import CardMenu from './CardMenu.vue'
import CardCover from './CardCover.vue' import CardCover from './CardCover.vue'
import DueDate from './badges/DueDate.vue' import DueDate from './badges/DueDate.vue'
import { getCurrentUser } from '@nextcloud/auth'
export default { export default {
name: 'CardItem', name: 'CardItem',
@@ -198,6 +204,10 @@ export default {
}, },
}, },
methods: { methods: {
focus(card) {
card = this.$refs[`card${card}`]
card.focus()
},
openCard() { openCard() {
if (this.dragging) { if (this.dragging) {
return return
@@ -205,7 +215,7 @@ export default {
const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id const boardId = this.card && this.card.boardId ? this.card.boardId : this.$route.params.id
this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id } }).catch(() => {}) this.$router.push({ name: 'card', params: { id: boardId, cardId: this.card.id } }).catch(() => {})
}, },
blurTitle(e) { onTitleBlur(e) {
// TODO Handle empty title // TODO Handle empty title
if (e.target.innerText !== this.card.title) { if (e.target.innerText !== this.card.title) {
this.$store.dispatch('updateCardTitle', { this.$store.dispatch('updateCardTitle', {
@@ -213,9 +223,45 @@ export default {
title: e.target.innerText, title: e.target.innerText,
}) })
} }
this.$store.dispatch('toggleShortcutLock', false)
},
onTitleFocus() {
this.$store.dispatch('toggleShortcutLock', true)
}, },
cancelEdit() { cancelEdit() {
this.$refs.titleContentEditable.textContent = this.card.title this.$refs.titleContentEditable.textContent = this.card.title
this.$store.dispatch('toggleShortcutLock', false)
},
handleCardKeyboardShortcut(key) {
if (OCP.Accessibility.disableKeyboardShortcuts()) {
return
}
if (!this.canEdit || this.$store.state.shortcutLock || key.shiftKey || key.ctrlKey || key.altKey || key.metaKey) {
return
}
switch (key.code) {
case 'KeyE':
this.$refs.titleContentEditable?.focus()
break
case 'KeyA':
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
break
case 'KeyO':
this.$store.dispatch('changeCardDoneStatus', { ...this.card, done: !this.card.done })
break
case 'KeyM':
this.$el.querySelector('button.action-item__menutoggle')?.click()
break
case 'Enter':
case 'Space':
this.openCard().then(() => document.getElementById('app-sidebar-vue')?.focus())
break
case 'KeyS':
this.toggleSelfAsignment()
break
}
}, },
applyLabelFilter(label) { applyLabelFilter(label) {
if (this.dragging) { if (this.dragging) {
@@ -223,6 +269,18 @@ export default {
} }
this.$nextTick(() => this.$store.dispatch('toggleFilter', { tags: [label.id] })) this.$nextTick(() => this.$store.dispatch('toggleFilter', { tags: [label.id] }))
}, },
toggleSelfAsignment() {
const isAssigned = this.card.assignedUsers.find(
(item) => item.type === 0 && item.participant.uid === getCurrentUser()?.uid,
)
this.$store.dispatch(isAssigned ? 'removeUserFromCard' : 'assignCardToUser', {
card: this.card,
assignee: {
userId: getCurrentUser()?.uid,
type: 0,
},
})
},
}, },
} }
</script> </script>
@@ -253,17 +311,17 @@ export default {
cursor: pointer; cursor: pointer;
} }
&:hover {
border: 2px solid var(--color-border-dark);
}
&.current-card { &.current-card {
border: 2px solid var(--color-primary-element); border: 2px solid var(--color-primary-element);
} }
&:focus, &:focus-visible, &:focus-within { &:focus, &:focus-visible, &:focus-within {
outline: none; outline: none;
border: 2px solid var(--color-border-maxcontrast);
&.current-card {
border: 2px solid var(--color-primary-element); border: 2px solid var(--color-primary-element);
} }
}
.card-upper { .card-upper {
display: flex; display: flex;

View File

@@ -0,0 +1,228 @@
<template>
<NcModal size="normal"
data-text-el="formatting-help"
:name="t('deck', 'Keyboard shortcuts')"
@close="$emit('close')">
<div class="help-modal">
<h2>{{ t('deck', 'Keyboard shortcuts') }}</h2>
<p>{{ t('deck', 'Speed up using Deck with simple shortcuts.') }}</p>
<h3>{{ t('deck', 'Board actions') }}</h3>
<table>
<thead>
<tr>
<th>{{ t('deck', 'Keyboard shortcut') }}</th>
<th>{{ t('deck', 'Action') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<kbd>{{ t('deck', 'Shift') }}</kbd>
+
<kbd>{{ t('deck', 'Scroll') }}</kbd>
</td>
<td>
{{ t('deck', 'Scroll sideways') }}
</td>
</tr>
<tr>
<td>
<kbd></kbd>
<kbd></kbd>
<kbd></kbd>
<kbd></kbd>
</td>
<td>
{{ t('deck', 'Navigate between cards') }}
</td>
</tr>
<tr>
<td>
<kbd>{{ t('deck', 'Esc') }}</kbd>
</td>
<td>
{{ t('deck', 'Close card details') }}
</td>
</tr>
<tr>
<td>
<kbd>{{ t('deck', 'Ctrl') }}</kbd>
<kbd>F</kbd>
</td>
<td>
{{ t('deck', 'Search') }}
</td>
</tr>
<tr>
<td>
<kbd>F</kbd>
</td>
<td>
{{ t('deck', 'Show card filters') }}
</td>
</tr>
<tr>
<td>
<kbd>X</kbd>
</td>
<td>
{{ t('deck', 'Clear card filters') }}
</td>
</tr>
<tr>
<td>
<kbd>?</kbd>
</td>
<td>
{{ t('deck', 'Show help dialog') }}
</td>
</tr>
</tbody>
</table>
<h3>{{ t('deck', 'Card actions') }}</h3>
<p>{{ t('deck', 'The following actions can be triggered on the currently highlighted card') }}</p>
<table>
<thead>
<tr>
<th>{{ t('deck', 'Keyboard shortcut') }}</th>
<th>{{ t('deck', 'Action') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<kbd>{{ t('deck', 'Enter') }}</kbd>
<kbd>{{ t('deck', 'Space') }}</kbd>
</td>
<td>
{{ t('deck', 'Open card details') }}
</td>
</tr>
<tr>
<td>
<kbd>E</kbd>
</td>
<td>
{{ t('deck', 'Edit the card title') }}
</td>
</tr>
<tr>
<td>
<kbd>S</kbd>
</td>
<td>
{{ t('deck', 'Assign yorself to the current card') }}
</td>
</tr>
<tr>
<td>
<kbd>A</kbd>
</td>
<td>
{{ t('deck', 'Archive/unarchive the current card') }}
</td>
</tr>
<tr>
<td>
<kbd>O</kbd>
</td>
<td>
{{ t('deck', 'Mark card as completed/not completed') }}
</td>
</tr>
<tr>
<td>
<kbd>M</kbd>
</td>
<td>
{{ t('deck', 'Open card menu') }}
</td>
</tr>
</tbody>
</table>
</div>
</NcModal>
</template>
<script>
import { NcModal, Tooltip } from '@nextcloud/vue'
export default {
name: 'HelpModal',
components: {
NcModal,
},
directives: {
Tooltip,
},
}
</script>
<style lang="scss" scoped>
.help-modal {
width: max-content;
padding: 30px 40px 20px;
user-select: text;
h2, h3 {
font-weight: bold;
}
}
table {
margin-top: 24px;
border-collapse: collapse;
tbody tr {
&:hover, &:focus, &:active {
background-color: transparent !important;
}
}
thead tr {
border: none;
}
th {
font-weight: bold;
padding: .75rem 1rem .75rem 0;
border-bottom: 2px solid var(--color-background-darker);
}
td {
padding: .75rem 1rem .75rem 0;
border-top: 1px solid var(--color-background-dark);
border-bottom: unset;
&.noborder {
border-top: unset;
}
&.ellipsis_top {
padding-bottom: 0;
}
&.ellipsis {
padding-top: 0;
padding-bottom: 0;
}
&.ellipsis_bottom {
padding-top: 0;
}
}
kbd {
font-size: smaller;
}
code {
padding: .2em .4em;
font-size: 90%;
background-color: var(--color-background-dark);
border-radius: 6px;
}
}
</style>

View File

@@ -62,6 +62,10 @@
</template> </template>
<template #footer> <template #footer>
<NcAppNavigationSettings :title="t('deck', 'Deck settings')"> <NcAppNavigationSettings :title="t('deck', 'Deck settings')">
<NcButton @click="showHelp = true">
{{ t('deck', 'Keyboard shortcuts') }}
</NcButton>
<HelpModal v-if="showHelp" @close="showHelp=false" />
<div> <div>
<div> <div>
<input id="toggle-modal" <input id="toggle-modal"
@@ -117,7 +121,7 @@
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import ClickOutside from 'vue-click-outside' import ClickOutside from 'vue-click-outside'
import { NcAppNavigation, NcAppNavigationItem, NcAppNavigationSettings, NcMultiselect } from '@nextcloud/vue' import { NcAppNavigation, NcAppNavigationItem, NcAppNavigationSettings, NcMultiselect, NcButton } from '@nextcloud/vue'
import AppNavigationAddBoard from './AppNavigationAddBoard.vue' import AppNavigationAddBoard from './AppNavigationAddBoard.vue'
import AppNavigationBoardCategory from './AppNavigationBoardCategory.vue' import AppNavigationBoardCategory from './AppNavigationBoardCategory.vue'
import { loadState } from '@nextcloud/initial-state' import { loadState } from '@nextcloud/initial-state'
@@ -127,6 +131,8 @@ import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
import CalendarIcon from 'vue-material-design-icons/Calendar.vue' import CalendarIcon from 'vue-material-design-icons/Calendar.vue'
import DeckIcon from './../icons/DeckIcon.vue' import DeckIcon from './../icons/DeckIcon.vue'
import ShareVariantIcon from 'vue-material-design-icons/Share.vue' import ShareVariantIcon from 'vue-material-design-icons/Share.vue'
import HelpModal from './../modals/HelpModal.vue'
import { subscribe } from '@nextcloud/event-bus'
const canCreateState = loadState('deck', 'canCreate') const canCreateState = loadState('deck', 'canCreate')
@@ -135,6 +141,7 @@ export default {
components: { components: {
NcAppNavigation, NcAppNavigation,
NcAppNavigationSettings, NcAppNavigationSettings,
NcButton,
AppNavigationAddBoard, AppNavigationAddBoard,
AppNavigationBoardCategory, AppNavigationBoardCategory,
NcMultiselect, NcMultiselect,
@@ -143,6 +150,7 @@ export default {
CalendarIcon, CalendarIcon,
DeckIcon, DeckIcon,
ShareVariantIcon, ShareVariantIcon,
HelpModal,
}, },
directives: { directives: {
ClickOutside, ClickOutside,
@@ -160,6 +168,7 @@ export default {
groupLimit: [], groupLimit: [],
groupLimitDisabled: true, groupLimitDisabled: true,
canCreate: canCreateState, canCreate: canCreateState,
showHelp: false,
} }
}, },
computed: { computed: {
@@ -196,6 +205,11 @@ export default {
}, },
}, },
}, },
mounted() {
subscribe('deck:global:toggle-help-dialog', () => {
this.showHelp = !this.showHelp
})
},
beforeMount() { beforeMount() {
if (this.isAdmin) { if (this.isAdmin) {
this.groupLimit = this.$store.getters.config('groupLimit') this.groupLimit = this.$store.getters.config('groupLimit')

View File

@@ -50,7 +50,7 @@ export default {
if (users.length > 0) { if (users.length > 0) {
users.forEach((user) => { users.forEach((user) => {
if (card.assignedUsers.findIndex((u) => u.participant.uid === user) === -1) { if (!card?.assignedUsers || card.assignedUsers.findIndex((u) => u.participant.uid === user) === -1) {
allUsersMatch = false allUsersMatch = false
} }
}) })

View File

@@ -74,6 +74,7 @@ export default new Vuex.Store({
activity: [], activity: [],
activityLoadMore: true, activityLoadMore: true,
filter: { tags: [], users: [], due: '' }, filter: { tags: [], users: [], due: '' },
shortcutLock: false,
}, },
getters: { getters: {
config: state => (key) => { config: state => (key) => {
@@ -307,7 +308,9 @@ export default new Vuex.Store({
Vue.delete(state.currentBoard.acl, removeIndex) Vue.delete(state.currentBoard.acl, removeIndex)
} }
}, },
TOGGLE_SHORTCUT_LOCK(state, lock) {
state.shortcutLock = lock
},
}, },
actions: { actions: {
async setConfig({ commit }, config) { async setConfig({ commit }, config) {
@@ -515,5 +518,8 @@ export default new Vuex.Store({
newOwner, newOwner,
}) })
}, },
toggleShortcutLock({ commit }, lock) {
commit('TOGGLE_SHORTCUT_LOCK', lock)
},
}, },
}) })

View File

@@ -371,7 +371,7 @@ h2 {
} }
.multiselect { .multiselect {
min-width: auto; min-width: auto !important;
} }
.empty-content { .empty-content {