Merge pull request #1189 from nextcloud/boardTimeline

Board and Card Timeline
This commit is contained in:
Julius Härtl
2019-08-27 12:40:44 +02:00
committed by GitHub
6 changed files with 204 additions and 8 deletions

View File

@@ -0,0 +1,77 @@
<!--
- @copyright Copyright (c) 2019 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 v-if="activity" class="activity">
<img :src="activity.icon" class="activity--icon">
<div class="activity--message" v-html="parseMessage(activity)" />
<div class="activity--timestamp">{{ getTime(activity.datetime) }}</div>
</div>
</template>
<script>
export default {
name: 'ActivityEntry',
props: {
activity: {
type: Object,
default: null
}
},
methods: {
getTime(timestamp) {
return OC.Util.relativeModifiedDate(timestamp)
},
parseMessage(activity) {
let subject = activity.subject_rich[0]
let parameters = activity.subject_rich[1]
if (parameters.after && typeof parameters.after.id === 'string' && parameters.after.id.startsWith('dt:')) {
let dateTime = parameters.after.id.substr(3)
parameters.after.name = window.moment(dateTime).format('L LTS')
}
return OCA.Activity.RichObjectStringParser.parseMessage(subject, parameters)
}
}
}
</script>
<style scoped lang="scss">
.activity {
display: flex;
.activity--icon {
width: 16px;
height: 16px;
flex-shrink: 0;
flex-grow: 0;
}
.activity--message {
margin-left: 10px;
}
.activity--timestamp {
color: var(--color-text-maxcontrast);
text-align: right;
font-size: 0.8em;
width: 25%;
padding: 1px;
}
}
</style>

View File

@@ -1,15 +1,21 @@
<template> <template>
<div> <div>
timeline <div v-if="isLoading" class="icon icon-loading" />
<ActivityEntry v-for="entry in boardActivity" v-else :key="entry.activity_id"
:activity="entry" />
<button v-if="activityLoadMore" @click="loadMore">Load More</button>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'
import ActivityEntry from '../ActivityEntry'
export default { export default {
name: 'TimelineTabSidebard', name: 'TimelineTabSidebard',
components: { components: {
ActivityEntry
}, },
props: { props: {
board: { board: {
@@ -19,8 +25,37 @@ export default {
}, },
data() { data() {
return { return {
isLoading: false,
params: {
type: 'deck',
since: 0,
object_id: this.board.id
}
}
},
computed: {
...mapState({
boardActivity: 'activity',
activityLoadMore: 'activityLoadMore'
})
},
created() {
this.loadBoardActivity()
},
methods: {
loadBoardActivity() {
this.isLoading = true
this.$store.dispatch('loadActivity', this.params).then(response => {
this.isLoading = false
})
},
loadMore() {
let array = Object.values(this.boardActivity)
let aId = (array[array.length - 1].activity_id)
this.params.since = aId
this.loadBoardActivity()
} }
} }
} }
</script> </script>

View File

@@ -77,7 +77,10 @@
</button> </button>
</AppSidebarTab> </AppSidebarTab>
<AppSidebarTab :order="2" name="Timeline" icon="icon-activity"> <AppSidebarTab :order="2" name="Timeline" icon="icon-activity">
this is the activity tab <div v-if="isLoading" class="icon icon-loading" />
<ActivityEntry v-for="entry in cardActivity" v-else :key="entry.activity_id"
:activity="entry" />
<button v-if="activityLoadMore" @click="loadMore">Load More</button>
</AppSidebarTab> </AppSidebarTab>
</app-sidebar> </app-sidebar>
</template> </template>
@@ -88,10 +91,12 @@ import { mapState } from 'vuex'
import VueEasymde from 'vue-easymde' import VueEasymde from 'vue-easymde'
import { Actions } from 'nextcloud-vue/dist/Components/Actions' import { Actions } from 'nextcloud-vue/dist/Components/Actions'
import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton' import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton'
import ActivityEntry from '../ActivityEntry'
export default { export default {
name: 'CardSidebar', name: 'CardSidebar',
components: { components: {
ActivityEntry,
AppSidebar, AppSidebar,
AppSidebarTab, AppSidebarTab,
Multiselect, Multiselect,
@@ -122,13 +127,21 @@ export default {
toolbar: false toolbar: false
}, },
lastModifiedRelative: null, lastModifiedRelative: null,
lastCreatedRemative: null lastCreatedRemative: null,
params: {
type: 'filter',
since: 0,
object_type: 'deck_card',
object_id: this.id
}
} }
}, },
computed: { computed: {
...mapState({ ...mapState({
currentBoard: state => state.currentBoard, currentBoard: state => state.currentBoard,
assignableUsers: state => state.assignableUsers assignableUsers: state => state.assignableUsers,
cardActivity: 'activity',
activityLoadMore: 'activityLoadMore'
}), }),
currentCard() { currentCard() {
return this.$store.getters.cardById(this.id) return this.$store.getters.cardById(this.id)
@@ -161,8 +174,16 @@ export default {
handler() { handler() {
this.copiedCard = JSON.parse(JSON.stringify(this.currentCard)) this.copiedCard = JSON.parse(JSON.stringify(this.currentCard))
this.allLabels = this.currentCard.labels this.allLabels = this.currentCard.labels
this.assignedUsers = this.currentCard.assignedUsers.map((item) => item.participant)
if (this.currentCard.assignedUsers.length > 0) {
this.assignedUsers = this.currentCard.assignedUsers.map((item) => item.participant)
}
this.desc = this.currentCard.description
this.updateRelativeTimestamps() this.updateRelativeTimestamps()
this.params.object_id = this.id
this.loadCardActivity()
} }
}, },
@@ -172,6 +193,7 @@ export default {
}, },
created() { created() {
setInterval(this.updateRelativeTimestamps, 10000) setInterval(this.updateRelativeTimestamps, 10000)
this.loadCardActivity()
}, },
destroyed() { destroyed() {
clearInterval(this.updateRelativeTimestamps) clearInterval(this.updateRelativeTimestamps)
@@ -230,6 +252,19 @@ export default {
} }
this.$store.dispatch('removeLabel', data) this.$store.dispatch('removeLabel', data)
}, },
loadCardActivity() {
this.isLoading = true
this.$store.dispatch('loadActivity', this.params).then(response => {
this.isLoading = false
})
},
loadMore() {
let array = Object.values(this.cardActivity)
let aId = (array[array.length - 1].activity_id)
this.params.since = aId
this.loadCardActivity()
},
clickAddNewAttachmment() { clickAddNewAttachmment() {
} }

View File

@@ -92,6 +92,11 @@ export default new Router({
return { return {
id: parseInt(route.params.id, 10) id: parseInt(route.params.id, 10)
} }
},
sidebar: (route) => {
return {
id: parseInt(route.params.id, 10)
}
} }
} }
}, },

