1 parent
9c90edabe3
commit
4eabeb6a15
2 files changed
+143
-22
No files matched your search
@@ -4613,22 +4613,22 @@ onUnmounted(() => {
|
|||||||
<div class="settings-appearance-group">
|
<div class="settings-appearance-group">
|
||||||
<Label>{{ t("settings.tabLayout") }}</Label>
|
<Label>{{ t("settings.tabLayout") }}</Label>
|
||||||
<div class="settings-appearance-choice-grid">
|
<div class="settings-appearance-choice-grid">
|
||||||
<Button type="button" variant="outline" class="settings-choice-card h-auto justify-start border p-3" :class="editTabLayout === 'scroll' ? 'dbx-choice-selected' : ''" @click="setTabLayout('scroll')">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 justify-start overflow-hidden whitespace-normal border p-3" :class="editTabLayout === 'scroll' ? 'dbx-choice-selected' : ''" @click="setTabLayout('scroll')">
|
||||||
<div class="w-full min-w-0 text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="text-sm font-medium">
|
<div class="text-sm font-medium">
|
||||||
{{ t("settings.tabLayoutScroll") }}
|
{{ t("settings.tabLayoutScroll") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="break-words whitespace-normal text-xs text-muted-foreground">
|
||||||
{{ t("settings.tabLayoutScrollDescription") }}
|
{{ t("settings.tabLayoutScrollDescription") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" class="settings-choice-card h-auto justify-start border p-3" :class="editTabLayout === 'wrap' ? 'dbx-choice-selected' : ''" @click="setTabLayout('wrap')">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 justify-start overflow-hidden whitespace-normal border p-3" :class="editTabLayout === 'wrap' ? 'dbx-choice-selected' : ''" @click="setTabLayout('wrap')">
|
||||||
<div class="w-full min-w-0 text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="text-sm font-medium">
|
<div class="text-sm font-medium">
|
||||||
{{ t("settings.tabLayoutWrap") }}
|
{{ t("settings.tabLayoutWrap") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="break-words whitespace-normal text-xs text-muted-foreground">
|
||||||
{{ t("settings.tabLayoutWrapDescription") }}
|
{{ t("settings.tabLayoutWrapDescription") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4801,10 +4801,10 @@ onUnmounted(() => {
|
|||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
<Button type="button" variant="outline" class="h-auto items-start justify-start border p-3" :class="editDataTabReuseMode === 'always-new' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'always-new'">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 items-start justify-start overflow-hidden whitespace-normal border p-3" :class="editDataTabReuseMode === 'always-new' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'always-new'">
|
||||||
<div class="text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
<div class="text-sm font-medium">{{ t("settings.dataTabReuseAlwaysNew") }}</div>
|
<div class="min-w-0 break-words text-sm font-medium">{{ t("settings.dataTabReuseAlwaysNew") }}</div>
|
||||||
<Tooltip :open="dataTabReuseModeHelp === 'always-new'">
|
<Tooltip :open="dataTabReuseModeHelp === 'always-new'">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'always-new'" @mouseleave="dataTabReuseModeHelp = null">
|
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'always-new'" @mouseleave="dataTabReuseModeHelp = null">
|
||||||
@@ -4818,10 +4818,10 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" class="h-auto items-start justify-start border p-3" :class="editDataTabReuseMode === 'same-table' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'same-table'">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 items-start justify-start overflow-hidden whitespace-normal border p-3" :class="editDataTabReuseMode === 'same-table' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'same-table'">
|
||||||
<div class="text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
<div class="text-sm font-medium">{{ t("settings.dataTabReuseSameTable") }}</div>
|
<div class="min-w-0 break-words text-sm font-medium">{{ t("settings.dataTabReuseSameTable") }}</div>
|
||||||
<Tooltip :open="dataTabReuseModeHelp === 'same-table'">
|
<Tooltip :open="dataTabReuseModeHelp === 'same-table'">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'same-table'" @mouseleave="dataTabReuseModeHelp = null">
|
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'same-table'" @mouseleave="dataTabReuseModeHelp = null">
|
||||||
@@ -4835,10 +4835,10 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" class="h-auto items-start justify-start border p-3" :class="editDataTabReuseMode === 'active-tab' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'active-tab'">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 items-start justify-start overflow-hidden whitespace-normal border p-3" :class="editDataTabReuseMode === 'active-tab' ? 'dbx-choice-selected' : ''" @click="editDataTabReuseMode = 'active-tab'">
|
||||||
<div class="text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
<div class="text-sm font-medium">{{ t("settings.dataTabReuseActiveTab") }}</div>
|
<div class="min-w-0 break-words text-sm font-medium">{{ t("settings.dataTabReuseActiveTab") }}</div>
|
||||||
<Tooltip :open="dataTabReuseModeHelp === 'active-tab'">
|
<Tooltip :open="dataTabReuseModeHelp === 'active-tab'">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'active-tab'" @mouseleave="dataTabReuseModeHelp = null">
|
<span class="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground" @click.stop @pointerdown.stop @mouseenter="dataTabReuseModeHelp = 'active-tab'" @mouseleave="dataTabReuseModeHelp = null">
|
||||||
@@ -4914,18 +4914,18 @@ onUnmounted(() => {
|
|||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<Button type="button" variant="outline" class="h-auto justify-start border p-3" :class="editRoutineSourceOpenMode === 'query-tab' ? 'dbx-choice-selected' : ''" @click="setRoutineSourceOpenMode('query-tab')">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 justify-start overflow-hidden whitespace-normal border p-3" :class="editRoutineSourceOpenMode === 'query-tab' ? 'dbx-choice-selected' : ''" @click="setRoutineSourceOpenMode('query-tab')">
|
||||||
<div class="text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="text-sm font-medium">{{ t("settings.routineSourceOpenModeQueryTab") }}</div>
|
<div class="text-sm font-medium">{{ t("settings.routineSourceOpenModeQueryTab") }}</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="break-words whitespace-normal text-xs text-muted-foreground">
|
||||||
{{ t("settings.routineSourceOpenModeQueryTabDescription") }}
|
{{ t("settings.routineSourceOpenModeQueryTabDescription") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" class="h-auto justify-start border p-3" :class="editRoutineSourceOpenMode === 'dialog' ? 'dbx-choice-selected' : ''" @click="setRoutineSourceOpenMode('dialog')">
|
<Button type="button" variant="outline" class="settings-choice-card h-auto min-w-0 justify-start overflow-hidden whitespace-normal border p-3" :class="editRoutineSourceOpenMode === 'dialog' ? 'dbx-choice-selected' : ''" @click="setRoutineSourceOpenMode('dialog')">
|
||||||
<div class="text-left">
|
<div class="w-full min-w-0 text-left">
|
||||||
<div class="text-sm font-medium">{{ t("settings.routineSourceOpenModeDialog") }}</div>
|
<div class="text-sm font-medium">{{ t("settings.routineSourceOpenModeDialog") }}</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="break-words whitespace-normal text-xs text-muted-foreground">
|
||||||
{{ t("settings.routineSourceOpenModeDialogDescription") }}
|
{{ t("settings.routineSourceOpenModeDialogDescription") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { cn } from "@/lib/common/utils";
|
||||||
|
|
||||||
|
const dialogSource = readFileSync(new URL("../EditorSettingsDialog.vue", import.meta.url), "utf8");
|
||||||
|
const buttonSource = readFileSync(new URL("../../ui/button/index.ts", import.meta.url), "utf8");
|
||||||
|
const templateSource = dialogSource.slice(dialogSource.indexOf("<template>"));
|
||||||
|
const buttonBaseClass = /cva\(\s*"([^"]+)"/.exec(buttonSource)?.[1];
|
||||||
|
if (!buttonBaseClass) throw new Error("Missing shared Button base class contract");
|
||||||
|
|
||||||
|
const affectedChoiceKeys = ["tabLayoutScroll", "tabLayoutWrap", "dataTabReuseAlwaysNew", "dataTabReuseSameTable", "dataTabReuseActiveTab", "routineSourceOpenModeQueryTab", "routineSourceOpenModeDialog"] as const;
|
||||||
|
|
||||||
|
function sourceIndexForKey(key: string): number {
|
||||||
|
const index = templateSource.indexOf(`t("settings.${key}")`);
|
||||||
|
if (index < 0) throw new Error(`Missing settings key: ${key}`);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buttonBlockForKey(key: string): string {
|
||||||
|
const keyIndex = sourceIndexForKey(key);
|
||||||
|
const start = templateSource.lastIndexOf("<Button", keyIndex);
|
||||||
|
const end = templateSource.indexOf("</Button>", keyIndex);
|
||||||
|
if (start < 0 || end < 0) throw new Error(`Missing Button for settings key: ${key}`);
|
||||||
|
return templateSource.slice(start, end + "</Button>".length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openingTag(source: string, element: string): string {
|
||||||
|
const start = source.indexOf(`<${element}`);
|
||||||
|
const end = tagEnd(source, start);
|
||||||
|
if (start < 0 || end < 0) throw new Error(`Missing <${element}> opening tag`);
|
||||||
|
return source.slice(start, end + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementTagForKey(key: string, element = "div"): string {
|
||||||
|
const keyIndex = sourceIndexForKey(key);
|
||||||
|
const start = templateSource.lastIndexOf(`<${element}`, keyIndex);
|
||||||
|
const end = tagEnd(templateSource, start);
|
||||||
|
if (start < 0 || end < 0) throw new Error(`Missing <${element}> for settings key: ${key}`);
|
||||||
|
return templateSource.slice(start, end + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementTagForKeyInButton(buttonKey: string, key: string, element = "div"): string {
|
||||||
|
const block = buttonBlockForKey(buttonKey);
|
||||||
|
const keyIndex = block.indexOf(`t("settings.${key}")`);
|
||||||
|
const start = block.lastIndexOf(`<${element}`, keyIndex);
|
||||||
|
const end = tagEnd(block, start);
|
||||||
|
if (keyIndex < 0 || start < 0 || end < 0) throw new Error(`Missing <${element}> for settings key ${key} in ${buttonKey}`);
|
||||||
|
return block.slice(start, end + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagEnd(source: string, start: number): number {
|
||||||
|
let quote: '"' | "'" | null = null;
|
||||||
|
for (let index = start; index < source.length; index += 1) {
|
||||||
|
const char = source[index];
|
||||||
|
if (quote) {
|
||||||
|
if (char === quote && source[index - 1] !== "\\") quote = null;
|
||||||
|
} else if (char === '"' || char === "'") {
|
||||||
|
quote = char;
|
||||||
|
} else if (char === ">") {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function classNameFromTag(tag: string): string {
|
||||||
|
const match = /\sclass="([^"]+)"/.exec(tag);
|
||||||
|
if (!match) throw new Error(`Missing static class attribute in: ${tag}`);
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectClassTokens(className: string, tokens: readonly string[]) {
|
||||||
|
const classes = new Set(className.split(/\s+/));
|
||||||
|
for (const token of tokens) expect(classes.has(token), `${className} should include ${token}`).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("EditorSettingsDialog choice card containment", () => {
|
||||||
|
it("overrides the shared Button nowrap contract on two- and three-column cards", () => {
|
||||||
|
for (const key of affectedChoiceKeys) {
|
||||||
|
const cardClass = classNameFromTag(openingTag(buttonBlockForKey(key), "Button"));
|
||||||
|
expectClassTokens(cardClass, ["settings-choice-card", "min-w-0", "whitespace-normal", "overflow-hidden"]);
|
||||||
|
|
||||||
|
const mergedClass = cn(buttonBaseClass, cardClass);
|
||||||
|
expectClassTokens(mergedClass, ["min-w-0", "whitespace-normal", "overflow-hidden"]);
|
||||||
|
expect(mergedClass.split(/\s+/)).not.toContain("whitespace-nowrap");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets long two-column descriptions wrap inside a shrinkable text container", () => {
|
||||||
|
for (const key of ["tabLayoutScroll", "tabLayoutWrap", "routineSourceOpenModeQueryTab", "routineSourceOpenModeDialog"] as const) {
|
||||||
|
const block = buttonBlockForKey(key);
|
||||||
|
expect(block).toMatch(/<div class="[^"]*\bmin-w-0\b[^"]*\btext-left\b[^"]*">/);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [buttonKey, descriptionKey] of [
|
||||||
|
["tabLayoutScroll", "tabLayoutScrollDescription"],
|
||||||
|
["tabLayoutWrap", "tabLayoutWrapDescription"],
|
||||||
|
["routineSourceOpenModeQueryTab", "routineSourceOpenModeQueryTabDescription"],
|
||||||
|
["routineSourceOpenModeDialog", "routineSourceOpenModeDialogDescription"],
|
||||||
|
] as const) {
|
||||||
|
expectClassTokens(classNameFromTag(elementTagForKeyInButton(buttonKey, descriptionKey)), ["whitespace-normal", "break-words"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps three-column titles shrinkable without allowing help icons to collapse", () => {
|
||||||
|
for (const key of ["dataTabReuseAlwaysNew", "dataTabReuseSameTable", "dataTabReuseActiveTab"] as const) {
|
||||||
|
const block = buttonBlockForKey(key);
|
||||||
|
expect(block).toMatch(/<div class="[^"]*\bmin-w-0\b[^"]*\btext-left\b[^"]*">/);
|
||||||
|
expect(block).toMatch(/<div class="[^"]*\bflex\b[^"]*\bmin-w-0\b[^"]*\bitems-center\b[^"]*">/);
|
||||||
|
expectClassTokens(classNameFromTag(elementTagForKey(key)), ["min-w-0", "break-words"]);
|
||||||
|
expect(block).toMatch(/<span class="[^"]*\bshrink-0\b[^"]*cursor-help[^"]*"[^>]*>/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves intentional single-line truncation for cards with tooltips", () => {
|
||||||
|
for (const key of ["appLayoutSeparatedDescription", "appLayoutClassicDescription", "iconThemeDefaultDescription"] as const) {
|
||||||
|
expectClassTokens(classNameFromTag(elementTagForKey(key)), ["truncate"]);
|
||||||
|
}
|
||||||
|
expect(dialogSource).toContain('class="text-xs text-muted-foreground truncate">\n {{ iconThemeBlackDescriptionText }}');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user