Skip to content

Commit 37ec3a7

Browse files
authored
Merge pull request #232 from OpenPecha/preset-text-pair-subtask
Preset text pair subtask
2 parents 11b5e3c + ae8830d commit 37ec3a7

11 files changed

Lines changed: 452 additions & 75 deletions

File tree

src/components/api/searchApi.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,19 @@ export const searchSegments = async ({
9191
return data;
9292
};
9393

94+
export const fetchTextLanguages = async (textId: string) => {
95+
const { data } = await axiosInstance.get(`/api/v1/texts/${textId}/languages`);
96+
return data;
97+
};
98+
99+
export const fetchLanguageVersions = async (
100+
textId: string,
101+
language: string,
102+
) => {
103+
const { data } = await axiosInstance.get(
104+
`/api/v1/texts/${textId}/languages/${language}/versions`,
105+
);
106+
return data;
107+
};
108+
94109
export type { SearchCommon };

src/components/routes/groups/GroupsTable.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ const GroupsTable = ({ groups, isLoading }: GroupsTableProps) => {
5454
size="sm"
5555
/>
5656
</Pecha.TableCell>
57-
<Pecha.TableCell>{groupTypeLabel(group.group_type)}</Pecha.TableCell>
57+
<Pecha.TableCell>
58+
{groupTypeLabel(group.group_type)}
59+
</Pecha.TableCell>
5860
<Pecha.TableCell className="text-muted-foreground font-mono text-sm">
5961
{group.slug}
6062
</Pecha.TableCell>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import axiosInstance from "@/config/axios-config";
2+
3+
const getAuthHeaders = () => ({
4+
Authorization: `Bearer ${sessionStorage.getItem("accessToken")}`,
5+
});
6+
7+
export interface PresetRequest {
8+
version_id: string;
9+
language: string;
10+
}
11+
12+
export interface PresetResponse {
13+
id: string;
14+
subtask_id: string;
15+
version_id: string;
16+
language: string;
17+
created_at: string;
18+
created_by: string;
19+
updated_at?: string;
20+
updated_by?: string;
21+
}
22+
23+
export const createOrUpdatePreset = async (
24+
subtaskId: string,
25+
preset: PresetRequest,
26+
): Promise<PresetResponse> => {
27+
const { data } = await axiosInstance.post(
28+
`/api/v1/cms/sub-tasks/${subtaskId}/preset`,
29+
preset,
30+
{
31+
headers: getAuthHeaders(),
32+
},
33+
);
34+
return data;
35+
};
36+
37+
export const getPreset = async (
38+
subtaskId: string,
39+
): Promise<PresetResponse | null> => {
40+
try {
41+
const { data } = await axiosInstance.get(
42+
`/api/v1/cms/sub-tasks/${subtaskId}/preset`,
43+
{
44+
headers: getAuthHeaders(),
45+
},
46+
);
47+
return data;
48+
} catch (error: any) {
49+
if (error.response?.status === 404) {
50+
return null;
51+
}
52+
throw error;
53+
}
54+
};
55+
56+
export const deletePreset = async (subtaskId: string): Promise<void> => {
57+
await axiosInstance.delete(`/api/v1/cms/sub-tasks/${subtaskId}/preset`, {
58+
headers: getAuthHeaders(),
59+
});
60+
};

src/components/routes/task/api/taskApi.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,9 @@ export const deleteSubTaskAudio = async (sub_task_id: string) => {
180180
};
181181

182182
export const deleteSubTaskTimestamp = async (sub_task_id: string) => {
183-
await axiosInstance.delete(
184-
`/api/v1/cms/sub-tasks/${sub_task_id}/timestamp`,
185-
{
186-
headers: getAuthHeaders(),
187-
},
188-
);
183+
await axiosInstance.delete(`/api/v1/cms/sub-tasks/${sub_task_id}/timestamp`, {
184+
headers: getAuthHeaders(),
185+
});
189186
};
190187

191188
export const generateDayAudio = async (

src/components/routes/task/components/view/TaskView.test.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,7 @@ describe("TaskView Component", () => {
170170
expect(screen.getByText("Segment: 1:00 – 2:30")).toBeInTheDocument();
171171
});
172172
expect(
173-
document.querySelector(
174-
'audio[src="https://example.com/day-audio.mp3"]',
175-
),
173+
document.querySelector('audio[src="https://example.com/day-audio.mp3"]'),
176174
).toBeInTheDocument();
177175
});
178176

@@ -197,9 +195,7 @@ describe("TaskView Component", () => {
197195
<TaskView onEditTask={mockOnEditTask} taskId="task-123" />,
198196
);
199197
await waitFor(() => {
200-
expect(
201-
screen.getByText("Timeline: 0:30 – 1:30"),
202-
).toBeInTheDocument();
198+
expect(screen.getByText("Timeline: 0:30 – 1:30")).toBeInTheDocument();
203199
});
204200
expect(document.querySelector("audio")).not.toBeInTheDocument();
205201
});
@@ -230,7 +226,9 @@ describe("TaskView Component", () => {
230226
/>,
231227
);
232228
await waitFor(() => {
233-
expect(screen.getByText("Task With Both Audio Sources")).toBeInTheDocument();
229+
expect(
230+
screen.getByText("Task With Both Audio Sources"),
231+
).toBeInTheDocument();
234232
});
235233
expect(
236234
container.querySelector(

src/components/routes/task/components/view/TaskView.tsx

Lines changed: 135 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { useQuery } from "@tanstack/react-query";
1+
import { useQuery, useQueryClient } from "@tanstack/react-query";
2+
import { getPreset } from "../../api/presetApi";
3+
import { fetchLanguageVersions } from "@/components/api/searchApi";
24
import { Pecha } from "@/components/ui/shadimport";
5+
import { useState } from "react";
6+
import { VersionSelectorModal } from "@/components/ui/molecules/version-selector/VersionSelectorModal";
37
import axiosInstance from "@/config/axios-config";
48
import {
59
ContentIcon,
@@ -56,6 +60,52 @@ const SubtaskContent = ({
5660
}
5761
};
5862

63+
const SourceReferenceWithVersion = ({ subtask }: { subtask: any }) => {
64+
const { data: preset } = useQuery({
65+
queryKey: ["preset", subtask.id],
66+
queryFn: () => getPreset(subtask.id),
67+
enabled: !!subtask.id,
68+
});
69+
70+
const { data: versionsData } = useQuery({
71+
queryKey: ["languageVersions", subtask.source_text_id, preset?.language],
72+
queryFn: () =>
73+
fetchLanguageVersions(subtask.source_text_id, preset!.language),
74+
enabled: !!preset && !!subtask.source_text_id && !!preset.language,
75+
});
76+
77+
const versionTitle = versionsData?.available_versions?.find(
78+
(v: any) => v.id === preset?.version_id,
79+
)?.title;
80+
81+
return (
82+
<div className="relative">
83+
<SourceReferenceContent content={subtask.content} />
84+
{preset && (
85+
<div className="relative mt-2 ml-4">
86+
{/* Connecting line */}
87+
<div className="absolute -top-2 left-0 w-px h-2 bg-blue-400 dark:bg-blue-500" />
88+
<div className="absolute top-0 left-0 w-3 h-px bg-blue-400 dark:bg-blue-500" />
89+
90+
{/* Version info box */}
91+
<div className="ml-4 flex items-center gap-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 rounded-md border border-blue-200 dark:border-blue-800">
92+
<span className="text-xs font-semibold text-blue-600 dark:text-blue-400 uppercase tracking-wide">
93+
{preset.language}
94+
</span>
95+
<span className="text-xs text-gray-500 dark:text-gray-400"></span>
96+
<span
97+
className="text-sm text-gray-700 dark:text-gray-300 truncate max-w-[300px]"
98+
title={versionTitle || preset.version_id}
99+
>
100+
{versionTitle || preset.version_id.substring(0, 8) + "..."}
101+
</span>
102+
</div>
103+
</div>
104+
)}
105+
</div>
106+
);
107+
};
108+
59109
const SubtaskCard = ({
60110
subtask,
61111
listeners,
@@ -67,47 +117,96 @@ const SubtaskCard = ({
67117
isEditable?: boolean;
68118
dayAudioUrl?: string | null;
69119
}) => {
120+
const [isModalOpen, setIsModalOpen] = useState(false);
121+
const queryClient = useQueryClient();
122+
123+
const { data: preset } = useQuery({
124+
queryKey: ["preset", subtask.id],
125+
queryFn: () => getPreset(subtask.id),
126+
enabled: !!subtask.id && subtask.content_type === "SOURCE_REFERENCE",
127+
});
128+
70129
return (
71-
<div
72-
className={`border rounded-xl bg-[#ffffff] dark:bg-[#161616] border-gray-300 dark:border-input p-2 space-y-2`}
73-
>
74-
<div className="flex items-center justify-between">
75-
<div className="flex items-center border w-fit bg-[#F7F7F7] dark:bg-sidebar-secondary px-2 py-1 text-sm rounded-md border-dashed gap-2">
76-
<ContentIcon type={subtask.content_type} /> {subtask.content_type}
130+
<>
131+
<div
132+
className={`border rounded-xl bg-[#ffffff] dark:bg-[#161616] border-gray-300 dark:border-input p-2 space-y-2`}
133+
>
134+
<div className="flex items-center justify-between">
135+
<div className="flex items-center gap-2">
136+
<div className="flex items-center border w-fit bg-[#F7F7F7] dark:bg-sidebar-secondary px-2 py-1 text-sm rounded-md border-dashed gap-2">
137+
<ContentIcon type={subtask.content_type} /> {subtask.content_type}
138+
</div>
139+
{subtask.content_type === "SOURCE_REFERENCE" &&
140+
!preset &&
141+
subtask.source_text_id && (
142+
<Pecha.Button
143+
variant="outline"
144+
size="sm"
145+
onClick={() => setIsModalOpen(true)}
146+
className="text-xs"
147+
>
148+
Add Version
149+
</Pecha.Button>
150+
)}
151+
</div>
152+
{listeners && isEditable && (
153+
<PiDotsSixVertical
154+
className="w-5 h-5 text-gray-400 dark:text-muted-foreground cursor-grab active:cursor-grabbing"
155+
{...listeners}
156+
/>
157+
)}
77158
</div>
78-
{listeners && isEditable && (
79-
<PiDotsSixVertical
80-
className="w-5 h-5 text-gray-400 dark:text-muted-foreground cursor-grab active:cursor-grabbing"
81-
{...listeners}
159+
{subtask.content_type === "SOURCE_REFERENCE" ? (
160+
<SourceReferenceWithVersion subtask={subtask} />
161+
) : (
162+
<SubtaskContent
163+
type={subtask.content_type}
164+
content={subtask.content}
82165
/>
83166
)}
167+
{subtask.audio_url ? (
168+
<div className="border-t border-dashed pt-2">
169+
<audio
170+
controls
171+
src={subtask.audio_url}
172+
className="w-full"
173+
preload="metadata"
174+
/>
175+
</div>
176+
) : (
177+
subtask.start_ms != null &&
178+
subtask.end_ms != null &&
179+
(dayAudioUrl ? (
180+
<AudioSegmentPlayer
181+
audioUrl={dayAudioUrl}
182+
startMs={subtask.start_ms}
183+
endMs={subtask.end_ms}
184+
/>
185+
) : (
186+
<p className="text-xs text-muted-foreground border-t border-dashed pt-2">
187+
Timeline: {formatMs(subtask.start_ms)}{" "}
188+
{formatMs(subtask.end_ms)}
189+
</p>
190+
))
191+
)}
84192
</div>
85-
<SubtaskContent type={subtask.content_type} content={subtask.content} />
86-
{subtask.audio_url ? (
87-
<div className="border-t border-dashed pt-2">
88-
<audio
89-
controls
90-
src={subtask.audio_url}
91-
className="w-full"
92-
preload="metadata"
93-
/>
94-
</div>
95-
) : (
96-
subtask.start_ms != null &&
97-
subtask.end_ms != null &&
98-
(dayAudioUrl ? (
99-
<AudioSegmentPlayer
100-
audioUrl={dayAudioUrl}
101-
startMs={subtask.start_ms}
102-
endMs={subtask.end_ms}
193+
194+
{subtask.content_type === "SOURCE_REFERENCE" &&
195+
subtask.source_text_id && (
196+
<VersionSelectorModal
197+
isOpen={isModalOpen}
198+
onOpenChange={setIsModalOpen}
199+
textId={subtask.source_text_id}
200+
subtaskId={subtask.id}
201+
onSuccess={() => {
202+
setIsModalOpen(false);
203+
queryClient.invalidateQueries({
204+
queryKey: ["preset", subtask.id],
205+
});
206+
}}
103207
/>
104-
) : (
105-
<p className="text-xs text-muted-foreground border-t border-dashed pt-2">
106-
Timeline: {formatMs(subtask.start_ms)}{formatMs(subtask.end_ms)}
107-
</p>
108-
))
109-
)}
110-
</div>
208+
)}
209+
</>
111210
);
112211
};
113212

src/components/ui/atoms/markdown-editor.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ const MarkdownEditor = ({
8686
const prefix = "## ";
8787
const insertText = `${prefix}${currentValue.slice(start, end)}`;
8888
const nextValue =
89-
currentValue.slice(0, lineStart) +
90-
insertText +
91-
currentValue.slice(end);
89+
currentValue.slice(0, lineStart) + insertText + currentValue.slice(end);
9290
const cursorStart = lineStart + prefix.length;
9391
const cursorEnd = cursorStart + (end - start);
9492
return { nextValue, cursorStart, cursorEnd };
@@ -124,14 +122,11 @@ const MarkdownEditor = ({
124122
setIsLinkDialogOpen(true);
125123
};
126124

127-
const handleInsertLink = ({
128-
label,
129-
url,
130-
}: {
131-
label: string;
132-
url: string;
133-
}) => {
134-
const selection = savedSelection.current ?? { start: value.length, end: value.length };
125+
const handleInsertLink = ({ label, url }: { label: string; url: string }) => {
126+
const selection = savedSelection.current ?? {
127+
start: value.length,
128+
end: value.length,
129+
};
135130
const markdown = buildMarkdownLink(label, url);
136131
const result = insertAtSelection(
137132
value,

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,10 @@ const MarkdownLinkDialog = ({
6464
const [linkType, setLinkType] = useState<LinkType>("group");
6565
const [searchQuery, setSearchQuery] = useState("");
6666
const [debouncedQuery] = useDebounce(searchQuery.trim(), DEBOUNCE_MS);
67-
const [selectedGroup, setSelectedGroup] = useState<AuthorGroupListItem | null>(
68-
null,
69-
);
70-
const [selectedTextItem, setSelectedTextItem] = useState<TextTitleItem | null>(
71-
null,
72-
);
67+
const [selectedGroup, setSelectedGroup] =
68+
useState<AuthorGroupListItem | null>(null);
69+
const [selectedTextItem, setSelectedTextItem] =
70+
useState<TextTitleItem | null>(null);
7371
const [selectedSegment, setSelectedSegment] = useState<SegmentItem | null>(
7472
null,
7573
);
@@ -386,7 +384,8 @@ const MarkdownLinkDialog = ({
386384
</p>
387385
<div className="max-h-48 space-y-2 overflow-y-auto">
388386
{segments.map((segment, index) => {
389-
const isSelected = selectedSegment?.segment_id === segment.segment_id;
387+
const isSelected =
388+
selectedSegment?.segment_id === segment.segment_id;
390389
return (
391390
<button
392391
key={segment.segment_id || index}

0 commit comments

Comments
 (0)