From 34135209d91f64b014637ac50aa0df5f027ad369 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:27:34 +0900 Subject: [PATCH] feat: Test run detail pane (#381) --- frontend/components/Comments.tsx | 13 ++ frontend/components/History.tsx | 13 ++ frontend/components/ResizablePane.tsx | 71 +++++++ frontend/messages/de.json | 3 +- frontend/messages/en.json | 3 +- frontend/messages/ja.json | 3 +- frontend/messages/pt-BR.json | 3 +- frontend/messages/zh-CN.json | 3 +- .../[projectId]/runs/[runId]/RunEditor.tsx | 12 +- .../runs/[runId]/TestCaseDetailDialog.tsx | 197 ------------------ .../runs/[runId]/TestCaseSelector.tsx | 51 ++--- .../[runId]/cases/[caseId]/CaseDetail.tsx | 112 ++++++++++ .../[runId]/cases/[caseId]/DetailPane.tsx | 95 +++++++++ .../runs/[runId]/cases/[caseId]/page.tsx | 61 ++++++ .../[projectId]/runs/[runId]/layout.tsx | 117 +++++++++++ .../[projectId]/runs/[runId]/page.tsx | 104 +-------- frontend/types/run.ts | 23 +- frontend/utils/formGuard.ts | 19 +- 18 files changed, 564 insertions(+), 339 deletions(-) create mode 100644 frontend/components/Comments.tsx create mode 100644 frontend/components/History.tsx create mode 100644 frontend/components/ResizablePane.tsx delete mode 100644 frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/TestCaseDetailDialog.tsx create mode 100644 frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/CaseDetail.tsx create mode 100644 frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/DetailPane.tsx create mode 100644 frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/cases/[caseId]/page.tsx create mode 100644 frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/layout.tsx diff --git a/frontend/components/Comments.tsx b/frontend/components/Comments.tsx new file mode 100644 index 0000000..a6abe03 --- /dev/null +++ b/frontend/components/Comments.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { Alert } from '@heroui/react'; + +export default function Comments() { + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/components/History.tsx b/frontend/components/History.tsx new file mode 100644 index 0000000..3d30f0b --- /dev/null +++ b/frontend/components/History.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { Alert } from '@heroui/react'; + +export default function History() { + return ( +
+
+ +
+
+ ); +} diff --git a/frontend/components/ResizablePane.tsx b/frontend/components/ResizablePane.tsx new file mode 100644 index 0000000..5330539 --- /dev/null +++ b/frontend/components/ResizablePane.tsx @@ -0,0 +1,71 @@ +'use client'; +import { useState, useRef, useEffect, ReactNode } from 'react'; + +type Props = { + leftPane: ReactNode; + rightPane: ReactNode; +}; + +export default function ResizablePanes({ leftPane, rightPane }: Props) { + const [leftWidth, setLeftWidth] = useState(70); // default 70% + const [isDragging, setIsDragging] = useState(false); + const containerRef = useRef(null); + + const minLeftWidth = 40; // left panel min width 40% + const minRightWidth = 15; // right panel min width 15% + + const handleMouseDown = () => { + setIsDragging(true); + }; + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging || !containerRef.current) return; + + const containerRect = containerRef.current.getBoundingClientRect(); + const newLeftWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100; + + // Clamp the width between min and max + const maxLeftWidth = 100 - minRightWidth; + const clampedWidth = Math.max(minLeftWidth, Math.min(maxLeftWidth, newLeftWidth)); + + setLeftWidth(clampedWidth); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging]); + + return ( +
+
+ {leftPane} +
+ +
+ +
+ {rightPane} +
+
+ ); +} diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 65eb4e3..2d81a38 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -353,7 +353,8 @@ "case_title_or_description": "Testfall-Titel oder Beschreibung", "selected": "Ausgewählt", "tags": "Tags", - "select_tags": "Tags auswählen" + "select_tags": "Tags auswählen", + "no_case_selected": "Kein Testfall ausgewählt" }, "Members": { "member_management": "Mitgliederverwaltung", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 83c5ee4..b63160a 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -353,7 +353,8 @@ "case_title_or_description": "Test case title or description", "selected": "Selected", "tags": "Tags", - "select_tags": "Select tags" + "select_tags": "Select tags", + "no_case_selected": "No test case selected" }, "Members": { "member_management": "Member Management", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 9827ae1..6c42c16 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -353,7 +353,8 @@ "case_title_or_description": "テストケースのタイトルまたは説明", "selected": "選択済み", "tags": "タグ", - "select_tags": "タグを選択" + "select_tags": "タグを選択", + "no_case_selected": "テストケースが選択されていません" }, "Members": { "member_management": "メンバー管理", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index f372406..7f7c8d6 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -353,7 +353,8 @@ "case_title_or_description": "Título ou descrição do caso de teste", "selected": "Selecionado", "tags": "Tags", - "select_tags": "Selecionar tags" + "select_tags": "Selecionar tags", + "no_case_selected": "Nenhum caso de teste selecionado" }, "Members": { "member_management": "Gerenciamento de Membros", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index af2f603..2e77188 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -353,7 +353,8 @@ "case_title_or_description": "测试用例标题或描述", "selected": "已选择", "tags": "标签", - "select_tags": "选择标签" + "select_tags": "选择标签", + "no_case_selected": "未选择测试用例" }, "Members": { "member_management": "成员管理", diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx index 09da940..08d1af2 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx @@ -111,7 +111,9 @@ export default function RunEditor({ const [statusFilter, setStatusFilter] = useState([]); const [tagFilter, setTagFilter] = useState([]); const router = useRouter(); - useFormGuard(isDirty, messages.areYouSureLeave); + + // not show warning when navigating to test case detail page + useFormGuard(isDirty, messages.areYouSureLeave, [`/projects/${projectId}/runs/${runId}/cases/\\d+`]); const fetchRunAndStatusCount = async () => { const { run, statusCounts } = await fetchRun(tokenContext.token.access_token, Number(runId)); @@ -136,7 +138,10 @@ export default function RunEditor({ setTestCases(casesData); }; + const isSignedIn = tokenContext.isSignedIn(); useEffect(() => { + if (!isSignedIn) return; + async function fetchDataEffect() { if (!tokenContext.isSignedIn()) { return; @@ -156,7 +161,7 @@ export default function RunEditor({ fetchDataEffect(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tokenContext]); + }, [isSignedIn]); useEffect(() => { function onFilter() { @@ -502,6 +507,9 @@ export default function RunEditor({
void; - messages: RunMessages; - testTypeMessages: TestTypeMessages; - priorityMessages: PriorityMessages; -}; - -const defaultTestCase = { - id: 0, - title: '', - state: 0, - priority: 0, - type: 0, - automationStatus: 0, - description: '', - template: 0, - preConditions: '', - expectedResults: '', - folderId: 0, -}; - -export default function TestCaseDetailDialog({ - isOpen, - caseId, - onCancel, - messages, - testTypeMessages, - priorityMessages, -}: Props) { - const context = useContext(TokenContext); - const [testCase, setTestCase] = useState(defaultTestCase); - - useEffect(() => { - async function fetchDataEffect() { - if (!context.isSignedIn()) { - return; - } - - if (!caseId || caseId <= 0) { - return; - } - - try { - const data = await fetchCase(context.token.access_token, Number(caseId)); - if (data.Steps && data.Steps.length > 0) { - data.Steps.sort((a: StepType, b: StepType) => { - const stepNoA = a.caseSteps.stepNo; - const stepNoB = b.caseSteps.stepNo; - return stepNoA - stepNoB; - }); - } - - setTestCase(data); - } catch (error: unknown) { - logError('Error fetching case data', error); - } - } - - fetchDataEffect(); - }, [context, caseId]); - - return ( - { - onCancel(); - }} - classNames={{ - header: 'border-b-[1px] border-[#e5e5e5]', - body: 'border-b-[1px] border-[#e5e5e5]', - }} - > - - {testCase.title} - -

{messages.description}

-
{testCase.description}
- -
-
-

{messages.priority}

- -
- -
-

{messages.type}

-
{testTypeMessages[testTypes[testCase.type].uid]}
-
-
- -
-

{messages.tags}

-
- {testCase.Tags && testCase.Tags.length > 0 ? ( - testCase.Tags.map((tag) => ( - - {tag.name} - - )) - ) : ( - - - )} -
-
-
- - {templates[testCase.template].uid === 'text' ? ( - <> -

{messages.testDetail}

-
-
-