Clone test cases UI (#302)

This commit is contained in:
kimatata
2025-10-19 17:09:09 +09:00
committed by GitHub
parent 9ba1f3f1f8
commit 7c1dfd49b9
11 changed files with 217 additions and 29 deletions

View File

@@ -211,7 +211,12 @@
"selected": "Selected",
"type": "Type",
"select_types": "Select types",
"cases_moved": "Test cases moved"
"cases_selected": "cases selected",
"select_action": "Select action",
"move": "Move",
"clone": "Clone",
"cases_moved": "Test cases moved",
"cases_cloned": "Test cases cloned"
},
"Case": {
"back_to_cases": "Back to test cases",

View File

@@ -212,7 +212,12 @@
"selected": "選択済み",
"type": "タイプ",
"select_types": "タイプを選択",
"cases_moved": "テストケースを移動しました"
"cases_selected": "件のテストケース",
"select_action": "アクションを選択",
"move": "移動",
"clone": "クローン",
"cases_moved": "テストケースを移動しました",
"cases_cloned": "テストケースをクローンしました"
},
"Case": {
"back_to_cases": "テストケース一覧に戻る",

View File

@@ -211,7 +211,12 @@
"selected": "Selecionado",
"type": "Tipo",
"select_types": "Selecionar tipos",
"cases_moved": "Casos de teste movidos"
"cases_selected": "casos selecionados",
"select_action": "Selecionar ação",
"move": "Mover",
"clone": "Clonar",
"cases_moved": "Casos de teste movidos",
"cases_cloned": "Casos de teste clonados"
},
"Case": {
"back_to_cases": "Voltar para os casos de teste",

View File

@@ -0,0 +1,118 @@
'use client';
import { useState } from 'react';
import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner, addToast } from '@heroui/react';
import { Copy, Forward } from 'lucide-react';
import { CasesMessages } from '@/types/case';
import { moveCases, cloneCases } from '@/utils/caseControl';
type Props = {
isOpen: boolean;
testCaseIds: number[];
projectId: string;
targetFolderId?: number;
isDisabled: boolean;
onCancel: () => void;
onMoved: () => void;
messages: CasesMessages;
token: string;
};
export default function CaseDialog({
isOpen,
testCaseIds,
projectId,
targetFolderId,
isDisabled,
onCancel,
onMoved,
messages,
token,
}: Props) {
const [isProcessing, setIsProcessing] = useState(false);
const handleMove = async () => {
if (!targetFolderId) {
return;
}
setIsProcessing(true);
const success = await moveCases(token, testCaseIds, targetFolderId, Number(projectId));
setIsProcessing(false);
if (success) {
addToast({
title: 'Success',
color: 'success',
description: messages.casesMoved,
});
onMoved();
onCancel();
} else {
console.error('Error moving cases');
}
};
const handleClone = async () => {
if (!targetFolderId) {
return;
}
setIsProcessing(true);
const success = await cloneCases(token, testCaseIds, targetFolderId, Number(projectId));
setIsProcessing(false);
if (success) {
addToast({ title: 'Success', color: 'success', description: messages.casesCloned });
onCancel();
} else {
console.error('Error cloning cases');
}
};
return (
<Modal
isOpen={isOpen}
onOpenChange={() => {
onCancel();
}}
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">{messages.selectAction}</ModalHeader>
<ModalBody>
<p>
{testCaseIds.length} {messages.casesSelected}
</p>
</ModalBody>
<ModalFooter>
{isProcessing ? (
<Spinner />
) : (
<>
<Button variant="light" size="sm" onPress={onCancel}>
{messages.close}
</Button>
<Button
color="primary"
size="sm"
onPress={handleClone}
startContent={<Copy size={16} />}
isDisabled={isDisabled}
>
{messages.clone}
</Button>
<Button
color="primary"
size="sm"
onPress={handleMove}
startContent={<Forward size={16} />}
isDisabled={isDisabled}
>
{messages.move}
</Button>
</>
)}
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@@ -1,13 +1,13 @@
'use client';
import { useState, useEffect, useContext } from 'react';
import { useState, useEffect, useContext, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { addToast } from '@heroui/react';
import TestCaseTable from './TestCaseTable';
import CaseDialog from './CaseDialog';
import { TokenContext } from '@/utils/TokenProvider';
import { fetchCases, createCase, deleteCases, exportCases, moveCases } from '@/utils/caseControl';
import { CaseType, CasesMessages } from '@/types/case';
import CaseMoveDialog from './CaseMoveDialog';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
import { TokenContext } from '@/utils/TokenProvider';
import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl';
import { CaseType, CasesMessages } from '@/types/case';
import { PriorityMessages } from '@/types/priority';
import { TestTypeMessages } from '@/types/testType';
import { LocaleCodeType } from '@/types/locale';
@@ -98,25 +98,6 @@ export default function CasesPane({
fetchDataEffect();
}, [context, folderId, searchParams]);
useEffect(() => {
const unsubscribe = onMoveEvent(async (e) => {
const { testCaseIds, targetFolderId } = e.detail;
const moveRet = await moveCases(context.token.access_token, testCaseIds, targetFolderId, Number(projectId));
if (!moveRet) {
console.error('Error moving cases');
return;
}
setCases(cases.filter((entry) => !testCaseIds.includes(entry.id)));
addToast({
title: 'Success',
color: 'success',
description: messages.casesMoved,
});
});
return unsubscribe;
}, [cases, context.token.access_token, messages.casesMoved, projectId]);
const closeDialog = () => setIsCaseDialogOpen(false);
const onSubmit = async (title: string, description: string) => {
@@ -159,6 +140,30 @@ export default function CasesPane({
updateUrlParams({ title: title, priority: priorities, type: types });
};
// **************************************************************************
// Move/Clone cases
// **************************************************************************
const [isMoveDialogOpen, setIsMoveDialogOpen] = useState(false);
const [selectedCaseIds, setSelectedCaseIds] = useState<number[]>([]);
const [targetFolderId, setTargetFolderId] = useState<number | undefined>(undefined);
const openMoveDialog = useCallback((caseIds: number[], folderId?: number) => {
setSelectedCaseIds(caseIds);
setTargetFolderId(folderId);
setIsMoveDialogOpen(true);
}, []);
const handleMoved = () => {
setCases((prev) => prev.filter((c) => !selectedCaseIds.includes(c.id)));
};
useEffect(() => {
const unsubscribe = onMoveEvent(async (e) => {
const { testCaseIds, targetFolderId } = e.detail;
openMoveDialog(testCaseIds, targetFolderId);
});
return unsubscribe;
}, [openMoveDialog]);
return (
<>
<TestCaseTable
@@ -181,6 +186,18 @@ export default function CasesPane({
<CaseDialog isOpen={isCaseDialogOpen} onCancel={closeDialog} onSubmit={onSubmit} messages={messages} />
<CaseMoveDialog
isOpen={isMoveDialogOpen}
testCaseIds={selectedCaseIds}
projectId={projectId}
targetFolderId={targetFolderId}
isDisabled={!context.isProjectDeveloper(Number(projectId))}
onCancel={() => setIsMoveDialogOpen(false)}
onMoved={handleMoved}
messages={messages}
token={context.token.access_token}
/>
<DeleteConfirmDialog
isOpen={isDeleteConfirmDialogOpen}
onCancel={closeDeleteConfirmDialog}

View File

@@ -286,7 +286,13 @@ export default function TestCaseTable({
placement="top-left"
>
<PopoverTrigger>
<Button startContent={<Filter size={16} />} size="sm" variant="bordered" className="me-2">
<Button
startContent={<Filter size={16} />}
endContent={<ChevronDown size={16} />}
size="sm"
variant="bordered"
className="me-2"
>
{messages.filter}
</Button>
</PopoverTrigger>

View File

@@ -40,7 +40,12 @@ export default function Page({ params }: { params: { projectId: string; folderId
selected: t('selected'),
type: t('type'),
selectTypes: t('select_types'),
casesSelected: t('cases_selected'),
selectAction: t('select_action'),
move: t('move'),
clone: t('clone'),
casesMoved: t('cases_moved'),
casesCloned: t('cases_cloned'),
};
const priorityTranslation = useTranslations('Priority');

View File

@@ -84,7 +84,12 @@ type CasesMessages = {
selected: string;
type: string;
selectTypes: string;
casesSelected: string;
selectAction: string;
move: string;
clone: string;
casesMoved: string;
casesCloned: string;
};
type CaseMessages = {

View File

@@ -169,6 +169,28 @@ async function deleteCases(jwt: string, deleteCaseIds: number[], projectId: numb
}
}
async function cloneCases(jwt: string, moveCaseIds: number[], targetFolderId: number, projectId: number) {
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({ caseIds: moveCaseIds, targetFolderId }),
};
const url = `${apiServer}/cases/clone?projectId=${projectId}`;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error: unknown) {
logError('Error cloning project', error);
}
}
async function exportCases(jwt: string, folderId: number, type: string) {
if (type !== 'json' && type !== 'csv') {
console.error('export type error. type:', type);
@@ -202,4 +224,4 @@ async function exportCases(jwt: string, folderId: number, type: string) {
}
}
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, exportCases };
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, cloneCases, exportCases };