diff --git a/src/features/authFiles/uiState.ts b/src/features/authFiles/uiState.ts index dd1a6a44..5631faea 100644 --- a/src/features/authFiles/uiState.ts +++ b/src/features/authFiles/uiState.ts @@ -7,7 +7,6 @@ export type AuthFilesUiState = { problemOnly?: boolean; compactMode?: boolean; search?: string; - regexSearchMode?: boolean; page?: number; pageSize?: number; regularPageSize?: number; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 0180caf2..e3c92930 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -497,15 +497,11 @@ "pagination_next": "Next", "pagination_info": "Page {{current}} / {{total}} · {{count}} files", "search_label": "Search configs", - "search_placeholder": "Filter by name, type, or provider", - "search_regex_placeholder": "Match name, type, or provider with a regex", - "search_regex_invalid": "Enter a valid regex pattern (max {{max}} characters)", - "search_regex_unsafe": "This regex may freeze the page and has been blocked (avoid nested quantifiers, alternation in repeated groups, or backreferences)", + "search_placeholder": "Filter by name, type, or provider. Use * as a wildcard", "problem_filter_label": "Problem Filter", "problem_filter_only": "Only show problematic credentials", "display_options_label": "Display options", "compact_mode_label": "Compact mode", - "regex_search_mode_label": "Regex mode", "sort_label": "Sort", "sort_default": "Default", "sort_az": "A-Z Name", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 261c6de9..4b2bf270 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -497,15 +497,11 @@ "pagination_next": "Следующая", "pagination_info": "Страница {{current}} / {{total}} · {{count}} файлов", "search_label": "Поиск конфигов", - "search_placeholder": "Фильтр по имени, типу или провайдеру", - "search_regex_placeholder": "Сопоставление имени, типа или провайдера по regex", - "search_regex_invalid": "Введите корректный regex-шаблон (не более {{max}} символов)", - "search_regex_unsafe": "Этот regex может вызвать зависание страницы и был заблокирован (избегайте вложенных квантификаторов, альтернативы в повторяющихся группах или обратных ссылок)", + "search_placeholder": "Фильтр по имени, типу или провайдеру, поддерживается wildcard *", "problem_filter_label": "Фильтр проблем", "problem_filter_only": "Показывать только проблемные учётные данные", "display_options_label": "Параметры отображения", "compact_mode_label": "Компактный режим", - "regex_search_mode_label": "Режим regex", "sort_label": "Сортировка", "sort_default": "По умолчанию", "sort_az": "A-Z Имя", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 6017afdf..725f26bf 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -497,15 +497,11 @@ "pagination_next": "下一页", "pagination_info": "第 {{current}} / {{total}} 页 · 共 {{count}} 个文件", "search_label": "搜索配置文件", - "search_placeholder": "输入名称、类型或提供方关键字", - "search_regex_placeholder": "输入正则表达式匹配名称、类型或提供方", - "search_regex_invalid": "请输入有效的正则表达式(最多 {{max}} 个字符)", - "search_regex_unsafe": "该正则表达式可能导致页面卡顿,已阻止执行(避免嵌套量词、重复分组中的 | 或反向引用)", + "search_placeholder": "输入名称、类型或提供方关键字,支持 * 通配", "problem_filter_label": "问题筛选", "problem_filter_only": "仅显示有问题凭证", "display_options_label": "显示选项", "compact_mode_label": "简略模式", - "regex_search_mode_label": "正则模式", "sort_label": "排序", "sort_default": "默认", "sort_az": "A-Z 名称", diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 8f1e4be0..6916b4a8 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -24,7 +24,6 @@ import { IconFilterAll } from '@/components/ui/icons'; import { EmptyState } from '@/components/ui/EmptyState'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { copyToClipboard } from '@/utils/clipboard'; -import { isLikelyUnsafeJsRegex } from '@/utils/regexSafety'; import { MAX_CARD_PAGE_SIZE, MIN_CARD_PAGE_SIZE, @@ -68,7 +67,15 @@ const BATCH_BAR_BASE_TRANSFORM = 'translateX(-50%)'; const BATCH_BAR_HIDDEN_TRANSFORM = 'translateX(-50%) translateY(56px)'; const DEFAULT_REGULAR_PAGE_SIZE = 9; const DEFAULT_COMPACT_PAGE_SIZE = 12; -const MAX_REGEX_SEARCH_PATTERN_LENGTH = 120; + +const escapeWildcardSearchSegment = (value: string) => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +const buildWildcardSearch = (value: string): RegExp | null => { + if (!value.includes('*')) return null; + const pattern = value.split('*').map(escapeWildcardSearchSegment).join('.*'); + return new RegExp(pattern, 'i'); +}; export function AuthFilesPage() { const { t } = useTranslation(); @@ -83,7 +90,6 @@ export function AuthFilesPage() { const [problemOnly, setProblemOnly] = useState(false); const [compactMode, setCompactMode] = useState(false); const [search, setSearch] = useState(''); - const [regexSearchMode, setRegexSearchMode] = useState(false); const [page, setPage] = useState(1); const [pageSizeByMode, setPageSizeByMode] = useState({ regular: DEFAULT_REGULAR_PAGE_SIZE, @@ -204,9 +210,6 @@ export function AuthFilesPage() { if (typeof persisted.search === 'string') { setSearch(persisted.search); } - if (typeof persisted.regexSearchMode === 'boolean') { - setRegexSearchMode(persisted.regexSearchMode); - } if (typeof persisted.page === 'number' && Number.isFinite(persisted.page)) { setPage(Math.max(1, Math.round(persisted.page))); } @@ -242,7 +245,6 @@ export function AuthFilesPage() { problemOnly, compactMode, search, - regexSearchMode, page, pageSize, regularPageSize: pageSizeByMode.regular, @@ -257,7 +259,6 @@ export function AuthFilesPage() { pageSize, pageSizeByMode, problemOnly, - regexSearchMode, search, sortMode, uiStateHydrated, @@ -377,62 +378,24 @@ export function AuthFilesPage() { }, [filesMatchingProblemFilter]); const normalizedSearch = search.trim(); - const { regexSearch, regexSearchErrorKey } = useMemo(() => { - if (!regexSearchMode || !normalizedSearch) { - return { regexSearch: null as RegExp | null, regexSearchErrorKey: undefined as string | undefined }; - } - - if (normalizedSearch.length > MAX_REGEX_SEARCH_PATTERN_LENGTH) { - return { - regexSearch: null, - regexSearchErrorKey: 'auth_files.search_regex_invalid', - }; - } - - if (isLikelyUnsafeJsRegex(normalizedSearch)) { - return { - regexSearch: null, - regexSearchErrorKey: 'auth_files.search_regex_unsafe', - }; - } - - try { - return { regexSearch: new RegExp(normalizedSearch, 'i'), regexSearchErrorKey: undefined }; - } catch { - return { - regexSearch: null, - regexSearchErrorKey: 'auth_files.search_regex_invalid', - }; - } - }, [normalizedSearch, regexSearchMode]); - - const searchError = regexSearchErrorKey - ? t(regexSearchErrorKey, { max: MAX_REGEX_SEARCH_PATTERN_LENGTH }) - : undefined; + const wildcardSearch = useMemo(() => buildWildcardSearch(normalizedSearch), [normalizedSearch]); const filtered = useMemo(() => { + const normalizedTerm = normalizedSearch.toLowerCase(); + return filesMatchingProblemFilter.filter((item) => { const matchType = filter === 'all' || item.type === filter; - const matchSearch = (() => { - if (!normalizedSearch) return true; - if (!regexSearchMode) { - const term = normalizedSearch.toLowerCase(); - return ( - item.name.toLowerCase().includes(term) || - (item.type || '').toString().toLowerCase().includes(term) || - (item.provider || '').toString().toLowerCase().includes(term) - ); - } - - if (!regexSearch) return false; - - return [item.name, item.type, item.provider].some((value) => - regexSearch.test((value || '').toString()) - ); - })(); + const matchSearch = + !normalizedSearch || + [item.name, item.type, item.provider].some((value) => { + const content = (value || '').toString(); + return wildcardSearch + ? wildcardSearch.test(content) + : content.toLowerCase().includes(normalizedTerm); + }); return matchType && matchSearch; }); - }, [filesMatchingProblemFilter, filter, normalizedSearch, regexSearch, regexSearchMode]); + }, [filesMatchingProblemFilter, filter, normalizedSearch, wildcardSearch]); const sorted = useMemo(() => { const copy = [...filtered]; @@ -744,12 +707,7 @@ export function AuthFilesPage() { setSearch(e.target.value); setPage(1); }} - placeholder={ - regexSearchMode - ? t('auth_files.search_regex_placeholder') - : t('auth_files.search_placeholder') - } - error={searchError} + placeholder={t('auth_files.search_placeholder')} />