Skip to content

Commit 673d4b3

Browse files
authored
Merge pull request #2017 from NicoPennec/fix-guests
Display embedded author for guests in comments and news
2 parents 814f9d0 + 512566d commit 673d4b3

8 files changed

Lines changed: 224 additions & 43 deletions

File tree

src/components/pages/entities/EntityNews.vue

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
class="flexrow-item"
2525
:font-size="14"
2626
:is-link="false"
27-
:person="personMap.get(news.author_id)"
27+
:person="news.person"
2828
:size="30"
29-
v-if="personMap.get(news.author_id)"
29+
v-if="news.person"
3030
/>
3131

3232
<div class="flexrow-item task-type-wrapper ml1">
@@ -95,12 +95,7 @@ export default {
9595
},
9696
9797
computed: {
98-
...mapGetters([
99-
'currentProduction',
100-
'personMap',
101-
'taskTypeMap',
102-
'taskStatusMap'
103-
])
98+
...mapGetters(['currentProduction', 'taskTypeMap', 'taskStatusMap'])
10499
},
105100
106101
methods: {
@@ -136,6 +131,7 @@ export default {
136131
this.isLoading = true
137132
this.getEntityNews(this.entity.id)
138133
.then(data => {
134+
// The author is already resolved and enriched by the store action.
139135
this.newsList = data.data
140136
})
141137
.catch(err => {

src/components/widgets/Comment.vue

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@
2626
:is-link="!isCurrentUserClient"
2727
v-if="!isCurrentUserClient || isAuthorClient"
2828
/>
29-
<strong class="flexrow-item">
30-
<people-name
31-
:person="comment.person"
32-
v-if="!isCurrentUserClient || isAuthorClient"
33-
/>
34-
</strong>
29+
<people-name
30+
class="flexrow-item strong"
31+
:person="comment.person"
32+
v-if="!isCurrentUserClient || isAuthorClient"
33+
/>
3534
<div class="filler"></div>
3635
<span class="flexrow-item date" :title="fullDate">
3736
{{ shortDate }}
@@ -153,18 +152,23 @@
153152
v-for="replyComment in comment.replies || []"
154153
>
155154
<div class="flexrow">
156-
<people-avatar
157-
class="flexrow-item"
158-
:size="18"
159-
:font-size="10"
160-
:person="personMap.get(replyComment.person_id)"
161-
:is-link="!isCurrentUserClient"
162-
/>
163-
<strong class="flexrow-item">
155+
<template
156+
v-if="
157+
!isCurrentUserClient || isReplyAuthorClient(replyComment)
158+
"
159+
>
160+
<people-avatar
161+
class="flexrow-item"
162+
:size="18"
163+
:font-size="10"
164+
:person="replyComment.person"
165+
:is-link="!isCurrentUserClient"
166+
/>
164167
<people-name
165-
:person="personMap.get(replyComment.person_id)"
168+
class="flexrow-item strong"
169+
:person="replyComment.person"
166170
/>
167-
</strong>
171+
</template>
168172
<span
169173
class="flexrow-item reply-date"
170174
:title="replyFullDate(replyComment.date)"
@@ -418,7 +422,7 @@
418422
:font-size="12"
419423
:is-link="!isCurrentUserClient"
420424
/>
421-
<people-name class="flexrow-item" :person="comment.person" />
425+
<people-name class="flexrow-item strong" :person="comment.person" />
422426
<span class="filler"> </span>
423427
<span class="flexrow-item date" :title="fullDate">
424428
{{ shortDate }}
@@ -718,9 +722,16 @@ const boxShadowStyle = computed(() => {
718722
})
719723
720724
const isAuthorClient = computed(() => {
721-
return personMap.value.get(props.comment.person_id)?.role === 'client'
725+
const author =
726+
personMap.value.get(props.comment.person_id) || props.comment.person
727+
return author?.role === 'client'
722728
})
723729
730+
const isReplyAuthorClient = reply => {
731+
const author = personMap.value.get(reply.person_id) || reply.person
732+
return author?.role === 'client'
733+
}
734+
724735
const shortenText = (text, length) => {
725736
return stringHelpers.shortenText(text, length)
726737
}

src/components/widgets/PeopleName.vue

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
person_id: person.id
88
}
99
}"
10-
:title="person.full_name"
10+
:title="displayName"
1111
v-if="person?.id && withLink"
1212
>
13-
{{ person.full_name }}
13+
{{ displayName }}
1414
</router-link>
1515
<span class="person-name" v-else-if="person">
16-
{{ person.full_name }}
16+
{{ displayName }}
1717
</span>
1818
</template>
1919

2020
<script setup>
21-
defineProps({
21+
import { computed } from 'vue'
22+
23+
const props = defineProps({
2224
person: {
2325
type: Object,
2426
required: true
@@ -28,6 +30,11 @@ defineProps({
2830
default: false
2931
}
3032
})
33+
34+
// full_name is embedded by the API; name is the client-computed fallback.
35+
const displayName = computed(
36+
() => props.person?.full_name || props.person?.name || undefined
37+
)
3138
</script>
3239
3340
<style lang="scss" scoped>

src/store/modules/entities.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import entitiesApi from '@/store/api/entities'
2+
import peopleStore from '@/store/modules/people'
23
import { RESET_ALL } from '@/store/mutation-types'
34

45
const initialState = {}
@@ -9,7 +10,15 @@ const getters = {}
910

1011
const actions = {
1112
async getEntityNews({ commit }, entityId) {
12-
return entitiesApi.getEntityNews(entityId)
13+
const news = await entitiesApi.getEntityNews(entityId)
14+
// Resolve the author once: live personMap, API-embedded fallback for
15+
// guests, then enrich it (initials, color, avatar) for the avatar.
16+
news.data?.forEach(entry => {
17+
entry.person =
18+
peopleStore.cache.personMap.get(entry.author_id) || entry.person
19+
peopleStore.helpers.addAdditionalInformation(entry.person)
20+
})
21+
return news
1322
},
1423

1524
async getEntityPreviewFiles({ commit }, entityId) {

src/store/modules/people.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const helpers = {
7878

7979
if (person.has_avatar) {
8080
const lastUpdate = person.updated_at || person.created_at
81-
const timestamp = Date.parse(lastUpdate)
81+
const timestamp = Date.parse(lastUpdate) || ''
8282
person.avatarPath = `/api/pictures/thumbnails/persons/${person.id}.png?t=${timestamp}`
8383
}
8484

src/store/modules/tasks.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,19 @@ const helpers = {
119119

120120
getTaskStatus(taskStatusId) {
121121
return taskStatusStore.cache.taskStatusMap.get(taskStatusId)
122+
},
123+
124+
// Fall back to the API-embedded author since guests aren't in personMap.
125+
resolveAuthor(personId, embedded) {
126+
const person = personStore.cache.personMap.get(personId) || embedded
127+
return personStore.helpers.addAdditionalInformation(person)
128+
},
129+
130+
enrichCommentAuthors(comment) {
131+
comment.person = helpers.resolveAuthor(comment.person_id, comment.person)
132+
comment.replies?.forEach(reply => {
133+
reply.person = helpers.resolveAuthor(reply.person_id, reply.person)
134+
})
122135
}
123136
}
124137

@@ -878,9 +891,7 @@ const mutations = {
878891
},
879892

880893
[LOAD_TASK_COMMENTS_END](state, { taskId, comments }) {
881-
comments.forEach(comment => {
882-
comment.person = personStore.cache.personMap.get(comment.person_id)
883-
})
894+
comments.forEach(comment => helpers.enrichCommentAuthors(comment))
884895
state.taskComments[taskId] = sortComments([...comments])
885896
state.taskPreviews[taskId] = comments.reduce((previews, comment) => {
886897
if (comment.previews && comment.previews.length > 0) {
@@ -926,14 +937,7 @@ const mutations = {
926937
comment.task_status = helpers.getTaskStatus(comment.task_status_id)
927938
}
928939

929-
if (comment.person === undefined) {
930-
const getPerson = personStore.getters.getPerson(personStore.state)
931-
comment.person = getPerson(comment.person_id)
932-
}
933-
934-
comment.person = personStore.helpers.addAdditionalInformation(
935-
comment.person
936-
)
940+
helpers.enrichCommentAuthors(comment)
937941

938942
if (!taskId) {
939943
taskId = comment.object_id
@@ -1331,6 +1335,7 @@ const mutations = {
13311335
[ADD_REPLY_TO_COMMENT](state, { comment, reply }) {
13321336
if (!comment.replies) comment.replies = []
13331337
if (!comment.replies.find(r => r.id === reply.id)) {
1338+
reply.person = helpers.resolveAuthor(reply.person_id, reply.person)
13341339
comment.replies.push(reply)
13351340
comment.attachment_files = [
13361341
...(comment.attachment_files || []),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { mount } from '@vue/test-utils'
2+
3+
import PeopleName from '@/components/widgets/PeopleName.vue'
4+
5+
const mountName = person =>
6+
mount(PeopleName, { props: { person, withLink: false } })
7+
8+
describe('widgets/PeopleName', () => {
9+
test('renders the embedded full_name', () => {
10+
const wrapper = mountName({ id: 'person-guest', full_name: 'Guest Author' })
11+
expect(wrapper.find('.person-name').text()).toEqual('Guest Author')
12+
})
13+
14+
test('falls back to the client-computed name when full_name is missing', () => {
15+
const wrapper = mountName({ id: 'person-guest', name: 'Guest Author' })
16+
expect(wrapper.find('.person-name').text()).toEqual('Guest Author')
17+
})
18+
19+
test('renders empty when neither full_name nor name is set', () => {
20+
const wrapper = mountName({ id: 'person-guest' })
21+
expect(wrapper.find('.person-name').text()).toEqual('')
22+
})
23+
})

tests/unit/store/tasks.spec.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { vi } from 'vitest'
2+
3+
// Importing the tasks module transitively pulls in the root store
4+
// (lib/models → timezone → @/store); stub it so no Vuex store is built.
5+
vi.mock('@/store', () => ({ default: {} }))
6+
7+
import tasksStore from '@/store/modules/tasks'
8+
import peopleStore from '@/store/modules/people'
9+
10+
describe('Tasks store', () => {
11+
describe('Comment author resolution', () => {
12+
const studioPerson = {
13+
id: 'person-studio',
14+
first_name: 'Studio',
15+
last_name: 'Member',
16+
full_name: 'Studio Member (live)',
17+
role: 'manager',
18+
has_avatar: false
19+
}
20+
21+
beforeEach(() => {
22+
peopleStore.cache.personMap = new Map([['person-studio', studioPerson]])
23+
})
24+
25+
afterEach(() => {
26+
peopleStore.cache.personMap = new Map()
27+
})
28+
29+
test('LOAD_TASK_COMMENTS_END prefers personMap over embedded author', () => {
30+
const state = { taskComments: {}, taskPreviews: {} }
31+
const comments = [
32+
{
33+
id: 'comment-studio',
34+
person_id: 'person-studio',
35+
created_at: '2026-05-20T10:00:00',
36+
pinned: false,
37+
// Embedded author is stale; the live personMap must win.
38+
person: { id: 'person-studio', full_name: 'Studio Member (stale)' }
39+
}
40+
]
41+
tasksStore.mutations.LOAD_TASK_COMMENTS_END(state, {
42+
taskId: 'task-1',
43+
comments
44+
})
45+
const [comment] = state.taskComments['task-1']
46+
expect(comment.person.full_name).toEqual('Studio Member (live)')
47+
})
48+
49+
test('LOAD_TASK_COMMENTS_END falls back to the embedded guest author', () => {
50+
const state = { taskComments: {}, taskPreviews: {} }
51+
const comments = [
52+
{
53+
id: 'comment-guest',
54+
person_id: 'person-guest',
55+
created_at: '2026-05-20T11:00:00',
56+
pinned: false,
57+
// Guests are absent from personMap; the API embeds the author.
58+
person: {
59+
id: 'person-guest',
60+
first_name: 'Guest',
61+
last_name: 'Author',
62+
full_name: 'Guest Author',
63+
role: 'client',
64+
has_avatar: false
65+
}
66+
}
67+
]
68+
tasksStore.mutations.LOAD_TASK_COMMENTS_END(state, {
69+
taskId: 'task-1',
70+
comments
71+
})
72+
const [comment] = state.taskComments['task-1']
73+
expect(comment.person.full_name).toEqual('Guest Author')
74+
// addAdditionalInformation enriches the embedded author for the avatar.
75+
expect(comment.person.initials).toEqual('GA')
76+
expect(comment.person.name).toEqual('Guest Author')
77+
})
78+
79+
test('LOAD_TASK_COMMENTS_END resolves embedded reply authors too', () => {
80+
const state = { taskComments: {}, taskPreviews: {} }
81+
const comments = [
82+
{
83+
id: 'comment-studio',
84+
person_id: 'person-studio',
85+
created_at: '2026-05-20T10:00:00',
86+
pinned: false,
87+
person: studioPerson,
88+
replies: [
89+
{
90+
id: 'reply-guest',
91+
person_id: 'person-guest',
92+
person: {
93+
id: 'person-guest',
94+
first_name: 'Guest',
95+
last_name: 'Author',
96+
full_name: 'Guest Author',
97+
role: 'client'
98+
}
99+
}
100+
]
101+
}
102+
]
103+
tasksStore.mutations.LOAD_TASK_COMMENTS_END(state, {
104+
taskId: 'task-1',
105+
comments
106+
})
107+
const [comment] = state.taskComments['task-1']
108+
expect(comment.replies[0].person.full_name).toEqual('Guest Author')
109+
expect(comment.replies[0].person.initials).toEqual('GA')
110+
})
111+
112+
test('ADD_REPLY_TO_COMMENT resolves the embedded guest reply author', () => {
113+
const comment = { id: 'comment-studio', replies: [] }
114+
const reply = {
115+
id: 'reply-guest',
116+
person_id: 'person-guest',
117+
person: {
118+
id: 'person-guest',
119+
first_name: 'Guest',
120+
last_name: 'Author',
121+
full_name: 'Guest Author',
122+
role: 'client'
123+
}
124+
}
125+
tasksStore.mutations.ADD_REPLY_TO_COMMENT({}, { comment, reply })
126+
expect(comment.replies[0].person.full_name).toEqual('Guest Author')
127+
expect(comment.replies[0].person.initials).toEqual('GA')
128+
})
129+
})
130+
})

0 commit comments

Comments
 (0)