From 0faa5f0be23d64ec6deae622b84d983abb36c9b2 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Tue, 6 May 2025 20:31:47 +0900 Subject: [PATCH] feat: export test run result (#220) --- backend/index.js | 2 + backend/package-lock.json | 170 +++++++++++++++++- backend/package.json | 3 +- backend/routes/cases/download.js | 20 ++- backend/routes/runs/download.js | 126 +++++++++++++ frontend/messages/en.json | 3 +- frontend/messages/ja.json | 3 +- .../folders/[folderId]/cases/CasesPane.tsx | 8 +- .../[folderId]/cases/TestCaseTable.tsx | 54 ++++-- .../[folderId]/cases/[caseId]/CaseEditor.tsx | 11 +- .../folders/[folderId]/cases/page.tsx | 2 +- .../projects/[projectId]/runs/RunsPage.tsx | 2 +- .../[projectId]/runs/[runId]/RunEditor.tsx | 85 +++++++-- .../[projectId]/runs/[runId]/page.tsx | 1 + .../projects/[projectId]/runs/page.tsx | 4 +- .../projects/[projectId]/runs/runsControl.ts | 34 ++++ frontend/types/case.ts | 2 +- frontend/types/run.ts | 1 + frontend/utils/caseControl.ts | 12 +- 19 files changed, 488 insertions(+), 55 deletions(-) create mode 100644 backend/routes/runs/download.js diff --git a/backend/index.js b/backend/index.js index 86d08bc..245d9b7 100644 --- a/backend/index.js +++ b/backend/index.js @@ -106,11 +106,13 @@ app.use('/attachments', attachmentsDeleteRoute); app.use('/attachments', attachmentsDownloadRoute); // "/runs" +const runsDownloadRoute = require('./routes/runs/download')(sequelize); const runsIndexRoute = require('./routes/runs/index')(sequelize); const runsShowRoute = require('./routes/runs/show')(sequelize); const runsNewRoute = require('./routes/runs/new')(sequelize); const runsEditRoute = require('./routes/runs/edit')(sequelize); const runDeleteRoute = require('./routes/runs/delete')(sequelize); +app.use('/runs', runsDownloadRoute); app.use('/runs', runsIndexRoute); app.use('/runs', runsShowRoute); app.use('/runs', runsNewRoute); diff --git a/backend/package-lock.json b/backend/package-lock.json index 930489a..0981383 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,7 +18,8 @@ "papaparse": "^5.5.2", "sequelize": "^6.37.5", "sequelize-cli": "^6.6.2", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "xmlbuilder2": "^3.1.1" } }, "node_modules/@gar/promisify": { @@ -219,6 +220,50 @@ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" }, + "node_modules/@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "dependencies": { + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", + "engines": { + "node": ">=8.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -396,6 +441,19 @@ "node": ">= 6" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -1087,6 +1145,18 @@ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1772,6 +1842,18 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -3436,6 +3518,20 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "dependencies": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -3637,6 +3733,38 @@ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" }, + "@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "requires": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + } + }, + "@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "requires": { + "@oozcitak/util": "8.3.8" + } + }, + "@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "requires": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + } + }, + "@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==" + }, "@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3777,6 +3905,21 @@ } } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + } + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -4323,6 +4466,11 @@ } } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4826,6 +4974,15 @@ } } }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -6049,6 +6206,17 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "requires": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + } + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index c61a9cd..b2b5881 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ "papaparse": "^5.5.2", "sequelize": "^6.37.5", "sequelize-cli": "^6.6.2", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "xmlbuilder2": "^3.1.1" } } diff --git a/backend/routes/cases/download.js b/backend/routes/cases/download.js index 9764ee8..147d9e7 100644 --- a/backend/routes/cases/download.js +++ b/backend/routes/cases/download.js @@ -30,14 +30,20 @@ module.exports = function (sequelize) { return res.status(404).send('No cases found'); } - const csv = Papa.unparse(cases, { - quotes: true, - skipEmptyLines: true, - }); + if (type === 'json') { + return res.json(cases); + } else if (type === 'csv') { + 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); + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', `attachment; filename=cases_folder_${folderId}.csv`); + return res.send(csv); + } + + return res.status(400).json({ error: 'Unsupported type. Use ?type=json or ?type=csv' }); } catch (error) { console.error(error); res.status(500).send('Internal Server Error'); diff --git a/backend/routes/runs/download.js b/backend/routes/runs/download.js new file mode 100644 index 0000000..fa52597 --- /dev/null +++ b/backend/routes/runs/download.js @@ -0,0 +1,126 @@ +const express = require('express'); +const router = express.Router(); +const Papa = require('papaparse'); +const { create } = require('xmlbuilder2'); + +const defineRun = require('../../models/runs'); +const defineRunCase = require('../../models/runCases'); +const defineCase = require('../../models/cases'); +const defineFolder = require('../../models/folders'); + +module.exports = function (sequelize) { + const { DataTypes } = require('sequelize'); + const { verifySignedIn } = require('../../middleware/auth')(sequelize); + const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize); + + const Run = defineRun(sequelize, DataTypes); + const RunCase = defineRunCase(sequelize, DataTypes); + const Case = defineCase(sequelize, DataTypes); + const Folder = defineFolder(sequelize, DataTypes); + + RunCase.belongsTo(Case, { foreignKey: 'caseId' }); + + router.get('/download/:runId', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => { + const { runId } = req.params; + const { type } = req.query; + + if (!runId) { + return res.status(400).json({ error: 'runId is required' }); + } + + try { + const run = await Run.findByPk(runId); + if (!run) { + return res.status(404).send('Run not found'); + } + + const runCases = await RunCase.findAll({ + where: { runId }, + include: [{ model: Case }], + }); + + if (type === 'xml') { + // JUnit xml valid status + const validStatuses = [1, 2, 4]; // 0: untested, 1: passed, 2 failed, 3: retest, 4: skipped + const filteredRunCases = runCases.filter((rc) => validStatuses.includes(rc.status)); + + // group cases by folder + const folderMap = new Map(); + for (const rc of filteredRunCases) { + const folderId = rc.Case.folderId; + if (!folderMap.has(folderId)) { + folderMap.set(folderId, []); + } + folderMap.get(folderId).push(rc); + } + + // Construct JUnit xml + const xml = create({ version: '1.0' }); + const root = xml.ele('testsuites'); + + for (const [folderId, cases] of folderMap.entries()) { + let folderName = ''; + const folder = await Folder.findByPk(folderId); + if (folder) { + folderName = folder.name; + } + + const suite = root.ele('testsuite', { + name: folderName, + tests: cases.length, + failures: cases.filter((c) => c.status === 2).length, + skipped: cases.filter((c) => c.status === 4).length, + }); + + for (const rc of cases) { + const testCase = suite.ele('testcase', { + name: rc.Case.title, + classname: folderName, + time: '0', + }); + + if (rc.status === 2) { + testCase.ele('failure', { message: 'Test failed' }).txt('Test case failed.'); + } else if (rc.status === 4) { + testCase.ele('skipped', { message: 'skipped' }); + } + } + } + + const xmlString = xml.end({ prettyPrint: true }); + + res.setHeader('Content-Type', 'application/xml'); + res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.xml`); + return res.send(xmlString); + } else if (type === 'json') { + return res.json(runCases); + } else if (type === 'csv') { + const records = runCases.map((rc) => ({ + id: rc.Case.id, + title: rc.Case.title, + state: rc.Case.state, + priority: rc.Case.priority, + type: rc.Case.type, + automationStatus: rc.Case.automationStatus, + status: rc.status, + })); + + const csv = Papa.unparse(records, { + quotes: true, + skipEmptyLines: true, + }); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', `attachment; filename=run_${runId}.csv`); + return res.send(csv); + } + + return res.status(400).json({ error: 'Unsupported type. Use ?type=xml or ?type=json or ?type=csv' }); + } catch (error) { + console.error(error); + res.status(500).send('Internal Server Error'); + } + }); + + return router; +}; diff --git a/frontend/messages/en.json b/frontend/messages/en.json index eace95a..34af3c9 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -188,7 +188,7 @@ "close": "Close", "are_you_sure": "Are you sure you want to delete test cases?", "new_test_case": "New Test Case", - "download_csv": "Download CSV", + "export": "Export", "status": "Status", "no_cases_found": "No test cases found", "case_title": "Test Case Title", @@ -254,6 +254,7 @@ "updating": "Updating...", "update": "Update", "updated_test_run": "Updated test run", + "export": "Export", "progress": "Progress", "refresh": "Refresh", "id": "ID", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 664ac9b..ccd1632 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -189,7 +189,7 @@ "close": "閉じる", "are_you_sure": "テストケースを削除してもよろしいですか?", "new_test_case": "新規テストケース", - "download_csv": "CSVダウンロード", + "export": "エクスポート", "status": "ステータス", "no_cases_found": "テストケースがありません", "case_title": "テストケースタイトル", @@ -255,6 +255,7 @@ "updating": "更新中...", "update": "更新", "updated_test_run": "テストランを更新しました", + "export": "エクスポート", "progress": "進捗", "refresh": "再読み込み", "id": "ID", diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx index 28b355e..26558cf 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useContext } from 'react'; import TestCaseTable from './TestCaseTable'; import CaseDialog from './CaseDialog'; import { TokenContext } from '@/utils/TokenProvider'; -import { fetchCases, createCase, deleteCases, csvDownload } from '@/utils/caseControl'; +import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseControl'; import { CaseType, CasesMessages } from '@/types/case'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import { PriorityMessages } from '@/types/priority'; @@ -74,8 +74,8 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa } }; - const onCsvDownload = async () => { - await csvDownload(context.token.access_token, Number(folderId)); + const onExportCases = async (type: string) => { + await exportCases(context.token.access_token, Number(folderId), type); }; return ( @@ -87,7 +87,7 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa onCreateCase={() => setIsCaseDialogOpen(true)} onDeleteCase={onDeleteCase} onDeleteCases={onDeleteCases} - onCsvDownload={onCsvDownload} + onExportCases={onExportCases} messages={messages} priorityMessages={priorityMessages} locale={locale} diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx index 0d63500..cd5242e 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx @@ -13,8 +13,9 @@ import { DropdownItem, Selection, SortDescriptor, + ButtonGroup, } from '@heroui/react'; -import { Plus, MoreVertical, Trash, Download } from 'lucide-react'; +import { Plus, MoreVertical, Trash, Download, FileDown, ChevronDown, FileJson, FileSpreadsheet } from 'lucide-react'; import { Link } from '@/src/i18n/routing'; import { CaseType, CasesMessages } from '@/types/case'; import { PriorityMessages } from '@/types/priority'; @@ -28,7 +29,7 @@ type Props = { onCreateCase: () => void; onDeleteCase: (caseId: number) => void; onDeleteCases: (caseIds: number[]) => void; - onCsvDownload: () => void; + onExportCases: (type: string) => void; messages: CasesMessages; priorityMessages: PriorityMessages; locale: LocaleCodeType; @@ -41,7 +42,7 @@ export default function TestCaseTable({ onCreateCase, onDeleteCase, onDeleteCases, - onCsvDownload, + onExportCases, messages, priorityMessages, locale, @@ -58,6 +59,7 @@ export default function TestCaseTable({ column: 'id', direction: 'ascending', }); + const [exportType, setExportType] = useState(new Set(['json'])); const sortedItems = useMemo(() => { if (cases.length === 0) { @@ -133,6 +135,10 @@ export default function TestCaseTable({ setSelectedKeys(new Set([])); }; + const handleExportTypeChange = (keys: Selection) => { + setExportType(new Set(Array.from(keys as Set))); + }; + const classNames = useMemo( () => ({ wrapper: ['max-w-3xl'], @@ -170,16 +176,38 @@ export default function TestCaseTable({ {messages.delete} )} - + + + + + + + + }> + json + + }> + csv + + + + + + + + + + }> + xml + + }> + json + + }> + csv + + + +