diff --git a/backend/routes/cases/index.js b/backend/routes/cases/index.js index d32a0b3..16fc54e 100644 --- a/backend/routes/cases/index.js +++ b/backend/routes/cases/index.js @@ -11,7 +11,7 @@ export default function (sequelize) { const Case = defineCase(sequelize, DataTypes); router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => { - const { folderId, priority, type, q } = req.query; + const { folderId, title, priority, type } = req.query; if (!folderId) { return res.status(400).json({ error: 'folderId is required' }); @@ -22,15 +22,15 @@ export default function (sequelize) { folderId: folderId, }; - if (q) { - const searchTerm = q.trim(); + if (title) { + const searchTerm = title.trim(); 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) { - whereClause[Op.or] = [{ title: { [Op.like]: `%${q}%` } }]; + if (searchTerm.length >= 1) { + whereClause[Op.or] = [{ title: { [Op.like]: `%${searchTerm}%` } }]; } } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e546a20..9a43321 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -204,13 +204,13 @@ "case_description": "Test Case Description", "create": "Create", "please_enter": "Please enter test case title", + "filter": "Filter", "clear_all": "Clear all", "apply": "Apply", "select_priorities": "Select priorities", "selected": "Selected", "type": "Type", - "select_types": "Select types", - "search_placeholder": "Type to search..." + "select_types": "Select types" }, "Case": { "back_to_cases": "Back to test cases", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index 7bd7cf1..6084d3f 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -205,13 +205,13 @@ "case_description": "テストケース詳細", "create": "作成", "please_enter": "テストケースタイトルを入力してください", + "filter": "フィルター", "clear_all": "すべてクリア", "apply": "適用", "select_priorities": "優先度を選択", "selected": "選択済み", "type": "タイプ", - "select_types": "タイプを選択", - "search_placeholder": "入力して検索..." + "select_types": "タイプを選択" }, "Case": { "back_to_cases": "テストケース一覧に戻る", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index e575d7c..99105af 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -204,13 +204,13 @@ "case_description": "Descrição do Caso de Teste", "create": "Criar", "please_enter": "Por favor, insira o título do caso de teste", + "filter": "Filtrar", "clear_all": "Limpar tudo", "apply": "Aplicar", "select_priorities": "Selecionar prioridades", "selected": "Selecionado", "type": "Tipo", - "select_types": "Selecionar tipos", - "search_placeholder": "Digite para buscar..." + "select_types": "Selecionar tipos" }, "Case": { "back_to_cases": "Voltar para os casos de teste", diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx index 6bfbbf9..b9e99c5 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/CasesPane.tsx @@ -32,20 +32,25 @@ export default function CasesPane({ }: Props) { const [cases, setCases] = useState([]); const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false); + const [titleFilter, setTitleFilter] = useState(''); const [priorityFilter, setPriorityFilter] = useState([]); const [typeFilter, setTypeFilter] = useState([]); - const [queryTerm, setQueryTerm] = useState(''); const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false); const [deleteCaseIds, setDeleteCaseIds] = useState([]); - const [isSearching, setIsSearching] = useState(false); const context = useContext(TokenContext); const router = useRouter(); 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()); + if (updates.title) { + currentParams.set('title', updates.title); + } else { + currentParams.delete('title'); + } + if (updates.priority && updates.priority.length > 0) { currentParams.set('priority', updates.priority.join(',')); } else { @@ -58,12 +63,6 @@ export default function CasesPane({ currentParams.delete('type'); } - if (updates.q) { - currentParams.set('q', updates.q); - } else { - currentParams.delete('q'); - } - const newUrl = `${window.location.pathname}?${currentParams.toString()}`; router.push(newUrl, { scroll: false }); }; @@ -72,27 +71,25 @@ export default function CasesPane({ async function fetchDataEffect() { if (!context.isSignedIn()) return; + const titleParam = searchParams.get('title') || ''; const priorityParam = parseQueryParam(searchParams.get('priority')); const typeParam = parseQueryParam(searchParams.get('type')); - const queryParam = searchParams.get('q') || ''; + setTitleFilter(titleParam); setPriorityFilter(priorityParam); setTypeFilter(typeParam); - setQueryTerm(queryParam); try { const data = await fetchCases( context.token.access_token, Number(folderId), + titleParam || undefined, priorityParam.length > 0 ? priorityParam : undefined, - typeParam.length > 0 ? typeParam : undefined, - queryParam || undefined + typeParam.length > 0 ? typeParam : undefined ); setCases(data); } catch (error: unknown) { 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); }; - const handleFilterChange = (priorities: number[], types: number[]) => { + const handleFilterChange = (title: string, priorities: number[], types: number[]) => { + setTitleFilter(title); setPriorityFilter(priorities); setTypeFilter(types); - updateUrlParams({ priority: priorities, type: types, q: queryTerm }); - }; - - const handleQueryChange = (q: string) => { - setQueryTerm(q); - if (q.trim()) { - setIsSearching(true); - } - updateUrlParams({ priority: priorityFilter, type: typeFilter, q }); + updateUrlParams({ title: title, priority: priorities, type: types }); }; return ( @@ -159,15 +149,13 @@ export default function CasesPane({ onDeleteCases={onDeleteCases} onExportCases={onExportCases} onFilterChange={handleFilterChange} - onQueryChange={handleQueryChange} + activeTitleFilter={titleFilter} activePriorityFilters={priorityFilter} activeTypeFilters={typeFilter} messages={messages} priorityMessages={priorityMessages} testTypeMessages={testTypeMessages} locale={locale} - queryTerm={queryTerm} - isSearching={isSearching} /> diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseFilter.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseFilter.tsx index bdef510..de6a17a 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseFilter.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseFilter.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection } from '@heroui/react'; -import { ChevronDown, Circle } from 'lucide-react'; +import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection, Input } from '@heroui/react'; +import { SearchIcon, ChevronDown, Circle } from 'lucide-react'; import { PriorityMessages } from '@/types/priority'; import { TestTypeMessages } from '@/types/testType'; import { CasesMessages } from '@/types/case'; @@ -10,24 +10,29 @@ type TestCaseFilterProps = { messages: CasesMessages; priorityMessages: PriorityMessages; testTypeMessages: TestTypeMessages; + activeTitleFilter: string; activePriorityFilters: number[]; activeTypeFilters: number[]; - onFilterChange: (priorities: number[], types: number[]) => void; - handleFilterChange: () => void; + onFilterChange: (title: string, priorities: number[], types: number[]) => void; }; export default function TestCaseFilter({ messages, priorityMessages, testTypeMessages, + activeTitleFilter, activePriorityFilters, activeTypeFilters, onFilterChange, - handleFilterChange, }: TestCaseFilterProps) { + const [title, setTitle] = useState(''); const [selectedPriorities, setSelectedPriorities] = useState(new Set([])); const [selectedTypes, setSelectedTypes] = useState(new Set([])); + useEffect(() => { + setTitle(activeTitleFilter); + }, [activeTitleFilter]); + useEffect(() => { if (activePriorityFilters.length > 0) { const activeKeys = activePriorityFilters.map((index) => priorities[index]?.uid).filter(Boolean); @@ -69,82 +74,97 @@ export default function TestCaseFilter({ .filter((index) => index !== -1); } - onFilterChange(priorityIndices, typeIndices); - handleFilterChange(); + onFilterChange(title, priorityIndices, typeIndices); }; const handleClearFilter = () => { setSelectedPriorities(new Set([])); setSelectedTypes(new Set([])); - onFilterChange([], []); - handleFilterChange(); + onFilterChange('', [], []); }; - const isFilterEmpty = - (selectedPriorities === 'all' || (selectedPriorities instanceof Set && selectedPriorities.size === 0)) && - (selectedTypes === 'all' || (selectedTypes instanceof Set && selectedTypes.size === 0)); - return ( -
-
-

{messages.priority}

- - - - - - {priorities.map((priority) => ( - -
- - {priorityMessages[priority.uid]} -
-
- ))} -
-
+
+
+

{messages.caseTitle}

+ } + type="search" + value={title} + onValueChange={setTitle} + maxLength={100} + />
- -
-

{messages.type}

- - - - - - {testTypes.map((type) => ( - - {testTypeMessages[type.uid]} - - ))} - - +
+
+

{messages.priority}

+ + + + + + {priorities.map((priority) => ( + +
+ + {priorityMessages[priority.uid]} +
+
+ ))} +
+
+
+
+

{messages.type}

+ + + + + + {testTypes.map((type) => ( + + {testTypeMessages[type.uid]} + + ))} + + +
- -
- -
diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx index a496f01..bf62b65 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback, ReactNode, useEffect } from 'react'; +import { useState, useMemo, useCallback, ReactNode } from 'react'; import { Table, TableHeader, @@ -13,22 +13,12 @@ import { DropdownItem, Selection, SortDescriptor, - cn, Badge, - Input, - Spinner, + Popover, + PopoverContent, + PopoverTrigger, } from '@heroui/react'; -import { - Plus, - MoreVertical, - Trash, - FileDown, - ChevronDown, - Filter, - FileJson, - FileSpreadsheet, - SearchIcon, -} 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'; @@ -36,7 +26,6 @@ import { PriorityMessages } from '@/types/priority'; import { TestTypeMessages } from '@/types/testType'; import TestCasePriority from '@/components/TestCasePriority'; import { LocaleCodeType } from '@/types/locale'; -import useDebounce from '@/utils/useDebounce'; import { highlightSearchTerm } from '@/utils/highlightSearchTerm'; type Props = { @@ -47,16 +36,14 @@ type Props = { onDeleteCase: (caseId: number) => void; onDeleteCases: (caseIds: number[]) => void; onExportCases: (type: string) => void; - onFilterChange: (priorities: number[], types: number[]) => void; - onQueryChange: (q: string) => void; - queryTerm: string; + onFilterChange: (query: string, priorities: number[], types: number[]) => void; + activeTitleFilter: string; activePriorityFilters: number[]; activeTypeFilters: number[]; messages: CasesMessages; priorityMessages: PriorityMessages; testTypeMessages: TestTypeMessages; locale: LocaleCodeType; - isSearching: boolean; }; export default function TestCaseTable({ @@ -68,15 +55,13 @@ export default function TestCaseTable({ onDeleteCases, onExportCases, onFilterChange, - onQueryChange, + activeTitleFilter, activePriorityFilters, activeTypeFilters, messages, priorityMessages, testTypeMessages, locale, - queryTerm, - isSearching, }: Props) { const headerColumns = [ { name: messages.id, uid: 'id', sortable: true }, @@ -91,15 +76,6 @@ export default function TestCaseTable({ direction: 'ascending', }); 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(() => { if (cases.length === 0) { @@ -118,17 +94,6 @@ export default function TestCaseTable({ onDeleteCase(deleteCaseId); }; - const handleFilterChange = () => { - setShowFilter(!showFilter); - }; - - const handleQueryChange = (value: string) => { - setLocalQueryTerm(value); - if (value.length >= 2 || value.length === 0) { - debouncedQuery(value); - } - }; - const renderCell = useCallback( (testCase: CaseType, columnKey: string): ReactNode => { const cellValue = testCase[columnKey as keyof CaseType]; @@ -148,7 +113,7 @@ export default function TestCaseTable({ > {highlightSearchTerm({ text: cellValue as string, - searchTerm: localQueryTerm, + searchTerm: activeTitleFilter, })} ); @@ -179,7 +144,7 @@ export default function TestCaseTable({ } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [localQueryTerm] + [activeTitleFilter] ); const handleDeleteCases = () => { @@ -193,7 +158,7 @@ export default function TestCaseTable({ setSelectedKeys(new Set([])); }; - const hasActiveFilters = activePriorityFilters.length > 0 || activeTypeFilters.length > 0; + const activeFilterNum = (activeTitleFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length; const classNames = useMemo( () => ({ @@ -233,42 +198,35 @@ export default function TestCaseTable({ {messages.delete} )} - } - endContent={isSearching && } - type="search" - value={localQueryTerm} - onValueChange={handleQueryChange} - aria-label={messages.searchPlaceholder} - maxLength={100} - /> - - - + + + + + + { + setShowFilter(false); + onFilterChange(newTitleFilter, newPriorityFilters, newTypeFilters); + }} + /> + +
- {showFilter && ( - - )}
(false); const [isUpdating, setIsUpdating] = useState(false); const [isDirty, setIsDirty] = useState(false); - const [exportType, setExportType] = useState(new Set(['xml'])); const router = useRouter(); useFormGuard(isDirty, messages.areYouSureLeave); @@ -201,10 +199,6 @@ export default function RunEditor({ setIsDirty(false); }; - const handleExportTypeChange = (keys: Selection) => { - setExportType(new Set(Array.from(keys as Set))); - }; - return ( <>
diff --git a/frontend/types/case.ts b/frontend/types/case.ts index 4304e80..a90e828 100644 --- a/frontend/types/case.ts +++ b/frontend/types/case.ts @@ -77,13 +77,13 @@ type CasesMessages = { caseDescription: string; create: string; pleaseEnter: string; + filter: string; clearAll: string; apply: string; selectPriorities: string; selected: string; type: string; selectTypes: string; - searchPlaceholder: string; }; type CaseMessages = { diff --git a/frontend/utils/caseControl.ts b/frontend/utils/caseControl.ts index fced7d1..0b8dfb7 100644 --- a/frontend/utils/caseControl.ts +++ b/frontend/utils/caseControl.ts @@ -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}`]; + if (title) { + queryParams.push(`title=${title}`); + } + if (priority && priority.length > 0) { queryParams.push(`priority=${priority.join(',')}`); } @@ -37,10 +41,6 @@ async function fetchCases(jwt: string, folderId: number, priority?: number[], ty queryParams.push(`type=${type.join(',')}`); } - if (q) { - queryParams.push(`q=${q}`); - } - const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; const url = `${apiServer}/cases${query}`; diff --git a/frontend/utils/highlightSearchTerm.tsx b/frontend/utils/highlightSearchTerm.tsx index eb1216c..d5aa355 100644 --- a/frontend/utils/highlightSearchTerm.tsx +++ b/frontend/utils/highlightSearchTerm.tsx @@ -12,7 +12,7 @@ export function highlightSearchTerm({ text, searchTerm, className, - minSearchLength = 2, + minSearchLength = 1, }: HighlightSearchTermProps): ReactNode { if (!text || !searchTerm || searchTerm.length < minSearchLength) { return text || null;