feat(grid): add conditional bulk updates for MySQL

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-21 01:04:16 +08:00
1 parent 10d1509d92
commit a7edb8f8db
29 files changed
+1813 -85

No files matched your search

+274 -22
View File
@@ -66,6 +66,7 @@ import DataGridToolbar from "@/components/grid/DataGridToolbar.vue";
import DataGridExtractorDialog from "@/components/grid/DataGridExtractorDialog.vue";
import DataGridColumnHeader from "@/components/grid/DataGridColumnHeader.vue";
import DataGridQueryControls from "@/components/grid/DataGridQueryControls.vue";
import DataGridFilterBuilder from "@/components/grid/DataGridFilterBuilder.vue";
import DataGridFilterWorkbench from "@/components/grid/DataGridFilterWorkbench.vue";
import DataGridTextFilterWorkbench from "@/components/grid/DataGridTextFilterWorkbench.vue";
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
@@ -101,7 +102,7 @@ import {
isTdengineExistingRowReadonlyColumn,
usesSyntheticRowIdKey,
} from "@/lib/table/tableEditing";
import { buildDataGridColumnDistinctValuesSql, buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
import { buildDataGridColumnDistinctValuesSql, buildDataGridConditionalUpdateSql, buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
import {
buildVisibleTransposeRows,
averageTransposeRecordWidth,
@@ -836,6 +837,17 @@ function onGridContextMenuClose() {
const bulkEditDialogOpen = ref(false);
const bulkEditValue = ref("");
const bulkEditScope = ref<"selection" | "condition">("selection");
const conditionalBulkEditConditionSource = ref<"current" | "builder" | "sql">("builder");
const conditionalBulkEditCurrentWhereInput = ref("");
const conditionalBulkEditWhereInput = ref("");
const conditionalBulkEditMatchCount = ref<number | undefined>(undefined);
const conditionalBulkEditMatchCountLoading = ref(false);
const conditionalBulkEditMatchCountError = ref("");
const conditionalBulkEditMatchCountStale = ref(false);
const conditionalBulkEditConfirmOpen = ref(false);
const conditionalBulkEditStatement = ref("");
let conditionalBulkEditMatchCountRequestId = 0;
const inlineBulkEditActive = ref(false);
const copyColumnNamesDialogOpen = ref(false);
const copyColumnNamesDialogColumns = ref<string[]>([]);
@@ -966,27 +978,70 @@ const filterBuilderColumns = computed(() => props.tableMeta?.columns ?? []);
const filterBuilderColumnOptions = computed(() => filterBuilderColumns.value.map((column) => column.name));
const structuredFilterCacheKey = computed(() => props.cacheKey || [props.connectionId ?? "", props.database ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? ""].join("\u0001"));
const structuredFilterScopeKey = computed(() => [props.connectionId ?? "", props.database ?? "", props.schema ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? "", filterBuilderColumnOptions.value.join("\0")].join("\u0001"));
function isStructuredFilterRuleComplete(rule: StructuredFilterRule): boolean {
return filterModeIsSupportedForDatabase(rule.mode, resolvedDatabaseType.value) && filterModeHasCompleteValue(rule.mode, rule.rawValue, rule.rawEndValue);
}
async function buildStructuredFilterCondition(rule: StructuredFilterRule): Promise<string | undefined> {
const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName);
const usesList = filterModeUsesList(rule.mode);
const usesRange = filterModeUsesRange(rule.mode);
return (
(await buildDataGridContextFilterCondition({
databaseType: resolvedDatabaseType.value,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId),
columnName: rule.columnName,
columnInfo,
mode: rule.mode,
value: !usesList && filterModeNeedsValue(rule.mode) ? parseFilterValue(rule.rawValue, columnInfo, resolvedDatabaseType.value) : null,
values: usesList ? parseFilterValues(rule.rawValue, columnInfo, resolvedDatabaseType.value) : undefined,
endValue: usesRange ? parseFilterValue(rule.rawEndValue, columnInfo, resolvedDatabaseType.value) : undefined,
})) ?? undefined
);
}
const filterBuilder = useDataGridFilterBuilder({
columns: filterBuilderColumnOptions,
createId: uuid,
isComplete: (rule) => filterModeIsSupportedForDatabase(rule.mode, resolvedDatabaseType.value) && filterModeHasCompleteValue(rule.mode, rule.rawValue, rule.rawEndValue),
buildCondition: async (rule) => {
const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName);
const usesList = filterModeUsesList(rule.mode);
const usesRange = filterModeUsesRange(rule.mode);
return (
(await buildDataGridContextFilterCondition({
databaseType: resolvedDatabaseType.value,
columnName: rule.columnName,
columnInfo,
mode: rule.mode,
value: !usesList && filterModeNeedsValue(rule.mode) ? parseFilterValue(rule.rawValue, columnInfo, resolvedDatabaseType.value) : null,
values: usesList ? parseFilterValues(rule.rawValue, columnInfo, resolvedDatabaseType.value) : undefined,
endValue: usesRange ? parseFilterValue(rule.rawEndValue, columnInfo, resolvedDatabaseType.value) : undefined,
})) ?? undefined
);
},
isComplete: isStructuredFilterRuleComplete,
buildCondition: buildStructuredFilterCondition,
});
const conditionalBulkEditFilterBuilder = useDataGridFilterBuilder({
columns: filterBuilderColumnOptions,
createId: uuid,
isComplete: isStructuredFilterRuleComplete,
buildCondition: buildStructuredFilterCondition,
});
const conditionalBulkEditRules = conditionalBulkEditFilterBuilder.rules;
const conditionalBulkEditColumnSearch = conditionalBulkEditFilterBuilder.columnSearch;
const filteredConditionalBulkEditColumns = conditionalBulkEditFilterBuilder.filteredColumns;
function addConditionalBulkEditRule() {
if (!conditionalBulkEditRules.value.length) {
conditionalBulkEditFilterBuilder.ensureRule();
return;
}
conditionalBulkEditFilterBuilder.addRule();
}
function resetConditionalBulkEditMatchCountPreview() {
conditionalBulkEditMatchCountRequestId++;
conditionalBulkEditMatchCount.value = undefined;
conditionalBulkEditMatchCountLoading.value = false;
conditionalBulkEditMatchCountError.value = "";
conditionalBulkEditMatchCountStale.value = false;
}
function invalidateConditionalBulkEditMatchCountPreview() {
conditionalBulkEditMatchCountRequestId++;
if (conditionalBulkEditMatchCount.value !== undefined || conditionalBulkEditMatchCountLoading.value) conditionalBulkEditMatchCountStale.value = true;
conditionalBulkEditMatchCount.value = undefined;
conditionalBulkEditMatchCountLoading.value = false;
conditionalBulkEditMatchCountError.value = "";
}
watch([conditionalBulkEditConditionSource, conditionalBulkEditCurrentWhereInput, conditionalBulkEditWhereInput, conditionalBulkEditRules], invalidateConditionalBulkEditMatchCountPreview, { deep: true });
const structuredFilterRules = filterBuilder.rules;
const filterBuilderOpen = filterBuilder.open;
const filterBuilderColumnSearch = filterBuilder.columnSearch;
@@ -3571,6 +3626,8 @@ const {
requestDeleteRows,
cloneRows: cloneEditorRows,
saveChanges,
executeConditionalUpdate,
cancelConditionalUpdate,
discardChanges,
canUndoPendingChange,
canRedoPendingChange,
@@ -3579,6 +3636,9 @@ const {
rowDataWithChanges,
ensureQuickEntryDraftRow,
isSavingNewRow,
isConditionalUpdateActive,
conditionalUpdateExecution,
coerceCellValue,
canEditColumn,
resetGridVerticalScroll,
getResetScrollAfterResult,
@@ -3696,7 +3756,7 @@ const showDataGridTopbar = computed(
);
function canEditRowItem(item: RowItem | undefined): boolean {
return !!props.editable && !!item && !item.isDeleted && (item.isNew || item.isDraft || canEditExistingRows.value);
return !isConditionalUpdateActive.value && !!props.editable && !!item && !item.isDeleted && (item.isNew || item.isDraft || canEditExistingRows.value);
}
function canEditCellItem(item: RowItem | undefined, columnIndex: number): boolean {
@@ -7632,17 +7692,159 @@ function setSelectionNull() {
function openBulkEditDialog() {
if (!props.editable || !selectionHasEditableCells()) return;
const currentWhere = currentWhereInput()?.trim() ?? "";
bulkEditValue.value = "";
bulkEditScope.value = "selection";
conditionalBulkEditCurrentWhereInput.value = currentWhere;
conditionalBulkEditWhereInput.value = currentWhere;
conditionalBulkEditConditionSource.value = currentWhere ? "current" : "builder";
conditionalBulkEditFilterBuilder.reset();
resetConditionalBulkEditMatchCountPreview();
bulkEditDialogOpen.value = true;
}
function applyBulkEditValue() {
interface ConditionalBulkEditTarget {
columnIndex: number;
columnName: string;
}
const conditionalBulkEditTarget = computed<ConditionalBulkEditTarget | null>(() => {
if (props.context !== "table-data" || resolvedDatabaseType.value !== "mysql" || !props.connectionId || !props.tableMeta || props.customSaveHandler || hasPendingChanges.value) return null;
let visibleColumnIndex: number | undefined;
if (selectedRange.value) {
if (selectedRange.value.startCol !== selectedRange.value.endCol) return null;
visibleColumnIndex = selectedRange.value.startCol;
} else if (hasColumnSelection.value) {
const selectedColumns = selectedVisibleColumnIndexes();
if (selectedColumns.length !== 1) return null;
visibleColumnIndex = selectedColumns[0];
}
if (visibleColumnIndex === undefined) return null;
const columnIndex = actualColumnIndex(visibleColumnIndex);
const sourceColumn = props.sourceColumns ? props.sourceColumns[columnIndex] : props.result.columns[columnIndex];
if (!sourceColumn) return null;
const columnInfo = props.tableMeta.columns.find((column) => column.name.toLowerCase() === sourceColumn.toLowerCase());
if (!columnInfo) return null;
if (columnInfo.is_primary_key || props.tableMeta.primaryKeys.some((primaryKey) => primaryKey.toLowerCase() === columnInfo.name.toLowerCase())) return null;
const extra = columnInfo.extra?.toLowerCase() ?? "";
if (extra.includes("generated always as") && !extra.includes("identity")) return null;
if (!selectionHasEditableCells()) return null;
const hasExistingTarget = editableSelectionCells().some(({ item }) => !item.isNew && !item.isDraft);
if (!hasExistingTarget) return null;
return { columnIndex, columnName: columnInfo.name };
});
async function currentConditionalBulkEditWhere(): Promise<string> {
switch (conditionalBulkEditConditionSource.value) {
case "current":
return conditionalBulkEditCurrentWhereInput.value.trim();
case "builder":
return conditionalBulkEditFilterBuilder.buildWhere();
case "sql":
return conditionalBulkEditWhereInput.value.trim();
}
}
async function previewConditionalBulkEditMatchCount() {
if (!props.connectionId || !props.tableMeta) return;
const requestId = ++conditionalBulkEditMatchCountRequestId;
conditionalBulkEditMatchCount.value = undefined;
conditionalBulkEditMatchCountError.value = "";
conditionalBulkEditMatchCountStale.value = false;
conditionalBulkEditMatchCountLoading.value = true;
try {
const whereInput = await currentConditionalBulkEditWhere();
if (!whereInput) {
conditionalBulkEditMatchCountError.value = t("grid.conditionalBulkEditConditionRequired");
return;
}
const sql = await buildDataGridCountSql({
databaseType: resolvedDatabaseType.value,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId),
catalog: props.tableMeta.catalog,
database: props.tableMeta.database,
schema: props.tableMeta.schema,
tableName: props.tableMeta.tableName,
whereInput,
});
const result = await api.executeQuery(props.connectionId, props.executionDatabase ?? props.database ?? "", sql, undefined, undefined, dataGridCountQueryOptions(connectionStore.getConfig(props.connectionId), settingsStore.editorSettings.globalQueryTimeoutSecs));
const count = Number(result.rows?.[0]?.[0] ?? 0);
if (!Number.isFinite(count) || count < 0) throw new Error("Invalid row count returned by the database");
if (requestId !== conditionalBulkEditMatchCountRequestId) return;
conditionalBulkEditMatchCount.value = count;
} catch (error: any) {
if (requestId !== conditionalBulkEditMatchCountRequestId) return;
conditionalBulkEditMatchCountError.value = t("grid.conditionalBulkEditPreviewCountFailed", { message: error?.message || String(error) });
} finally {
if (requestId === conditionalBulkEditMatchCountRequestId) conditionalBulkEditMatchCountLoading.value = false;
}
}
async function applyBulkEditValue() {
if (bulkEditScope.value === "condition") {
const target = conditionalBulkEditTarget.value;
if (!target || !props.tableMeta) {
toast(t("grid.conditionalBulkEditUnavailable"), 5000);
return;
}
try {
const whereInput = await currentConditionalBulkEditWhere();
if (!whereInput) {
toast(t("grid.conditionalBulkEditConditionRequired"), 5000);
return;
}
const value = bulkEditValue.value === "" ? null : coerceCellValue(bulkEditValue.value, undefined, target.columnIndex);
const statement = await buildDataGridConditionalUpdateSql({
databaseType: resolvedDatabaseType.value,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId),
tableMeta: props.tableMeta,
columnName: target.columnName,
value,
whereInput,
});
if (!statement) {
toast(t("grid.conditionalBulkEditUnavailable"), 5000);
return;
}
conditionalBulkEditStatement.value = statement;
bulkEditDialogOpen.value = false;
conditionalBulkEditConfirmOpen.value = true;
} catch (error: any) {
toast(t("grid.conditionalBulkEditPrepareFailed", { message: error?.message || String(error) }), 5000);
}
return;
}
// Empty input sets the selected cells to SQL NULL (the placeholder hints "Value, or NULL").
const value = bulkEditValue.value === "" ? null : bulkEditValue.value;
if (!fillSelectionWithValue(value)) return;
bulkEditDialogOpen.value = false;
}
async function confirmConditionalBulkEdit() {
const statement = conditionalBulkEditStatement.value;
if (!statement) return;
conditionalBulkEditConfirmOpen.value = false;
const result = await executeConditionalUpdate(statement);
if (!result) {
if (saveError.value) toast(saveError.value, 5000);
return;
}
conditionalBulkEditStatement.value = "";
if (typeof result.affectedRows === "number") {
toast(t("grid.conditionalBulkEditAppliedCount", { count: result.affectedRows }));
} else {
toast(t("grid.conditionalBulkEditApplied"));
}
}
async function cancelRunningConditionalBulkEdit() {
const cancelled = await cancelConditionalUpdate();
if (!cancelled && saveError.value) toast(saveError.value, 5000);
}
interface EditableSelectionCell {
item: RowItem;
visibleCol: number;
@@ -10731,7 +10933,7 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
ref="gridRef"
data-grid-root
data-grid-active="true"
class="h-full flex flex-col overflow-hidden outline-none"
class="relative h-full flex flex-col overflow-hidden outline-none"
:class="{
'data-grid--editing-cell': !!editingCell,
'data-grid--dark': isDark,
@@ -12594,7 +12796,45 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
:copy-column-detail-column-name="copyColumnDetailColumnName"
/>
<DataGridBulkEditDialog v-if="bulkEditDialogMounted" v-model:open="bulkEditDialogOpen" v-model:value="bulkEditValue" :selected-cell-count="selectedCellCount" @apply="applyBulkEditValue" />
<DataGridBulkEditDialog
v-if="bulkEditDialogMounted"
v-model:open="bulkEditDialogOpen"
v-model:value="bulkEditValue"
v-model:scope="bulkEditScope"
v-model:condition-source="conditionalBulkEditConditionSource"
v-model:where-input="conditionalBulkEditWhereInput"
:selected-cell-count="selectedCellCount"
:conditional-available="!!conditionalBulkEditTarget"
:conditional-column="conditionalBulkEditTarget?.columnName"
:current-where="conditionalBulkEditCurrentWhereInput"
:conditional-count="conditionalBulkEditMatchCount"
:conditional-count-loading="conditionalBulkEditMatchCountLoading"
:conditional-count-error="conditionalBulkEditMatchCountError"
:conditional-count-stale="conditionalBulkEditMatchCountStale"
@apply="applyBulkEditValue"
@preview-count="previewConditionalBulkEditMatchCount"
>
<template #condition-builder>
<div class="space-y-2">
<DataGridFilterBuilder
:rules="conditionalBulkEditRules"
:columns="filterBuilderColumnOptions"
:filtered-columns="filteredConditionalBulkEditColumns"
:mode-options="filterModeOptions"
:column-search="conditionalBulkEditColumnSearch"
:show-header="false"
:show-footer="false"
layout="panel"
@add="addConditionalBulkEditRule"
@remove="conditionalBulkEditFilterBuilder.removeRule($event)"
@move="(id, targetIndex) => conditionalBulkEditFilterBuilder.moveRule(id, targetIndex)"
@update-rule="(id, patch) => conditionalBulkEditFilterBuilder.updateRule(id, patch)"
@update:column-search="conditionalBulkEditColumnSearch = $event"
/>
<Button variant="outline" size="sm" :disabled="!filterBuilderColumnOptions.length" @click="addConditionalBulkEditRule">{{ t("grid.filterBuilderAddRule") }}</Button>
</div>
</template>
</DataGridBulkEditDialog>
<DataGridInsertRowsDialog v-if="insertRowsDialogMounted" v-model:open="insertRowsDialogOpen" :can-place-at-selection="canPlaceInsertAtSelection" :initial-position="insertPosition" @insert="insertRows" />
<DataGridExtractorDialog v-model:open="extractorConfigOpen" :preference="selectedCopyPreference" :options="settingsStore.editorSettings.dataGridExtractorOptions" :items="copyPreferenceMenuItems" :preview="previewWithPreference" @save="saveExtractorConfiguration" />
@@ -12627,6 +12867,18 @@ function currentGridContextMenuItems(): ContextMenuItem[] {
<SqlPreviewPanel :sql="previewSqlText" :loading="isPreviewLoading" :can-undo="canUndoPendingChange" :can-redo="canRedoPendingChange" @undo="undoGridChange" @redo="redoGridChange" @close="closeSqlPreview" />
</div>
<DangerConfirmDialog
v-model:open="conditionalBulkEditConfirmOpen"
:title="t('grid.conditionalBulkEditConfirmTitle')"
:message="t('grid.conditionalBulkEditConfirmMessage')"
:sql="conditionalBulkEditStatement"
:confirm-label="t('grid.conditionalBulkEditConfirm')"
:close-on-confirm="false"
@confirm="confirmConditionalBulkEdit"
/>
<div v-if="isConditionalUpdateActive" class="absolute inset-0 z-50 flex items-center justify-center bg-background/75 backdrop-blur-[1px]">
<QueryLoadingState class="p-6" :show-cancel="true" :cancelling="conditionalUpdateExecution?.cancelling" @cancel="cancelRunningConditionalBulkEdit" />
</div>
<DangerConfirmDialog
v-model:open="showDeleteRowConfirm"
:message="
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -6,18 +7,143 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
const value = defineModel<string>("value", { default: "" });
defineProps<{ selectedCellCount: number }>();
const emit = defineEmits<{ apply: [] }>();
const scope = defineModel<"selection" | "condition">("scope", { default: "selection" });
const conditionSource = defineModel<"current" | "builder" | "sql">("conditionSource", { default: "builder" });
const whereInput = defineModel<string>("whereInput", { default: "" });
defineProps<{
selectedCellCount: number;
conditionalAvailable?: boolean;
conditionalColumn?: string;
currentWhere?: string;
conditionalCount?: number;
conditionalCountLoading?: boolean;
conditionalCountError?: string;
conditionalCountStale?: boolean;
}>();
const emit = defineEmits<{ apply: []; previewCount: [] }>();
const dragOffset = ref({ x: 0, y: 0 });
const isDragging = ref(false);
const dragStartPosition = ref({ x: 0, y: 0 });
const dragStartOffset = ref({ x: 0, y: 0 });
const activePointerId = ref<number | null>(null);
const dialogContentStyle = computed(() => {
if (isDragging.value || dragOffset.value.x !== 0 || dragOffset.value.y !== 0) {
return {
transform: `translate(${dragOffset.value.x}px, ${dragOffset.value.y}px)`,
transition: isDragging.value ? "none" : "transform 0.15s ease-out",
};
}
return {};
});
function resetDialogDragOffset() {
dragOffset.value = { x: 0, y: 0 };
isDragging.value = false;
activePointerId.value = null;
}
function startDialogDrag(event: PointerEvent) {
if (event.button !== undefined && event.button !== 0) return;
isDragging.value = true;
activePointerId.value = event.pointerId;
dragStartPosition.value = { x: event.clientX, y: event.clientY };
dragStartOffset.value = { ...dragOffset.value };
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
}
function moveDialogDrag(event: PointerEvent) {
if (!isDragging.value || event.pointerId !== activePointerId.value) return;
dragOffset.value = {
x: dragStartOffset.value.x + event.clientX - dragStartPosition.value.x,
y: dragStartOffset.value.y + event.clientY - dragStartPosition.value.y,
};
}
function endDialogDrag(event: PointerEvent) {
if (!isDragging.value || event.pointerId !== activePointerId.value) return;
isDragging.value = false;
activePointerId.value = null;
try {
(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);
} catch {
// Pointer capture may already have been released by the browser.
}
}
watch(open, (isOpen) => {
if (isOpen) {
void nextTick(resetDialogDragOffset);
} else {
resetDialogDragOffset();
}
});
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-[420px]">
<DialogHeader
<DialogContent :style="dialogContentStyle" class="sm:max-w-[680px]">
<DialogHeader class="cursor-move select-none" @pointerdown="startDialogDrag" @pointermove="moveDialogDrag" @pointerup="endDialogDrag" @pointercancel="endDialogDrag"
><DialogTitle>{{ t("grid.bulkEditTitle") }}</DialogTitle></DialogHeader
>
<div class="space-y-2">
<p class="text-sm text-muted-foreground">{{ t("grid.bulkEditDescription", { count: selectedCellCount }) }}</p>
<div v-if="conditionalAvailable" class="grid gap-2 pb-1">
<button type="button" class="rounded-md border px-3 py-2 text-left transition-colors" :class="scope === 'selection' ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'" @click="scope = 'selection'">
<span class="block text-sm font-medium">{{ t("grid.bulkEditSelectionScope") }}</span>
<span class="mt-0.5 block text-xs text-muted-foreground">{{ t("grid.bulkEditSelectionScopeDescription", { count: selectedCellCount }) }}</span>
</button>
<button type="button" class="rounded-md border px-3 py-2 text-left transition-colors" :class="scope === 'condition' ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'" @click="scope = 'condition'">
<span class="block text-sm font-medium">{{ t("grid.bulkEditConditionalScope") }}</span>
<span class="mt-0.5 block text-xs text-muted-foreground">{{ t("grid.bulkEditConditionalScopeDescription", { column: conditionalColumn }) }}</span>
</button>
</div>
<p v-else class="text-sm text-muted-foreground">{{ t("grid.bulkEditDescription", { count: selectedCellCount }) }}</p>
<div v-if="scope === 'condition'" class="space-y-2">
<div class="text-sm font-medium">{{ t("grid.bulkEditConditionalConditionSource") }}</div>
<div class="grid gap-2" :class="currentWhere ? 'sm:grid-cols-3' : 'sm:grid-cols-2'">
<button v-if="currentWhere" type="button" class="rounded-md border px-3 py-2 text-left transition-colors" :class="conditionSource === 'current' ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'" @click="conditionSource = 'current'">
<span class="block text-sm font-medium">{{ t("grid.bulkEditConditionalUseCurrentFilter") }}</span>
<span class="mt-0.5 block text-xs text-muted-foreground">{{ t("grid.bulkEditConditionalUseCurrentFilterDescription") }}</span>
</button>
<button type="button" class="rounded-md border px-3 py-2 text-left transition-colors" :class="conditionSource === 'builder' ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'" @click="conditionSource = 'builder'">
<span class="block text-sm font-medium">{{ t("grid.bulkEditConditionalBuildFilter") }}</span>
<span class="mt-0.5 block text-xs text-muted-foreground">{{ t("grid.bulkEditConditionalBuildFilterDescription") }}</span>
</button>
<button type="button" class="rounded-md border px-3 py-2 text-left transition-colors" :class="conditionSource === 'sql' ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'" @click="conditionSource = 'sql'">
<span class="block text-sm font-medium">{{ t("grid.bulkEditConditionalAdvancedWhere") }}</span>
<span class="mt-0.5 block text-xs text-muted-foreground">{{ t("grid.bulkEditConditionalAdvancedWhereDescription") }}</span>
</button>
</div>
<div v-if="conditionSource === 'current' && currentWhere" class="rounded-md bg-muted/60 px-2.5 py-2">
<code class="block max-h-20 overflow-auto whitespace-pre-wrap break-all text-xs">{{ currentWhere }}</code>
</div>
<div v-else-if="conditionSource === 'builder'" class="max-h-72 overflow-auto rounded-md border bg-muted/10 p-2">
<slot name="condition-builder" />
</div>
<template v-else>
<label class="text-sm font-medium" for="bulk-edit-where-input">{{ t("grid.bulkEditConditionalWhere") }}</label>
<textarea
id="bulk-edit-where-input"
v-model="whereInput"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
spellcheck="false"
rows="3"
class="dbx-data-grid-value-font min-h-20 w-full min-w-0 resize-y rounded-[6px] border border-input bg-transparent px-2.5 py-1.5 text-base outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 md:text-sm"
:placeholder="t('grid.bulkEditConditionalWherePlaceholder')"
/>
<p class="text-xs text-muted-foreground">{{ t("grid.bulkEditConditionalWhereDescription") }}</p>
</template>
<div class="flex flex-wrap items-center gap-2 pt-1">
<Button variant="outline" size="sm" :disabled="conditionalCountLoading" @click="emit('previewCount')">
{{ conditionalCountLoading ? t("grid.conditionalBulkEditPreviewingCount") : t("grid.conditionalBulkEditPreviewCount") }}
</Button>
<span v-if="conditionalCount !== undefined" class="text-sm font-medium text-foreground">{{ t("grid.conditionalBulkEditPreviewCountResult", { count: conditionalCount }) }}</span>
<span v-else-if="conditionalCountStale" class="text-xs text-muted-foreground">{{ t("grid.conditionalBulkEditPreviewCountStale") }}</span>
</div>
<p v-if="conditionalCountError" class="text-xs text-destructive">{{ conditionalCountError }}</p>
</div>
<textarea
v-model="value"
autocapitalize="off"