Skip to content

Commit 3a3fc67

Browse files
feat: allows editing and deleting unpublished comments. trainees cant view unpublished comments from trainer during review. submitting a report publishes all comments. all comment updates are validated in backend as well (#298)
1 parent fb336a1 commit 3a3fc67

33 files changed

+1389
-50
lines changed

packages/api/schema.gql

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ type Comment {
33
id: ID!
44
text: String
55
user: UserInterface!
6+
published: Boolean!
67
}
78

89
"""
@@ -24,6 +25,20 @@ type CreateCommentPayload {
2425
commentable: CommentableInterface!
2526
}
2627

28+
type UpdateCommentPayload {
29+
comment: Comment!
30+
commentable: CommentableInterface!
31+
}
32+
33+
type DeleteCommentPayload {
34+
comment: Comment!
35+
commentable: CommentableInterface!
36+
}
37+
38+
type PublishCommentsPayload {
39+
report: Report!
40+
}
41+
2742
type Day implements CommentableInterface {
2843
comments: [Comment!]!
2944
createdAt: String!
@@ -129,6 +144,41 @@ type Mutation {
129144
"""
130145
createCommentOnReport(text: String!, id: ID!, traineeId: ID!): CreateCommentPayload!
131146

147+
"""
148+
Updates a comment on a Day which is identified by the id argument.
149+
"""
150+
updateCommentOnDay(text: String!, id: ID!, traineeId: ID!, commentId: ID!): UpdateCommentPayload!
151+
152+
"""
153+
Updates a comment on a Entry which is identified by the id argument.
154+
"""
155+
updateCommentOnEntry(text: String!, id: ID!, traineeId: ID!, commentId: ID!): UpdateCommentPayload!
156+
157+
"""
158+
Updates a comment on a Report which is identified by the id argument.
159+
"""
160+
updateCommentOnReport(text: String!, id: ID!, traineeId: ID!, commentId: ID!): UpdateCommentPayload!
161+
162+
"""
163+
Deletes a comment on a Day which is identified by the id argument.
164+
"""
165+
deleteCommentOnDay(id: ID!, traineeId: ID!, commentId: ID!): DeleteCommentPayload!
166+
167+
"""
168+
Deletes a comment on a Entry which is identified by the id argument.
169+
"""
170+
deleteCommentOnEntry(id: ID!, traineeId: ID!, commentId: ID!): DeleteCommentPayload!
171+
172+
"""
173+
Deletes a comment on a Report which is identified by the id argument.
174+
"""
175+
deleteCommentOnReport(id: ID!, traineeId: ID!, commentId: ID!): DeleteCommentPayload!
176+
177+
"""
178+
Publishes all comments on a report which is identified by the id argument.
179+
"""
180+
publishAllComments(id: ID!, traineeId: ID!): PublishCommentsPayload!
181+
132182
"""
133183
Creates a new entry which is assigned to the matching report based on the day Id
134184
"""

packages/api/src/graphql.ts

Lines changed: 117 additions & 0 deletions
Large diffs are not rendered by default.

packages/backend/src/i18n/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const GermanTranslations: Translations = {
1313
missingReport: 'Bericht konnte nicht gefunden werden',
1414
missingDay: 'Tag konnte nicht gefunden werden',
1515
missingEntry: 'Eintrag konnte nicht gefunden werden',
16+
missingComment: 'Kommentar konnte nicht gefunden werden',
1617
wrongReportStatus: 'Der Bericht hat den falschen Status',
1718
wrongDayStatus: 'Der Tag hat den falschen Status',
1819
userAlreadyExists: 'Ein Benutzer mit dieser Email existiert bereits',
@@ -26,6 +27,7 @@ export const GermanTranslations: Translations = {
2627
missingPeriod: 'Die Ausbildungsperiode fehlt',
2728
cantDeleteYourself: 'Ein Admin kann sich nicht selber für die Löschung markieren',
2829
cantChangeOwnEmail: 'Ein Admin kann seine eigene E-Mail Adresse nicht ändern',
30+
cantEditPublishedComment: 'Veröffentlichte Kommentare können nicht mehr editiert werden',
2931
},
3032
email: {
3133
hello: 'Hallo',

packages/backend/src/i18n/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const EnglishTranslations: Translations = {
1313
missingReport: "clouldn't find report",
1414
missingDay: "clouldn't find day",
1515
missingEntry: "couldn't find entry",
16+
missingComment: "couldn't find comment",
1617
wrongReportStatus: 'wrong report status',
1718
wrongDayStatus: 'wrong Day status',
1819
userAlreadyExists: 'User with this Email already exists',
@@ -26,6 +27,7 @@ export const EnglishTranslations: Translations = {
2627
missingPeriod: 'Missing period',
2728
cantDeleteYourself: "An Admin can't mark themselves for deletion",
2829
cantChangeOwnEmail: "An Admin can't change their own e-mail address",
30+
cantEditPublishedComment: 'Published Comments can not be edited',
2931
},
3032
email: {
3133
hello: 'Hello',

packages/backend/src/i18n/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type Translations = {
1717
missingReport: string
1818
missingDay: string
1919
missingEntry: string
20+
missingComment: string
2021
wrongReportStatus: string
2122
wrongDayStatus: string
2223
userAlreadyExists: string
@@ -30,6 +31,7 @@ export type Translations = {
3031
missingPeriod: string
3132
cantDeleteYourself: string
3233
cantChangeOwnEmail: string
34+
cantEditPublishedComment: string
3335
}
3436
email: EmailTranslations
3537
print: PrintTranslations

packages/backend/src/resolvers/comment.resolver.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { AuthenticatedContext, GqlResolvers } from '@lara/api'
44

55
import { reportById } from '../repositories/report.repo'
66
import { userById } from '../repositories/user.repo'
7-
import { commentableReports, createCommentOnCommentable } from '../services/comment.service'
7+
import {
8+
commentableReports,
9+
createCommentOnCommentable,
10+
deleteCommentOnCommentable,
11+
publishCommentsOnReport,
12+
updateCommentOnCommentable,
13+
} from '../services/comment.service'
814
import { reportDayByDayId } from '../services/day.service'
915
import { reportDayEntryByEntryId } from '../services/entry.service'
1016
import { t } from '../i18n'
@@ -71,5 +77,92 @@ export const commentResolver: GqlResolvers<AuthenticatedContext> = {
7177
report,
7278
})
7379
},
80+
updateCommentOnDay: async (_parent, { id, text, traineeId, commentId }, { currentUser }) => {
81+
const reports = await commentableReports(traineeId)
82+
const { report, day } = reportDayByDayId(id, reports)
83+
84+
return updateCommentOnCommentable({
85+
commentable: day,
86+
text,
87+
currentUser,
88+
report,
89+
commentId,
90+
})
91+
},
92+
updateCommentOnEntry: async (_parent, { id, text, traineeId, commentId }, { currentUser }) => {
93+
const reports = await commentableReports(traineeId)
94+
const { report, entry } = reportDayEntryByEntryId(id, reports)
95+
96+
return updateCommentOnCommentable({
97+
commentable: entry,
98+
text,
99+
currentUser,
100+
report,
101+
commentId,
102+
})
103+
},
104+
updateCommentOnReport: async (_parent, { id, text, traineeId, commentId }, { currentUser }) => {
105+
const report = await reportById(id)
106+
107+
if (report?.traineeId !== traineeId) {
108+
throw new GraphQLError(t('errors.missingReport', currentUser.language))
109+
}
110+
111+
return updateCommentOnCommentable({
112+
commentable: report,
113+
text,
114+
currentUser,
115+
report,
116+
commentId,
117+
})
118+
},
119+
deleteCommentOnDay: async (_parent, { id, traineeId, commentId }, { currentUser }) => {
120+
const reports = await commentableReports(traineeId)
121+
const { report, day } = reportDayByDayId(id, reports)
122+
123+
return deleteCommentOnCommentable({
124+
commentable: day,
125+
currentUser,
126+
report,
127+
commentId,
128+
})
129+
},
130+
deleteCommentOnEntry: async (_parent, { id, traineeId, commentId }, { currentUser }) => {
131+
const reports = await commentableReports(traineeId)
132+
const { report, entry } = reportDayEntryByEntryId(id, reports)
133+
134+
return deleteCommentOnCommentable({
135+
commentable: entry,
136+
currentUser,
137+
report,
138+
commentId,
139+
})
140+
},
141+
deleteCommentOnReport: async (_parent, { id, traineeId, commentId }, { currentUser }) => {
142+
const report = await reportById(id)
143+
144+
if (report?.traineeId !== traineeId) {
145+
throw new GraphQLError(t('errors.missingReport', currentUser.language))
146+
}
147+
148+
return deleteCommentOnCommentable({
149+
commentable: report,
150+
currentUser,
151+
report,
152+
commentId,
153+
})
154+
},
155+
publishAllComments: async (_parent, { id, traineeId }, { currentUser }) => {
156+
const report = await reportById(id)
157+
158+
if (report?.traineeId !== traineeId) {
159+
throw new GraphQLError(t('errors.missingReport', currentUser.language))
160+
}
161+
162+
return publishCommentsOnReport({
163+
currentUser,
164+
report,
165+
})
166+
},
74167
},
75168
}

packages/backend/src/resolvers/report.resolver.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ export const reportTraineeResolver: GqlResolvers<TraineeContext> = {
7575
: report
7676
}
7777

78+
if (report && report.status === 'review')
79+
return {
80+
...report,
81+
comments: report.comments.filter((com) => com.published),
82+
days: report.days.map((day) => ({
83+
...day,
84+
entries: day.entries.map((entry) => ({
85+
...entry,
86+
comments: entry.comments.filter((com) => com.published),
87+
})),
88+
comments: day.comments.filter((com) => com.published),
89+
})),
90+
}
91+
7892
return report
7993
},
8094
},

