feat: import test cases UI using Excel (#339)
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
import { useState, ChangeEvent, DragEvent } from 'react';
|
||||
import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Spinner, Alert } from '@heroui/react';
|
||||
import { ArrowUpFromLine } from 'lucide-react';
|
||||
import { CasesMessages } from '@/types/case';
|
||||
import { importCases } from '@/utils/caseControl';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
folderId: number;
|
||||
isDisabled: boolean;
|
||||
onImport: () => void;
|
||||
onCancel: () => void;
|
||||
messages: CasesMessages;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export default function CaseImportDialog({ isOpen, folderId, isDisabled, onImport, onCancel, messages, token }: Props) {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
const filesArray = Array.from(event.dataTransfer.files);
|
||||
handleFiles(filesArray);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: ChangeEvent) => {
|
||||
if (event.target) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files) {
|
||||
handleFiles(Array.from(input.files));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFiles = async (filesArray: File[]) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
if (filesArray.length !== 1) {
|
||||
console.error('Error multiple file');
|
||||
} else {
|
||||
const ret = await importCases(token, folderId, filesArray[0]);
|
||||
if (ret.error) {
|
||||
setImportError(ret.error);
|
||||
} else {
|
||||
onImport();
|
||||
}
|
||||
}
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
const onCloseDialog = () => {
|
||||
setImportError(null);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={() => {
|
||||
onCloseDialog();
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">{messages.importCases}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<div className={`mt-1 text-neutral-500 dark:text-neutral-400 text-sm rounded`}>
|
||||
<div>{messages.importAvailable}</div>
|
||||
<a href="/template/unittcms-import-template-v1.xlsx" download className="text-tiny underline">
|
||||
{messages.downloadTemplate}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{importError && <Alert color="danger" className="mt-1" title="error" description={importError} />}
|
||||
<div
|
||||
className="flex items-center justify-center w-full mt-3"
|
||||
onDrop={(event) => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
handleDrop(event);
|
||||
}}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
>
|
||||
<label
|
||||
htmlFor="dropzone-file"
|
||||
className={`flex flex-col items-center justify-center w-full h-32 border-2 border-neutral-200 border-dashed rounded-lg bg-neutral-50 dark:hover:bg-bray-800 dark:bg-neutral-700 hover:bg-neutral-100 dark:border-neutral-600 dark:hover:border-neutral-500 dark:hover:bg-neutral-600 ${isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<ArrowUpFromLine />
|
||||
<p className="mb-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<span className="font-semibold">{messages.clickToUpload}</span>
|
||||
<span>{messages.orDragAndDrop}</span>
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">{messages.maxFileSize}: 50 MB</p>
|
||||
</div>
|
||||
<input
|
||||
id="dropzone-file"
|
||||
type="file"
|
||||
className="hidden"
|
||||
disabled={isDisabled}
|
||||
onChange={(e) => handleInput(e)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{isProcessing ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Button variant="light" size="sm" onPress={onCloseDialog}>
|
||||
{messages.close}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useContext, useCallback } from 'react';
|
||||
import { addToast } from '@heroui/react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import TestCaseTable from './TestCaseTable';
|
||||
import CaseDialog from './CaseDialog';
|
||||
import CaseMoveDialog from './CaseMoveDialog';
|
||||
import CaseImportDialog from './CaseImportDialog';
|
||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl';
|
||||
@@ -76,38 +78,38 @@ export default function CasesPane({
|
||||
router.push(newUrl, { scroll: false });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDataEffect() {
|
||||
if (!context.isSignedIn()) return;
|
||||
const refreshCases = useCallback(async () => {
|
||||
if (!context.isSignedIn()) return;
|
||||
|
||||
const searchParam = searchParams.get('search') || '';
|
||||
const priorityParam = parseQueryParam(searchParams.get('priority'));
|
||||
const typeParam = parseQueryParam(searchParams.get('type'));
|
||||
const tagParam = parseQueryParam(searchParams.get('tag'));
|
||||
const searchParam = searchParams.get('search') || '';
|
||||
const priorityParam = parseQueryParam(searchParams.get('priority'));
|
||||
const typeParam = parseQueryParam(searchParams.get('type'));
|
||||
const tagParam = parseQueryParam(searchParams.get('tag'));
|
||||
|
||||
setSearchFilter(searchParam);
|
||||
setPriorityFilter(priorityParam);
|
||||
setTypeFilter(typeParam);
|
||||
setTagFilter(tagParam);
|
||||
setSearchFilter(searchParam);
|
||||
setPriorityFilter(priorityParam);
|
||||
setTypeFilter(typeParam);
|
||||
setTagFilter(tagParam);
|
||||
|
||||
try {
|
||||
const data = await fetchCases(
|
||||
context.token.access_token,
|
||||
Number(folderId),
|
||||
searchParam || undefined,
|
||||
priorityParam.length > 0 ? priorityParam : undefined,
|
||||
typeParam.length > 0 ? typeParam : undefined,
|
||||
tagParam.length > 0 ? tagParam : undefined
|
||||
);
|
||||
setCases(data);
|
||||
} catch (error: unknown) {
|
||||
logError('Error fetching cases:', error);
|
||||
}
|
||||
try {
|
||||
const data = await fetchCases(
|
||||
context.token.access_token,
|
||||
Number(folderId),
|
||||
searchParam || undefined,
|
||||
priorityParam.length > 0 ? priorityParam : undefined,
|
||||
typeParam.length > 0 ? typeParam : undefined,
|
||||
tagParam.length > 0 ? tagParam : undefined
|
||||
);
|
||||
setCases(data);
|
||||
} catch (error: unknown) {
|
||||
logError('Error fetching cases:', error);
|
||||
}
|
||||
|
||||
fetchDataEffect();
|
||||
}, [context, folderId, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshCases();
|
||||
}, [refreshCases]);
|
||||
|
||||
const closeDialog = () => setIsCaseDialogOpen(false);
|
||||
|
||||
const onSubmit = async (title: string, description: string) => {
|
||||
@@ -175,6 +177,20 @@ export default function CasesPane({
|
||||
return unsubscribe;
|
||||
}, [openMoveDialog]);
|
||||
|
||||
// **************************************************************************
|
||||
// Import cases
|
||||
// **************************************************************************
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
const handleImport = () => {
|
||||
refreshCases();
|
||||
setIsImportDialogOpen(false);
|
||||
addToast({
|
||||
title: 'Success',
|
||||
color: 'success',
|
||||
description: messages.casesImported,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TestCaseTable
|
||||
@@ -184,6 +200,7 @@ export default function CasesPane({
|
||||
onCreateCase={() => setIsCaseDialogOpen(true)}
|
||||
onDeleteCase={onDeleteCase}
|
||||
onDeleteCases={onDeleteCases}
|
||||
onShowImportDialog={() => setIsImportDialogOpen(true)}
|
||||
onExportCases={onExportCases}
|
||||
onFilterChange={handleFilterChange}
|
||||
activeSearchFilter={searchFilter}
|
||||
@@ -210,6 +227,16 @@ export default function CasesPane({
|
||||
token={context.token.access_token}
|
||||
/>
|
||||
|
||||
<CaseImportDialog
|
||||
isOpen={isImportDialogOpen}
|
||||
folderId={Number(folderId)}
|
||||
isDisabled={!context.isProjectDeveloper(Number(projectId))}
|
||||
onImport={handleImport}
|
||||
onCancel={() => setIsImportDialogOpen(false)}
|
||||
messages={messages}
|
||||
token={context.token.access_token}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
isOpen={isDeleteConfirmDialogOpen}
|
||||
onCancel={closeDeleteConfirmDialog}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Plus,
|
||||
MoreVertical,
|
||||
Trash,
|
||||
FileUp,
|
||||
FileDown,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
@@ -45,6 +46,7 @@ type Props = {
|
||||
onCreateCase: () => void;
|
||||
onDeleteCase: (caseId: number) => void;
|
||||
onDeleteCases: (caseIds: number[]) => void;
|
||||
onShowImportDialog: () => void;
|
||||
onExportCases: (type: string) => void;
|
||||
onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void;
|
||||
activeSearchFilter: string;
|
||||
@@ -64,6 +66,7 @@ export default function TestCaseTable({
|
||||
onCreateCase,
|
||||
onDeleteCase,
|
||||
onDeleteCases,
|
||||
onShowImportDialog,
|
||||
onExportCases,
|
||||
onFilterChange,
|
||||
activeSearchFilter,
|
||||
@@ -355,6 +358,15 @@ export default function TestCaseTable({
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button
|
||||
startContent={<FileUp size={16} />}
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className="me-2"
|
||||
onPress={onShowImportDialog}
|
||||
>
|
||||
{messages.import}
|
||||
</Button>
|
||||
<Button
|
||||
startContent={<Plus size={16} />}
|
||||
size="sm"
|
||||
|
||||
@@ -49,6 +49,14 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
||||
casesCloned: t('cases_cloned'),
|
||||
tags: t('tags'),
|
||||
selectTags: t('select_tags'),
|
||||
import: t('import'),
|
||||
importCases: t('import_cases'),
|
||||
importAvailable: t('import_available'),
|
||||
downloadTemplate: t('download_template'),
|
||||
clickToUpload: t('click_to_upload'),
|
||||
orDragAndDrop: t('or_drag_and_drop'),
|
||||
maxFileSize: t('max_file_size'),
|
||||
casesImported: t('cases_imported'),
|
||||
};
|
||||
|
||||
const priorityTranslation = useTranslations('Priority');
|
||||
|
||||
Reference in New Issue
Block a user