feat: inplement auth case steps

This commit is contained in:
Takeshi Kimata
2024-06-08 18:46:23 +09:00
parent cb0aa20aa2
commit 121fa7de55
7 changed files with 70 additions and 36 deletions

View File

@@ -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();

View File

@@ -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();

View File

@@ -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>
</> </>
); );
} }

View File

@@ -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>
</> </>
); );

View File

@@ -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)}

View File

@@ -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);

View File

@@ -21,6 +21,7 @@ type CaseStepType = {
updatedAt: Date; updatedAt: Date;
CaseId: number; CaseId: number;
StepId: number; StepId: number;
stepNo: number;
}; };
type StepType = { type StepType = {