feat: implement bulk step update

This commit is contained in:
Takeshi Kimata
2024-06-22 16:54:08 +09:00
parent a4f81c282a
commit f86b2a38cb
8 changed files with 167 additions and 197 deletions

View File

@@ -73,10 +73,8 @@ app.use('/cases', casesEditRoute);
app.use('/cases', casesDeleteRoute); app.use('/cases', casesDeleteRoute);
// "/steps" // "/steps"
const stepsNewRoute = require('./routes/steps/new')(sequelize); const stepsEditRoute = require('./routes/steps/edit')(sequelize);
const stepsDeleteRoute = require('./routes/steps/delete')(sequelize); app.use('/steps', stepsEditRoute);
app.use('/steps', stepsNewRoute);
app.use('/steps', stepsDeleteRoute);
// "/attachments" // "/attachments"
const attachmentsNewRoute = require('./routes/attachments/new')(sequelize); const attachmentsNewRoute = require('./routes/attachments/new')(sequelize);

View File

@@ -1,60 +0,0 @@
const express = require('express');
const router = express.Router();
const defineStep = require('../../models/steps');
const defineCaseStep = require('../../models/caseSteps');
const { DataTypes, Op } = require('sequelize');
module.exports = function (sequelize) {
const Step = defineStep(sequelize, DataTypes);
const CaseStep = defineCaseStep(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize);
router.delete('/:stepId', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
const stepId = req.params.stepId;
const caseId = req.query.caseId;
const t = await sequelize.transaction();
try {
const step = await Step.findByPk(stepId);
if (!step) {
await t.rollback();
return res.status(404).send('Step not found');
}
// Get caseStep to be deleted.
const deletingCaseStep = await CaseStep.findOne({
where: {
StepId: stepId,
},
transaction: t,
});
// Decrease stepNo for all caseSteps with greater than the caseStep to be deleted.
await CaseStep.update(
{ stepNo: sequelize.literal('stepNo - 1') },
{
where: {
CaseId: caseId,
stepNo: {
[Op.gt]: deletingCaseStep.stepNo,
},
},
transaction: t,
}
);
await step.destroy({ transaction: t });
await t.commit();
res.status(204).send();
} catch (error) {
console.error(error);
await t.rollback();
res.status(500).send('Internal Server Error');
}
});
return router;
};

View File

@@ -0,0 +1,90 @@
const express = require('express');
const router = express.Router();
const defineStep = require('../../models/steps');
const defineCaseStep = require('../../models/caseSteps');
const { DataTypes } = require('sequelize');
module.exports = function (sequelize) {
const Step = defineStep(sequelize, DataTypes);
const CaseStep = defineCaseStep(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize);
router.post('/update', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
const caseId = req.query.caseId;
const steps = req.body;
const t = await sequelize.transaction();
const createStep = async (step) => {
const newStep = await Step.create(
{
step: step.step,
result: step.result,
},
{ transaction: t }
);
await CaseStep.create(
{
caseId: caseId,
stepId: newStep.id,
stepNo: step.caseSteps.stepNo,
},
{ transaction: t }
);
return newStep;
};
const deleteStep = async (step) => {
await CaseStep.destroy({
where: { stepId: step.id },
transaction: t,
});
await Step.destroy({
where: { id: step.id },
transaction: t,
});
return null;
};
const updateStep = async (step) => {
await Step.update(step, {
where: { id: step.id },
transaction: t,
});
await CaseStep.update(
{
stepNo: step.caseSteps.stepNo,
},
{
where: { stepId: step.id },
transaction: t,
}
);
return step;
};
try {
const results = await Promise.all(
steps.map(async (step) => {
if (step.editState === 'new') {
return createStep(step);
} else if (step.editState === 'deleted') {
return deleteStep(step);
} else if (step.editState === 'changed') {
return updateStep(step);
} else if (step.editState === 'notChanged') {
return step;
}
})
);
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');
}
});
return router;
};

View File

@@ -1,65 +0,0 @@
const express = require('express');
const router = express.Router();
const defineStep = require('../../models/steps');
const defineCaseStep = require('../../models/caseSteps');
const { DataTypes, Op } = require('sequelize');
module.exports = function (sequelize) {
const Step = defineStep(sequelize, DataTypes);
const CaseStep = defineCaseStep(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize);
router.post('/', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
const newStepNo = req.query.newStepNo;
const caseId = req.query.caseId;
const t = await sequelize.transaction();
try {
// Update existing stepNo for steps with stepNo greater than or equal to newStepNo
const maxStepNo = await CaseStep.max('stepNo', {
where: { caseId: caseId },
transaction: t,
});
if (maxStepNo >= newStepNo) {
await CaseStep.update(
{ stepNo: sequelize.literal('stepNo + 1') },
{
where: {
caseId: caseId,
stepNo: { [Op.gte]: newStepNo },
},
transaction: t,
}
);
}
const newStep = await Step.create(
{
step: '',
result: '',
},
{ transaction: t }
);
await CaseStep.create(
{
caseId: caseId,
stepId: newStep.id,
stepNo: newStepNo,
},
{ transaction: t }
);
await t.commit();
res.json(newStep);
} catch (error) {
console.error(error);
await t.rollback();
res.status(500).send('Internal Server Error');
}
});
return router;
};

