Vue board sidebar sharing (#1114)

Vue board sidebar sharing
This commit is contained in:
Julius Härtl
2019-08-12 13:10:04 +02:00
committed by GitHub
27 changed files with 19197 additions and 200 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,5 @@ tests/integration/vendor/
tests/integration/composer.lock
vendor/
*.lock
package-lock.json
\.idea/

View File

@@ -72,10 +72,6 @@ input.input-inline {
border-radius: 0;
}
#app img {
box-sizing: content-box;
}
#searchbox {
display: flex !important;
}

View File

@@ -76,7 +76,7 @@ class CardController extends Controller {
* @param int $order
* @return \OCP\AppFramework\Db\Entity
*/
public function create($title, $stackId, $type, $order = 999) {
public function create($title, $stackId, $type = 'plain', $order = 999) {
return $this->cardService->create($title, $stackId, $type, $order, $this->userId);
}

17991
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -26,18 +26,19 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@babel/polyfill": "^7.0.0",
"@babel/polyfill": "^7.4.4",
"nextcloud-axios": "^0.2.0",
"nextcloud-server": "^0.15.9",
"nextcloud-server": "^0.15.10",
"nextcloud-vue": "^0.12.1",
"nextcloud-vue-collections": "^0.5.2",
"vue": "^2.5.16",
"vue": "^2.6.7",
"vue-click-outside": "^1.0.7",
"vue-color": "^2.7.0",
"vue-infinite-loading": "^2.4.1",
"vue-router": "^3.0.1",
"vue-easymde": "^0.1.6",
"vue-infinite-loading": "^2.4.4",
"vue-router": "^3.1.2",
"vue-smooth-dnd": "^0.8.0",
"vuex": "^3.0.1",
"vuex": "^3.1.1",
"vuex-router-sync": "^5.0.0"
},
"browserslist": [
@@ -48,42 +49,42 @@
"node": ">=10.0.0"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/core": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@vue/test-utils": "^1.0.0-beta.25",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.0.0",
"babel-loader": "^8.0.4",
"@babel/preset-env": "^7.5.5",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-eslint": "^10.0.2",
"babel-jest": "^24.8.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.0.0",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-loader": "^2.2.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^3.1.0",
"eslint-plugin-vue": "^4.5.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^4.0.0",
"jest": "^24.0.0",
"jest": "^24.8.0",
"jest-serializer-vue": "^2.0.2",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.10.0",
"node-sass": "^4.12.0",
"prettier-eslint": "^9.0.0",
"raw-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"sass-loader": "^7.2.0",
"stylelint": "^8.4.0",
"stylelint-config-recommended-scss": "^3.2.0",
"stylelint-config-recommended-scss": "^3.3.0",
"stylelint-webpack-plugin": "^0.10.5",
"vue-jest": "^3.0.0",
"vue-loader": "^15.4.2",
"vue-jest": "^3.0.4",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.1",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.1.2"
"vue-template-compiler": "^2.6.10",
"webpack": "^4.39.1",
"webpack-cli": "^3.3.6",
"webpack-merge": "^4.2.1"
},
"jest": {
"moduleFileExtensions": [

View File

@@ -74,7 +74,7 @@ export default {
}),
// TODO: properly handle sidebar showing for route subview and board sidebar
sidebarRouterView() {
console.log(this.$route)
// console.log(this.$route)
return this.$route.name === 'card' || this.$route.name === 'board.details'
},
sidebarShown() {
@@ -119,12 +119,10 @@ export default {
}
}
.deck-main {
bottom: 0;
overflow: auto;
position: absolute;
top: 44px;
width: 100%;
}
</style>
<style>
#content * {
box-sizing: border-box;
}
</style>

View File

@@ -60,7 +60,7 @@ export default {
},
methods: {
updateColor() {
this.$emit('input', this.color)
this.$emit('input', this.color.hex)
},
hidePicker() {
this.showFullPicker = false

View File

@@ -32,21 +32,24 @@
</div>
</div>
<div v-if="board" class="crumb svg">
<div class="board-bullet" />
<a href="#todo">{{ board.title }}</a>
<span style="display: inline;" class="icon-shared" />
<div :style="{backgroundColor: '#' + board.color}" class="board-bullet" />
<a href="#">{{ board.title }}</a>
<router-link :to="{name: 'board.details'}" class="icon-shared" />
</div>
<div v-if="board" class="board-actions">
<div id="stack-add">
<form>
<label for="new-stack-input-main" class="hidden-visually">Add a new stack</label>
<input id="new-stack-input-main" type="text" class="no-close"
placeholder="Add a new stack">
<input class="icon-confirm" type="button" title="Submit">
<input id="new-stack-input-main" v-model="newStackTitle" type="text"
class="no-close"
placeholder="Add a new stack" @keyup.enter="clickAddNewStack()">
<input class="icon-confirm" type="button" title="Submit"
@click="clickAddNewStack()">
</form>
</div>
<div class="board-action-buttons">
<button title="Show archived cards" class="icon icon-archive" />
<button :style="archivStyle" title="Show archived cards" class="icon icon-archive"
@click="toggleShowArchived" />
<button :class="[(compactMode ? 'icon-toggle-compact-collapsed' : 'icon-toggle-compact-expanded')]" title="Toggle compact mode" class="icon"
@click="toggleCompactMode" />
<router-link v-tooltip="t('deck', 'Board settings')" :to="{name: 'board.details'}" class="icon-settings-dark"
@@ -68,10 +71,24 @@ export default {
default: null
}
},
data() {
return {
newStackTitle: '',
stack: '',
showArchived: false
}
},
computed: {
...mapState({
compactMode: state => state.compactMode
})
}),
archivStyle() {
if (this.showArchived === true) {
return 'opacity: 1.0'
}
return 'opacity: 0.3'
}
},
methods: {
toggleNav() {
@@ -82,6 +99,16 @@ export default {
},
toggleCompactMode() {
this.$store.dispatch('toggleCompactMode')
},
toggleShowArchived() {
this.$store.dispatch('toggleShowArchived')
this.showArchived = !this.showArchived
},
clickAddNewStack() {
this.stack = { title: this.newStackTitle }
this.$store.dispatch('createStack', this.stack)
this.newStackTitle = ''
this.stack = null
}
}
}

