feat: view customize on member's role cases pane

This commit is contained in:
Takeshi Kimata
2024-06-06 21:39:52 +09:00
parent f9cf3ef209
commit 07259f6daf
11 changed files with 167 additions and 24 deletions

View File

@@ -81,8 +81,8 @@ function verifyEditableMiddleware(sequelize) {
return res.status(400).json({ error: 'projectId is required' }); return res.status(400).json({ error: 'projectId is required' });
} }
const isDeveloper = await isDeveloper(projectId, req.userId); const isDeveloperRet = await isDeveloper(projectId, req.userId);
if (isDeveloper) { if (isDeveloperRet) {
next(); next();
return; return;
} }
@@ -109,8 +109,8 @@ function verifyEditableMiddleware(sequelize) {
return res.status(404).send('failed to find projectId'); return res.status(404).send('failed to find projectId');
} }
const isDeveloper = await isDeveloper(projectId, req.userId); const isDeveloperRet = await isDeveloper(projectId, req.userId);
if (isDeveloper) { if (isDeveloperRet) {
next(); next();
return; return;
} }
@@ -149,8 +149,8 @@ function verifyEditableMiddleware(sequelize) {
return res.status(404).send('failed to find projectId'); return res.status(404).send('failed to find projectId');
} }
const isDeveloper = await isDeveloper(projectId, req.userId); const isDeveloperRet = await isDeveloper(projectId, req.userId);
if (isDeveloper) { if (isDeveloperRet) {
next(); next();
return; return;
} }
@@ -161,6 +161,8 @@ function verifyEditableMiddleware(sequelize) {
async function isDeveloper(projectId, userId) { async function isDeveloper(projectId, userId) {
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
Project.hasMany(Member, { foreignKey: 'projectId' });
Member.belongsTo(Project, { foreignKey: 'projectId' });
const project = await Project.findOne({ const project = await Project.findOne({
where: { id: projectId }, where: { id: projectId },
@@ -177,7 +179,7 @@ function verifyEditableMiddleware(sequelize) {
} }
// owner has developer or higher authority // owner has developer or higher authority
if (project.userId === req.userId) { if (project.userId === userId) {
return true; return true;
} }

View File

@@ -162,7 +162,11 @@
"high": "High", "high": "High",
"medium": "Medium", "medium": "Medium",
"low": "Low", "low": "Low",
"no_cases_found": "No test cases found" "no_cases_found": "No test cases found",
"case_title": "Test Case Title",
"case_description": "Test Case Description",
"create": "Create",
"please_eEnter": "Please enter test case title"
}, },
"Case": { "Case": {
"back_to_cases": "Back to test cases", "back_to_cases": "Back to test cases",

View File

@@ -161,7 +161,11 @@
"high": "高", "high": "高",
"medium": "中", "medium": "中",
"low": "低", "low": "低",
"no_cases_found": "テストケースがありません" "no_cases_found": "テストケースがありません",
"case_title": "テストケースタイトル",
"case_description": "テストケース詳細",
"create": "作成",
"please_rnter": "テストケースタイトルを入力してください"
}, },
"Case": { "Case": {
"back_to_cases": "テストケース一覧に戻る", "back_to_cases": "テストケース一覧に戻る",

View File

@@ -25,7 +25,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null); const [selectedFolder, setSelectedFolder] = useState<FolderType | null>(null);
const { folderId } = useGetCurrentIds(); const { folderId } = useGetCurrentIds();
const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false); const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false);
const [editingFolder, setEditingProject] = useState<FolderType | null>(null); const [editingFolder, setEditingFolder] = useState<FolderType | null>(null);
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
@@ -54,12 +54,12 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const openDialogForCreate = () => { const openDialogForCreate = () => {
setIsFolderDialogOpen(true); setIsFolderDialogOpen(true);
setEditingProject(null); setEditingFolder(null);
}; };
const closeDialog = () => { const closeDialog = () => {
setIsFolderDialogOpen(false); setIsFolderDialogOpen(false);
setEditingProject(null); setEditingFolder(null);
}; };
const onSubmit = async (name: string, detail: string) => { const onSubmit = async (name: string, detail: string) => {
@@ -82,7 +82,7 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
}; };
const onEditClick = (folder: FolderType) => { const onEditClick = (folder: FolderType) => {
setEditingProject(folder); setEditingFolder(folder);
setIsFolderDialogOpen(true); setIsFolderDialogOpen(true);
}; };

View File

@@ -0,0 +1,102 @@
'use client';
import React from 'react';
import { useState, useEffect } from 'react';
import { Button, Input, Textarea, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
import { CasesMessages } from '@/types/case';
type Props = {
isOpen: boolean;
onCancel: () => void;
onSubmit: (title: string, description: string) => void;
messages: CasesMessages;
};
export default function CaseDialog({ isOpen, onCancel, onSubmit, messages }: Props) {
const [caseTitle, setCaseName] = useState({
text: 'Untitled Case',
isValid: false,
errorMessage: '',
});
const [caseDescription, setCaseDescription] = useState({
text: '',
isValid: false,
errorMessage: '',
});
const clear = () => {
setCaseName({
isValid: false,
text: 'Untitled Case',
errorMessage: '',
});
setCaseDescription({
isValid: false,
text: '',
errorMessage: '',
});
};
const validate = () => {
if (!caseTitle.text) {
setCaseName({
text: '',
isValid: true,
errorMessage: messages.pleaseEnter,
});
return;
}
onSubmit(caseTitle.text, caseDescription.text);
clear();
};
return (
<Modal
isOpen={isOpen}
onOpenChange={() => {
onCancel();
}}
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">{messages.newTestCase}</ModalHeader>
<ModalBody>
<Input
type="text"
label={messages.caseTitle}
value={caseTitle.text}
isInvalid={caseTitle.isValid}
errorMessage={caseTitle.errorMessage}
onChange={(e) => {
setCaseName({
...caseTitle,
text: e.target.value,
});
}}
/>
<Textarea
label={messages.caseDescription}
value={caseDescription.text}
isInvalid={caseDescription.isValid}
errorMessage={caseDescription.errorMessage}
onChange={(e) => {
setCaseDescription({
...caseDescription,
text: e.target.value,
});
}}
/>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onCancel}>
{messages.close}
</Button>
<Button color="primary" onPress={validate}>
{messages.create}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@@ -5,6 +5,7 @@ import TestCaseTable from './TestCaseTable';
import { fetchCases, createCase, deleteCases } from './caseControl'; import { fetchCases, createCase, deleteCases } from './caseControl';
import { CaseType, CasesMessages } from '@/types/case'; import { CaseType, CasesMessages } from '@/types/case';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
import CaseDialog from './CaseDialog';
type Props = { type Props = {
projectId: string; projectId: string;
@@ -16,6 +17,7 @@ type Props = {
export default function CasesPane({ projectId, folderId, messages, locale }: Props) { export default function CasesPane({ projectId, folderId, messages, locale }: Props) {
const [cases, setCases] = useState<CaseType[]>([]); const [cases, setCases] = useState<CaseType[]>([]);
const context = useContext(TokenContext); const context = useContext(TokenContext);
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
@@ -33,11 +35,14 @@ export default function CasesPane({ projectId, folderId, messages, locale }: Pro
fetchDataEffect(); fetchDataEffect();
}, [context, folderId]); }, [context, folderId]);
const handleCreateCase = async (folderId: string) => { const closeDialog = () => {
const newCase = await createCase(context.token.access_token, folderId); setIsCaseDialogOpen(false);
const updateCases = [...cases]; };
updateCases.push(newCase);
setCases(updateCases); const onSubmit = async (title: string, description: string) => {
const newCase = await createCase(context.token.access_token, folderId, title, description);
setCases([...cases, newCase]);
closeDialog();
}; };
// Delete confirm dialog // Delete confirm dialog
@@ -70,14 +75,17 @@ export default function CasesPane({ projectId, folderId, messages, locale }: Pro
<> <>
<TestCaseTable <TestCaseTable
projectId={projectId} projectId={projectId}
isDisabled={!context.isProjectDeveloper(Number(projectId))}
cases={cases} cases={cases}
onCreateCase={() => handleCreateCase(folderId)} onCreateCase={() => setIsCaseDialogOpen(true)}
onDeleteCase={onDeleteCase} onDeleteCase={onDeleteCase}
onDeleteCases={onDeleteCases} onDeleteCases={onDeleteCases}
messages={messages} messages={messages}
locale={locale} locale={locale}
/> />
<CaseDialog isOpen={isCaseDialogOpen} onCancel={closeDialog} onSubmit={onSubmit} messages={messages} />
<DeleteConfirmDialog <DeleteConfirmDialog
isOpen={isDeleteConfirmDialogOpen} isOpen={isDeleteConfirmDialogOpen}
onCancel={closeDeleteConfirmDialog} onCancel={closeDeleteConfirmDialog}

View File

@@ -21,6 +21,7 @@ import { priorities } from '@/config/selection';
type Props = { type Props = {
projectId: string; projectId: string;
isDisabled: boolean;
cases: CaseType[]; cases: CaseType[];
onCreateCase: () => void; onCreateCase: () => void;
onDeleteCase: (caseId: number) => void; onDeleteCase: (caseId: number) => void;
@@ -31,6 +32,7 @@ type Props = {
export default function TestCaseTable({ export default function TestCaseTable({
projectId, projectId,
isDisabled,
cases, cases,
onCreateCase, onCreateCase,
onDeleteCase, onDeleteCase,
@@ -99,7 +101,11 @@ export default function TestCaseTable({
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="test case actions"> <DropdownMenu aria-label="test case actions">
<DropdownItem className="text-danger" onPress={() => handleDeleteCase(testCase.id)}> <DropdownItem
className="text-danger"
isDisabled={isDisabled}
onPress={() => handleDeleteCase(testCase.id)}
>
{messages.deleteCase} {messages.deleteCase}
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
@@ -149,6 +155,7 @@ export default function TestCaseTable({
<Button <Button
startContent={<Trash size={16} />} startContent={<Trash size={16} />}
size="sm" size="sm"
isDisabled={isDisabled}
color="danger" color="danger"
className="me-2" className="me-2"
onPress={handleDeleteCases} onPress={handleDeleteCases}
@@ -156,7 +163,13 @@ export default function TestCaseTable({
{messages.delete} {messages.delete}
</Button> </Button>
)} )}
<Button startContent={<Plus size={16} />} size="sm" color="primary" onPress={onCreateCase}> <Button
startContent={<Plus size={16} />}
size="sm"
isDisabled={isDisabled}
color="primary"
onPress={onCreateCase}
>
{messages.newTestCase} {messages.newTestCase}
</Button> </Button>
</div> </div>

View File

@@ -175,6 +175,7 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
<Button <Button
startContent={<Save size={16} />} startContent={<Save size={16} />}
size="sm" size="sm"
isDisabled={!context.isProjectDeveloper(Number(projectId))}
color="primary" color="primary"
isLoading={isUpdating} isLoading={isUpdating}
onPress={async () => { onPress={async () => {
@@ -316,6 +317,7 @@ export default function CaseEditor({ projectId, folderId, caseId, messages, loca
<Button <Button
startContent={<Plus size={16} />} startContent={<Plus size={16} />}
size="sm" size="sm"
isDisabled={!context.isProjectDeveloper(Number(projectId))}
color="primary" color="primary"
className="ms-3" className="ms-3"
onPress={() => onPlusClick(1)} onPress={() => onPlusClick(1)}

View File

@@ -48,14 +48,14 @@ async function fetchCases(jwt: string, folderId: string) {
} }
} }
async function createCase(jwt: string, folderId: string) { async function createCase(jwt: string, folderId: string, title: string, description: string) {
const newCase = { const newCase = {
title: 'untitled case', title: title,
state: 0, state: 0,
priority: 2, priority: 2,
type: 0, type: 0,
automationStatus: 0, automationStatus: 0,
description: '', description: description,
template: 0, template: 0,
preConditions: '', preConditions: '',
expectedResults: '', expectedResults: '',

View File

@@ -20,6 +20,10 @@ export default function Page({ params }: { params: { projectId: string; folderId
medium: t('medium'), medium: t('medium'),
low: t('low'), low: t('low'),
noCasesFound: t('no_cases_found'), noCasesFound: t('no_cases_found'),
caseTitle: t('case_title'),
caseDescription: t('case_description'),
create: t('create'),
pleaseEnter: t('please_enter'),
}; };
return ( return (

View File

@@ -76,6 +76,10 @@ export type CasesMessages = {
medium: string; medium: string;
low: string; low: string;
noCasesFound: string; noCasesFound: string;
caseTitle: string;
caseDescription: string;
create: string;
pleaseEnter: string;
}; };
export type CaseMessages = { export type CaseMessages = {