diff --git a/src/features/authFiles/hooks/useAuthFilesData.ts b/src/features/authFiles/hooks/useAuthFilesData.ts index ccf441a..879f09c 100644 --- a/src/features/authFiles/hooks/useAuthFilesData.ts +++ b/src/features/authFiles/hooks/useAuthFilesData.ts @@ -16,8 +16,10 @@ import { type DeleteAllOptions = { filter: string; problemOnly: boolean; + disabledOnly: boolean; onResetFilterToAll: () => void; onResetProblemOnly: () => void; + onResetDisabledOnly: () => void; }; export type UseAuthFilesDataResult = { @@ -275,17 +277,28 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles const handleDeleteAll = useCallback( (deleteAllOptions: DeleteAllOptions) => { - const { filter, problemOnly, onResetFilterToAll, onResetProblemOnly } = deleteAllOptions; + const { + filter, + problemOnly, + disabledOnly, + onResetFilterToAll, + onResetProblemOnly, + onResetDisabledOnly, + } = deleteAllOptions; const isFiltered = filter !== 'all'; const isProblemOnly = problemOnly === true; + const isDisabledOnly = disabledOnly === true; const typeLabel = isFiltered ? getTypeLabel(t, filter) : t('auth_files.filter_all'); - const confirmMessage = isProblemOnly - ? isFiltered + let confirmMessage = t('auth_files.delete_all_confirm'); + if (isDisabledOnly) { + confirmMessage = t('auth_files.delete_filtered_result_confirm'); + } else if (isProblemOnly) { + confirmMessage = isFiltered ? t('auth_files.delete_problem_filtered_confirm', { type: typeLabel }) - : t('auth_files.delete_problem_confirm') - : isFiltered - ? t('auth_files.delete_filtered_confirm', { type: typeLabel }) - : t('auth_files.delete_all_confirm'); + : t('auth_files.delete_problem_confirm'); + } else if (isFiltered) { + confirmMessage = t('auth_files.delete_filtered_confirm', { type: typeLabel }); + } showConfirmation({ title: t('auth_files.delete_all_title', { defaultValue: 'Delete All Files' }), @@ -295,7 +308,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles onConfirm: async () => { setDeletingAll(true); try { - if (!isFiltered && !isProblemOnly) { + if (!isFiltered && !isProblemOnly && !isDisabledOnly) { await authFilesApi.deleteAll(); showNotification(t('auth_files.delete_all_success'), 'success'); setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); @@ -305,15 +318,19 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles if (isRuntimeOnlyAuthFile(file)) return false; if (isFiltered && file.type !== filter) return false; if (isProblemOnly && !hasAuthFileStatusMessage(file)) return false; + if (isDisabledOnly && file.disabled !== true) return false; return true; }); if (filesToDelete.length === 0) { - const emptyMessage = isProblemOnly - ? isFiltered + let emptyMessage = t('auth_files.delete_filtered_none', { type: typeLabel }); + if (isDisabledOnly) { + emptyMessage = t('auth_files.delete_filtered_result_none'); + } else if (isProblemOnly) { + emptyMessage = isFiltered ? t('auth_files.delete_problem_filtered_none', { type: typeLabel }) - : t('auth_files.delete_problem_none') - : t('auth_files.delete_filtered_none', { type: typeLabel }); + : t('auth_files.delete_problem_none'); + } showNotification(emptyMessage, 'info'); setDeletingAll(false); return; @@ -327,7 +344,12 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles applyDeletedFiles(result.files); - if (failed === 0 && isProblemOnly) { + if (failed === 0 && isDisabledOnly) { + showNotification( + t('auth_files.delete_filtered_result_success', { count: success }), + 'success' + ); + } else if (failed === 0 && isProblemOnly) { showNotification( isFiltered ? t('auth_files.delete_problem_filtered_success', { @@ -342,6 +364,11 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), 'success' ); + } else if (isDisabledOnly) { + showNotification( + t('auth_files.delete_filtered_result_partial', { success, failed }), + 'warning' + ); } else if (isProblemOnly) { showNotification( isFiltered @@ -366,6 +393,9 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles if (isProblemOnly) { onResetProblemOnly(); } + if (isDisabledOnly) { + onResetDisabledOnly(); + } } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : ''; diff --git a/src/features/authFiles/uiState.ts b/src/features/authFiles/uiState.ts index 403b00d..618bff7 100644 --- a/src/features/authFiles/uiState.ts +++ b/src/features/authFiles/uiState.ts @@ -5,6 +5,7 @@ export type AuthFilesSortMode = (typeof AUTH_FILES_SORT_MODES)[number]; export type AuthFilesUiState = { filter?: string; problemOnly?: boolean; + disabledOnly?: boolean; compactMode?: boolean; search?: string; page?: number; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 80ee210..c4af0f2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -501,6 +501,8 @@ "delete_problem_button_with_type": "Delete Problematic {{type}} Files", "delete_problem_confirm": "Are you sure you want to delete all problematic auth files? This operation cannot be undone!", "delete_problem_filtered_confirm": "Are you sure you want to delete all problematic {{type}} auth files? This operation cannot be undone!", + "delete_filtered_result_button": "Delete filtered results", + "delete_filtered_result_confirm": "Are you sure you want to delete auth files in the current filtered results? This operation cannot be undone!", "upload_error_json": "Only JSON files are allowed", "upload_error_size": "File size cannot exceed {{maxSize}}", "upload_success": "File uploaded successfully", @@ -516,6 +518,9 @@ "delete_problem_filtered_partial": "Problematic {{type}} auth files deletion finished: {{success}} succeeded, {{failed}} failed", "delete_problem_none": "No deletable problematic auth files under the current filter", "delete_problem_filtered_none": "No deletable problematic {{type}} auth files under the current filter", + "delete_filtered_result_success": "Deleted {{count}} auth files from the filtered results successfully", + "delete_filtered_result_partial": "Filtered result deletion finished: {{success}} succeeded, {{failed}} failed", + "delete_filtered_result_none": "No deletable auth files in the current filtered results", "files_count": "files", "pagination_prev": "Previous", "pagination_next": "Next", @@ -524,6 +529,7 @@ "search_placeholder": "Filter by name, type, or provider. Use * as a wildcard", "problem_filter_label": "Problem Filter", "problem_filter_only": "Only show problematic credentials", + "disabled_filter_only": "Only show disabled credentials", "display_options_label": "Display options", "compact_mode_label": "Compact mode", "sort_label": "Sort", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index d86de27..0210709 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -501,6 +501,8 @@ "delete_problem_button_with_type": "Удалить проблемные файлы {{type}}", "delete_problem_confirm": "Удалить все проблемные файлы авторизации? Это действие нельзя отменить!", "delete_problem_filtered_confirm": "Удалить все проблемные файлы авторизации {{type}}? Это действие нельзя отменить!", + "delete_filtered_result_button": "Удалить результаты фильтра", + "delete_filtered_result_confirm": "Удалить файлы авторизации из текущих результатов фильтра? Это действие нельзя отменить!", "upload_error_json": "Допустимы только файлы JSON", "upload_error_size": "Размер файла не может превышать {{maxSize}}", "upload_success": "Файл успешно загружен", @@ -516,6 +518,9 @@ "delete_problem_filtered_partial": "Удаление проблемных файлов авторизации {{type}} завершено: успешных {{success}}, ошибок {{failed}}", "delete_problem_none": "Нет проблемных файлов авторизации для удаления при текущем фильтре", "delete_problem_filtered_none": "Нет проблемных файлов авторизации {{type}} для удаления при текущем фильтре", + "delete_filtered_result_success": "Удалено файлов авторизации из результатов фильтра: {{count}}", + "delete_filtered_result_partial": "Удаление результатов фильтра завершено: успешных {{success}}, ошибок {{failed}}", + "delete_filtered_result_none": "Нет файлов авторизации для удаления в текущих результатах фильтра", "files_count": "файлов", "pagination_prev": "Предыдущая", "pagination_next": "Следующая", @@ -524,6 +529,7 @@ "search_placeholder": "Фильтр по имени, типу или провайдеру, поддерживается wildcard *", "problem_filter_label": "Фильтр проблем", "problem_filter_only": "Показывать только проблемные учётные данные", + "disabled_filter_only": "Показывать только отключённые учётные данные", "display_options_label": "Параметры отображения", "compact_mode_label": "Компактный режим", "sort_label": "Сортировка", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 403ad52..dabfd9b 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -501,6 +501,8 @@ "delete_problem_button_with_type": "删除 {{type}} 问题凭证", "delete_problem_confirm": "确定要删除所有有问题的认证文件吗?此操作不可恢复!", "delete_problem_filtered_confirm": "确定要删除筛选出的有问题的 {{type}} 认证文件吗?此操作不可恢复!", + "delete_filtered_result_button": "删除筛选结果", + "delete_filtered_result_confirm": "确定要删除当前筛选结果中的认证文件吗?此操作不可恢复!", "upload_error_json": "只能上传JSON文件", "upload_error_size": "文件大小不能超过 {{maxSize}}", "upload_success": "文件上传成功", @@ -516,6 +518,9 @@ "delete_problem_filtered_partial": "有问题的 {{type}} 认证文件删除完成,成功 {{success}} 个,失败 {{failed}} 个", "delete_problem_none": "当前没有可删除的有问题认证文件", "delete_problem_filtered_none": "当前筛选类型 ({{type}}) 下没有可删除的有问题认证文件", + "delete_filtered_result_success": "成功删除 {{count}} 个筛选结果中的认证文件", + "delete_filtered_result_partial": "筛选结果删除完成,成功 {{success}} 个,失败 {{failed}} 个", + "delete_filtered_result_none": "当前筛选结果中没有可删除的认证文件", "files_count": "个文件", "pagination_prev": "上一页", "pagination_next": "下一页", @@ -524,6 +529,7 @@ "search_placeholder": "输入名称、类型或提供方关键字,支持 * 通配", "problem_filter_label": "问题筛选", "problem_filter_only": "仅显示有问题凭证", + "disabled_filter_only": "仅显示已停用凭证", "display_options_label": "显示选项", "compact_mode_label": "简略模式", "sort_label": "排序", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 7f9aeba..f760f71 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -501,6 +501,8 @@ "delete_problem_button_with_type": "刪除 {{type}} 問題憑證", "delete_problem_confirm": "確定要刪除所有有問題的驗證檔案嗎?此操作無法還原!", "delete_problem_filtered_confirm": "確定要刪除篩選出的有問題的 {{type}} 驗證檔案嗎?此操作無法還原!", + "delete_filtered_result_button": "刪除篩選結果", + "delete_filtered_result_confirm": "確定要刪除目前篩選結果中的驗證檔案嗎?此操作無法還原!", "upload_error_json": "只能上傳 JSON 檔案", "upload_error_size": "檔案大小不能超過 {{maxSize}}", "upload_success": "檔案上傳成功", @@ -516,6 +518,9 @@ "delete_problem_filtered_partial": "有問題的 {{type}} 驗證檔案刪除完成,成功 {{success}} 個,失敗 {{failed}} 個", "delete_problem_none": "目前沒有可刪除的有問題驗證檔案", "delete_problem_filtered_none": "目前篩選類型({{type}})下沒有可刪除的有問題驗證檔案", + "delete_filtered_result_success": "成功刪除 {{count}} 個篩選結果中的驗證檔案", + "delete_filtered_result_partial": "篩選結果刪除完成,成功 {{success}} 個,失敗 {{failed}} 個", + "delete_filtered_result_none": "目前篩選結果中沒有可刪除的驗證檔案", "files_count": "個檔案", "pagination_prev": "上一頁", "pagination_next": "下一頁", @@ -524,6 +529,7 @@ "search_placeholder": "輸入名稱、類型或供應方關鍵字,支援 * 萬用字元", "problem_filter_label": "問題篩選", "problem_filter_only": "僅顯示有問題憑證", + "disabled_filter_only": "僅顯示已停用憑證", "display_options_label": "顯示選項", "compact_mode_label": "簡略模式", "sort_label": "排序", diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 6916b4a..efa2658 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -88,6 +88,7 @@ export function AuthFilesPage() { const [filter, setFilter] = useState<'all' | string>('all'); const [problemOnly, setProblemOnly] = useState(false); + const [disabledOnly, setDisabledOnly] = useState(false); const [compactMode, setCompactMode] = useState(false); const [search, setSearch] = useState(''); const [page, setPage] = useState(1); @@ -201,6 +202,9 @@ export function AuthFilesPage() { if (typeof persisted.problemOnly === 'boolean') { setProblemOnly(persisted.problemOnly); } + if (typeof persisted.disabledOnly === 'boolean') { + setDisabledOnly(persisted.disabledOnly); + } if ( typeof persistedCompactMode !== 'boolean' && typeof persisted.compactMode === 'boolean' @@ -243,6 +247,7 @@ export function AuthFilesPage() { writeAuthFilesUiState({ filter, problemOnly, + disabledOnly, compactMode, search, page, @@ -254,6 +259,7 @@ export function AuthFilesPage() { writePersistedAuthFilesCompactMode(compactMode); }, [ compactMode, + disabledOnly, filter, page, pageSize, @@ -354,9 +360,14 @@ export function AuthFilesPage() { return Array.from(types); }, [files]); - const filesMatchingProblemFilter = useMemo( - () => (problemOnly ? files.filter(hasAuthFileStatusMessage) : files), - [files, problemOnly] + const filesMatchingStatusFilters = useMemo( + () => + files.filter((file) => { + if (problemOnly && !hasAuthFileStatusMessage(file)) return false; + if (disabledOnly && file.disabled !== true) return false; + return true; + }), + [disabledOnly, files, problemOnly] ); const sortOptions = useMemo( @@ -369,13 +380,13 @@ export function AuthFilesPage() { ); const typeCounts = useMemo(() => { - const counts: Record = { all: filesMatchingProblemFilter.length }; - filesMatchingProblemFilter.forEach((file) => { + const counts: Record = { all: filesMatchingStatusFilters.length }; + filesMatchingStatusFilters.forEach((file) => { if (!file.type) return; counts[file.type] = (counts[file.type] || 0) + 1; }); return counts; - }, [filesMatchingProblemFilter]); + }, [filesMatchingStatusFilters]); const normalizedSearch = search.trim(); const wildcardSearch = useMemo(() => buildWildcardSearch(normalizedSearch), [normalizedSearch]); @@ -383,7 +394,7 @@ export function AuthFilesPage() { const filtered = useMemo(() => { const normalizedTerm = normalizedSearch.toLowerCase(); - return filesMatchingProblemFilter.filter((item) => { + return filesMatchingStatusFilters.filter((item) => { const matchType = filter === 'all' || item.type === filter; const matchSearch = !normalizedSearch || @@ -395,7 +406,7 @@ export function AuthFilesPage() { }); return matchType && matchSearch; }); - }, [filesMatchingProblemFilter, filter, normalizedSearch, wildcardSearch]); + }, [filesMatchingStatusFilters, filter, normalizedSearch, wildcardSearch]); const sorted = useMemo(() => { const copy = [...filtered]; @@ -634,13 +645,19 @@ export function AuthFilesPage() { ); - const deleteAllButtonLabel = problemOnly - ? filter === 'all' - ? t('auth_files.delete_problem_button') - : t('auth_files.delete_problem_button_with_type', { type: getTypeLabel(t, filter) }) - : filter === 'all' + const deleteAllButtonLabel = (() => { + if (disabledOnly) { + return t('auth_files.delete_filtered_result_button'); + } + if (problemOnly) { + return filter === 'all' + ? t('auth_files.delete_problem_button') + : t('auth_files.delete_problem_button_with_type', { type: getTypeLabel(t, filter) }); + } + return filter === 'all' ? t('auth_files.delete_all_button') : `${t('common.delete')} ${getTypeLabel(t, filter)}`; + })(); return (
@@ -671,8 +688,10 @@ export function AuthFilesPage() { handleDeleteAll({ filter, problemOnly, + disabledOnly, onResetFilterToAll: () => setFilter('all'), onResetProblemOnly: () => setProblemOnly(false), + onResetDisabledOnly: () => setDisabledOnly(false), }) } disabled={disableControls || loading || deletingAll} @@ -757,6 +776,21 @@ export function AuthFilesPage() { } />
+
+ { + setDisabledOnly(value); + setPage(1); + }} + ariaLabel={t('auth_files.disabled_filter_only')} + label={ + + {t('auth_files.disabled_filter_only')} + + } + /> +