From 1457b3bfa1e4d3a58888c8259bdc4779c8cc3995 Mon Sep 17 00:00:00 2001 From: Nikita Bakun Date: Tue, 1 Sep 2026 00:08:27 +0300 Subject: [PATCH] fix(object-list): keep searching across all pages --- components/object/list.tsx | 84 ++++++++++++++++++++-------- i18n/locales/ar-MA.json | 6 +- i18n/locales/de-DE.json | 6 +- i18n/locales/en-US.json | 6 +- i18n/locales/es-ES.json | 6 +- i18n/locales/fr-FR.json | 6 +- i18n/locales/id-ID.json | 6 +- i18n/locales/it-IT.json | 6 +- i18n/locales/ja-JP.json | 6 +- i18n/locales/ko-KR.json | 6 +- i18n/locales/pt-BR.json | 6 +- i18n/locales/ru-RU.json | 6 +- i18n/locales/tr-TR.json | 6 +- i18n/locales/vi-VN.json | 6 +- i18n/locales/zh-CN.json | 6 +- lib/object-list-pagination.ts | 1 + lib/object-list-state.ts | 50 ++++++++++++++--- tests/lib/object-list-source.test.js | 11 +++- tests/lib/object-list-state.test.ts | 84 +++++++++++++++++++++++++++- 19 files changed, 236 insertions(+), 78 deletions(-) diff --git a/components/object/list.tsx b/components/object/list.tsx index b96dd08d..080aadd7 100644 --- a/components/object/list.tsx +++ b/components/object/list.tsx @@ -54,11 +54,17 @@ import { normalizeDateToIso } from "@/lib/safe-date" import { buildBucketPath } from "@/lib/bucket-path" import { createObjectListScope, + resolveObjectListAutoSearchState, resolveObjectListDisplayState, + resolveObjectListLoadButtonMode, shouldApplyObjectListResponse, shouldResetObjectListPagination, } from "@/lib/object-list-state" -import { OBJECT_LIST_DEFAULT_PAGE_SIZE, resolveObjectListPageSize } from "@/lib/object-list-pagination" +import { + OBJECT_LIST_AUTO_SEARCH_DEBOUNCE_MS, + OBJECT_LIST_DEFAULT_PAGE_SIZE, + resolveObjectListPageSize, +} from "@/lib/object-list-pagination" import { resolveBucketVersioningState, shouldForceDeleteObjects, @@ -136,6 +142,7 @@ export function ObjectList({ const tasks = useTasks() const [searchTerm, setSearchTerm] = React.useState("") + const [autoSearchStopped, setAutoSearchStopped] = React.useState(false) const [showDeleted, setShowDeleted] = useLocalStorage("object-list-show-deleted", false) const [loading, setLoading] = React.useState(false) const [listError, setListError] = React.useState(null) @@ -387,23 +394,24 @@ export function ObjectList({ return item.Key !== prefix }) }, [data, searchTerm, prefix, displayKey]) + const hasSearchTerm = Boolean(searchTerm.trim()) + const hasMore = Boolean(nextToken) + const { isAutoSearching, canResumeSearch } = resolveObjectListAutoSearchState( + hasSearchTerm, + hasMore, + autoSearchStopped, + ) const displayState = resolveObjectListDisplayState({ searchTerm, filteredCount: filteredData.length, loadedCount: data.length, - hasMore: Boolean(nextToken), + hasMore: hasMore && !autoSearchStopped, loading, }) - const filteredEmptyState = displayState === "filtered-partial" || displayState === "filtered-empty" - const emptyTitle = filteredEmptyState - ? t(displayState === "filtered-partial" ? "No matches in loaded objects" : "No matching objects") - : t("No Objects") + const filteredEmptyState = displayState === "filtered-empty" + const emptyTitle = filteredEmptyState ? t("No matching objects") : t("No Objects") const emptyDescription = filteredEmptyState - ? t( - displayState === "filtered-partial" - ? "More objects have not been searched yet." - : "No loaded objects match this filter.", - ) + ? t("No loaded objects match this filter.") : t("Upload files or create folders to populate this bucket.") const downloadFile = React.useCallback( @@ -765,6 +773,24 @@ export function ObjectList({ void fetchObjects({ token: nextToken, append: true }) }, [fetchObjects, nextToken]) + React.useEffect(() => { + setAutoSearchStopped(false) + }, [searchTerm]) + + React.useEffect(() => { + const term = searchTerm.trim() + if (!term) return + if (autoSearchStopped) return + if (!nextToken || loading) return + + const timer = window.setTimeout(() => { + if (searchTerm.trim() !== term || autoSearchStopped || loadingRef.current || !nextToken) return + loadNextBatch() + }, OBJECT_LIST_AUTO_SEARCH_DEBOUNCE_MS) + + return () => window.clearTimeout(timer) + }, [searchTerm, nextToken, loadNextBatch, loading, autoSearchStopped]) + React.useEffect(() => { const node = loadMoreRef.current if (!node || !nextToken || typeof IntersectionObserver === "undefined") return @@ -785,6 +811,22 @@ export function ObjectList({ } }, [loadNextBatch, nextToken]) + const loadButtonMode = resolveObjectListLoadButtonMode({ isAutoSearching, canResumeSearch, loading, hasMore }) + const loadButtonLabel = { + stop: t("Stop search"), + resume: t("Resume search"), + loading: t("Loading more objects"), + load: t("Load next objects"), + done: t("All objects loaded"), + }[loadButtonMode] + const loadButtonAction = + loadButtonMode === "stop" + ? () => setAutoSearchStopped(true) + : loadButtonMode === "resume" + ? () => setAutoSearchStopped(false) + : loadNextBatch + const loadButtonDisabled = loadButtonMode === "loading" || loadButtonMode === "done" + return (
- {loading ? : null} - - {loading ? t("Loading more objects") : nextToken ? t("Load next objects") : t("All objects loaded")} - + {loading || isAutoSearching ? : null} + {loadButtonLabel} ) : null}
) : null} diff --git a/i18n/locales/ar-MA.json b/i18n/locales/ar-MA.json index 0bcd0d1b..8027c718 100644 --- a/i18n/locales/ar-MA.json +++ b/i18n/locales/ar-MA.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "لا توجد موفّرات OIDC مُعدّة بعد.", "No Objects": "لا توجد كائنات", "No loaded objects match this filter.": "لا تتطابق أي كائنات محملة مع عامل التصفية هذا.", - "No matches in loaded objects": "لا توجد نتائج في الكائنات المحملة", "No matching objects": "لا توجد كائنات مطابقة", - "More objects have not been searched yet.": "لا تزال هناك كائنات لم يتم البحث فيها.", "No Policies": "لا توجد سياسات", "No Secret Configured": "لم يتم إعداد سر", "No Selection": "لا يوجد تحديد", @@ -1526,11 +1524,13 @@ "Rename Failed": "فشلت إعادة التسمية", "Rename Object": "إعادة تسمية الكائن", "Renaming object": "جارٍ إعادة تسمية الكائن", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "الكائنات المحملة فقط", "Load next objects": "تحميل الكائنات التالية", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "تم تحميل جميع الكائنات", "Failed to load more objects. Loaded objects remain available.": "فشل تحميل المزيد من الكائنات. تظل الكائنات المحملة متاحة.", "Retry": "إعادة المحاولة", diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json index 65f03d14..a704a527 100644 --- a/i18n/locales/de-DE.json +++ b/i18n/locales/de-DE.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Es sind noch keine OIDC-Anbieter konfiguriert.", "No Objects": "Keine Objekte", "No loaded objects match this filter.": "Keine geladenen Objekte entsprechen diesem Filter.", - "No matches in loaded objects": "Keine Treffer in geladenen Objekten", "No matching objects": "Keine passenden Objekte", - "More objects have not been searched yet.": "Weitere Objekte wurden noch nicht durchsucht.", "No Policies": "Keine Richtlinien", "No Secret Configured": "Kein Secret konfiguriert", "No Selection": "Keine Auswahl", @@ -1526,11 +1524,13 @@ "Rename Failed": "Umbenennen fehlgeschlagen", "Rename Object": "Objekt umbenennen", "Renaming object": "Objekt wird umbenannt", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Nur geladene Objekte", "Load next objects": "Nächste Objekte laden", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Alle Objekte geladen", "Failed to load more objects. Loaded objects remain available.": "Weitere Objekte konnten nicht geladen werden. Geladene Objekte bleiben verfügbar.", "Retry": "Erneut versuchen", diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index a219004a..684548e2 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -698,9 +698,7 @@ "No OIDC providers configured yet.": "No OIDC providers configured yet.", "No Objects": "No Objects", "No loaded objects match this filter.": "No loaded objects match this filter.", - "No matches in loaded objects": "No matches in loaded objects", "No matching objects": "No matching objects", - "More objects have not been searched yet.": "More objects have not been searched yet.", "No Policies": "No Policies", "No Secret Configured": "No Secret Configured", "No Selection": "No Selection", @@ -1526,11 +1524,13 @@ "Rename Failed": "Rename Failed", "Rename Object": "Rename Object", "Renaming object": "Renaming object", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Search objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Loaded objects only", "Load next objects": "Load next objects", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "All objects loaded", "Failed to load more objects. Loaded objects remain available.": "Failed to load more objects. Loaded objects remain available.", "Retry": "Retry", diff --git a/i18n/locales/es-ES.json b/i18n/locales/es-ES.json index c94f164b..935389b3 100644 --- a/i18n/locales/es-ES.json +++ b/i18n/locales/es-ES.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Aún no hay proveedores OIDC configurados.", "No Objects": "Sin Objetos", "No loaded objects match this filter.": "Ningún objeto cargado coincide con este filtro.", - "No matches in loaded objects": "Sin coincidencias en los objetos cargados", "No matching objects": "No hay objetos coincidentes", - "More objects have not been searched yet.": "Aún quedan objetos por buscar.", "No Policies": "Sin Políticas", "No Secret Configured": "No hay secreto configurado", "No Selection": "Sin Selección", @@ -1526,11 +1524,13 @@ "Rename Failed": "Error al renombrar", "Rename Object": "Renombrar objeto", "Renaming object": "Renombrando objeto", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Solo objetos cargados", "Load next objects": "Cargar los siguientes objetos", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Todos los objetos cargados", "Failed to load more objects. Loaded objects remain available.": "No se pudieron cargar más objetos. Los objetos cargados siguen disponibles.", "Retry": "Reintentar", diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json index 117eca61..48d44362 100644 --- a/i18n/locales/fr-FR.json +++ b/i18n/locales/fr-FR.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Aucun fournisseur OIDC n'est encore configuré.", "No Objects": "Aucun objet", "No loaded objects match this filter.": "Aucun objet chargé ne correspond à ce filtre.", - "No matches in loaded objects": "Aucune correspondance dans les objets chargés", "No matching objects": "Aucun objet correspondant", - "More objects have not been searched yet.": "D'autres objets n'ont pas encore été recherchés.", "No Policies": "Aucune politique", "No Secret Configured": "Aucun secret configuré", "No Selection": "Aucune sélection", @@ -1526,11 +1524,13 @@ "Rename Failed": "Échec du renommage", "Rename Object": "Renommer l’objet", "Renaming object": "Renommage de l’objet", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Objets chargés uniquement", "Load next objects": "Charger les objets suivants", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Tous les objets sont chargés", "Failed to load more objects. Loaded objects remain available.": "Impossible de charger d’autres objets. Les objets chargés restent disponibles.", "Retry": "Réessayer", diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json index 3d56791a..33153c44 100644 --- a/i18n/locales/id-ID.json +++ b/i18n/locales/id-ID.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Belum ada penyedia OIDC yang dikonfigurasi.", "No Objects": "Tidak Ada Objek", "No loaded objects match this filter.": "Tidak ada objek yang dimuat yang cocok dengan filter ini.", - "No matches in loaded objects": "Tidak ada kecocokan dalam objek yang dimuat", "No matching objects": "Tidak ada objek yang cocok", - "More objects have not been searched yet.": "Masih ada objek yang belum dicari.", "No Policies": "Tidak Ada Kebijakan", "No Secret Configured": "Belum Ada Secret yang Dikonfigurasi", "No Selection": "Tidak Ada Pilihan", @@ -1526,11 +1524,13 @@ "Rename Failed": "Gagal mengganti nama", "Rename Object": "Ganti nama objek", "Renaming object": "Mengganti nama objek", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Hanya objek yang dimuat", "Load next objects": "Muat objek berikutnya", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Semua objek telah dimuat", "Failed to load more objects. Loaded objects remain available.": "Gagal memuat lebih banyak objek. Objek yang telah dimuat tetap tersedia.", "Retry": "Coba lagi", diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json index bf9c2f95..d730c567 100644 --- a/i18n/locales/it-IT.json +++ b/i18n/locales/it-IT.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Non è ancora configurato alcun provider OIDC.", "No Objects": "Nessun oggetto", "No loaded objects match this filter.": "Nessun oggetto caricato corrisponde a questo filtro.", - "No matches in loaded objects": "Nessuna corrispondenza negli oggetti caricati", "No matching objects": "Nessun oggetto corrispondente", - "More objects have not been searched yet.": "Altri oggetti non sono ancora stati cercati.", "No Policies": "Nessuna politica", "No Secret Configured": "Nessun segreto configurato", "No Selection": "Nessuna selezione", @@ -1526,11 +1524,13 @@ "Rename Failed": "Rinomina non riuscita", "Rename Object": "Rinomina oggetto", "Renaming object": "Rinomina oggetto in corso", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Solo oggetti caricati", "Load next objects": "Carica gli oggetti successivi", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Tutti gli oggetti sono stati caricati", "Failed to load more objects. Loaded objects remain available.": "Impossibile caricare altri oggetti. Gli oggetti caricati restano disponibili.", "Retry": "Riprova", diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json index 0b391f72..a6fe06b8 100644 --- a/i18n/locales/ja-JP.json +++ b/i18n/locales/ja-JP.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "まだ OIDC プロバイダーは設定されていません。", "No Objects": "オブジェクトなし", "No loaded objects match this filter.": "読み込まれたオブジェクトはこのフィルターに一致しません。", - "No matches in loaded objects": "読み込み済みオブジェクトに一致なし", "No matching objects": "一致するオブジェクトなし", - "More objects have not been searched yet.": "まだ検索されていないオブジェクトがあります。", "No Policies": "ポリシーなし", "No Secret Configured": "シークレット未設定", "No Selection": "選択なし", @@ -1526,11 +1524,13 @@ "Rename Failed": "名前の変更に失敗しました", "Rename Object": "オブジェクト名を変更", "Renaming object": "オブジェクト名を変更中", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "読み込み済みオブジェクトのみ", "Load next objects": "次のオブジェクトを読み込む", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "すべてのオブジェクトを読み込みました", "Failed to load more objects. Loaded objects remain available.": "追加のオブジェクトを読み込めませんでした。読み込み済みのオブジェクトは引き続き利用できます。", "Retry": "再試行", diff --git a/i18n/locales/ko-KR.json b/i18n/locales/ko-KR.json index a09f4e11..a5fccf47 100644 --- a/i18n/locales/ko-KR.json +++ b/i18n/locales/ko-KR.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "아직 구성된 OIDC 공급자가 없습니다.", "No Objects": "객체 없음", "No loaded objects match this filter.": "로드된 객체가 이 필터와 일치하지 않습니다.", - "No matches in loaded objects": "로드된 객체에서 일치 항목 없음", "No matching objects": "일치하는 객체 없음", - "More objects have not been searched yet.": "아직 검색되지 않은 객체가 더 있습니다.", "No Policies": "정책 없음", "No Secret Configured": "구성된 시크릿 없음", "No Selection": "선택 없음", @@ -1526,11 +1524,13 @@ "Rename Failed": "이름 변경 실패", "Rename Object": "객체 이름 변경", "Renaming object": "객체 이름 변경 중", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "로드된 객체만", "Load next objects": "다음 객체 로드", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "모든 객체 로드됨", "Failed to load more objects. Loaded objects remain available.": "추가 객체를 로드하지 못했습니다. 로드된 객체는 계속 사용할 수 있습니다.", "Retry": "다시 시도", diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json index 4087c9c4..64746cfb 100644 --- a/i18n/locales/pt-BR.json +++ b/i18n/locales/pt-BR.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Ainda não há provedores OIDC configurados.", "No Objects": "Sem Objetos", "No loaded objects match this filter.": "Nenhum objeto carregado corresponde a este filtro.", - "No matches in loaded objects": "Nenhuma correspondência nos objetos carregados", "No matching objects": "Nenhum objeto correspondente", - "More objects have not been searched yet.": "Ainda há objetos que não foram pesquisados.", "No Policies": "Sem Políticas", "No Secret Configured": "Nenhum segredo configurado", "No Selection": "Sem Seleção", @@ -1526,11 +1524,13 @@ "Rename Failed": "Falha ao renomear", "Rename Object": "Renomear objeto", "Renaming object": "Renomeando objeto", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Apenas objetos carregados", "Load next objects": "Carregar próximos objetos", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Todos os objetos carregados", "Failed to load more objects. Loaded objects remain available.": "Falha ao carregar mais objetos. Os objetos carregados continuam disponíveis.", "Retry": "Tentar novamente", diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json index bbda3010..7749423a 100644 --- a/i18n/locales/ru-RU.json +++ b/i18n/locales/ru-RU.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "OIDC-провайдеры еще не настроены.", "No Objects": "Нет объектов", "No loaded objects match this filter.": "Ни один загруженный объект не соответствует этому фильтру.", - "No matches in loaded objects": "Нет совпадений среди загруженных объектов", "No matching objects": "Нет подходящих объектов", - "More objects have not been searched yet.": "Остались объекты, которые еще не были проверены.", "No Policies": "Нет политик", "No Secret Configured": "Секрет не настроен", "No Selection": "Нет выбора", @@ -1526,11 +1524,13 @@ "Rename Failed": "Не удалось переименовать", "Rename Object": "Переименовать объект", "Renaming object": "Переименование объекта", - "Filter loaded objects": "Фильтровать загруженные объекты", + "Search objects": "Фильтровать загруженные объекты", "Loaded {count} objects": "Загружено {count} объектов", "Filtering and sorting apply to loaded objects": "Фильтрация и сортировка применяются к загруженным объектам", "Loaded objects only": "Только загруженные объекты", "Load next objects": "Загрузить следующие объекты", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Все объекты загружены", "Failed to load more objects. Loaded objects remain available.": "Не удалось загрузить дополнительные объекты. Загруженные объекты остаются доступными.", "Retry": "Повторить", diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json index af883fee..0c378e11 100644 --- a/i18n/locales/tr-TR.json +++ b/i18n/locales/tr-TR.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Henüz yapılandırılmış OIDC sağlayıcısı yok.", "No Objects": "Nesne Yok", "No loaded objects match this filter.": "Yüklenen hiçbir nesne bu filtreyle eşleşmiyor.", - "No matches in loaded objects": "Yüklenen nesnelerde eşleşme yok", "No matching objects": "Eşleşen nesne yok", - "More objects have not been searched yet.": "Henüz aranmamış başka nesneler var.", "No Policies": "Politika Yok", "No Secret Configured": "Sır Yapılandırılmadı", "No Selection": "Seçim Yok", @@ -1526,11 +1524,13 @@ "Rename Failed": "Yeniden adlandırma başarısız", "Rename Object": "Nesneyi yeniden adlandır", "Renaming object": "Nesne yeniden adlandırılıyor", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Yalnızca yüklenen nesneler", "Load next objects": "Sonraki nesneleri yükle", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Tüm nesneler yüklendi", "Failed to load more objects. Loaded objects remain available.": "Daha fazla nesne yüklenemedi. Yüklenen nesneler kullanılabilir durumda kalır.", "Retry": "Yeniden dene", diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json index 67d18f0f..2de1051e 100644 --- a/i18n/locales/vi-VN.json +++ b/i18n/locales/vi-VN.json @@ -699,9 +699,7 @@ "No OIDC providers configured yet.": "Chưa có nhà cung cấp OIDC nào được cấu hình.", "No Objects": "Không có đối tượng nào", "No loaded objects match this filter.": "Không có đối tượng đã tải nào khớp với bộ lọc này.", - "No matches in loaded objects": "Không có kết quả trong các đối tượng đã tải", "No matching objects": "Không có đối tượng phù hợp", - "More objects have not been searched yet.": "Vẫn còn các đối tượng chưa được tìm kiếm.", "No Policies": "Không có chính sách nào", "No Secret Configured": "Chưa cấu hình Secret", "No Selection": "Chưa chọn mục nào", @@ -1526,11 +1524,13 @@ "Rename Failed": "Đổi tên thất bại", "Rename Object": "Đổi tên đối tượng", "Renaming object": "Đang đổi tên đối tượng", - "Filter loaded objects": "Filter loaded objects", + "Search objects": "Filter loaded objects", "Loaded {count} objects": "Loaded {count} objects", "Filtering and sorting apply to loaded objects": "Filtering and sorting apply to loaded objects", "Loaded objects only": "Chỉ các đối tượng đã tải", "Load next objects": "Tải các đối tượng tiếp theo", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "Đã tải tất cả đối tượng", "Failed to load more objects. Loaded objects remain available.": "Không thể tải thêm đối tượng. Các đối tượng đã tải vẫn khả dụng.", "Retry": "Thử lại", diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 5caa8d90..ac255c48 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -698,9 +698,7 @@ "No OIDC providers configured yet.": "当前还没有配置任何 OIDC provider。", "No Objects": "无对象", "No loaded objects match this filter.": "已加载的对象均不匹配此筛选条件。", - "No matches in loaded objects": "已加载对象中无匹配项", "No matching objects": "无匹配对象", - "More objects have not been searched yet.": "仍有更多对象尚未搜索。", "No Policies": "无策略", "No Secret Configured": "未配置密钥", "No Selection": "未选择文件/文件夹", @@ -1526,11 +1524,13 @@ "Rename Failed": "重命名失败", "Rename Object": "重命名对象", "Renaming object": "正在重命名对象", - "Filter loaded objects": "筛选已加载对象", + "Search objects": "筛选已加载对象", "Loaded {count} objects": "已加载 {count} 个对象", "Filtering and sorting apply to loaded objects": "筛选和排序作用于已加载对象", "Loaded objects only": "仅已加载对象", "Load next objects": "加载下一批对象", + "Stop search": "Stop search", + "Resume search": "Resume search", "All objects loaded": "已加载所有对象", "Failed to load more objects. Loaded objects remain available.": "无法加载更多对象。已加载的对象仍然可用。", "Retry": "重试", diff --git a/lib/object-list-pagination.ts b/lib/object-list-pagination.ts index a554a23f..f45a3f94 100644 --- a/lib/object-list-pagination.ts +++ b/lib/object-list-pagination.ts @@ -1,4 +1,5 @@ export const OBJECT_LIST_DEFAULT_PAGE_SIZE = 1000 +export const OBJECT_LIST_AUTO_SEARCH_DEBOUNCE_MS = 250 export function resolveObjectListPageSize(value: unknown): number { if (typeof value === "number" && Number.isInteger(value) && value > 0) { diff --git a/lib/object-list-state.ts b/lib/object-list-state.ts index 1fa7133b..3a1f2a65 100644 --- a/lib/object-list-state.ts +++ b/lib/object-list-state.ts @@ -12,13 +12,7 @@ interface ObjectListResponseGuardParams { activeScope: ObjectListScope } -export type ObjectListDisplayState = - | "loading" - | "empty" - | "filtered-loading" - | "filtered-partial" - | "filtered-empty" - | "content" +export type ObjectListDisplayState = "loading" | "empty" | "filtered-loading" | "filtered-empty" | "content" interface ObjectListDisplayStateParams { searchTerm: string @@ -68,7 +62,45 @@ export function resolveObjectListDisplayState({ return loading && loadedCount === 0 ? "loading" : "empty" } - if (loading) return "filtered-loading" - if (hasMore) return "filtered-partial" + if (loading || hasMore) return "filtered-loading" return "filtered-empty" } + +export interface ObjectListAutoSearchState { + isAutoSearching: boolean + canResumeSearch: boolean +} + +export function resolveObjectListAutoSearchState( + hasSearchTerm: boolean, + hasMore: boolean, + stopped: boolean, +): ObjectListAutoSearchState { + const isSearchable = hasSearchTerm && hasMore + return { + isAutoSearching: isSearchable && !stopped, + canResumeSearch: isSearchable && stopped, + } +} + +export type ObjectListLoadButtonMode = "stop" | "resume" | "loading" | "load" | "done" + +interface ObjectListLoadButtonModeParams { + isAutoSearching: boolean + canResumeSearch: boolean + loading: boolean + hasMore: boolean +} + +export function resolveObjectListLoadButtonMode({ + isAutoSearching, + canResumeSearch, + loading, + hasMore, +}: ObjectListLoadButtonModeParams): ObjectListLoadButtonMode { + if (isAutoSearching) return "stop" + if (canResumeSearch) return "resume" + if (loading) return "loading" + if (hasMore) return "load" + return "done" +} diff --git a/tests/lib/object-list-source.test.js b/tests/lib/object-list-source.test.js index 422bef2b..8f7eb3e6 100644 --- a/tests/lib/object-list-source.test.js +++ b/tests/lib/object-list-source.test.js @@ -36,19 +36,24 @@ test("last modified sorting discloses partial results and keeps continuation vis const source = fs.readFileSync("components/object/list.tsx", "utf8") assert.match(source, /\{nextToken \? \(\s+]*>\{t\("Loaded objects only"\)\}/) - assert.match(source, /\{data\.length > 0 \? \(\s+ 0 \? \(\s+ { const source = fs.readFileSync("components/object/list.tsx", "utf8") assert.equal(source.includes("resolveObjectListDisplayState"), true) - assert.equal(source.includes('displayState === "filtered-partial"'), true) + assert.equal(source.includes('displayState === "filtered-empty"'), true) assert.equal(source.includes('displayState === "filtered-loading"'), true) assert.equal(source.includes("emptyTitle={emptyTitle}"), true) assert.equal(source.includes("emptyDescription={emptyDescription}"), true) diff --git a/tests/lib/object-list-state.test.ts b/tests/lib/object-list-state.test.ts index 1781c3d9..0de9bb8e 100644 --- a/tests/lib/object-list-state.test.ts +++ b/tests/lib/object-list-state.test.ts @@ -2,7 +2,9 @@ import test from "node:test" import assert from "node:assert/strict" import { createObjectListScope, + resolveObjectListAutoSearchState, resolveObjectListDisplayState, + resolveObjectListLoadButtonMode, shouldApplyObjectListResponse, shouldResetObjectListPagination, } from "../../lib/object-list-state" @@ -147,7 +149,7 @@ test("resolveObjectListDisplayState keeps a filtered append request in loading s ) }) -test("resolveObjectListDisplayState reports when unsearched objects remain", () => { +test("resolveObjectListDisplayState keeps searching while unsearched objects remain", () => { assert.equal( resolveObjectListDisplayState({ searchTerm: "backup", @@ -156,7 +158,7 @@ test("resolveObjectListDisplayState reports when unsearched objects remain", () hasMore: true, loading: false, }), - "filtered-partial", + "filtered-loading", ) }) @@ -198,3 +200,81 @@ test("resolveObjectListDisplayState ignores whitespace around the filter term", "empty", ) }) + +test("resolveObjectListAutoSearchState auto-searches while a term is set, more remains, and it isn't stopped", () => { + assert.deepEqual(resolveObjectListAutoSearchState(true, true, false), { + isAutoSearching: true, + canResumeSearch: false, + }) +}) + +test("resolveObjectListAutoSearchState offers resume once auto-search is stopped with more remaining", () => { + assert.deepEqual(resolveObjectListAutoSearchState(true, true, true), { + isAutoSearching: false, + canResumeSearch: true, + }) +}) + +test("resolveObjectListAutoSearchState does nothing once everything has been loaded", () => { + assert.deepEqual(resolveObjectListAutoSearchState(true, false, false), { + isAutoSearching: false, + canResumeSearch: false, + }) +}) + +test("resolveObjectListAutoSearchState does nothing without an active search term", () => { + assert.deepEqual(resolveObjectListAutoSearchState(false, true, false), { + isAutoSearching: false, + canResumeSearch: false, + }) +}) + +test("resolveObjectListLoadButtonMode prioritizes stopping an active auto-search", () => { + assert.equal( + resolveObjectListLoadButtonMode({ isAutoSearching: true, canResumeSearch: false, loading: true, hasMore: true }), + "stop", + ) +}) + +test("resolveObjectListLoadButtonMode offers resume over a plain manual load when search was stopped", () => { + assert.equal( + resolveObjectListLoadButtonMode({ + isAutoSearching: false, + canResumeSearch: true, + loading: false, + hasMore: true, + }), + "resume", + ) +}) + +test("resolveObjectListLoadButtonMode reports loading for a plain in-flight fetch", () => { + assert.equal( + resolveObjectListLoadButtonMode({ isAutoSearching: false, canResumeSearch: false, loading: true, hasMore: true }), + "loading", + ) +}) + +test("resolveObjectListLoadButtonMode offers a manual load when more remains and nothing is in flight", () => { + assert.equal( + resolveObjectListLoadButtonMode({ + isAutoSearching: false, + canResumeSearch: false, + loading: false, + hasMore: true, + }), + "load", + ) +}) + +test("resolveObjectListLoadButtonMode reports done once nothing remains", () => { + assert.equal( + resolveObjectListLoadButtonMode({ + isAutoSearching: false, + canResumeSearch: false, + loading: false, + hasMore: false, + }), + "done", + ) +})