View File

@@ -54,7 +54,9 @@ export default new Vuex.Store({
boards: [], boards: [],
sharees: [], sharees: [],
assignableUsers: [], assignableUsers: [],
boardFilter: BOARD_FILTERS.ALL boardFilter: BOARD_FILTERS.ALL,
activity: [],
activityLoadMore: true
}, },
getters: { getters: {
boards: state => { boards: state => {
@@ -140,6 +142,20 @@ export default new Vuex.Store({
state.sharees = shareesUsersAndGroups.users state.sharees = shareesUsersAndGroups.users
state.sharees.push(...shareesUsersAndGroups.groups) state.sharees.push(...shareesUsersAndGroups.groups)
}, },
setActivity(state, activity) {
activity.forEach(element => {
if (element.subject_rich[1].board.id === state.currentBoard.id) {
state.activity.push(element)
}
})
},
clearActivity(state) {
state.activity = []
},
setActivityLoadMore(state, value) {
state.activityLoadMore = value
},
setAssignableUsers(state, users) { setAssignableUsers(state, users) {
state.assignableUsers = users state.assignableUsers = users
}, },
@@ -268,6 +284,30 @@ export default new Vuex.Store({
commit('setSharees', response.data.ocs.data) commit('setSharees', response.data.ocs.data)
}) })
}, },
loadActivity({ commit }, obj) {
const params = new URLSearchParams()
params.append('format', 'json')
params.append('type', 'deck')
params.append('since', obj.since)
params.append('object_type', obj.object_type)
params.append('object_id', obj.object_id)
if (obj.since === 0) {
commit('clearActivity')
}
let keyword = 'deck'
if (obj.type === 'filter') {
keyword = 'filter'
}
axios.get(OC.linkToOCS('apps/activity/api/v2/activity') + keyword, { params }).then((response) => {
commit('setActivity', response.data.ocs.data)
commit('setActivityLoadMore', true)
if (response.data.ocs.meta.statuscode === 304) {
commit('setActivityLoadMore', false)
}
})
},
setBoardFilter({ commmit }, filter) { setBoardFilter({ commmit }, filter) {
commmit('setBoardFilter', filter) commmit('setBoardFilter', filter)

View File

@@ -23,6 +23,10 @@
use OCP\Util; use OCP\Util;
Util::addScript('activity', 'richObjectStringParser');
Util::addScript('activity', 'templates');
Util::addStyle('activity', 'style');
style('deck', 'globalstyles'); style('deck', 'globalstyles');
script('deck', 'deck'); script('deck', 'deck');