Compare commits

...

1 Commits

Author SHA1 Message Date
Julius Härtl
a2755f0671 WIP: Naive approach on content sync
Signed-off-by: Julius Härtl <jus@bitgrid.net>
2020-08-01 09:12:43 +02:00
6 changed files with 148 additions and 19 deletions

32
package-lock.json generated
View File

@@ -5254,29 +5254,24 @@
} }
}, },
"@nextcloud/event-bus": { "@nextcloud/event-bus": {
"version": "1.1.4", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.2.0.tgz",
"integrity": "sha512-It27KzmUaSQ7w22nHFwOn8XgeVG0HYYOSNG9gs4UkP5VqcZ16m4ydt3GkMpWcyFec4OUjJc+yf7omRc3pNxsSw==", "integrity": "sha512-pNS0R6Mvgj4WnbJQ8LYjxRjCbRndpwjHNyZYm0zl8U71gbHsUvQIIzTdW7WYg6Nz/FjAlrdmDXJDFLh1DDcIFA==",
"requires": { "requires": {
"@types/semver": "^6.2.1", "@types/semver": "^7.1.0",
"core-js": "^3.6.2", "core-js": "^3.6.2",
"semver": "^6.3.0" "semver": "^7.3.2"
}, },
"dependencies": { "dependencies": {
"@types/semver": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz",
"integrity": "sha512-+beqKQOh9PYxuHvijhVl+tIHvT6tuwOrE9m14zd+MT2A38KoKZhh7pYJ0SNleLtwDsiIxHDsIk9bv01oOxvSvA=="
},
"core-js": { "core-js": {
"version": "3.6.5", "version": "3.6.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
}, },
"semver": { "semver": {
"version": "6.3.0", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
} }
} }
}, },
@@ -5635,8 +5630,7 @@
"@types/node": { "@types/node": {
"version": "13.13.4", "version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==", "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA=="
"dev": true
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
@@ -5656,6 +5650,14 @@
"integrity": "sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ==", "integrity": "sha512-boy4xPNEtiw6N3abRhBi/e7hNvy3Tt8E9ZRAQrwAGzoCGZS/1wjo9KY7JHhnfnEsG5wSjDbymCozUM9a3ea7OQ==",
"dev": true "dev": true
}, },
"@types/semver": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.1.tgz",
"integrity": "sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag==",
"requires": {
"@types/node": "*"
}
},
"@types/stack-utils": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",

View File

@@ -34,6 +34,7 @@
"@nextcloud/auth": "^1.3.0", "@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.3.2", "@nextcloud/axios": "^1.3.2",
"@nextcloud/dialogs": "^1.4.0", "@nextcloud/dialogs": "^1.4.0",
"@nextcloud/event-bus": "^1.2.0",
"@nextcloud/files": "^1.1.0", "@nextcloud/files": "^1.1.0",
"@nextcloud/initial-state": "^1.1.2", "@nextcloud/initial-state": "^1.1.2",
"@nextcloud/l10n": "^1.3.0", "@nextcloud/l10n": "^1.3.0",

View File

