feat(grid): improve save error feedback and dirty cell highlighting
This commit is contained in:
1 parent
a2e1128670
commit
27d72f8776
15 files changed
+130
-17
No files matched your search
@@ -656,6 +656,7 @@ const { searchText, deferredSearchText: deferredClientSearchText, overlayVisible
|
||||
|
||||
const orderByInput = ref(props.initialOrderByInput ?? "");
|
||||
const whereFilterInput = ref(props.initialWhereInput ?? "");
|
||||
const queryControlError = ref("");
|
||||
const conditionColumns = computed(() => dataGridConditionColumnOptions(props.tableMeta?.columns ?? props.result.columns, resolvedDatabaseType.value));
|
||||
const conditionIdentifierQuote = computed(() => dataGridConditionIdentifierQuote(resolvedDatabaseType.value, connectionStore.connectionIdentifierQuote?.(props.connectionId)));
|
||||
const conditionHistoryScope = computed(() => ({
|
||||
@@ -4507,7 +4508,7 @@ async function applyOrderBySearch() {
|
||||
emit("update:orderByInput", orderByInput.value);
|
||||
if (orderByClause) rememberDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByClause);
|
||||
isApplyingWhere.value = true;
|
||||
saveError.value = "";
|
||||
queryControlError.value = "";
|
||||
currentPage.value = 1;
|
||||
clearSort();
|
||||
try {
|
||||
@@ -4530,7 +4531,7 @@ async function applyOrderBySearch() {
|
||||
});
|
||||
await props.onExecuteSql(sql);
|
||||
} catch (e: any) {
|
||||
saveError.value = String(e?.message || e);
|
||||
queryControlError.value = String(e?.message || e);
|
||||
} finally {
|
||||
isApplyingWhere.value = false;
|
||||
}
|
||||
@@ -4541,7 +4542,7 @@ async function applyWhereFilter() {
|
||||
const whereInput = currentWhereInput();
|
||||
if (whereInput) rememberDataGridConditionHistory("where", conditionHistoryScope.value, whereInput);
|
||||
isApplyingWhere.value = true;
|
||||
saveError.value = "";
|
||||
queryControlError.value = "";
|
||||
currentPage.value = 1;
|
||||
emit("update:whereInput", whereInput ?? "");
|
||||
try {
|
||||
@@ -4564,7 +4565,7 @@ async function applyWhereFilter() {
|
||||
});
|
||||
await props.onExecuteSql(sql);
|
||||
} catch (e: any) {
|
||||
saveError.value = String(e?.message || e);
|
||||
queryControlError.value = String(e?.message || e);
|
||||
} finally {
|
||||
isApplyingWhere.value = false;
|
||||
}
|
||||
@@ -4773,7 +4774,7 @@ const canvasSurfaceWidth = computed(() => {
|
||||
if (vw <= 0) return total;
|
||||
return Math.min(vw, total);
|
||||
});
|
||||
const canvasRenderStyleKey = computed(() => `${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${canvasBackingPixelRatio.value}:${isDark.value}:${themePalette.value}:${tableFontFamily.value}:${tableFontSize.value}`);
|
||||
const canvasRenderStyleKey = computed(() => `${settingsStore.editorSettings.theme}:${settingsStore.editorSettings.uiScale}:${canvasBackingPixelRatio.value}:${isDark.value}:${themePalette.value}:${tableFontFamily.value}:${tableFontSize.value}:${!!saveError.value}`);
|
||||
const CANVAS_MOUSE_WHEEL_SCROLL_MULTIPLIER = 1.5;
|
||||
const CANVAS_TRACKPAD_DELTA_THRESHOLD = 40;
|
||||
let canvasPixelRatioMediaQuery: MediaQueryList | null = null;
|
||||
@@ -5405,6 +5406,7 @@ watch(
|
||||
showCellDetail,
|
||||
editingCell,
|
||||
frozenColumnCount,
|
||||
saveError,
|
||||
// Pending edit structures can contain large nested cell maps; the editor
|
||||
// version ref gives the canvas a cheap invalidation signal without a deep watch.
|
||||
pendingChangesVersion,
|
||||
@@ -8459,7 +8461,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
data-grid-root
|
||||
data-grid-active="true"
|
||||
class="h-full flex flex-col overflow-hidden outline-none"
|
||||
:class="{ 'data-grid--editing-cell': !!editingCell, 'data-grid--dark': isDark }"
|
||||
:class="{ 'data-grid--editing-cell': !!editingCell, 'data-grid--dark': isDark, 'data-grid--has-save-error': !!saveError }"
|
||||
:style="gridStyle"
|
||||
tabindex="0"
|
||||
@keydown="onGridKeydown"
|
||||
@@ -9959,7 +9961,8 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
|
||||
<!-- Error bar -->
|
||||
<ErrorBanner v-if="saveError" :message="saveError" copy-mode="label" dismissible @dismiss="saveError = ''" />
|
||||
<ErrorBanner v-if="queryControlError" variant="card" :title="t('grid.queryError')" :message="queryControlError" copy-mode="label" dismissible @dismiss="queryControlError = ''" />
|
||||
<ErrorBanner v-if="saveError" variant="card" :title="t('grid.saveErrorTitle')" :message="saveError" copy-mode="label" dismissible @dismiss="saveError = ''" />
|
||||
|
||||
<!-- Bottom status bar -->
|
||||
<div v-if="!isErrorResult" class="grid grid-cols-[max-content_minmax(0,1fr)_max-content] items-center gap-2 px-3 py-1 border-t text-xs text-muted-foreground bg-muted/30 shrink-0">
|
||||
@@ -10121,7 +10124,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
--data-grid-row-new-bg: rgb(243, 243, 243);
|
||||
--data-grid-row-deleted-bg: rgb(255, 244, 244);
|
||||
--data-grid-cell-active-bg: rgb(244, 248, 255);
|
||||
--data-grid-cell-dirty-bg: rgb(255, 248, 230);
|
||||
--data-grid-cell-dirty-bg: rgb(166, 210, 255);
|
||||
--data-grid-cell-selected-bg: rgb(239, 246, 255);
|
||||
--data-grid-cell-selected-single-bg: rgb(191, 219, 254);
|
||||
--data-grid-cell-selected-dirty-bg: rgb(235, 224, 184);
|
||||
@@ -10142,13 +10145,18 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
[data-grid-root].data-grid--has-save-error {
|
||||
--data-grid-cell-dirty-bg: rgb(250, 212, 216) !important;
|
||||
--data-grid-cell-selected-dirty-bg: rgb(240, 192, 198) !important;
|
||||
}
|
||||
|
||||
[data-grid-root].data-grid--dark,
|
||||
:global(.dark) [data-grid-root] {
|
||||
--data-grid-row-muted-bg: rgb(40, 40, 43);
|
||||
--data-grid-row-new-bg: rgb(51, 51, 55);
|
||||
--data-grid-row-deleted-bg: rgb(55, 31, 32);
|
||||
--data-grid-cell-active-bg: rgb(25, 34, 46);
|
||||
--data-grid-cell-dirty-bg: rgb(94, 75, 26);
|
||||
--data-grid-cell-dirty-bg: rgb(33, 66, 131);
|
||||
--data-grid-cell-selected-bg: rgb(20, 40, 60);
|
||||
--data-grid-cell-selected-single-bg: rgb(30, 64, 96);
|
||||
--data-grid-cell-selected-dirty-bg: rgb(76, 66, 38);
|
||||
@@ -10169,12 +10177,18 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
background-color: rgb(19, 20, 22);
|
||||
}
|
||||
|
||||
[data-grid-root].data-grid--dark.data-grid--has-save-error,
|
||||
:global(.dark) [data-grid-root].data-grid--has-save-error {
|
||||
--data-grid-cell-dirty-bg: rgb(94, 56, 57) !important;
|
||||
--data-grid-cell-selected-dirty-bg: rgb(114, 66, 67) !important;
|
||||
}
|
||||
|
||||
@supports (background: color-mix(in oklab, white 50%, transparent)) {
|
||||
[data-grid-root] {
|
||||
--data-grid-row-muted-bg: color-mix(in oklab, var(--muted) 99%, var(--foreground));
|
||||
--data-grid-row-new-bg: color-mix(in oklab, var(--primary) 5%, transparent);
|
||||
--data-grid-row-deleted-bg: color-mix(in oklab, var(--destructive) 5%, transparent);
|
||||
--data-grid-cell-dirty-bg: color-mix(in oklab, rgb(240 177 0) 10%, transparent);
|
||||
--data-grid-cell-dirty-bg: color-mix(in oklab, rgb(166 210 255) 85%, var(--background));
|
||||
--data-grid-cell-selected-bg: color-mix(in oklab, rgb(59 130 246) 12%, var(--background));
|
||||
--data-grid-cell-selected-single-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background));
|
||||
--data-grid-cell-selected-dirty-bg: color-mix(in oklab, rgb(234 181 50) 30%, color-mix(in oklab, rgb(59 130 246) 18%, var(--background)));
|
||||
@@ -10185,6 +10199,15 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
--data-grid-row-number-deleted-bg: color-mix(in oklab, var(--destructive) 15%, var(--background));
|
||||
--data-grid-row-number-selected-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background));
|
||||
}
|
||||
[data-grid-root].data-grid--has-save-error {
|
||||
--data-grid-cell-dirty-bg: rgb(250, 212, 216) !important;
|
||||
--data-grid-cell-selected-dirty-bg: rgb(240, 192, 198) !important;
|
||||
}
|
||||
[data-grid-root].data-grid--dark.data-grid--has-save-error,
|
||||
:global(.dark) [data-grid-root].data-grid--has-save-error {
|
||||
--data-grid-cell-dirty-bg: rgb(94, 56, 57) !important;
|
||||
--data-grid-cell-selected-dirty-bg: rgb(114, 66, 67) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.data-grid-header-shell {
|
||||
@@ -10673,6 +10696,17 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
background-color: var(--data-grid-cell-dirty-bg) !important;
|
||||
}
|
||||
|
||||
.cell-dirty::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 5px 5px 0;
|
||||
border-color: transparent #f59e0b transparent transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cell-search-match {
|
||||
background-color: var(--data-grid-cell-search-bg) !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { Copy, TriangleAlert } from "@lucide/vue";
|
||||
import { Copy, TriangleAlert, X } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
@@ -12,7 +12,7 @@ const { toast } = useToast();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
message: string;
|
||||
variant?: "banner" | "centered";
|
||||
variant?: "banner" | "centered" | "card";
|
||||
title?: string;
|
||||
dismissible?: boolean;
|
||||
copyMode?: "icon" | "label";
|
||||
@@ -41,8 +41,30 @@ async function copy() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- card: 卡片类报错信息面板 -->
|
||||
<div v-if="variant === 'card'" class="mx-3 my-2 rounded-lg border border-destructive/30 bg-destructive/10 p-3 shadow-sm shrink-0 select-text flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-2 border-b border-destructive/20 pb-2">
|
||||
<div class="flex items-center gap-2 font-semibold text-xs text-destructive">
|
||||
<TriangleAlert class="h-4 w-4 text-destructive shrink-0" aria-hidden="true" />
|
||||
<span>{{ displayTitle }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<Button variant="outline" size="sm" class="h-6 gap-1 px-2 text-[11px] border-destructive/30 text-destructive hover:bg-destructive/15 hover:text-destructive" :aria-label="t('grid.copy')" @click.stop="copy">
|
||||
<Copy class="h-3 w-3" />
|
||||
{{ t("grid.copy") }}
|
||||
</Button>
|
||||
<Button v-if="dismissible" variant="ghost" size="icon-sm" class="h-6 w-6 text-destructive/70 hover:text-destructive hover:bg-destructive/15" :aria-label="t('grid.dismiss')" @click.stop="emit('dismiss')">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-40 min-h-[56px] overflow-y-auto rounded bg-background/80 dark:bg-background/50 border border-destructive/20 p-2.5 text-xs font-mono leading-relaxed text-destructive break-words whitespace-pre-wrap select-text cursor-text" @mousedown.stop @click.stop>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- banner: 紧凑内联横幅 -->
|
||||
<div v-if="variant === 'banner'" class="flex items-center gap-2 px-3 py-1.5 border-t bg-destructive/10 text-destructive text-xs shrink-0">
|
||||
<div v-else-if="variant === 'banner'" class="flex items-center gap-2 px-3 py-1.5 border-t bg-destructive/10 text-destructive text-xs shrink-0">
|
||||
<span class="flex-1 min-w-0 break-all">{{ message }}</span>
|
||||
<button v-if="copyMode === 'label'" type="button" class="shrink-0 hover:underline" :aria-label="t('grid.copy')" @click.stop="copy">
|
||||
{{ t("grid.copy") }}
|
||||
|
||||
Reference in new issue
Block a user