feat: inplement auth case steps
This commit is contained in:
@@ -7,11 +7,12 @@ const { DataTypes, Op } = require('sequelize');
|
|||||||
module.exports = function (sequelize) {
|
module.exports = function (sequelize) {
|
||||||
const Step = defineStep(sequelize, DataTypes);
|
const Step = defineStep(sequelize, DataTypes);
|
||||||
const CaseStep = defineCaseStep(sequelize, DataTypes);
|
const CaseStep = defineCaseStep(sequelize, DataTypes);
|
||||||
|
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
|
||||||
|
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize);
|
||||||
|
|
||||||
router.delete('/:stepId', async (req, res) => {
|
router.delete('/:stepId', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
|
||||||
const stepId = req.params.stepId;
|
const stepId = req.params.stepId;
|
||||||
// TODO The caseId should not be specified from the front end, but should be traced from stepId by association.
|
const caseId = req.query.caseId;
|
||||||
const caseId = req.query.parentCaseId;
|
|
||||||
|
|
||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ const { DataTypes, Op } = require('sequelize');
|
|||||||
module.exports = function (sequelize) {
|
module.exports = function (sequelize) {
|
||||||
const Step = defineStep(sequelize, DataTypes);
|
const Step = defineStep(sequelize, DataTypes);
|
||||||
const CaseStep = defineCaseStep(sequelize, DataTypes);
|
const CaseStep = defineCaseStep(sequelize, DataTypes);
|
||||||
|
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
|
||||||
|
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize);
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
|
||||||
const newStepNo = req.query.newStepNo;
|
const newStepNo = req.query.newStepNo;
|
||||||
const caseId = req.query.parentCaseId;
|
const caseId = req.query.caseId;
|
||||||
|
|
||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import { Image, Button, Tooltip, Card, CardBody } from '@nextui-org/react';
|
import { Image, Button, Tooltip, Card, CardBody } from '@nextui-org/react';
|
||||||
import { AttachmentType, CaseMessages } from '@/types/case';
|
import { AttachmentType, CaseMessages } from '@/types/case';
|
||||||
import { Trash, ArrowDownToLine } from 'lucide-react';
|
import { Trash, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react';
|
||||||
import { isImage } from './isImage';
|
import { isImage } from './isImage';
|
||||||
|
import { ChangeEvent, DragEvent } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isDisabled: boolean;
|
||||||
attachments: AttachmentType[];
|
attachments: AttachmentType[];
|
||||||
onAttachmentDownload: (attachmentId: number, downloadFileName: string) => void;
|
onAttachmentDownload: (attachmentId: number, downloadFileName: string) => void;
|
||||||
onAttachmentDelete: (attachmentId: number) => void;
|
onAttachmentDelete: (attachmentId: number) => void;
|
||||||
|
onFilesDrop: (event: DragEvent) => void;
|
||||||
|
onFilesInput: (event: ChangeEvent) => void;
|
||||||
messages: CaseMessages;
|
messages: CaseMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CaseAttachmentsEditor({
|
export default function CaseAttachmentsEditor({
|
||||||
|
isDisabled = false,
|
||||||
attachments = [],
|
attachments = [],
|
||||||
onAttachmentDownload,
|
onAttachmentDownload,
|
||||||
onAttachmentDelete,
|
onAttachmentDelete,
|
||||||
|
onFilesDrop,
|
||||||
|
onFilesInput,
|
||||||
messages,
|
messages,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
let images = [];
|
let images: AttachmentType[] = [];
|
||||||
let others = [];
|
let others: AttachmentType[] = [];
|
||||||
|
|
||||||
attachments.forEach((attachment) => {
|
attachments.forEach((attachment) => {
|
||||||
if (isImage(attachment)) {
|
if (isImage(attachment)) {
|
||||||
@@ -39,6 +46,7 @@ export default function CaseAttachmentsEditor({
|
|||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
|
isDisabled={isDisabled}
|
||||||
className="bg-transparent rounded-full"
|
className="bg-transparent rounded-full"
|
||||||
onPress={() => onAttachmentDelete(image.id)}
|
onPress={() => onAttachmentDelete(image.id)}
|
||||||
>
|
>
|
||||||
@@ -82,6 +90,39 @@ export default function CaseAttachmentsEditor({
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center w-96 mt-3"
|
||||||
|
onDrop={(event) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onFilesDrop(event);
|
||||||
|
}}
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor="dropzone-file"
|
||||||
|
className={`flex flex-col items-center justify-center w-full h-32 border-2 border-neutral-200 border-dashed rounded-lg bg-neutral-50 dark:hover:bg-bray-800 dark:bg-neutral-700 hover:bg-neutral-100 dark:border-neutral-600 dark:hover:border-neutral-500 dark:hover:bg-neutral-600 ${isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<ArrowUpFromLine />
|
||||||
|
<p className="mb-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
<span className="font-semibold">{messages.clickToUpload}</span>
|
||||||
|
<span>{messages.orDragAndDrop}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-500 dark:text-neutral-400">{messages.maxFileSize}: 50 MB</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="dropzone-file"
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
disabled={isDisabled}
|
||||||
|
onChange={(e) => onFilesInput(e)}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip } from '@nextui-org/react';
|
import { Input, Textarea, Select, SelectItem, Button, Divider, Tooltip } from '@nextui-org/react';
|
||||||
import { useRouter } from '@/src/navigation';
|
import { useRouter } from '@/src/navigation';
|
||||||
import { Save, Plus, ArrowLeft, ArrowUpFromLine, Circle } from 'lucide-react';
|
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';
|
||||||
@@ -46,7 +46,7 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onPlusClick = async (newStepNo: number) => {
|
const onPlusClick = async (newStepNo: number) => {
|
||||||
const newStep = await fetchCreateStep(newStepNo, Number(caseId));
|
const newStep = await fetchCreateStep(context.token.access_token, newStepNo, Number(caseId));
|
||||||
if (newStep) {
|
if (newStep) {
|
||||||
newStep.caseSteps = { stepNo: newStepNo };
|
newStep.caseSteps = { stepNo: newStepNo };
|
||||||
const updatedSteps = testCase.Steps.map((step) => {
|
const updatedSteps = testCase.Steps.map((step) => {
|
||||||
@@ -80,7 +80,7 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
|
|||||||
const deletedStepNo = deletedStep.caseSteps.stepNo;
|
const deletedStepNo = deletedStep.caseSteps.stepNo;
|
||||||
|
|
||||||
// delete request
|
// delete request
|
||||||
await fetchDeleteStep(stepId, caseId);
|
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) {
|
||||||
@@ -326,6 +326,7 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<CaseStepsEditor
|
<CaseStepsEditor
|
||||||
|
isDisabled={!context.isProjectDeveloper(Number(projectId))}
|
||||||
steps={testCase.Steps}
|
steps={testCase.Steps}
|
||||||
onStepUpdate={(stepId, changeStep) => {
|
onStepUpdate={(stepId, changeStep) => {
|
||||||
setTestCase({
|
setTestCase({
|
||||||
@@ -349,33 +350,16 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
|
|||||||
<Divider className="my-6" />
|
<Divider className="my-6" />
|
||||||
<h6 className="font-bold">{messages.attachments}</h6>
|
<h6 className="font-bold">{messages.attachments}</h6>
|
||||||
<CaseAttachmentsEditor
|
<CaseAttachmentsEditor
|
||||||
|
isDisabled={!context.isProjectDeveloper(Number(projectId))}
|
||||||
attachments={testCase.Attachments}
|
attachments={testCase.Attachments}
|
||||||
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
|
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
|
||||||
fetchDownloadAttachment(attachmentId, downloadFileName)
|
fetchDownloadAttachment(attachmentId, downloadFileName)
|
||||||
}
|
}
|
||||||
onAttachmentDelete={onAttachmentDelete}
|
onAttachmentDelete={onAttachmentDelete}
|
||||||
|
onFilesDrop={handleDrop}
|
||||||
|
onFilesInput={handleInput}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className="flex items-center justify-center w-96 mt-3"
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={(event) => event.preventDefault()}
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
htmlFor="dropzone-file"
|
|
||||||
className="flex flex-col items-center justify-center w-full h-32 border-2 border-neutral-200 border-dashed rounded-lg cursor-pointer bg-neutral-50 dark:hover:bg-bray-800 dark:bg-neutral-700 hover:bg-neutral-100 dark:border-neutral-600 dark:hover:border-neutral-500 dark:hover:bg-neutral-600"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
||||||
<ArrowUpFromLine />
|
|
||||||
<p className="mb-2 text-sm text-neutral-500 dark:text-neutral-400">
|
|
||||||
<span className="font-semibold">{messages.clickToUpload}</span>
|
|
||||||
<span>{messages.orDragAndDrop}</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">{messages.maxFileSize}: 50 MB</p>
|
|
||||||
</div>
|
|
||||||
<input id="dropzone-file" type="file" className="hidden" onChange={handleInput} multiple />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CaseMessages, StepType } from '@/types/case';
|
|||||||
import { Plus, Trash } from 'lucide-react';
|
import { Plus, Trash } from 'lucide-react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isDisabled: boolean;
|
||||||
steps: StepType[];
|
steps: StepType[];
|
||||||
onStepUpdate: (stepId: number, step: StepType) => void;
|
onStepUpdate: (stepId: number, step: StepType) => void;
|
||||||
onStepPlus: (newStepNo: number) => void;
|
onStepPlus: (newStepNo: number) => void;
|
||||||
@@ -10,7 +11,7 @@ type Props = {
|
|||||||
messages: CaseMessages;
|
messages: CaseMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function StepsEditor({ steps, onStepUpdate, onStepPlus, onStepDelete, messages }: Props) {
|
export default function StepsEditor({ isDisabled, steps, onStepUpdate, onStepPlus, onStepDelete, messages }: Props) {
|
||||||
// sort steps by junction table's column
|
// sort steps by junction table's column
|
||||||
const sortedSteps = steps.slice().sort((a, b) => {
|
const sortedSteps = steps.slice().sort((a, b) => {
|
||||||
const stepNoA = a.caseSteps.stepNo;
|
const stepNoA = a.caseSteps.stepNo;
|
||||||
@@ -51,6 +52,7 @@ export default function StepsEditor({ steps, onStepUpdate, onStepPlus, onStepDel
|
|||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
|
isDisabled={isDisabled}
|
||||||
className="bg-transparent rounded-full"
|
className="bg-transparent rounded-full"
|
||||||
onPress={() => onStepDelete(step.id)}
|
onPress={() => onStepDelete(step.id)}
|
||||||
>
|
>
|
||||||
@@ -60,6 +62,7 @@ export default function StepsEditor({ steps, onStepUpdate, onStepPlus, onStepDel
|
|||||||
<Tooltip content={messages.insertStep} placement="left">
|
<Tooltip content={messages.insertStep} placement="left">
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
isDisabled={isDisabled}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-transparent rounded-full"
|
className="bg-transparent rounded-full"
|
||||||
onPress={() => onStepPlus(step.caseSteps.stepNo + 1)}
|
onPress={() => onStepPlus(step.caseSteps.stepNo + 1)}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import Config from '@/config/config';
|
import Config from '@/config/config';
|
||||||
const apiServer = Config.apiServer;
|
const apiServer = Config.apiServer;
|
||||||
|
|
||||||
async function fetchCreateStep(newStepNo: number, parentCaseId: number) {
|
async function fetchCreateStep(jwt: string, newStepNo: number, parentCaseId: number) {
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = `${apiServer}/steps?newStepNo=${newStepNo}&parentCaseId=${parentCaseId}`;
|
const url = `${apiServer}/steps?newStepNo=${newStepNo}&caseId=${parentCaseId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
@@ -23,15 +24,16 @@ async function fetchCreateStep(newStepNo: number, parentCaseId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDeleteStep(stepId: number, parentCaseId: number) {
|
async function fetchDeleteStep(jwt: string, stepId: number, parentCaseId: number) {
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = `${apiServer}/steps/${stepId}?parentCaseId=${parentCaseId}`;
|
const url = `${apiServer}/steps/${stepId}?caseId=${parentCaseId}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type CaseStepType = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
CaseId: number;
|
CaseId: number;
|
||||||
StepId: number;
|
StepId: number;
|
||||||
|
stepNo: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StepType = {
|
type StepType = {
|
||||||
|
|||||||
Reference in New Issue
Block a user