View File

@@ -6,9 +6,9 @@ import { Save, Plus, ArrowLeft, Circle } from 'lucide-react';
import { priorities, testTypes, templates } from '@/config/selection'; import { priorities, testTypes, templates } from '@/config/selection';
import CaseStepsEditor from './CaseStepsEditor'; import CaseStepsEditor from './CaseStepsEditor';
import CaseAttachmentsEditor from './CaseAttachmentsEditor'; import CaseAttachmentsEditor from './CaseAttachmentsEditor';
import { CaseType, AttachmentType, CaseMessages } from '@/types/case'; import { CaseType, AttachmentType, CaseMessages, StepType } from '@/types/case';
import { fetchCase, updateCase } from '../../../../../../../../../utils/caseControl'; import { fetchCase, updateCase } from '@/utils/caseControl';
import { fetchCreateStep, fetchDeleteStep } from './stepControl'; import { updateSteps } from './stepControl';
import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl'; import { fetchCreateAttachments, fetchDownloadAttachment, fetchDeleteAttachment } from './attachmentControl';
import { TokenContext } from '@/utils/TokenProvider'; import { TokenContext } from '@/utils/TokenProvider';
@@ -43,16 +43,30 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase); const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false); const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
const [isUpdating, setIsUpdating] = useState<boolean>(false); const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [plusCount, setPlusCount] = useState<number>(0);
const router = useRouter(); const router = useRouter();
const onPlusClick = async (newStepNo: number) => { const onPlusClick = async (newStepNo: number) => {
const newStep = await fetchCreateStep(context.token.access_token, newStepNo, Number(caseId)); const newStep: StepType = {
if (newStep) { id: plusCount,
newStep.caseSteps = { stepNo: newStepNo }; step: '',
result: '',
createdAt: new Date(),
updatedAt: new Date(),
caseSteps: {
stepNo: newStepNo,
},
uid: `uid${plusCount}`,
editState: 'new',
};
setPlusCount(plusCount + 1);
if (testCase.Steps) {
const updatedSteps = testCase.Steps.map((step) => { const updatedSteps = testCase.Steps.map((step) => {
if (step.caseSteps.stepNo >= newStepNo) { if (step.caseSteps.stepNo >= newStepNo) {
return { return {
...step, ...step,
editState: step.editState === 'notChanged' ? 'changed' : step.editState,
caseSteps: { caseSteps: {
...step.caseSteps, ...step.caseSteps,
stepNo: step.caseSteps.stepNo + 1, stepNo: step.caseSteps.stepNo + 1,
@@ -73,19 +87,19 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
const onDeleteClick = async (stepId: number) => { const onDeleteClick = async (stepId: number) => {
// find deletedStep's stepNo // find deletedStep's stepNo
if (testCase.Steps) {
const deletedStep = testCase.Steps.find((step) => step.id === stepId); const deletedStep = testCase.Steps.find((step) => step.id === stepId);
if (!deletedStep) { if (!deletedStep) {
return; return;
} }
const deletedStepNo = deletedStep.caseSteps.stepNo; const deletedStepNo = deletedStep.caseSteps.stepNo;
deletedStep.editState = 'deleted';
// delete request
await fetchDeleteStep(context.token.access_token, stepId, Number(caseId));
const updatedSteps = testCase.Steps.map((step) => { const updatedSteps = testCase.Steps.map((step) => {
if (step.caseSteps.stepNo > deletedStepNo) { if (step.caseSteps.stepNo > deletedStepNo) {
return { return {
...step, ...step,
editState: step.editState === 'notChanged' ? 'changed' : step.editState,
caseSteps: { caseSteps: {
...step.caseSteps, ...step.caseSteps,
stepNo: step.caseSteps.stepNo - 1, stepNo: step.caseSteps.stepNo - 1,
@@ -93,21 +107,22 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
}; };
} }
return step; return step;
}).filter((step) => step.id !== stepId); });
setTestCase({ setTestCase({
...testCase, ...testCase,
Steps: updatedSteps, Steps: updatedSteps,
}); });
}
}; };
const handleDrop = async (event) => { const handleDrop = async (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
handleFetchCreateAttachments(caseId, event.dataTransfer.files); handleFetchCreateAttachments(Number(caseId), event.dataTransfer.files);
}; };
const handleInput = (event) => { const handleInput = (event: DragEvent) => {
handleFetchCreateAttachments(caseId, event.target.files); handleFetchCreateAttachments(Number(caseId), event.target.files);
}; };
const handleFetchCreateAttachments = async (caseId: number, files: File[]) => { const handleFetchCreateAttachments = async (caseId: number, files: File[]) => {
@@ -147,6 +162,9 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
} }
try { try {
const data = await fetchCase(context.token.access_token, Number(caseId)); const data = await fetchCase(context.token.access_token, Number(caseId));
data.Steps.forEach((step: StepType) => {
step.editState = 'notChanged';
});
setTestCase(data); setTestCase(data);
} catch (error: any) { } catch (error: any) {
console.error('Error in effect:', error.message); console.error('Error in effect:', error.message);
@@ -181,6 +199,9 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
onPress={async () => { onPress={async () => {
setIsUpdating(true); setIsUpdating(true);
await updateCase(context.token.access_token, testCase); await updateCase(context.token.access_token, testCase);
if (testCase.Steps) {
await updateSteps(context.token.access_token, Number(caseId), testCase.Steps);
}
setIsUpdating(false); setIsUpdating(false);
}} }}
> >

View File

@@ -19,9 +19,12 @@ export default function StepsEditor({ isDisabled, steps, onStepUpdate, onStepPlu
return stepNoA - stepNoB; return stepNoA - stepNoB;
}); });
// filter steps
const filteredSteps = sortedSteps.filter((entry) => entry.editState !== 'deleted');
return ( return (
<> <>
{sortedSteps.map((step, index) => ( {filteredSteps.map((step, index) => (
<div key={index} className="flex"> <div key={index} className="flex">
<div className="bg-neutral-50 dark:bg-neutral-600 rounded-full flex items-center justify-center min-w-unit-8 w-unit-8 h-unit-8 mt-3 me-2"> <div className="bg-neutral-50 dark:bg-neutral-600 rounded-full flex items-center justify-center min-w-unit-8 w-unit-8 h-unit-8 mt-3 me-2">
<div>{step.caseSteps.stepNo}</div> <div>{step.caseSteps.stepNo}</div>

View File

@@ -1,16 +1,19 @@
import Config from '@/config/config'; import Config from '@/config/config';
import { StepType } from '@/types/case';
const apiServer = Config.apiServer; const apiServer = Config.apiServer;
async function fetchCreateStep(jwt: string, newStepNo: number, parentCaseId: number) { async function updateSteps(jwt: string, caseId: number, steps: StepType[]) {
console.log(steps);
const fetchOptions = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`, Authorization: `Bearer ${jwt}`,
}, },
body: JSON.stringify(steps),
}; };
const url = `${apiServer}/steps?newStepNo=${newStepNo}&caseId=${parentCaseId}`; const url = `${apiServer}/steps/update?caseId=${caseId}`;
try { try {
const response = await fetch(url, fetchOptions); const response = await fetch(url, fetchOptions);
@@ -24,26 +27,4 @@ async function fetchCreateStep(jwt: string, newStepNo: number, parentCaseId: num
} }
} }
async function fetchDeleteStep(jwt: string, stepId: number, parentCaseId: number) { export { updateSteps };
const fetchOptions = {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};
const url = `${apiServer}/steps/${stepId}?caseId=${parentCaseId}`;
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 project:', error);
throw error;
}
}
export { fetchCreateStep, fetchDeleteStep };

View File

@@ -10,17 +10,17 @@ type CaseType = {
preConditions: string; preConditions: string;
expectedResults: string; expectedResults: string;
folderId: number; folderId: number;
Steps: StepType[]; // additional property Steps?: StepType[];
Attachments: AttachmentType[]; // additional property Attachments?: AttachmentType[];
isIncluded: boolean; // additional property isIncluded?: boolean;
runStatus: number; // additional property runStatus?: number;
}; };
type CaseStepType = { type CaseStepType = {
createdAt: Date; createdAt?: Date;
updatedAt: Date; updatedAt?: Date;
CaseId: number; CaseId?: number;
StepId: number; StepId?: number;
stepNo: number; stepNo: number;
}; };
@@ -31,6 +31,8 @@ type StepType = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
caseSteps: CaseStepType; caseSteps: CaseStepType;
uid: string;
editState: 'notChanged' | 'changed' | 'new' | 'deleted';
}; };
type CaseAttachmentType = { type CaseAttachmentType = {