diff --git a/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte b/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte index d88f3df6322..8d8ef0c5dd9 100644 --- a/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte +++ b/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte @@ -284,6 +284,9 @@ {#snippet trigger(toggle)} {@const open = () => { closeForm() + if (knownTags.size === 0) { + openCreate('') + } toggle() }}
@@ -331,9 +334,11 @@
{#snippet listPage()} -
- -
+ {#if knownTags.size > 0} +
+ +
+ {/if}
{#each selectedTags as tag (tag)} {@render tagRow(tag, true)} @@ -470,7 +475,15 @@ {/if}
-
+
{ + e.preventDefault() + if (!opts.submitDisabled) { + opts.onSubmit() + } + }} + >
@@ -520,5 +533,5 @@ The tag “{newTagName.trim()}” already exists.

{/if} - + {/snippet} diff --git a/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte.spec.ts b/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte.spec.ts index afe19b14ba4..7391ec20ad2 100644 --- a/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte.spec.ts +++ b/js-packages/web-console/src/lib/components/pipelines/table/Tags.svelte.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { page } from 'vitest/browser' +import { page, userEvent } from 'vitest/browser' import { render } from 'vitest-browser-svelte' import type { PipelineManagerApi } from '$lib/compositions/usePipelineManager.svelte' import Tags from './Tags.svelte' @@ -90,13 +90,30 @@ describe('Tags.svelte', () => { tags: ['prod|ef4444'] }) }) + + it('opens the create form directly when no known tags exist yet', async () => { + renderTags({ tags: [], knownTags: [] }) + await page.getByRole('button', { name: 'Tag' }).click() + + // Empty pool skips the search list and lands on create immediately. + await expect.element(page.getByPlaceholder('Tag name')).toBeVisible() + await expect.element(page.getByRole('button', { name: 'Create' })).toBeVisible() + expect(page.getByPlaceholder('Search').elements()).toHaveLength(0) + }) + + it('shows search when known tags exist', async () => { + renderTags({ tags: [], knownTags: ['dev'] }) + await page.getByRole('button', { name: 'Tag' }).click() + + await expect.element(page.getByPlaceholder('Search')).toBeVisible() + }) }) - describe('create form validation', () => { + describe('create form', () => { it('blocks an invalid name and surfaces the reason; a valid name is color-encoded', async () => { - const { patchPipeline } = renderTags({ tags: [] }) + const { patchPipeline } = renderTags({ tags: [], knownTags: [] }) + // Empty knownTags opens create directly — no "Create a new tag" list click. await page.getByRole('button', { name: 'Tag' }).click() - await page.getByRole('button', { name: 'Create a new tag' }).click() const nameInput = page.getByPlaceholder('Tag name') const createButton = page.getByRole('button', { name: 'Create' }) @@ -117,5 +134,61 @@ describe('Tags.svelte', () => { tags: ['qa|ef4444'] }) }) + + it('submits the create form when Enter is pressed in the name field', async () => { + const { patchPipeline } = renderTags({ tags: [], knownTags: [] }) + await page.getByRole('button', { name: 'Tag' }).click() + + const nameInput = page.getByPlaceholder('Tag name') + await nameInput.fill('qa') + // `fill` leaves the input focused; Enter submits the surrounding
. + await userEvent.keyboard('{Enter}') + + expect(patchPipeline).toHaveBeenCalledWith('test-pipeline', { + tags: ['qa|ef4444'] + }) + }) + + it('does not submit on Enter when the create button is disabled', async () => { + const { patchPipeline } = renderTags({ tags: [], knownTags: [] }) + await page.getByRole('button', { name: 'Tag' }).click() + + const nameInput = page.getByPlaceholder('Tag name') + await expect.element(page.getByRole('button', { name: 'Create' })).toBeDisabled() + // Focus the empty name field and press Enter — submit must stay a no-op. + await nameInput.click() + await userEvent.keyboard('{Enter}') + + expect(patchPipeline).not.toHaveBeenCalled() + }) + + it('selects a color with Tab and Space before submitting', async () => { + const { patchPipeline } = renderTags({ tags: [], knownTags: [] }) + await page.getByRole('button', { name: 'Tag' }).click() + + const nameInput = page.getByPlaceholder('Tag name') + await nameInput.fill('qa') + // Tab moves from the name field onto the first color swatch (Red), then + // Purple; Space activates that focused swatch like a mouse click. + await userEvent.keyboard('{Tab}') + await userEvent.keyboard('{Tab}') + await expect.element(page.getByRole('button', { name: 'Purple' })).toHaveFocus() + await userEvent.keyboard(' ') + + await expect + .element(page.getByRole('button', { name: 'Purple' })) + .toHaveAttribute('aria-pressed', 'true') + // Enter on a color swatch only re-activates the button (expected). Shift+Tab + // the same number of times returns focus to the name field, then Enter submits. + await userEvent.keyboard('{Shift>}{Tab}{/Shift}') + await userEvent.keyboard('{Shift>}{Tab}{/Shift}') + await expect.element(nameInput).toHaveFocus() + await userEvent.keyboard('{Enter}') + + // Purple is #a855f7 in the palette → stored as |a855f7. + expect(patchPipeline).toHaveBeenCalledWith('test-pipeline', { + tags: ['qa|a855f7'] + }) + }) }) })