mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 10:28:44 +00:00
feat(ui): progressively shrink data grid toolbar
This commit is contained in:
@@ -354,7 +354,9 @@ import { resolveGridFocusRestoreTarget } from "@/lib/dataGrid/dataGridFocusResto
|
||||
import { buildOrderedGridRows, type GridInsertRowPosition, type GridNewRowPlacement } from "@/lib/dataGrid/gridNewRowPlacement";
|
||||
import {
|
||||
DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH,
|
||||
DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER,
|
||||
dataGridDeleteRowToolbarState,
|
||||
dataGridToolbarActionCollapseCount,
|
||||
isDataGridToolbarCompact,
|
||||
type DataGridReloadIntent,
|
||||
type DataGridToolbarActionCapability,
|
||||
@@ -712,6 +714,7 @@ const columnCommentMap = computed(() => {
|
||||
const dataGridTopbarWidth = ref(0);
|
||||
const dataGridViewportWidth = ref(0);
|
||||
const dataGridTopbarOverflowCompact = ref(false);
|
||||
const dataGridTopbarOverflowActionCount = ref(0);
|
||||
const dataGridTopbarExpandedRequiredWidth = ref(0);
|
||||
const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
@@ -733,6 +736,8 @@ const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.c
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
|
||||
const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
|
||||
const responsiveDataGridToolbarActionCount = computed(() => dataGridToolbarActionCollapseCount(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH));
|
||||
const compactDataGridToolbarActionCount = computed(() => Math.max(responsiveDataGridToolbarActionCount.value, dataGridTopbarOverflowActionCount.value));
|
||||
const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll);
|
||||
const queryResultMaxRows = computed(() => effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows));
|
||||
const paginationMaxRows = computed(() => (isResultsContext.value ? queryResultMaxRows.value : undefined));
|
||||
@@ -2561,6 +2566,8 @@ let gridScrollbarsRuntime: DataGridScrollbarsRuntime;
|
||||
let dataGridTopbarResizeObserver: ResizeObserver | null = null;
|
||||
let dataGridTopbarMutationObserver: MutationObserver | null = null;
|
||||
let dataGridTopbarRecheckTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const DATA_GRID_TOPBAR_RECHECK_DELAY_MS = 360;
|
||||
const DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX = 16;
|
||||
let cellEditResizeObserver: ResizeObserver | null = null;
|
||||
// vue-virtual-scroller's @resize only fires in pageMode (it observes window),
|
||||
// so in container-scroll mode (transpose uses RecycleScroller without pageMode)
|
||||
@@ -3210,43 +3217,57 @@ function observeGridHorizontalScrollbarScroller() {
|
||||
|
||||
function updateDataGridTopbarWidth() {
|
||||
const topbar = dataGridTopbarRef.value;
|
||||
const previousActionCount = compactDataGridToolbarActionCount.value;
|
||||
dataGridTopbarWidth.value = topbar?.clientWidth ?? 0;
|
||||
dataGridViewportWidth.value = typeof window === "undefined" ? 0 : window.innerWidth;
|
||||
|
||||
if (!topbar) return;
|
||||
|
||||
const fixedBreakpointCompact = isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH);
|
||||
if (fixedBreakpointCompact) return;
|
||||
|
||||
if (dataGridTopbarOverflowCompact.value) {
|
||||
if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) {
|
||||
dataGridTopbarOverflowCompact.value = false;
|
||||
dataGridTopbarExpandedRequiredWidth.value = 0;
|
||||
}
|
||||
if (compactDataGridToolbarActionCount.value !== previousActionCount) {
|
||||
scheduleDataGridTopbarRecheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataGridTopbarOverflowCompact.value || dataGridTopbarOverflowActionCount.value > 0) {
|
||||
if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) {
|
||||
dataGridTopbarOverflowCompact.value = false;
|
||||
dataGridTopbarOverflowActionCount.value = 0;
|
||||
dataGridTopbarExpandedRequiredWidth.value = 0;
|
||||
scheduleDataGridTopbarRecheck();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The action list gains preview/save/rollback controls in editable grids. The
|
||||
// fixed breakpoint cannot see that extra width, so capture the full layout
|
||||
// width before the overflow-clip wrapper can hide the trailing action.
|
||||
// responsive breakpoint cannot see that extra width, so progressively
|
||||
// compact the leftmost controls before overflow-clip hides a trailing action.
|
||||
if (topbar.scrollWidth > topbar.clientWidth + 1) {
|
||||
dataGridTopbarExpandedRequiredWidth.value = topbar.scrollWidth;
|
||||
dataGridTopbarOverflowCompact.value = true;
|
||||
dataGridTopbarExpandedRequiredWidth.value = Math.max(dataGridTopbarExpandedRequiredWidth.value, topbar.scrollWidth + DATA_GRID_TOPBAR_EXPAND_HYSTERESIS_PX);
|
||||
if (!compactDataGridToolbar.value) {
|
||||
dataGridTopbarOverflowCompact.value = true;
|
||||
} else if (compactDataGridToolbarActionCount.value < DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.length) {
|
||||
dataGridTopbarOverflowActionCount.value = compactDataGridToolbarActionCount.value + 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
scheduleDataGridTopbarRecheck();
|
||||
}
|
||||
}
|
||||
|
||||
function resetDataGridTopbarOverflowCompact() {
|
||||
if (!dataGridTopbarOverflowCompact.value) return;
|
||||
if (!dataGridTopbarOverflowCompact.value && dataGridTopbarOverflowActionCount.value === 0) return;
|
||||
dataGridTopbarOverflowCompact.value = false;
|
||||
dataGridTopbarOverflowActionCount.value = 0;
|
||||
dataGridTopbarExpandedRequiredWidth.value = 0;
|
||||
scheduleDataGridTopbarRecheck();
|
||||
}
|
||||
|
||||
function scheduleDataGridTopbarRecheck() {
|
||||
if (dataGridTopbarRecheckTimer) clearTimeout(dataGridTopbarRecheckTimer);
|
||||
// Wait for the label-width transition to finish before measuring the newly
|
||||
// shortened action list. If it still overflows, updateDataGridTopbarWidth
|
||||
// captures its new, smaller expanded width.
|
||||
dataGridTopbarRecheckTimer = setTimeout(() => {
|
||||
dataGridTopbarRecheckTimer = null;
|
||||
updateDataGridTopbarWidth();
|
||||
}, 360);
|
||||
}, DATA_GRID_TOPBAR_RECHECK_DELAY_MS);
|
||||
}
|
||||
|
||||
function clearDataGridTopbarRecheckTimer() {
|
||||
@@ -12676,7 +12697,8 @@ function openGridSnapshot() {
|
||||
|
||||
<DataGridToolbar
|
||||
class="ml-auto"
|
||||
:compact="compactDataGridToolbar"
|
||||
:compact-action-count="compactDataGridToolbarActionCount"
|
||||
:navigation-visible="props.result.columns.length > 0"
|
||||
:refresh="refreshToolbarCapability"
|
||||
:auto-refresh="autoRefreshToolbarCapability"
|
||||
:add-row="addRowToolbarCapability"
|
||||
@@ -12733,17 +12755,17 @@ function openGridSnapshot() {
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<template #navigation>
|
||||
<template #navigation="{ compact }">
|
||||
<Tooltip v-if="props.result.columns.length">
|
||||
<TooltipTrigger as-child>
|
||||
<Popover v-model:open="goToColumnOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compactDataGridToolbar ? 'data-grid-topbar-action-button--compact' : '', goToColumnOpen ? 'text-primary bg-primary/10' : '']">
|
||||
<Button data-toolbar-action="navigation" variant="ghost" size="sm" :class="['data-grid-topbar-action-button h-5 shrink-0 text-xs px-1.5', compact ? 'data-grid-topbar-action-button--compact' : '', goToColumnOpen ? 'text-primary bg-primary/10' : '']">
|
||||
<Columns3 class="data-grid-topbar-action-icon w-3 h-3" />
|
||||
<span
|
||||
class="data-grid-topbar-action-label"
|
||||
:class="{
|
||||
'data-grid-topbar-action-label--compact': compactDataGridToolbar,
|
||||
'data-grid-topbar-action-label--compact': compact,
|
||||
}"
|
||||
>{{ t("grid.goToColumn") }}</span
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch, type CSSProperties } from "vue";
|
||||
import { Check, Columns3, GripVertical, Search } from "@lucide/vue";
|
||||
import { Check, GripVertical, ListChecks, Search } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -306,7 +306,7 @@ onBeforeUnmount(resetColumnDragState);
|
||||
:title="t('grid.columnVisibility')"
|
||||
:aria-label="t('grid.columnVisibility')"
|
||||
>
|
||||
<Columns3 class="h-3.5 w-3.5" />
|
||||
<ListChecks class="h-3.5 w-3.5" />
|
||||
<span v-if="!compact">{{ t("grid.columnVisibility") }}</span>
|
||||
<span v-if="!compact && (grid?.hiddenColumnCount ?? 0) > 0" class="tabular-nums"> {{ grid?.visibleColumnCount }}/{{ grid?.displayableColumnCount }} </span>
|
||||
</Button>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER,
|
||||
dataGridToolbarIntervalOptions,
|
||||
isDataGridToolbarActionCompact,
|
||||
isDataGridToolbarCapabilityDisabled,
|
||||
isDataGridToolbarCapabilityVisible,
|
||||
selectDataGridToolbarAddRowItem,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
triggerDataGridToolbarCopy,
|
||||
triggerDataGridToolbarAction,
|
||||
type DataGridToolbarActionCapability,
|
||||
type DataGridToolbarActionKey,
|
||||
type DataGridToolbarAddRowCapability,
|
||||
type DataGridToolbarAutoRefreshCapability,
|
||||
type DataGridToolbarCopyCapability,
|
||||
@@ -25,6 +28,8 @@ import {
|
||||
|
||||
const props = defineProps<{
|
||||
compact?: boolean;
|
||||
compactActionCount?: number;
|
||||
navigationVisible?: boolean;
|
||||
refresh: DataGridToolbarActionCapability;
|
||||
autoRefresh?: DataGridToolbarAutoRefreshCapability;
|
||||
addRow?: DataGridToolbarAddRowCapability;
|
||||
@@ -39,11 +44,36 @@ const props = defineProps<{
|
||||
rollback?: DataGridToolbarActionCapability;
|
||||
}>();
|
||||
|
||||
const actionButtonClass = computed(() => ["data-grid-topbar-action-button h-5 shrink-0 px-1.5 text-xs", props.compact ? "data-grid-topbar-action-button--compact" : ""]);
|
||||
const autoRefreshIntervals = computed(() => (props.autoRefresh ? dataGridToolbarIntervalOptions(props.autoRefresh.intervalOptions, props.autoRefresh.intervalSeconds) : []));
|
||||
const visibleActionOrder = computed<DataGridToolbarActionKey[]>(() => {
|
||||
const visibility: Record<DataGridToolbarActionKey, boolean> = {
|
||||
refresh: isDataGridToolbarCapabilityVisible(props.refresh),
|
||||
autoRefresh: isDataGridToolbarCapabilityVisible(props.autoRefresh),
|
||||
navigation: props.navigationVisible === true,
|
||||
copyData: isDataGridToolbarCapabilityVisible(props.copyData),
|
||||
addRow: isDataGridToolbarCapabilityVisible(props.addRow),
|
||||
deleteRow: isDataGridToolbarCapabilityVisible(props.deleteRow),
|
||||
exportData: isDataGridToolbarCapabilityVisible(props.exportData),
|
||||
transpose: isDataGridToolbarCapabilityVisible(props.transpose),
|
||||
tableInfo: isDataGridToolbarCapabilityVisible(props.tableInfo),
|
||||
layerPreview: isDataGridToolbarCapabilityVisible(props.layerPreview),
|
||||
preview: isDataGridToolbarCapabilityVisible(props.preview),
|
||||
save: isDataGridToolbarCapabilityVisible(props.save),
|
||||
rollback: isDataGridToolbarCapabilityVisible(props.rollback),
|
||||
};
|
||||
return DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.filter((action) => visibility[action]);
|
||||
});
|
||||
|
||||
function actionLabelClass() {
|
||||
return { "data-grid-topbar-action-label--compact": props.compact };
|
||||
function actionIsCompact(action: DataGridToolbarActionKey): boolean {
|
||||
return isDataGridToolbarActionCompact(action, visibleActionOrder.value, props.compactActionCount ?? 0, props.compact === true);
|
||||
}
|
||||
|
||||
function actionButtonClass(action: DataGridToolbarActionKey) {
|
||||
return ["data-grid-topbar-action-button h-5 shrink-0 px-1.5 text-xs", actionIsCompact(action) ? "data-grid-topbar-action-button--compact" : ""];
|
||||
}
|
||||
|
||||
function actionLabelClass(action: DataGridToolbarActionKey) {
|
||||
return { "data-grid-topbar-action-label--compact": actionIsCompact(action) };
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -53,10 +83,10 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(refresh)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="actionButtonClass" :disabled="isDataGridToolbarCapabilityDisabled(refresh)" @click="void triggerDataGridToolbarAction(refresh)">
|
||||
<Button data-toolbar-action="refresh" variant="ghost" size="sm" :class="actionButtonClass('refresh')" :disabled="isDataGridToolbarCapabilityDisabled(refresh)" @click="void triggerDataGridToolbarAction(refresh)">
|
||||
<Loader2 v-if="refresh.loading" class="data-grid-topbar-action-icon h-3 w-3 animate-spin" />
|
||||
<RefreshCcw v-else class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ refresh.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('refresh')">{{ refresh.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ refresh.tooltip ?? refresh.label }}</TooltipContent>
|
||||
@@ -67,14 +97,15 @@ function actionLabelClass() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="[...actionButtonClass, autoRefresh?.enabled ? 'bg-primary/10 text-primary hover:bg-primary/15' : 'text-muted-foreground hover:text-foreground']"
|
||||
data-toolbar-action="autoRefresh"
|
||||
:class="[...actionButtonClass('autoRefresh'), autoRefresh?.enabled ? 'bg-primary/10 text-primary hover:bg-primary/15' : 'text-muted-foreground hover:text-foreground']"
|
||||
:disabled="isDataGridToolbarCapabilityDisabled(autoRefresh)"
|
||||
:title="autoRefresh?.label"
|
||||
:aria-label="autoRefresh?.label"
|
||||
:aria-pressed="autoRefresh?.enabled"
|
||||
>
|
||||
<Timer class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ autoRefresh?.enabled ? `${autoRefresh.intervalSeconds}s` : autoRefresh?.shortLabel }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('autoRefresh')">{{ autoRefresh?.enabled ? `${autoRefresh.intervalSeconds}s` : autoRefresh?.shortLabel }}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-40">
|
||||
@@ -91,14 +122,14 @@ function actionLabelClass() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<slot name="navigation" />
|
||||
<slot name="navigation" :compact="actionIsCompact('navigation')" />
|
||||
|
||||
<div v-if="isDataGridToolbarCapabilityVisible(copyData)" class="flex h-5 shrink-0 items-stretch overflow-hidden rounded-md border border-border">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-5 rounded-none border-0 px-1.5 text-xs" :disabled="copyData?.disabled" @click="void triggerDataGridToolbarCopy(copyData)">
|
||||
<Copy class="h-3 w-3" :class="compact ? '' : 'mr-1'" />
|
||||
<span v-if="!compact">{{ copyData?.label }}</span>
|
||||
<Button data-toolbar-action="copyData" variant="ghost" size="sm" :class="[...actionButtonClass('copyData'), 'rounded-none border-0']" :disabled="copyData?.disabled" @click="void triggerDataGridToolbarCopy(copyData)">
|
||||
<Copy class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('copyData')">{{ copyData?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ copyData?.tooltip ?? copyData?.label }}</TooltipContent>
|
||||
@@ -124,9 +155,9 @@ function actionLabelClass() {
|
||||
<div v-if="isDataGridToolbarCapabilityVisible(addRow)" class="flex h-5 shrink-0 items-stretch overflow-hidden rounded-md border border-border">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-5 rounded-none border-0 px-1.5 text-xs" :disabled="isDataGridToolbarCapabilityDisabled(addRow)" @click="void triggerDataGridToolbarAction(addRow)">
|
||||
<Plus class="h-3 w-3" :class="compact ? '' : 'mr-1'" />
|
||||
<span v-if="!compact">{{ addRow?.label }}</span>
|
||||
<Button data-toolbar-action="addRow" variant="ghost" size="sm" :class="[...actionButtonClass('addRow'), 'rounded-none border-0']" :disabled="isDataGridToolbarCapabilityDisabled(addRow)" @click="void triggerDataGridToolbarAction(addRow)">
|
||||
<Plus class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('addRow')">{{ addRow?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ addRow?.tooltip ?? addRow?.label }}</TooltipContent>
|
||||
@@ -152,9 +183,9 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(deleteRow)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="actionButtonClass" :disabled="isDataGridToolbarCapabilityDisabled(deleteRow)" @click="void triggerDataGridToolbarAction(deleteRow)">
|
||||
<Button data-toolbar-action="deleteRow" variant="ghost" size="sm" :class="actionButtonClass('deleteRow')" :disabled="isDataGridToolbarCapabilityDisabled(deleteRow)" @click="void triggerDataGridToolbarAction(deleteRow)">
|
||||
<Trash2 class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ deleteRow?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('deleteRow')">{{ deleteRow?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ deleteRow?.tooltip ?? deleteRow?.label }}</TooltipContent>
|
||||
@@ -164,9 +195,9 @@ function actionLabelClass() {
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="actionButtonClass" :disabled="exportData?.disabled || !exportData?.items.length">
|
||||
<Button data-toolbar-action="exportData" variant="ghost" size="sm" :class="actionButtonClass('exportData')" :disabled="exportData?.disabled || !exportData?.items.length">
|
||||
<Download class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ exportData?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('exportData')">{{ exportData?.label }}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
@@ -184,9 +215,17 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(transpose)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="[...actionButtonClass, transpose?.active ? 'bg-primary/10 text-primary hover:bg-primary/15' : '']" :disabled="isDataGridToolbarCapabilityDisabled(transpose)" :aria-pressed="transpose?.active" @click="void triggerDataGridToolbarAction(transpose)">
|
||||
<Button
|
||||
data-toolbar-action="transpose"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="[...actionButtonClass('transpose'), transpose?.active ? 'bg-primary/10 text-primary hover:bg-primary/15' : '']"
|
||||
:disabled="isDataGridToolbarCapabilityDisabled(transpose)"
|
||||
:aria-pressed="transpose?.active"
|
||||
@click="void triggerDataGridToolbarAction(transpose)"
|
||||
>
|
||||
<Rows3 class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ transpose?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('transpose')">{{ transpose?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ transpose?.tooltip ?? transpose?.label }}</TooltipContent>
|
||||
@@ -194,9 +233,17 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(tableInfo)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="[...actionButtonClass, tableInfo?.active ? 'bg-primary/10 text-primary hover:bg-primary/15' : '']" :disabled="isDataGridToolbarCapabilityDisabled(tableInfo)" :aria-pressed="tableInfo?.active" @click="void triggerDataGridToolbarAction(tableInfo)">
|
||||
<Button
|
||||
data-toolbar-action="tableInfo"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="[...actionButtonClass('tableInfo'), tableInfo?.active ? 'bg-primary/10 text-primary hover:bg-primary/15' : '']"
|
||||
:disabled="isDataGridToolbarCapabilityDisabled(tableInfo)"
|
||||
:aria-pressed="tableInfo?.active"
|
||||
@click="void triggerDataGridToolbarAction(tableInfo)"
|
||||
>
|
||||
<TableProperties class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ tableInfo?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('tableInfo')">{{ tableInfo?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ tableInfo?.tooltip ?? tableInfo?.label }}</TooltipContent>
|
||||
@@ -204,9 +251,9 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(layerPreview)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="actionButtonClass" :disabled="isDataGridToolbarCapabilityDisabled(layerPreview)" @click="void triggerDataGridToolbarAction(layerPreview)">
|
||||
<Button data-toolbar-action="layerPreview" variant="ghost" size="sm" :class="actionButtonClass('layerPreview')" :disabled="isDataGridToolbarCapabilityDisabled(layerPreview)" @click="void triggerDataGridToolbarAction(layerPreview)">
|
||||
<Map class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ layerPreview?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('layerPreview')">{{ layerPreview?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ layerPreview?.tooltip ?? layerPreview?.label }}</TooltipContent>
|
||||
@@ -214,10 +261,10 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(preview)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" :class="[...actionButtonClass, 'text-sky-600 hover:bg-sky-500/10 hover:text-sky-700']" :disabled="isDataGridToolbarCapabilityDisabled(preview)" @click="void triggerDataGridToolbarAction(preview)">
|
||||
<Button data-toolbar-action="preview" variant="ghost" size="sm" :class="[...actionButtonClass('preview'), 'text-sky-600 hover:bg-sky-500/10 hover:text-sky-700']" :disabled="isDataGridToolbarCapabilityDisabled(preview)" @click="void triggerDataGridToolbarAction(preview)">
|
||||
<Loader2 v-if="preview?.loading" class="data-grid-topbar-action-icon h-3 w-3 animate-spin" />
|
||||
<Eye v-else class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ preview?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('preview')">{{ preview?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm">{{ preview?.tooltip ?? preview?.label }}</TooltipContent>
|
||||
@@ -225,20 +272,20 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(save)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="default" size="sm" :class="[...actionButtonClass, 'data-grid-topbar-action-button--commit relative']" :disabled="isDataGridToolbarCapabilityDisabled(save)" @click="void triggerDataGridToolbarAction(save)">
|
||||
<Button data-toolbar-action="save" variant="default" size="sm" :class="[...actionButtonClass('save'), 'data-grid-topbar-action-button--commit relative']" :disabled="isDataGridToolbarCapabilityDisabled(save)" @click="void triggerDataGridToolbarAction(save)">
|
||||
<Loader2 v-if="save?.loading" class="data-grid-topbar-action-icon h-3 w-3 animate-spin" />
|
||||
<Save v-else-if="compact || !save?.pendingCount" class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<Save v-else-if="actionIsCompact('save') || !save?.pendingCount" class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span
|
||||
v-if="save && save.pendingCount > 0"
|
||||
:class="
|
||||
compact
|
||||
actionIsCompact('save')
|
||||
? 'absolute -right-1 -top-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-300 px-1 text-[9px] font-semibold leading-none text-amber-950 shadow-[0_0_0_1px_rgba(120,53,15,0.16)] dark:bg-amber-400'
|
||||
: 'mr-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-300 px-1 text-[9px] font-semibold leading-none text-amber-950 shadow-[0_0_0_1px_rgba(120,53,15,0.16)] dark:bg-amber-400'
|
||||
"
|
||||
>
|
||||
{{ save.pendingCount }}
|
||||
</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ save?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('save')">{{ save?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" class="max-w-sm"
|
||||
@@ -248,9 +295,9 @@ function actionLabelClass() {
|
||||
|
||||
<Tooltip v-if="isDataGridToolbarCapabilityVisible(rollback)">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="outline" size="sm" :class="actionButtonClass" :disabled="isDataGridToolbarCapabilityDisabled(rollback)" @click="void triggerDataGridToolbarAction(rollback)">
|
||||
<Button data-toolbar-action="rollback" variant="outline" size="sm" :class="actionButtonClass('rollback')" :disabled="isDataGridToolbarCapabilityDisabled(rollback)" @click="void triggerDataGridToolbarAction(rollback)">
|
||||
<RotateCcw class="data-grid-topbar-action-icon h-3 w-3" />
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass()">{{ rollback?.label }}</span>
|
||||
<span class="data-grid-topbar-action-label" :class="actionLabelClass('rollback')">{{ rollback?.label }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ rollback?.tooltip ?? rollback?.label }}</TooltipContent>
|
||||
|
||||
@@ -1476,7 +1476,7 @@ export default {
|
||||
startAutoRefresh: "Start auto-refresh",
|
||||
stopAutoRefresh: "Stop auto-refresh",
|
||||
missingResultRun: "This result is no longer available",
|
||||
exportResultArchive: "Save Result Archive",
|
||||
exportResultArchive: "Save Results",
|
||||
importResultArchive: "Import Result Archive",
|
||||
importedResultArchive: "Imported results",
|
||||
resultArchiveExported: "Results exported",
|
||||
|
||||
@@ -1453,7 +1453,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "Iniciar actualización automática",
|
||||
stopAutoRefresh: "Detener actualización automática",
|
||||
missingResultRun: "Este resultado ya no está disponible",
|
||||
exportResultArchive: "Guardar archivo de resultados",
|
||||
exportResultArchive: "Guardar resultados",
|
||||
importResultArchive: "Importar archivo de resultados",
|
||||
importedResultArchive: "Resultados importados",
|
||||
resultArchiveExported: "Resultados exportados",
|
||||
|
||||
@@ -1451,7 +1451,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "Avvia aggiornamento automatico",
|
||||
stopAutoRefresh: "Ferma aggiornamento automatico",
|
||||
missingResultRun: "Questo risultato non e piu disponibile",
|
||||
exportResultArchive: "Salva Archivio Risultati",
|
||||
exportResultArchive: "Salva risultati",
|
||||
importResultArchive: "Importa Archivio Risultati",
|
||||
importedResultArchive: "Risultati importati",
|
||||
resultArchiveExported: "Risultati esportati",
|
||||
|
||||
@@ -1470,7 +1470,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "自動更新を開始",
|
||||
stopAutoRefresh: "自動更新を停止",
|
||||
missingResultRun: "この結果は利用できなくなりました",
|
||||
exportResultArchive: "結果アーカイブを保存",
|
||||
exportResultArchive: "結果を保存",
|
||||
importResultArchive: "結果アーカイブをインポート",
|
||||
importedResultArchive: "結果をインポートしました",
|
||||
resultArchiveExported: "結果をエクスポートしました",
|
||||
|
||||
@@ -1357,7 +1357,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "자동 새로고침 시작",
|
||||
stopAutoRefresh: "자동 새로고침 중지",
|
||||
missingResultRun: "이 결과는 더 이상 사용할 수 없습니다",
|
||||
exportResultArchive: "결과 아카이브 저장",
|
||||
exportResultArchive: "결과 저장",
|
||||
importResultArchive: "결과 아카이브 가져오기",
|
||||
importedResultArchive: "결과를 가져왔습니다",
|
||||
resultArchiveExported: "결과를 내보냈습니다",
|
||||
|
||||
@@ -1453,7 +1453,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "Iniciar atualização automática",
|
||||
stopAutoRefresh: "Parar atualização automática",
|
||||
missingResultRun: "Este resultado não está mais disponível",
|
||||
exportResultArchive: "Salvar Arquivo de Resultados",
|
||||
exportResultArchive: "Salvar resultados",
|
||||
importResultArchive: "Importar Arquivo de Resultados",
|
||||
importedResultArchive: "Resultados importados",
|
||||
resultArchiveExported: "Resultados exportados",
|
||||
|
||||
@@ -1476,7 +1476,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "Otomatik yenilemeyi başlat",
|
||||
stopAutoRefresh: "Otomatik yenilemeyi durdur",
|
||||
missingResultRun: "Bu sonuç artık kullanılamıyor",
|
||||
exportResultArchive: "Sonuç Arşivini Kaydet",
|
||||
exportResultArchive: "Sonuçları Kaydet",
|
||||
importResultArchive: "Sonuç Arşivini İçe Aktar",
|
||||
importedResultArchive: "İçe aktarılan sonuçlar",
|
||||
resultArchiveExported: "Sonuçlar dışa aktarıldı",
|
||||
|
||||
@@ -1400,7 +1400,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "开始自动刷新",
|
||||
stopAutoRefresh: "停止自动刷新",
|
||||
missingResultRun: "此结果已不可用",
|
||||
exportResultArchive: "保存结果归档",
|
||||
exportResultArchive: "保存结果",
|
||||
importResultArchive: "导入结果归档",
|
||||
importedResultArchive: "导入的结果",
|
||||
resultArchiveExported: "结果已导出",
|
||||
|
||||
@@ -1452,7 +1452,7 @@ export default withEnglishFallback({
|
||||
startAutoRefresh: "開始自動重新整理",
|
||||
stopAutoRefresh: "停止自動重新整理",
|
||||
missingResultRun: "此結果已不再可用",
|
||||
exportResultArchive: "儲存結果封存",
|
||||
exportResultArchive: "儲存結果",
|
||||
importResultArchive: "匯入結果封存",
|
||||
importedResultArchive: "已匯入的結果",
|
||||
resultArchiveExported: "結果已匯出",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH,
|
||||
DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER,
|
||||
dataGridDeleteRowToolbarState,
|
||||
dataGridToolbarActionCollapseCount,
|
||||
dataGridToolbarCompactBreakpoint,
|
||||
dataGridToolbarIntervalOptions,
|
||||
isDataGridToolbarActionCompact,
|
||||
isDataGridToolbarCompact,
|
||||
selectDataGridToolbarAddRowItem,
|
||||
selectDataGridToolbarAutoRefreshInterval,
|
||||
@@ -63,6 +66,29 @@ describe("data grid toolbar capabilities", () => {
|
||||
expect(isDataGridToolbarCompact(1000, 1100)).toBe(false);
|
||||
});
|
||||
|
||||
it("progressively compacts toolbar actions as the available width shrinks", () => {
|
||||
expect(dataGridToolbarActionCollapseCount(1050, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(0);
|
||||
expect(dataGridToolbarActionCollapseCount(1049, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(1);
|
||||
expect(dataGridToolbarActionCollapseCount(1010, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(1);
|
||||
expect(dataGridToolbarActionCollapseCount(1009, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(2);
|
||||
expect(dataGridToolbarActionCollapseCount(0, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(0);
|
||||
expect(dataGridToolbarActionCollapseCount(100, 1440, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.length);
|
||||
});
|
||||
|
||||
it("compacts visible actions from left to right while skipping unavailable actions", () => {
|
||||
const visibleActions = ["refresh", "navigation", "addRow", "preview", "save", "rollback"] as const;
|
||||
|
||||
expect(DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.slice(-3)).toEqual(["preview", "save", "rollback"]);
|
||||
expect(isDataGridToolbarActionCompact("refresh", visibleActions, 3)).toBe(true);
|
||||
expect(isDataGridToolbarActionCompact("navigation", visibleActions, 3)).toBe(true);
|
||||
expect(isDataGridToolbarActionCompact("addRow", visibleActions, 3)).toBe(true);
|
||||
expect(isDataGridToolbarActionCompact("preview", visibleActions, 3)).toBe(false);
|
||||
expect(isDataGridToolbarActionCompact("save", visibleActions, 3)).toBe(false);
|
||||
expect(isDataGridToolbarActionCompact("rollback", visibleActions, 3)).toBe(false);
|
||||
expect(isDataGridToolbarActionCompact("autoRefresh", visibleActions, 3)).toBe(false);
|
||||
expect(isDataGridToolbarActionCompact("rollback", visibleActions, 0, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not invoke hidden or disabled actions", async () => {
|
||||
const onTrigger = vi.fn();
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
const DATA_GRID_COMPACT_TOPBAR_MIN_WIDTH = 900;
|
||||
const DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH = 1050;
|
||||
const DATA_GRID_COMPACT_TOPBAR_VIEWPORT_RATIO = 0.75;
|
||||
const DATA_GRID_TOOLBAR_ACTION_COLLAPSE_STEP = 40;
|
||||
export const DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH = DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH;
|
||||
|
||||
export const DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER = ["refresh", "autoRefresh", "navigation", "copyData", "addRow", "deleteRow", "exportData", "transpose", "tableInfo", "layerPreview", "preview", "save", "rollback"] as const;
|
||||
|
||||
export type DataGridToolbarActionKey = (typeof DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER)[number];
|
||||
|
||||
export function dataGridToolbarCompactBreakpoint(viewportWidth: number, minimumWidth = DATA_GRID_COMPACT_TOPBAR_MIN_WIDTH): number {
|
||||
const normalizedViewportWidth = Number.isFinite(viewportWidth) ? Math.max(0, viewportWidth) : DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH;
|
||||
const normalizedMinimumWidth = Math.min(DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH, Math.max(DATA_GRID_COMPACT_TOPBAR_MIN_WIDTH, minimumWidth));
|
||||
@@ -13,6 +18,20 @@ export function isDataGridToolbarCompact(toolbarWidth: number, viewportWidth: nu
|
||||
return toolbarWidth > 0 && toolbarWidth < dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth);
|
||||
}
|
||||
|
||||
export function dataGridToolbarActionCollapseCount(toolbarWidth: number, viewportWidth: number, minimumWidth?: number): number {
|
||||
if (!Number.isFinite(toolbarWidth) || toolbarWidth <= 0) return 0;
|
||||
const deficit = dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth) - toolbarWidth;
|
||||
if (deficit <= 0) return 0;
|
||||
return Math.min(DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER.length, Math.ceil(deficit / DATA_GRID_TOOLBAR_ACTION_COLLAPSE_STEP));
|
||||
}
|
||||
|
||||
export function isDataGridToolbarActionCompact(action: DataGridToolbarActionKey, visibleActions: readonly DataGridToolbarActionKey[], compactActionCount: number, forceCompact = false): boolean {
|
||||
if (forceCompact) return true;
|
||||
const actionIndex = visibleActions.indexOf(action);
|
||||
if (actionIndex < 0) return false;
|
||||
return actionIndex < Math.max(0, Math.floor(compactActionCount));
|
||||
}
|
||||
|
||||
export type DataGridReloadIntent = "refresh";
|
||||
|
||||
export interface DataGridToolbarActionCapability {
|
||||
|
||||
+10
-3
@@ -6,6 +6,7 @@ import { compileScript, compileTemplate, parse } from "vue/compiler-sfc";
|
||||
const contentAreaPath = "apps/desktop/src/components/layout/ContentArea.vue";
|
||||
const appPath = "apps/desktop/src/App.vue";
|
||||
const dataGridPath = "apps/desktop/src/components/grid/DataGrid.vue";
|
||||
const dataGridToolbarPath = "apps/desktop/src/components/grid/DataGridToolbar.vue";
|
||||
const viewSwitcherPath = "apps/desktop/src/components/layout/QueryResultViewSwitcher.vue";
|
||||
const toolbarActionsPath = "apps/desktop/src/components/layout/QueryResultToolbarActions.vue";
|
||||
|
||||
@@ -25,7 +26,7 @@ function assertSfcCompiles(path: string): void {
|
||||
}
|
||||
|
||||
test("query result toolbar SFCs compile", () => {
|
||||
for (const path of [contentAreaPath, dataGridPath, viewSwitcherPath, toolbarActionsPath]) assertSfcCompiles(path);
|
||||
for (const path of [contentAreaPath, dataGridPath, dataGridToolbarPath, viewSwitcherPath, toolbarActionsPath]) assertSfcCompiles(path);
|
||||
});
|
||||
|
||||
test("ContentArea keeps query-result insert and delete capabilities separate", () => {
|
||||
@@ -169,14 +170,19 @@ test("standalone result views use the same compact toolbar breakpoint", () => {
|
||||
assert.match(dataGrid, /isDataGridToolbarCompact\(dataGridTopbarWidth\.value, dataGridViewportWidth\.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH\)/);
|
||||
});
|
||||
|
||||
test("embedded result toolbar compacts when editing actions overflow its available width", () => {
|
||||
test("embedded result toolbar progressively compacts when editing actions overflow its available width", () => {
|
||||
const dataGrid = source(dataGridPath);
|
||||
const toolbar = source(dataGridToolbarPath);
|
||||
|
||||
assert.match(dataGrid, /dataGridTopbarMutationObserver = new MutationObserver\(updateDataGridTopbarWidth\)/);
|
||||
assert.match(dataGrid, /dataGridTopbarMutationObserver\.observe\(topbar, \{ childList: true, characterData: true, subtree: true \}\)/);
|
||||
assert.match(dataGrid, /topbar\.scrollWidth > topbar\.clientWidth \+ 1/);
|
||||
assert.match(dataGrid, /dataGridTopbarOverflowCompact\.value = true/);
|
||||
assert.match(dataGrid, /dataGridTopbarOverflowActionCount\.value = compactDataGridToolbarActionCount\.value \+ 1/);
|
||||
assert.match(dataGrid, /dataGridTopbarWidth\.value >= dataGridTopbarExpandedRequiredWidth\.value/);
|
||||
assert.match(dataGrid, /:compact-action-count="compactDataGridToolbarActionCount"/);
|
||||
assert.match(toolbar, /DATA_GRID_TOOLBAR_ACTION_COLLAPSE_ORDER\.filter/);
|
||||
assert.match(toolbar, /<slot name="navigation" :compact="actionIsCompact\('navigation'\)"/);
|
||||
});
|
||||
|
||||
test("embedded result toolbar remeasures after the SQL preview action is removed", () => {
|
||||
@@ -184,7 +190,8 @@ test("embedded result toolbar remeasures after the SQL preview action is removed
|
||||
|
||||
assert.match(dataGrid, /if \(previousVisible && !visible\) resetDataGridTopbarOverflowCompact\(\)/);
|
||||
assert.match(dataGrid, /dataGridTopbarOverflowCompact\.value = false/);
|
||||
assert.match(dataGrid, /dataGridTopbarRecheckTimer = setTimeout\(/);
|
||||
assert.match(dataGrid, /dataGridTopbarOverflowActionCount\.value = 0/);
|
||||
assert.match(dataGrid, /function scheduleDataGridTopbarRecheck\(\)/);
|
||||
assert.match(dataGrid, /clearDataGridTopbarRecheckTimer\(\)/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user