Merge pull request #1189 from nextcloud/boardTimeline
Board and Card Timeline
This commit is contained in:
77
src/components/ActivityEntry.vue
Normal file
77
src/components/ActivityEntry.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user