Merge pull request #5358 from nextcloud/backport/3758/stable28
This commit is contained in:
@@ -40,12 +40,14 @@
|
||||
|
||||
<router-view name="sidebar" :visible="!cardDetailsInModal || !$route.params.cardId" />
|
||||
</div>
|
||||
<KeyboardShortcuts />
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import AppNavigation from './components/navigation/AppNavigation.vue'
|
||||
import KeyboardShortcuts from './components/KeyboardShortcuts.vue'
|
||||
import { NcModal, NcContent, NcAppContent } from '@nextcloud/vue'
|
||||
import { BoardApi } from './services/BoardApi.js'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
@@ -60,6 +62,7 @@ export default {
|
||||
NcModal,
|
||||
NcContent,
|
||||
NcAppContent,
|
||||
KeyboardShortcuts,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
|
||||
<template>
|
||||
<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 class="board-bullet icon-calendar-dark" />
|
||||
<h2 dir="auto">
|
||||
@@ -32,9 +35,6 @@
|
||||
{{ t('deck', 'Add card') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<NcModal v-if="showAddCardModal" class="card-selector" @close="clickHideAddCardModel">
|
||||
<CreateNewCardCustomPicker show-created-notice @cancel="clickHideAddCardModel" />
|
||||
</NcModal>
|
||||
</div>
|
||||
<div v-else-if="board" class="board-title">
|
||||
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
|
||||
@@ -49,9 +49,14 @@
|
||||
<SessionList v-if="isNotifyPushEnabled && presentUsers.length"
|
||||
:sessions="presentUsers" />
|
||||
<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"
|
||||
:value="searchQuery"
|
||||
@focus="$store.dispatch('toggleShortcutLock', true)"
|
||||
@blur="$store.dispatch('toggleShortcutLock', false)"
|
||||
@input="$store.commit('setSearchQuery', $event.target.value)">
|
||||
</div>
|
||||
<div v-if="board && canManage && !showArchived && !board.archived"
|
||||
@@ -70,7 +75,9 @@
|
||||
type="text"
|
||||
class="no-close"
|
||||
:placeholder="t('deck', 'List name')"
|
||||
required>
|
||||
required
|
||||
@focus="$store.dispatch('toggleShortcutLock', true)"
|
||||
@blur="$store.dispatch('toggleShortcutLock', false)">
|
||||
<input v-tooltip="t('deck', 'Add list')"
|
||||
class="icon-confirm"
|
||||
type="submit"
|
||||
@@ -88,6 +95,7 @@
|
||||
@hide="filterVisible=false">
|
||||
<!-- We cannot use NcActions here are the popover trigger does not update on reactive icons -->
|
||||
<NcButton slot="trigger"
|
||||
ref="filterPopover"
|
||||
:title="t('deck', 'Apply filter')"
|
||||
class="filter-button"
|
||||
:type="isFilterActive ? 'primary' : 'tertiary'">
|
||||
@@ -233,6 +241,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { NcActions, NcActionButton, NcAvatar, NcButton, NcPopover, NcModal } from '@nextcloud/vue'
|
||||
import labelStyle from '../mixins/labelStyle.js'
|
||||
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 { isNotifyPushEnabled } from '../sessions.js'
|
||||
import CreateNewCardCustomPicker from '../views/CreateNewCardCustomPicker.vue'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
export default {
|
||||
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() {
|
||||
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('')
|
||||
},
|
||||
methods: {
|
||||
@@ -406,6 +427,23 @@ export default {
|
||||
}
|
||||
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>
|
||||
|
||||
279
src/components/KeyboardShortcuts.vue
Normal file
279
src/components/KeyboardShortcuts.vue
Normal 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>
|
||||
@@ -21,7 +21,7 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="board-wrapper">
|
||||
<div class="board-wrapper" :tabindex="-1">
|
||||
<Controls :board="board" />
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
@@ -86,7 +86,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { Container, Draggable } from 'vue-smooth-dnd'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import Controls from '../Controls.vue'
|
||||
|
||||
@@ -26,10 +26,14 @@
|
||||
|
||||
<template>
|
||||
<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"
|
||||
:tabindex="0"
|
||||
class="card"
|
||||
@click="openCard">
|
||||
@click="openCard"
|
||||
@keyup.self="handleCardKeyboardShortcut"
|
||||
@mouseenter="focus(card.id)">
|
||||
<div v-if="standalone" class="card-related">
|
||||
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" dir="auto" />
|
||||
{{ board.title }} » {{ stack.title }}
|
||||
@@ -47,7 +51,8 @@
|
||||
tabindex="0"
|
||||
contenteditable="true"
|
||||
role="textbox"
|
||||
@blur="blurTitle"
|
||||
@focus="onTitleFocus"
|
||||
@blur="onTitleBlur"
|
||||
@click.stop
|
||||
@keyup.esc="cancelEdit"
|
||||
@keyup.stop>{{ card.title }}</span>
|
||||
@@ -92,6 +97,7 @@ import AttachmentDragAndDrop from '../AttachmentDragAndDrop.vue'
|
||||
import CardMenu from './CardMenu.vue'
|
||||
import CardCover from './CardCover.vue'
|
||||
import DueDate from './badges/DueDate.vue'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
export default {
|
||||
name: 'CardItem',
|
||||
@@ -198,6 +204,10 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focus(card) {
|
||||
card = this.$refs[`card${card}`]
|
||||
card.focus()
|
||||
},
|
||||
openCard() {
|
||||
if (this.dragging) {
|
||||
return
|
||||
@@ -205,7 +215,7 @@ export default {
|
||||
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(() => {})
|
||||
},
|
||||
blurTitle(e) {
|
||||
onTitleBlur(e) {
|
||||
// TODO Handle empty title
|
||||
if (e.target.innerText !== this.card.title) {
|
||||
this.$store.dispatch('updateCardTitle', {
|
||||
@@ -213,9 +223,45 @@ export default {
|
||||
title: e.target.innerText,
|
||||
})
|
||||
}
|
||||
this.$store.dispatch('toggleShortcutLock', false)
|
||||
},
|
||||
onTitleFocus() {
|
||||
this.$store.dispatch('toggleShortcutLock', true)
|
||||
},
|
||||
cancelEdit() {
|
||||
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) {
|
||||
if (this.dragging) {
|
||||
@@ -223,6 +269,18 @@ export default {
|
||||
}
|
||||
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>
|
||||
@@ -253,17 +311,17 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 2px solid var(--color-border-dark);
|
||||
}
|
||||
&.current-card {
|
||||
border: 2px solid var(--color-primary-element);
|
||||
}
|
||||
|
||||
&:focus, &:focus-visible, &:focus-within {
|
||||
outline: none;
|
||||
border: 2px solid var(--color-border-maxcontrast);
|
||||
&.current-card {
|
||||
border: 2px solid var(--color-primary-element);
|
||||
}
|
||||
}
|
||||
|
||||
.card-upper {
|
||||
display: flex;
|
||||
|
||||
228
src/components/modals/HelpModal.vue
Normal file
228
src/components/modals/HelpModal.vue
Normal 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>
|
||||
@@ -62,6 +62,10 @@
|
||||
</template>
|
||||
<template #footer>
|
||||
<NcAppNavigationSettings :title="t('deck', 'Deck settings')">
|
||||
<NcButton @click="showHelp = true">
|
||||
{{ t('deck', 'Keyboard shortcuts') }}
|
||||
</NcButton>
|
||||
<HelpModal v-if="showHelp" @close="showHelp=false" />
|
||||
<div>
|
||||
<div>
|
||||
<input id="toggle-modal"
|
||||
@@ -117,7 +121,7 @@
|
||||
import axios from '@nextcloud/axios'
|
||||
import { mapGetters } from 'vuex'
|
||||
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 AppNavigationBoardCategory from './AppNavigationBoardCategory.vue'
|
||||
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 DeckIcon from './../icons/DeckIcon.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')
|
||||
|
||||
@@ -135,6 +141,7 @@ export default {
|
||||
components: {
|
||||
NcAppNavigation,
|
||||
NcAppNavigationSettings,
|
||||
NcButton,
|
||||
AppNavigationAddBoard,
|
||||
AppNavigationBoardCategory,
|
||||
NcMultiselect,
|
||||
@@ -143,6 +150,7 @@ export default {
|
||||
CalendarIcon,
|
||||
DeckIcon,
|
||||
ShareVariantIcon,
|
||||
HelpModal,
|
||||
},
|
||||
directives: {
|
||||
ClickOutside,
|
||||
@@ -160,6 +168,7 @@ export default {
|
||||
groupLimit: [],
|
||||
groupLimitDisabled: true,
|
||||
canCreate: canCreateState,
|
||||
showHelp: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -196,6 +205,11 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
subscribe('deck:global:toggle-help-dialog', () => {
|
||||
this.showHelp = !this.showHelp
|
||||
})
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.isAdmin) {
|
||||
this.groupLimit = this.$store.getters.config('groupLimit')
|
||||
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
|
||||
if (users.length > 0) {
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -74,6 +74,7 @@ export default new Vuex.Store({
|
||||
activity: [],
|
||||
activityLoadMore: true,
|
||||
filter: { tags: [], users: [], due: '' },
|
||||
shortcutLock: false,
|
||||
},
|
||||
getters: {
|
||||
config: state => (key) => {
|
||||
@@ -307,7 +308,9 @@ export default new Vuex.Store({
|
||||
Vue.delete(state.currentBoard.acl, removeIndex)
|
||||
}
|
||||
},
|
||||
|
||||
TOGGLE_SHORTCUT_LOCK(state, lock) {
|
||||
state.shortcutLock = lock
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async setConfig({ commit }, config) {
|
||||
@@ -515,5 +518,8 @@ export default new Vuex.Store({
|
||||
newOwner,
|
||||
})
|
||||
},
|
||||
toggleShortcutLock({ commit }, lock) {
|
||||
commit('TOGGLE_SHORTCUT_LOCK', lock)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -371,7 +371,7 @@ h2 {
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
min-width: auto;
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
|
||||
Reference in New Issue
Block a user