Merge branch 'master' into enh/archiveAllCardsFromStack
This commit is contained in:
@@ -154,6 +154,7 @@ export default {
|
||||
.visualdiff ins {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.visualdiff del {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
@@ -207,8 +207,4 @@ export default {
|
||||
margin: 20px 20px 60px 20px;
|
||||
}
|
||||
|
||||
.modal__content button {
|
||||
float: right;
|
||||
margin: 40px 3px 3px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { CollectionList } from 'nextcloud-vue-collections'
|
||||
export default {
|
||||
name: 'CollaborationView',
|
||||
components: {
|
||||
CollectionList: CollectionList,
|
||||
CollectionList,
|
||||
},
|
||||
computed: {
|
||||
boardId() {
|
||||
|
||||
@@ -31,11 +31,13 @@
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="board" class="board-actions">
|
||||
<div v-if="canManage && !showArchived"
|
||||
<div v-if="canManage && !showArchived && !board.archived"
|
||||
id="stack-add"
|
||||
v-click-outside="hideAddStack">
|
||||
<Actions v-if="!isAddStackVisible" :title="t('deck', 'Add new list')">
|
||||
<ActionButton icon="icon-add" @click.stop="showAddStack" />
|
||||
<Actions v-if="!isAddStackVisible">
|
||||
<ActionButton icon="icon-add" @click.stop="showAddStack">
|
||||
{{ t('deck', 'Add new list') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<form v-else @submit.prevent="addNewStack()">
|
||||
<label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add new list') }}</label>
|
||||
@@ -54,14 +56,14 @@
|
||||
</div>
|
||||
<div class="board-action-buttons">
|
||||
<Popover>
|
||||
<Actions slot="trigger" :style="filterOpacity" :title="t('deck', 'Apply filter')">
|
||||
<ActionButton icon="icon-filter" />
|
||||
<Actions slot="trigger" :title="t('deck', 'Apply filter')">
|
||||
<ActionButton v-if="isFilterActive" icon="icon-filter_set" />
|
||||
<ActionButton v-else icon="icon-filter" />
|
||||
</Actions>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<h3>{{ t('deck', 'Filter by tag') }}</h3>
|
||||
{{ filter }}
|
||||
<div v-for="label in board.labels" :key="label.id" class="filter--item">
|
||||
<input
|
||||
:id="label.id"
|
||||
@@ -74,6 +76,17 @@
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="unassigned"
|
||||
v-model="filter.unassigned"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
value="unassigned"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
|
||||
</div>
|
||||
<div v-for="user in board.users" :key="user.uid" class="filter--item">
|
||||
<input
|
||||
:id="user.uid"
|
||||
@@ -146,6 +159,10 @@
|
||||
@click="beforeSetFilter">
|
||||
<label for="noDue">{{ t('deck', 'No due date') }}</label>
|
||||
</div>
|
||||
|
||||
<Button :disabled="!isFilterActive" @click="clearFilter">
|
||||
{{ t('deck', 'Clear filter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
@@ -196,7 +213,7 @@ export default {
|
||||
stack: '',
|
||||
showArchived: false,
|
||||
isAddStackVisible: false,
|
||||
filter: { tags: [], users: [], due: '' },
|
||||
filter: { tags: [], users: [], due: '', unassigned: false },
|
||||
}
|
||||
},
|
||||
|
||||
@@ -219,11 +236,19 @@ export default {
|
||||
}
|
||||
return 'opacity: .5;'
|
||||
},
|
||||
filterOpacity() {
|
||||
isFilterActive() {
|
||||
if (this.filter.tags.length !== 0 || this.filter.users.length !== 0 || this.filter.due !== '') {
|
||||
return 'opacity: 1;'
|
||||
return true
|
||||
}
|
||||
return 'opacity: .5;'
|
||||
return false
|
||||
},
|
||||
labelsSorted() {
|
||||
return [...this.board.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
board() {
|
||||
this.clearFilter()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -232,8 +257,14 @@ export default {
|
||||
this.filter.due = ''
|
||||
this.$store.dispatch('setFilter', { ...this.filter })
|
||||
}
|
||||
if (e.target.value === 'unassigned') {
|
||||
this.filter.users = []
|
||||
}
|
||||
},
|
||||
setFilter() {
|
||||
if (this.filter.users.length > 0) {
|
||||
this.filter.unassigned = false
|
||||
}
|
||||
this.$nextTick(() => this.$store.dispatch('setFilter', { ...this.filter }))
|
||||
},
|
||||
toggleNav() {
|
||||
@@ -266,12 +297,19 @@ export default {
|
||||
this.$router.push({ name: 'board.details' })
|
||||
}
|
||||
},
|
||||
clearFilter() {
|
||||
const filterReset = { tags: [], users: [], due: '' }
|
||||
this.$store.dispatch('setFilter', { ...filterReset })
|
||||
this.filter = filterReset
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.controls {
|
||||
display: flex;
|
||||
|
||||
.board-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -299,20 +337,12 @@ export default {
|
||||
}
|
||||
|
||||
#app-navigation-toggle-custom {
|
||||
position: static;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
display: inline-block !important;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#app-navigation-toggle-custom {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.board-actions {
|
||||
@@ -331,6 +361,7 @@ export default {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.filter--item {
|
||||
input + label {
|
||||
display: block;
|
||||
@@ -347,11 +378,13 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
width: 250px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.filter h3 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'List',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -23,13 +23,33 @@
|
||||
<template>
|
||||
<div class="board-wrapper">
|
||||
<Controls :board="board" />
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="loading" key="loading" class="emptycontent">
|
||||
<div class="icon icon-loading" />
|
||||
<h2>{{ t('deck', 'Loading board') }}</h2>
|
||||
<p />
|
||||
</div>
|
||||
<div v-else-if="board && !loading" key="board" class="board">
|
||||
<EmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
|
||||
{{ t('deck', 'No lists available') }}
|
||||
<template #desc>
|
||||
{{ t('deck', 'Create a new list to add cards to this board') }}
|
||||
<form @submit.prevent="addNewStack()">
|
||||
<input id="new-stack-input-main"
|
||||
v-model="newStackTitle"
|
||||
v-focus
|
||||
type="text"
|
||||
class="no-close"
|
||||
:placeholder="t('deck', 'List name')"
|
||||
required>
|
||||
<input v-tooltip="t('deck', 'Add new list')"
|
||||
class="icon-confirm"
|
||||
type="submit"
|
||||
value="">
|
||||
</form>
|
||||
</template>
|
||||
</EmptyContent>
|
||||
<div v-else-if="!isEmpty && !loading" key="board" class="board">
|
||||
<Container lock-axix="y"
|
||||
orientation="horizontal"
|
||||
:drag-handle-selector="dragHandleSelector"
|
||||
@@ -54,6 +74,7 @@ import { Container, Draggable } from 'vue-smooth-dnd'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import Controls from '../Controls'
|
||||
import Stack from './Stack'
|
||||
import { EmptyContent } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'Board',
|
||||
@@ -62,6 +83,7 @@ export default {
|
||||
Container,
|
||||
Draggable,
|
||||
Stack,
|
||||
EmptyContent,
|
||||
},
|
||||
inject: [
|
||||
'boardApi',
|
||||
@@ -72,9 +94,10 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
newStackTitle: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -91,6 +114,9 @@ export default {
|
||||
dragHandleSelector() {
|
||||
return this.canEdit ? null : '.no-drag'
|
||||
},
|
||||
isEmpty() {
|
||||
return this.stacksByBoard.length === 0
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id: 'fetchData',
|
||||
@@ -117,13 +143,13 @@ export default {
|
||||
this.$store.dispatch('orderStack', { stack: this.stacksByBoard[removedIndex], removedIndex, addedIndex })
|
||||
},
|
||||
|
||||
createStack() {
|
||||
addNewStack() {
|
||||
const newStack = {
|
||||
title: 'FooBar',
|
||||
title: this.newStackTitle,
|
||||
boardId: this.id,
|
||||
order: this.stacksByBoard().length,
|
||||
}
|
||||
this.$store.dispatch('createStack', newStack)
|
||||
this.newStackTitle = ''
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -131,12 +157,25 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import "../../css/animations.scss";
|
||||
@import '../../css/animations.scss';
|
||||
|
||||
$board-spacing: 15px;
|
||||
$stack-spacing: 10px;
|
||||
$stack-width: 300px;
|
||||
|
||||
form {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
|
||||
input[type=text] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.board-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
@@ -89,6 +89,12 @@ export default {
|
||||
li {
|
||||
display: flex;
|
||||
height: 44px;
|
||||
|
||||
&:hover, &:active, &.focus {
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -119,12 +125,5 @@ export default {
|
||||
background-color: transparent;
|
||||
opacity: 0.5;
|
||||
}
|
||||
li {
|
||||
&:hover, &:active, &.focus {
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -178,20 +178,25 @@ export default {
|
||||
#shareWithList {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#shareWithList li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.username {
|
||||
padding: 12px 9px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.board-owner-label {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.avatarLabel {
|
||||
padding: 6px
|
||||
}
|
||||
|
||||
.avatardiv {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 16px;
|
||||
|
||||
@@ -25,21 +25,24 @@
|
||||
<div class="stack">
|
||||
<div v-click-outside="stopCardCreation" class="stack--header">
|
||||
<transition name="fade" mode="out-in">
|
||||
<h3 v-if="!canManage">
|
||||
<h3 v-if="!canManage || isArchived">
|
||||
{{ stack.title }}
|
||||
</h3>
|
||||
<h3 v-else-if="!editing" @click="startEditing(stack)">
|
||||
<h3 v-else-if="!editing"
|
||||
v-tooltip="stack.title"
|
||||
class="stack--title"
|
||||
@click="startEditing(stack)">
|
||||
{{ stack.title }}
|
||||
</h3>
|
||||
<form v-else @submit.prevent="finishedEdit(stack)">
|
||||
<input v-model="copiedStack.title" v-focus type="text">
|
||||
<input v-tooltip="t('deck', 'Add a new stack')"
|
||||
<input v-tooltip="t('deck', 'Add a new list')"
|
||||
class="icon-confirm"
|
||||
type="submit"
|
||||
value="">
|
||||
</form>
|
||||
</transition>
|
||||
<Actions v-if="canManage" :force-menu="true">
|
||||
<Actions v-if="canManage && !isArchived" :force-menu="true">
|
||||
<ActionButton icon="icon-archive" @click="modalArchivAllCardsShow=true">
|
||||
{{ t('deck', 'Archive all cards') }}
|
||||
</ActionButton>
|
||||
@@ -47,7 +50,7 @@
|
||||
{{ t('deck', 'Delete list') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="canEdit && !showArchived">
|
||||
<Actions v-if="canEdit && !showArchived && !isArchived">
|
||||
<ActionButton icon="icon-add" @click.stop="showAddCard=true">
|
||||
{{ t('deck', 'Add card') }}
|
||||
</ActionButton>
|
||||
@@ -57,7 +60,7 @@
|
||||
<Modal v-if="modalArchivAllCardsShow" @close="modalArchivAllCardsShow=false">
|
||||
<div class="modal__content">
|
||||
<h3>{{ t('deck', 'Archive all cards in this list') }}</h3>
|
||||
<progress :value="archiveAllCardsProgress" :max="stackLen" />
|
||||
<progress :value="stackTransfer.current" :max="stackTransfer.total" />
|
||||
<button class="primary" @click="archiveAllCardsFromStack(stack)">
|
||||
{{ t('deck', 'Archive all cards') }}
|
||||
</button>
|
||||
@@ -112,7 +115,9 @@
|
||||
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { Container, Draggable } from 'vue-smooth-dnd'
|
||||
|
||||
import { Actions, ActionButton, Modal } from '@nextcloud/vue'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import CardItem from '../cards/CardItem'
|
||||
|
||||
export default {
|
||||
@@ -141,14 +146,17 @@ export default {
|
||||
stateCardCreating: false,
|
||||
animate: false,
|
||||
modalArchivAllCardsShow: false,
|
||||
archiveAllCardsProgress: null,
|
||||
stackLen: 0,
|
||||
stackTransfer: {
|
||||
total: 0,
|
||||
current: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'canManage',
|
||||
'canEdit',
|
||||
'isArchived',
|
||||
]),
|
||||
...mapState({
|
||||
showArchived: state => state.showArchived,
|
||||
@@ -200,9 +208,9 @@ export default {
|
||||
},
|
||||
archiveAllCardsFromStack(stack) {
|
||||
|
||||
this.stackLen = this.cardsByStack.length
|
||||
this.stackTransfer.total = this.cardsByStack.length
|
||||
this.cardsByStack.forEach((card, index) => {
|
||||
this.archiveAllCardsProgress = index
|
||||
this.stackTransfer.current = index
|
||||
this.$store.dispatch('archiveUnarchiveCard', { ...card, archived: true })
|
||||
})
|
||||
this.modalArchivAllCardsShow = false
|
||||
@@ -234,7 +242,7 @@ export default {
|
||||
})
|
||||
this.$router.push({ name: 'card', params: { cardId: newCard.id } })
|
||||
} catch (e) {
|
||||
OCP.Toast.error('Could not create card: ' + e.response.data.message)
|
||||
showError('Could not create card: ' + e.response.data.message)
|
||||
} finally {
|
||||
this.stateCardCreating = false
|
||||
}
|
||||
@@ -263,12 +271,14 @@ export default {
|
||||
margin: 3px -3px;
|
||||
margin-right: -10px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 3px;
|
||||
background-color: var(--color-main-background-translucent);
|
||||
cursor: grab;
|
||||
|
||||
h3, form {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
cursor: inherit;
|
||||
|
||||
input[type=text] {
|
||||
flex-grow: 1;
|
||||
@@ -276,6 +286,13 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.stack--title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc($stack-width - 60px);
|
||||
}
|
||||
|
||||
.stack--card-add {
|
||||
position: sticky;
|
||||
top: 52px;
|
||||
@@ -310,6 +327,7 @@ export default {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stack .smooth-dnd-container.vertical {
|
||||
margin-top: 3px;
|
||||
}
|
||||
@@ -322,6 +340,7 @@ export default {
|
||||
.slide-top-leave-active {
|
||||
transition: all 100ms ease;
|
||||
}
|
||||
|
||||
.slide-top-enter, .slide-top-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
@@ -334,10 +353,6 @@ export default {
|
||||
min-height: 100px;
|
||||
text-align: center;
|
||||
margin: 20px 20px 20px 20px;
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content button {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="labels">
|
||||
<li v-for="label in labels" :key="label.id" :class="{editing: (editingLabelId === label.id)}">
|
||||
<li v-for="label in labelsSorted" :key="label.id" :class="{editing: (editingLabelId === label.id)}">
|
||||
<!-- Edit Tag -->
|
||||
<template v-if="editingLabelId === label.id">
|
||||
<form class="label-form" @submit.prevent="updateLabel(label)">
|
||||
@@ -14,24 +14,30 @@
|
||||
type="submit"
|
||||
value=""
|
||||
class="icon-confirm">
|
||||
<input v-tooltip="t('deck', 'Cancel')"
|
||||
value=""
|
||||
class="icon-close"
|
||||
@click="editingLabelId = null">
|
||||
<Actions>
|
||||
<ActionButton v-tooltip="{content: missingDataLabel, show: !editLabelObjValidated, trigger: 'manual' }"
|
||||
:disabled="!editLabelObjValidated"
|
||||
icon="icon-close"
|
||||
@click="editingLabelId = null">
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</form>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div :style="{ backgroundColor: `#${label.color}`, color:textColor(label.color) }" class="label-title">
|
||||
<span>{{ label.title }}</span>
|
||||
<div class="label-title" @click="clickEdit(label)">
|
||||
<span :style="{ backgroundColor: `#${label.color}`, color: textColor(label.color) }">{{ label.title }}</span>
|
||||
</div>
|
||||
<button v-if="canManage"
|
||||
v-tooltip="t('deck', 'Edit')"
|
||||
class="icon-rename"
|
||||
@click="clickEdit(label)" />
|
||||
<button v-if="canManage"
|
||||
v-tooltip="t('deck', 'Delete')"
|
||||
class="icon-delete"
|
||||
@click="deleteLabel(label.id)" />
|
||||
<Actions v-if="canManage && !isArchived">
|
||||
<ActionButton icon="icon-rename" @click="clickEdit(label)">
|
||||
{{ t('deck', 'Edit') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="canManage && !isArchived">
|
||||
<ActionButton icon="icon-delete" @click="deleteLabel(label.id)">
|
||||
{{ t('deck', 'Delete') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</template>
|
||||
</li>
|
||||
|
||||
@@ -48,14 +54,15 @@
|
||||
type="submit"
|
||||
value=""
|
||||
class="icon-confirm">
|
||||
<input v-tooltip="t('deck', 'Cancel')"
|
||||
value=""
|
||||
class="icon-close"
|
||||
@click="addLabel=false">
|
||||
<Actions>
|
||||
<ActionButton icon="icon-close" @click="addLabel=false">
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</form>
|
||||
</template>
|
||||
</li>
|
||||
<button v-if="canManage" @click="clickShowAddLabel()">
|
||||
<button v-if="canManage && !isArchived" @click="clickShowAddLabel()">
|
||||
<span class="icon-add" />{{ t('deck', 'Add a new tag') }}
|
||||
</button>
|
||||
</ul>
|
||||
@@ -66,12 +73,14 @@
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import Color from '../../mixins/color'
|
||||
import { ColorPicker } from '@nextcloud/vue'
|
||||
import { ColorPicker, Actions, ActionButton } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'TagsTabSidebar',
|
||||
components: {
|
||||
ColorPicker,
|
||||
Actions,
|
||||
ActionButton,
|
||||
},
|
||||
mixins: [Color],
|
||||
data() {
|
||||
@@ -88,6 +97,7 @@ export default {
|
||||
...mapGetters({
|
||||
labels: 'currentBoardLabels',
|
||||
canManage: 'canManage',
|
||||
isArchived: 'isArchived',
|
||||
}),
|
||||
addLabelObjValidated() {
|
||||
if (this.addLabelObj.title === '') {
|
||||
@@ -111,6 +121,9 @@ export default {
|
||||
|
||||
return true
|
||||
},
|
||||
labelsSorted() {
|
||||
return [...this.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
@@ -145,7 +158,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
$clickable-area: 37px;
|
||||
$clickable-area: 44px;
|
||||
|
||||
.labels li {
|
||||
display: flex;
|
||||
@@ -153,12 +166,23 @@ export default {
|
||||
align-items: stretch;
|
||||
height: $clickable-area;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
flex-grow: 1;
|
||||
border-radius: 3px;
|
||||
padding: 7px;
|
||||
padding: 10px;
|
||||
|
||||
&:hover, span:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
border-radius: 15px;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
}
|
||||
&:not(.editing) button {
|
||||
@@ -191,15 +215,11 @@ export default {
|
||||
display: flex;
|
||||
input[type=text] {
|
||||
flex-grow: 1;
|
||||
margin: 5px;
|
||||
}
|
||||
input[type=submit] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
button,
|
||||
input:not([type='text']):last-child {
|
||||
min-width: $clickable-area;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
margin-left: -1px;
|
||||
width: 35px;
|
||||
background-color: var(--color-main-background);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
routeTo: function() {
|
||||
routeTo() {
|
||||
return {
|
||||
name: 'board',
|
||||
params: { id: this.board.id },
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
<div class="board-list-bullet" />
|
||||
</div>
|
||||
<div class="board-list-title-cell">
|
||||
Title
|
||||
{{ t('deck', 'Title') }}
|
||||
</div>
|
||||
<div class="board-list-avatars-cell">
|
||||
Members
|
||||
{{ t('deck', 'Members') }}
|
||||
</div>
|
||||
<div class="board-list-actions-cell" />
|
||||
</div>
|
||||
<BoardItem v-for="board in filteredBoards" :key="board.id" :board="board" />
|
||||
<BoardItem v-for="board in boardsSorted" :key="board.id" :board="board" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,15 +59,18 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
boardsSorted() {
|
||||
return [...this.filteredBoards].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
filteredBoards() {
|
||||
const query = this.$store.getters.getSearchQuery
|
||||
return this.$store.getters.filteredBoards.filter((board) => {
|
||||
return board.title.toLowerCase().includes(query.toLowerCase())
|
||||
return board.deletedAt <= 0 && board.title.toLowerCase().includes(query.toLowerCase())
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
navFilter: function(value) {
|
||||
navFilter(value) {
|
||||
this.$store.commit('setBoardFilter', value)
|
||||
},
|
||||
},
|
||||
@@ -83,6 +86,11 @@ export default {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.board-list-row:not(.board-list-header-row):hover {
|
||||
transition: background-color 0.3s ease;
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
|
||||
.board-list-header-row {
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,27 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="attachment-list">
|
||||
<ul>
|
||||
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
|
||||
<button class="icon-upload" @click="clickAddNewAttachmment()">
|
||||
{{ t('deck', 'Upload attachment') }}
|
||||
</button>
|
||||
<input ref="localAttachments"
|
||||
type="file"
|
||||
style="display: none;"
|
||||
multiple
|
||||
@change="handleUploadFile">
|
||||
<ul class="attachment-list">
|
||||
<li v-for="attachment in uploadQueue" :key="attachment.name" class="attachment">
|
||||
<a class="fileicon" :style="mimetypeForAttachment('none')" />
|
||||
<div class="details">
|
||||
<a>
|
||||
<div class="filename">
|
||||
<span class="basename">{{ attachment.name }}</span>
|
||||
</div>
|
||||
<progress :value="attachment.progress" max="100" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="attachment in attachments"
|
||||
:key="attachment.id"
|
||||
class="attachment">
|
||||
@@ -53,21 +72,29 @@
|
||||
</Actions>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AttachmentDragAndDrop>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Actions, ActionButton } from '@nextcloud/vue'
|
||||
import AttachmentDragAndDrop from '../AttachmentDragAndDrop'
|
||||
import relativeDate from '../../mixins/relativeDate'
|
||||
import { formatFileSize } from '@nextcloud/files'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { mapState } from 'vuex'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import attachmentUpload from '../../mixins/attachmentUpload'
|
||||
const maxUploadSizeState = loadState('deck', 'maxUploadSize')
|
||||
|
||||
export default {
|
||||
name: 'AttachmentList',
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
AttachmentDragAndDrop,
|
||||
},
|
||||
mixins: [relativeDate],
|
||||
mixins: [relativeDate, attachmentUpload],
|
||||
|
||||
props: {
|
||||
cardId: {
|
||||
type: Number,
|
||||
@@ -84,7 +111,11 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
modalShow: false,
|
||||
file: '',
|
||||
overwriteAttachment: null,
|
||||
isDraggingOver: false,
|
||||
maxUploadSize: maxUploadSizeState,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -101,19 +132,50 @@ export default {
|
||||
}
|
||||
},
|
||||
attachmentUrl() {
|
||||
return (attachment) => OC.generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
|
||||
return (attachment) => generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
|
||||
},
|
||||
formattedFileSize() {
|
||||
return (filesize) => formatFileSize(filesize)
|
||||
},
|
||||
...mapState({
|
||||
currentBoard: state => state.currentBoard,
|
||||
}),
|
||||
isReadOnly() {
|
||||
return !this.$store.getters.canEdit
|
||||
},
|
||||
dropHintText() {
|
||||
if (this.isReadOnly) {
|
||||
return t('deck', 'This board is read only')
|
||||
} else {
|
||||
return t('deck', 'Drop your files to upload')
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
created() {
|
||||
this.$store.dispatch('fetchAttachments', this.cardId)
|
||||
},
|
||||
methods: {
|
||||
handleUploadFile(event) {
|
||||
const files = event.target.files ?? []
|
||||
for (const file of files) {
|
||||
this.onLocalAttachmentSelected(file)
|
||||
}
|
||||
event.target.value = ''
|
||||
},
|
||||
clickAddNewAttachmment() {
|
||||
this.$refs.localAttachments.click()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.icon-upload {
|
||||
padding-left: 35px;
|
||||
background-position: 10px center;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
&.selector {
|
||||
padding: 10px;
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
<DatetimePicker v-model="duedate"
|
||||
:placeholder="t('deck', 'Set a due date')"
|
||||
type="datetime"
|
||||
:minute-step="5"
|
||||
:show-second="false"
|
||||
:format="format"
|
||||
:disabled="saving || !canEdit"
|
||||
confirm />
|
||||
<Actions v-if="canEdit">
|
||||
@@ -129,11 +132,6 @@
|
||||
href="https://deck.readthedocs.io/en/latest/Markdown/"
|
||||
target="_blank"
|
||||
class="icon icon-info" />
|
||||
<Actions v-if="canEdit">
|
||||
<ActionButton v-if="descriptionEditing" icon="icon-attach" @click="showAttachmentModal()">
|
||||
{{ t('deck', 'Add Attachment') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="canEdit">
|
||||
<ActionButton v-if="!descriptionEditing" icon="icon-rename" @click="showEditor()">
|
||||
{{ t('deck', 'Edit description') }}
|
||||
@@ -142,6 +140,11 @@
|
||||
{{ t('deck', 'View description') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<Actions v-if="canEdit">
|
||||
<ActionButton v-if="descriptionEditing" icon="icon-attach" @click="showAttachmentModal()">
|
||||
{{ t('deck', 'Add Attachment') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</h5>
|
||||
|
||||
<div v-if="!descriptionEditing"
|
||||
@@ -149,10 +152,12 @@
|
||||
@click="clickedPreview"
|
||||
v-html="renderedDescription" />
|
||||
<VueEasymde v-else
|
||||
:key="copiedCard.id"
|
||||
ref="markdownEditor"
|
||||
:value="copiedCard.description"
|
||||
v-model="copiedCard.description"
|
||||
:configs="mdeConfig"
|
||||
@input="updateDescription" />
|
||||
@input="updateDescription"
|
||||
@blur="saveDescription" />
|
||||
</AppSidebarTab>
|
||||
|
||||
<AppSidebarTab id="attachments"
|
||||
@@ -202,8 +207,13 @@ import MarkdownItTaskLists from 'markdown-it-task-lists'
|
||||
import { formatFileSize } from '@nextcloud/files'
|
||||
import relativeDate from '../../mixins/relativeDate'
|
||||
import AttachmentList from './AttachmentList'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getLocale } from '@nextcloud/l10n'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
const markdownIt = new MarkdownIt()
|
||||
const markdownIt = new MarkdownIt({
|
||||
linkify: true,
|
||||
})
|
||||
markdownIt.use(MarkdownItTaskLists, { enabled: true, label: true, labelAfter: true })
|
||||
|
||||
const capabilities = window.OC.getCapabilities()
|
||||
@@ -239,6 +249,7 @@ export default {
|
||||
addedLabelToCard: null,
|
||||
copiedCard: null,
|
||||
allLabels: null,
|
||||
locale: getLocale(),
|
||||
|
||||
saving: false,
|
||||
markdownIt: null,
|
||||
@@ -250,13 +261,15 @@ export default {
|
||||
autosave: { enabled: false, uniqueId: 'unique' },
|
||||
toolbar: false,
|
||||
},
|
||||
lastModifiedRelative: null,
|
||||
lastCreatedRemative: null,
|
||||
descriptionSaveTimeout: null,
|
||||
descriptionSaving: false,
|
||||
hasActivity: capabilities && capabilities.activity,
|
||||
hasComments: !!OC.appswebroots['comments'],
|
||||
modalShow: false,
|
||||
format: {
|
||||
stringify: this.stringify,
|
||||
parse: this.parse,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -277,7 +290,7 @@ export default {
|
||||
}
|
||||
},
|
||||
attachmentUrl() {
|
||||
return (attachment) => OC.generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
|
||||
return (attachment) => generateUrl(`/apps/deck/cards/${attachment.cardId}/attachment/${attachment.id}`)
|
||||
},
|
||||
formattedFileSize() {
|
||||
return (filesize) => formatFileSize(filesize)
|
||||
@@ -286,7 +299,7 @@ export default {
|
||||
return this.$store.getters.cardById(this.id)
|
||||
},
|
||||
subtitle() {
|
||||
return t('deck', 'Modified') + ': ' + this.lastModifiedRelative + ' ' + t('deck', 'Created') + ': ' + this.lastCreatedRemative
|
||||
return t('deck', 'Modified') + ': ' + this.relativeDate(this.currentCard.lastModified * 1000) + ' ' + t('deck', 'Created') + ': ' + this.relativeDate(this.currentCard.createdAt * 1000)
|
||||
},
|
||||
formatedAssignables() {
|
||||
return this.assignables.map(item => {
|
||||
@@ -325,7 +338,7 @@ export default {
|
||||
},
|
||||
},
|
||||
renderedDescription() {
|
||||
return markdownIt.render(this.copiedCard.description)
|
||||
return markdownIt.render(this.copiedCard.description || '')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -333,21 +346,19 @@ export default {
|
||||
this.initialize()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
setInterval(this.updateRelativeTimestamps, 10000)
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.updateRelativeTimestamps)
|
||||
},
|
||||
mounted() {
|
||||
this.initialize()
|
||||
},
|
||||
methods: {
|
||||
initialize() {
|
||||
async initialize() {
|
||||
if (!this.currentCard) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.copiedCard) {
|
||||
await this.saveDescription()
|
||||
}
|
||||
|
||||
this.copiedCard = JSON.parse(JSON.stringify(this.currentCard))
|
||||
this.allLabels = this.currentCard.labels
|
||||
|
||||
@@ -362,7 +373,6 @@ export default {
|
||||
}
|
||||
|
||||
this.desc = this.currentCard.description
|
||||
this.updateRelativeTimestamps()
|
||||
},
|
||||
showEditor() {
|
||||
if (!this.canEdit) {
|
||||
@@ -404,13 +414,10 @@ export default {
|
||||
}
|
||||
return match
|
||||
})
|
||||
this.updateDescription(updatedDescription)
|
||||
this.$set(this.copiedCard, 'description', updatedDescription)
|
||||
this.$store.dispatch('updateCardDesc', this.copiedCard)
|
||||
}
|
||||
},
|
||||
updateRelativeTimestamps() {
|
||||
this.lastModifiedRelative = OC.Util.relativeModifiedDate(this.currentCard.lastModified * 1000)
|
||||
this.lastCreatedRemative = OC.Util.relativeModifiedDate(this.currentCard.createdAt * 1000)
|
||||
},
|
||||
setDue() {
|
||||
this.$store.dispatch('updateCardDue', this.copiedCard)
|
||||
},
|
||||
@@ -418,20 +425,21 @@ export default {
|
||||
this.copiedCard.duedate = null
|
||||
this.$store.dispatch('updateCardDue', this.copiedCard)
|
||||
},
|
||||
updateDescription(text) {
|
||||
this.copiedCard.description = text
|
||||
async saveDescription() {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.copiedCard, 'descriptionLastEdit') || this.descriptionSaving) {
|
||||
return
|
||||
}
|
||||
this.descriptionSaving = true
|
||||
await this.$store.dispatch('updateCardDesc', this.copiedCard)
|
||||
delete this.copiedCard.descriptionLastEdit
|
||||
this.descriptionSaving = false
|
||||
},
|
||||
updateDescription() {
|
||||
this.copiedCard.descriptionLastEdit = Date.now()
|
||||
clearTimeout(this.descriptionSaveTimeout)
|
||||
this.descriptionSaveTimeout = setTimeout(async() => {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.copiedCard, 'descriptionLastEdit') || this.descriptionSaving) {
|
||||
return
|
||||
}
|
||||
this.descriptionSaving = true
|
||||
await this.$store.dispatch('updateCardDesc', this.copiedCard)
|
||||
delete this.copiedCard.descriptionLastEdit
|
||||
this.descriptionSaving = false
|
||||
await this.saveDescription()
|
||||
}, 2500)
|
||||
|
||||
},
|
||||
|
||||
closeSidebar() {
|
||||
@@ -482,17 +490,25 @@ export default {
|
||||
}
|
||||
this.$store.dispatch('removeLabel', data)
|
||||
},
|
||||
stringify(date) {
|
||||
return moment(date).locale(this.locale).format('LLL')
|
||||
},
|
||||
parse(value) {
|
||||
return moment(value, 'LLL', this.locale).toDate()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "~easymde/dist/easymde.min.css";
|
||||
@import '~easymde/dist/easymde.min.css';
|
||||
|
||||
.vue-easymde, .CodeMirror {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-preview,
|
||||
.editor-statusbar {
|
||||
display: none;
|
||||
@@ -562,8 +578,8 @@ export default {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
padding: 1px 3px;
|
||||
border-radius: 3px;
|
||||
padding: 0px 5px;
|
||||
border-radius: 15px;
|
||||
font-size: 85%;
|
||||
margin-right: 3px;
|
||||
}
|
||||
@@ -600,23 +616,33 @@ export default {
|
||||
#description-preview {
|
||||
min-height: 100px;
|
||||
|
||||
&::v-deep {
|
||||
@import './../../css/markdown';
|
||||
}
|
||||
|
||||
&::v-deep input {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
&::v-deep a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
width: 25vw;
|
||||
min-width: 250px;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
margin: 20px 20px 60px 20px;
|
||||
min-height: 120px;
|
||||
margin: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.modal__content button {
|
||||
float: right;
|
||||
margin: 40px 3px 3px 0;
|
||||
&::v-deep .attachment-list {
|
||||
flex-shrink: 1;
|
||||
overflow: scroll;
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -21,113 +21,27 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<AttachmentDragAndDrop :card-id="card.id" class="drop-upload--sidebar">
|
||||
<button class="icon-upload" @click="clickAddNewAttachmment()">
|
||||
{{ t('settings', 'Upload attachment') }}
|
||||
</button>
|
||||
<input ref="localAttachments"
|
||||
type="file"
|
||||
style="display: none;"
|
||||
multiple
|
||||
@change="handleUploadFile">
|
||||
<div class="attachment-list">
|
||||
<ul>
|
||||
<li v-for="attachment in uploadQueue" :key="attachment.name" class="attachment">
|
||||
<a class="fileicon" :style="mimetypeForAttachment('none')" />
|
||||
<div class="details">
|
||||
<a>
|
||||
<div class="filename">
|
||||
<span class="basename">{{ attachment.name }}</span>
|
||||
</div>
|
||||
<progress :value="attachment.progress" max="100" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<AttachmentList
|
||||
:card-id="card.id"
|
||||
:removable="true"
|
||||
@deleteAttachment="deleteAttachment"
|
||||
@restoreAttachment="restoreAttachment" />
|
||||
</ul>
|
||||
</div>
|
||||
</AttachmentDragAndDrop>
|
||||
<AttachmentList
|
||||
:card-id="card.id"
|
||||
:removable="true"
|
||||
@deleteAttachment="deleteAttachment"
|
||||
@restoreAttachment="restoreAttachment" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import AttachmentDragAndDrop from '../AttachmentDragAndDrop'
|
||||
import attachmentUpload from '../../mixins/attachmentUpload'
|
||||
import AttachmentList from './AttachmentList'
|
||||
const maxUploadSizeState = loadState('deck', 'maxUploadSize')
|
||||
|
||||
export default {
|
||||
name: 'CardSidebarTabAttachments',
|
||||
components: {
|
||||
AttachmentDragAndDrop,
|
||||
AttachmentList,
|
||||
},
|
||||
mixins: [ attachmentUpload ],
|
||||
props: {
|
||||
card: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalShow: false,
|
||||
file: '',
|
||||
overwriteAttachment: null,
|
||||
isDraggingOver: false,
|
||||
maxUploadSize: maxUploadSizeState,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentBoard: state => state.currentBoard,
|
||||
}),
|
||||
isReadOnly() {
|
||||
return !this.$store.getters.canEdit
|
||||
},
|
||||
dropHintText() {
|
||||
if (this.isReadOnly) {
|
||||
return t('deck', 'This board is read only')
|
||||
} else {
|
||||
return t('deck', 'Drop your files to upload')
|
||||
}
|
||||
},
|
||||
attachments() {
|
||||
return [...this.$store.getters.attachmentsByCard(this.card.id)].sort((a, b) => b.id - a.id)
|
||||
},
|
||||
mimetypeForAttachment() {
|
||||
return (mimetype) => {
|
||||
const url = OC.MimeType.getIconUrl(mimetype)
|
||||
const styles = {
|
||||
'background-image': `url("${url}")`,
|
||||
}
|
||||
return styles
|
||||
}
|
||||
},
|
||||
cardId() {
|
||||
return this.card.id
|
||||
},
|
||||
},
|
||||
created: function() {
|
||||
this.$store.dispatch('fetchAttachments', this.card.id)
|
||||
},
|
||||
methods: {
|
||||
handleUploadFile(event) {
|
||||
const files = event.target.files ?? []
|
||||
for (const file of files) {
|
||||
this.onLocalAttachmentSelected(file)
|
||||
}
|
||||
event.target.value = ''
|
||||
},
|
||||
clickAddNewAttachmment() {
|
||||
this.$refs.localAttachments.click()
|
||||
},
|
||||
deleteAttachment(attachment) {
|
||||
this.$store.dispatch('deleteAttachment', attachment)
|
||||
},
|
||||
@@ -137,94 +51,3 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-upload {
|
||||
padding-left: 35px;
|
||||
background-position: 10px center;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
&.selector {
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
max-width: 500px;
|
||||
min-width: 200px;
|
||||
max-height: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #eee;
|
||||
z-index: 2;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 3px darkgray;
|
||||
overflow: scroll;
|
||||
}
|
||||
h3.attachment-selector {
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
.icon-close {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
li.attachment {
|
||||
display: flex;
|
||||
padding: 3px;
|
||||
min-height: 44px;
|
||||
|
||||
&.deleted {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.fileicon {
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-size: contain;
|
||||
}
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
flex-basis: 50%;
|
||||
line-height: 110%;
|
||||
padding: 2px;
|
||||
}
|
||||
.filename {
|
||||
width: 70%;
|
||||
display: flex;
|
||||
.basename {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.extension {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.filesize, .filedate {
|
||||
font-size: 90%;
|
||||
color: darkgray;
|
||||
}
|
||||
.app-popover-menu-utils {
|
||||
position: relative;
|
||||
right: -10px;
|
||||
button {
|
||||
height: 32px;
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
button.icon-history {
|
||||
width: 44px;
|
||||
}
|
||||
progress {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { Avatar } from '@nextcloud/vue'
|
||||
import CommentItem from './CommentItem'
|
||||
import CommentForm from './CommentForm'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
export default {
|
||||
name: 'CardSidebarTabComments',
|
||||
components: {
|
||||
@@ -54,7 +54,7 @@ export default {
|
||||
return {
|
||||
newComment: '',
|
||||
isLoading: false,
|
||||
currentUser: OC.getCurrentUser(),
|
||||
currentUser: getCurrentUser(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -115,5 +115,5 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../css/comments";
|
||||
@import '../../css/comments';
|
||||
</style>
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../css/comments";
|
||||
@import '../../css/comments';
|
||||
|
||||
.atwho-wrap {
|
||||
width: 100%;
|
||||
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../css/comments";
|
||||
@import '../../css/comments';
|
||||
|
||||
.reply {
|
||||
border-left: 3px solid var(--color-primary-element);
|
||||
@@ -195,6 +195,7 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment--content::v-deep a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
<script>
|
||||
import { Avatar, PopoverMenu, Tooltip } from '@nextcloud/vue'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'AvatarList',
|
||||
@@ -93,10 +94,10 @@ export default {
|
||||
}
|
||||
const user = assignable.participant.uid
|
||||
const size = 32
|
||||
const avatarUrl = OC.generateUrl('/avatar/{user}/{size}',
|
||||
const avatarUrl = generateUrl('/avatar/{user}/{size}',
|
||||
{
|
||||
user: user,
|
||||
size: size,
|
||||
user,
|
||||
size,
|
||||
})
|
||||
return window.location.protocol + '//' + window.location.host + avatarUrl
|
||||
}
|
||||
@@ -142,6 +143,7 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-list {
|
||||
float: right;
|
||||
display: inline-flex;
|
||||
@@ -152,6 +154,9 @@ export default {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
box-sizing: content-box !important;
|
||||
margin-right: -12px;
|
||||
transition: margin-right 0.2s ease-in-out;
|
||||
|
||||
&.icon-more {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -159,15 +164,12 @@ export default {
|
||||
background-color: var(--color-background-dark) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
& {
|
||||
margin-right: -12px;
|
||||
transition: margin-right 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
&:hover div:nth-child(n+2) /deep/ .avatardiv {
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.popovermenu {
|
||||
display: block;
|
||||
margin: 40px -6px;
|
||||
|
||||
@@ -34,138 +34,32 @@
|
||||
|
||||
<AvatarList :users="card.assignedUsers" />
|
||||
|
||||
<div @click.stop.prevent>
|
||||
<Actions v-if="canEdit">
|
||||
<ActionButton v-if="showArchived === false" icon="icon-user" @click="assignCardToMe()">
|
||||
{{ t('deck', 'Assign to me') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-archive" @click="archiveUnarchiveCard()">
|
||||
{{ t('deck', (showArchived ? 'Unarchive card' : 'Archive card')) }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="showArchived === false" icon="icon-delete" @click="deleteCard()">
|
||||
{{ t('deck', 'Delete card') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-external" @click.stop="modalShow=true">
|
||||
{{ t('deck', 'Move card') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-settings-dark" @click="openCard">
|
||||
{{ t('deck', 'Card details') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
<Modal v-if="modalShow" title="Move card to another board" @close="modalShow=false">
|
||||
<div class="modal__content">
|
||||
<Multiselect v-model="selectedBoard"
|
||||
:placeholder="t('deck', 'Select a board')"
|
||||
:options="boards"
|
||||
label="title"
|
||||
@select="loadStacksFromBoard" />
|
||||
<Multiselect v-model="selectedStack"
|
||||
:placeholder="t('deck', 'Select a stack')"
|
||||
:options="stacksFromBoard"
|
||||
label="title" />
|
||||
|
||||
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="moveCard">
|
||||
{{ t('deck', 'Move card') }}
|
||||
</button>
|
||||
<button @click="modalShow=false">
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
<CardMenu :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import AvatarList from './AvatarList'
|
||||
import { Modal, Actions, ActionButton, Multiselect } from '@nextcloud/vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import axios from '@nextcloud/axios'
|
||||
import CardMenu from './CardMenu'
|
||||
|
||||
export default {
|
||||
name: 'CardBadges',
|
||||
components: { AvatarList, Actions, ActionButton, Modal, Multiselect },
|
||||
components: { AvatarList, CardMenu },
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalShow: false,
|
||||
selectedBoard: '',
|
||||
selectedStack: '',
|
||||
stacksFromBoard: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'canEdit',
|
||||
]),
|
||||
...mapState({
|
||||
showArchived: state => state.showArchived,
|
||||
currentBoard: state => state.currentBoard,
|
||||
}),
|
||||
checkListCount() {
|
||||
return (this.card.description.match(/^\s*(\*|-|(\d\.))\s+\[\s*(\s|x)\s*\](.*)$/gim) || []).length
|
||||
return (this.card.description.match(/^\s*([*+-]|(\d\.))\s+\[\s*(\s|x)\s*\](.*)$/gim) || []).length
|
||||
},
|
||||
checkListCheckedCount() {
|
||||
return (this.card.description.match(/^\s*(\*|-|(\d\.))\s+\[\s*x\s*\](.*)$/gim) || []).length
|
||||
},
|
||||
compactMode() {
|
||||
return false
|
||||
return (this.card.description.match(/^\s*([*+-]|(\d\.))\s+\[\s*x\s*\](.*)$/gim) || []).length
|
||||
},
|
||||
card() {
|
||||
return this.$store.getters.cardById(this.id)
|
||||
},
|
||||
isBoardAndStackChoosen() {
|
||||
if (this.selectedBoard === '' || this.selectedStack === '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
boards() {
|
||||
return this.$store.getters.boards.filter(board => {
|
||||
return board.id !== this.currentBoard.id
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openCard() {
|
||||
this.$router.push({ name: 'card', params: { cardId: this.id } })
|
||||
},
|
||||
deleteCard() {
|
||||
this.$store.dispatch('deleteCard', this.card)
|
||||
},
|
||||
archiveUnarchiveCard() {
|
||||
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
||||
},
|
||||
assignCardToMe() {
|
||||
this.copiedCard = Object.assign({}, this.card)
|
||||
this.$store.dispatch('assignCardToUser', {
|
||||
card: this.copiedCard,
|
||||
assignee: {
|
||||
userId: OC.getCurrentUser().uid,
|
||||
type: 0,
|
||||
},
|
||||
})
|
||||
},
|
||||
moveCard() {
|
||||
this.copiedCard = Object.assign({}, this.card)
|
||||
this.copiedCard.stackId = this.selectedStack.id
|
||||
this.$store.dispatch('moveCard', this.copiedCard)
|
||||
this.modalShow = false
|
||||
},
|
||||
async loadStacksFromBoard(board) {
|
||||
try {
|
||||
console.debug(board)
|
||||
const url = OC.generateUrl('/apps/deck/stacks/' + board.id)
|
||||
const response = await axios.get(url)
|
||||
this.stacksFromBoard = response.data
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -233,23 +127,8 @@ export default {
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .125s;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
width: 25vw;
|
||||
min-width: 250px;
|
||||
height: 120px;
|
||||
text-align: center;
|
||||
margin: 20px 20px 60px 20px;
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content button {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
class="card"
|
||||
@click="openCard">
|
||||
<div class="card-upper">
|
||||
<h3 v-if="showArchived || !canEdit">
|
||||
<h3 v-if="isArchived || showArchived || !canEdit">
|
||||
{{ card.title }}
|
||||
</h3>
|
||||
<h3 v-else-if="!editing">
|
||||
@@ -47,15 +47,18 @@
|
||||
<input type="button" class="icon-confirm" @click="finishedEdit(card)">
|
||||
</form>
|
||||
|
||||
<div v-if="!editing" class="right">
|
||||
<div v-if="!editing" class="duedate right">
|
||||
<transition name="zoom">
|
||||
<div v-if="card.duedate" :class="dueIcon">
|
||||
<span>{{ relativeDate }}</span>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<CardMenu v-if="!editing && compactMode" :id="id" class="right" />
|
||||
</div>
|
||||
<transition-group name="zoom"
|
||||
<transition-group v-if="card.labels.length"
|
||||
name="zoom"
|
||||
tag="ul"
|
||||
class="labels"
|
||||
@click="openCard">
|
||||
@@ -78,10 +81,11 @@ import CardBadges from './CardBadges'
|
||||
import Color from '../../mixins/color'
|
||||
import labelStyle from '../../mixins/labelStyle'
|
||||
import AttachmentDragAndDrop from '../AttachmentDragAndDrop'
|
||||
import CardMenu from './CardMenu'
|
||||
|
||||
export default {
|
||||
name: 'CardItem',
|
||||
components: { CardBadges, AttachmentDragAndDrop },
|
||||
components: { CardBadges, AttachmentDragAndDrop, CardMenu },
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
@@ -106,6 +110,7 @@ export default {
|
||||
}),
|
||||
...mapGetters([
|
||||
'canEdit',
|
||||
'isArchived',
|
||||
]),
|
||||
card() {
|
||||
return this.$store.getters.cardById(this.id)
|
||||
@@ -137,16 +142,9 @@ export default {
|
||||
return moment(this.card.duedate).format('LLLL')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentCard(newValue) {
|
||||
if (newValue) {
|
||||
this.$nextTick(() => this.$el.scrollIntoView())
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openCard() {
|
||||
this.$router.push({ name: 'card', params: { cardId: this.id } })
|
||||
this.$router.push({ name: 'card', params: { cardId: this.id } }).catch(() => {})
|
||||
},
|
||||
startEditing(card) {
|
||||
this.copiedCard = Object.assign({}, card)
|
||||
@@ -175,6 +173,10 @@ export default {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0 5px 1px var(--color-box-shadow);
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
box-shadow: 0 0 2px 0 var(--color-box-shadow);
|
||||
@@ -189,7 +191,7 @@ export default {
|
||||
|
||||
.card-upper {
|
||||
display: flex;
|
||||
min-height: 50px;
|
||||
min-height: 44px;
|
||||
form {
|
||||
display: flex;
|
||||
padding: 5px 7px;
|
||||
@@ -200,7 +202,7 @@ export default {
|
||||
|
||||
}
|
||||
h3 {
|
||||
margin: 14px $card-padding;
|
||||
margin: 12px $card-padding;
|
||||
flex-grow: 1;
|
||||
font-size: 100%;
|
||||
overflow-x: hidden;
|
||||
@@ -227,7 +229,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
padding: 3px 7px;
|
||||
padding: 0px 5px;
|
||||
border-radius: 15px;
|
||||
font-size: 85%;
|
||||
margin-right: 3px;
|
||||
@@ -257,10 +259,14 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.duedate {
|
||||
margin-right: 9px;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-right: 9px;
|
||||
}
|
||||
|
||||
.icon.due {
|
||||
@@ -269,6 +275,7 @@ export default {
|
||||
margin-top: 9px;
|
||||
margin-bottom: 9px;
|
||||
padding: 3px 4px;
|
||||
padding-right: 0;
|
||||
font-size: 90%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -284,14 +291,17 @@ export default {
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-primary-text);
|
||||
opacity: .7;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
&.now {
|
||||
background-color: var(--color-warning);
|
||||
opacity: .7;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
&.next {
|
||||
background-color: var(--color-background-dark);
|
||||
opacity: .7;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -303,8 +313,11 @@ export default {
|
||||
}
|
||||
|
||||
.compact {
|
||||
min-height: 50px;
|
||||
min-height: 44px;
|
||||
|
||||
.duedate {
|
||||
margin-right: 0;
|
||||
}
|
||||
&.has-labels {
|
||||
padding-bottom: $card-padding;
|
||||
}
|
||||
|
||||
188
src/components/cards/CardMenu.vue
Normal file
188
src/components/cards/CardMenu.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div @click.stop.prevent>
|
||||
<Actions v-if="canEdit && !isArchived">
|
||||
<ActionButton v-if="showArchived === false && !isCurrentUserAssigned" icon="icon-user" @click="assignCardToMe()">
|
||||
{{ t('deck', 'Assign to me') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="showArchived === false && isCurrentUserAssigned" icon="icon-user" @click="unassignCardFromMe()">
|
||||
{{ t('deck', 'Unassign myself') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-archive" @click="archiveUnarchiveCard()">
|
||||
{{ showArchived ? t('deck', 'Unarchive card') : t('deck', 'Archive card') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="showArchived === false" icon="icon-delete" @click="deleteCard()">
|
||||
{{ t('deck', 'Delete card') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-external" @click.stop="modalShow=true">
|
||||
{{ t('deck', 'Move card') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-settings-dark" @click="openCard">
|
||||
{{ t('deck', 'Card details') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</div>
|
||||
<Modal v-if="modalShow" :title="t('deck', 'Move card to another board')" @close="modalShow=false">
|
||||
<div class="modal__content">
|
||||
<h3>{{ t('deck', 'Move card to another board') }}</h3>
|
||||
<Multiselect v-model="selectedBoard"
|
||||
:placeholder="t('deck', 'Select a board')"
|
||||
:options="boards"
|
||||
:max-height="100"
|
||||
label="title"
|
||||
@select="loadStacksFromBoard" />
|
||||
<Multiselect v-model="selectedStack"
|
||||
:placeholder="t('deck', 'Select a list')"
|
||||
:options="stacksFromBoard"
|
||||
:max-height="100"
|
||||
label="title" />
|
||||
|
||||
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="moveCard">
|
||||
{{ t('deck', 'Move card') }}
|
||||
</button>
|
||||
<button @click="modalShow=false">
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { Modal, Actions, ActionButton, Multiselect } from '@nextcloud/vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
export default {
|
||||
name: 'CardMenu',
|
||||
components: { Actions, ActionButton, Modal, Multiselect },
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalShow: false,
|
||||
selectedBoard: '',
|
||||
selectedStack: '',
|
||||
stacksFromBoard: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'canEdit',
|
||||
'isArchived',
|
||||
]),
|
||||
...mapState({
|
||||
showArchived: state => state.showArchived,
|
||||
currentBoard: state => state.currentBoard,
|
||||
}),
|
||||
|
||||
card() {
|
||||
return this.$store.getters.cardById(this.id)
|
||||
},
|
||||
isBoardAndStackChoosen() {
|
||||
if (this.selectedBoard === '' || this.selectedStack === '') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
boards() {
|
||||
return this.$store.getters.boards.filter(board => {
|
||||
return board.id !== this.currentBoard.id
|
||||
})
|
||||
},
|
||||
isCurrentUserAssigned() {
|
||||
return this.card.assignedUsers.find((item) => item.type === 0 && item.participant.uid === getCurrentUser()?.uid)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openCard() {
|
||||
this.$router.push({ name: 'card', params: { cardId: this.id } })
|
||||
},
|
||||
deleteCard() {
|
||||
this.$store.dispatch('deleteCard', this.card)
|
||||
},
|
||||
archiveUnarchiveCard() {
|
||||
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
||||
},
|
||||
assignCardToMe() {
|
||||
this.$store.dispatch('assignCardToUser', {
|
||||
card: this.card,
|
||||
assignee: {
|
||||
userId: getCurrentUser()?.uid,
|
||||
type: 0,
|
||||
},
|
||||
})
|
||||
},
|
||||
unassignCardFromMe() {
|
||||
this.$store.dispatch('removeUserFromCard', {
|
||||
card: this.card,
|
||||
assignee: {
|
||||
userId: getCurrentUser()?.uid,
|
||||
type: 0,
|
||||
},
|
||||
})
|
||||
},
|
||||
moveCard() {
|
||||
this.copiedCard = Object.assign({}, this.card)
|
||||
this.copiedCard.stackId = this.selectedStack.id
|
||||
this.$store.dispatch('moveCard', this.copiedCard)
|
||||
this.modalShow = false
|
||||
},
|
||||
async loadStacksFromBoard(board) {
|
||||
try {
|
||||
const url = generateUrl('/apps/deck/stacks/' + board.id)
|
||||
const response = await axios.get(url)
|
||||
this.stacksFromBoard = response.data
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal__content {
|
||||
width: 25vw;
|
||||
min-width: 250px;
|
||||
min-height: 120px;
|
||||
text-align: center;
|
||||
margin: 20px 20px 100px 20px;
|
||||
|
||||
.multiselect {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content button {
|
||||
float: right;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -21,67 +21,66 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="app-navigation" :class="{'icon-loading': loading}">
|
||||
<ul id="deck-navigation">
|
||||
<AppNavigationVue :class="{'icon-loading': loading}">
|
||||
<template #list>
|
||||
<AppNavigationBoardCategory
|
||||
id="deck-navigation-all"
|
||||
to="/board"
|
||||
:text="t('deck', 'All boards')"
|
||||
:boards="noneArchivedBoards"
|
||||
:open-on-add-boards="true"
|
||||
icon="icon-deck" />
|
||||
<AppNavigationBoardCategory
|
||||
id="deck-navigation-archived"
|
||||
to="/board/archived"
|
||||
:text="t('deck', 'Archived boards')"
|
||||
:boards="archivedBoards"
|
||||
icon="icon-archive" />
|
||||
<AppNavigationBoardCategory
|
||||
id="deck-navigation-shared"
|
||||
:text="t('deck', 'Shared boards')"
|
||||
to="/board/shared"
|
||||
:text="t('deck', 'Shared with you')"
|
||||
:boards="sharedBoards"
|
||||
icon="icon-shared" />
|
||||
<AppNavigationAddBoard v-if="canCreate" />
|
||||
</ul>
|
||||
<div v-if="isAdmin"
|
||||
id="app-settings"
|
||||
v-click-outside="closeMenu"
|
||||
:class="{open: opened}">
|
||||
<div id="app-settings-header">
|
||||
<button class="settings-button" @click="toggleMenu">
|
||||
{{ t('deck', 'Settings') }}
|
||||
</button>
|
||||
</div>
|
||||
<div id="app-settings-content">
|
||||
<Multiselect v-model="groupLimit"
|
||||
:class="{'icon-loading-small': groupLimitDisabled}"
|
||||
open-direction="bottom"
|
||||
:options="groups"
|
||||
:multiple="true"
|
||||
:disabled="groupLimitDisabled"
|
||||
:placeholder="t('deck', 'Limit deck usage of groups')"
|
||||
label="displayname"
|
||||
track-by="id"
|
||||
@input="updateConfig" />
|
||||
<p>{{ t('deck', 'Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<AppNavigationSettings>
|
||||
<div>
|
||||
<Multiselect v-model="groupLimit"
|
||||
:class="{'icon-loading-small': groupLimitDisabled}"
|
||||
open-direction="bottom"
|
||||
:options="groups"
|
||||
:multiple="true"
|
||||
:disabled="groupLimitDisabled"
|
||||
:placeholder="t('deck', 'Limit deck usage of groups')"
|
||||
label="displayname"
|
||||
track-by="id"
|
||||
@input="updateConfig" />
|
||||
<p>{{ t('deck', 'Limiting Deck will block users not part of those groups from creating their own boards. Users will still be able to work on boards that have been shared with them.') }}</p>
|
||||
</div>
|
||||
</AppNavigationSettings>
|
||||
</template>
|
||||
</AppNavigationVue>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { mapGetters } from 'vuex'
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
import { Multiselect } from '@nextcloud/vue'
|
||||
|
||||
import { AppNavigation as AppNavigationVue, AppNavigationSettings, Multiselect } from '@nextcloud/vue'
|
||||
import AppNavigationAddBoard from './AppNavigationAddBoard'
|
||||
import AppNavigationBoardCategory from './AppNavigationBoardCategory'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
|
||||
|
||||
const canCreateState = loadState('deck', 'canCreate')
|
||||
|
||||
export default {
|
||||
name: 'AppNavigation',
|
||||
components: {
|
||||
AppNavigationVue,
|
||||
AppNavigationSettings,
|
||||
AppNavigationAddBoard,
|
||||
AppNavigationBoardCategory,
|
||||
Multiselect,
|
||||
@@ -118,13 +117,13 @@ export default {
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.isAdmin) {
|
||||
axios.get(OC.generateUrl('apps/deck/config')).then((response) => {
|
||||
axios.get(generateUrl('apps/deck/config')).then((response) => {
|
||||
this.groupLimit = response.data.groupLimit
|
||||
this.groupLimitDisabled = false
|
||||
}, (error) => {
|
||||
console.error('Error while loading groupLimit', error.response)
|
||||
})
|
||||
axios.get(OC.linkToOCS('cloud', 2) + 'groups').then((response) => {
|
||||
axios.get(generateOcsUrl('cloud', 2) + 'groups').then((response) => {
|
||||
this.groups = response.data.ocs.data.groups.reduce((obj, item) => {
|
||||
obj.push({
|
||||
id: item,
|
||||
@@ -138,15 +137,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMenu() {
|
||||
this.opened = !this.opened
|
||||
},
|
||||
closeMenu() {
|
||||
this.opened = false
|
||||
},
|
||||
updateConfig() {
|
||||
this.groupLimitDisabled = true
|
||||
axios.post(OC.generateUrl('apps/deck/config/groupLimit'), {
|
||||
axios.post(generateUrl('apps/deck/config/groupLimit'), {
|
||||
value: this.groupLimit,
|
||||
}).then(() => {
|
||||
this.groupLimitDisabled = false
|
||||
@@ -165,4 +158,8 @@ export default {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .app-navigation-toggle {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,38 +20,30 @@
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<li id="deck-navigation-add"
|
||||
<AppNavigationItem v-if="!editing"
|
||||
:title="t('deck', 'Create new board')"
|
||||
:class="[{'icon-loading-small': loading, 'editing': editing}, classes]">
|
||||
<a class="icon-add" href="#" @click.prevent.stop="startCreateBoard">
|
||||
{{ t('deck', 'Create new board') }}
|
||||
</a>
|
||||
|
||||
<!-- edit entry -->
|
||||
<div v-if="editing" class="app-navigation-entry-edit">
|
||||
<ColorPicker v-model="color" class="app-navigation-entry-bullet-wrapper">
|
||||
<div :style="{ backgroundColor: color }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
|
||||
</ColorPicker>
|
||||
<form @submit.prevent.stop="createBoard">
|
||||
<input :placeholder="t('deck', 'New board title')" type="text" required>
|
||||
<input type="submit" value="" class="icon-confirm">
|
||||
<input type="submit"
|
||||
value=""
|
||||
class="icon-close"
|
||||
@click.stop.prevent="cancelEdit">
|
||||
</form>
|
||||
|
||||
<!-- <ColorPicker v-model="color" /> -->
|
||||
</div>
|
||||
</li>
|
||||
icon="icon-add"
|
||||
@click.prevent.stop="startCreateBoard" />
|
||||
<div v-else class="board-create">
|
||||
<ColorPicker v-model="color" class="app-navigation-entry-bullet-wrapper">
|
||||
<div :style="{ backgroundColor: color }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
|
||||
</ColorPicker>
|
||||
<form @submit.prevent.stop="createBoard">
|
||||
<input :placeholder="t('deck', 'New board title')" type="text" required>
|
||||
<input type="submit" value="" class="icon-confirm">
|
||||
<Actions><ActionButton icon="icon-close" @click.stop.prevent="cancelEdit" /></Actions>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ColorPicker } from '@nextcloud/vue'
|
||||
import { ColorPicker, ActionButton, Actions, AppNavigationItem } from '@nextcloud/vue'
|
||||
|
||||
const randomColor = () => '#' + ((1 << 24) * Math.random() | 0).toString(16)
|
||||
|
||||
export default {
|
||||
name: 'AppNavigationAddBoard',
|
||||
components: { ColorPicker },
|
||||
components: { ColorPicker, AppNavigationItem, ActionButton, Actions },
|
||||
directives: {},
|
||||
props: {},
|
||||
data() {
|
||||
@@ -59,7 +51,7 @@ export default {
|
||||
classes: [],
|
||||
editing: false,
|
||||
loading: false,
|
||||
color: '#000000',
|
||||
color: randomColor(),
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
@@ -72,31 +64,42 @@ export default {
|
||||
createBoard(e) {
|
||||
const title = e.currentTarget.childNodes[0].value
|
||||
this.$store.dispatch('createBoard', {
|
||||
title: title,
|
||||
title,
|
||||
color: this.color.substring(1),
|
||||
})
|
||||
this.editing = false
|
||||
this.color = randomColor()
|
||||
},
|
||||
cancelEdit(e) {
|
||||
this.editing = false
|
||||
this.item.edit.reset(e)
|
||||
this.color = randomColor()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
#app-navigation .app-navigation-entry-edit div {
|
||||
width: auto;
|
||||
display: block;
|
||||
.board-create {
|
||||
order: 1;
|
||||
display: flex;
|
||||
height: 44px;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
input[type='text'] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-navigation-entry-bullet-wrapper {
|
||||
position: absolute;
|
||||
left: 33px;
|
||||
width: 44px !important;
|
||||
margin: 6px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
.color0 {
|
||||
width: 30px !important;
|
||||
margin: 5px;
|
||||
margin-left: 7px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-size: 14px;
|
||||
|
||||
@@ -20,75 +20,80 @@
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<router-link :id="`board-${board.id}`"
|
||||
:title="board.title"
|
||||
:class="[{'icon-loading-small': loading, deleted: deleted, editing: editing }, classes]"
|
||||
<AppNavigationItem v-if="!editing"
|
||||
:title="!deleted ? board.title : undoText"
|
||||
:loading="loading"
|
||||
:to="routeTo"
|
||||
tag="li">
|
||||
<div :style="{ backgroundColor: `#${board.color}` }" class="app-navigation-entry-bullet" />
|
||||
<a href="#">
|
||||
{{ board.title }}
|
||||
</a>
|
||||
:undo="deleted"
|
||||
@undo="unDelete">
|
||||
<AppNavigationIconBullet slot="icon" :color="board.color" />
|
||||
|
||||
<div v-if="actions.length > 0" class="app-navigation-entry-utils">
|
||||
<ul>
|
||||
<li class="app-navigation-entry-utils-menu-button">
|
||||
<button v-if="board.acl.length === 0"
|
||||
v-tooltip="t('deck', 'Share')"
|
||||
class="icon-shared"
|
||||
style="opacity: 0.3"
|
||||
@click="showSidebar" />
|
||||
<button v-else
|
||||
v-tooltip="t('deck', 'Share')"
|
||||
class="icon-shared"
|
||||
@click="showSidebar" />
|
||||
</li>
|
||||
<li class="app-navigation-entry-utils-menu-button">
|
||||
<button v-click-outside="hideMenu" v-tooltip="t('deck', 'Options')" @click="showMenu" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div :class="{ 'open': menuOpen }" class="app-navigation-entry-menu">
|
||||
<PopoverMenu :menu="actions" />
|
||||
</div>
|
||||
<AppNavigationCounter v-if="board.acl.length"
|
||||
slot="counter"
|
||||
class="icon-shared"
|
||||
style="opacity: 0.5" />
|
||||
|
||||
<!-- undo action -->
|
||||
<div v-if="deleted" class="app-navigation-entry-deleted">
|
||||
<div class="app-navigation-entry-deleted-description">
|
||||
{{ undoText }}
|
||||
</div>
|
||||
<button
|
||||
:title="t('settings', 'Undo')"
|
||||
class="app-navigation-entry-deleted-button icon-history"
|
||||
@click="unDelete" />
|
||||
</div>
|
||||
|
||||
<!-- edit entry -->
|
||||
<div v-if="editing" class="app-navigation-entry-edit">
|
||||
<ColorPicker class="app-navigation-entry-bullet-wrapper" :value="`#${board.color}`" @input="updateColor">
|
||||
<div :style="{ backgroundColor: getColor }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
|
||||
</ColorPicker>
|
||||
<form @submit.prevent.stop="applyEdit">
|
||||
<input v-model="editTitle" type="text" required>
|
||||
<input type="submit" value="" class="icon-confirm">
|
||||
<input type="submit"
|
||||
value=""
|
||||
class="icon-close"
|
||||
@click.stop.prevent="cancelEdit">
|
||||
</form>
|
||||
</div>
|
||||
</router-link>
|
||||
<template v-if="!deleted" slot="actions">
|
||||
<ActionButton v-if="canManage && !board.archived"
|
||||
icon="icon-rename"
|
||||
:close-after-click="true"
|
||||
@click="actionEdit">
|
||||
{{ t('deck', 'Edit board') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canManage && !board.archived"
|
||||
icon="icon-clone"
|
||||
:close-after-click="true"
|
||||
@click="actionClone">
|
||||
{{ t('deck', 'Clone board ') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canManage && board.archived"
|
||||
icon="icon-archive"
|
||||
:close-after-click="true"
|
||||
@click="actionUnarchive">
|
||||
{{ t('deck', 'Unarchive board ') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canManage && !board.archived"
|
||||
icon="icon-archive"
|
||||
:close-after-click="true"
|
||||
@click="actionArchive">
|
||||
{{ t('deck', 'Archive board ') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canManage"
|
||||
icon="icon-delete"
|
||||
:close-after-click="true"
|
||||
@click="actionDelete">
|
||||
{{ t('deck', 'Delete board ') }}
|
||||
</ActionButton>
|
||||
<ActionButton icon="icon-more" :close-after-click="true" @click="actionDetails">
|
||||
{{ t('deck', 'Board details') }}
|
||||
</ActionButton>
|
||||
</template>
|
||||
</AppNavigationItem>
|
||||
<div v-else-if="editing" class="board-edit">
|
||||
<ColorPicker class="app-navigation-entry-bullet-wrapper" :value="`#${board.color}`" @input="updateColor">
|
||||
<div :style="{ backgroundColor: getColor }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
|
||||
</ColorPicker>
|
||||
<form @submit.prevent.stop="applyEdit">
|
||||
<input v-model="editTitle" type="text" required>
|
||||
<input type="submit" value="" class="icon-confirm">
|
||||
<Actions><ActionButton icon="icon-close" @click.stop.prevent="cancelEdit" /></Actions>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PopoverMenu, ColorPicker } from '@nextcloud/vue'
|
||||
import { AppNavigationIconBullet, AppNavigationCounter, AppNavigationItem, ColorPicker, Actions, ActionButton } from '@nextcloud/vue'
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
|
||||
export default {
|
||||
name: 'AppNavigationBoard',
|
||||
components: {
|
||||
AppNavigationIconBullet,
|
||||
AppNavigationCounter,
|
||||
AppNavigationItem,
|
||||
ColorPicker,
|
||||
PopoverMenu,
|
||||
Actions,
|
||||
ActionButton,
|
||||
},
|
||||
directives: {
|
||||
ClickOutside,
|
||||
@@ -118,126 +123,17 @@ export default {
|
||||
}
|
||||
return this.board.color
|
||||
},
|
||||
undoText: function() {
|
||||
// todo translation
|
||||
return 'deleted ' + this.board.title
|
||||
undoText() {
|
||||
return t('deck', 'Board {0} deleted', [this.board.title])
|
||||
},
|
||||
routeTo: function() {
|
||||
routeTo() {
|
||||
return {
|
||||
name: 'board',
|
||||
params: { id: this.board.id },
|
||||
}
|
||||
},
|
||||
actions: function() {
|
||||
/* eslint-disable vue/no-side-effects-in-computed-properties */
|
||||
/* eslint-disable vue/no-async-in-computed-properties */
|
||||
const actions = []
|
||||
|
||||
// do not show actions while the item is loading
|
||||
if (this.loading === false) {
|
||||
const canManage = this.board.permissions.PERMISSION_MANAGE
|
||||
|
||||
if (canManage) {
|
||||
actions.push({
|
||||
action: () => {
|
||||
this.hideMenu()
|
||||
this.editTitle = this.board.title
|
||||
this.editColor = '#' + this.board.color
|
||||
this.editing = true
|
||||
},
|
||||
icon: 'icon-rename',
|
||||
text: t('deck', 'Edit board'),
|
||||
})
|
||||
}
|
||||
|
||||
actions.push({
|
||||
action: async() => {
|
||||
this.hideMenu()
|
||||
this.loading = true
|
||||
try {
|
||||
const newBoard = await this.$store.dispatch('cloneBoard', this.board)
|
||||
this.loading = false
|
||||
const route = this.routeTo
|
||||
route.params.id = newBoard.id
|
||||
this.$router.push(route)
|
||||
} catch (e) {
|
||||
OC.Notification.showTemporary(t('deck', 'An error occurred'))
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
icon: 'icon-clone',
|
||||
text: t('deck', 'Clone board'),
|
||||
})
|
||||
if (canManage) {
|
||||
if (!this.board.archived) {
|
||||
actions.push({
|
||||
action: () => {
|
||||
this.hideMenu()
|
||||
this.loading = true
|
||||
this.$store.dispatch('archiveBoard', this.board)
|
||||
},
|
||||
icon: 'icon-archive',
|
||||
text: t('deck', 'Archive board'),
|
||||
})
|
||||
} else {
|
||||
actions.push({
|
||||
action: () => {
|
||||
this.hideMenu()
|
||||
this.loading = true
|
||||
this.$store.dispatch('unarchiveBoard', this.board)
|
||||
},
|
||||
icon: 'icon-archive',
|
||||
text: t('deck', 'Unarchive board'),
|
||||
})
|
||||
}
|
||||
|
||||
actions.push({
|
||||
action: () => {
|
||||
OC.dialogs.confirmDestructive(
|
||||
t('deck', 'Are you sure you want to delete the board {title}? This will delete all the data of this board.', { title: this.board.title }),
|
||||
t('deck', 'Delete the board?'),
|
||||
{
|
||||
type: OC.dialogs.YES_NO_BUTTONS,
|
||||
confirm: t('deck', 'Delete'),
|
||||
confirmClasses: 'error',
|
||||
cancel: t('deck', 'Cancel'),
|
||||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
this.hideMenu()
|
||||
this.loading = true
|
||||
this.boardApi.deleteBoard(this.board)
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
this.deleted = true
|
||||
this.undoTimeoutHandle = setTimeout(() => {
|
||||
this.$store.dispatch('removeBoard', this.board)
|
||||
}, 7000)
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
},
|
||||
icon: 'icon-delete',
|
||||
text: t('deck', 'Delete board'),
|
||||
})
|
||||
}
|
||||
|
||||
actions.push({
|
||||
action: () => {
|
||||
const route = this.routeTo
|
||||
route.name = 'board.details'
|
||||
this.$router.push(route)
|
||||
},
|
||||
icon: 'icon-settings-dark',
|
||||
text: t('deck', 'Board details'),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return actions
|
||||
canManage() {
|
||||
return this.board.permissions.PERMISSION_MANAGE
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
@@ -253,15 +149,66 @@ export default {
|
||||
this.deleted = false
|
||||
})
|
||||
},
|
||||
showMenu() {
|
||||
this.menuOpen = true
|
||||
},
|
||||
hideMenu() {
|
||||
this.menuOpen = false
|
||||
},
|
||||
updateColor(newColor) {
|
||||
this.editColor = newColor
|
||||
},
|
||||
actionEdit() {
|
||||
this.editTitle = this.board.title
|
||||
this.editColor = '#' + this.board.color
|
||||
this.editing = true
|
||||
},
|
||||
async actionClone() {
|
||||
this.loading = true
|
||||
try {
|
||||
const newBoard = await this.$store.dispatch('cloneBoard', this.board)
|
||||
this.loading = false
|
||||
const route = this.routeTo
|
||||
route.params.id = newBoard.id
|
||||
this.$router.push(route)
|
||||
} catch (e) {
|
||||
OC.Notification.showTemporary(t('deck', 'An error occurred'))
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
actionArchive() {
|
||||
this.loading = true
|
||||
this.$store.dispatch('archiveBoard', this.board)
|
||||
},
|
||||
actionUnarchive() {
|
||||
this.loading = true
|
||||
this.$store.dispatch('unarchiveBoard', this.board)
|
||||
},
|
||||
actionDelete() {
|
||||
OC.dialogs.confirmDestructive(
|
||||
t('deck', 'Are you sure you want to delete the board {title}? This will delete all the data of this board.', { title: this.board.title }),
|
||||
t('deck', 'Delete the board?'),
|
||||
{
|
||||
type: OC.dialogs.YES_NO_BUTTONS,
|
||||
confirm: t('deck', 'Delete'),
|
||||
confirmClasses: 'error',
|
||||
cancel: t('deck', 'Cancel'),
|
||||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
this.loading = true
|
||||
this.boardApi.deleteBoard(this.board)
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
this.deleted = true
|
||||
this.undoTimeoutHandle = setTimeout(() => {
|
||||
this.$store.dispatch('removeBoard', this.board)
|
||||
}, 7000)
|
||||
})
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
},
|
||||
actionDetails() {
|
||||
const route = this.routeTo
|
||||
route.name = 'board.details'
|
||||
this.$router.push(route)
|
||||
},
|
||||
applyEdit(e) {
|
||||
this.editing = false
|
||||
if (this.editTitle || this.editColor) {
|
||||
@@ -291,17 +238,29 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#app-navigation #deck-navigation .editing {
|
||||
height: auto !important;
|
||||
.board-edit {
|
||||
margin-left: 44px;
|
||||
order: 1;
|
||||
display: flex;
|
||||
height: 44px;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
input[type='text'] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-navigation-entry-bullet-wrapper {
|
||||
position: absolute;
|
||||
left: 33px;
|
||||
width: 44px !important;
|
||||
margin: 6px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
.color0 {
|
||||
width: 30px !important;
|
||||
margin: 5px;
|
||||
margin-left: 7px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-size: 14px;
|
||||
|
||||
@@ -20,33 +20,31 @@
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<li v-if="boards.length > 0"
|
||||
:id="id"
|
||||
<AppNavigationItem v-if="boards.length > 0"
|
||||
:title="text"
|
||||
:class="{'open': opened, 'collapsible': collapsible }">
|
||||
<button v-if="collapsible" class="collapse" @click.prevent.stop="toggleCollapse" />
|
||||
<a :class="icon" href="#">
|
||||
{{ text }}
|
||||
</a>
|
||||
<ul v-if="boards.length > 0">
|
||||
<AppNavigationBoard v-for="board in boards" :key="board.id" :board="board" />
|
||||
</ul>
|
||||
</li>
|
||||
:icon="icon"
|
||||
:to="to"
|
||||
:allow-collapse="collapsible"
|
||||
:open="opened">
|
||||
<AppNavigationBoard v-for="board in boardsSorted" :key="board.id" :board="board" />
|
||||
</AppNavigationItem>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
import AppNavigationBoard from './AppNavigationBoard'
|
||||
import { AppNavigationItem } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'AppNavigationBoardCategory',
|
||||
components: {
|
||||
AppNavigationItem,
|
||||
AppNavigationBoard,
|
||||
},
|
||||
directives: {
|
||||
ClickOutside,
|
||||
},
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -78,22 +76,19 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
boardsSorted() {
|
||||
return [...this.boards].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
collapsible() {
|
||||
return this.boards.length > 0
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
boards: function(newVal, prevVal) {
|
||||
boards(newVal, prevVal) {
|
||||
if (this.openOnAddBoards === true && prevVal.length < newVal.length) {
|
||||
this.opened = true
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
this.opened = !this.opened
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user