Skip to content

Commit ace3a1a

Browse files
fix: render actual content instead of HTML when highlighting search terms in stories or tasks (#117)
1 parent 3e02d30 commit ace3a1a

File tree

4 files changed

+114
-42
lines changed

4 files changed

+114
-42
lines changed

webview-ui/src/components/hai/DetailedView.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
55
import { addHighlighting } from "../../utils/add-highlighting"
66
import CopyClipboard from "../common/CopyClipboard"
77

8+
// Define interface to store both original and highlighted versions
9+
interface IHighlightedHaiTask {
10+
original: IHaiTask
11+
highlighted: IHaiTask
12+
}
813
interface DetailedViewProps {
914
task: IHaiTask | null
1015
story: IHaiStory | null
@@ -34,17 +39,34 @@ const DetailedView: React.FC<DetailedViewProps> = ({ task, story, onTaskSelect,
3439
}, [story])
3540

3641
// Filter tasks based on the search query and apply highlighting
37-
const filteredTasks = useMemo(() => {
38-
if (!searchQuery.trim() || !fuse) return story?.tasks || []
42+
const filteredTasks = useMemo<IHighlightedHaiTask[]>(() => {
43+
if (!searchQuery.trim() || !fuse) {
44+
// If no search query, return all tasks with both original and highlighted pointing to the same object
45+
return (story?.tasks || []).map((task) => ({
46+
original: task,
47+
highlighted: task,
48+
}))
49+
}
50+
51+
// With search query, apply highlighting to copies while preserving originals
3952
const results = fuse.search(searchQuery)
4053
return results.map(({ item, matches }) => {
54+
// Store original task
55+
const originalTask = item
56+
// Create copy for highlighting
4157
const highlightedTask = { ...item }
58+
59+
// Apply highlighting to the copy
4260
matches?.forEach((match) => {
4361
if (match.key && match.indices && isTaskField(match.key)) {
4462
highlightedTask[match.key] = addHighlighting(String(match.value), match.indices)
4563
}
4664
})
47-
return highlightedTask
65+
66+
return {
67+
original: originalTask,
68+
highlighted: highlightedTask,
69+
}
4870
})
4971
}, [searchQuery, fuse, story])
5072

@@ -126,7 +148,7 @@ const DetailedView: React.FC<DetailedViewProps> = ({ task, story, onTaskSelect,
126148
<div>
127149
{filteredTasks.map((task) => (
128150
<div
129-
key={task.id}
151+
key={task.original.id}
130152
style={{
131153
display: "flex",
132154
alignItems: "center",
@@ -162,8 +184,8 @@ const DetailedView: React.FC<DetailedViewProps> = ({ task, story, onTaskSelect,
162184
fontWeight: "bold",
163185
color: "var(--vscode-descriptionForeground)",
164186
}}>
165-
<span dangerouslySetInnerHTML={{ __html: task.id }} />
166-
{task.subTaskTicketId && (
187+
<span dangerouslySetInnerHTML={{ __html: task.highlighted.id }} />
188+
{task.highlighted.subTaskTicketId && (
167189
<span
168190
style={{
169191
fontSize: "12px",
@@ -173,7 +195,7 @@ const DetailedView: React.FC<DetailedViewProps> = ({ task, story, onTaskSelect,
173195
textOverflow: "ellipsis",
174196
}}
175197
dangerouslySetInnerHTML={{
176-
__html: ` • ${task.subTaskTicketId}`,
198+
__html: ` • ${task.highlighted.subTaskTicketId}`,
177199
}}
178200
/>
179201
)}{" "}
@@ -188,7 +210,7 @@ const DetailedView: React.FC<DetailedViewProps> = ({ task, story, onTaskSelect,
188210
wordBreak: "break-word",
189211
overflowWrap: "anywhere",
190212
}}
191-
dangerouslySetInnerHTML={{ __html: task.list }}
213+
dangerouslySetInnerHTML={{ __html: task.highlighted.list }}
192214
/>
193215
</div>
194216

@@ -205,24 +227,24 @@ const DetailedView: React.FC<DetailedViewProps> = ({ task, story, onTaskSelect,
205227
onClick={() => {
206228
onTaskSelect({
207229
context: `${story?.name}: ${story?.description}`,
208-
...task,
209-
id: `${story?.id}-${task.id}`,
230+
...task.original,
231+
id: `${story?.id}-${task.original.id}`,
210232
})
211233
}}>
212234
<span className="codicon codicon-play" style={{ fontSize: 14, cursor: "pointer" }} />
213235
</VSCodeButton>
214236
<CopyClipboard
215237
title="Copy Task"
216238
onCopyContent={() => {
217-
return `Task (${task.id}): ${task.list}\nAcceptance: ${task.acceptance}\n\nContext:\nStory (${story?.id}): ${story?.name}\nStory Acceptance: ${story?.description}\n`
239+
return `Task (${task.original.id}): ${task.original.list}\nAcceptance: ${task.original.acceptance}\n\nContext:\nStory (${story?.id}): ${story?.name}\nStory Acceptance: ${story?.description}\n`
218240
}}
219241
/>
220242
<VSCodeButton
221243
appearance="icon"
222244
title="View Task"
223245
onClick={() => {
224-
setSelectedTask(task)
225-
onTaskClick(task)
246+
setSelectedTask(task.original)
247+
onTaskClick(task.original)
226248
}}>
227249
<span className="codicon codicon-eye" style={{ fontSize: 14, cursor: "pointer" }} />
228250
</VSCodeButton>

webview-ui/src/components/hai/HaiStoryAccordion.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ import { IHaiClineTask, IHaiTask, IHaiStory } from "../../interfaces/hai-task.in
33
import HaiTaskComponent from "./HaiTaskComponent"
44
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
55

6+
// Interface for the highlighted task structure
7+
interface IHighlightedHaiTask {
8+
original: IHaiTask
9+
highlighted: IHaiTask
10+
}
11+
612
interface HaiStoryAccordionProps {
713
onTaskClick: (task: IHaiTask) => void
814
onStoryClick: (story: IHaiStory) => void
915
name: string
1016
description: string
11-
tasks: IHaiTask[]
17+
tasks: IHighlightedHaiTask[]
1218
onTaskSelect: (task: IHaiClineTask) => void
1319
id: string
1420
prdId: string
1521
storyTicketId?: string
1622
isAllExpanded: boolean
23+
originalStory: IHaiStory
1724
}
1825

1926
export const HaiStoryAccordion: React.FC<HaiStoryAccordionProps> = ({
@@ -27,6 +34,7 @@ export const HaiStoryAccordion: React.FC<HaiStoryAccordionProps> = ({
2734
id,
2835
prdId,
2936
isAllExpanded,
37+
originalStory,
3038
}) => {
3139
const [isExpanded, setIsExpanded] = useState<boolean>(true)
3240

@@ -135,10 +143,7 @@ export const HaiStoryAccordion: React.FC<HaiStoryAccordionProps> = ({
135143
/>
136144
</div>
137145
</div>
138-
<VSCodeButton
139-
appearance="icon"
140-
title="View Story"
141-
onClick={() => onStoryClick({ id, prdId, name, description, storyTicketId, tasks })}>
146+
<VSCodeButton appearance="icon" title="View Story" onClick={() => onStoryClick(originalStory)}>
142147
<span
143148
className="codicon codicon-eye"
144149
style={{
@@ -164,7 +169,7 @@ export const HaiStoryAccordion: React.FC<HaiStoryAccordionProps> = ({
164169
boxSizing: "border-box",
165170
}}>
166171
{tasks.map((task) => (
167-
<div key={task.id}>
172+
<div key={task.original.id}>
168173
<div>
169174
<HaiTaskComponent
170175
id={id}

webview-ui/src/components/hai/HaiTaskComponent.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import { IHaiClineTask, IHaiTask } from "../../interfaces/hai-task.interface"
33
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
44
import CopyClipboard from "../common/CopyClipboard"
55

6+
// Interface for the highlighted task structure
7+
interface IHighlightedHaiTask {
8+
original: IHaiTask
9+
highlighted: IHaiTask
10+
}
11+
612
interface HaiTaskComponentProps {
713
id: string
814
prdId: string
915
name: string
1016
description: string
11-
task: IHaiTask
17+
task: IHighlightedHaiTask
1218
onTaskClick: (task: IHaiTask) => void
1319
onTaskSelect: (task: IHaiClineTask) => void
1420
}
@@ -52,8 +58,8 @@ const HaiTaskComponent: React.FC<HaiTaskComponentProps> = ({ id, prdId, name, de
5258
fontWeight: "bold",
5359
color: "var(--vscode-descriptionForeground)",
5460
}}>
55-
<span dangerouslySetInnerHTML={{ __html: task.id }} />
56-
{task.subTaskTicketId && (
61+
<span dangerouslySetInnerHTML={{ __html: task.highlighted.id }} />
62+
{task.highlighted.subTaskTicketId && (
5763
<span
5864
style={{
5965
fontSize: "12px",
@@ -62,12 +68,12 @@ const HaiTaskComponent: React.FC<HaiTaskComponentProps> = ({ id, prdId, name, de
6268
textOverflow: "ellipsis",
6369
}}
6470
dangerouslySetInnerHTML={{
65-
__html: ` • ${task.subTaskTicketId}`,
71+
__html: ` • ${task.highlighted.subTaskTicketId}`,
6672
}}
6773
/>
6874
)}{" "}
6975
</span>
70-
{task.status === "Completed" && (
76+
{task.original.status === "Completed" && (
7177
<span
7278
className={`codicon codicon-pass-filled`}
7379
style={{ marginLeft: "4px", color: "green", fontSize: "13px" }}
@@ -83,7 +89,7 @@ const HaiTaskComponent: React.FC<HaiTaskComponentProps> = ({ id, prdId, name, de
8389
wordBreak: "break-word",
8490
overflowWrap: "anywhere",
8591
}}
86-
dangerouslySetInnerHTML={{ __html: task.list }}
92+
dangerouslySetInnerHTML={{ __html: task.highlighted.list }}
8793
/>
8894
</div>
8995

@@ -99,19 +105,19 @@ const HaiTaskComponent: React.FC<HaiTaskComponentProps> = ({ id, prdId, name, de
99105
onClick={() => {
100106
onTaskSelect({
101107
context: `${name}: ${description}`,
102-
...task,
103-
id: `${prdId}-${id}-${task.id}`,
108+
...task.original,
109+
id: `${prdId}-${id}-${task.original.id}`,
104110
})
105111
}}>
106112
<span className="codicon codicon-play" style={{ fontSize: 14, cursor: "pointer" }} />
107113
</VSCodeButton>
108114
<CopyClipboard
109115
title="Copy Task"
110116
onCopyContent={() => {
111-
return `Task (${task.id}): ${task.list}\nAcceptance: ${task.acceptance}\n\nContext:\nStory (${id}): ${name}\nStory Acceptance: ${description}\n`
117+
return `Task (${task.original.id}): ${task.original.list}\nAcceptance: ${task.original.acceptance}\n\nContext:\nStory (${id}): ${name}\nStory Acceptance: ${description}\n`
112118
}}
113119
/>
114-
<VSCodeButton appearance="icon" title="View Task" onClick={() => onTaskClick(task)}>
120+
<VSCodeButton appearance="icon" title="View Task" onClick={() => onTaskClick(task.original)}>
115121
<span className="codicon codicon-eye" style={{ fontSize: 14, cursor: "pointer" }} />
116122
</VSCodeButton>
117123
</div>

webview-ui/src/components/hai/hai-tasks-list.tsx

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import { v4 as uuidv4 } from "uuid"
66
import Fuse from "fuse.js"
77
import { addHighlighting } from "../../utils/add-highlighting"
88

9+
// Define interfaces to store both original and highlighted versions
10+
interface IHighlightedHaiTask {
11+
original: IHaiTask
12+
highlighted: IHaiTask
13+
}
14+
15+
interface IHighlightedHaiStory {
16+
original: IHaiStory
17+
highlighted: IHaiStory & { tasks: IHighlightedHaiTask[] }
18+
}
19+
920
type SearchableTaskFields = keyof IHaiTask
1021
const TASK_PREFIX = "tasks."
1122

@@ -47,12 +58,28 @@ export function HaiTasksList({
4758
}, [haiTaskList])
4859

4960
const taskSearchResults = useMemo(() => {
50-
if (!searchQuery.trim()) return haiTaskList
61+
if (!searchQuery.trim()) {
62+
// For no search query, return the original list with both original and highlighted
63+
// properties pointing to the same objects (no highlighting needed)
64+
return haiTaskList.map((story) => ({
65+
original: story,
66+
highlighted: {
67+
...story,
68+
tasks: story.tasks.map((task) => ({
69+
original: task,
70+
highlighted: task,
71+
})),
72+
},
73+
}))
74+
}
5175

5276
const searchResults = fuse.search(searchQuery)
5377

5478
return searchResults
5579
.map(({ item, matches }) => {
80+
// Store the original story
81+
const originalStory = item
82+
// Create a copy for highlighting
5683
const highlightedStory = { ...item }
5784
let hasStoryMatch = false
5885

@@ -69,9 +96,10 @@ export function HaiTasksList({
6996
})
7097

7198
// Process task-level matches
72-
const processedTasks = highlightedStory.tasks
99+
const processedTasks = originalStory.tasks
73100
.map((task) => {
74101
let hasTaskMatch = false
102+
// Create a copy for highlighting
75103
const highlightedTask = { ...task }
76104

77105
matches?.forEach((match) => {
@@ -84,19 +112,29 @@ export function HaiTasksList({
84112
}
85113
})
86114

87-
return hasTaskMatch || hasStoryMatch ? highlightedTask : null
115+
// Only keep tasks that match or are in a matching story
116+
return hasTaskMatch || hasStoryMatch
117+
? {
118+
original: task,
119+
highlighted: highlightedTask,
120+
}
121+
: null
88122
})
89-
.filter((task): task is IHaiTask => task !== null)
123+
.filter((task): task is IHighlightedHaiTask => task !== null)
90124

91125
if (processedTasks.length === 0) return null
92126
setIsAllExpanded(true)
93127

128+
// Return both the original and highlighted versions
94129
return {
95-
...highlightedStory,
96-
tasks: processedTasks,
130+
original: originalStory,
131+
highlighted: {
132+
...highlightedStory,
133+
tasks: processedTasks,
134+
},
97135
}
98136
})
99-
.filter((story): story is IHaiStory => story !== null)
137+
.filter((story): story is IHighlightedHaiStory => story !== null)
100138
}, [searchQuery, haiTaskList, fuse])
101139

102140
function isTaskField(key: string): key is SearchableTaskFields {
@@ -204,13 +242,14 @@ export function HaiTasksList({
204242
{taskSearchResults.length > 0 ? (
205243
taskSearchResults.map((story) => (
206244
<HaiStoryAccordion
207-
description={story.description}
208-
storyTicketId={story.storyTicketId}
245+
description={story.highlighted.description}
246+
storyTicketId={story.highlighted.storyTicketId}
209247
key={uuidv4()}
210-
name={story.name}
211-
tasks={story.tasks}
212-
id={story.id}
213-
prdId={story.prdId}
248+
name={story.highlighted.name}
249+
tasks={story.highlighted.tasks}
250+
id={story.highlighted.id}
251+
prdId={story.highlighted.prdId}
252+
originalStory={story.original}
214253
onTaskSelect={selectedHaiTask}
215254
onTaskClick={onTaskClick}
216255
onStoryClick={onStoryClick}

0 commit comments

Comments
 (0)