Skip to content

Commit 11b5e3c

Browse files
committed
feat(search): add segment search functionality to MarkdownLinkDialog
- Introduced a new API function `searchSegments` to fetch segments based on user input. - Updated `MarkdownLinkDialog` to support segment linking, including state management for selected segments. - Enhanced the UI to display search results for segments, improving user experience when linking content. - Adjusted placeholder text and rendering logic to accommodate the new segment search feature.
1 parent c1f2f03 commit 11b5e3c

2 files changed

Lines changed: 128 additions & 4 deletions

File tree

src/components/api/searchApi.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,29 @@ export const fetchTextDetails = async ({
6666
return data;
6767
};
6868

69+
type SearchSegments = {
70+
content: string;
71+
};
72+
73+
export type SegmentSearchResult = {
74+
id: string;
75+
pecha_segment_id?: string;
76+
text_id: string;
77+
content: string;
78+
type: string;
79+
};
80+
81+
export type SegmentSearchResponse = {
82+
segments: SegmentSearchResult[];
83+
};
84+
85+
export const searchSegments = async ({
86+
content,
87+
}: SearchSegments): Promise<SegmentSearchResponse> => {
88+
const { data } = await axiosInstance.post(`/api/v1/segments/search`, {
89+
content,
90+
});
91+
return data;
92+
};
93+
6994
export type { SearchCommon };

src/components/ui/molecules/markdown-editor/MarkdownLinkDialog.tsx

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import {
2020
} from "@/components/routes/groups/api/groupsApi";
2121
import {
2222
fetchTextDetails,
23+
searchSegments,
2324
searchTitles,
25+
type SegmentSearchResult,
2426
} from "@/components/api/searchApi";
2527
import { buildGroupLink, buildSegmentLink } from "@/lib/markdownLinks";
2628
import { flattenSegments, getLastSegmentId } from "@/lib/utils";
2729
import pechaIcon from "@/assets/icon/pecha_icon.png";
2830

29-
type LinkType = "group" | "text";
31+
type LinkType = "group" | "text" | "segment";
3032

3133
export type MarkdownLinkSelection = {
3234
label: string;
@@ -71,6 +73,8 @@ const MarkdownLinkDialog = ({
7173
const [selectedSegment, setSelectedSegment] = useState<SegmentItem | null>(
7274
null,
7375
);
76+
const [selectedSearchSegment, setSelectedSearchSegment] =
77+
useState<SegmentSearchResult | null>(null);
7478

7579
useEffect(() => {
7680
if (!open) {
@@ -79,13 +83,15 @@ const MarkdownLinkDialog = ({
7983
setSelectedGroup(null);
8084
setSelectedTextItem(null);
8185
setSelectedSegment(null);
86+
setSelectedSearchSegment(null);
8287
}
8388
}, [open]);
8489

8590
useEffect(() => {
8691
setSelectedGroup(null);
8792
setSelectedTextItem(null);
8893
setSelectedSegment(null);
94+
setSelectedSearchSegment(null);
8995
}, [linkType, debouncedQuery]);
9096

9197
const { data: groupData, isLoading: isLoadingGroups } = useQuery({
@@ -112,7 +118,16 @@ const MarkdownLinkDialog = ({
112118
enabled: open && linkType === "text" && debouncedQuery.length > 0,
113119
});
114120

121+
const { data: segmentSearchData, isLoading: isLoadingSegmentSearch } =
122+
useQuery({
123+
queryKey: ["markdown-link-segment-search", debouncedQuery],
124+
queryFn: () => searchSegments({ content: debouncedQuery }),
125+
enabled: open && linkType === "segment" && debouncedQuery.length > 0,
126+
});
127+
115128
const textTitles: TextTitleItem[] = titleData ?? [];
129+
const searchedSegments: SegmentSearchResult[] =
130+
segmentSearchData?.segments ?? [];
116131

117132
const {
118133
data: detailsData,
@@ -171,7 +186,9 @@ const MarkdownLinkDialog = ({
171186
const canConfirm =
172187
linkType === "group"
173188
? !!selectedGroup
174-
: !!selectedTextItem && !!selectedSegment;
189+
: linkType === "segment"
190+
? !!selectedSearchSegment
191+
: !!selectedTextItem && !!selectedSegment;
175192

176193
const handleConfirm = () => {
177194
if (linkType === "group" && selectedGroup) {
@@ -194,6 +211,17 @@ const MarkdownLinkDialog = ({
194211
url: buildSegmentLink(segmentId),
195212
});
196213
onOpenChange(false);
214+
return;
215+
}
216+
217+
if (linkType === "segment" && selectedSearchSegment) {
218+
const segmentId =
219+
selectedSearchSegment.pecha_segment_id || selectedSearchSegment.id;
220+
onConfirm({
221+
label: selectedText.trim() || selectedSearchSegment.content,
222+
url: buildSegmentLink(segmentId),
223+
});
224+
onOpenChange(false);
197225
}
198226
};
199227

@@ -288,6 +316,58 @@ const MarkdownLinkDialog = ({
288316
);
289317
};
290318

319+
const renderDirectSegmentResults = () => {
320+
if (!debouncedQuery) {
321+
return (
322+
<p className="px-3 py-4 text-sm text-muted-foreground">
323+
Search for segment content to link directly.
324+
</p>
325+
);
326+
}
327+
328+
if (isLoadingSegmentSearch) {
329+
return (
330+
<p className="px-3 py-4 text-sm text-muted-foreground">
331+
Searching segments…
332+
</p>
333+
);
334+
}
335+
336+
if (searchedSegments.length === 0) {
337+
return (
338+
<p className="px-3 py-4 text-sm text-muted-foreground">
339+
No segments found.
340+
</p>
341+
);
342+
}
343+
344+
return (
345+
<ul>
346+
{searchedSegments.map((segment) => {
347+
const isSelected = selectedSearchSegment?.id === segment.id;
348+
return (
349+
<li key={segment.id}>
350+
<button
351+
type="button"
352+
className={`w-full rounded-md border p-2 text-left text-sm hover:bg-muted/40 ${
353+
isSelected
354+
? "border-[#801A1E] bg-muted/40"
355+
: "border-dashed border-gray-300 dark:border-[#313132]"
356+
}`}
357+
onClick={() => setSelectedSearchSegment(segment)}
358+
>
359+
<div
360+
className="line-clamp-3"
361+
dangerouslySetInnerHTML={{ __html: segment.content }}
362+
/>
363+
</button>
364+
</li>
365+
);
366+
})}
367+
</ul>
368+
);
369+
};
370+
291371
const renderSegmentResults = () => {
292372
if (!selectedTextItem) return null;
293373

@@ -370,13 +450,28 @@ const MarkdownLinkDialog = ({
370450
>
371451
Text
372452
</button>
453+
<button
454+
type="button"
455+
className={`flex-1 rounded px-3 py-1.5 text-sm font-medium transition-colors ${
456+
linkType === "segment"
457+
? "bg-muted text-foreground"
458+
: "text-muted-foreground hover:text-foreground"
459+
}`}
460+
onClick={() => setLinkType("segment")}
461+
>
462+
Segment
463+
</button>
373464
</div>
374465

375466
<div className="flex items-center rounded-md border border-input px-2">
376467
<IoMdSearch className="size-4 shrink-0 text-muted-foreground" />
377468
<Pecha.Input
378469
placeholder={
379-
linkType === "group" ? "Search groups…" : "Search text titles…"
470+
linkType === "group"
471+
? "Search groups…"
472+
: linkType === "text"
473+
? "Search text titles…"
474+
: "Search segment content…"
380475
}
381476
className="border-0 shadow-none focus-visible:ring-0"
382477
value={searchQuery}
@@ -385,7 +480,11 @@ const MarkdownLinkDialog = ({
385480
</div>
386481

387482
<div className="max-h-52 overflow-y-auto rounded-md border border-input">
388-
{linkType === "group" ? renderGroupResults() : renderTextResults()}
483+
{linkType === "group"
484+
? renderGroupResults()
485+
: linkType === "text"
486+
? renderTextResults()
487+
: renderDirectSegmentResults()}
389488
</div>
390489

391490
{linkType === "text" && renderSegmentResults()}

0 commit comments

Comments
 (0)