Skip to content

Commit 5c0cb36

Browse files
authored
Merge pull request #1327 from topcoder-platform/pm-2177_comments
feat(PM-2177) Added reply button and ability to add comments
2 parents f9c98ad + 5e6dc93 commit 5c0cb36

File tree

14 files changed

+339
-35
lines changed

14 files changed

+339
-35
lines changed
Lines changed: 3 additions & 0 deletions
Loading

src/apps/review/src/lib/assets/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ReactComponent as IconChevronDown } from './selector.svg'
44
import { ReactComponent as IconError } from './icon-error.svg'
55
import { ReactComponent as IconAiReview } from './icon-ai-review.svg'
66
import { ReactComponent as IconSubmission } from './icon-phase-submission.svg'
7+
import { ReactComponent as IconReply } from './icon-reply.svg'
78
import { ReactComponent as IconRegistration } from './icon-phase-registration.svg'
89
import { ReactComponent as IconPhaseReview } from './icon-phase-review.svg'
910
import { ReactComponent as IconAppeal } from './icon-phase-appeal.svg'
@@ -47,6 +48,7 @@ export {
4748
IconAppeal,
4849
IconAppealResponse,
4950
IconPhaseWinners,
51+
IconReply,
5052
IconDeepseekAi,
5153
IconClock,
5254
IconPremium,

src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.module.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ $error-line-height: 14px;
157157
}
158158
}
159159

