feat: Test run detail pane (#381)
This commit is contained in:
13
frontend/components/Comments.tsx
Normal file
13
frontend/components/Comments.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Alert } from '@heroui/react';
|
||||||
|
|
||||||
|
export default function Comments() {
|
||||||
|
return (
|
||||||
|
<div className="h-full text-default-500">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Alert color="secondary" title="Sorry" description={'Comments function will be implemented'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
frontend/components/History.tsx
Normal file
13
frontend/components/History.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Alert } from '@heroui/react';
|
||||||
|
|
||||||
|
export default function History() {
|
||||||
|
return (
|
||||||
|
<div className="h-full text-default-500">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Alert color="secondary" title="Sorry" description={'History function will be implemented'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
frontend/components/ResizablePane.tsx
Normal file
71
frontend/components/ResizablePane.tsx
Normal file
@@ -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<HTMLDivElement>(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 (
|
||||||
|
<div ref={containerRef} className="flex h-full" style={{ userSelect: isDragging ? 'none' : 'auto' }}>
|
||||||
|
<div
|
||||||
|
className="border-r-1 dark:border-neutral-700 overflow-auto"
|
||||||
|
style={{ width: `${leftWidth}%`, minWidth: `${minLeftWidth}%` }}
|
||||||
|
>
|
||||||
|
{leftPane}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="w-1 cursor-col-resize hover:bg-primary/50 active:bg-primary transition-colors"
|
||||||
|
role="separator"
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto" style={{ minWidth: `${minRightWidth}%` }}>
|
||||||
|
{rightPane}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -353,7 +353,8 @@
|
|||||||
"case_title_or_description": "Testfall-Titel oder Beschreibung",
|
"case_title_or_description": "Testfall-Titel oder Beschreibung",
|
||||||
"selected": "Ausgewählt",
|
"selected": "Ausgewählt",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"select_tags": "Tags auswählen"
|
"select_tags": "Tags auswählen",
|
||||||
|
"no_case_selected": "Kein Testfall ausgewählt"
|
||||||
},
|
},
|
||||||
"Members": {
|
"Members": {
|
||||||
"member_management": "Mitgliederverwaltung",
|
"member_management": "Mitgliederverwaltung",
|
||||||
|
|||||||
@@ -353,7 +353,8 @@
|
|||||||
"case_title_or_description": "Test case title or description",
|
"case_title_or_description": "Test case title or description",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"select_tags": "Select tags"
|
"select_tags": "Select tags",
|
||||||
|
"no_case_selected": "No test case selected"
|
||||||
},
|
},
|
||||||
"Members": {
|
"Members": {
|
||||||
"member_management": "Member Management",
|
"member_management": "Member Management",
|
||||||
|
|||||||
@@ -353,7 +353,8 @@
|
|||||||
"case_title_or_description": "テストケースのタイトルまたは説明",
|
"case_title_or_description": "テストケースのタイトルまたは説明",
|
||||||
"selected": "選択済み",
|
"selected": "選択済み",
|
||||||
"tags": "タグ",
|
"tags": "タグ",
|
||||||
"select_tags": "タグを選択"
|
"select_tags": "タグを選択",
|
||||||
|
"no_case_selected": "テストケースが選択されていません"
|
||||||
},
|
},
|
||||||
"Members": {
|
"Members": {
|
||||||
"member_management": "メンバー管理",
|
"member_management": "メンバー管理",
|
||||||
|
|||||||
@@ -353,7 +353,8 @@
|
|||||||
"case_title_or_description": "Título ou descrição do caso de teste",
|
"case_title_or_description": "Título ou descrição do caso de teste",
|
||||||
"selected": "Selecionado",
|
"selected": "Selecionado",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"select_tags": "Selecionar tags"
|
"select_tags": "Selecionar tags",
|
||||||
|
"no_case_selected": "Nenhum caso de teste selecionado"
|
||||||
},
|
},
|
||||||
"Members": {
|
"Members": {
|
||||||
"member_management": "Gerenciamento de Membros",
|
"member_management": "Gerenciamento de Membros",
|
||||||
|
|||||||
@@ -353,7 +353,8 @@
|
|||||||
"case_title_or_description": "测试用例标题或描述",
|
"case_title_or_description": "测试用例标题或描述",
|
||||||
"selected": "已选择",
|
"selected": "已选择",
|
||||||
"tags": "标签",
|
"tags": "标签",
|
||||||
"select_tags": "选择标签"
|
"select_tags": "选择标签",
|
||||||
|
"no_case_selected": "未选择测试用例"
|
||||||
},
|
},
|
||||||
"Members": {
|
"Members": {
|
||||||
"member_management": "成员管理",
|
"member_management": "成员管理",
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ export default function RunEditor({
|
|||||||
const [statusFilter, setStatusFilter] = useState<number[]>([]);
|
const [statusFilter, setStatusFilter] = useState<number[]>([]);
|
||||||
const [tagFilter, setTagFilter] = useState<number[]>([]);
|
const [tagFilter, setTagFilter] = useState<number[]>([]);
|
||||||
const router = useRouter();
|
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 fetchRunAndStatusCount = async () => {
|
||||||
const { run, statusCounts } = await fetchRun(tokenContext.token.access_token, Number(runId));
|
const { run, statusCounts } = await fetchRun(tokenContext.token.access_token, Number(runId));
|
||||||
@@ -136,7 +138,10 @@ export default function RunEditor({
|
|||||||
setTestCases(casesData);
|
setTestCases(casesData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSignedIn = tokenContext.isSignedIn();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isSignedIn) return;
|
||||||
|
|
||||||
async function fetchDataEffect() {
|
async function fetchDataEffect() {
|
||||||
if (!tokenContext.isSignedIn()) {
|
if (!tokenContext.isSignedIn()) {
|
||||||
return;
|
return;
|
||||||
@@ -156,7 +161,7 @@ export default function RunEditor({
|
|||||||
|
|
||||||
fetchDataEffect();
|
fetchDataEffect();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [tokenContext]);
|
}, [isSignedIn]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onFilter() {
|
function onFilter() {
|
||||||
@@ -502,6 +507,9 @@ export default function RunEditor({
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-9/12">
|
<div className="w-9/12">
|
||||||
<TestCaseSelector
|
<TestCaseSelector
|
||||||
|
projectId={projectId}
|
||||||
|
runId={runId}
|
||||||
|
locale={locale}
|
||||||
cases={filteredTestCases}
|
cases={filteredTestCases}
|
||||||
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
isDisabled={!tokenContext.isProjectReporter(Number(projectId))}
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
import { useState, useEffect, useContext } from 'react';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
Avatar,
|
|
||||||
Textarea,
|
|
||||||
Chip,
|
|
||||||
} from '@heroui/react';
|
|
||||||
import { testTypes, templates } from '@/config/selection';
|
|
||||||
import { RunMessages } from '@/types/run';
|
|
||||||
import { CaseType, StepType } from '@/types/case';
|
|
||||||
import { PriorityMessages } from '@/types/priority';
|
|
||||||
import TestCasePriority from '@/components/TestCasePriority';
|
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
|
||||||
import { fetchCase } from '@/utils/caseControl';
|
|
||||||
import { TestTypeMessages } from '@/types/testType';
|
|
||||||
import { logError } from '@/utils/errorHandler';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
caseId: number;
|
|
||||||
onCancel: () => 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<CaseType>(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 (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size="5xl"
|
|
||||||
scrollBehavior="outside"
|
|
||||||
onOpenChange={() => {
|
|
||||||
onCancel();
|
|
||||||
}}
|
|
||||||
classNames={{
|
|
||||||
header: 'border-b-[1px] border-[#e5e5e5]',
|
|
||||||
body: 'border-b-[1px] border-[#e5e5e5]',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader className="flex flex-col gap-1">{testCase.title}</ModalHeader>
|
|
||||||
<ModalBody>
|
|
||||||
<p className={'font-bold mt-2'}>{messages.description}</p>
|
|
||||||
<div>{testCase.description}</div>
|
|
||||||
|
|
||||||
<div className="flex my-2">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<p className={'font-bold'}>{messages.priority}</p>
|
|
||||||
<TestCasePriority priorityValue={testCase.priority} priorityMessages={priorityMessages} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-1/2">
|
|
||||||
<p className={'font-bold'}>{messages.type}</p>
|
|
||||||
<div>{testTypeMessages[testTypes[testCase.type].uid]}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-2">
|
|
||||||
<p className={'font-bold'}>{messages.tags}</p>
|
|
||||||
<div className="flex gap-1 flex-wrap mt-1">
|
|
||||||
{testCase.Tags && testCase.Tags.length > 0 ? (
|
|
||||||
testCase.Tags.map((tag) => (
|
|
||||||
<Chip key={tag.id} size="sm" variant="flat">
|
|
||||||
{tag.name}
|
|
||||||
</Chip>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span>-</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalBody>
|
|
||||||
{templates[testCase.template].uid === 'text' ? (
|
|
||||||
<>
|
|
||||||
<p className={'font-bold mt-2'}>{messages.testDetail}</p>
|
|
||||||
<div className="flex gap-2 my-2">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<Textarea
|
|
||||||
isReadOnly
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
label={messages.preconditions}
|
|
||||||
value={testCase.preConditions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
|
||||||
<Textarea
|
|
||||||
isReadOnly
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
label={messages.expectedResult}
|
|
||||||
value={testCase.expectedResults}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className={'font-bold mt-2'}>{messages.steps}</p>
|
|
||||||
{testCase.Steps &&
|
|
||||||
testCase.Steps.map((step) => (
|
|
||||||
<div key={step.id} className="flex items-center my-1">
|
|
||||||
<Avatar className="me-2" size="sm" name={step.caseSteps.stepNo.toString()} />
|
|
||||||
<div key={step.id} className="grow flex gap-2">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<Textarea
|
|
||||||
isReadOnly
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
label={messages.detailsOfTheStep}
|
|
||||||
value={step.step}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
|
||||||
<Textarea
|
|
||||||
isReadOnly
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
label={messages.expectedResult}
|
|
||||||
value={step.result}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="light" onPress={onCancel}>
|
|
||||||
{messages.close}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,9 @@ import {
|
|||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
Chip,
|
Chip,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { ChevronDown, MoveDiagonal, MoreVertical, CopyPlus, CopyMinus } from 'lucide-react';
|
import { ChevronDown, MoreVertical, CopyPlus, CopyMinus } from 'lucide-react';
|
||||||
import TestCaseDetailDialog from './TestCaseDetailDialog';
|
|
||||||
import RunCaseStatus from './RunCaseStatus';
|
import RunCaseStatus from './RunCaseStatus';
|
||||||
|
import { Link, NextUiLinkClasses } from '@/src/i18n/routing';
|
||||||
import { testRunCaseStatus } from '@/config/selection';
|
import { testRunCaseStatus } from '@/config/selection';
|
||||||
import { CaseType } from '@/types/case';
|
import { CaseType } from '@/types/case';
|
||||||
import { RunMessages } from '@/types/run';
|
import { RunMessages } from '@/types/run';
|
||||||
@@ -27,6 +27,9 @@ import { TestTypeMessages } from '@/types/testType';
|
|||||||
import { TestRunCaseStatusMessages } from '@/types/status';
|
import { TestRunCaseStatusMessages } from '@/types/status';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
projectId: string;
|
||||||
|
runId: string;
|
||||||
|
locale: string;
|
||||||
cases: CaseType[];
|
cases: CaseType[];
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
selectedKeys: Selection;
|
selectedKeys: Selection;
|
||||||
@@ -41,6 +44,9 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TestCaseSelector({
|
export default function TestCaseSelector({
|
||||||
|
projectId,
|
||||||
|
runId,
|
||||||
|
locale,
|
||||||
cases,
|
cases,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
selectedKeys,
|
selectedKeys,
|
||||||
@@ -50,7 +56,6 @@ export default function TestCaseSelector({
|
|||||||
onExcludeCase,
|
onExcludeCase,
|
||||||
messages,
|
messages,
|
||||||
testRunCaseStatusMessages,
|
testRunCaseStatusMessages,
|
||||||
testTypeMessages,
|
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
@@ -106,7 +111,6 @@ export default function TestCaseSelector({
|
|||||||
|
|
||||||
return isIncluded;
|
return isIncluded;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCell = (testCase: CaseType, columnKey: string): ReactNode => {
|
const renderCell = (testCase: CaseType, columnKey: string): ReactNode => {
|
||||||
const cellValue = testCase[columnKey as keyof CaseType];
|
const cellValue = testCase[columnKey as keyof CaseType];
|
||||||
const isIncluded = isCaseIncluded(testCase);
|
const isIncluded = isCaseIncluded(testCase);
|
||||||
@@ -115,15 +119,16 @@ export default function TestCaseSelector({
|
|||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case 'title':
|
case 'title':
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className={isIncluded ? '' : notIncludedCaseClass}>
|
||||||
size="sm"
|
<Link
|
||||||
variant="light"
|
href={`/projects/${projectId}/runs/${runId}/cases/${testCase.id}`}
|
||||||
className="group"
|
locale={locale}
|
||||||
endContent={<MoveDiagonal size={12} className="text-transparent group-hover:text-inherit" />}
|
className={NextUiLinkClasses}
|
||||||
onPress={() => showTestCaseDetailDialog(testCase.id)}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{cellValue as string}
|
{cellValue as string}
|
||||||
</Button>
|
</Link>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
case 'priority':
|
case 'priority':
|
||||||
return (
|
return (
|
||||||
@@ -238,17 +243,6 @@ export default function TestCaseSelector({
|
|||||||
onSelectionChange(keys);
|
onSelectionChange(keys);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test Case Detail
|
|
||||||
const [isTestCaseDetailDialogOpen, setIsTestCaseDetailDialogOpen] = useState(false);
|
|
||||||
const [showingTestCaseId, setShowingTestCaseId] = useState<number>(0);
|
|
||||||
const showTestCaseDetailDialog = (showTestCaseId: number) => {
|
|
||||||
setIsTestCaseDetailDialogOpen(true);
|
|
||||||
setShowingTestCaseId(showTestCaseId);
|
|
||||||
};
|
|
||||||
const hideTestCaseDetailDialog = () => {
|
|
||||||
setIsTestCaseDetailDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table
|
<Table
|
||||||
@@ -283,15 +277,6 @@ export default function TestCaseSelector({
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<TestCaseDetailDialog
|
|
||||||
isOpen={isTestCaseDetailDialogOpen}
|
|
||||||
caseId={showingTestCaseId}
|
|
||||||
onCancel={hideTestCaseDetailDialog}
|
|
||||||
messages={messages}
|
|
||||||
priorityMessages={priorityMessages}
|
|
||||||
testTypeMessages={testTypeMessages}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Textarea, Chip } from '@heroui/react';
|
||||||
|
import { templates, testTypes } from '@/config/selection';
|
||||||
|
import type { CaseType } from '@/types/case';
|
||||||
|
import type { RunDetailMessages } from '@/types/run';
|
||||||
|
import type { PriorityMessages } from '@/types/priority';
|
||||||
|
import type { TestTypeMessages } from '@/types/testType';
|
||||||
|
import TestCasePriority from '@/components/TestCasePriority';
|
||||||
|
import { Link, NextUiLinkClasses } from '@/src/i18n/routing';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId: string;
|
||||||
|
testCase: CaseType;
|
||||||
|
locale: string;
|
||||||
|
messages: RunDetailMessages;
|
||||||
|
testTypeMessages: TestTypeMessages;
|
||||||
|
priorityMessages: PriorityMessages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CaseDetail({
|
||||||
|
projectId,
|
||||||
|
testCase,
|
||||||
|
locale,
|
||||||
|
messages,
|
||||||
|
testTypeMessages,
|
||||||
|
priorityMessages,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className="h-full p-4 text-default-500">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Link
|
||||||
|
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
|
||||||
|
locale={locale}
|
||||||
|
className={`${NextUiLinkClasses}`}
|
||||||
|
>
|
||||||
|
#{testCase.id} {testCase.title}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="font-bold">{messages.description}</p>
|
||||||
|
<div>{testCase.description}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="font-bold">{messages.priority}</p>
|
||||||
|
<TestCasePriority priorityValue={testCase.priority} priorityMessages={priorityMessages} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="font-bold">{messages.type}</p>
|
||||||
|
<div>{testTypeMessages[testTypes[testCase.type].uid]}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="font-bold">{messages.tags}</p>
|
||||||
|
<div className="flex gap-1 flex-wrap mt-1">
|
||||||
|
{testCase.Tags &&
|
||||||
|
testCase.Tags.length > 0 &&
|
||||||
|
testCase.Tags.map((tag) => (
|
||||||
|
<Chip key={tag.id} size="sm" variant="flat">
|
||||||
|
{tag.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{templates[testCase.template].uid === 'text' ? (
|
||||||
|
<>
|
||||||
|
<p className="font-bold mt-2">{messages.testDetail}</p>
|
||||||
|
<div className="flex gap-2 my-2">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Textarea
|
||||||
|
isReadOnly
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
label={messages.preconditions}
|
||||||
|
value={testCase.preConditions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Textarea
|
||||||
|
isReadOnly
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
label={messages.expectedResult}
|
||||||
|
value={testCase.expectedResults}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="font-bold mt-2">{messages.steps}</p>
|
||||||
|
{testCase.Steps &&
|
||||||
|
testCase.Steps.length > 0 &&
|
||||||
|
testCase.Steps.map((step) => (
|
||||||
|
<div key={step.id} className="flex gap-2 my-2">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Textarea isReadOnly size="sm" variant="flat" label={messages.detailsOfTheStep} value={step.step} />
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<Textarea isReadOnly size="sm" variant="flat" label={messages.expectedResult} value={step.result} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState, useContext } from 'react';
|
||||||
|
import { Tabs, Tab, Chip } from '@heroui/react';
|
||||||
|
import CaseDetail from './CaseDetail';
|
||||||
|
import Comments from '@/components/Comments';
|
||||||
|
import History from '@/components/History';
|
||||||
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
|
import { fetchCase } from '@/utils/caseControl';
|
||||||
|
import { logError } from '@/utils/errorHandler';
|
||||||
|
import type { CaseType, StepType } from '@/types/case';
|
||||||
|
import type { RunDetailMessages } from '@/types/run';
|
||||||
|
import type { PriorityMessages } from '@/types/priority';
|
||||||
|
import type { TestTypeMessages } from '@/types/testType';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId: string;
|
||||||
|
locale: string;
|
||||||
|
caseId: string;
|
||||||
|
messages: RunDetailMessages;
|
||||||
|
testTypeMessages: TestTypeMessages;
|
||||||
|
priorityMessages: PriorityMessages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TestCaseDetailPane({
|
||||||
|
projectId,
|
||||||
|
locale,
|
||||||
|
caseId,
|
||||||
|
messages,
|
||||||
|
testTypeMessages,
|
||||||
|
priorityMessages,
|
||||||
|
}: Props) {
|
||||||
|
const context = useContext(TokenContext);
|
||||||
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
const [testCase, setTestCase] = useState<CaseType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchDataEffect() {
|
||||||
|
if (!context.isSignedIn()) return;
|
||||||
|
if (!caseId || Number(caseId) <= 0) return;
|
||||||
|
|
||||||
|
setIsFetching(true);
|
||||||
|
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) => a.caseSteps.stepNo - b.caseSteps.stepNo);
|
||||||
|
}
|
||||||
|
setTestCase(data);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logError('Error fetching case data', error);
|
||||||
|
} finally {
|
||||||
|
setIsFetching(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDataEffect();
|
||||||
|
}, [context, caseId]);
|
||||||
|
|
||||||
|
if (isFetching || !testCase) {
|
||||||
|
return <div>loading...</div>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col p-3">
|
||||||
|
<Tabs aria-label="Options" size="sm">
|
||||||
|
<Tab key="caseDetail" title="Case Detail">
|
||||||
|
<CaseDetail
|
||||||
|
projectId={projectId}
|
||||||
|
testCase={testCase}
|
||||||
|
locale={locale}
|
||||||
|
messages={messages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
|
priorityMessages={priorityMessages}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
key="comments"
|
||||||
|
title={
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>Comments</span>
|
||||||
|
<Chip size="sm" variant="faded">
|
||||||
|
3
|
||||||
|
</Chip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Comments />
|
||||||
|
</Tab>
|
||||||
|
<Tab key="history" title="History">
|
||||||
|
<History />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import DetailPane from './DetailPane';
|
||||||
|
import type { RunDetailMessages } from '@/types/run';
|
||||||
|
import type { PriorityMessages } from '@/types/priority';
|
||||||
|
import type { TestTypeMessages } from '@/types/testType';
|
||||||
|
|
||||||
|
export default function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { projectId: string; runId: string; caseId: string; locale: string };
|
||||||
|
}) {
|
||||||
|
const t = useTranslations('Run');
|
||||||
|
const messages: RunDetailMessages = {
|
||||||
|
title: t('title'),
|
||||||
|
description: t('description'),
|
||||||
|
priority: t('priority'),
|
||||||
|
type: t('type'),
|
||||||
|
tags: t('tags'),
|
||||||
|
testDetail: t('test_detail'),
|
||||||
|
steps: t('steps'),
|
||||||
|
preconditions: t('preconditions'),
|
||||||
|
expectedResult: t('expected_result'),
|
||||||
|
detailsOfTheStep: t('details_of_the_step'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const pt = useTranslations('Priority');
|
||||||
|
const priorityMessages: PriorityMessages = {
|
||||||
|
critical: pt('critical'),
|
||||||
|
high: pt('high'),
|
||||||
|
medium: pt('medium'),
|
||||||
|
low: pt('low'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tt = useTranslations('Type');
|
||||||
|
const testTypeMessages: TestTypeMessages = {
|
||||||
|
other: tt('other'),
|
||||||
|
security: tt('security'),
|
||||||
|
performance: tt('performance'),
|
||||||
|
accessibility: tt('accessibility'),
|
||||||
|
functional: tt('functional'),
|
||||||
|
acceptance: tt('acceptance'),
|
||||||
|
usability: tt('usability'),
|
||||||
|
smokeSanity: tt('smoke_sanity'),
|
||||||
|
compatibility: tt('compatibility'),
|
||||||
|
destructive: tt('destructive'),
|
||||||
|
regression: tt('regression'),
|
||||||
|
automated: tt('automated'),
|
||||||
|
manual: tt('manual'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DetailPane
|
||||||
|
projectId={params.projectId}
|
||||||
|
caseId={params.caseId}
|
||||||
|
locale={params.locale}
|
||||||
|
messages={messages}
|
||||||
|
priorityMessages={priorityMessages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import RunEditor from './RunEditor';
|
||||||
|
import ResizablePanes from '@/components/ResizablePane';
|
||||||
|
import { RunMessages } from '@/types/run';
|
||||||
|
import { PriorityMessages } from '@/types/priority';
|
||||||
|
import { RunStatusMessages, TestRunCaseStatusMessages } from '@/types/status';
|
||||||
|
import { TestTypeMessages } from '@/types/testType';
|
||||||
|
|
||||||
|
export default function RunLayout({
|
||||||
|
children,
|
||||||
|
params: { projectId, runId, locale },
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: { projectId: string; runId: string; locale: string };
|
||||||
|
}) {
|
||||||
|
const t = useTranslations('Run');
|
||||||
|
const messages: RunMessages = {
|
||||||
|
backToRuns: t('back_to_runs'),
|
||||||
|
updating: t('updating'),
|
||||||
|
update: t('update'),
|
||||||
|
updatedTestRun: t('updated_test_run'),
|
||||||
|
export: t('export'),
|
||||||
|
progress: t('progress'),
|
||||||
|
refresh: t('refresh'),
|
||||||
|
id: t('id'),
|
||||||
|
title: t('title'),
|
||||||
|
pleaseEnter: t('please_enter'),
|
||||||
|
description: t('description'),
|
||||||
|
priority: t('priority'),
|
||||||
|
actions: t('actions'),
|
||||||
|
status: t('status'),
|
||||||
|
selectTestCase: t('select_test_case'),
|
||||||
|
testCaseSelection: t('test_case_selection'),
|
||||||
|
includeInRun: t('include_in_run'),
|
||||||
|
excludeFromRun: t('exclude_from_run'),
|
||||||
|
noCasesFound: t('no_cases_found'),
|
||||||
|
areYouSureLeave: t('are_you_sure_leave'),
|
||||||
|
type: t('type'),
|
||||||
|
testDetail: t('test_detail'),
|
||||||
|
steps: t('steps'),
|
||||||
|
preconditions: t('preconditions'),
|
||||||
|
expectedResult: t('expected_result'),
|
||||||
|
detailsOfTheStep: t('details_of_the_step'),
|
||||||
|
close: t('close'),
|
||||||
|
filter: t('filter'),
|
||||||
|
clearAll: t('clear_all'),
|
||||||
|
apply: t('apply'),
|
||||||
|
selectStatus: t('select_status'),
|
||||||
|
pleaseSave: t('please_save'),
|
||||||
|
caseTitleOrDescription: t('case_title_or_description'),
|
||||||
|
selected: t('selected'),
|
||||||
|
tags: t('tags'),
|
||||||
|
selectTags: t('select_tags'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const rst = useTranslations('RunStatus');
|
||||||
|
const runStatusMessages: RunStatusMessages = {
|
||||||
|
new: rst('new'),
|
||||||
|
inProgress: rst('inProgress'),
|
||||||
|
underReview: rst('underReview'),
|
||||||
|
rejected: rst('rejected'),
|
||||||
|
done: rst('done'),
|
||||||
|
closed: rst('closed'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const rcst = useTranslations('RunCaseStatus');
|
||||||
|
const testRunCaseStatusMessages: TestRunCaseStatusMessages = {
|
||||||
|
untested: rcst('untested'),
|
||||||
|
passed: rcst('passed'),
|
||||||
|
failed: rcst('failed'),
|
||||||
|
retest: rcst('retest'),
|
||||||
|
skipped: rcst('skipped'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const pt = useTranslations('Priority');
|
||||||
|
const priorityMessages: PriorityMessages = {
|
||||||
|
critical: pt('critical'),
|
||||||
|
high: pt('high'),
|
||||||
|
medium: pt('medium'),
|
||||||
|
low: pt('low'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tt = useTranslations('Type');
|
||||||
|
const testTypeMessages: TestTypeMessages = {
|
||||||
|
other: tt('other'),
|
||||||
|
security: tt('security'),
|
||||||
|
performance: tt('performance'),
|
||||||
|
accessibility: tt('accessibility'),
|
||||||
|
functional: tt('functional'),
|
||||||
|
acceptance: tt('acceptance'),
|
||||||
|
usability: tt('usability'),
|
||||||
|
smokeSanity: tt('smoke_sanity'),
|
||||||
|
compatibility: tt('compatibility'),
|
||||||
|
destructive: tt('destructive'),
|
||||||
|
regression: tt('regression'),
|
||||||
|
automated: tt('automated'),
|
||||||
|
manual: tt('manual'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizablePanes
|
||||||
|
leftPane={
|
||||||
|
<RunEditor
|
||||||
|
projectId={projectId}
|
||||||
|
runId={runId}
|
||||||
|
messages={messages}
|
||||||
|
runStatusMessages={runStatusMessages}
|
||||||
|
testRunCaseStatusMessages={testRunCaseStatusMessages}
|
||||||
|
priorityMessages={priorityMessages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightPane={children}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,105 +1,13 @@
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import RunEditor from './RunEditor';
|
|
||||||
import { RunMessages } from '@/types/run';
|
|
||||||
import { PriorityMessages } from '@/types/priority';
|
|
||||||
import { RunStatusMessages, TestRunCaseStatusMessages } from '@/types/status';
|
|
||||||
import { TestTypeMessages } from '@/types/testType';
|
|
||||||
|
|
||||||
export default function Page({ params }: { params: { projectId: string; runId: string; locale: string } }) {
|
export default function Page() {
|
||||||
const t = useTranslations('Run');
|
const t = useTranslations('Run');
|
||||||
const messages: RunMessages = {
|
|
||||||
backToRuns: t('back_to_runs'),
|
|
||||||
updating: t('updating'),
|
|
||||||
update: t('update'),
|
|
||||||
updatedTestRun: t('updated_test_run'),
|
|
||||||
export: t('export'),
|
|
||||||
progress: t('progress'),
|
|
||||||
refresh: t('refresh'),
|
|
||||||
id: t('id'),
|
|
||||||
title: t('title'),
|
|
||||||
pleaseEnter: t('please_enter'),
|
|
||||||
description: t('description'),
|
|
||||||
priority: t('priority'),
|
|
||||||
actions: t('actions'),
|
|
||||||
status: t('status'),
|
|
||||||
selectTestCase: t('select_test_case'),
|
|
||||||
testCaseSelection: t('test_case_selection'),
|
|
||||||
includeInRun: t('include_in_run'),
|
|
||||||
excludeFromRun: t('exclude_from_run'),
|
|
||||||
noCasesFound: t('no_cases_found'),
|
|
||||||
areYouSureLeave: t('are_you_sure_leave'),
|
|
||||||
type: t('type'),
|
|
||||||
testDetail: t('test_detail'),
|
|
||||||
steps: t('steps'),
|
|
||||||
preconditions: t('preconditions'),
|
|
||||||
expectedResult: t('expected_result'),
|
|
||||||
detailsOfTheStep: t('details_of_the_step'),
|
|
||||||
close: t('close'),
|
|
||||||
filter: t('filter'),
|
|
||||||
clearAll: t('clear_all'),
|
|
||||||
apply: t('apply'),
|
|
||||||
selectStatus: t('select_status'),
|
|
||||||
pleaseSave: t('please_save'),
|
|
||||||
caseTitleOrDescription: t('case_title_or_description'),
|
|
||||||
selected: t('selected'),
|
|
||||||
tags: t('tags'),
|
|
||||||
selectTags: t('select_tags'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const rst = useTranslations('RunStatus');
|
|
||||||
const runStatusMessages: RunStatusMessages = {
|
|
||||||
new: rst('new'),
|
|
||||||
inProgress: rst('inProgress'),
|
|
||||||
underReview: rst('underReview'),
|
|
||||||
rejected: rst('rejected'),
|
|
||||||
done: rst('done'),
|
|
||||||
closed: rst('closed'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const rcst = useTranslations('RunCaseStatus');
|
|
||||||
const testRunCaseStatusMessages: TestRunCaseStatusMessages = {
|
|
||||||
untested: rcst('untested'),
|
|
||||||
passed: rcst('passed'),
|
|
||||||
failed: rcst('failed'),
|
|
||||||
retest: rcst('retest'),
|
|
||||||
skipped: rcst('skipped'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const pt = useTranslations('Priority');
|
|
||||||
const priorityMessages: PriorityMessages = {
|
|
||||||
critical: pt('critical'),
|
|
||||||
high: pt('high'),
|
|
||||||
medium: pt('medium'),
|
|
||||||
low: pt('low'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tt = useTranslations('Type');
|
|
||||||
const testTypeMessages: TestTypeMessages = {
|
|
||||||
other: tt('other'),
|
|
||||||
security: tt('security'),
|
|
||||||
performance: tt('performance'),
|
|
||||||
accessibility: tt('accessibility'),
|
|
||||||
functional: tt('functional'),
|
|
||||||
acceptance: tt('acceptance'),
|
|
||||||
usability: tt('usability'),
|
|
||||||
smokeSanity: tt('smoke_sanity'),
|
|
||||||
compatibility: tt('compatibility'),
|
|
||||||
destructive: tt('destructive'),
|
|
||||||
regression: tt('regression'),
|
|
||||||
automated: tt('automated'),
|
|
||||||
manual: tt('manual'),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RunEditor
|
<div className="container mx-auto max-w-3xl pt-6 px-6 flex-grow">
|
||||||
projectId={params.projectId}
|
<div className="w-full p-3 flex items-center justify-between">
|
||||||
runId={params.runId}
|
<h3 className="font-bold">{t('no_case_selected')}</h3>
|
||||||
messages={messages}
|
</div>
|
||||||
runStatusMessages={runStatusMessages}
|
</div>
|
||||||
testRunCaseStatusMessages={testRunCaseStatusMessages}
|
|
||||||
priorityMessages={priorityMessages}
|
|
||||||
testTypeMessages={testTypeMessages}
|
|
||||||
locale={params.locale}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,25 @@ type RunMessages = {
|
|||||||
selectTags: string;
|
selectTags: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { RunType, RunCaseType, RunStatusCountType, ProgressSeriesType, RunsMessages, RunMessages };
|
type RunDetailMessages = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
priority: string;
|
||||||
|
type: string;
|
||||||
|
tags: string;
|
||||||
|
testDetail: string;
|
||||||
|
steps: string;
|
||||||
|
preconditions: string;
|
||||||
|
expectedResult: string;
|
||||||
|
detailsOfTheStep: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
RunType,
|
||||||
|
RunCaseType,
|
||||||
|
RunStatusCountType,
|
||||||
|
ProgressSeriesType,
|
||||||
|
RunsMessages,
|
||||||
|
RunMessages,
|
||||||
|
RunDetailMessages,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const useFormGuard = (isDirty: boolean, confirmText: string) => {
|
export const useFormGuard = (isDirty: boolean, confirmText: string, ignorePaths?: string[]) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
if (isDirty && event.target instanceof Element && event.target.closest('a:not([target="_blank"]')) {
|
if (!isDirty) return;
|
||||||
|
|
||||||
|
if (event.target instanceof Element) {
|
||||||
|
const anchor = event.target.closest('a:not([target="_blank"])');
|
||||||
|
if (!anchor) return;
|
||||||
|
|
||||||
|
const href = anchor.getAttribute('href');
|
||||||
|
if (!href) return;
|
||||||
|
|
||||||
|
// do not show confirm for ignored paths
|
||||||
|
if (ignorePaths && ignorePaths.some((path) => href.includes(path))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!window.confirm(confirmText)) {
|
if (!window.confirm(confirmText)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -25,5 +38,5 @@ export const useFormGuard = (isDirty: boolean, confirmText: string) => {
|
|||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
window.removeEventListener('click', handleClick, true);
|
window.removeEventListener('click', handleClick, true);
|
||||||
};
|
};
|
||||||
}, [confirmText, isDirty]);
|
}, [confirmText, isDirty, ignorePaths]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user