fix(editor): Cleanup legacy button variants (#41175)

This commit is contained in:
Sebastien Powell 2026-10-15 09:31:34 +00:00 committed by GitHub
parent 4cb7a6ee3d
commit ba97e46afc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 67 additions and 44 deletions

View File

@ -7,11 +7,6 @@ const stubs = ['N8nSpinner', 'N8nIcon'];
describe('components', () => {
describe('N8nButton', () => {
it('maps the legacy subtle variant to brand', () => {
const wrapper = render(N8nButton, { props: { variant: 'subtle' }, global: { stubs } });
expect(wrapper.getByRole('button').className).toContain('brand');
});
describe('rendering', () => {
it('should render correctly with default props', () => {
const wrapper = render(N8nButton, {

View File

@ -27,14 +27,6 @@ const effectiveSize = computed(() => {
return props.size;
});
// Map legacy variant values to current ones
const effectiveVariant = computed(() => {
if (props.variant === 'subtle') return 'brand';
if (props.variant === 'highlight') return 'ghost';
if (props.variant === 'highlight-fill') return 'ghost';
return props.variant;
});
const computedIconSize = computed((): IconSize | undefined => {
if (props.iconSize) return props.iconSize;
if (effectiveSize.value === 'xsmall') return 'xsmall';
@ -57,7 +49,7 @@ const classes = computed(() =>
cn(
'button',
$style.button,
$style[effectiveVariant.value],
$style[props.variant],
$style[effectiveSize.value],
props.loading && $style.loading,
props.iconOnly && $style.iconOnly,

View File

@ -373,7 +373,7 @@ onBeforeUnmount(() => {
<span>{{ emptyText }}</span>
<N8nButton
v-if="isFilteredByTab"
variant="subtle"
variant="outline"
size="small"
data-test-id="command-bar-search-all"
@mousedown.prevent

View File

@ -38,12 +38,6 @@ const effectiveSize = computed(() => {
return props.size;
});
const effectiveVariant = computed(() => {
if (props.variant === 'highlight') return 'ghost';
if (props.variant === 'highlight-fill') return 'ghost';
return props.variant;
});
const computedIconSize = computed((): IconSize => {
if (effectiveSize.value === 'xsmall') return 'xsmall';
return effectiveSize.value;
@ -53,7 +47,7 @@ const classes = computed(() =>
cn(
'button',
$style.toggle,
$style[effectiveVariant.value],
$style[props.variant],
$style[effectiveSize.value],
$style.iconOnly,
props.disabled && $style.disabled,

View File

@ -6,15 +6,12 @@ import { type IconName } from '../components/N8nIcon/icons';
const BUTTON_VARIANT = ['solid', 'brand', 'ghost', 'outline', 'destructive', 'success'] as const;
export type ButtonVariant = (typeof BUTTON_VARIANT)[number];
/** @deprecated Use 'brand' for 'subtle'. Use 'ghost' for 'highlight' or 'highlight-fill'. */
export type LegacyButtonVariant = 'highlight' | 'highlight-fill' | 'subtle';
const BUTTON_SIZE = ['mini', 'xmini', 'small', 'medium', 'large', 'xlarge', 'xsmall'] as const;
export type ButtonSize = (typeof BUTTON_SIZE)[number];
export interface ButtonProps {
/** Determines the visual style of the button */
variant?: ButtonVariant | LegacyButtonVariant;
variant?: ButtonVariant;
/** Determines the size of the button */
size?: ButtonSize;
/** If passed, the button will be rendered as a link */

View File

@ -170,7 +170,7 @@ describe('AgentChatMessageList', () => {
});
const callout = wrapper.get('[data-testid="agent-chat-retry-error"]');
const retry = callout.get('[data-testid="agent-chat-retry"]');
expect(wrapper.findComponent({ name: 'N8nButton' }).props('variant')).toBe('subtle');
expect(wrapper.findComponent({ name: 'N8nButton' }).props('variant')).toBe('outline');
await retry.trigger('click');
expect(wrapper.emitted('retry')).toBeUndefined();
await wrapper.setProps({ retryDisabled: false });

View File

@ -443,6 +443,28 @@ describe('AgentEvalChecksPanel', () => {
expect(getAllByTestId(/agent-eval-check-/)).toHaveLength(4);
});
it('marks the selected status filter', async () => {
const user = userEvent.setup();
const { getByTestId } = renderFour();
const all = getByTestId('agent-eval-checks-filter-all');
const pass = getByTestId('agent-eval-checks-filter-pass');
expect(all).toHaveAttribute('aria-pressed', 'true');
expect(all.className).toContain('brand');
expect(pass).toHaveAttribute('aria-pressed', 'false');
expect(pass.className).toContain('outline');
expect(pass.className).not.toContain('brand');
await user.click(pass);
expect(pass).toHaveAttribute('aria-pressed', 'true');
expect(pass.className).toContain('brand');
expect(all).toHaveAttribute('aria-pressed', 'false');
expect(all.className).toContain('outline');
expect(all.className).not.toContain('brand');
});
it('filters down to only passing rows', async () => {
const user = userEvent.setup();
const { getAllByTestId, getByTestId } = renderFour();
@ -1447,6 +1469,7 @@ describe('AgentEvalChecksPanel', () => {
await vi.waitFor(() =>
expect(queryByTestId('agent-eval-checks-filter-needs-work')).not.toBeInTheDocument(),
);
expect(getByTestId('agent-eval-checks-filter-all')).toHaveAttribute('aria-pressed', 'true');
expect(getAllByTestId(/agent-eval-check-/)).toHaveLength(2);
});
});

View File

@ -20,7 +20,7 @@ const i18n = useI18n();
<template v-if="retryMessageId" #trailingContent>
<N8nButton
size="small"
variant="subtle"
variant="outline"
:class="$style.retryButton"
:disabled="retryDisabled"
data-testid="agent-chat-retry"

View File

@ -454,7 +454,8 @@ onBeforeUnmount(store.stopPollingRun);
:key="option.key"
type="button"
size="small"
:variant="statusFilter === option.key ? 'outline' : 'subtle'"
:variant="statusFilter === option.key ? 'brand' : 'outline'"
:aria-pressed="statusFilter === option.key"
:data-testid="`agent-eval-checks-filter-${option.key}`"
@click="setStatusFilter(option.key)"
>
@ -482,7 +483,7 @@ onBeforeUnmount(store.stopPollingRun);
{{ i18n.baseText('agents.builder.agentEvals.suggestion.applyAll') }}
</N8nButton>
<N8nButton
variant="subtle"
variant="outline"
size="small"
:disabled="disabled || rerunning"
:loading="rerunning || inFlight"
@ -553,7 +554,7 @@ onBeforeUnmount(store.stopPollingRun);
</N8nButton>
<div v-if="hasMore" :class="$style.loadMore">
<N8nButton variant="subtle" size="small" :loading="review.loadingMore" @click="onLoadMore">
<N8nButton variant="outline" size="small" :loading="review.loadingMore" @click="onLoadMore">
{{ i18n.baseText('agents.builder.agentEvals.review.loadMore') }}
</N8nButton>
</div>

View File

@ -279,7 +279,7 @@ watch(
<!-- Only the rule is editable; a case with no criteria column can still be deleted. -->
<N8nButton
v-if="whatToCheck"
variant="subtle"
variant="outline"
size="small"
icon-only
:disabled="disabled || runningCheck"
@ -292,7 +292,7 @@ watch(
</template>
</N8nButton>
<N8nButton
variant="subtle"
variant="outline"
size="small"
icon-only
:disabled="disabled"
@ -330,7 +330,7 @@ watch(
{{ i18n.baseText('generic.save') }}
</N8nButton>
<N8nButton
variant="subtle"
variant="outline"
size="small"
:data-test-id="testId && `${testId}-rule-cancel`"
@click="onCancelEditWhatToCheck"
@ -421,7 +421,7 @@ watch(
<div :class="$style.correctionActions">
<N8nButton
size="small"
variant="subtle"
variant="outline"
:disabled="disabled"
:loading="runningCheck"
:data-test-id="testId && `${testId}-run-check`"

View File

@ -218,6 +218,23 @@ describe('TrialBanner', () => {
expect(button?.getAttribute('style')).toBeNull();
});
it('should map the subtle variant to outline without legacy style overrides', () => {
cloudPlanStore.state.data = {
bannerConfig: {
cta: {
text: 'Upgrade',
style: 'success',
variant: 'subtle',
},
},
} as never;
const { container } = renderComponent();
const button = container.querySelector('button');
expect(button?.className).toContain('outline');
expect(button?.getAttribute('style')).toBeNull();
});
it('should apply legacy style overrides when backend sends style without variant', () => {
cloudPlanStore.state.data = {
bannerConfig: {

View File

@ -106,14 +106,18 @@ const shouldHideTrialBanner = computed(
// Banner config from backend
const bannerTimeLeft = computed(() => cloudPlanStore.bannerTimeLeft);
const showExecutions = computed(() => cloudPlanStore.showExecutions);
const bannerCta = computed(() => ({
text: cloudPlanStore.bannerCta.text,
icon: cloudPlanStore.bannerCta.icon as IconName | undefined,
size: cloudPlanStore.bannerCta.size as 'small' | 'medium',
style: cloudPlanStore.bannerCta.style,
variant: cloudPlanStore.bannerCta.variant as ButtonVariant | undefined,
href: cloudPlanStore.bannerCta.href,
}));
const bannerCta = computed(() => {
const variant = cloudPlanStore.bannerCta.variant;
return {
text: cloudPlanStore.bannerCta.text,
icon: cloudPlanStore.bannerCta.icon as IconName | undefined,
size: cloudPlanStore.bannerCta.size as 'small' | 'medium',
style: cloudPlanStore.bannerCta.style,
// BannerConfig.cta.variant still lists the old button value 'subtle'.
variant: variant === 'subtle' ? 'outline' : variant,
href: cloudPlanStore.bannerCta.href,
};
});
const bannerIcon = computed(() => cloudPlanStore.bannerIcon as IconName | undefined);
// If backend sends variant, use it directly. Otherwise, map legacy style to variant.

View File

@ -375,7 +375,7 @@ onBeforeUnmount(() => {
<N8nLoading v-if="isLoadingMore" :rows="1" />
<N8nButton
v-if="hasLoadError"
variant="subtle"
variant="outline"
size="small"
:disabled="isLoading || isLoadingMore"
@click="retry"