@@ -22,6 +22,9 @@
<template> <template>
<div class="board-wrapper"> <div class="board-wrapper">
<div v-if="remoteUpdate" class="board-update-notification">
{{ t('deck', 'The board has been updated by someone else.') }} <a @click="updateFromRemote">{{ t('deck', 'Update') }}</a>
</div>
<Controls :board="board" /> <Controls :board="board" />
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div v-if="loading" key="loading" class="emptycontent"> <div v-if="loading" key="loading" class="emptycontent">
@@ -54,6 +57,9 @@ import { Container, Draggable } from 'vue-smooth-dnd'
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from 'vuex'
import Controls from '../Controls' import Controls from '../Controls'
import Stack from './Stack' import Stack from './Stack'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
const BOARD_POLLING_INTERVAL = 1000
export default { export default {
name: 'Board', name: 'Board',
@@ -81,6 +87,7 @@ export default {
...mapState({ ...mapState({
board: state => state.currentBoard, board: state => state.currentBoard,
showArchived: state => state.showArchived, showArchived: state => state.showArchived,
remoteUpdate: state => state.stack.remoteUpdate,
}), }),
...mapGetters([ ...mapGetters([
'canEdit', 'canEdit',
@@ -100,6 +107,18 @@ export default {
}, },
created() { created() {
this.fetchData() this.fetchData()
setInterval(() => {
this.$store.dispatch('poll', this.id)
}, BOARD_POLLING_INTERVAL)
subscribe('deck:card:modified', (card) => {
console.log('card modified', card.lastModified)
this.$store.dispatch('updateBoardLastModified', { ...this.board, lastModified: card.lastModified })
})
subscribe('deck:stack:modified', (stack) => {
console.log('card modified', stack.lastModified)
this.$store.dispatch('updateBoardLastModified', { ...this.board, lastModified: stack.lastModified })
})
}, },
methods: { methods: {
async fetchData() { async fetchData() {
@@ -113,6 +132,10 @@ export default {
this.loading = false this.loading = false
}, },
updateFromRemote() {
this.$store.dispatch('pollApply', this.id)
},
onDropStack({ removedIndex, addedIndex }) { onDropStack({ removedIndex, addedIndex }) {
this.$store.dispatch('orderStack', { stack: this.stacksByBoard[removedIndex], removedIndex, addedIndex }) this.$store.dispatch('orderStack', { stack: this.stacksByBoard[removedIndex], removedIndex, addedIndex })
}, },
@@ -193,4 +216,33 @@ export default {
} }
} }
.board-update-notification {
position: absolute;
background-color: var(--color-primary-light);
border-radius: var(--border-radius-large);
z-index: 1000;
padding: 4px 20px;
text-align: center;
display: inline-block;
width: auto;
margin: 10px auto;
top: 0;
left: 50%;
transform: translate(-50%, 0px);
animation: slideFromTop var(--animation-slow) ease-out forwards;
a {
font-weight: bold;
padding-left: 20px;
padding-right: 20px;
}
}
@keyframes slideFromTop
{
from {transform: translate(-50%, -100px); opacity: 0;}
to { transform: translate(-50%, 0); opacity: 1;}
}
</style> </style>

View File

@@ -22,6 +22,7 @@
import { CardApi } from './../services/CardApi' import { CardApi } from './../services/CardApi'
import Vue from 'vue' import Vue from 'vue'
import { emit } from '@nextcloud/event-bus'
const apiClient = new CardApi() const apiClient = new CardApi()
@@ -102,6 +103,7 @@ export default {
if (existingIndex !== -1) { if (existingIndex !== -1) {
const existingCard = state.cards.find(_card => _card.id === card.id) const existingCard = state.cards.find(_card => _card.id === card.id)
Vue.set(state.cards, existingIndex, Object.assign({}, existingCard, card)) Vue.set(state.cards, existingIndex, Object.assign({}, existingCard, card))
emit('deck:card:modified', card)
} else { } else {
state.cards.push(card) state.cards.push(card)
} }
@@ -111,11 +113,13 @@ export default {
if (existingIndex !== -1) { if (existingIndex !== -1) {
state.cards.splice(existingIndex, 1) state.cards.splice(existingIndex, 1)
} }
emit('deck:card:modified', card)
}, },
updateCard(state, card) { updateCard(state, card) {
const existingIndex = state.cards.findIndex(_card => _card.id === card.id) const existingIndex = state.cards.findIndex(_card => _card.id === card.id)
if (existingIndex !== -1) { if (existingIndex !== -1) {
Vue.set(state.cards, existingIndex, Object.assign({}, state.cards[existingIndex], card)) Vue.set(state.cards, existingIndex, Object.assign({}, state.cards[existingIndex], card))
emit('deck:card:modified', card)
} }
}, },
updateCardsReorder(state, cards) { updateCardsReorder(state, cards) {
@@ -126,19 +130,24 @@ export default {
Vue.set(state.cards[existingIndex], 'stackId', newCard.stackId) Vue.set(state.cards[existingIndex], 'stackId', newCard.stackId)
} }
} }
emit('deck:card:modified', cards[cards.length - 1])
}, },
assignCardToUser(state, user) { assignCardToUser(state, { card, user }) {
const existingIndex = state.cards.findIndex(_card => _card.id === user.cardId) const existingIndex = state.cards.findIndex(_card => _card.id === user.cardId)
if (existingIndex !== -1) { if (existingIndex !== -1) {
state.cards[existingIndex].assignedUsers.push(user) state.cards[existingIndex].assignedUsers.push(user)
} }
// FIXME: workaround since we have no server time on assignments
emit('deck:card:modified', { ...card, lastModified: Date.now() / 1000 })
}, },
removeUserFromCard(state, user) { removeUserFromCard(state, { card, user }) {
const existingIndex = state.cards.findIndex(_card => _card.id === user.cardId) const existingIndex = state.cards.findIndex(_card => _card.id === user.cardId)
if (existingIndex !== -1) { if (existingIndex !== -1) {
const foundIndex = state.cards[existingIndex].assignedUsers.findIndex(_user => _user.id === user.id) const foundIndex = state.cards[existingIndex].assignedUsers.findIndex(_user => _user.id === user.id)
if (foundIndex !== -1) { if (foundIndex !== -1) {
state.cards[existingIndex].assignedUsers.splice(foundIndex, 1) state.cards[existingIndex].assignedUsers.splice(foundIndex, 1)
// FIXME: workaround since we have no server time on assignments
emit('deck:card:modified', { ...card, lastModified: Date.now() / 1000 })
} }
} }
}, },
@@ -148,17 +157,20 @@ export default {
Vue.set(state.cards[existingIndex], property, card[property]) Vue.set(state.cards[existingIndex], property, card[property])
} }
Vue.set(state.cards[existingIndex], 'lastModified', Date.now() / 1000) Vue.set(state.cards[existingIndex], 'lastModified', Date.now() / 1000)
emit('deck:card:modified', card)
}, },
cardIncreaseAttachmentCount(state, cardId) { cardIncreaseAttachmentCount(state, cardId) {
const existingIndex = state.cards.findIndex(_card => _card.id === cardId) const existingIndex = state.cards.findIndex(_card => _card.id === cardId)
if (existingIndex !== -1) { if (existingIndex !== -1) {
Vue.set(state.cards[existingIndex], 'attachmentCount', state.cards[existingIndex].attachmentCount + 1) Vue.set(state.cards[existingIndex], 'attachmentCount', state.cards[existingIndex].attachmentCount + 1)
emit('deck:card:modified', state.cards[existingIndex])
} }
}, },
cardDecreaseAttachmentCount(state, cardId) { cardDecreaseAttachmentCount(state, cardId) {
const existingIndex = state.cards.findIndex(_card => _card.id === cardId) const existingIndex = state.cards.findIndex(_card => _card.id === cardId)
if (existingIndex !== -1) { if (existingIndex !== -1) {
Vue.set(state.cards[existingIndex], 'attachmentCount', state.cards[existingIndex].attachmentCount - 1) Vue.set(state.cards[existingIndex], 'attachmentCount', state.cards[existingIndex].attachmentCount - 1)
emit('deck:card:modified', state.cards[existingIndex])
} }
}, },
}, },
@@ -213,11 +225,11 @@ export default {
}, },
async assignCardToUser({ commit }, { card, assignee }) { async assignCardToUser({ commit }, { card, assignee }) {
const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type) const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type)
commit('assignCardToUser', user) commit('assignCardToUser', { card, user })
}, },
async removeUserFromCard({ commit }, { card, assignee }) { async removeUserFromCard({ commit }, { card, assignee }) {
const user = await apiClient.removeUser(card.id, assignee.userId, assignee.type) const user = await apiClient.removeUser(card.id, assignee.userId, assignee.type)
commit('removeUserFromCard', user) commit('removeUserFromCard', { card, user })
}, },
async addLabel({ commit }, data) { async addLabel({ commit }, data) {
await apiClient.assignLabelToCard(data) await apiClient.assignLabelToCard(data)

View File

@@ -317,6 +317,11 @@ export default new Vuex.Store({
const storedBoard = await apiClient.updateBoard(board) const storedBoard = await apiClient.updateBoard(board)
commit('addBoard', storedBoard) commit('addBoard', storedBoard)
}, },
updateBoardLastModified({ commit }, board) {
commit('addBoard', board)
commit('setCurrentBoard', board)
},
createBoard({ commit }, boardData) { createBoard({ commit }, boardData) {
apiClient.createBoard(boardData) apiClient.createBoard(boardData)
.then((board) => { .then((board) => {

View File

@@ -21,14 +21,18 @@
*/ */
import Vue from 'vue' import Vue from 'vue'
import { BoardApi } from './../services/BoardApi'
import { StackApi } from './../services/StackApi' import { StackApi } from './../services/StackApi'
import applyOrderToArray from './../helpers/applyOrderToArray' import applyOrderToArray from './../helpers/applyOrderToArray'
import { emit } from '@nextcloud/event-bus'
const boardApiClient = new BoardApi()
const apiClient = new StackApi() const apiClient = new StackApi()
export default { export default {
state: { state: {
stacks: [], stacks: [],
remoteUpdate: null,
}, },
getters: { getters: {
stacksByBoard: state => (id) => { stacksByBoard: state => (id) => {
@@ -36,6 +40,12 @@ export default {
}, },
}, },
mutations: { mutations: {
clearStacks(state) {
state.stacks = []
},
updateRemote(state, response) {
Vue.set(state, 'remoteUpdate', response)
},
addStack(state, stack) { addStack(state, stack) {
const existingIndex = state.stacks.findIndex(_stack => _stack.id === stack.id) const existingIndex = state.stacks.findIndex(_stack => _stack.id === stack.id)
if (existingIndex !== -1) { if (existingIndex !== -1) {
@@ -73,6 +83,7 @@ export default {
OC.Notification.showTemporary('Failed to change order') OC.Notification.showTemporary('Failed to change order')
console.error(err.response.data.message) console.error(err.response.data.message)
commit('orderStack', { stack, addedIndex, removedIndex }) commit('orderStack', { stack, addedIndex, removedIndex })
emit('deck:stack:modified', { ...stack, lastModified: Date.now() / 1000 })
}) })
}, },
async loadStacks({ commit }, boardId) { async loadStacks({ commit }, boardId) {
@@ -89,13 +100,57 @@ export default {
} }
delete stack.cards delete stack.cards
commit('addStack', stack) commit('addStack', stack)
emit('deck:stack:modified', { ...stack, lastModified: Date.now() / 1000 })
} }
}, },
async poll({ commit, rootState, state }, boardId) {
if (!rootState.currentBoard) {
return
}
// TODO: set If-Modified-Since header
const board = await boardApiClient.loadById(rootState.currentBoard.id)
console.debug('[deck] poll: remote(' + board.lastModified + ') local(' + rootState.currentBoard.lastModified + ') update(' + state.remoteUpdate?.lastModified + ')')
if (rootState.currentBoard.lastModified >= board.lastModified || state.remoteUpdate?.lastModified === board.lastModified) {
console.debug('[deck] poll: no new data for board ' + board.title)
return
}
let call = 'loadStacks'
if (this.state.showArchived === true) {
call = 'loadArchivedStacks'
}
const stacks = await apiClient[call](boardId)
board.stacks = stacks
commit('updateRemote', board)
console.debug('[deck] poll: applied new data for board ' + board.title)
},
async pollApply({ commit, state }, boardId) {
commit('clearCards')
commit('clearStacks')
// TODO: trigger board updated at on every operation
// event bus deck:board:modified board
// event bus deck:card:modified card
// event bus deck:stack:modified stack
for (const i in state.remoteUpdate.stacks) {
const stack = state.remoteUpdate.stacks[i]
for (const j in stack.cards) {
commit('addCard', stack.cards[j])
}
delete stack.cards
commit('addStack', stack)
}
delete state.remoteUpdate.stacks
commit('setCurrentBoard', state.remoteUpdate)
commit('updateRemote', null)
},
createStack({ commit }, stack) { createStack({ commit }, stack) {
stack.boardId = this.state.currentBoard.id stack.boardId = this.state.currentBoard.id
apiClient.createStack(stack) apiClient.createStack(stack)
.then((createdStack) => { .then((createdStack) => {
commit('addStack', createdStack) commit('addStack', createdStack)
emit('deck:stack:modified', { ...createdStack, lastModified: Date.now() / 1000 })
}) })
}, },
deleteStack({ commit }, stack) { deleteStack({ commit }, stack) {
@@ -103,12 +158,14 @@ export default {
.then((stack) => { .then((stack) => {
commit('deleteStack', stack) commit('deleteStack', stack)
commit('moveStackToTrash', stack) commit('moveStackToTrash', stack)
emit('deck:stack:modified', { ...stack, lastModified: Date.now() / 1000 })
}) })
}, },
updateStack({ commit }, stack) { updateStack({ commit }, stack) {
apiClient.updateStack(stack) apiClient.updateStack(stack)
.then((stack) => { .then((stack) => {
commit('updateStack', stack) commit('updateStack', stack)
emit('deck:stack:modified', { ...stack, lastModified: Date.now() / 1000 })
}) })
}, },
}, },