View File

@@ -26,19 +26,7 @@
<div v-if="board" class="board">
<container lock-axix="y" orientation="horizontal" @drop="onDropStack">
<draggable v-for="stack in stacksByBoard" :key="stack.id" class="stack">
<h3>{{ stack.title }}</h3>
<container :get-child-payload="payloadForCard(stack.id)" group-name="stack" @drop="($event) => onDropCard(stack.id, $event)">
<draggable v-for="card in cardsByStack(stack.id)" :key="card.id">
<card-item v-if="card" :id="card.id" />
</draggable>
</container>
<div class="card create">
<div title="Add card">
<i class="icon icon-add" />
<span class="hidden-visually">Add card</span>
</div>
</div>
<stack :stack="stack" />
</draggable>
</container>
</div>
@@ -55,15 +43,15 @@
import { Container, Draggable } from 'vue-smooth-dnd'
import { mapState } from 'vuex'
import Controls from '../Controls'
import CardItem from '../cards/CardItem'
import Stack from './Stack'
export default {
name: 'Board',
components: {
CardItem,
Controls,
Container,
Draggable
Draggable,
Stack
},
inject: [
'boardApi'
@@ -81,17 +69,21 @@ export default {
},
computed: {
...mapState({
board: state => state.currentBoard
board: state => state.currentBoard,
showArchived: state => state.showArchived
}),
stacksByBoard() {
return this.$store.getters.stacksByBoard(this.board.id)
},
cardsByStack() {
return (id) => this.$store.getters.cardsByStack(id)
}
/* cardsByStack() {
return (id) => this.$store.getters.cardsByStack(id)
} */
},
watch: {
'$route': 'fetchData'
id: 'fetchData',
showArchived() {
this.fetchData()
}
},
created() {
this.fetchData()
@@ -102,22 +94,22 @@ export default {
.then((board) => {
this.$store.dispatch('setCurrentBoard', board)
this.$store.dispatch('loadStacks', board)
this.$store.dispatch('setAssignableUsers', board.users)
this.loading = false
console.log(board)
this.$store.state.labels = board.labels
})
},
onDropStack({ removedIndex, addedIndex }) {
this.$store.dispatch('orderStack', { stack: this.stacksByBoard[removedIndex], removedIndex, addedIndex })
},
onDropCard({ removedIndex, addedIndex }) {
/* onDropCard({ removedIndex, addedIndex }) {
},
payloadForCard(stackId) {
}, */
/* payloadForCard(stackId) {
return index => {
return this.cardsByStack(stackId)[index]
}
},
}, */
createStack() {
let newStack = {
title: 'FooBar',
@@ -126,6 +118,9 @@ export default {
}
this.$store.dispatch('createStack', newStack)
}
/* deleteStack(stack) {
this.$store.dispatch('deleteStack', stack)
} */
}
}
</script>
@@ -146,6 +141,7 @@ export default {
padding-top: 0;
}
/*
.smooth-dnd-container.vertical {
display: flex;
flex-direction: column;
@@ -157,6 +153,6 @@ export default {
.smooth-dnd-container.vertical .smooth-dnd-draggable-wrapper {
height: auto;
}
} */
</style>

View File

@@ -21,7 +21,7 @@
-->
<template>
<app-sidebar
<app-sidebar v-if="board != null"
:actions="[]"
:title="board.title"
@close="closeSidebar">
@@ -35,7 +35,7 @@
</AppSidebarTab>
<AppSidebarTab name="Deleted items" icon="icon-delete">
<DeletedTabSidebard :board="board" />
<DeletedTabSidebar :board="board" />
</AppSidebarTab>
<AppSidebarTab name="Timeline" icon="icon-activity">
@@ -49,7 +49,7 @@
import { mapState } from 'vuex'
import SharingTabSidebard from './SharingTabSidebard'
import TagsTabSidebard from './TagsTabSidebard'
import DeletedTabSidebard from './DeletedTabSidebard'
import DeletedTabSidebar from './DeletedTabSidebar'
import TimelineTabSidebard from './TimelineTabSidebard'
import { AppSidebar, AppSidebarTab } from 'nextcloud-vue'
@@ -60,9 +60,15 @@ export default {
AppSidebarTab,
SharingTabSidebard,
TagsTabSidebard,
DeletedTabSidebard,
DeletedTabSidebar,
TimelineTabSidebard
},
props: {
id: {
type: Number,
required: true
}
},
computed: {
...mapState({
board: state => state.currentBoard,

View File

@@ -0,0 +1,128 @@
<template>
<div>
<h3>{{ t('deck', 'Deleted stacks') }}</h3>
<ul>
<li v-for="deletedStack in deletedStacks" :key="deletedStack.id">
<span class="icon icon-deck" />
<span class="title">{{ deletedStack.title }}</span>
<button
:title="t('settings', 'Undo')"
class="app-navigation-entry-deleted-button icon-history"
@click="stackUndoDelete(deletedStack)" />
<!-- <span class="live-relative-timestamp" data-timestamp="{{ deletedStack.deletedAt*1000 }}">{{deletedStack.deletedAt | relativeDateFilter }}</span>
<a @click="stackUndoDelete(deletedStack)"><span class="icon icon-history"></span></a> -->
</li>
</ul>
<h3>{{ t('deck', 'Deleted cards') }}</h3>
<ul>
<li v-for="deletedCard in deletedCards" :key="deletedCard.id">
<div class="icon icon-deck" />
<span class="title">{{ deletedCard.title }}</span>
<button
:title="t('settings', 'Undo')"
class="app-navigation-entry-deleted-button icon-history"
@click="cardUndoDelete(deletedCard)" />
</li>
<!-- <li ng-repeat="deletedCard in cardservice.deleted">
<span class="icon icon-deck"></span>
<span class="title">{{deletedCard.title}} ({{stackservice.tryAllThenDeleted(deletedCard.stackId).title}})</span>
<span class="live-relative-timestamp" data-timestamp="{{ deletedCard.deletedAt*1000 }}">{{deletedCard.deletedAt | relativeDateFilter }}</span>
<a ng-click="cardOrCardAndStackUndoDelete(deletedCard)">
<span class="icon icon-history"></span>
</a>
</li> -->
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'DeletedTabSidebar',
components: {
},
props: {
board: {
type: Object,
default: undefined
}
},
data() {
return {
isLoading: false,
copiedDeletedStack: null,
copiedDeletedCard: null
}
},
computed: {
...mapState({
deletedStacks: state => state.stack.deletedStacks,
deletedCards: state => state.stack.deletedCards
})
},
created() {
this.getData()
},
methods: {
getData() {
this.isLoading = true
this.$store.dispatch('deletedItems', this.board.id).then(response => {
this.isLoading = false
})
},
stackUndoDelete(deletedStack) {
this.copiedDeletedStack = Object.assign({}, deletedStack)
this.copiedDeletedStack.deletedAt = 0
this.$store.dispatch('stackUndoDelete', this.copiedDeletedStack)
this.getData()
},
cardUndoDelete(deletedCard) {
this.copiedDeletedCard = Object.assign({}, deletedCard)
this.copiedDeletedCard.deletedAt = 0
this.$store.dispatch('cardUndoDelete', this.copiedDeletedCard)
this.getData()
}
}
}
</script>
<style scoped lang="scss">
ul {
display: flex;
flex-direction: row;
* {
flex-basis: 44px;
}
.title {
flex-grow: 2;
}
.live-relative-timestamp {
flex-grow: 1;
}
}
li {
display: flex;
flex-direction: row;
* {
flex-basis: 44px;
}
.title {
flex-grow: 2;
}
.live-relative-timestamp {
flex-grow: 1;
}
}
</style>

View File

@@ -1,26 +0,0 @@
<template>
<div>
deleted
</div>
</template>
<script>
export default {
name: 'DeletedTabSidebard',
components: {
},
props: {
board: {
type: Object,
default: undefined
}
},
data() {
return {
}
}
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<multiselect v-model="addAcl" :options="sharees" label="label"
<multiselect v-model="addAcl" :options="unallocatedSharees" label="label"
@input="clickAddAcl" @search-change="asyncFind">
<template #option="scope">
{{ scope.option.label }}
@@ -23,34 +23,36 @@
{{ acl.participant.displayname }}
</span>
<input :checked="acl.permissionEdit" type="checkbox" @click="clickEditAcl(acl)">
<label for="checkbox">{{ t('deck', 'Edit') }}</label>
<input :checked="acl.permissionShare" type="checkbox" @click="clickShareAcl(acl)">
<label for="checkbox">{{ t('deck', 'Share') }}</label>
<input :checked="acl.permissionManage" type="checkbox" @click="clickManageAcl(acl)">
<label for="checkbox">{{ t('deck', 'Manage') }}</label>
<button v-tooltip="t('deck', 'Delete')" class="icon-delete" @click="clickDeleteAcl(acl)" />
<Actions>
<ActionCheckbox :checked="acl.permissionEdit" @change="clickEditAcl(acl)">{{ t('deck', 'Can edit') }}</ActionCheckbox>
</Actions>
<Actions>
<ActionCheckbox :checked="acl.permissionShare" @change="clickShareAcl(acl)">{{ t('deck', 'Can share') }}</ActionCheckbox>
<ActionCheckbox :checked="acl.permissionManage" @change="clickManageAcl(acl)">{{ t('deck', 'Can manage') }}</ActionCheckbox>
<ActionButton icon="icon-delete" @click="clickDeleteAcl(acl)">{{ t('deck', 'Delete') }}</ActionButton>
</Actions>
</li>
</ul>
<CollaborationView />
<collection-list v-if="board.id" :id="`${board.id}`" :name="board.title"
type="deck" />
</div>
</template>
<script>
import { Avatar, Multiselect } from 'nextcloud-vue'
import CollaborationView from '../CollaborationView'
import { Avatar, Multiselect, Actions, ActionButton, ActionCheckbox } from 'nextcloud-vue'
import { CollectionList } from 'nextcloud-vue-collections'
import { mapGetters } from 'vuex'
export default {
name: 'SharingTabSidebard',
components: {
Avatar,
Actions,
ActionButton,
ActionCheckbox,
Multiselect,
CollaborationView
CollectionList
},
props: {
board: {
@@ -68,7 +70,14 @@ export default {
computed: {
...mapGetters({
sharees: 'sharees'
})
}),
unallocatedSharees() {
return this.sharees.filter((sharee) => {
return Object.values(this.board.acl).findIndex((acl) => {
return acl.participant.uid === sharee.value.shareWith
})
})
}
},
methods: {
asyncFind(query) {
@@ -108,3 +117,16 @@ export default {
}
}
</script>
<style scoped>
#shareWithList {
margin-bottom: 20px;
}
#shareWithList li {
display: flex;
align-items: center;
}
.username {
padding: 12px 9px;
flex-grow: 1;
}
</style>

View File

@@ -0,0 +1,172 @@
<!--
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
- @author Jakob Röhrl <jakob.roehrl@web.de>
-
- @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 class="stack">
<div class="stack--header">
<transition name="fade" mode="out-in">
<h3 v-if="!editing" @click="startEditing(stack)">{{ stack.title }}</h3>
<form v-else>
<input v-model="copiedStack.title" type="text" autofocus>
<input type="button" class="icon-confirm" @click="finishedEdit(stack)">
</form>
</transition>
<Actions>
<ActionButton icon="icon-delete" @click="deleteStack(stack)">{{ t('deck', 'Delete stack') }}</ActionButton>
</Actions>
</div>
<!-- <container :get-child-payload="payloadForCard(stack.id)" group-name="stack" @drop="($event) => onDropCard(stack.id, $event)"> -->
<container :get-child-payload="payloadForCard(stack.id)" group-name="stack" @drop="onDropCard">
<draggable v-for="card in cardsByStack(stack.id)" :key="card.id">
<card-item v-if="card" :id="card.id" />
</draggable>
</container>
<form class="stack--card-add" @submit.prevent="clickAddCard()">
<label for="new-stack-input-main" class="hidden-visually">Add a new card</label>
<input id="new-stack-input-main" v-model="newCardTitle" type="text"
class="no-close"
placeholder="Add a new card">
<input class="icon-confirm" type="button" title="Submit">
</form>
</div>
</template>
<script>
import { Container, Draggable } from 'vue-smooth-dnd'
import { Actions } from 'nextcloud-vue/dist/Components/Actions'
import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton'
import CardItem from '../cards/CardItem'
export default {
name: 'Stack',
components: {
Actions,
ActionButton,
CardItem,
Container,
Draggable
},
props: {
stack: {
type: Object,
default: undefined
}
},
data() {
return {
editing: false,
copiedStack: '',
newCardTitle: ''
}
},
computed: {
cardsByStack() {
return (id) => this.$store.getters.cardsByStack(id)
}
},
methods: {
onDropCard({ removedIndex, addedIndex }) {
},
payloadForCard(stackId) {
return index => {
return this.cardsByStack(stackId)[index]
}
},
deleteStack(stack) {
this.$store.dispatch('deleteStack', stack)
},
startEditing(stack) {
this.copiedStack = Object.assign({}, stack)
this.editing = true
},
finishedEdit(stack) {
if (this.copiedStack.title !== stack.title) {
this.$store.dispatch('updateStack', this.copiedStack)
}
this.editing = false
},
clickAddCard() {
let newCard = {
title: this.newCardTitle,
stackId: this.stack.id,
boardId: this.stack.boardId
}
this.$store.dispatch('addCard', newCard)
}
}
}
</script>
<style lang="scss" scoped>
$stack-spacing: 10px;
$stack-width: 300px;
.stack {
width: $stack-width;
padding: $stack-spacing;
padding-top: 0;
}
.smooth-dnd-container.vertical {
display: flex;
flex-direction: column;
}
.smooth-dnd-container.vertical > .smooth-dnd-draggable-wrapper {
overflow: initial;
}
.smooth-dnd-container.vertical .smooth-dnd-draggable-wrapper {
height: auto;
}
.stack--header {
display: flex;
h3, form {
flex-grow: 1;
display: flex;
input[type=text] {
flex-grow: 1;
}
}
}
.stack--card-add {
display: flex;
input[type=text] {
flex-grow: 1;
}
}
</style>

View File

@@ -21,45 +21,194 @@
-->
<template>
<app-sidebar
:actions="[]"
title="andy-yeo-1387151-unsplash.jpg"
subtitle="4,3 MB, last edited 19 days ago"
<app-sidebar v-if="currentCard !== null && copiedCard !== null"
:actions="toolbarActions"
:title="currentCard.title"
:subtitle="subtitle"
@close="closeSidebar">
<template #action />
<AppSidebarTab name="Description" icon="icon-description">
this is the description tab
<AppSidebarTab :order="0" name="Details" icon="icon-home">
<p>Tags</p>
<multiselect v-model="allLabels" :multiple="true" :options="currentBoard.labels"
:taggable="true" label="title"
track-by="id" @select="addLabelToCard" @remove="removeLabelFromCard">
<template #option="scope">
<span>{{ scope.option.title }}</span>
</template>
</multiselect>
<p>Assign to user</p>
<multiselect v-model="assignedUsers" :multiple="true" :options="assignableUsers"
label="displayname"
track-by="primaryKey"
@select="assignUserToCard" @remove="removeUserFromCard">
<template #option="scope">
{{ scope.option.displayname }}
</template>
</multiselect>
<p>Due to</p>
<DatetimePicker v-model="copiedCard.duedate" type="datetime" lang="en"
format="YYYY-MM-DD HH:mm" confirm @change="setDue()" />
<button v-tooltip="t('deck', 'Delete')" v-if="copiedCard.duedate" class="icon-delete"
@click="removeDue()" />
<markdown-editor ref="markdownEditor" v-model="desc" :configs="{autofocus: true, autosave: {enabled: true, uniqueId: 'unique'}, toolbar: false}" />
</AppSidebarTab>
<AppSidebarTab name="Timeline" icon="icon-activity">
<AppSidebarTab :order="1" name="Attachments" icon="icon-files-dark">
{{ currentCard.attachments }}
<button class="icon-upload" @click="clickAddNewAttachmment()">
{{ t('deck', 'Upload attachment') }}
</button>
</AppSidebarTab>
<AppSidebarTab :order="2" name="Timeline" icon="icon-activity">
this is the activity tab
</AppSidebarTab>
<AppSidebarTab name="Attachments" icon="icon-files-dark">
this is the files tab
</AppSidebarTab>
</app-sidebar>
</template>
<script>
import { AppSidebar, AppSidebarTab } from 'nextcloud-vue'
import { AppSidebar, AppSidebarTab, Multiselect, DatetimePicker } from 'nextcloud-vue'
import { mapState } from 'vuex'
import markdownEditor from 'vue-easymde/src/markdown-editor'
export default {
name: 'CardSidebar',
components: {
AppSidebar,
AppSidebarTab
AppSidebarTab,
Multiselect,
DatetimePicker,
markdownEditor
},
props: {
id: {
type: Number,
required: true
}
},
data() {
return {
assignedUsers: null,
addedLabelToCard: null,
isLoading: false,
copiedCard: null,
allLabels: null,
desc: null
}
},
computed: {
...mapState({
currentBoard: state => state.currentBoard,
assignableUsers: state => state.assignableUsers
}),
currentCard() {
return this.$store.getters.cardById(this.id)
},
subtitle() {
let lastModified = this.currentCard.lastModified
let createdAt = this.currentCard.createdAt
return t('deck', 'Modified') + ': ' + lastModified + ' ' + t('deck', 'Created') + ': ' + createdAt
},
toolbarActions() {
return [
{
action: () => {
},
icon: 'icon-archive-dark',
text: t('deck', 'Assign to me')
},
{
action: () => {
},
icon: 'icon-archive',
text: t('deck', (this.showArchived ? 'Unarchive card' : 'Archive card'))
}
]
}
},
watch: {
'currentCard': {
immediate: true,
handler() {
this.copiedCard = JSON.parse(JSON.stringify(this.currentCard))
this.allLabels = this.currentCard.labels
this.assignedUsers = this.currentCard.assignedUsers.map((item) => item.participant)
this.desc = this.currentCard.description
}
},
desc() {
this.copiedCard.description = this.desc
this.saveDesc()
}
},
methods: {
setDue() {
this.$store.dispatch('updateCardDue', this.copiedCard)
},
removeDue() {
this.copiedCard.duedate = null
this.$store.dispatch('updateCardDue', this.copiedCard)
},
saveDesc() {
this.$store.dispatch('updateCardDesc', this.copiedCard)
},
closeSidebar() {
this.$router.push({ name: 'board' })
},
assignUserToCard(user) {
this.copiedCard.newUserUid = user.uid
this.$store.dispatch('assignCardToUser', this.copiedCard)
},
removeUserFromCard(user) {
this.copiedCard.removeUserUid = user.uid
this.$store.dispatch('removeUserFromCard', this.copiedCard)
},
addLabelToCard(newLabel) {
this.copiedCard.labels.push(newLabel)
let data = {
card: this.copiedCard,
labelId: newLabel.id
}
this.$store.dispatch('addLabel', data)
},
removeLabelFromCard(removedLabel) {
let removeIndex = this.copiedCard.labels.findIndex((label) => {
return label.id === removedLabel.id
})
if (removeIndex !== -1) {
this.copiedCard.labels.splice(removeIndex, 1)
}
let data = {
card: this.copiedCard,
labelId: removedLabel.id
}
this.$store.dispatch('removeLabel', data)
},
clickAddNewAttachmment() {
}
}
}
</script>
<style lang="scss" scoped>
<style>
@import "~easymde/dist/easymde.min.css";
.editor-preview,
.editor-statusbar {
display: none;
}
</style>

View File

@@ -22,16 +22,21 @@
<template>
<div class="badges">
<div v-if="true" class="card-files icon icon-files-dark" />
<div v-if="true" class="card-comments icon icon-comment" />
<div v-if="true" :class="{'icon-calendar': true, 'icon-calendar-dark': false}" class="due icon now">
<span>Now</span>
<div v-if="card.attachments" class="card-files icon icon-files-dark" />
<div v-if="card.description" class="card-comments icon icon-filetype-text" />
<div v-if="card.duedate" :class="dueIcon">
<span>{{ dueTimeDiff }}</span>
</div>
<div v-if="true" class="card-tasks icon icon-checkmark">
<div v-if="card.description && card.description.match(/\[\s*\]/g)" class="card-tasks icon icon-checkmark">
<!-- TODO: get checkbox values -->
<span>0/0</span>
</div>
<div v-if="true" class="card-assigned-users">
<avatar user="admin" />
<div v-if="card.assignedUsers" class="card-assigned-users">
<avatar v-for="user in card.assignedUsers" :key="user.id" :user="user.participant.primaryKey" />
</div>
</div>
</template>
@@ -50,6 +55,43 @@ export default {
computed: {
compactMode() {
return false
},
dueIcon() {
let timeInHours = Math.round((Date.parse(this.card.duedate) - Date.now()) / 1000 / 60 / 60 / 24)
if (timeInHours === 1) {
return 'icon-calendar-dark due icon next'
}
if (timeInHours === 0) {
return 'icon-calendar-dark due icon now'
}
if (timeInHours < 0) {
return 'icon-calendar-dark due icon overdue'
}
},
dueTimeDiff() {
let unit = 'Minutes'
let timeInMin = (Date.parse(this.card.duedate) - Date.now()) / 60000
if (timeInMin > 59) {
timeInMin /= 60
unit = 'Hours'
}
if (timeInMin > 23) {
timeInMin /= 24
unit = 'Days'
}
if (timeInMin > 355) {
timeInMin /= 355
unit = 'Years'
}
return Math.round(timeInMin) + ' ' + unit
},
card() {
return this.$store.getters.cardById(this.id)
}
}
}

View File

@@ -22,28 +22,37 @@
<template>
<div :class="{'compact': compactMode}" tag="div" class="card"
@click="openCard">
@click.self="openCard">
<div class="card-upper">
<h3 @click.stop="startEditing">{{ card.title }}</h3>
<h3 @click.stop="startEditing(card)">{{ card.title }}</h3>
<transition name="fade" mode="out-in">
<form v-if="editing">
<input :value="card.title" type="text" autofocus>
<input type="button" class="icon-confirm" @click.stop="editing=false">
<input v-model="copiedCard.title" type="text" autofocus>
<input type="button" class="icon-confirm" @click="finishedEdit(card)">
</form>
<action v-if="!editing" :actions="visibilityPopover" @click.stop="" />
<Actions @click.stop.prevent>
<ActionButton 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 icon="icon-delete" @click="deleteCard()">{{ t('deck', 'Delete card') }}</ActionButton>
<ActionButton icon="icon-settings-dark" @click="setCurrentCard()">{{ t('deck', 'Card details') }}</ActionButton>
</Actions>
</transition>
</div>
<ul class="labels">
<li v-for="label in labels" :key="label.id" :style="labelStyle(label)"><span>{{ label.title }}</span></li>
<ul class="labels" @click="openCard">
<li v-for="label in card.labels" :key="label.id" :style="labelStyle(label)"><span>{{ label.title }}</span></li>
</ul>
<div v-show="!compactMode" class="card-controls compact-item">
<card-badges />
<div v-show="!compactMode" class="card-controls compact-item" @click="openCard">
<card-badges :id="id" />
</div>
</div>
</template>
<script>
import { PopoverMenu, Action } from 'nextcloud-vue'
import { PopoverMenu } from 'nextcloud-vue'
import { Actions } from 'nextcloud-vue/dist/Components/Actions'
import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton'
import ClickOutside from 'vue-click-outside'
import { mapState } from 'vuex'
@@ -53,7 +62,7 @@ import Color from '../../mixins/color'
export default {
name: 'CardItem',
components: { PopoverMenu, CardBadges, LabelTag, Action },
components: { PopoverMenu, CardBadges, LabelTag, Actions, ActionButton },
directives: {
ClickOutside
},
@@ -67,12 +76,14 @@ export default {
data() {
return {
menuOpened: false,
editing: false
editing: false,
copiedCard: ''
}
},
computed: {
...mapState({
compactMode: state => state.compactMode
compactMode: state => state.compactMode,
showArchived: state => state.showArchived
}),
card() {
return this.$store.getters.cardById(this.id)
@@ -80,12 +91,6 @@ export default {
menu() {
return []
},
labels() {
return [
{ id: 1, title: 'ToDo', color: 'aa0000' },
{ id: 2, title: 'Done', color: '33ff33' }
]
},
labelStyle() {
return (label) => {
return {
@@ -93,25 +98,11 @@ export default {
color: this.textColor(label.color)
}
}
},
visibilityPopover() {
return [
{
action: () => {},
icon: 'icon-archive-dark',
text: t('deck', 'Archive card')
},
{
action: () => {},
icon: 'icon-settings-dark',
text: t('deck', 'Card details')
}
]
}
},
methods: {
openCard() {
this.$router.push({ name: 'card', params: { cardId: 123 } })
this.$router.push({ name: 'card', params: { cardId: this.id } })
},
togglePopoverMenu() {
this.menuOpened = !this.menuOpened
@@ -119,9 +110,33 @@ export default {
hidePopoverMenu() {
this.menuOpened = false
},
startEditing() {
startEditing(card) {
this.copiedCard = Object.assign({}, card)
this.editing = true
},
finishedEdit(card) {
if (this.copiedCard.title !== card.title) {
this.$store.dispatch('updateCard', this.copiedCard)
}
this.editing = false
},
deleteCard() {
this.$store.dispatch('deleteCard', this.card)
},
archiveUnarchiveCard() {
this.copiedCard = Object.assign({}, this.card)
this.copiedCard.archived = !this.copiedCard.archived
this.$store.dispatch('archiveUnarchiveCard', this.copiedCard)
},
assignCardToMe() {
this.copiedCard = Object.assign({}, this.card)
this.copiedCard.newUserUid = this.card.owner.uid
this.$store.dispatch('assignCardToUser', this.copiedCard)
},
setCurrentCard() {
this.$store.dispatch('setCurrentCard', this.card)
}
}
}
</script>
@@ -153,8 +168,9 @@ export default {
display: flex;
padding: 5px 7px;
position: absolute;
width: calc(100% - 14px);
input[type=text] {
width: 100%;
flex-grow: 1;
}
}

View File

@@ -102,7 +102,8 @@ export default {
]),
isAdmin() {
// eslint-disable-next-line
return oc_isadmin
//return oc_isadmin
return OC.isUserAdmin()
}
},
beforeMount() {

View File

@@ -63,11 +63,10 @@ export default {
this.editing = true
},
createBoard(e) {
console.log(this.color)
const title = e.currentTarget.childNodes[0].value
this.$store.dispatch('createBoard', {
title: title,
color: this.color.hex.substring(1)
color: this.color.substring(1)
})
this.editing = false
},

View File

@@ -27,8 +27,14 @@
<a href="#">
{{ board.title }}
</a>
<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" class="icon-shared" style="opacity: 0.3"
@click="showSidebar" />
<button v-else class="icon-shared" @click="showSidebar" />
</li>
<li class="app-navigation-entry-utils-menu-button">
<button v-click-outside="hideMenu" @click="showMenu" />
</li>
@@ -212,6 +218,11 @@ export default {
},
cancelEdit(e) {
this.editing = false
},
showSidebar() {
const route = this.routeTo
route.name = 'board.details'
this.$router.push(route)
}
},
inject: [

View File

@@ -86,6 +86,13 @@ export default new Router({
components: {
default: Boards,
sidebar: BoardSidebar
},
props: {
default: (route) => {
return {
id: parseInt(route.params.id, 10)
}
}
}
},
{
@@ -93,6 +100,13 @@ export default new Router({
name: 'card',
components: {
sidebar: CardSidebar
},
props: {
sidebar: (route) => {
return {
id: parseInt(route.params.cardId, 10)
}
}
}
}
]

167
src/services/CardApi.js Normal file
View File

@@ -0,0 +1,167 @@
/*
* @copyright Copyright (c) 2018 Michael Weimann <mail@michael-weimann.eu>
*
* @author Michael Weimann <mail@michael-weimann.eu>
*
* @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/>.
*
*/
import axios from 'nextcloud-axios'
export class CardApi {
url(url) {
url = `/apps/deck${url}`
return OC.generateUrl(url)
}
addCard(card) {
return axios.post(this.url(`/cards`), card)
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
deleteCard(cardId) {
return axios.delete(this.url(`/cards/${cardId}`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
updateCard(card) {
return axios.put(this.url(`/cards/${card.id}`), card)
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
assignUser(card) {
return axios.post(this.url(`/cards/${card.id}/assign`), { userId: card.newUserUid })
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
removeUser(card) {
return axios.delete(this.url(`/cards/${card.id}/assign/${card.removeUserUid}`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
archiveCard(card) {
return axios.put(this.url(`/cards/${card.id}/archive`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
unArchiveCard(card) {
return axios.put(this.url(`/cards/${card.id}/unarchive`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
assignLabelToCard(data) {
return axios.post(this.url(`/cards/${data.card.id}/label/${data.labelId}`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
removeLabelFromCard(data) {
return axios.delete(this.url(`/cards/${data.card.id}/label/${data.labelId}`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
}

View File

@@ -44,6 +44,51 @@ export class StackApi {
})
}
deletedStacks(boardId) {
return axios.get(this.url(`/${boardId}/stacks/deleted`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
deletedCards(boardId) {
return axios.get(this.url(`/${boardId}/cards/deleted`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
loadArchivedStacks(boardId) {
return axios.get(this.url(`/stacks/${boardId}/archived`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
/**
* @param {Stack} stack
* @returns {Promise}
@@ -78,4 +123,34 @@ export class StackApi {
})
}
deleteStack(stackId) {
return axios.delete(this.url(`/stacks/${stackId}`))
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
updateStack(stack) {
return axios.put(this.url(`/stacks/${stack.id}`), stack)
.then(
(response) => {
return Promise.resolve(response.data)
},
(err) => {
return Promise.reject(err)
}
)
.catch((err) => {
return Promise.reject(err)
})
}
}

View File

@@ -20,8 +20,11 @@
*
*/
import { CardApi } from './../services/CardApi'
import Vue from 'vue'
const apiClient = new CardApi()
export default {
state: {
cards: []
@@ -35,6 +38,9 @@ export default {
}
},
mutations: {
clearCards(state) {
state.cards = []
},
addCard(state, card) {
let existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) {
@@ -43,8 +49,125 @@ export default {
} else {
state.cards.push(card)
}
},
deleteCard(state, card) {
let existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) {
state.cards.splice(existingIndex, 1)
}
},
updateTitle(state, card) {
let existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) {
state.cards[existingIndex].title = card.title
}
},
assignCardToUser(state, user) {
let existingIndex = state.cards.findIndex(_card => _card.id === user.cardId)
if (existingIndex !== -1) {
state.cards[existingIndex].assignedUsers.push(user)
}
},
removeUserFromCard(state, user) {
let existingIndex = state.cards.findIndex(_card => _card.id === user.cardId)
if (existingIndex !== -1) {
let foundIndex = state.cards[existingIndex].assignedUsers.findIndex(_user => _user.id === user.id)
if (foundIndex !== -1) {
state.cards[existingIndex].assignedUsers.splice(foundIndex, 1)
}
}
},
updateCardDesc(state, card) {
let existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) {
state.cards[existingIndex].description = card.description
}
},
updateCardDue(state, card) {
let existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) {
state.cards[existingIndex].duedate = card.duedate
}
},
updateCardLabels(state, card) {
let existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) {
let existingCard = state.cards.find(_card => _card.id === card.id)
existingCard.labels = card.labels
}
}
},
actions: {
addCard({ commit }, card) {
apiClient.addCard(card)
.then((createdCard) => {
commit('addCard', createdCard)
})
},
updateCard({ commit }, card) {
apiClient.updateCard(card)
.then((updatedCard) => {
commit('updateTitle', updatedCard)
})
},
deleteCard({ commit }, card) {
apiClient.deleteCard(card.id)
.then((card) => {
commit('deleteCard', card)
})
},
archiveUnarchiveCard({ commit }, card) {
let call = 'archiveCard'
if (card.archived === false) {
call = 'unArchiveCard'
}
apiClient[call](card)
.then((card) => {
commit('deleteCard', card)
})
},
assignCardToUser({ commit }, card) {
apiClient.assignUser(card)
.then((user) => {
commit('assignCardToUser', user)
})
},
removeUserFromCard({ commit }, card) {
apiClient.removeUser(card)
.then((user) => {
commit('removeUserFromCard', user)
})
},
addLabel({ commit }, data) {
apiClient.assignLabelToCard(data)
.then(() => {
commit('updateCardLabels', data.card)
})
},
removeLabel({ commit }, data) {
apiClient.removeLabelFromCard(data)
.then(() => {
commit('updateCardLabels', data.card)
})
},
cardUndoDelete({ commit }, card) {
apiClient.updateCard(card)
.then((card) => {
commit('addCard', card)
})
},
updateCardDesc({ commit }, card) {
apiClient.updateCard(card)
.then((updatedCard) => {
commit('updateCardDesc', updatedCard)
})
},
updateCardDue({ commit }, card) {
apiClient.updateCard(card)
.then((card) => {
commit('updateCardDue', card)
})
}
}
}

View File

@@ -46,12 +46,15 @@ export default new Vuex.Store({
},
strict: debug,
state: {
showArchived: false,
navShown: true,
compactMode: false,
sidebarShown: false,
currentBoard: null,
currentCard: null,
boards: [],
sharees: [],
assignableUsers: [],
boardFilter: BOARD_FILTERS.ALL
},
getters: {
@@ -90,6 +93,9 @@ export default new Vuex.Store({
}
},
mutations: {
toggleShowArchived(state) {
state.showArchived = !state.showArchived
},
/**
* Adds or replaces a board in the store.
* Matches a board by it's id.
@@ -131,8 +137,12 @@ export default new Vuex.Store({
setBoards(state, boards) {
state.boards = boards
},
setSharees(state, sharees) {
state.sharees = sharees
setSharees(state, shareesUsersAndGroups) {
state.sharees = shareesUsersAndGroups.users
state.sharees.push(...shareesUsersAndGroups.groups)
},
setAssignableUsers(state, users) {
state.assignableUsers = users
},
setBoardFilter(state, filter) {
state.boardFilter = filter
@@ -140,6 +150,9 @@ export default new Vuex.Store({
setCurrentBoard(state, board) {
state.currentBoard = board
},
setCurrentCard(state, card) {
state.currentCard = card
},
// label mutators
removeLabelFromCurrentBoard(state, labelId) {
@@ -166,8 +179,8 @@ export default new Vuex.Store({
},
// acl mutators
addAclToCurrentBoard(state, acl) {
console.log(state.currentBoard)
addAclToCurrentBoard(state, createdAcl) {
Vue.set(state.currentBoard.acl, createdAcl.id, createdAcl)
},
updateAclFromCurrentBoard(state, acl) {
for (var acl_ in state.currentBoard.acl) {
@@ -178,15 +191,24 @@ export default new Vuex.Store({
}
},
deleteAclFromCurrentBoard(state, acl) {
for (var acl_ in state.currentBoard.acl) {
if (state.currentBoard.acl[acl_].participant.uid === acl.participant.uid) {
delete state.currentBoard.acl[acl_]
let removeIndex = -1
for (var index in state.currentBoard.acl) {
var attr = state.currentBoard.acl[index]
if (acl.id === attr.id) {
removeIndex = index
break
}
}
if (removeIndex > -1) {
Vue.delete(state.currentBoard.acl, removeIndex)
}
}
},
actions: {
toggleShowArchived({ commit }) {
commit('toggleShowArchived')
},
/**
* @param commit
* @param state
@@ -244,9 +266,10 @@ export default new Vuex.Store({
params.append('itemType', 0)
params.append('itemType', 1)
axios.get(OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees', { params }).then((response) => {
commit('setSharees', response.data.ocs.data.users)
commit('setSharees', response.data.ocs.data)
})
},
setBoardFilter({ commmit }, filter) {
commmit('setBoardFilter', filter)
},
@@ -262,6 +285,12 @@ export default new Vuex.Store({
setCurrentBoard({ commit }, board) {
commit('setCurrentBoard', board)
},
setAssignableUsers({ commit }, board) {
commit('setAssignableUsers', board)
},
setCurrentCard({ commit }, card) {
commit('setCurrentCard', card)
},
// label actions
removeLabelFromCurrentBoard({ commit }, label) {
@@ -285,11 +314,11 @@ export default new Vuex.Store({
},
// acl actions
addAclToCurrentBoard({ commit }, acl) {
acl.boardId = this.state.currentBoard.id
apiClient.addAcl(acl)
.then((acl) => {
commit('addAclToCurrentBoard', acl)
addAclToCurrentBoard({ commit }, newAcl) {
newAcl.boardId = this.state.currentBoard.id
apiClient.addAcl(newAcl)
.then((returnAcl) => {
commit('addAclToCurrentBoard', returnAcl)
})
},
updateAclFromCurrentBoard({ commit }, acl) {

View File

@@ -28,7 +28,9 @@ const apiClient = new StackApi()
export default {
state: {
stacks: []
stacks: [],
deletedStacks: [],
deletedCards: []
},
getters: {
stacksByBoard: state => (id) => {
@@ -51,6 +53,29 @@ export default {
for (let i = 0; i < newOrder.length; i++) {
newOrder[i].order = parseInt(i)
}
},
deleteStack(state, stack) {
let existingIndex = state.stacks.findIndex(_stack => _stack.id === stack.id)
if (existingIndex !== -1) {
state.stacks.splice(existingIndex, 1)
}
},
updateStack(state, stack) {
let existingIndex = state.stacks.findIndex(_stack => _stack.id === stack.id)
if (existingIndex !== -1) {
state.stacks[existingIndex].title = stack.title
}
},
setDeletedStacks(state, delStacks) {
state.deletedStacks = []
if (delStacks.length > 0) {
state.deletedStacks = delStacks
}
},
setDeletedCards(state, delCards) {
state.deletedCards = []
state.deletedCards = delCards
}
},
actions: {
@@ -64,7 +89,12 @@ export default {
})
},
loadStacks({ commit }, board) {
apiClient.loadStacks(board.id)
commit('clearCards')
let call = 'loadStacks'
if (this.state.showArchived === true) {
call = 'loadArchivedStacks'
}
apiClient[call](board.id)
.then((stacks) => {
for (let i in stacks) {
let stack = stacks[i]
@@ -77,10 +107,40 @@ export default {
})
},
createStack({ commit }, stack) {
stack.boardId = this.state.currentBoard.id
apiClient.createStack(stack)
.then((createdStack) => {
commit('addStack', createdStack)
})
},
deleteStack({ commit }, stack) {
apiClient.deleteStack(stack.id)
.then((stack) => {
commit('deleteStack', stack)
})
},
updateStack({ commit }, stack) {
apiClient.updateStack(stack)
.then((stack) => {
commit('updateStack', stack)
})
},
deletedItems({ commit }, boardId) {
apiClient.deletedStacks(boardId)
.then((deletedStacks) => {
commit('setDeletedStacks', deletedStacks)
})
apiClient.deletedCards(boardId)
.then((deletedCards) => {
commit('setDeletedCards', deletedCards)
})
},
stackUndoDelete({ commit }, stack) {
apiClient.updateStack(stack)
.then((stack) => {
commit('addStack', stack)
})
}
}
}

View File

@@ -6,7 +6,8 @@ module.exports = {
output: {
path: path.resolve(__dirname, './js'),
publicPath: '/js/',
filename: 'deck.js'
filename: 'deck.js',
jsonpFunction: 'webpackJsonpOCADeck'
},
module: {
rules: [