chore: Include test case title query in filters to improve UI consistency (#284)
This commit is contained in:
@@ -11,7 +11,7 @@ export default function (sequelize) {
|
|||||||
const Case = defineCase(sequelize, DataTypes);
|
const Case = defineCase(sequelize, DataTypes);
|
||||||
|
|
||||||
router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
|
router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
|
||||||
const { folderId, priority, type, q } = req.query;
|
const { folderId, title, 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' });
|
||||||
@@ -22,15 +22,15 @@ export default function (sequelize) {
|
|||||||
folderId: folderId,
|
folderId: folderId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (q) {
|
if (title) {
|
||||||
const searchTerm = q.trim();
|
const searchTerm = title.trim();
|
||||||
|
|
||||||
if (searchTerm.length > 100) {
|
if (searchTerm.length > 100) {
|
||||||
return res.status(400).json({ error: 'Search term too long' });
|
return res.status(400).json({ error: 'too long title param' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm.length >= 2) {
|
if (searchTerm.length >= 1) {
|
||||||
whereClause[Op.or] = [{ title: { [Op.like]: `%${q}%` } }];
|
whereClause[Op.or] = [{ title: { [Op.like]: `%${searchTerm}%` } }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,13 +204,13 @@
|
|||||||
"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",
|
||||||
|
"filter": "Filter",
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"select_priorities": "Select priorities",
|
"select_priorities": "Select priorities",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"select_types": "Select types",
|
"select_types": "Select types"
|
||||||
"search_placeholder": "Type to search..."
|
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "Back to test cases",
|
"back_to_cases": "Back to test cases",
|
||||||
|
|||||||
@@ -205,13 +205,13 @@
|
|||||||
"case_description": "テストケース詳細",
|
"case_description": "テストケース詳細",
|
||||||
"create": "作成",
|
"create": "作成",
|
||||||
"please_enter": "テストケースタイトルを入力してください",
|
"please_enter": "テストケースタイトルを入力してください",
|
||||||
|
"filter": "フィルター",
|
||||||
"clear_all": "すべてクリア",
|
"clear_all": "すべてクリア",
|
||||||
"apply": "適用",
|
"apply": "適用",
|
||||||
"select_priorities": "優先度を選択",
|
"select_priorities": "優先度を選択",
|
||||||
"selected": "選択済み",
|
"selected": "選択済み",
|
||||||
"type": "タイプ",
|
"type": "タイプ",
|
||||||
"select_types": "タイプを選択",
|
"select_types": "タイプを選択"
|
||||||
"search_placeholder": "入力して検索..."
|
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "テストケース一覧に戻る",
|
"back_to_cases": "テストケース一覧に戻る",
|
||||||
|
|||||||
@@ -204,13 +204,13 @@
|
|||||||
"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",
|
||||||
|
"filter": "Filtrar",
|
||||||
"clear_all": "Limpar tudo",
|
"clear_all": "Limpar tudo",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
"select_priorities": "Selecionar prioridades",
|
"select_priorities": "Selecionar prioridades",
|
||||||
"selected": "Selecionado",
|
"selected": "Selecionado",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"select_types": "Selecionar tipos",
|
"select_types": "Selecionar tipos"
|
||||||
"search_placeholder": "Digite para buscar..."
|
|
||||||
},
|
},
|
||||||
"Case": {
|
"Case": {
|
||||||
"back_to_cases": "Voltar para os casos de teste",
|
"back_to_cases": "Voltar para os casos de teste",
|
||||||
|
|||||||
@@ -32,20 +32,25 @@ export default function CasesPane({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const [cases, setCases] = useState<CaseType[]>([]);
|
const [cases, setCases] = useState<CaseType[]>([]);
|
||||||
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
|
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
|
||||||
|
const [titleFilter, setTitleFilter] = useState('');
|
||||||
const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
|
const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
|
||||||
const [typeFilter, setTypeFilter] = useState<number[]>([]);
|
const [typeFilter, setTypeFilter] = useState<number[]>([]);
|
||||||
const [queryTerm, setQueryTerm] = useState('');
|
|
||||||
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
|
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
|
||||||
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
|
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
|
|
||||||
const context = useContext(TokenContext);
|
const context = useContext(TokenContext);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const updateUrlParams = (updates: { priority?: number[]; type?: number[]; q?: string }) => {
|
const updateUrlParams = (updates: { title?: string; priority?: number[]; type?: number[] }) => {
|
||||||
const currentParams = new URLSearchParams(searchParams.toString());
|
const currentParams = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
|
if (updates.title) {
|
||||||
|
currentParams.set('title', updates.title);
|
||||||
|
} else {
|
||||||
|
currentParams.delete('title');
|
||||||
|
}
|
||||||
|
|
||||||
if (updates.priority && updates.priority.length > 0) {
|
if (updates.priority && updates.priority.length > 0) {
|
||||||
currentParams.set('priority', updates.priority.join(','));
|
currentParams.set('priority', updates.priority.join(','));
|
||||||
} else {
|
} else {
|
||||||
@@ -58,12 +63,6 @@ export default function CasesPane({
|
|||||||
currentParams.delete('type');
|
currentParams.delete('type');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.q) {
|
|
||||||
currentParams.set('q', updates.q);
|
|
||||||
} else {
|
|
||||||
currentParams.delete('q');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUrl = `${window.location.pathname}?${currentParams.toString()}`;
|
const newUrl = `${window.location.pathname}?${currentParams.toString()}`;
|
||||||
router.push(newUrl, { scroll: false });
|
router.push(newUrl, { scroll: false });
|
||||||
};
|
};
|
||||||
@@ -72,27 +71,25 @@ export default function CasesPane({
|
|||||||
async function fetchDataEffect() {
|
async function fetchDataEffect() {
|
||||||
if (!context.isSignedIn()) return;
|
if (!context.isSignedIn()) return;
|
||||||
|
|
||||||
|
const titleParam = searchParams.get('title') || '';
|
||||||
const priorityParam = parseQueryParam(searchParams.get('priority'));
|
const priorityParam = parseQueryParam(searchParams.get('priority'));
|
||||||
const typeParam = parseQueryParam(searchParams.get('type'));
|
const typeParam = parseQueryParam(searchParams.get('type'));
|
||||||
const queryParam = searchParams.get('q') || '';
|
|
||||||
|
|
||||||
|
setTitleFilter(titleParam);
|
||||||
setPriorityFilter(priorityParam);
|
setPriorityFilter(priorityParam);
|
||||||
setTypeFilter(typeParam);
|
setTypeFilter(typeParam);
|
||||||
setQueryTerm(queryParam);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchCases(
|
const data = await fetchCases(
|
||||||
context.token.access_token,
|
context.token.access_token,
|
||||||
Number(folderId),
|
Number(folderId),
|
||||||
|
titleParam || undefined,
|
||||||
priorityParam.length > 0 ? priorityParam : undefined,
|
priorityParam.length > 0 ? priorityParam : undefined,
|
||||||
typeParam.length > 0 ? typeParam : undefined,
|
typeParam.length > 0 ? typeParam : undefined
|
||||||
queryParam || undefined
|
|
||||||
);
|
);
|
||||||
setCases(data);
|
setCases(data);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logError('Error fetching cases:', error);
|
logError('Error fetching cases:', error);
|
||||||
} finally {
|
|
||||||
setIsSearching(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,18 +131,11 @@ export default function CasesPane({
|
|||||||
await exportCases(context.token.access_token, Number(folderId), type);
|
await exportCases(context.token.access_token, Number(folderId), type);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = (priorities: number[], types: number[]) => {
|
const handleFilterChange = (title: string, priorities: number[], types: number[]) => {
|
||||||
|
setTitleFilter(title);
|
||||||
setPriorityFilter(priorities);
|
setPriorityFilter(priorities);
|
||||||
setTypeFilter(types);
|
setTypeFilter(types);
|
||||||
updateUrlParams({ priority: priorities, type: types, q: queryTerm });
|
updateUrlParams({ title: title, priority: priorities, type: types });
|
||||||
};
|
|
||||||
|
|
||||||
const handleQueryChange = (q: string) => {
|
|
||||||
setQueryTerm(q);
|
|
||||||
if (q.trim()) {
|
|
||||||
setIsSearching(true);
|
|
||||||
}
|
|
||||||
updateUrlParams({ priority: priorityFilter, type: typeFilter, q });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -159,15 +149,13 @@ export default function CasesPane({
|
|||||||
onDeleteCases={onDeleteCases}
|
onDeleteCases={onDeleteCases}
|
||||||
onExportCases={onExportCases}
|
onExportCases={onExportCases}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
onQueryChange={handleQueryChange}
|
activeTitleFilter={titleFilter}
|
||||||
activePriorityFilters={priorityFilter}
|
activePriorityFilters={priorityFilter}
|
||||||
activeTypeFilters={typeFilter}
|
activeTypeFilters={typeFilter}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
priorityMessages={priorityMessages}
|
priorityMessages={priorityMessages}
|
||||||
testTypeMessages={testTypeMessages}
|
testTypeMessages={testTypeMessages}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
queryTerm={queryTerm}
|
|
||||||
isSearching={isSearching}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CaseDialog isOpen={isCaseDialogOpen} onCancel={closeDialog} onSubmit={onSubmit} messages={messages} />
|
<CaseDialog isOpen={isCaseDialogOpen} onCancel={closeDialog} onSubmit={onSubmit} messages={messages} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection } from '@heroui/react';
|
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection, Input } from '@heroui/react';
|
||||||
import { ChevronDown, Circle } from 'lucide-react';
|
import { SearchIcon, ChevronDown, Circle } from 'lucide-react';
|
||||||
import { PriorityMessages } from '@/types/priority';
|
import { PriorityMessages } from '@/types/priority';
|
||||||
import { TestTypeMessages } from '@/types/testType';
|
import { TestTypeMessages } from '@/types/testType';
|
||||||
import { CasesMessages } from '@/types/case';
|
import { CasesMessages } from '@/types/case';
|
||||||
@@ -10,24 +10,29 @@ type TestCaseFilterProps = {
|
|||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
testTypeMessages: TestTypeMessages;
|
testTypeMessages: TestTypeMessages;
|
||||||
|
activeTitleFilter: string;
|
||||||
activePriorityFilters: number[];
|
activePriorityFilters: number[];
|
||||||
activeTypeFilters: number[];
|
activeTypeFilters: number[];
|
||||||
onFilterChange: (priorities: number[], types: number[]) => void;
|
onFilterChange: (title: string, priorities: number[], types: number[]) => void;
|
||||||
handleFilterChange: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TestCaseFilter({
|
export default function TestCaseFilter({
|
||||||
messages,
|
messages,
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
testTypeMessages,
|
testTypeMessages,
|
||||||
|
activeTitleFilter,
|
||||||
activePriorityFilters,
|
activePriorityFilters,
|
||||||
activeTypeFilters,
|
activeTypeFilters,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
handleFilterChange,
|
|
||||||
}: TestCaseFilterProps) {
|
}: TestCaseFilterProps) {
|
||||||
|
const [title, setTitle] = useState<string>('');
|
||||||
const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([]));
|
const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([]));
|
||||||
const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([]));
|
const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([]));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(activeTitleFilter);
|
||||||
|
}, [activeTitleFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activePriorityFilters.length > 0) {
|
if (activePriorityFilters.length > 0) {
|
||||||
const activeKeys = activePriorityFilters.map((index) => priorities[index]?.uid).filter(Boolean);
|
const activeKeys = activePriorityFilters.map((index) => priorities[index]?.uid).filter(Boolean);
|
||||||
@@ -69,82 +74,97 @@ export default function TestCaseFilter({
|
|||||||
.filter((index) => index !== -1);
|
.filter((index) => index !== -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterChange(priorityIndices, typeIndices);
|
onFilterChange(title, priorityIndices, typeIndices);
|
||||||
handleFilterChange();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearFilter = () => {
|
const handleClearFilter = () => {
|
||||||
setSelectedPriorities(new Set([]));
|
setSelectedPriorities(new Set([]));
|
||||||
setSelectedTypes(new Set([]));
|
setSelectedTypes(new Set([]));
|
||||||
onFilterChange([], []);
|
onFilterChange('', [], []);
|
||||||
handleFilterChange();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFilterEmpty =
|
|
||||||
(selectedPriorities === 'all' || (selectedPriorities instanceof Set && selectedPriorities.size === 0)) &&
|
|
||||||
(selectedTypes === 'all' || (selectedTypes instanceof Set && selectedTypes.size === 0));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-end border-t border-default-200 p-3">
|
<div className="p-3">
|
||||||
<div className="flex-col space-y-2 mr-2">
|
<div className="mb-3 space-y-1">
|
||||||
<h3 className="text-default-500 text-small">{messages.priority}</h3>
|
<h3 className="text-default-500 text-small">{messages.caseTitle}</h3>
|
||||||
<Dropdown>
|
<Input
|
||||||
<DropdownTrigger>
|
variant="bordered"
|
||||||
<Button size="sm" variant="bordered" endContent={<ChevronDown size={16} />}>
|
classNames={{
|
||||||
{selectedPriorities === 'all' || selectedPriorities.size === 0
|
base: 'max-w-full h-8',
|
||||||
? messages.selectPriorities
|
mainWrapper: 'h-full',
|
||||||
: `${selectedPriorities.size} ${messages.selected || 'selected'}`}
|
input: 'text-small',
|
||||||
</Button>
|
}}
|
||||||
</DropdownTrigger>
|
size="sm"
|
||||||
<DropdownMenu
|
startContent={<SearchIcon size={18} />}
|
||||||
aria-label="Priority filter"
|
type="search"
|
||||||
selectionMode="multiple"
|
value={title}
|
||||||
selectedKeys={selectedPriorities}
|
onValueChange={setTitle}
|
||||||
onSelectionChange={handlePrioritySelectionChange}
|
maxLength={100}
|
||||||
>
|
/>
|
||||||
{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>
|
||||||
|
<div className="mb-3 flex justify-between gap-2">
|
||||||
<div className="flex-col space-y-2 mr-2">
|
<div className="flex-col space-y-1">
|
||||||
<h3 className="text-default-500 text-small">{messages.type}</h3>
|
<h3 className="text-default-500 text-small">{messages.priority}</h3>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button size="sm" variant="bordered" endContent={<ChevronDown size={16} />}>
|
<Button size="sm" variant="bordered" className="w-32" endContent={<ChevronDown size={16} />}>
|
||||||
{selectedTypes === 'all' || selectedTypes.size === 0
|
{selectedPriorities === 'all' || selectedPriorities.size === 0
|
||||||
? messages.selectTypes || 'Select Types'
|
? messages.selectPriorities
|
||||||
: `${selectedTypes.size} ${messages.selected || 'selected'}`}
|
: `${selectedPriorities.size} ${messages.selected || 'selected'}`}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
className="max-h-[50vh] overflow-y-auto"
|
aria-label="Priority filter"
|
||||||
aria-label="Type filter"
|
selectionMode="multiple"
|
||||||
selectionMode="multiple"
|
selectedKeys={selectedPriorities}
|
||||||
selectedKeys={selectedTypes}
|
onSelectionChange={handlePrioritySelectionChange}
|
||||||
onSelectionChange={handleTypeSelectionChange}
|
>
|
||||||
>
|
{priorities.map((priority) => (
|
||||||
{testTypes.map((type) => (
|
<DropdownItem
|
||||||
<DropdownItem key={type.uid} textValue={testTypeMessages[type.uid]} className="flex items-center">
|
key={priority.uid}
|
||||||
<span className="text-sm">{testTypeMessages[type.uid]}</span>
|
textValue={priorityMessages[priority.uid]}
|
||||||
</DropdownItem>
|
className="flex items-center"
|
||||||
))}
|
>
|
||||||
</DropdownMenu>
|
<div className="flex items-center gap-2">
|
||||||
</Dropdown>
|
<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-1">
|
||||||
|
<h3 className="text-default-500 text-small">{messages.type}</h3>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button size="sm" variant="bordered" className="w-32" 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>
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
<div className="ml-auto">
|
<Button className="me-2" size="sm" variant="light" onPress={handleClearFilter}>
|
||||||
<Button className="me-2" size="sm" variant="light" onPress={handleClearFilter} isDisabled={isFilterEmpty}>
|
|
||||||
{messages.clearAll}
|
{messages.clearAll}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="solid" color="primary" onPress={handleApplyFilter} isDisabled={isFilterEmpty}>
|
<Button size="sm" variant="solid" color="primary" onPress={handleApplyFilter}>
|
||||||
{messages.apply}
|
{messages.apply}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback, ReactNode, useEffect } from 'react';
|
import { useState, useMemo, useCallback, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
@@ -13,22 +13,12 @@ import {
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
Selection,
|
Selection,
|
||||||
SortDescriptor,
|
SortDescriptor,
|
||||||
cn,
|
|
||||||
Badge,
|
Badge,
|
||||||
Input,
|
Popover,
|
||||||
Spinner,
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import {
|
import { Plus, MoreVertical, Trash, FileDown, ChevronDown, Filter, FileJson, FileSpreadsheet } from 'lucide-react';
|
||||||
Plus,
|
|
||||||
MoreVertical,
|
|
||||||
Trash,
|
|
||||||
FileDown,
|
|
||||||
ChevronDown,
|
|
||||||
Filter,
|
|
||||||
FileJson,
|
|
||||||
FileSpreadsheet,
|
|
||||||
SearchIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import TestCaseFilter from './TestCaseFilter';
|
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';
|
||||||
@@ -36,7 +26,6 @@ import { PriorityMessages } from '@/types/priority';
|
|||||||
import { TestTypeMessages } from '@/types/testType';
|
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';
|
||||||
import useDebounce from '@/utils/useDebounce';
|
|
||||||
import { highlightSearchTerm } from '@/utils/highlightSearchTerm';
|
import { highlightSearchTerm } from '@/utils/highlightSearchTerm';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -47,16 +36,14 @@ 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;
|
onFilterChange: (query: string, priorities: number[], types: number[]) => void;
|
||||||
onQueryChange: (q: string) => void;
|
activeTitleFilter: string;
|
||||||
queryTerm: string;
|
|
||||||
activePriorityFilters: number[];
|
activePriorityFilters: number[];
|
||||||
activeTypeFilters: number[];
|
activeTypeFilters: number[];
|
||||||
messages: CasesMessages;
|
messages: CasesMessages;
|
||||||
priorityMessages: PriorityMessages;
|
priorityMessages: PriorityMessages;
|
||||||
testTypeMessages: TestTypeMessages;
|
testTypeMessages: TestTypeMessages;
|
||||||
locale: LocaleCodeType;
|
locale: LocaleCodeType;
|
||||||
isSearching: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TestCaseTable({
|
export default function TestCaseTable({
|
||||||
@@ -68,15 +55,13 @@ export default function TestCaseTable({
|
|||||||
onDeleteCases,
|
onDeleteCases,
|
||||||
onExportCases,
|
onExportCases,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
onQueryChange,
|
activeTitleFilter,
|
||||||
activePriorityFilters,
|
activePriorityFilters,
|
||||||
activeTypeFilters,
|
activeTypeFilters,
|
||||||
messages,
|
messages,
|
||||||
priorityMessages,
|
priorityMessages,
|
||||||
testTypeMessages,
|
testTypeMessages,
|
||||||
locale,
|
locale,
|
||||||
queryTerm,
|
|
||||||
isSearching,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
{ name: messages.id, uid: 'id', sortable: true },
|
{ name: messages.id, uid: 'id', sortable: true },
|
||||||
@@ -91,15 +76,6 @@ export default function TestCaseTable({
|
|||||||
direction: 'ascending',
|
direction: 'ascending',
|
||||||
});
|
});
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
const [localQueryTerm, setLocalQueryTerm] = useState(queryTerm);
|
|
||||||
|
|
||||||
const debouncedQuery = useDebounce((value: unknown) => {
|
|
||||||
onQueryChange(value as string);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalQueryTerm(queryTerm);
|
|
||||||
}, [queryTerm]);
|
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
if (cases.length === 0) {
|
if (cases.length === 0) {
|
||||||
@@ -118,17 +94,6 @@ export default function TestCaseTable({
|
|||||||
onDeleteCase(deleteCaseId);
|
onDeleteCase(deleteCaseId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = () => {
|
|
||||||
setShowFilter(!showFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQueryChange = (value: string) => {
|
|
||||||
setLocalQueryTerm(value);
|
|
||||||
if (value.length >= 2 || value.length === 0) {
|
|
||||||
debouncedQuery(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCell = useCallback(
|
const renderCell = useCallback(
|
||||||
(testCase: CaseType, columnKey: string): ReactNode => {
|
(testCase: CaseType, columnKey: string): ReactNode => {
|
||||||
const cellValue = testCase[columnKey as keyof CaseType];
|
const cellValue = testCase[columnKey as keyof CaseType];
|
||||||
@@ -148,7 +113,7 @@ export default function TestCaseTable({
|
|||||||
>
|
>
|
||||||
{highlightSearchTerm({
|
{highlightSearchTerm({
|
||||||
text: cellValue as string,
|
text: cellValue as string,
|
||||||
searchTerm: localQueryTerm,
|
searchTerm: activeTitleFilter,
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -179,7 +144,7 @@ export default function TestCaseTable({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[localQueryTerm]
|
[activeTitleFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteCases = () => {
|
const handleDeleteCases = () => {
|
||||||
@@ -193,7 +158,7 @@ export default function TestCaseTable({
|
|||||||
setSelectedKeys(new Set([]));
|
setSelectedKeys(new Set([]));
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActiveFilters = activePriorityFilters.length > 0 || activeTypeFilters.length > 0;
|
const activeFilterNum = (activeTitleFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length;
|
||||||
|
|
||||||
const classNames = useMemo(
|
const classNames = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -233,42 +198,35 @@ export default function TestCaseTable({
|
|||||||
{messages.delete}
|
{messages.delete}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Popover placement="bottom" isOpen={showFilter} onOpenChange={(open) => setShowFilter(open)}>
|
||||||
className="me-2"
|
<Badge
|
||||||
variant="bordered"
|
color="danger"
|
||||||
classNames={{
|
content={activeFilterNum}
|
||||||
base: 'max-w-full sm:max-w-[12rem] h-8',
|
isInvisible={activeFilterNum === 0}
|
||||||
mainWrapper: 'h-full',
|
shape="circle"
|
||||||
input: 'text-small',
|
placement="top-left"
|
||||||
}}
|
|
||||||
placeholder={messages.searchPlaceholder}
|
|
||||||
size="sm"
|
|
||||||
startContent={<SearchIcon size={18} />}
|
|
||||||
endContent={isSearching && <Spinner size="sm" />}
|
|
||||||
type="search"
|
|
||||||
value={localQueryTerm}
|
|
||||||
onValueChange={handleQueryChange}
|
|
||||||
aria-label={messages.searchPlaceholder}
|
|
||||||
maxLength={100}
|
|
||||||
/>
|
|
||||||
<Badge
|
|
||||||
color="warning"
|
|
||||||
content=""
|
|
||||||
isInvisible={!hasActiveFilters}
|
|
||||||
shape="circle"
|
|
||||||
size="sm"
|
|
||||||
placement="bottom-left"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
isIconOnly
|
|
||||||
onPress={handleFilterChange}
|
|
||||||
className={cn('me-2', showFilter && 'bg-primary')}
|
|
||||||
>
|
>
|
||||||
<Filter size={16} className={cn('text-default-500', showFilter && 'text-white')} />
|
<PopoverTrigger>
|
||||||
</Button>
|
<Button startContent={<Filter size={16} />} size="sm" variant="bordered" className="me-2">
|
||||||
</Badge>
|
{messages.filter}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</Badge>
|
||||||
|
<PopoverContent>
|
||||||
|
<TestCaseFilter
|
||||||
|
messages={messages}
|
||||||
|
priorityMessages={priorityMessages}
|
||||||
|
testTypeMessages={testTypeMessages}
|
||||||
|
activeTitleFilter={activeTitleFilter}
|
||||||
|
activePriorityFilters={activePriorityFilters}
|
||||||
|
activeTypeFilters={activeTypeFilters}
|
||||||
|
onFilterChange={(newTitleFilter, newPriorityFilters, newTypeFilters) => {
|
||||||
|
setShowFilter(false);
|
||||||
|
onFilterChange(newTitleFilter, newPriorityFilters, newTypeFilters);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button
|
||||||
@@ -305,17 +263,6 @@ 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>
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export default function Page({ params }: { params: { projectId: string; folderId
|
|||||||
create: t('create'),
|
create: t('create'),
|
||||||
pleaseEnter: t('please_enter'),
|
pleaseEnter: t('please_enter'),
|
||||||
apply: t('apply'),
|
apply: t('apply'),
|
||||||
|
filter: t('filter'),
|
||||||
clearAll: t('clear_all'),
|
clearAll: t('clear_all'),
|
||||||
selectPriorities: t('select_priorities'),
|
selectPriorities: t('select_priorities'),
|
||||||
selected: t('selected'),
|
selected: t('selected'),
|
||||||
type: t('type'),
|
type: t('type'),
|
||||||
selectTypes: t('select_types'),
|
selectTypes: t('select_types'),
|
||||||
searchPlaceholder: t('search_placeholder'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const priorityTranslation = useTranslations('Priority');
|
const priorityTranslation = useTranslations('Priority');
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
addToast,
|
addToast,
|
||||||
ButtonGroup,
|
|
||||||
Badge,
|
Badge,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import {
|
import {
|
||||||
@@ -103,7 +102,6 @@ export default function RunEditor({
|
|||||||
const [isNameInvalid] = useState<boolean>(false);
|
const [isNameInvalid] = useState<boolean>(false);
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [exportType, setExportType] = useState(new Set(['xml']));
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useFormGuard(isDirty, messages.areYouSureLeave);
|
useFormGuard(isDirty, messages.areYouSureLeave);
|
||||||
|
|
||||||
@@ -201,10 +199,6 @@ export default function RunEditor({
|
|||||||
setIsDirty(false);
|
setIsDirty(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportTypeChange = (keys: Selection) => {
|
|
||||||
setExportType(new Set(Array.from(keys as Set<string>)));
|
|
||||||
};
|
|
||||||
|
|
||||||
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 p-3 flex items-center justify-between">
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ type CasesMessages = {
|
|||||||
caseDescription: string;
|
caseDescription: string;
|
||||||
create: string;
|
create: string;
|
||||||
pleaseEnter: string;
|
pleaseEnter: string;
|
||||||
|
filter: string;
|
||||||
clearAll: string;
|
clearAll: string;
|
||||||
apply: string;
|
apply: string;
|
||||||
selectPriorities: string;
|
selectPriorities: string;
|
||||||
selected: string;
|
selected: string;
|
||||||
type: string;
|
type: string;
|
||||||
selectTypes: string;
|
selectTypes: string;
|
||||||
searchPlaceholder: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type CaseMessages = {
|
type CaseMessages = {
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ async function fetchCase(jwt: string, caseId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCases(jwt: string, folderId: number, priority?: number[], type?: number[], q?: string) {
|
async function fetchCases(jwt: string, folderId: number, title?: string, priority?: number[], type?: number[]) {
|
||||||
const queryParams = [`folderId=${folderId}`];
|
const queryParams = [`folderId=${folderId}`];
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
queryParams.push(`title=${title}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (priority && priority.length > 0) {
|
if (priority && priority.length > 0) {
|
||||||
queryParams.push(`priority=${priority.join(',')}`);
|
queryParams.push(`priority=${priority.join(',')}`);
|
||||||
}
|
}
|
||||||
@@ -37,10 +41,6 @@ async function fetchCases(jwt: string, folderId: number, priority?: number[], ty
|
|||||||
queryParams.push(`type=${type.join(',')}`);
|
queryParams.push(`type=${type.join(',')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (q) {
|
|
||||||
queryParams.push(`q=${q}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||||
|
|
||||||
const url = `${apiServer}/cases${query}`;
|
const url = `${apiServer}/cases${query}`;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function highlightSearchTerm({
|
|||||||
text,
|
text,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
className,
|
className,
|
||||||
minSearchLength = 2,
|
minSearchLength = 1,
|
||||||
}: HighlightSearchTermProps): ReactNode {
|
}: HighlightSearchTermProps): ReactNode {
|
||||||
if (!text || !searchTerm || searchTerm.length < minSearchLength) {
|
if (!text || !searchTerm || searchTerm.length < minSearchLength) {
|
||||||
return text || null;
|
return text || null;
|
||||||
|
|||||||
Reference in New Issue
Block a user