feat: test case filtering (#276)
This commit is contained in:
@@ -203,7 +203,13 @@
|
||||
"case_title": "Test Case Title",
|
||||
"case_description": "Test Case Description",
|
||||
"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": {
|
||||
"back_to_cases": "Back to test cases",
|
||||
|
||||
@@ -204,7 +204,13 @@
|
||||
"case_title": "テストケースタイトル",
|
||||
"case_description": "テストケース詳細",
|
||||
"create": "作成",
|
||||
"please_enter": "テストケースタイトルを入力してください"
|
||||
"please_enter": "テストケースタイトルを入力してください",
|
||||
"clear_all": "すべてクリア",
|
||||
"apply": "適用",
|
||||
"select_priorities": "優先度を選択",
|
||||
"selected": "選択済み",
|
||||
"type": "タイプ",
|
||||
"select_types": "タイプを選択"
|
||||
},
|
||||
"Case": {
|
||||
"back_to_cases": "テストケース一覧に戻る",
|
||||
|
||||
@@ -203,7 +203,13 @@
|
||||
"case_title": "Título do Caso de Teste",
|
||||
"case_description": "Descrição do Caso de Teste",
|
||||
"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": {
|
||||
"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 { NodeApi } from 'react-arborist';
|
||||
import { useContext } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import FolderEditMenu from './FolderEditMenu';
|
||||
|
||||
import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder';
|
||||
@@ -33,6 +34,7 @@ export default function FolderItem({
|
||||
onDeleteClick,
|
||||
}: FolderItemProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const context = useContext(TokenContext);
|
||||
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 (
|
||||
<TreeItem
|
||||
style={style}
|
||||
isSelected={isSelected}
|
||||
onClick={() => router.push(`/projects/${projectId}/folders/${node.data.folderData.id}/cases`, { locale })}
|
||||
onClick={() => handleClick()}
|
||||
toggleButton={toggleButton}
|
||||
icon={<Folder size={20} color="#F7C24E" fill="#F7C24E" className="flex-shrink-0" />}
|
||||
label={node.data.name}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import TestCaseTable from './TestCaseTable';
|
||||
import CaseDialog from './CaseDialog';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
@@ -7,6 +8,7 @@ import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseCo
|
||||
import { CaseType, CasesMessages } from '@/types/case';
|
||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
|
||||
import { PriorityMessages } from '@/types/priority';
|
||||
import { TestTypeMessages } from '@/types/testType';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
import { logError } from '@/utils/errorHandler';
|
||||
|
||||
@@ -15,21 +17,63 @@ type Props = {
|
||||
folderId: string;
|
||||
messages: CasesMessages;
|
||||
priorityMessages: PriorityMessages;
|
||||
testTypeMessages: TestTypeMessages;
|
||||
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 context = useContext(TokenContext);
|
||||
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
|
||||
const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
|
||||
const [typeFilter, setTypeFilter] = useState<number[]>([]);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDataEffect() {
|
||||
if (!context.isSignedIn()) {
|
||||
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 {
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
logError('Error fetching cases:', error);
|
||||
@@ -37,7 +81,7 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
||||
}
|
||||
|
||||
fetchDataEffect();
|
||||
}, [context, folderId]);
|
||||
}, [context, folderId, searchParams]);
|
||||
|
||||
const closeDialog = () => {
|
||||
setIsCaseDialogOpen(false);
|
||||
@@ -49,7 +93,6 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
// Delete confirm dialog
|
||||
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
|
||||
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
|
||||
const closeDeleteConfirmDialog = () => {
|
||||
@@ -79,6 +122,28 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
||||
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 (
|
||||
<>
|
||||
<TestCaseTable
|
||||
@@ -89,8 +154,12 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
|
||||
onDeleteCase={onDeleteCase}
|
||||
onDeleteCases={onDeleteCases}
|
||||
onExportCases={onExportCases}
|
||||
onFilterChange={handleFilterChange}
|
||||
activePriorityFilters={priorityFilter}
|
||||
activeTypeFilters={typeFilter}
|
||||
messages={messages}
|
||||
priorityMessages={priorityMessages}
|
||||
testTypeMessages={testTypeMessages}
|
||||
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,
|
||||
SortDescriptor,
|
||||
ButtonGroup,
|
||||
cn,
|
||||
Badge,
|
||||
} 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 { CaseType, CasesMessages } from '@/types/case';
|
||||
import { PriorityMessages } from '@/types/priority';
|
||||
import { TestTypeMessages } from '@/types/testType';
|
||||
import TestCasePriority from '@/components/TestCasePriority';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
|
||||
@@ -30,8 +34,12 @@ type Props = {
|
||||
onDeleteCase: (caseId: number) => void;
|
||||
onDeleteCases: (caseIds: number[]) => void;
|
||||
onExportCases: (type: string) => void;
|
||||
onFilterChange: (priorities: number[], types: number[]) => void;
|
||||
activePriorityFilters: number[];
|
||||
activeTypeFilters: number[];
|
||||
messages: CasesMessages;
|
||||
priorityMessages: PriorityMessages;
|
||||
testTypeMessages: TestTypeMessages;
|
||||
locale: LocaleCodeType;
|
||||
};
|
||||
|
||||
@@ -43,8 +51,12 @@ export default function TestCaseTable({
|
||||
onDeleteCase,
|
||||
onDeleteCases,
|
||||
onExportCases,
|
||||
onFilterChange,
|
||||
activePriorityFilters,
|
||||
activeTypeFilters,
|
||||
messages,
|
||||
priorityMessages,
|
||||
testTypeMessages,
|
||||
locale,
|
||||
}: Props) {
|
||||
const headerColumns = [
|
||||
@@ -60,6 +72,7 @@ export default function TestCaseTable({
|
||||
direction: 'ascending',
|
||||
});
|
||||
const [exportType, setExportType] = useState(new Set(['json']));
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
if (cases.length === 0) {
|
||||
@@ -78,6 +91,10 @@ export default function TestCaseTable({
|
||||
onDeleteCase(deleteCaseId);
|
||||
};
|
||||
|
||||
const handleFilterChange = () => {
|
||||
setShowFilter(!showFilter);
|
||||
};
|
||||
|
||||
const renderCell = useCallback((testCase: CaseType, columnKey: string): ReactNode => {
|
||||
const cellValue = testCase[columnKey as keyof CaseType];
|
||||
|
||||
@@ -140,6 +157,8 @@ export default function TestCaseTable({
|
||||
setExportType(new Set(Array.from(keys as Set<string>)));
|
||||
};
|
||||
|
||||
const hasActiveFilters = activePriorityFilters.length > 0 || activeTypeFilters.length > 0;
|
||||
|
||||
const classNames = useMemo(
|
||||
() => ({
|
||||
wrapper: ['max-w-3xl'],
|
||||
@@ -161,69 +180,98 @@ export default function TestCaseTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
|
||||
<h3 className="font-bold">{messages.testCaseList}</h3>
|
||||
|
||||
<div>
|
||||
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
|
||||
<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>
|
||||
<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') && (
|
||||
<Button
|
||||
startContent={<Trash size={16} />}
|
||||
size="sm"
|
||||
isDisabled={isDisabled}
|
||||
color="danger"
|
||||
className="me-2"
|
||||
onPress={handleDeleteCases}
|
||||
>
|
||||
{messages.delete}
|
||||
</Button>
|
||||
)}
|
||||
<ButtonGroup className="me-2">
|
||||
<Button
|
||||
startContent={<FileDown size={16} />}
|
||||
size="sm"
|
||||
onPress={() => onExportCases(Array.from(exportType)[0])}
|
||||
>
|
||||
{messages.export} {exportType}
|
||||
</Button>
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm">
|
||||
<ChevronDown size={16} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Export options"
|
||||
className="max-w-[300px]"
|
||||
selectedKeys={exportType}
|
||||
selectionMode="single"
|
||||
onSelectionChange={handleExportTypeChange}
|
||||
>
|
||||
<DropdownItem key="json" startContent={<FileJson size={16} />}>
|
||||
json
|
||||
</DropdownItem>
|
||||
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}>
|
||||
csv
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
startContent={<Trash size={16} />}
|
||||
startContent={<Plus size={16} />}
|
||||
size="sm"
|
||||
isDisabled={isDisabled}
|
||||
color="danger"
|
||||
className="me-2"
|
||||
onPress={handleDeleteCases}
|
||||
color="primary"
|
||||
onPress={onCreateCase}
|
||||
>
|
||||
{messages.delete}
|
||||
{messages.newTestCase}
|
||||
</Button>
|
||||
)}
|
||||
<ButtonGroup className="me-2">
|
||||
<Button
|
||||
startContent={<FileDown size={16} />}
|
||||
size="sm"
|
||||
onPress={() => onExportCases(Array.from(exportType)[0])}
|
||||
>
|
||||
{messages.export} {exportType}
|
||||
</Button>
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm">
|
||||
<ChevronDown size={16} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Export options"
|
||||
className="max-w-[300px]"
|
||||
selectedKeys={exportType}
|
||||
selectionMode="single"
|
||||
onSelectionChange={handleExportTypeChange}
|
||||
>
|
||||
<DropdownItem key="json" startContent={<FileJson size={16} />}>
|
||||
json
|
||||
</DropdownItem>
|
||||
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}>
|
||||
csv
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
startContent={<Plus size={16} />}
|
||||
size="sm"
|
||||
isDisabled={isDisabled}
|
||||
color="primary"
|
||||
onPress={onCreateCase}
|
||||
>
|
||||
{messages.newTestCase}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{showFilter && (
|
||||
<TestCaseFilter
|
||||
messages={messages}
|
||||
priorityMessages={priorityMessages}
|
||||
testTypeMessages={testTypeMessages}
|
||||
activePriorityFilters={activePriorityFilters}
|
||||
activeTypeFilters={activeTypeFilters}
|
||||
onFilterChange={onFilterChange}
|
||||
handleFilterChange={handleFilterChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Table
|
||||
isCompact
|
||||
removeWrapper
|
||||
aria-label="Tese cases table"
|
||||
aria-label="Test cases table"
|
||||
classNames={classNames}
|
||||
selectedKeys={selectedKeys}
|
||||
selectionMode="multiple"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getTranslations } from 'next-intl/server';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import CasesPane from './CasesPane';
|
||||
import { PriorityMessages } from '@/types/priority';
|
||||
import { TestTypeMessages } from '@/types/testType';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
|
||||
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'),
|
||||
create: t('create'),
|
||||
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');
|
||||
@@ -42,6 +49,23 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
||||
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 (
|
||||
<>
|
||||
<CasesPane
|
||||
@@ -50,6 +74,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
||||
locale={params.locale as LocaleCodeType}
|
||||
messages={messages}
|
||||
priorityMessages={priorityMessages}
|
||||
testTypeMessages={testTypeMessages}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -77,6 +77,12 @@ type CasesMessages = {
|
||||
caseDescription: string;
|
||||
create: string;
|
||||
pleaseEnter: string;
|
||||
clearAll: string;
|
||||
apply: string;
|
||||
selectPriorities: string;
|
||||
selected: string;
|
||||
type: string;
|
||||
selectTypes: string;
|
||||
};
|
||||
|
||||
type CaseMessages = {
|
||||
|
||||
@@ -26,8 +26,20 @@ async function fetchCase(jwt: string, caseId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCases(jwt: string, folderId: number) {
|
||||
const url = `${apiServer}/cases?folderId=${folderId}`;
|
||||
async function fetchCases(jwt: string, folderId: number, priority?: number[], type?: number[]) {
|
||||
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 {
|
||||
const response = await fetch(url, {
|
||||
@@ -43,9 +55,10 @@ async function fetchCases(jwt: string, folderId: number) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
return data || [];
|
||||
} catch (error: unknown) {
|
||||
logError('Error fetching data', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user