diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 04e5ffde3b..381d7215f1 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -136,6 +136,7 @@ import { useTheme } from "@/composables/useTheme"; import { useSettingsStore } from "@/stores/settingsStore"; import { nextDataGridSortState, type DataGridSortDirection } from "@/lib/dataGridSort"; import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities"; +import { forgetDataGridConditionHistory, loadDataGridConditionHistory, rememberDataGridConditionHistory } from "@/lib/dataGridConditionHistory"; const SqlPreviewPanel = defineAsyncComponent(() => import("@/components/editor/SqlPreviewPanel.vue")); @@ -153,6 +154,11 @@ interface PreparedCopyValue { ready: boolean; } +type ConditionSuggestion = { + value: string; + kind: "column" | "history"; +}; + const props = defineProps<{ result: QueryResult; sql?: string; @@ -383,14 +389,14 @@ const searchInputRef = ref(); const measureRef = ref(); const suggestionLeft = ref(0); -const whereSuggestions = ref([]); +const whereSuggestions = ref([]); const whereSuggestionIndex = ref(-1); const whereFilterInputRef = ref(); const whereMeasureRef = ref(); const whereSuggestionLeft = ref(0); const whereSuggestionPosition = ref({ left: 0, top: 0 }); -const orderBySuggestions = ref([]); +const orderBySuggestions = ref([]); const orderBySuggestionIndex = ref(-1); const orderByInputRef = ref(); const orderByMeasureRef = ref(); @@ -401,6 +407,12 @@ const orderByInput = ref(props.initialOrderByInput ?? ""); const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0); const whereFilterInput = ref(props.initialWhereInput ?? ""); const hasWhereFilterInput = computed(() => whereFilterInput.value.trim().length > 0); +const conditionHistoryScope = computed(() => ({ + connectionId: props.connectionId, + database: props.database, + schema: props.tableMeta?.schema ?? props.schema, + tableName: props.tableMeta?.tableName, +})); const searchSplitContainerRef = ref(); const searchSplitWhereWidth = ref(null); const isResizingSearchSplit = ref(false); @@ -1200,11 +1212,18 @@ function acceptWhereSuggestion() { const idx = whereSuggestionIndex.value; if (idx < 0 || idx >= whereSuggestions.value.length) return; const sug = whereSuggestions.value[idx]; + if (sug.kind === "history") { + whereFilterInput.value = sug.value; + whereSuggestions.value = []; + whereSuggestionIndex.value = -1; + whereFilterInputRef.value?.focus(); + return; + } const lastWordMatch = whereFilterInput.value.match(/([^\s,()><=!&|]+)$/); if (lastWordMatch) { const lastWord = lastWordMatch[1]; const prefix = whereFilterInput.value.slice(0, -lastWord.length); - whereFilterInput.value = prefix + sug; + whereFilterInput.value = prefix + sug.value; } whereSuggestions.value = []; whereSuggestionIndex.value = -1; @@ -1221,20 +1240,39 @@ function navigateWhereSuggestion(delta: number) { whereSuggestionIndex.value = Math.min(Math.max(whereSuggestionIndex.value + delta, 0), whereSuggestions.value.length - 1); } +function showWhereHistorySuggestions() { + const history = loadDataGridConditionHistory("where", conditionHistoryScope.value, whereFilterInput.value); + whereSuggestions.value = history.map((value) => ({ value, kind: "history" })); + whereSuggestionIndex.value = whereSuggestions.value.length ? 0 : -1; + if (whereSuggestions.value.length) updateWhereSuggestionPosition(); +} + +function deleteWhereHistorySuggestion(value: string) { + const history = forgetDataGridConditionHistory("where", conditionHistoryScope.value, value); + const query = whereFilterInput.value; + const filtered = query.trim() ? loadDataGridConditionHistory("where", conditionHistoryScope.value, query) : history; + whereSuggestions.value = filtered.map((item) => ({ value: item, kind: "history" })); + whereSuggestionIndex.value = whereSuggestions.value.length ? Math.min(whereSuggestionIndex.value, whereSuggestions.value.length - 1) : -1; +} + watch(whereFilterInput, (val) => { emit("update:whereInput", currentWhereInput() ?? ""); persistStructuredFilterState(); whereSuggestions.value = []; if (!props.tableMeta?.columns?.length) return; const trimmed = val.trim(); - if (trimmed.length === 0) return; + if (trimmed.length === 0) { + showWhereHistorySuggestions(); + return; + } const lastToken = trimmed.split(/[\s,()><=!&|]+/).pop() || ""; if (lastToken.length > 0) { const tl = lastToken.toLowerCase(); whereSuggestions.value = props.tableMeta.columns .map((c) => c.name) .filter((n) => n.toLowerCase().startsWith(tl) && n.toLowerCase() !== tl) - .slice(0, 8); + .slice(0, 8) + .map((value) => ({ value, kind: "column" })); whereSuggestionIndex.value = 0; updateWhereSuggestionPosition(); } @@ -1322,11 +1360,18 @@ function acceptOrderBySuggestion() { const idx = orderBySuggestionIndex.value; if (idx < 0 || idx >= orderBySuggestions.value.length) return; const sug = orderBySuggestions.value[idx]; + if (sug.kind === "history") { + orderByInput.value = sug.value; + orderBySuggestions.value = []; + orderBySuggestionIndex.value = -1; + orderByInputRef.value?.focus(); + return; + } const lastWordMatch = orderByInput.value.match(/([^\s,()]+)$/); if (lastWordMatch) { const lastWord = lastWordMatch[1]; const prefix = orderByInput.value.slice(0, -lastWord.length); - orderByInput.value = prefix + sug; + orderByInput.value = prefix + sug.value; } orderBySuggestions.value = []; orderBySuggestionIndex.value = -1; @@ -1343,19 +1388,38 @@ function navigateOrderBySuggestion(delta: number) { orderBySuggestionIndex.value = Math.min(Math.max(orderBySuggestionIndex.value + delta, 0), orderBySuggestions.value.length - 1); } +function showOrderByHistorySuggestions() { + const history = loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByInput.value); + orderBySuggestions.value = history.map((value) => ({ value, kind: "history" })); + orderBySuggestionIndex.value = orderBySuggestions.value.length ? 0 : -1; + if (orderBySuggestions.value.length) updateOrderBySuggestionPosition(); +} + +function deleteOrderByHistorySuggestion(value: string) { + const history = forgetDataGridConditionHistory("orderBy", conditionHistoryScope.value, value); + const query = orderByInput.value; + const filtered = query.trim() ? loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, query) : history; + orderBySuggestions.value = filtered.map((item) => ({ value: item, kind: "history" })); + orderBySuggestionIndex.value = orderBySuggestions.value.length ? Math.min(orderBySuggestionIndex.value, orderBySuggestions.value.length - 1) : -1; +} + watch(orderByInput, (val) => { emit("update:orderByInput", val); orderBySuggestions.value = []; if (!props.tableMeta?.columns?.length) return; const trimmed = val.trim(); - if (trimmed.length === 0) return; + if (trimmed.length === 0) { + showOrderByHistorySuggestions(); + return; + } const lastToken = trimmed.split(/[\s,()]+/).pop() || ""; if (lastToken.length > 0 && !["asc", "desc"].includes(lastToken.toLowerCase())) { const tl = lastToken.toLowerCase(); orderBySuggestions.value = props.tableMeta.columns .map((c) => c.name) .filter((n) => n.toLowerCase().startsWith(tl) && n.toLowerCase() !== tl) - .slice(0, 8); + .slice(0, 8) + .map((value) => ({ value, kind: "column" })); orderBySuggestionIndex.value = 0; updateOrderBySuggestionPosition(); } @@ -3437,6 +3501,7 @@ async function applyOrderBySearch() { if (!props.onExecuteSql) return; const orderByClause = orderByInput.value.trim() || undefined; emit("update:orderByInput", orderByInput.value); + if (orderByClause) rememberDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByClause); isApplyingWhere.value = true; saveError.value = ""; currentPage.value = 1; @@ -3467,10 +3532,12 @@ async function applyOrderBySearch() { async function applyWhereFilter() { if (!props.onExecuteSql) return; + const whereInput = currentWhereInput(); + if (whereInput) rememberDataGridConditionHistory("where", conditionHistoryScope.value, whereInput); isApplyingWhere.value = true; saveError.value = ""; currentPage.value = 1; - emit("update:whereInput", currentWhereInput() ?? ""); + emit("update:whereInput", whereInput ?? ""); try { const tableMeta = await waitForTableMeta(); if (!tableMeta) return; @@ -3482,7 +3549,7 @@ async function applyWhereFilter() { primaryKeys: tableMeta.primaryKeys, orderBy: orderByInput.value.trim() || (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined), limit: pageSize.value, - whereInput: currentWhereInput(), + whereInput, includeRowId: usesSyntheticRowIdKey(props.databaseType, tableMeta.primaryKeys), }); await props.onExecuteSql(sql); @@ -6253,17 +6320,21 @@ const gridContextMenuItems = computed(() => { class="flex-1 h-5 min-w-0 text-xs bg-transparent outline-none placeholder:text-muted-foreground/60" placeholder="" @keydown="onWhereFilterKeydown" + @focus="showWhereHistorySuggestions" @click="updateWhereSuggestionPosition" @blur="dismissWhereSuggestions" /> +