packages/backend/src/services/comment.service.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const generateComment = (text: string, user: User): Comment => {
2020
createdAt: new Date().toISOString(),
2121
userId: user.id,
2222
text,
23+
published: false,
2324
}
2425
}
2526

@@ -62,6 +63,133 @@ export const createCommentOnCommentable = async ({
6263
}
6364
}
6465

66+
type UpdateCommentOnCommentableOptions = {
67+
commentable?: CommentableInterface
68+
report?: Report
69+
text: string
70+
currentUser: User
71+
commentId: string
72+
}
73+
74+
/**
75+
* Abstract method to update comments for entries, days and reports
76+
* @param options Data for updating a comment
77+
* @returns GQL update comment payload
78+
*/
79+
export const updateCommentOnCommentable = async ({
80+
commentable,
81+
text,
82+
report,
83+
currentUser,
84+
commentId,
85+
}: UpdateCommentOnCommentableOptions): Promise<GqlResolversTypes['UpdateCommentPayload']> => {
86+
const t = createT(currentUser.language)
87+
88+
if (!report) {
89+
throw new GraphQLError(t('errors.missingReport'))
90+
}
91+
92+
if (!commentable) {
93+
throw new GraphQLError(t('errors.missingCommentable'))
94+
}
95+
96+
const comment = commentable.comments.find((com) => com.id === commentId)
97+
if (!comment) {
98+
throw new GraphQLError(t('errors.missingComment'))
99+
}
100+
if (comment.published) {
101+
throw new GraphQLError(t('errors.cantEditPublishedComment'))
102+
}
103+
comment.text = text
104+
105+
await updateReport(report, { updateKeys: ['days', 'comments'] })
106+
107+
return {
108+
comment,
109+
commentable,
110+
}
111+
}
112+
113+
type DeleteCommentOnCommentableOptions = {
114+
commentable?: CommentableInterface
115+
report?: Report
116+
currentUser: User
117+
commentId: string
118+
}
119+
120+
/**
121+
* Abstract method to delete comments for entries, days and reports
122+
* @param options Data for deleting a comment
123+
* @returns GQL delete comment payload
124+
*/
125+
export const deleteCommentOnCommentable = async ({
126+
commentable,
127+
report,
128+
currentUser,
129+
commentId,
130+
}: DeleteCommentOnCommentableOptions): Promise<GqlResolversTypes['DeleteCommentPayload']> => {
131+
const t = createT(currentUser.language)
132+
133+
if (!report) {
134+
throw new GraphQLError(t('errors.missingReport'))
135+
}
136+
137+
if (!commentable) {
138+
throw new GraphQLError(t('errors.missingCommentable'))
139+
}
140+
141+
const comment = commentable.comments.find((com) => com.id === commentId)
142+
if (!comment) {
143+
throw new GraphQLError(t('errors.missingComment'))
144+
}
145+
if (comment.published) {
146+
throw new GraphQLError(t('errors.cantEditPublishedComment'))
147+
}
148+
commentable.comments = commentable.comments.filter((com) => com.id !== comment.id)
149+
150+
await updateReport(report, { updateKeys: ['days', 'comments'] })
151+
152+
return {
153+
comment,
154+
commentable,
155+
}
156+
}
157+
158+
type PublishCommenstOnReportOptions = {
159+
report?: Report
160+
currentUser: User
161+
}
162+
163+
/**
164+
* Abstract method to publish all comments on a report
165+
* @param options Data for publishing comments
166+
* @returns GQL publish comments payload
167+
*/
168+
export const publishCommentsOnReport = async ({
169+
report,
170+
currentUser,
171+
}: PublishCommenstOnReportOptions): Promise<GqlResolversTypes['PublishCommentsPayload']> => {
172+
const t = createT(currentUser.language)
173+
174+
if (!report) {
175+
throw new GraphQLError(t('errors.missingReport'))
176+
}
177+
178+
report.comments.forEach((com) => (com.published = true))
179+
report.days.forEach((day) => {
180+
day.comments.forEach((com) => (com.published = true))
181+
day.entries.forEach((entry) => {
182+
entry.comments.forEach((com) => (com.published = true))
183+
})
184+
})
185+
186+
await updateReport(report, { updateKeys: ['days', 'comments'] })
187+
188+
return {
189+
report,
190+
}
191+
}
192+
65193
export const commentableReports = async (traineeId: string): Promise<Report[]> => {
66194
const trainee = await traineeById(traineeId)
67195

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@rebass/grid": "^6.1.0",
1414
"react": "^19.1.1",
1515
"react-dom": "^19.1.1",
16+
"react-localization": "^2.0.6",
1617
"styled-components": "^6.1.19",
1718
"styled-modern-normalize": "^0.2.0"
1819
},

0 commit comments

Comments
 (0)