feat(grid): improve data copy format controls
This commit is contained in:
18 files changed
+235
-35
No files matched your search
@@ -11,6 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ErrorBanner from "@/components/ui/ErrorBanner.vue";
|
||||
import DataGrid from "@/components/grid/DataGrid.vue";
|
||||
import DataGridCopyFormatControl from "@/components/grid/DataGridCopyFormatControl.vue";
|
||||
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
@@ -116,6 +117,7 @@ const sortInput = ref("");
|
||||
const filterInputRef = ref<HTMLTextAreaElement>();
|
||||
const sortInputRef = ref<HTMLTextAreaElement>();
|
||||
const dataGridRef = ref<InstanceType<typeof DataGrid>>();
|
||||
const viewOptionsOpen = ref(false);
|
||||
const mongoUpdateTarget = computed(() => (props.databaseType === "mongodb" && mongoCopyDocumentsAvailable.value ? { collection: props.collection, idColumn: "_id" as const } : undefined));
|
||||
const documentViewerRef = ref<HTMLElement>();
|
||||
const documentSearchInputRef = ref<HTMLInputElement>();
|
||||
@@ -126,6 +128,11 @@ const documentSearchHasNavigated = ref(false);
|
||||
const documentViewerSearchActive = ref(false);
|
||||
const columnVisibilitySearch = ref("");
|
||||
const columnVisibilityOptions = computed(() => dataGridRef.value?.filteredColumnVisibilityOptions(columnVisibilitySearch.value) ?? []);
|
||||
|
||||
function openDataGridExtractorConfiguration() {
|
||||
viewOptionsOpen.value = false;
|
||||
void nextTick(() => dataGridRef.value?.openExtractorConfiguration());
|
||||
}
|
||||
const tableSearchSplitContainerRef = ref<HTMLDivElement>();
|
||||
const tableFindPaneWidth = ref<number | null>(null);
|
||||
const isResizingTableSearchSplit = ref(false);
|
||||
@@ -1571,7 +1578,7 @@ defineExpose({ focusSearch });
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover v-if="viewMode === 'table' && gridResult.columns.length">
|
||||
<Popover v-if="viewMode === 'table' && gridResult.columns.length" v-model:open="viewOptionsOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-7 shrink-0 text-foreground hover:bg-accent" :class="{ 'bg-accent text-foreground': dataGridRef?.nullColumnsHidden }" :title="t('grid.viewOptions')" :aria-label="t('grid.viewOptions')">
|
||||
<Wrench class="h-4 w-4" />
|
||||
@@ -1588,6 +1595,13 @@ defineExpose({ focusSearch });
|
||||
<span v-if="(dataGridRef?.allNullColumnCount ?? 0) > 0" class="text-muted-foreground tabular-nums"> ({{ dataGridRef?.allNullColumnCount }}) </span>
|
||||
</span>
|
||||
</label>
|
||||
<DataGridCopyFormatControl
|
||||
:current-label="dataGridRef?.defaultCopyExtractorLabel ?? '-'"
|
||||
:current-value="dataGridRef?.defaultCopyExtractor ?? ''"
|
||||
:items="dataGridRef?.copyExtractorMenuItems ?? []"
|
||||
@select="dataGridRef?.setDefaultCopyExtractor($event)"
|
||||
@configure="openDataGridExtractorConfiguration"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
Eraser,
|
||||
Columns3,
|
||||
PencilRuler,
|
||||
Settings2,
|
||||
WandSparkles,
|
||||
} from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -206,7 +207,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { simpleDataGridOrderByMatchesSort, simpleDataGridOrderByReferencesMissingColumn, type DataGridSortDirection, type DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
|
||||
import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarCopyCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar";
|
||||
import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar";
|
||||
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo";
|
||||
@@ -5269,6 +5270,10 @@ function copyExtractorLabel(extractor: DataGridCopyExtractorId): string {
|
||||
}
|
||||
|
||||
const selectedCopyExtractor = computed(() => settingsStore.editorSettings.dataGridCopyExtractor);
|
||||
const defaultCopyExtractorLabel = computed(() => {
|
||||
if (selectedCopyExtractor.value !== "sql-inserts") return copyExtractorLabel(selectedCopyExtractor.value);
|
||||
return t(settingsStore.editorSettings.dataGridExtractorOptions.sql.excludePrimaryKeysFromInsert ? "grid.copyExtractorSqlInsertsWithoutPrimaryKeys" : "grid.copyExtractorSqlInsertsWithPrimaryKeys");
|
||||
});
|
||||
const extractorConfigOpen = ref(false);
|
||||
const extractorMenuItems = computed(() =>
|
||||
DATA_GRID_COPY_EXTRACTOR_IDS.map((extractor) => ({
|
||||
@@ -5278,23 +5283,23 @@ const extractorMenuItems = computed(() =>
|
||||
separatorBefore: DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS[extractor].separatorBefore,
|
||||
})),
|
||||
);
|
||||
const copyToolbarCapability = computed<DataGridToolbarCopyCapability>(() => ({
|
||||
label: copyExtractorLabel(selectedCopyExtractor.value),
|
||||
tooltip: `${t("grid.copy")} (${shortcutMod}+C)`,
|
||||
currentValue: selectedCopyExtractor.value,
|
||||
disabled: !canCopyWithExtractor(selectedCopyExtractor.value),
|
||||
items: [...extractorMenuItems.value, { value: "__configure__", label: t("grid.copyExtractorConfigure"), separatorBefore: true }],
|
||||
onCopy: async () => {
|
||||
await copyWithExtractor(selectedCopyExtractor.value);
|
||||
},
|
||||
onSelect: (value) => {
|
||||
if (value === "__configure__") {
|
||||
extractorConfigOpen.value = true;
|
||||
return;
|
||||
}
|
||||
settingsStore.updateEditorSettings({ dataGridCopyExtractor: value as DataGridCopyExtractorId });
|
||||
},
|
||||
}));
|
||||
function openExtractorConfiguration() {
|
||||
extractorConfigOpen.value = true;
|
||||
}
|
||||
|
||||
function setDefaultCopyExtractor(value: string) {
|
||||
const item = extractorMenuItems.value.find((candidate) => candidate.value === value);
|
||||
if (!item || item.disabled) return;
|
||||
settingsStore.updateEditorSettings({ dataGridCopyExtractor: item.value });
|
||||
}
|
||||
|
||||
function sqlInsertExtractorOptions(excludePrimaryKeysFromInsert: boolean) {
|
||||
const options = settingsStore.editorSettings.dataGridExtractorOptions;
|
||||
return {
|
||||
...options,
|
||||
sql: { ...options.sql, excludePrimaryKeysFromInsert },
|
||||
};
|
||||
}
|
||||
|
||||
function saveExtractorConfiguration(value: { extractor: DataGridCopyExtractorId; options: typeof settingsStore.editorSettings.dataGridExtractorOptions }) {
|
||||
settingsStore.updateEditorSettings({ dataGridCopyExtractor: value.extractor, dataGridExtractorOptions: value.options });
|
||||
@@ -7597,6 +7602,11 @@ defineExpose({
|
||||
exportSql,
|
||||
exportXlsx,
|
||||
exportTxt,
|
||||
defaultCopyExtractor: selectedCopyExtractor,
|
||||
defaultCopyExtractorLabel,
|
||||
copyExtractorMenuItems: extractorMenuItems,
|
||||
setDefaultCopyExtractor,
|
||||
openExtractorConfiguration,
|
||||
});
|
||||
|
||||
// ---- CustomContextMenu ----
|
||||
@@ -7637,22 +7647,34 @@ function buildExtractorContextItems(): ContextMenuItem[] {
|
||||
items.push({ label: "", separator: true });
|
||||
}
|
||||
const selected = extractor === selectedCopyExtractor.value;
|
||||
if (extractor === "sql-inserts") {
|
||||
for (const excludePrimaryKeysFromInsert of [false, true]) {
|
||||
const options = sqlInsertExtractorOptions(excludePrimaryKeysFromInsert);
|
||||
items.push({
|
||||
label: t(excludePrimaryKeysFromInsert ? "grid.copyExtractorSqlInsertsWithoutPrimaryKeys" : "grid.copyExtractorSqlInsertsWithPrimaryKeys"),
|
||||
action: () => void copyWithExtractor(extractor, options),
|
||||
disabled: !canCopyWithExtractor(extractor, options),
|
||||
checked: selected && settingsStore.editorSettings.dataGridExtractorOptions.sql.excludePrimaryKeysFromInsert === excludePrimaryKeysFromInsert,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
label: `${selected ? "✓ " : ""}${copyExtractorLabel(extractor)}`,
|
||||
label: copyExtractorLabel(extractor),
|
||||
action: () => {
|
||||
// One-off copy as the chosen format; do NOT persist it as the default —
|
||||
// the saved default stays controlled by the toolbar/settings dialog.
|
||||
void copyWithExtractor(extractor);
|
||||
},
|
||||
disabled: !canCopyWithExtractor(extractor),
|
||||
checked: selected,
|
||||
});
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: t("grid.copyExtractorConfigure"),
|
||||
action: () => {
|
||||
extractorConfigOpen.value = true;
|
||||
},
|
||||
icon: Settings2,
|
||||
action: openExtractorConfiguration,
|
||||
});
|
||||
return items;
|
||||
}
|
||||
@@ -7899,7 +7921,6 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
:compact="compactDataGridToolbar"
|
||||
:refresh="refreshToolbarCapability"
|
||||
:auto-refresh="autoRefreshToolbarCapability"
|
||||
:copy-data="copyToolbarCapability"
|
||||
:add-row="addRowToolbarCapability"
|
||||
:preview="previewToolbarCapability"
|
||||
:save="saveToolbarCapability"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, ChevronDown, Copy, Settings2 } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import type { DataGridToolbarMenuItem } from "@/lib/dataGrid/dataGridToolbar";
|
||||
|
||||
defineProps<{
|
||||
currentLabel: string;
|
||||
currentValue: string;
|
||||
items: readonly DataGridToolbarMenuItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
configure: [];
|
||||
select: [value: string];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-3 px-3 py-1.5 text-xs">
|
||||
<span class="flex min-w-0 items-center gap-2 font-medium">
|
||||
<Copy class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span>{{ t("grid.copyExtractorDefaultFormat") }}</span>
|
||||
</span>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-6 max-w-52 min-w-0 gap-1 px-2 text-xs">
|
||||
<span class="truncate">{{ currentLabel }}</span>
|
||||
<ChevronDown class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="min-w-52">
|
||||
<template v-for="item in items" :key="item.value">
|
||||
<DropdownMenuSeparator v-if="item.separatorBefore" />
|
||||
<DropdownMenuItem class="gap-2" :disabled="item.disabled" @select="emit('select', item.value)">
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<Check v-if="currentValue === item.value" class="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" :title="t('grid.copyExtractorConfigure')" :aria-label="t('grid.copyExtractorConfigure')" @click="emit('configure')">
|
||||
<Settings2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -109,9 +109,8 @@ function actionLabelClass() {
|
||||
<template v-for="item in copyData?.items ?? []" :key="item.value">
|
||||
<DropdownMenuSeparator v-if="item.separatorBefore" />
|
||||
<DropdownMenuItem class="gap-2" :disabled="item.disabled" @select="void selectDataGridToolbarCopyItem(copyData, item.value)">
|
||||
<Check v-if="copyData?.currentValue === item.value" class="h-3.5 w-3.5" />
|
||||
<span v-else class="h-3.5 w-3.5" />
|
||||
{{ item.label }}
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<Check v-if="copyData?.currentValue === item.value" class="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -19,6 +19,7 @@ import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import QueryErrorActions from "@/components/common/QueryErrorActions.vue";
|
||||
import QueryResultToolbarActions from "@/components/layout/QueryResultToolbarActions.vue";
|
||||
import QueryResultViewSwitcher from "@/components/layout/QueryResultViewSwitcher.vue";
|
||||
import DataGridCopyFormatControl from "@/components/grid/DataGridCopyFormatControl.vue";
|
||||
import DataGridFontFamilyControl from "@/components/grid/DataGridFontFamilyControl.vue";
|
||||
import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue";
|
||||
let dataGridComponentPromise: Promise<typeof import("@/components/grid/DataGrid.vue")> | undefined;
|
||||
@@ -112,6 +113,11 @@ type DataGridHandle = {
|
||||
allNullColumnCount: number;
|
||||
canToggleAllNullColumns: boolean;
|
||||
toggleAllNullColumns: () => void;
|
||||
defaultCopyExtractor: string;
|
||||
defaultCopyExtractorLabel: string;
|
||||
copyExtractorMenuItems: Array<{ value: string; label: string; disabled?: boolean; separatorBefore?: boolean }>;
|
||||
setDefaultCopyExtractor: (value: string) => void;
|
||||
openExtractorConfiguration: () => void;
|
||||
showDdl: boolean;
|
||||
toggleDdl: (tab?: TableInfoTab) => void;
|
||||
multiRowTranspose: boolean;
|
||||
@@ -211,6 +217,7 @@ const standaloneResultToolbarRef = ref<HTMLElement | null>(null);
|
||||
const standaloneResultToolbarWidth = ref(0);
|
||||
const resultTabsScrollerRef = ref<HTMLElement | null>(null);
|
||||
const columnVisibilitySearch = ref("");
|
||||
const dataGridViewOptionsOpen = ref(false);
|
||||
const columnVisibilityOptions = computed(() => dataGridRef.value?.filteredColumnVisibilityOptions(columnVisibilitySearch.value) ?? []);
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode);
|
||||
@@ -220,6 +227,11 @@ const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize)
|
||||
const redisKeyBrowserRef = ref<SearchableBrowserHandle>();
|
||||
const documentBrowserRef = ref<SearchableBrowserHandle>();
|
||||
|
||||
function openDataGridExtractorConfiguration() {
|
||||
dataGridViewOptionsOpen.value = false;
|
||||
void nextTick(() => dataGridRef.value?.openExtractorConfiguration());
|
||||
}
|
||||
|
||||
const etcdKeyBrowserRef = ref<SearchableBrowserHandle>();
|
||||
const etcdDashboardRef = ref<{ refresh?: () => boolean }>();
|
||||
const zookeeperKeyBrowserRef = ref<SearchableBrowserHandle>();
|
||||
@@ -1080,7 +1092,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1">
|
||||
<Popover v-if="activeOutputView === 'result' && activeTab.result && hasTabularResult && !activeElasticsearchJsonResponse">
|
||||
<Popover v-if="activeOutputView === 'result' && activeTab.result && hasTabularResult && !activeElasticsearchJsonResponse" v-model:open="dataGridViewOptionsOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-7 shrink-0 text-foreground hover:bg-accent" :title="t('grid.viewOptions')" :aria-label="t('grid.viewOptions')">
|
||||
<Wrench class="h-4 w-4" />
|
||||
@@ -1270,6 +1282,13 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
</span>
|
||||
<Switch size="sm" :model-value="!!dataGridRef?.nullColumnsHidden" :disabled="!dataGridRef?.canToggleAllNullColumns" :aria-label="t('grid.hideNullColumns')" @update:model-value="dataGridRef?.toggleAllNullColumns()" />
|
||||
</div>
|
||||
<DataGridCopyFormatControl
|
||||
:current-label="dataGridRef?.defaultCopyExtractorLabel ?? '-'"
|
||||
:current-value="dataGridRef?.defaultCopyExtractor ?? ''"
|
||||
:items="dataGridRef?.copyExtractorMenuItems ?? []"
|
||||
@select="dataGridRef?.setDefaultCopyExtractor($event)"
|
||||
@configure="openDataGridExtractorConfiguration"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<LightTooltip :text="t('editor.hideResultsPane')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
||||
@@ -1592,7 +1611,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Popover v-if="activeTab.result?.columns.length">
|
||||
<Popover v-if="activeTab.result?.columns.length" v-model:open="dataGridViewOptionsOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-7 shrink-0 text-foreground hover:bg-accent" :title="t('grid.viewOptions')" :aria-label="t('grid.viewOptions')">
|
||||
<Wrench class="h-4 w-4" />
|
||||
@@ -1758,6 +1777,13 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
||||
</span>
|
||||
<Switch size="sm" :model-value="!!dataGridRef?.nullColumnsHidden" :disabled="!dataGridRef?.canToggleAllNullColumns" :aria-label="t('grid.hideNullColumns')" @update:model-value="dataGridRef?.toggleAllNullColumns()" />
|
||||
</div>
|
||||
<DataGridCopyFormatControl
|
||||
:current-label="dataGridRef?.defaultCopyExtractorLabel ?? '-'"
|
||||
:current-value="dataGridRef?.defaultCopyExtractor ?? ''"
|
||||
:items="dataGridRef?.copyExtractorMenuItems ?? []"
|
||||
@select="dataGridRef?.setDefaultCopyExtractor($event)"
|
||||
@configure="openDataGridExtractorConfiguration"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onBeforeUnmount, onMounted, nextTick, type Component } from "vue";
|
||||
import { ChevronRight } from "@lucide/vue";
|
||||
import { Check, ChevronRight } from "@lucide/vue";
|
||||
import { shortcutDisplayKeys } from "@/lib/editor/shortcutDisplay";
|
||||
import { registerGlobalContextMenu, type ContextMenuRegistration } from "@/components/ui/customContextMenuRegistry";
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ContextMenuItem {
|
||||
separator?: boolean;
|
||||
icon?: Component;
|
||||
iconClass?: string;
|
||||
checked?: boolean;
|
||||
// Raw shortcut syntax such as `Mod+C` or `Shift+Alt+U`; display formatting stays in this component.
|
||||
shortcut?: string;
|
||||
variant?: "default" | "destructive";
|
||||
@@ -283,6 +284,7 @@ onBeforeUnmount(() => {
|
||||
<component :is="item.icon" v-if="item.icon" :class="['size-4', item.iconClass]" />
|
||||
</span>
|
||||
<span class="flex-1 whitespace-nowrap">{{ item.label }}</span>
|
||||
<Check v-if="item.checked" class="ml-4 size-4 shrink-0 text-primary" />
|
||||
<span v-if="item.shortcut" class="ml-8 inline-flex shrink-0 items-center gap-1 text-muted-foreground">
|
||||
<kbd v-for="key in shortcutKeys(item.shortcut)" :key="key" class="min-w-4 rounded border border-border/70 bg-muted/60 px-1 py-0.5 text-center font-mono text-[10px] leading-none text-muted-foreground shadow-xs">{{ key }}</kbd>
|
||||
</span>
|
||||
@@ -313,6 +315,7 @@ onBeforeUnmount(() => {
|
||||
<component :is="child.icon" v-if="child.icon" :class="['size-4', child.iconClass]" />
|
||||
</span>
|
||||
<span class="flex-1 whitespace-nowrap">{{ child.label }}</span>
|
||||
<Check v-if="child.checked" class="ml-4 size-4 shrink-0 text-primary" />
|
||||
<span v-if="child.shortcut" class="ml-8 inline-flex shrink-0 items-center gap-1 text-muted-foreground">
|
||||
<kbd v-for="key in shortcutKeys(child.shortcut)" :key="key" class="min-w-4 rounded border border-border/70 bg-muted/60 px-1 py-0.5 text-center font-mono text-[10px] leading-none text-muted-foreground shadow-xs">{{ key }}</kbd>
|
||||
</span>
|
||||
|
||||
@@ -97,6 +97,36 @@ describe("CustomContextMenu lifecycle", () => {
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("renders checked state after the menu label", async () => {
|
||||
const root = defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
CustomContextMenu,
|
||||
{ items: [{ label: "Current", checked: true }, { label: "Other" }] },
|
||||
{
|
||||
default: ({ onContextMenu }: { onContextMenu: (event: MouseEvent) => void }) => h("div", { id: "context-target", onContextmenu: onContextMenu }, "Target"),
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
const container = document.createElement("div");
|
||||
mountedContainers.push(container);
|
||||
document.body.append(container);
|
||||
const app = createApp(root);
|
||||
app.mount(container);
|
||||
|
||||
container.querySelector("#context-target")?.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true }));
|
||||
await nextTick();
|
||||
|
||||
const current = Array.from(document.body.querySelectorAll("button")).find((button) => button.textContent?.includes("Current"));
|
||||
const other = Array.from(document.body.querySelectorAll("button")).find((button) => button.textContent?.includes("Other"));
|
||||
expect(current?.lastElementChild?.tagName.toLowerCase()).toBe("svg");
|
||||
expect(other?.querySelector("svg")).toBeNull();
|
||||
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("resolves lazy menu items again for every open", async () => {
|
||||
let copied = false;
|
||||
const items = vi.fn(() =>
|
||||
|
||||
Reference in new issue
Block a user