feat: download test cases csv (#219)
This commit is contained in:
@@ -78,12 +78,14 @@ app.use('/folders', foldersEditRoute);
|
|||||||
app.use('/folders', foldersDeleteRoute);
|
app.use('/folders', foldersDeleteRoute);
|
||||||
|
|
||||||
// "/cases"
|
// "/cases"
|
||||||
|
const casesDownloadRoute = require('./routes/cases/download')(sequelize);
|
||||||
const casesIndexRoute = require('./routes/cases/index')(sequelize);
|
const casesIndexRoute = require('./routes/cases/index')(sequelize);
|
||||||
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize);
|
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize);
|
||||||
const casesShowRoute = require('./routes/cases/show')(sequelize);
|
const casesShowRoute = require('./routes/cases/show')(sequelize);
|
||||||
const casesNewRoute = require('./routes/cases/new')(sequelize);
|
const casesNewRoute = require('./routes/cases/new')(sequelize);
|
||||||
const casesEditRoute = require('./routes/cases/edit')(sequelize);
|
const casesEditRoute = require('./routes/cases/edit')(sequelize);
|
||||||
const casesDeleteRoute = require('./routes/cases/delete')(sequelize);
|
const casesDeleteRoute = require('./routes/cases/delete')(sequelize);
|
||||||
|
app.use('/cases', casesDownloadRoute);
|
||||||
app.use('/cases', casesIndexRoute);
|
app.use('/cases', casesIndexRoute);
|
||||||
app.use('/cases', casesIndexByProjectIdRoute);
|
app.use('/cases', casesIndexByProjectIdRoute);
|
||||||
app.use('/cases', casesShowRoute);
|
app.use('/cases', casesShowRoute);
|
||||||
|
|||||||
11
backend/package-lock.json
generated
11
backend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"papaparse": "^5.5.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
@@ -2358,6 +2359,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/papaparse": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
||||||
|
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA=="
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -5265,6 +5271,11 @@
|
|||||||
"aggregate-error": "^3.0.0"
|
"aggregate-error": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"papaparse": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
||||||
|
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA=="
|
||||||
|
},
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"papaparse": "^5.5.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
|
|||||||
48
backend/routes/cases/download.js
Normal file
48
backend/routes/cases/download.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const Papa = require('papaparse');
|
||||||
|
const defineCase = require('../../models/cases');
|
||||||
|
|
||||||
|
module.exports = function (sequelize) {
|
||||||
|
const Case = defineCase(sequelize, DataTypes);
|
||||||
|
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
|
||||||
|
const { verifyProjectVisibleFromFolderId } = require('../../middleware/verifyVisible')(sequelize);
|
||||||
|
|
||||||
|
router.get('/download', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
|
||||||
|
const { folderId, type } = req.query;
|
||||||
|
|
||||||
|
if (!folderId) {
|
||||||
|
return res.status(400).json({ error: 'folderId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return res.status(400).json({ error: 'download type is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cases = await Case.findAll({
|
||||||
|
where: { folderId },
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cases.length === 0) {
|
||||||
|
return res.status(404).send('No cases found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const csv = Papa.unparse(cases, {
|
||||||
|
quotes: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`);
|
||||||
|
res.send(csv);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -188,6 +188,7 @@
|
|||||||
"close": "Close",
|
"close": "Close",
|
||||||
"are_you_sure": "Are you sure you want to delete test cases?",
|
"are_you_sure": "Are you sure you want to delete test cases?",
|
||||||
"new_test_case": "New Test Case",
|
"new_test_case": "New Test Case",
|
||||||
|
"download_csv": "Download CSV",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"no_cases_found": "No test cases found",
|
"no_cases_found": "No test cases found",
|
||||||
"case_title": "Test Case Title",
|
"case_title": "Test Case Title",
|
||||||
|
|||||||
@@ -189,6 +189,7 @@
|
|||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"are_you_sure": "テストケースを削除してもよろしいですか?",
|
"are_you_sure": "テストケースを削除してもよろしいですか?",
|
||||||
"new_test_case": "新規テストケース",
|
"new_test_case": "新規テストケース",
|
||||||
|
"download_csv": "CSVダウンロード",
|
||||||
"status": "ステータス",
|
"status": "ステータス",
|
||||||
"no_cases_found": "テストケースがありません",
|
"no_cases_found": "テストケースがありません",
|
||||||
"case_title": "テストケースタイトル",
|
"case_title": "テストケースタイトル",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
|
||||||
import TestCaseTable from './TestCaseTable';
|
import TestCaseTable from './TestCaseTable';
|
||||||
import { fetchCases, createCase, deleteCases } from '@/utils/caseControl';
|
import CaseDialog from './CaseDialog';
|
||||||
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
|
import { fetchCases, createCase, deleteCases, csvDownload } from '@/utils/caseControl';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import CaseDialog from './CaseDialog';
|
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
@@ -74,6 +74,10 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCsvDownload = async () => {
|
||||||
|
await csvDownload(context.token.access_token, Number(folderId));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TestCaseTable
|
<TestCaseTable
|
||||||
@@ -83,6 +87,7 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
onCreateCase={() => setIsCaseDialogOpen(true)}
|
onCreateCase={() => setIsCaseDialogOpen(true)}
|
||||||
onDeleteCase={onDeleteCase}
|
onDeleteCase={onDeleteCase}
|
||||||
onDeleteCases={onDeleteCases}
|
onDeleteCases={onDeleteCases}
|
||||||
|
onCsvDownload={onCsvDownload}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
priorityMessages={priorityMessages}
|
priorityMessages={priorityMessages}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { Plus, MoreVertical, Trash } from 'lucide-react';
|
import { Plus, MoreVertical, Trash, Download } from 'lucide-react';
|
||||||
import { Link } from '@/src/i18n/routing';
|
import { Link } from '@/src/i18n/routing';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
@@ -28,6 +28,7 @@ type Props = {
|
|||||||
onCreateCase: () => void;
|
onCreateCase: () => void;
|
||||||
onDeleteCase: (caseId: number) => void;
|
onDeleteCase: (caseId: number) => void;
|
||||||
onDeleteCases: (caseIds: number[]) => void;
|
onDeleteCases: (caseIds: number[]) => void;
|
||||||
|
onCsvDownload: () => void;
|
||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
@@ -40,6 +41,7 @@ export default function TestCaseTable({
|
|||||||
onCreateCase,
|
onCreateCase,
|
||||||
onDeleteCase,
|
onDeleteCase,
|
||||||
onDeleteCases,
|
onDeleteCases,
|
||||||
|
onCsvDownload,
|
||||||
messages,
|
messages,
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
locale,
|
locale,
|
||||||
@@ -154,6 +156,7 @@ export default function TestCaseTable({
|
|||||||
<>
|
<>
|
||||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
||||||
<h3 className="font-bold">{messages.testCaseList}</h3>
|
<h3 className="font-bold">{messages.testCaseList}</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
||||||
<Button
|
<Button
|
||||||
@@ -167,6 +170,16 @@ export default function TestCaseTable({
|
|||||||
{messages.delete}
|
{messages.delete}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
startContent={<Download size={16} />}
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
className="me-2"
|
||||||
|
onPress={onCsvDownload}
|
||||||
|
>
|
||||||
|
{messages.downloadCsv}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
startContent={<Plus size={16} />}
|
startContent={<Plus size={16} />}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PriorityMessages } from '@/types/priority';
|
|
||||||
import CasesPane from './CasesPane';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import CasesPane from './CasesPane';
|
||||||
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
||||||
@@ -25,6 +25,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
close: t('close'),
|
close: t('close'),
|
||||||
areYouSure: t('are_you_sure'),
|
areYouSure: t('are_you_sure'),
|
||||||
newTestCase: t('new_test_case'),
|
newTestCase: t('new_test_case'),
|
||||||
|
downloadCsv: t('download_csv'),
|
||||||
status: t('status'),
|
status: t('status'),
|
||||||
noCasesFound: t('no_cases_found'),
|
noCasesFound: t('no_cases_found'),
|
||||||
caseTitle: t('case_title'),
|
caseTitle: t('case_title'),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type CasesMessages = {
|
|||||||
areYouSure: string;
|
areYouSure: string;
|
||||||
delete: string;
|
delete: string;
|
||||||
newTestCase: string;
|
newTestCase: string;
|
||||||
|
downloadCsv: string;
|
||||||
status: string;
|
status: string;
|
||||||
noCasesFound: string;
|
noCasesFound: string;
|
||||||
caseTitle: string;
|
caseTitle: string;
|
||||||
|
|||||||
@@ -132,4 +132,33 @@ async function deleteCases(jwt: string, deleteCaseIds: number[], projectId: numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchCase, fetchCases, updateCase, createCase, deleteCases };
|
async function csvDownload(jwt: string, folderId: number) {
|
||||||
|
const url = `${apiServer}/cases/download?folderId=${folderId}&type=csv`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const objectUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = objectUrl;
|
||||||
|
a.download = `folder_${folderId}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(objectUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching data:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchCase, fetchCases, updateCase, createCase, deleteCases, csvDownload };
|
||||||
|
|||||||
Reference in New Issue
Block a user