From 6f562f3532f5fdbd612eacc161e0dd60eb227499 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Sun, 7 Jul 2024 22:45:40 +0900 Subject: [PATCH] update runCase at once --- backend/index.js | 9 +- backend/routes/runcases/bulkDelete.js | 47 ------- backend/routes/runcases/bulkNew.js | 49 ------- backend/routes/runcases/delete.js | 37 ------ backend/routes/runcases/edit.js | 75 ++++++++--- backend/routes/runcases/new.js | 42 ------ frontend/messages/en.json | 3 +- frontend/messages/ja.json | 3 +- .../[projectId]/runs/[runId]/RunEditor.tsx | 111 ++++++++++------ .../[projectId]/runs/[runId]/page.tsx | 1 + .../projects/[projectId]/runs/runsControl.ts | 121 ++---------------- frontend/types/run.ts | 9 +- 12 files changed, 143 insertions(+), 364 deletions(-) delete mode 100644 backend/routes/runcases/bulkDelete.js delete mode 100644 backend/routes/runcases/bulkNew.js delete mode 100644 backend/routes/runcases/delete.js delete mode 100644 backend/routes/runcases/new.js diff --git a/backend/index.js b/backend/index.js index dff3fd8..b1427f9 100644 --- a/backend/index.js +++ b/backend/index.js @@ -22,6 +22,7 @@ app.use(express.static(path.join(__dirname, 'public'))); const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'database/database.sqlite', + logging: false, }); // "/" @@ -98,17 +99,9 @@ app.use('/runs', runDeleteRoute); // "/runcases" const runCaseIndexRoute = require('./routes/runcases/index')(sequelize); -const runCaseNewRoute = require('./routes/runcases/new')(sequelize); const runCaseEditRoute = require('./routes/runcases/edit')(sequelize); -const runCaseBuldNewRoute = require('./routes/runcases/bulkNew')(sequelize); -const runCaseDeleteRoute = require('./routes/runcases/delete')(sequelize); -const runCaseBulkDeleteRoute = require('./routes/runcases/bulkDelete')(sequelize); app.use('/runcases', runCaseIndexRoute); -app.use('/runcases', runCaseNewRoute); app.use('/runcases', runCaseEditRoute); -app.use('/runcases', runCaseBuldNewRoute); -app.use('/runcases', runCaseDeleteRoute); -app.use('/runcases', runCaseBulkDeleteRoute); // "/members" const membersIndexRoute = require('./routes/members/index')(sequelize); diff --git a/backend/routes/runcases/bulkDelete.js b/backend/routes/runcases/bulkDelete.js deleted file mode 100644 index 62bad38..0000000 --- a/backend/routes/runcases/bulkDelete.js +++ /dev/null @@ -1,47 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const defineRunCase = require('../../models/runCases'); -const { DataTypes, Op } = require('sequelize'); - -module.exports = function (sequelize) { - const RunCase = defineRunCase(sequelize, DataTypes); - const { verifySignedIn } = require('../../middleware/auth')(sequelize); - const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); - - router.post('/bulkdelete', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { - const recordsToDelete = req.body; - - // TODO Instead of receiving a combination of runId and caseId from frontend, - // receives only an array of caseId and constructs a pair from the query parameter runId - try { - const existingRunCases = await RunCase.findAll({ - where: { - [Op.or]: recordsToDelete.map((condition) => ({ - runId: condition.runId, - caseId: condition.caseId, - })), - }, - }); - - if (existingRunCases.length === 0) { - return res.status(200).send('No records found to delete'); - } - - await RunCase.destroy({ - where: { - [Op.or]: recordsToDelete.map((condition) => ({ - runId: condition.runId, - caseId: condition.caseId, - })), - }, - }); - - res.status(200).send('Records deleted successfully'); - } catch (error) { - console.error('Error deleting run cases:', error); - res.status(500).send('Internal Server Error'); - } - }); - - return router; -}; diff --git a/backend/routes/runcases/bulkNew.js b/backend/routes/runcases/bulkNew.js deleted file mode 100644 index e32b308..0000000 --- a/backend/routes/runcases/bulkNew.js +++ /dev/null @@ -1,49 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const defineRunCase = require('../../models/runCases'); -const { DataTypes, Op } = require('sequelize'); - -module.exports = function (sequelize) { - const { verifySignedIn } = require('../../middleware/auth')(sequelize); - const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); - const RunCase = defineRunCase(sequelize, DataTypes); - - router.post('/bulknew', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { - const recordsToCreate = req.body; - - // TODO Instead of receiving a combination of runId and caseId from frontend, - // receives only an array of caseId and constructs a pair from the query parameter runId - try { - const existingRunCases = await RunCase.findAll({ - where: { - [Op.or]: recordsToCreate.map((record) => ({ - runId: record.runId, - caseId: record.caseId, - })), - }, - }); - - // Filter out records that already exist - const recordsToCreateFiltered = recordsToCreate.filter((record) => { - return !existingRunCases.some( - (existingRecord) => existingRecord.runId == record.runId && existingRecord.caseId == record.caseId - ); - }); - - const newRunCases = await RunCase.bulkCreate( - recordsToCreateFiltered.map((record) => ({ - runId: record.runId, - caseId: record.caseId, - status: 0, - })) - ); - - res.json(newRunCases); - } catch (error) { - console.error(error); - res.status(500).send('Internal Server Error'); - } - }); - - return router; -}; diff --git a/backend/routes/runcases/delete.js b/backend/routes/runcases/delete.js deleted file mode 100644 index c7029c8..0000000 --- a/backend/routes/runcases/delete.js +++ /dev/null @@ -1,37 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const defineRunCase = require('../../models/runCases'); -const { DataTypes } = require('sequelize'); - -module.exports = function (sequelize) { - const { verifySignedIn } = require('../../middleware/auth')(sequelize); - const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); - const RunCase = defineRunCase(sequelize, DataTypes); - - router.delete('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { - const runId = req.query.runId; - const caseId = req.query.caseId; - - try { - // Get RunCase to be deleted. - const deletingRunCase = await RunCase.findOne({ - where: { - runId: runId, - caseId: caseId, - }, - }); - - if (!deletingRunCase) { - return res.status(404).send('RunCase not found'); - } - - await deletingRunCase.destroy(); - res.status(204).send(); - } catch (error) { - console.error(error); - res.status(500).send('Internal Server Error'); - } - }); - - return router; -}; diff --git a/backend/routes/runcases/edit.js b/backend/routes/runcases/edit.js index 5f14ca6..62cb791 100644 --- a/backend/routes/runcases/edit.js +++ b/backend/routes/runcases/edit.js @@ -8,31 +8,68 @@ module.exports = function (sequelize) { const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const RunCase = defineRunCase(sequelize, DataTypes); - router.put('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { + router.post('/update', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { const runId = req.query.runId; - const caseId = req.query.caseId; - const status = req.query.status; + const runCases = req.body; + const t = await sequelize.transaction(); + + console.log('############## start edit'); + console.log(runCases); + console.log('############## end edit'); + + const createRunCase = async (runCase) => { + const newRunCase = await RunCase.create( + { + runId: runId, + caseId: runCase.caseId, + status: 0, + }, + { transaction: t } + ); + return newRunCase; + }; + + const deleteRunCase = async (runCase) => { + await RunCase.destroy({ + where: { runId: runId, caseId: runCase.caseId }, + transaction: t, + }); + return null; + }; + + const updateRunCase = async (runCase) => { + await RunCase.update( + { + status: runCase.status, + }, + { + where: { id: runCase.id }, + transaction: t, + } + ); + return step; + }; try { - const runCase = await RunCase.findOne({ - where: { - runId: runId, - caseId: caseId, - }, - }); + const results = await Promise.all( + runCases.map(async (step) => { + if (step.editState === 'new') { + return createRunCase(step); + } else if (step.editState === 'deleted') { + return deleteRunCase(step); + } else if (step.editState === 'changed') { + return updateRunCase(step); + } else if (step.editState === 'notChanged') { + return step; + } + }) + ); - if (!runCase) { - return res.status(404).send('Runcase not found'); - } - - await runCase.update({ - runId, - caseId, - status, - }); - res.json(runCase); + await t.commit(); + res.json(results.filter((result) => result !== null)); } catch (error) { console.error(error); + await t.rollback(); res.status(500).send('Internal Server Error'); } }); diff --git a/backend/routes/runcases/new.js b/backend/routes/runcases/new.js deleted file mode 100644 index 161854f..0000000 --- a/backend/routes/runcases/new.js +++ /dev/null @@ -1,42 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const defineRunCase = require('../../models/runCases'); -const { DataTypes } = require('sequelize'); - -module.exports = function (sequelize) { - const { verifySignedIn } = require('../../middleware/auth')(sequelize); - const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); - const RunCase = defineRunCase(sequelize, DataTypes); - - router.post('/', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { - const runId = req.query.runId; - const caseId = req.query.caseId; - - try { - // Check if the record already exists - const existingRunCase = await RunCase.findOne({ - where: { - runId: runId, - caseId: caseId, - }, - }); - - if (existingRunCase) { - return res.status(400).send('Record already exists'); - } - - const newRunCase = await RunCase.create({ - runId: runId, - caseId: caseId, - status: 0, - }); - - res.json(newRunCase); - } 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 486c725..3744eac 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -277,7 +277,8 @@ "test_case_selection": "Test case selection", "include_in_run": "Include in run", "exclude_from_run": "Exclude from run", - "no_cases_found": "No cases found" + "no_cases_found": "No cases found", + "are_you_sure_leave": "Are you sure you want to leave the page?" }, "Members": { "member_management": "Member Management", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index d906fda..fc4fb75 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -173,7 +173,8 @@ "case_title": "テストケースタイトル", "case_description": "テストケース詳細", "create": "作成", - "please_rnter": "テストケースタイトルを入力してください" + "please_rnter": "テストケースタイトルを入力してください", + "are_you_sure_leave": "ページを離れてもよろしいですか" }, "Case": { "back_to_cases": "テストケース一覧に戻る", diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx index 7e4ed60..70b4ce8 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/RunEditor.tsx @@ -22,23 +22,15 @@ import { Save, ArrowLeft, Folder, ChevronDown, CopyPlus, CopyMinus, RotateCw } f import RunProgressChart from './RunPregressDonutChart'; import TestCaseSelector from './TestCaseSelector'; import { testRunStatus } from '@/config/selection'; -import { RunType, RunCaseType, RunCaseInfoType, RunStatusCountType, RunMessages } from '@/types/run'; +import { RunType, RunCaseType, RunStatusCountType, RunMessages } from '@/types/run'; import { CaseType } from '@/types/case'; import { FolderType } from '@/types/folder'; -import { - fetchRun, - updateRun, - fetchRunCases, - createRunCase, - updateRunCase, - bulkCreateRunCases, - deleteRunCase, - bulkDeleteRunCases, -} from '../runsControl'; +import { fetchRun, updateRun, fetchRunCases, updateRunCases } from '../runsControl'; import { fetchFolders } from '../../folders/foldersControl'; import { fetchCases } from '@/utils/caseControl'; import { TokenContext } from '@/utils/TokenProvider'; import { useTheme } from 'next-themes'; +import { useFormGuard } from '@/utils/formGuard'; const defaultTestRun = { id: 0, @@ -70,7 +62,9 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) const [testcases, setTestCases] = useState([]); const [isNameInvalid, setIsNameInvalid] = useState(false); const [isUpdating, setIsUpdating] = useState(false); + const [isDirty, setIsDirty] = useState(false); const router = useRouter(); + useFormGuard(isDirty, messages.areYouSureLeave); const fetchRunAndStatusCount = async () => { const { run, statusCounts } = await fetchRun(context.token.access_token, Number(runId)); @@ -102,6 +96,9 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) if (selectedFolder && selectedFolder.id) { try { const latestRunCases: RunCaseType[] = await fetchRunCases(context.token.access_token, Number(runId)); + latestRunCases.forEach((runCase: RunCaseType) => { + runCase.editState = 'notChanged'; + }); setRunCases(latestRunCases); const testCasesData: CaseType[] = await fetchCases(context.token.access_token, selectedFolder.id); @@ -130,11 +127,11 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) }, [selectedFolder]); const handleChangeStatus = async (changeCaseId: number, status: number) => { - await updateRunCase(context.token.access_token, Number(runId), changeCaseId, status); + setIsDirty(true); setTestCases((prevTestCases) => { return prevTestCases.map((testCase) => { if (testCase.id === changeCaseId) { - return { ...testCase, runStatus: status }; + return { ...testCase, runStatus: status, editState: 'changed' }; } return testCase; }); @@ -142,26 +139,28 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) }; const handleIncludeExcludeCase = async (isInclude: boolean, clickedTestCaseId: number) => { + setIsDirty(true); if (isInclude) { - const createdRunCase = await createRunCase(context.token.access_token, Number(runId), clickedTestCaseId); - setRunCases((prevRunCases) => { - return [...prevRunCases, createdRunCase]; - }); - } else { - await deleteRunCase(context.token.access_token, Number(runId), clickedTestCaseId); - setRunCases((prevRunCases) => { - return prevRunCases.filter((runCase) => runCase.caseId !== clickedTestCaseId); - }); - } + const newRunCase: RunCaseType = { + id: 0, + runId: Number(runId), + caseId: clickedTestCaseId, + status: 0, + editState: 'new', + }; - setTestCases((prevTestCases) => { - return prevTestCases.map((testCase) => { - if (testCase.id === clickedTestCaseId) { - return { ...testCase, isIncluded: isInclude }; - } - return testCase; - }); - }); + setRunCases([...runCases, newRunCase]); + } else { + const deleteRunCase = runCases.find((runCase) => runCase.caseId === clickedTestCaseId); + if (!deleteRunCase) { + return; + } + deleteRunCase.editState = 'deleted'; + + setRunCases((prevRunCases) => + prevRunCases.map((runCase) => (runCase.caseId === deleteRunCase.runId ? deleteRunCase : runCase)) + ); + } }; const handleBulkIncludeExcludeCases = async (isInclude: boolean) => { @@ -172,20 +171,49 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) keys = Array.from(selectedKeys).map(Number); } - const runCaseInfo: RunCaseInfoType[] = keys.map((caseId) => ({ - runId: Number(runId), - caseId: caseId, - })); if (isInclude) { - const createdRunCases = await bulkCreateRunCases(context.token.access_token, Number(runId), runCaseInfo); - setRunCases((prevRunCases) => [...prevRunCases, ...createdRunCases]); - } else { - await bulkDeleteRunCases(context.token.access_token, Number(runId), runCaseInfo); + // TODO fix and add unit test setRunCases((prevRunCases) => { - return prevRunCases.filter((runCase) => { - return !runCaseInfo.some((info) => info.caseId === runCase.caseId); + const updatedRunCases = prevRunCases.map((runCase) => { + if (keys.includes(runCase.caseId) && runCase.editState === 'deleted') { + return { ...runCase, editState: 'changed' }; + } + return runCase; }); + + keys.forEach((caseId) => { + const existingRunCase = prevRunCases.find((runCase) => runCase.caseId === caseId); + if (!existingRunCase) { + updatedRunCases.push({ + id: -1, + runId: Number(runId), + caseId: caseId, + status: -1, + editState: 'new', + }); + } + }); + + return updatedRunCases; }); + } else { + setRunCases((prevRunCases) => + prevRunCases + .filter((runCase) => { + // If editState is 'new', remove from the array + if (keys.includes(runCase.caseId) && runCase.editState === 'new') { + return false; + } + return true; + }) + .map((runCase) => { + // If editState isn't 'new', set editState to 'deleted'. + if (keys.includes(runCase.caseId) && runCase.editState !== 'new') { + return { ...runCase, editState: 'deleted' }; + } + return runCase; + }) + ); } const updatedTestCases = testcases.map((testcase) => { @@ -227,6 +255,7 @@ export default function RunEditor({ projectId, runId, messages, locale }: Props) onPress={async () => { setIsUpdating(true); await updateRun(context.token.access_token, testRun); + await updateRunCases(context.token.access_token, Number(runId), runCases); setIsUpdating(false); }} > diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/page.tsx index 2151895..f53a060 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/[runId]/page.tsx @@ -36,6 +36,7 @@ export default function Page({ params }: { params: { projectId: string; runId: s includeInRun: t('include_in_run'), excludeFromRun: t('exclude_from_run'), noCasesFound: t('no_cases_found'), + areYouSureLeave: t('are_you_sure_leave'), }; return ; diff --git a/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts b/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts index 811f5f4..857ce94 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts +++ b/frontend/src/app/[locale]/projects/[projectId]/runs/runsControl.ts @@ -1,6 +1,6 @@ import Config from '@/config/config'; const apiServer = Config.apiServer; -import { RunType, RunCaseInfoType } from '@/types/run'; +import { RunType, RunCaseType } from '@/types/run'; async function fetchRun(jwt: string, runId: number) { const url = `${apiServer}/runs/${runId}`; @@ -150,134 +150,29 @@ async function fetchRunCases(jwt: string, runId: number) { } } -async function createRunCase(jwt: string, runId: number, caseId: number) { +async function updateRunCases(jwt: string, runId: number, runCases: RunCaseType[]) { const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}`, }, + body: JSON.stringify(runCases), }; - const url = `${apiServer}/runcases?runId=${runId}&caseId=${caseId}`; + console.log(runCases); + const url = `${apiServer}/runcases/update?runId=${runId}`; try { const response = await fetch(url, fetchOptions); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } - const data = await response.json(); - return data; + return await response.json(); } catch (error: any) { - console.error('Error creating new runcase:', error); + console.error('Error deleting project:', error); throw error; } } -async function updateRunCase(jwt: string, runId: number, caseId: number, status: number) { - const fetchOptions = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwt}`, - }, - }; - - const url = `${apiServer}/runcases?runId=${runId}&caseId=${caseId}&status=${status}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error: any) { - console.error('Error updating runcase:', error); - throw error; - } -} - -async function bulkCreateRunCases(jwt: string, runId: number, runCaseInfo: RunCaseInfoType[]) { - const fetchOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwt}`, - }, - body: JSON.stringify(runCaseInfo), - }; - - const url = `${apiServer}/runcases/bulknew?runId=${runId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error: any) { - console.error('Error creating new runcase:', error); - throw error; - } -} - -async function deleteRunCase(jwt: string, runId: number, caseId: number) { - const fetchOptions = { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwt}`, - }, - }; - - const url = `${apiServer}/runcases?runId=${runId}&caseId=${caseId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - } catch (error: any) { - console.error('Error deleting runcase:', error); - throw error; - } -} - -async function bulkDeleteRunCases(jwt: string, runId: number, runCaseInfo: RunCaseInfoType[]) { - const fetchOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${jwt}`, - }, - body: JSON.stringify(runCaseInfo), - }; - - const url = `${apiServer}/runcases/bulkdelete?runId=${runId}`; - - try { - const response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - } catch (error: any) { - console.error('Error deleting runcase:', error); - throw error; - } -} - -export { - fetchRun, - fetchRuns, - createRun, - updateRun, - deleteRun, - fetchRunCases, - createRunCase, - updateRunCase, - bulkCreateRunCases, - deleteRunCase, - bulkDeleteRunCases, -}; +export { fetchRun, fetchRuns, createRun, updateRun, deleteRun, fetchRunCases, updateRunCases }; diff --git a/frontend/types/run.ts b/frontend/types/run.ts index 40b8f91..15285c7 100644 --- a/frontend/types/run.ts +++ b/frontend/types/run.ts @@ -14,11 +14,7 @@ type RunCaseType = { runId: number; caseId: number; status: number; -}; - -type RunCaseInfoType = { - runId: number; - caseId: number; + editState: 'notChanged' | 'changed' | 'new' | 'deleted'; }; type RunStatusCountType = { @@ -86,6 +82,7 @@ type RunMessages = { includeInRun: string; excludeFromRun: string; noCasesFound: string; + areYouSureLeave: string; }; -export { RunType, RunCaseType, RunCaseInfoType, RunStatusCountType, ProgressSeriesType, RunsMessages, RunMessages }; +export type { RunType, RunCaseType, RunStatusCountType, ProgressSeriesType, RunsMessages, RunMessages };