feat: view customize on member's role cases pane
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "テストケース一覧に戻る",
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user