From de9e26a241a096052bc44c8b7389d873f6ef0c57 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:38:09 +0900 Subject: [PATCH 1/6] fix: button links not responding when touched on a mobile device (#217) --- frontend/components/ClientLink.tsx | 10 ++++ .../src/app/[locale]/HeaderNavbarMenu.tsx | 7 +-- frontend/src/app/[locale]/PaneMainTitle.tsx | 60 ++++++++++--------- .../src/app/[locale]/account/AccountPage.tsx | 15 +++-- .../src/app/[locale]/account/authPage.tsx | 15 +++-- .../[folderId]/cases/TestCaseTable.tsx | 19 +++--- 6 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 frontend/components/ClientLink.tsx diff --git a/frontend/components/ClientLink.tsx b/frontend/components/ClientLink.tsx new file mode 100644 index 0000000..83db74d --- /dev/null +++ b/frontend/components/ClientLink.tsx @@ -0,0 +1,10 @@ +'use client'; +import { forwardRef } from 'react'; +import { Link } from '@/src/i18n/routing'; +type Props = React.ComponentPropsWithoutRef; +const ClientLink = forwardRef(({ href, ...props }, ref) => ( + +)); +ClientLink.displayName = 'ClientLink'; + +export default ClientLink; diff --git a/frontend/src/app/[locale]/HeaderNavbarMenu.tsx b/frontend/src/app/[locale]/HeaderNavbarMenu.tsx index 40e7bb2..1091d9e 100644 --- a/frontend/src/app/[locale]/HeaderNavbarMenu.tsx +++ b/frontend/src/app/[locale]/HeaderNavbarMenu.tsx @@ -101,12 +101,7 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) { {commonLinks.map((link) => link.isExternal ? ( - } - > + {link.label} diff --git a/frontend/src/app/[locale]/PaneMainTitle.tsx b/frontend/src/app/[locale]/PaneMainTitle.tsx index c2865b8..ec1e178 100644 --- a/frontend/src/app/[locale]/PaneMainTitle.tsx +++ b/frontend/src/app/[locale]/PaneMainTitle.tsx @@ -1,9 +1,9 @@ -import { title, subtitle } from '@/components/primitives'; import { Button, Link as NextUiLink } from '@heroui/react'; import { MoveUpRight } from 'lucide-react'; -import { Link } from '@/src/i18n/routing'; import { useTranslations } from 'next-intl'; +import { title, subtitle } from '@/components/primitives'; import { LocaleCodeType } from '@/types/locale'; +import ClientLink from '@/components/ClientLink'; type Props = { locale: LocaleCodeType; @@ -34,35 +34,37 @@ export default function MainTitle({ locale }: Props) {

{t('integrate_and_manage')}

- - - + - - - + - - - +
); diff --git a/frontend/src/app/[locale]/account/AccountPage.tsx b/frontend/src/app/[locale]/account/AccountPage.tsx index dd9a580..06e7d35 100644 --- a/frontend/src/app/[locale]/account/AccountPage.tsx +++ b/frontend/src/app/[locale]/account/AccountPage.tsx @@ -86,11 +86,16 @@ export default function AccountPage({ messages, locale }: Props) { ) : ( <> {messages.notOwnAnyProjects} - - - + )} diff --git a/frontend/src/app/[locale]/account/authPage.tsx b/frontend/src/app/[locale]/account/authPage.tsx index 6ff9cec..34a8df5 100644 --- a/frontend/src/app/[locale]/account/authPage.tsx +++ b/frontend/src/app/[locale]/account/authPage.tsx @@ -97,11 +97,16 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {

{messages.title}

- - - +
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 e22040e..d776422 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 @@ -14,8 +14,8 @@ import { Selection, SortDescriptor, } from '@heroui/react'; -import { Link, NextUiLinkClasses } from '@/src/i18n/routing'; import { Plus, MoreVertical, Trash } from 'lucide-react'; +import { Link } from '@/src/i18n/routing'; import { CaseType, CasesMessages } from '@/types/case'; import { PriorityMessages } from '@/types/priority'; import TestCasePriority from '@/components/TestCasePriority'; @@ -82,14 +82,15 @@ export default function TestCaseTable({ return {cellValue as number}; case 'title': return ( - ); case 'priority': From a05b39604a5783c9a8f0cdceebf579befb1bffee Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Thu, 1 May 2025 22:04:11 +0900 Subject: [PATCH 2/6] chore: update gitignore (#218) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 872c5fa..ff18e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage/ /playwright-screenshots/ /blob-report/ /playwright/.cache/ +.DS_Store From c2ef8d4c7bfecd947595bbb1972745275a6d92f4 Mon Sep 17 00:00:00 2001 From: kimatata <117462761+kimatata@users.noreply.github.com> Date: Tue, 6 May 2025 10:05:37 +0900 Subject: [PATCH 3/6] feat: download test cases csv (#219) --- backend/index.js | 2 + backend/package-lock.json | 11 +++++ backend/package.json | 1 + backend/routes/cases/download.js | 48 +++++++++++++++++++ frontend/messages/en.json | 1 + frontend/messages/ja.json | 1 + .../folders/[folderId]/cases/CasesPane.tsx | 11 +++-- .../[folderId]/cases/TestCaseTable.tsx | 15 +++++- .../folders/[folderId]/cases/page.tsx | 5 +- frontend/types/case.ts | 1 + frontend/utils/caseControl.ts | 31 +++++++++++- 11 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 backend/routes/cases/download.js diff --git a/backend/index.js b/backend/index.js index e05e2de..86d08bc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -78,12 +78,14 @@ app.use('/folders', foldersEditRoute); app.use('/folders', foldersDeleteRoute); // "/cases" +const casesDownloadRoute = require('./routes/cases/download')(sequelize); const casesIndexRoute = require('./routes/cases/index')(sequelize); const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize); const casesShowRoute = require('./routes/cases/show')(sequelize); const casesNewRoute = require('./routes/cases/new')(sequelize); const casesEditRoute = require('./routes/cases/edit')(sequelize); const casesDeleteRoute = require('./routes/cases/delete')(sequelize); +app.use('/cases', casesDownloadRoute); app.use('/cases', casesIndexRoute); app.use('/cases', casesIndexByProjectIdRoute); app.use('/cases', casesShowRoute); diff --git a/backend/package-lock.json b/backend/package-lock.json index 64215a5..930489a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "express-rate-limit": "^7.4.1", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", + "papaparse": "^5.5.2", "sequelize": "^6.37.5", "sequelize-cli": "^6.6.2", "sqlite3": "^5.1.7" @@ -2358,6 +2359,11 @@ "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": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5265,6 +5271,11 @@ "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": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/backend/package.json b/backend/package.json index b374248..c61a9cd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "express-rate-limit": "^7.4.1", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", + "papaparse": "^5.5.2", "sequelize": "^6.37.5", "sequelize-cli": "^6.6.2", "sqlite3": "^5.1.7" diff --git a/backend/routes/cases/download.js b/backend/routes/cases/download.js new file mode 100644 index 0000000..9764ee8 --- /dev/null +++ b/backend/routes/cases/download.js @@ -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; +}; diff --git a/frontend/messages/en.json b/frontend/messages/en.json index d0021d3..eace95a 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -188,6 +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", "status": "Status", "no_cases_found": "No test cases found", "case_title": "Test Case Title", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index dabd405..664ac9b 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -189,6 +189,7 @@ "close": "閉じる", "are_you_sure": "テストケースを削除してもよろしいですか?", "new_test_case": "新規テストケース", + "download_csv": "CSVダウンロード", "status": "ステータス", "no_cases_found": "テストケースがありません", "case_title": "テストケースタイトル", 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 c0ea790..28b355e 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 @@ -1,11 +1,11 @@ 'use client'; import { useState, useEffect, useContext } from 'react'; -import { TokenContext } from '@/utils/TokenProvider'; 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 DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; -import CaseDialog from './CaseDialog'; import { PriorityMessages } from '@/types/priority'; 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 ( <> setIsCaseDialogOpen(true)} onDeleteCase={onDeleteCase} onDeleteCases={onDeleteCases} + onCsvDownload={onCsvDownload} 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 d776422..0d63500 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 @@ -14,7 +14,7 @@ import { Selection, SortDescriptor, } 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 { CaseType, CasesMessages } from '@/types/case'; import { PriorityMessages } from '@/types/priority'; @@ -28,6 +28,7 @@ type Props = { onCreateCase: () => void; onDeleteCase: (caseId: number) => void; onDeleteCases: (caseIds: number[]) => void; + onCsvDownload: () => void; messages: CasesMessages; priorityMessages: PriorityMessages; locale: LocaleCodeType; @@ -40,6 +41,7 @@ export default function TestCaseTable({ onCreateCase, onDeleteCase, onDeleteCases, + onCsvDownload, messages, priorityMessages, locale, @@ -154,6 +156,7 @@ export default function TestCaseTable({ <>

{messages.testCaseList}

+
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && ( )} - + + + + + + + + }> + json + + }> + csv + + + +
- {isDirty && } + + + + + + }> + xml + + }> + json + + }> + csv + + + +