Add an initial Events Timeline for Stories
This doesn't cover all the possible events yet, but is a sufficient starting point.
This commit is contained in:
parent
2eb447ccb1
commit
fa0bee29d1
45
src/components/EventComment.vue
Normal file
45
src/components/EventComment.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
commented on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
<div class="event-body">
|
||||||
|
{{ event.comment.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventComment',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
42
src/components/EventStoryCreated.vue
Normal file
42
src/components/EventStoryCreated.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
created this story on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventStoryCreated',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
42
src/components/EventStoryUpdated.vue
Normal file
42
src/components/EventStoryUpdated.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
updated this story on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventStoryUpdated',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
53
src/components/EventTagsAdded.vue
Normal file
53
src/components/EventTagsAdded.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
tagged this story as
|
||||||
|
<span class="tag" v-for="tag in info.tags" :key="tag">{{ tag }}</span>
|
||||||
|
on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventTagsAdded',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
},
|
||||||
|
info () {
|
||||||
|
return JSON.parse(this.event.event_info)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag {
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin: 0 5px;
|
||||||
|
background-color: #f0ad4e;
|
||||||
|
color: #f7f6f4;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
48
src/components/EventTaskAssigneeUpdated.vue
Normal file
48
src/components/EventTaskAssigneeUpdated.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
updated the asssignee of the task "{{ info.task_title }}"
|
||||||
|
from <a href="" v-if="info.old_assignee_id">{{ info.old_assignee_fullname }}</a> <span v-if="!info.old_assignee_id">{{ info.old_assignee_fullname }}</span>
|
||||||
|
to <a href="">{{ info.new_assignee_fullname }}</a>
|
||||||
|
on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventTaskAssigneeUpdated',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
},
|
||||||
|
info () {
|
||||||
|
return JSON.parse(this.event.event_info)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
46
src/components/EventTaskCreated.vue
Normal file
46
src/components/EventTaskCreated.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
created the task "{{ info.task_title }}"
|
||||||
|
on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventTaskCreated',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
},
|
||||||
|
info () {
|
||||||
|
return JSON.parse(this.event.event_info)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
46
src/components/EventTaskUpdated.vue
Normal file
46
src/components/EventTaskUpdated.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-details">
|
||||||
|
<p class="event-title">
|
||||||
|
<a href="">{{ author.full_name }}</a>
|
||||||
|
updated the task "{{ info.task_title }}"
|
||||||
|
on {{ createdDate.toDateString() }}
|
||||||
|
at {{ createdDate.toLocaleTimeString() }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EventTaskUpdated',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
},
|
||||||
|
info () {
|
||||||
|
return JSON.parse(this.event.event_info)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
129
src/components/TimelineEvent.vue
Normal file
129
src/components/TimelineEvent.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline-event">
|
||||||
|
<p class="timeline-event-icon" :class="iconClass">
|
||||||
|
x
|
||||||
|
</p>
|
||||||
|
<EventStoryCreated :event="event" v-if="event.event_type === 'story_created'" />
|
||||||
|
<EventStoryUpdated :event="event" v-if="event.event_type === 'story_details_changed'" />
|
||||||
|
<EventTagsAdded :event="event" v-if="event.event_type === 'tags_added'" />
|
||||||
|
<EventTaskCreated :event="event" v-if="event.event_type === 'task_created'" />
|
||||||
|
<EventTaskUpdated :event="event" v-if="event.event_type === 'task_details_changed'" />
|
||||||
|
<EventTaskAssigneeUpdated :event="event" v-if="event.event_type === 'task_assignee_changed'" />
|
||||||
|
<EventComment :event="event" v-if="isComment" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import EventStoryCreated from '@/components/EventStoryCreated'
|
||||||
|
import EventStoryUpdated from '@/components/EventStoryUpdated'
|
||||||
|
import EventTagsAdded from '@/components/EventTagsAdded'
|
||||||
|
import EventTaskCreated from '@/components/EventTaskCreated'
|
||||||
|
import EventTaskUpdated from '@/components/EventTaskUpdated'
|
||||||
|
import EventTaskAssigneeUpdated from '@/components/EventTaskAssigneeUpdated'
|
||||||
|
import EventComment from '@/components/EventComment'
|
||||||
|
|
||||||
|
const PRIMARY_EVENTS = [
|
||||||
|
'story_created',
|
||||||
|
'user_comment'
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TimelineEvent',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
author: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['event'],
|
||||||
|
components: {
|
||||||
|
EventComment,
|
||||||
|
EventStoryCreated,
|
||||||
|
EventStoryUpdated,
|
||||||
|
EventTagsAdded,
|
||||||
|
EventTaskCreated,
|
||||||
|
EventTaskUpdated,
|
||||||
|
EventTaskAssigneeUpdated
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
createdDate () {
|
||||||
|
return new Date(this.event.created_at)
|
||||||
|
},
|
||||||
|
isComment () {
|
||||||
|
return this.event.event_type === 'user_comment'
|
||||||
|
},
|
||||||
|
iconClass () {
|
||||||
|
return {
|
||||||
|
primary: PRIMARY_EVENTS.includes(this.event.event_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getAuthor(this.event.author_id)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAuthor (authorId) {
|
||||||
|
const baseUrl = 'http://localhost:8080/v1'
|
||||||
|
const { data: author } = await axios.get(`${baseUrl}/users/${authorId}`)
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.timeline-event {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.timeline-event-icon {
|
||||||
|
flex: 0 0 30px;
|
||||||
|
margin: 3px 5px;
|
||||||
|
background-color: #f7f6f4;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 3px solid #ddd;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
flex-basis: 40px;
|
||||||
|
margin: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
background-color: #c43422;
|
||||||
|
color: #f7f6f4;
|
||||||
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-details {
|
||||||
|
margin-left: 50px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
|
||||||
|
::v-deep p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .event-title {
|
||||||
|
padding: 5px 10px;
|
||||||
|
line-height: 30px;
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 10;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .event-body {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -43,21 +43,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2>Events Timeline and Comments</h2>
|
<h2>Events Timeline and Comments</h2>
|
||||||
<div class="events">
|
<div class="events">
|
||||||
<div class="timeline-event" v-for="event in events" :key="event.id">
|
<TimelineEvent v-for="event in events" :key="event.id" :event="event"/>
|
||||||
{{ event.event_type }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TaskListEntry from '@/components/TaskListEntry.vue'
|
import TaskListEntry from '@/components/TaskListEntry.vue'
|
||||||
|
import TimelineEvent from '@/components/TimelineEvent.vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'StoryDetailView',
|
name: 'StoryDetailView',
|
||||||
components: {
|
components: {
|
||||||
TaskListEntry
|
TaskListEntry,
|
||||||
|
TimelineEvent
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -183,4 +183,20 @@ h2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.events {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: " ";
|
||||||
|
width: 4px;
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
left: 21px;
|
||||||
|
top: 20px;
|
||||||
|
background-color: #ddd;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user