feat: implement bulk step update
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
90
backend/routes/steps/edit.js
Normal file
90
backend/routes/steps/edit.js
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user