feat: test case filtering (#276)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes, Op } = require('sequelize');
|
||||||
const defineCase = require('../../models/cases');
|
const defineCase = require('../../models/cases');
|
||||||
|
|
||||||
module.exports = function (sequelize) {
|
module.exports = function (sequelize) {
|
||||||
@@ -9,17 +9,39 @@ module.exports = function (sequelize) {
|
|||||||
const { verifyProjectVisibleFromFolderId } = require('../../middleware/verifyVisible')(sequelize);
|
const { verifyProjectVisibleFromFolderId } = require('../../middleware/verifyVisible')(sequelize);
|
||||||
|
|
||||||
router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
|
router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
|
||||||
const { folderId } = req.query;
|
const { folderId, priority, type } = req.query;
|
||||||
|
|
||||||
if (!folderId) {
|
if (!folderId) {
|
||||||
return res.status(400).json({ error: 'folderId is required' });
|
return res.status(400).json({ error: 'folderId is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cases = await Case.findAll({
|
const whereClause = {
|
||||||
where: {
|
|
||||||
folderId: folderId,
|
folderId: folderId,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
if (priority) {
|
||||||
|
const priorityValues = priority
|
||||||
|
.split(',')
|
||||||
|
.map((p) => parseInt(p.trim(), 10))
|
||||||
|
.filter((p) => !isNaN(p));
|
||||||
|
if (priorityValues.length > 0) {
|
||||||
|
whereClause.priority = { [Op.in]: priorityValues };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
const typeValues = type
|
||||||
|
.split(',')
|
||||||
|
.map((t) => parseInt(t.trim(), 10))
|
||||||
|
.filter((t) => !isNaN(t));
|
||||||
|
if (typeValues.length > 0) {
|
||||||
|
whereClause.type = { [Op.in]: typeValues };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases = await Case.findAll({
|
||||||
|
where: whereClause,
|
||||||
});
|
});
|
||||||
res.json(cases);
|
res.json(cases);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -203,7 +203,13 @@
|
|||||||
"case_title": "Test Case Title",
|
"case_title": "Test Case Title",
|
||||||
"case_description": "Test Case Description",
|
"case_description": "Test Case Description",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"please_enter": "Please enter test case title"
|
"please_enter": "Please enter test case title",
|
||||||
|
"clear_all": "Clear all",
|
||||||
|
"apply": "Apply",
|
||||||
|
"select_priorities": "Select priorities",
|
||||||
|
"selected": "Selected",
|
||||||
|
"type": "Type",
|
||||||
|
"select_types": "Select types"
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "Back to test cases",
|
"back_to_cases": "Back to test cases",
|
||||||
|
|||||||
@@ -204,7 +204,13 @@
|
|||||||
"case_title": "テストケースタイトル",
|
"case_title": "テストケースタイトル",
|
||||||
"case_description": "テストケース詳細",
|
"case_description": "テストケース詳細",
|
||||||
"create": "作成",
|
"create": "作成",
|
||||||
"please_enter": "テストケースタイトルを入力してください"
|
"please_enter": "テストケースタイトルを入力してください",
|
||||||
|
"clear_all": "すべてクリア",
|
||||||
|
"apply": "適用",
|
||||||
|
"select_priorities": "優先度を選択",
|
||||||
|
"selected": "選択済み",
|
||||||
|
"type": "タイプ",
|
||||||
|
"select_types": "タイプを選択"
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "テストケース一覧に戻る",
|
"back_to_cases": "テストケース一覧に戻る",
|
||||||
|
|||||||
@@ -203,7 +203,13 @@
|
|||||||
"case_title": "Título do Caso de Teste",
|
"case_title": "Título do Caso de Teste",
|
||||||
"case_description": "Descrição do Caso de Teste",
|
"case_description": "Descrição do Caso de Teste",
|
||||||
"create": "Criar",
|
"create": "Criar",
|
||||||
"please_enter": "Por favor, insira o título do caso de teste"
|
"please_enter": "Por favor, insira o título do caso de teste",
|
||||||
|
"clear_all": "Limpar tudo",
|
||||||
|
"apply": "Aplicar",
|
||||||
|
"select_priorities": "Selecionar prioridades",
|
||||||
|
"selected": "Selecionado",
|
||||||
|
"type": "Tipo",
|
||||||
|
"select_types": "Selecionar tipos"
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "Voltar para os casos de teste",
|
"back_to_cases": "Voltar para os casos de teste",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Button } from '@heroui/react';
|
|||||||
import { ChevronDown, ChevronRight, Folder, Plus } from 'lucide-react';
|
import { ChevronDown, ChevronRight, Folder, Plus } from 'lucide-react';
|
||||||
import { NodeApi } from 'react-arborist';
|
import { NodeApi } from 'react-arborist';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import FolderEditMenu from './FolderEditMenu';
|
import FolderEditMenu from './FolderEditMenu';
|
||||||
|
|
||||||
import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder';
|
import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder';
|
||||||
@@ -33,6 +34,7 @@ export default function FolderItem({
|
|||||||
onDeleteClick,
|
onDeleteClick,
|
||||||
}: FolderItemProps) {
|
}: FolderItemProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const context = useContext(TokenContext);
|
const context = useContext(TokenContext);
|
||||||
const isSelected = selectedFolder && node.data.folderData.id === selectedFolder.id;
|
const isSelected = selectedFolder && node.data.folderData.id === selectedFolder.id;
|
||||||
|
|
||||||
@@ -69,11 +71,18 @@ export default function FolderItem({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const currentParams = new URLSearchParams(searchParams.toString());
|
||||||
|
const queryString = currentParams.toString();
|
||||||
|
const url = `/projects/${projectId}/folders/${node.data.folderData.id}/cases${queryString ? `?${queryString}` : ''}`;
|
||||||
|
router.push(url, { locale });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TreeItem
|
<TreeItem
|
||||||
style={style}
|
style={style}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onClick={() => router.push(`/projects/${projectId}/folders/${node.data.folderData.id}/cases`, { locale })}
|
onClick={() => handleClick()}
|
||||||
toggleButton={toggleButton}
|
toggleButton={toggleButton}
|
||||||
icon={<Folder size={20} color="#F7C24E" fill="#F7C24E" className="flex-shrink-0" />}
|
icon={<Folder size={20} color="#F7C24E" fill="#F7C24E" className="flex-shrink-0" />}
|
||||||
label={node.data.name}
|
label={node.data.name}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import TestCaseTable from './TestCaseTable';
|
import TestCaseTable from './TestCaseTable';
|
||||||
import CaseDialog from './CaseDialog';
|
import CaseDialog from './CaseDialog';
|
||||||
import { TokenContext } from '@/utils/TokenProvider';
|
import { TokenContext } from '@/utils/TokenProvider';
|
||||||
@@ -7,6 +8,7 @@ import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseCo
|
|||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
|
import { TestTypeMessages } from '@/types/testType';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
import { logError } from '@/utils/errorHandler';
|
import { logError } from '@/utils/errorHandler';
|
||||||
|
|
||||||
@@ -15,21 +17,63 @@ type Props = {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
|
testTypeMessages: TestTypeMessages;
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CasesPane({ projectId, folderId, messages, priorityMessages, locale }: Props) {
|
export default function CasesPane({
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
messages,
|
||||||
|
priorityMessages,
|
||||||
|
testTypeMessages,
|
||||||
|
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);
|
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
|
||||||
|
const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
|
||||||
|
const [typeFilter, setTypeFilter] = useState<number[]>([]);
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchDataEffect() {
|
async function fetchDataEffect() {
|
||||||
if (!context.isSignedIn()) {
|
if (!context.isSignedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const priorityParam = searchParams.get('priority');
|
||||||
|
let currentPriorityFilter: number[] = [];
|
||||||
|
if (priorityParam) {
|
||||||
|
currentPriorityFilter = priorityParam
|
||||||
|
.split(',')
|
||||||
|
.map((p) => parseInt(p.trim()))
|
||||||
|
.filter((p) => !isNaN(p));
|
||||||
|
setPriorityFilter(currentPriorityFilter);
|
||||||
|
} else {
|
||||||
|
setPriorityFilter([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeParam = searchParams.get('type');
|
||||||
|
let currentTypeFilter: number[] = [];
|
||||||
|
if (typeParam) {
|
||||||
|
currentTypeFilter = typeParam
|
||||||
|
.split(',')
|
||||||
|
.map((t) => parseInt(t.trim()))
|
||||||
|
.filter((t) => !isNaN(t));
|
||||||
|
setTypeFilter(currentTypeFilter);
|
||||||
|
} else {
|
||||||
|
setTypeFilter([]);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchCases(context.token.access_token, Number(folderId));
|
const data = await fetchCases(
|
||||||
|
context.token.access_token,
|
||||||
|
Number(folderId),
|
||||||
|
currentPriorityFilter.length > 0 ? currentPriorityFilter : undefined,
|
||||||
|
currentTypeFilter.length > 0 ? currentTypeFilter : undefined
|
||||||
|
);
|
||||||
setCases(data);
|
setCases(data);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logError('Error fetching cases:', error);
|
logError('Error fetching cases:', error);
|
||||||
@@ -37,7 +81,7 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchDataEffect();
|
fetchDataEffect();
|
||||||
}, [context, folderId]);
|
}, [context, folderId, searchParams]);
|
||||||
|
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
setIsCaseDialogOpen(false);
|
setIsCaseDialogOpen(false);
|
||||||
@@ -49,7 +93,6 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
closeDialog();
|
closeDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete confirm dialog
|
|
||||||
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
|
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
|
||||||
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
|
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
|
||||||
const closeDeleteConfirmDialog = () => {
|
const closeDeleteConfirmDialog = () => {
|
||||||
@@ -79,6 +122,28 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
await exportCases(context.token.access_token, Number(folderId), type);
|
await exportCases(context.token.access_token, Number(folderId), type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = (priorities: number[], types: number[]) => {
|
||||||
|
setPriorityFilter(priorities);
|
||||||
|
setTypeFilter(types);
|
||||||
|
|
||||||
|
const currentParams = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
|
if (priorities.length > 0) {
|
||||||
|
currentParams.set('priority', priorities.join(','));
|
||||||
|
} else {
|
||||||
|
currentParams.delete('priority');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (types.length > 0) {
|
||||||
|
currentParams.set('type', types.join(','));
|
||||||
|
} else {
|
||||||
|
currentParams.delete('type');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUrl = `${window.location.pathname}?${currentParams.toString()}`;
|
||||||
|
router.push(newUrl, { scroll: false });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TestCaseTable
|
<TestCaseTable
|
||||||
@@ -89,8 +154,12 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
|||||||
onDeleteCase={onDeleteCase}
|
onDeleteCase={onDeleteCase}
|
||||||
onDeleteCases={onDeleteCases}
|
onDeleteCases={onDeleteCases}
|
||||||
onExportCases={onExportCases}
|
onExportCases={onExportCases}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
activePriorityFilters={priorityFilter}
|
||||||
|
activeTypeFilters={typeFilter}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
priorityMessages={priorityMessages}
|
priorityMessages={priorityMessages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection } from '@heroui/react';
|
||||||
|
import { ChevronDown, Circle } from 'lucide-react';
|
||||||
|
import { PriorityMessages } from '@/types/priority';
|
||||||
|
import { TestTypeMessages } from '@/types/testType';
|
||||||
|
import { CasesMessages } from '@/types/case';
|
||||||
|
import { priorities, testTypes } from '@/config/selection';
|
||||||
|
|
||||||
|
type TestCaseFilterProps = {
|
||||||
|
messages: CasesMessages;
|
||||||
|
priorityMessages: PriorityMessages;
|
||||||
|
testTypeMessages: TestTypeMessages;
|
||||||
|
activePriorityFilters: number[];
|
||||||
|
activeTypeFilters: number[];
|
||||||
|
onFilterChange: (priorities: number[], types: number[]) => void;
|
||||||
|
handleFilterChange: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TestCaseFilter({
|
||||||
|
messages,
|
||||||
|
priorityMessages,
|
||||||
|
testTypeMessages,
|
||||||
|
activePriorityFilters,
|
||||||
|
activeTypeFilters,
|
||||||
|
onFilterChange,
|
||||||
|
handleFilterChange,
|
||||||
|
}: TestCaseFilterProps) {
|
||||||
|
const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([]));
|
||||||
|
const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([]));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activePriorityFilters.length > 0) {
|
||||||
|
const activeKeys = activePriorityFilters.map((index) => priorities[index]?.uid).filter(Boolean);
|
||||||
|
setSelectedPriorities(new Set(activeKeys));
|
||||||
|
} else {
|
||||||
|
setSelectedPriorities(new Set([]));
|
||||||
|
}
|
||||||
|
}, [activePriorityFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTypeFilters.length > 0) {
|
||||||
|
const activeKeys = activeTypeFilters.map((index) => testTypes[index]?.uid).filter(Boolean);
|
||||||
|
setSelectedTypes(new Set(activeKeys));
|
||||||
|
} else {
|
||||||
|
setSelectedTypes(new Set([]));
|
||||||
|
}
|
||||||
|
}, [activeTypeFilters]);
|
||||||
|
|
||||||
|
const handlePrioritySelectionChange = (keys: Selection) => {
|
||||||
|
setSelectedPriorities(keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeSelectionChange = (keys: Selection) => {
|
||||||
|
setSelectedTypes(keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApplyFilter = () => {
|
||||||
|
let priorityIndices: number[] = [];
|
||||||
|
if (selectedPriorities !== 'all' && selectedPriorities.size > 0) {
|
||||||
|
priorityIndices = Array.from(selectedPriorities)
|
||||||
|
.map((key) => priorities.findIndex((priority) => priority.uid === key))
|
||||||
|
.filter((index) => index !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let typeIndices: number[] = [];
|
||||||
|
if (selectedTypes !== 'all' && selectedTypes.size > 0) {
|
||||||
|
typeIndices = Array.from(selectedTypes)
|
||||||
|
.map((key) => testTypes.findIndex((type) => type.uid === key))
|
||||||
|
.filter((index) => index !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChange(priorityIndices, typeIndices);
|
||||||
|
handleFilterChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearFilter = () => {
|
||||||
|
setSelectedPriorities(new Set([]));
|
||||||
|
setSelectedTypes(new Set([]));
|
||||||
|
onFilterChange([], []);
|
||||||
|
handleFilterChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFilterEmpty =
|
||||||
|
(selectedPriorities === 'all' || (selectedPriorities instanceof Set && selectedPriorities.size === 0)) &&
|
||||||
|
(selectedTypes === 'all' || (selectedTypes instanceof Set && selectedTypes.size === 0));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-end border-t border-default-200 p-3">
|
||||||
|
<div className="flex-col space-y-2 mr-2">
|
||||||
|
<h3 className="text-default-500 text-small">{messages.priority}</h3>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button variant="bordered" endContent={<ChevronDown size={16} />}>
|
||||||
|
{selectedPriorities === 'all' || selectedPriorities.size === 0
|
||||||
|
? messages.selectPriorities
|
||||||
|
: `${selectedPriorities.size} ${messages.selected || 'selected'}`}
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
aria-label="Priority filter"
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={selectedPriorities}
|
||||||
|
onSelectionChange={handlePrioritySelectionChange}
|
||||||
|
>
|
||||||
|
{priorities.map((priority) => (
|
||||||
|
<DropdownItem key={priority.uid} textValue={priorityMessages[priority.uid]} className="flex items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Circle size={8} color={priority.color} fill={priority.color} />
|
||||||
|
<span className="text-sm">{priorityMessages[priority.uid]}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-col space-y-2 mr-2">
|
||||||
|
<h3 className="text-default-500 text-small">{messages.type}</h3>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button variant="bordered" endContent={<ChevronDown size={16} />}>
|
||||||
|
{selectedTypes === 'all' || selectedTypes.size === 0
|
||||||
|
? messages.selectTypes || 'Select Types'
|
||||||
|
: `${selectedTypes.size} ${messages.selected || 'selected'}`}
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
className="max-h-[50vh] overflow-y-auto"
|
||||||
|
aria-label="Type filter"
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={selectedTypes}
|
||||||
|
onSelectionChange={handleTypeSelectionChange}
|
||||||
|
>
|
||||||
|
{testTypes.map((type) => (
|
||||||
|
<DropdownItem key={type.uid} textValue={testTypeMessages[type.uid]} className="flex items-center">
|
||||||
|
<span className="text-sm">{testTypeMessages[type.uid]}</span>
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-auto">
|
||||||
|
<Button className="me-2" size="sm" variant="light" onPress={handleClearFilter} isDisabled={isFilterEmpty}>
|
||||||
|
{messages.clearAll}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="solid" color="primary" onPress={handleApplyFilter} isDisabled={isFilterEmpty}>
|
||||||
|
{messages.apply}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,11 +14,15 @@ import {
|
|||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
|
cn,
|
||||||
|
Badge,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { Plus, MoreVertical, Trash, FileDown, ChevronDown, FileJson, FileSpreadsheet } from 'lucide-react';
|
import { Plus, MoreVertical, Trash, FileDown, ChevronDown, Filter, FileJson, FileSpreadsheet } from 'lucide-react';
|
||||||
|
import TestCaseFilter from './TestCaseFilter';
|
||||||
import { Link } from '@/src/i18n/routing';
|
import { Link } from '@/src/i18n/routing';
|
||||||
import { CaseType, CasesMessages } from '@/types/case';
|
import { CaseType, CasesMessages } from '@/types/case';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
|
import { TestTypeMessages } from '@/types/testType';
|
||||||
import TestCasePriority from '@/components/TestCasePriority';
|
import TestCasePriority from '@/components/TestCasePriority';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
@@ -30,8 +34,12 @@ type Props = {
|
|||||||
onDeleteCase: (caseId: number) => void;
|
onDeleteCase: (caseId: number) => void;
|
||||||
onDeleteCases: (caseIds: number[]) => void;
|
onDeleteCases: (caseIds: number[]) => void;
|
||||||
onExportCases: (type: string) => void;
|
onExportCases: (type: string) => void;
|
||||||
|
onFilterChange: (priorities: number[], types: number[]) => void;
|
||||||
|
activePriorityFilters: number[];
|
||||||
|
activeTypeFilters: number[];
|
||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
|
testTypeMessages: TestTypeMessages;
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,8 +51,12 @@ export default function TestCaseTable({
|
|||||||
onDeleteCase,
|
onDeleteCase,
|
||||||
onDeleteCases,
|
onDeleteCases,
|
||||||
onExportCases,
|
onExportCases,
|
||||||
|
onFilterChange,
|
||||||
|
activePriorityFilters,
|
||||||
|
activeTypeFilters,
|
||||||
messages,
|
messages,
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
|
testTypeMessages,
|
||||||
locale,
|
locale,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
@@ -60,6 +72,7 @@ export default function TestCaseTable({
|
|||||||
direction: 'ascending',
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
const [exportType, setExportType] = useState(new Set(['json']));
|
const [exportType, setExportType] = useState(new Set(['json']));
|
||||||
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
if (cases.length === 0) {
|
if (cases.length === 0) {
|
||||||
@@ -78,6 +91,10 @@ export default function TestCaseTable({
|
|||||||
onDeleteCase(deleteCaseId);
|
onDeleteCase(deleteCaseId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
setShowFilter(!showFilter);
|
||||||
|
};
|
||||||
|
|
||||||
const renderCell = useCallback((testCase: CaseType, columnKey: string): ReactNode => {
|
const renderCell = useCallback((testCase: CaseType, columnKey: string): ReactNode => {
|
||||||
const cellValue = testCase[columnKey as keyof CaseType];
|
const cellValue = testCase[columnKey as keyof CaseType];
|
||||||
|
|
||||||
@@ -140,6 +157,8 @@ export default function TestCaseTable({
|
|||||||
setExportType(new Set(Array.from(keys as Set<string>)));
|
setExportType(new Set(Array.from(keys as Set<string>)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasActiveFilters = activePriorityFilters.length > 0 || activeTypeFilters.length > 0;
|
||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wrapper: ['max-w-3xl'],
|
wrapper: ['max-w-3xl'],
|
||||||
@@ -161,10 +180,27 @@ export default function TestCaseTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
<div className="border-b-1 dark:border-neutral-700 w-full ">
|
||||||
|
<div className="flex items-center justify-between p-3 ">
|
||||||
<h3 className="font-bold">{messages.testCaseList}</h3>
|
<h3 className="font-bold">{messages.testCaseList}</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<Badge
|
||||||
|
color="warning"
|
||||||
|
content=""
|
||||||
|
isInvisible={!hasActiveFilters}
|
||||||
|
shape="circle"
|
||||||
|
size="sm"
|
||||||
|
placement="bottom-left"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
onPress={handleFilterChange}
|
||||||
|
className={cn('me-2', showFilter && 'bg-primary')}
|
||||||
|
>
|
||||||
|
<Filter size={16} className={cn('text-default-500', showFilter && 'text-white')} />
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
||||||
<Button
|
<Button
|
||||||
startContent={<Trash size={16} />}
|
startContent={<Trash size={16} />}
|
||||||
@@ -219,11 +255,23 @@ export default function TestCaseTable({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showFilter && (
|
||||||
|
<TestCaseFilter
|
||||||
|
messages={messages}
|
||||||
|
priorityMessages={priorityMessages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
|
activePriorityFilters={activePriorityFilters}
|
||||||
|
activeTypeFilters={activeTypeFilters}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
|
handleFilterChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
isCompact
|
isCompact
|
||||||
removeWrapper
|
removeWrapper
|
||||||
aria-label="Tese cases table"
|
aria-label="Test cases table"
|
||||||
classNames={classNames}
|
classNames={classNames}
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
selectionMode="multiple"
|
selectionMode="multiple"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { getTranslations } from 'next-intl/server';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import CasesPane from './CasesPane';
|
import CasesPane from './CasesPane';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
|
import { TestTypeMessages } from '@/types/testType';
|
||||||
import { LocaleCodeType } from '@/types/locale';
|
import { LocaleCodeType } from '@/types/locale';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
|
||||||
@@ -32,6 +33,12 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
caseDescription: t('case_description'),
|
caseDescription: t('case_description'),
|
||||||
create: t('create'),
|
create: t('create'),
|
||||||
pleaseEnter: t('please_enter'),
|
pleaseEnter: t('please_enter'),
|
||||||
|
apply: t('apply'),
|
||||||
|
clearAll: t('clear_all'),
|
||||||
|
selectPriorities: t('select_priorities'),
|
||||||
|
selected: t('selected'),
|
||||||
|
type: t('type'),
|
||||||
|
selectTypes: t('select_types'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const priorityTranslation = useTranslations('Priority');
|
const priorityTranslation = useTranslations('Priority');
|
||||||
@@ -42,6 +49,23 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
low: priorityTranslation('low'),
|
low: priorityTranslation('low'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testTypeTranslation = useTranslations('Type');
|
||||||
|
const testTypeMessages: TestTypeMessages = {
|
||||||
|
other: testTypeTranslation('other'),
|
||||||
|
security: testTypeTranslation('security'),
|
||||||
|
performance: testTypeTranslation('performance'),
|
||||||
|
accessibility: testTypeTranslation('accessibility'),
|
||||||
|
functional: testTypeTranslation('functional'),
|
||||||
|
acceptance: testTypeTranslation('acceptance'),
|
||||||
|
usability: testTypeTranslation('usability'),
|
||||||
|
smokeSanity: testTypeTranslation('smoke_sanity'),
|
||||||
|
compatibility: testTypeTranslation('compatibility'),
|
||||||
|
destructive: testTypeTranslation('destructive'),
|
||||||
|
regression: testTypeTranslation('regression'),
|
||||||
|
automated: testTypeTranslation('automated'),
|
||||||
|
manual: testTypeTranslation('manual'),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CasesPane
|
<CasesPane
|
||||||
@@ -50,6 +74,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
locale={params.locale as LocaleCodeType}
|
locale={params.locale as LocaleCodeType}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
priorityMessages={priorityMessages}
|
priorityMessages={priorityMessages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ type CasesMessages = {
|
|||||||
caseDescription: string;
|
caseDescription: string;
|
||||||
create: string;
|
create: string;
|
||||||
pleaseEnter: string;
|
pleaseEnter: string;
|
||||||
|
clearAll: string;
|
||||||
|
apply: string;
|
||||||
|
selectPriorities: string;
|
||||||
|
selected: string;
|
||||||
|
type: string;
|
||||||
|
selectTypes: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CaseMessages = {
|
type CaseMessages = {
|
||||||
|
|||||||
@@ -26,8 +26,20 @@ async function fetchCase(jwt: string, caseId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCases(jwt: string, folderId: number) {
|
async function fetchCases(jwt: string, folderId: number, priority?: number[], type?: number[]) {
|
||||||
const url = `${apiServer}/cases?folderId=${folderId}`;
|
const queryParams = [`folderId=${folderId}`];
|
||||||
|
|
||||||
|
if (priority && priority.length > 0) {
|
||||||
|
queryParams.push(`priority=${priority.join(',')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type && type.length > 0) {
|
||||||
|
queryParams.push(`type=${type.join(',')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||||
|
|
||||||
|
const url = `${apiServer}/cases${query}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -43,9 +55,10 @@ async function fetchCases(jwt: string, folderId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data || [];
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logError('Error fetching data', error);
|
logError('Error fetching data', error);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user