diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue
index 2c33213fbc..b8a9718bfe 100644
--- a/apps/desktop/src/components/document/DocumentBrowser.vue
+++ b/apps/desktop/src/components/document/DocumentBrowser.vue
@@ -88,6 +88,7 @@ import type { GridNewRowMeta } from "@/lib/dataGrid/gridNewRowPlacement";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { documentDataGridColumnLayoutScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
import { documentGridColumnVisibilityScopeKey, migrateDocumentGridColumnVisibilityToLayout } from "@/lib/document/documentGridColumnVisibilityStorage";
+import { matchesElasticsearchIndexPattern, subscribeElasticsearchIndexCleared, type ElasticsearchIndexClearedDetail } from "@/lib/sidebar/elasticsearchIndexActions";
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
import JsonEditNode from "./JsonEditNode.vue";
@@ -2062,8 +2063,24 @@ async function loadDynamoDbTableDescription() {
}
}
+/**
+ * The sidebar's "clear index data" action deletes documents behind this tab's
+ * back, so an open browser would keep listing rows that no longer exist.
+ * Reload when the cleared index is the one on screen.
+ */
+function handleElasticsearchIndexCleared(detail: ElasticsearchIndexClearedDetail) {
+ if (detail.connectionId !== props.connectionId) return;
+ // Clearing a grouped node deletes from every index its pattern matches, so a
+ // tab open on any concrete index under the pattern must refresh as well.
+ if (detail.index !== props.collection && !matchesElasticsearchIndexPattern(detail.index, props.collection)) return;
+ void refreshDocuments();
+}
+
+let unsubscribeElasticsearchIndexCleared: (() => void) | undefined;
+
onMounted(async () => {
window.addEventListener("pointerdown", handleDocumentBrowserPointerDown, true);
+ unsubscribeElasticsearchIndexCleared = subscribeElasticsearchIndexCleared(handleElasticsearchIndexCleared);
try {
await connectionStore.ensureConnected(props.connectionId);
} catch (e) {
@@ -2078,6 +2095,8 @@ onMounted(async () => {
});
onBeforeUnmount(() => {
window.removeEventListener("pointerdown", handleDocumentBrowserPointerDown, true);
+ unsubscribeElasticsearchIndexCleared?.();
+ unsubscribeElasticsearchIndexCleared = undefined;
if (documentLoadExecutionId.value) void api.cancelQuery(documentLoadExecutionId.value);
documentRequestGeneration++;
loadedDocumentQueryTotalCountRequest = undefined;
diff --git a/apps/desktop/src/components/editor/DangerConfirmDialog.vue b/apps/desktop/src/components/editor/DangerConfirmDialog.vue
index 876a74de2c..b5711207af 100644
--- a/apps/desktop/src/components/editor/DangerConfirmDialog.vue
+++ b/apps/desktop/src/components/editor/DangerConfirmDialog.vue
@@ -35,6 +35,8 @@ const props = withDefaults(
closeOnConfirm?: boolean;
cancelable?: boolean;
cancelRunningLoading?: boolean;
+ /** Holds the confirm button back until the caller's own precondition is met (e.g. the operator typed the target name). */
+ confirmDisabled?: boolean;
}>(),
{
sql: "",
@@ -49,6 +51,7 @@ const props = withDefaults(
closeOnConfirm: true,
cancelable: false,
cancelRunningLoading: false,
+ confirmDisabled: false,
},
);
@@ -71,7 +74,9 @@ const dialogOpen = computed({
});
function onConfirm() {
- if (props.loading) return;
+ // Guard here as well as on the button: a disabled button still fires on some
+ // synthetic/keyboard paths, and this one gates a destructive operation.
+ if (props.loading || props.confirmDisabled) return;
if (props.closeOnConfirm) open.value = false;
emit("confirm");
}
@@ -129,7 +134,7 @@ async function copyFullCode() {
{{ t("dangerDialog.cancelRunning") }}
-