feat: import test cases UI using Excel (#339)
This commit is contained in:
43
backend/package-lock.json
generated
43
backend/package-lock.json
generated
@@ -1010,27 +1010,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/encoding": {
|
|
||||||
"version": "0.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"iconv-lite": "^0.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/encoding/node_modules/iconv-lite": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
@@ -1120,6 +1099,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -4286,26 +4266,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
|
||||||
},
|
},
|
||||||
"encoding": {
|
|
||||||
"version": "0.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"iconv-lite": "^0.6.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"iconv-lite": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"end-of-stream": {
|
"end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
@@ -4368,6 +4328,7 @@
|
|||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||||
|
"peer": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function (sequelize) {
|
|||||||
|
|
||||||
let errorMessage = null;
|
let errorMessage = null;
|
||||||
const casesToCreate = [];
|
const casesToCreate = [];
|
||||||
const requiredFields = ['title', 'preConditions', 'expectedResults'];
|
const requiredFields = ['title', 'priority', 'type', 'template'];
|
||||||
for (const [index, row] of jsonData.entries()) {
|
for (const [index, row] of jsonData.entries()) {
|
||||||
const rowNumber = index + 2;
|
const rowNumber = index + 2;
|
||||||
for (const field of requiredFields) {
|
for (const field of requiredFields) {
|
||||||
|
|||||||
@@ -240,7 +240,15 @@
|
|||||||
"cases_moved": "Test cases moved",
|
"cases_moved": "Test cases moved",
|
||||||
"cases_cloned": "Test cases cloned",
|
"cases_cloned": "Test cases cloned",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"select_tags": "Select tags"
|
"select_tags": "Select tags",
|
||||||
|
"import": "Import",
|
||||||
|
"import_cases": "Import Test Cases",
|
||||||
|
"import_available": "You can import test cases from Excel file (xlsx, xls). The Excel file to be imported must follow the specified format.",
|
||||||
|
"download_template": "Download Template",
|
||||||
|
"click_to_upload": "Click to upload",
|
||||||
|
"or_drag_and_drop": " or drag and drop",
|
||||||
|
"max_file_size": "Max. file size",
|
||||||
|
"cases_imported": "Test cases imported"
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "Back to test cases",
|
"back_to_cases": "Back to test cases",
|
||||||
|
|||||||
@@ -240,7 +240,15 @@
|
|||||||
"cases_moved": "テストケースを移動しました",
|
"cases_moved": "テストケースを移動しました",
|
||||||
"cases_cloned": "テストケースをクローンしました",
|
"cases_cloned": "テストケースをクローンしました",
|
||||||
"tags": "タグ",
|
"tags": "タグ",
|
||||||
"select_tags": "タグを選択"
|
"select_tags": "タグを選択",
|
||||||
|
"import": "インポート",
|
||||||
|
"import_cases": "テストケースのインポート",
|
||||||
|
"import_available": "Excelファイル(xlsx, xls)からテストケースをインポートできます。インポートするExcelファイルは既定のフォーマットに従う必要があります。",
|
||||||
|
"download_template": "テンプレートをダウンロード",
|
||||||
|
"click_to_upload": "クリックしてアップロード",
|
||||||
|
"or_drag_and_drop": "またはドラッグアンドドロップ",
|
||||||
|
"max_file_size": "最大ファイルサイズ",
|
||||||
|
"cases_imported": "テストケースがインポートされました"
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "テストケース一覧に戻る",
|
"back_to_cases": "テストケース一覧に戻る",
|
||||||
|
|||||||
@@ -240,7 +240,15 @@
|
|||||||
"cases_moved": "Casos de teste movidos",
|
"cases_moved": "Casos de teste movidos",
|
||||||
"cases_cloned": "Casos de teste clonados",
|
"cases_cloned": "Casos de teste clonados",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"select_tags": "Selecionar tags"
|
"select_tags": "Selecionar tags",
|
||||||
|
"import": "Importar",
|
||||||
|
"import_cases": "Importar Casos de Teste",
|
||||||
|
"import_available": "Você pode importar casos de teste a partir de um arquivo Excel (xlsx, xls). O arquivo Excel a ser importado deve seguir o formato especificado.",
|
||||||
|
"download_template": "Baixar Modelo",
|
||||||
|
"click_to_upload": "Clique para enviar",
|
||||||
|
"or_drag_and_drop": " ou arraste e solte",
|
||||||
|
"max_file_size": "Tamanho máx. do arquivo",
|
||||||
|
"cases_imported": "Casos de teste importados"
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "Voltar para os casos de teste",
|
"back_to_cases": "Voltar para os casos de teste",
|
||||||
|
|||||||
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
@@ -9988,9 +9988,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001700",
|
"version": "1.0.30001754",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
||||||
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
|
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -10004,7 +10004,8 @@
|
|||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
|
|||||||
BIN
frontend/public/template/unittcms-import-template-v1.xlsx
Normal file
BIN
frontend/public/template/unittcms-import-template-v1.xlsx
Normal file
Binary file not shown.
@@ -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';
|
'use client';
|
||||||
import { useState, useEffect, useContext, useCallback } from 'react';
|
import { useState, useEffect, useContext, useCallback } from 'react';
|
||||||
|
import { addToast } from '@heroui/react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import TestCaseTable from './TestCaseTable';
|
import TestCaseTable from './TestCaseTable';
|
||||||
import CaseDialog from './CaseDialog';
|
import CaseDialog from './CaseDialog';
|
||||||
import CaseMoveDialog from './CaseMoveDialog';
|
import CaseMoveDialog from './CaseMoveDialog';
|
||||||
|
import CaseImportDialog from './CaseImportDialog';
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl';
|
import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl';
|
||||||
@@ -76,38 +78,38 @@ export default function CasesPane({
|
|||||||
router.push(newUrl, { scroll: false });
|
router.push(newUrl, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshCases = useCallback(async () => {
|
||||||
async function fetchDataEffect() {
|
if (!context.isSignedIn()) return;
|
||||||
if (!context.isSignedIn()) return;
|
|
||||||
|
|
||||||
const searchParam = searchParams.get('search') || '';
|
const searchParam = searchParams.get('search') || '';
|
||||||
const priorityParam = parseQueryParam(searchParams.get('priority'));
|
const priorityParam = parseQueryParam(searchParams.get('priority'));
|
||||||
const typeParam = parseQueryParam(searchParams.get('type'));
|
const typeParam = parseQueryParam(searchParams.get('type'));
|
||||||
const tagParam = parseQueryParam(searchParams.get('tag'));
|
const tagParam = parseQueryParam(searchParams.get('tag'));
|
||||||
|
|
||||||
setSearchFilter(searchParam);
|
setSearchFilter(searchParam);
|
||||||
setPriorityFilter(priorityParam);
|
setPriorityFilter(priorityParam);
|
||||||
setTypeFilter(typeParam);
|
setTypeFilter(typeParam);
|
||||||
setTagFilter(tagParam);
|
setTagFilter(tagParam);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchCases(
|
const data = await fetchCases(
|
||||||
context.token.access_token,
|
context.token.access_token,
|
||||||
Number(folderId),
|
Number(folderId),
|
||||||
searchParam || undefined,
|
searchParam || undefined,
|
||||||
priorityParam.length > 0 ? priorityParam : undefined,
|
priorityParam.length > 0 ? priorityParam : undefined,
|
||||||
typeParam.length > 0 ? typeParam : undefined,
|
typeParam.length > 0 ? typeParam : undefined,
|
||||||
tagParam.length > 0 ? tagParam : undefined
|
tagParam.length > 0 ? tagParam : undefined
|
||||||
);
|
);
|
||||||
setCases(data);
|
setCases(data);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logError('Error fetching cases:', error);
|
logError('Error fetching cases:', error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchDataEffect();
|
|
||||||
}, [context, folderId, searchParams]);
|
}, [context, folderId, searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshCases();
|
||||||
|
}, [refreshCases]);
|
||||||
|
|
||||||
const closeDialog = () => setIsCaseDialogOpen(false);
|
const closeDialog = () => setIsCaseDialogOpen(false);
|
||||||
|
|
||||||
const onSubmit = async (title: string, description: string) => {
|
const onSubmit = async (title: string, description: string) => {
|
||||||
@@ -175,6 +177,20 @@ export default function CasesPane({
|
|||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [openMoveDialog]);
|
}, [openMoveDialog]);
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// Import cases
|
||||||
|
// **************************************************************************
|
||||||
|
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||||
|
const handleImport = () => {
|
||||||
|
refreshCases();
|
||||||
|
setIsImportDialogOpen(false);
|
||||||
|
addToast({
|
||||||
|
title: 'Success',
|
||||||
|
color: 'success',
|
||||||
|
description: messages.casesImported,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TestCaseTable
|
<TestCaseTable
|
||||||
@@ -184,6 +200,7 @@ export default function CasesPane({
|
|||||||
onCreateCase={() => setIsCaseDialogOpen(true)}
|
onCreateCase={() => setIsCaseDialogOpen(true)}
|
||||||
onDeleteCase={onDeleteCase}
|
onDeleteCase={onDeleteCase}
|
||||||
onDeleteCases={onDeleteCases}
|
onDeleteCases={onDeleteCases}
|
||||||
|
onShowImportDialog={() => setIsImportDialogOpen(true)}
|
||||||
onExportCases={onExportCases}
|
onExportCases={onExportCases}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
activeSearchFilter={searchFilter}
|
activeSearchFilter={searchFilter}
|
||||||
@@ -210,6 +227,16 @@ export default function CasesPane({
|
|||||||
token={context.token.access_token}
|
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
|
<DeleteConfirmDialog
|
||||||
isOpen={isDeleteConfirmDialogOpen}
|
isOpen={isDeleteConfirmDialogOpen}
|
||||||
onCancel={closeDeleteConfirmDialog}
|
onCancel={closeDeleteConfirmDialog}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Trash,
|
Trash,
|
||||||
|
FileUp,
|
||||||
FileDown,
|
FileDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -45,6 +46,7 @@ type Props = {
|
|||||||
onCreateCase: () => void;
|
onCreateCase: () => void;
|
||||||
onDeleteCase: (caseId: number) => void;
|
onDeleteCase: (caseId: number) => void;
|
||||||
onDeleteCases: (caseIds: number[]) => void;
|
onDeleteCases: (caseIds: number[]) => void;
|
||||||
|
onShowImportDialog: () => void;
|
||||||
onExportCases: (type: string) => void;
|
onExportCases: (type: string) => void;
|
||||||
onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void;
|
onFilterChange: (query: string, priorities: number[], types: number[], tag: number[]) => void;
|
||||||
activeSearchFilter: string;
|
activeSearchFilter: string;
|
||||||
@@ -64,6 +66,7 @@ export default function TestCaseTable({
|
|||||||
onCreateCase,
|
onCreateCase,
|
||||||
onDeleteCase,
|
onDeleteCase,
|
||||||
onDeleteCases,
|
onDeleteCases,
|
||||||
|
onShowImportDialog,
|
||||||
onExportCases,
|
onExportCases,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
activeSearchFilter,
|
activeSearchFilter,
|
||||||
@@ -355,6 +358,15 @@ export default function TestCaseTable({
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<Button
|
||||||
|
startContent={<FileUp size={16} />}
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
className="me-2"
|
||||||
|
onPress={onShowImportDialog}
|
||||||
|
>
|
||||||
|
{messages.import}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
startContent={<Plus size={16} />}
|
startContent={<Plus size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
casesCloned: t('cases_cloned'),
|
casesCloned: t('cases_cloned'),
|
||||||
tags: t('tags'),
|
tags: t('tags'),
|
||||||
selectTags: t('select_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');
|
const priorityTranslation = useTranslations('Priority');
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ type CasesMessages = {
|
|||||||
tags: string;
|
tags: string;
|
||||||
casesCloned: string;
|
casesCloned: string;
|
||||||
selectTags: string;
|
selectTags: string;
|
||||||
|
import: string;
|
||||||
|
importCases: string;
|
||||||
|
importAvailable: string;
|
||||||
|
downloadTemplate: string;
|
||||||
|
clickToUpload: string;
|
||||||
|
orDragAndDrop: string;
|
||||||
|
maxFileSize: string;
|
||||||
|
casesImported: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CaseMessages = {
|
type CaseMessages = {
|
||||||
|
|||||||
@@ -235,4 +235,25 @@ async function exportCases(jwt: string, folderId: number, type: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, cloneCases, exportCases };
|
async function importCases(jwt: string, folderId: number, file: File) {
|
||||||
|
const url = `${apiServer}/cases/import?folderId=${folderId}`;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logError('Error importing data', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, cloneCases, exportCases, importCases };
|
||||||
|
|||||||
Reference in New Issue
Block a user