160+
.remainingCharacters {
161+
font-family: "Nunito Sans", sans-serif;
162+
color: var(--GrayFontColor);
163+
font-size: 14px;
164+
line-height: 20px;
165+
}
166+
160167
.error {
161168
display: flex;
162169
align-items: center;

src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* Field Markdown Editor.
33
*/
4-
import { FC, useCallback, useContext, useEffect, useRef } from 'react'
4+
import { FC, useCallback, useContext, useEffect, useRef, useState } from 'react'
55
import _ from 'lodash'
6-
import CodeMirror from 'codemirror'
6+
import CodeMirror, { EditorChangeCancellable } from 'codemirror'
77
import EasyMDE from 'easymde'
88
import classNames from 'classnames'
99
import 'easymde/dist/easymde.min.css'
@@ -44,6 +44,7 @@ interface Props {
4444
showBorder?: boolean
4545
disabled?: boolean
4646
uploadCategory?: string
47+
maxCharactersAllowed?: number
4748
}
4849
const errorMessages = {
4950
fileTooLarge:
@@ -149,6 +150,9 @@ type CodeMirrorType = keyof typeof stateStrategy | 'variable-2'
149150
export const FieldMarkdownEditor: FC<Props> = (props: Props) => {
150151
const elementRef = useRef<HTMLTextAreaElement>(null)
151152
const easyMDE = useRef<any>(null)
153+
const [remainingCharacters, setRemainingCharacters] = useState(
154+
(props.maxCharactersAllowed || 0) - (props.initialValue?.length || 0),
155+
)
152156
const { challengeId }: ChallengeDetailContextModel = useContext(ChallengeDetailContext)
153157
const uploadCategory: string = props.uploadCategory ?? 'general'
154158

@@ -825,8 +829,30 @@ export const FieldMarkdownEditor: FC<Props> = (props: Props) => {
825829
uploadImage: true,
826830
})
827831

832+
easyMDE.current.codemirror.on('beforeChange', (cm: CodeMirror.Editor, change: EditorChangeCancellable) => {
833+
if (change.update) {
834+
const current = cm.getValue().length
835+
const incoming = change.text.join('\n').length
836+
const replaced = cm.indexFromPos(change.to) - cm.indexFromPos(change.from)
837+
838+
const newLength = current + incoming - replaced
839+
840+
if (props.maxCharactersAllowed) {
841+
if (newLength > props.maxCharactersAllowed) {
842+
change.cancel()
843+
}
844+
}
845+
}
846+
})
847+
828848
easyMDE.current.codemirror.on('change', (cm: CodeMirror.Editor) => {
829-
props.onChange?.(cm.getValue())
849+
if (props.maxCharactersAllowed) {
850+
const remaining = (props.maxCharactersAllowed || 0) - cm.getValue().length
851+
setRemainingCharacters(remaining)
852+
props.onChange?.(cm.getValue())
853+
} else {
854+
props.onChange?.(cm.getValue())
855+
}
830856
})
831857

832858
easyMDE.current.codemirror.on('blur', () => {
@@ -856,7 +882,13 @@ export const FieldMarkdownEditor: FC<Props> = (props: Props) => {
856882
})}
857883
>
858884
<textarea ref={elementRef} placeholder={props.placeholder} />
859-
885+
{props.maxCharactersAllowed && (
886+
<div className={styles.remainingCharacters}>
887+
{remainingCharacters}
888+
{' '}
889+
characters remaining
890+
</div>
891+
)}
860892
{props.error && (
861893
<div className={classNames(styles.error, 'errorMessage')}>
862894
{props.error}

src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import { FC, useMemo } from 'react'
1+
import { FC, useCallback, useMemo, useState } from 'react'
2+
import { mutate } from 'swr'
23

34
import { IconAiReview } from '~/apps/review/src/lib/assets/icons'
4-
import { ScorecardQuestion } from '~/apps/review/src/lib/models'
5+
import { ReviewsContextModel, ScorecardQuestion } from '~/apps/review/src/lib/models'
6+
import { createFeedbackComment } from '~/apps/review/src/lib/services'
7+
import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
8+
import { EnvironmentConfig } from '~/config'
59

610
import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../ScorecardViewer.context'
711
import { ScorecardQuestionRow } from '../ScorecardQuestionRow'
812
import { ScorecardScore } from '../../ScorecardScore'
913
import { MarkdownReview } from '../../../../MarkdownReview'
1014
import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
1115
import { AiFeedbackComments } from '../AiFeedbackComments/AiFeedbackComments'
16+
import { AiFeedbackReply } from '../AiFeedbackReply/AiFeedbackReply'
1217

1318
import styles from './AiFeedback.module.scss'
1419

@@ -21,9 +26,23 @@ const AiFeedback: FC<AiFeedbackProps> = props => {
2126
const feedback: any = useMemo(() => (
2227
aiFeedbackItems?.find((r: any) => r.scorecardQuestionId === props.question.id)
2328
), [props.question.id, aiFeedbackItems])
29+
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()
30+
const [showReply, setShowReply] = useState(false)
2431

2532
const commentsArr: any[] = (feedback?.comments) || []
2633

34+
const onShowReply = useCallback(() => {
35+
setShowReply(!showReply)
36+
}, [])
37+
38+
const onSubmitReply = useCallback(async (content: string) => {
39+
await createFeedbackComment(workflowId as string, workflowRun?.id as string, feedback?.id, {
40+
content,
41+
})
42+
await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun?.id}/items`)
43+
setShowReply(false)
44+
}, [workflowId, workflowRun?.id, feedback?.id])
45+
2746
if (!aiFeedbackItems?.length || !feedback) {
2847
return <></>
2948
}
@@ -50,10 +69,21 @@ const AiFeedback: FC<AiFeedbackProps> = props => {
5069

5170
<MarkdownReview value={feedback.content} />
5271

53-
<AiFeedbackActions feedback={feedback} actionType='runItem' />
72+
<AiFeedbackActions feedback={feedback} actionType='runItem' onPressReply={onShowReply} />
73+
74+
{
75+
showReply && (
76+
<AiFeedbackReply
77+
onSubmitReply={onSubmitReply}
78+
onCloseReply={function closeReply() {
79+
setShowReply(false)
80+
}}
81+
/>
82+
)
83+
}
5484

5585
{commentsArr.length > 0 && (
56-
<AiFeedbackComments comments={commentsArr} feedback={feedback} />
86+
<AiFeedbackComments comments={commentsArr} feedback={feedback} isRoot />
5787
)}
5888
</ScorecardQuestionRow>
5989
)

src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FC, useCallback, useContext, useEffect, useState } from 'react'
33
import { mutate } from 'swr'
44

55
import {
6+
IconReply,
67
IconThumbsDown,
78
IconThumbsDownFilled,
89
IconThumbsUp,
@@ -27,6 +28,7 @@ interface AiFeedbackActionsProps {
2728
actionType: 'comment' | 'runItem'
2829
comment?: AiFeedbackComment
2930
feedback?: any
31+
onPressReply: () => void
3032
}
3133

3234
export const AiFeedbackActions: FC<AiFeedbackActionsProps> = props => {
@@ -222,6 +224,15 @@ export const AiFeedbackActions: FC<AiFeedbackActionsProps> = props => {
222224
{userVote === 'DOWNVOTE' ? <IconThumbsDownFilled /> : <IconThumbsDown />}
223225
<span className={styles.count}>{downVotes}</span>
224226
</button>
227+
228+
<button
229+
type='button'
230+
className={styles.actionBtn}
231+
onClick={props.onPressReply}
232+
>
233+
<IconReply />
234+
<span className={styles.count}>Reply</span>
235+
</button>
225236
</div>
226237
)
227238
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { FC, useCallback, useState } from 'react'
2+
import { mutate } from 'swr'
3+
import classNames from 'classnames'
4+
import moment from 'moment'
5+
6+
import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext'
7+
import { createFeedbackComment } from '~/apps/review/src/lib/services'
8+
import { AiFeedbackItem, ReviewsContextModel } from '~/apps/review/src/lib/models'
9+
import { EnvironmentConfig } from '~/config'
10+
11+
import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
12+
import { AiFeedbackReply } from '../AiFeedbackReply/AiFeedbackReply'
13+
import { MarkdownReview } from '../../../../MarkdownReview'
14+
15+
import { AiFeedbackComment as AiFeedbackCommentType, AiFeedbackComments } from './AiFeedbackComments'
16+
import styles from './AiFeedbackComments.module.scss'
17+
18+
interface AiFeedbackCommentProps {
19+
comment: AiFeedbackCommentType
20+
feedback: AiFeedbackItem
21+
isRoot: boolean
22+
}
23+
24+
export const AiFeedbackComment: FC<AiFeedbackCommentProps> = props => {
25+
const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext()
26+
const [showReply, setShowReply] = useState(false)
27+
28+
const onShowReply = useCallback(() => {
29+
setShowReply(!showReply)
30+
}, [])
31+
32+
const onSubmitReply = useCallback(async (content: string, comment: AiFeedbackCommentType) => {
33+
await createFeedbackComment(workflowId as string, workflowRun?.id as string, props.feedback?.id, {
34+
content,
35+
parentId: comment.id,
36+
})
37+
await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun?.id}/items`)
38+
setShowReply(false)
39+
}, [workflowId, workflowRun?.id, props.feedback?.id])
40+
return (
41+
<div
42+
key={props.comment.id}
43+
className={classNames(styles.comment, {
44+
[styles.noMarginTop]: !props.isRoot,
45+
})}
46+
>
47+
<div className={styles.info}>
48+
<span className={styles.reply}>Reply</span>
49+
<span className={styles.text}> by </span>
50+
<span
51+
style={{
52+
color: props.comment.createdUser.ratingColor || '#0A0A0A',
53+
}}
54+
className={styles.name}
55+
>
56+
{props.comment.createdUser.handle}
57+
</span>
58+
<span className={styles.text}> on </span>
59+
<span className={styles.date}>
60+
{ moment(props.comment.createdAt)
61+
.local()
62+
.format('MMM DD, hh:mm A')}
63+
</span>
64+
</div>
65+
<MarkdownReview value={props.comment.content} />
66+
<AiFeedbackActions
67+
feedback={props.feedback}
68+
comment={props.comment}
69+
actionType='comment'
70+
onPressReply={onShowReply}
71+
/>
72+
{
73+
showReply && (
74+
<AiFeedbackReply
75+
onSubmitReply={function submitReply(content: string) {
76+
return onSubmitReply(content, props.comment)
77+
}}
78+
onCloseReply={function closeReply() {
79+
setShowReply(false)
80+
}}
81+
/>
82+
)
83+
}
84+
<AiFeedbackComments comments={props.comment.comments} feedback={props.feedback} isRoot={false} />
85+
</div>
86+
)
87+
}

src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.module.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
margin-top: 32px;
77
background-color: #E0E4E8;
88
padding: 16px;
9+
&.noMarginTop {
10+
margin-top: 0;
11+
padding: 0;
12+
padding-left: 16px;
13+
}
914
.info {
1015
margin: 16px 0;
1116
font-size: 14px;
Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { FC } from 'react'
2-
import moment from 'moment'
3-
4-
import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions'
2+
import classNames from 'classnames'
53

4+
import { AiFeedbackComment } from './AiFeedbackComment'
65
import styles from './AiFeedbackComments.module.scss'
76

87
export interface AiFeedbackVote {
@@ -12,6 +11,7 @@ export interface AiFeedbackVote {
1211
createdAt: string
1312
createdBy: string
1413
}
14+
1515
export interface AiFeedbackComment {
1616
id: string
1717
content: string
@@ -23,40 +23,21 @@ export interface AiFeedbackComment {
2323
handle: string
2424
ratingColor: string
2525
}
26+
comments: AiFeedbackComment[]
2627
votes: AiFeedbackVote[]
2728
}
2829

2930
interface AiFeedbackCommentsProps {
3031
comments: AiFeedbackComment[]
3132
feedback: any
33+
isRoot: boolean
3234
}
3335

3436
export const AiFeedbackComments: FC<AiFeedbackCommentsProps> = props => (
35-
<div className={styles.comments}>
36-
{props.comments.filter(c => !c.parentId)
37+
<div className={classNames(styles.comments)}>
38+
{props.comments
3739
.map((comment: AiFeedbackComment) => (
38-
<div key={comment.id} className={styles.comment}>
39-
<div className={styles.info}>
40-
<span className={styles.reply}>Reply</span>
41-
<span className={styles.text}> by </span>
42-
<span
43-
style={{
44-
color: comment.createdUser.ratingColor || '#0A0A0A',
45-
}}
46-
className={styles.name}
47-
>
48-
{comment.createdUser.handle}
49-
</span>
50-
<span className={styles.text}> on </span>
51-
<span className={styles.date}>
52-
{ moment(comment.createdAt)
53-
.local()
54-
.format('MMM DD, hh:mm A')}
55-
</span>
56-
</div>
57-
<div className={styles.commentContent}>{comment.content}</div>
58-
<AiFeedbackActions feedback={props.feedback} comment={comment} actionType='comment' />
59-
</div>
40+
<AiFeedbackComment isRoot={props.isRoot} comment={comment} feedback={props.feedback} />
6041
))}
6142
</div>
6243
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.replyWrapper {
4+
background-color: #E0E4E8;
5+
padding: 16px;
6+
margin-top: 16px;
7+
.title {
8+
font-family: "Nunito Sans", sans-serif;
9+
font-weight: 700;
10+
color: #0A0A0A;
11+
margin-bottom: 16px;
12+
}
13+
.blockBtns {
14+
margin-top: 24px;
15+
.cancelButton {
16+
margin-left: 16px;
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)