({
+ InputSelect: (await import('$lib/elements/forms/inputSelect.svelte')).default
+}));
+vi.mock('$app/state', () => ({ page: { params: { region: 'fra', project: 'project' } } }));
+vi.mock('$lib/stores/sdk', () => ({ sdk: { forProject: vi.fn() } }));
+
+afterEach(cleanup);
+
+it('does not restore a default that was removed while the column was required', async () => {
+ const user = userEvent.setup();
+ const data = {
+ elements: ['New York', 'Tokyo'],
+ default: 'New York',
+ required: false,
+ array: false
+ };
+ render(EnumColumn, { data });
+ const toggle = screen.getByRole('checkbox', { name: /^Required/ });
+ await user.click(toggle);
+ await user.click(screen.getByRole('button', { name: 'Remove New York' }));
+ await user.click(toggle);
+
+ expect(data.default).toBeNull();
+ expect(screen.getByRole('combobox', { name: /^Default value/ })).toHaveTextContent('NULL');
+});
+
+it('preserves and selects multi-word defaults and clears a removed default', async () => {
+ const user = userEvent.setup();
+ const data = {
+ elements: ['New York', 'Tokyo'],
+ default: 'New York',
+ required: false,
+ array: false
+ };
+ render(EnumColumn, { data });
+ const select = screen.getByRole('combobox', { name: /^Default value/ });
+ expect(select).toHaveTextContent('New York');
+
+ await user.type(screen.getByRole('textbox', { name: /^Elements/ }), 'Nizhny Novgorod{Enter}');
+ expect(data.default).toBe('New York');
+ await user.click(select);
+ await user.click(screen.getByRole('option', { name: 'Nizhny Novgorod' }));
+ expect(data.default).toBe('Nizhny Novgorod');
+
+ await user.click(screen.getByRole('button', { name: 'Remove Nizhny Novgorod' }));
+ expect(data.default).toBeNull();
+ expect(select).toHaveTextContent('NULL');
+});
+
+it.each(['Required', 'Array'])('restores a valid default after toggling %s', async (name) => {
+ const user = userEvent.setup();
+ const data = { elements: ['New York'], default: 'New York', required: false, array: false };
+ render(EnumColumn, { data });
+ const toggle = screen.getByRole('checkbox', { name: new RegExp(`^${name}`) });
+ const select = screen.getByRole('combobox', { name: /^Default value/ });
+
+ await user.click(toggle);
+ expect(data.default).toBeNull();
+ expect(select).toBeDisabled();
+
+ await user.click(toggle);
+ expect(data.default).toBe('New York');
+ expect(select).toBeEnabled();
+});
+
+it('creates an enum with complete values and the selected multi-word default', async () => {
+ const user = userEvent.setup();
+ const createEnumColumn = vi.fn().mockResolvedValue({});
+ vi.mocked(sdk.forProject).mockReturnValue({ tablesDB: { createEnumColumn } } as never);
+ const data = { elements: [], default: null, required: false, array: false };
+ render(EnumColumn, { data });
+
+ await user.type(screen.getByRole('textbox', { name: /^Elements/ }), 'New York, NY{Enter}');
+ await user.click(screen.getByRole('combobox', { name: /^Default value/ }));
+ await user.click(screen.getByRole('option', { name: 'New York, NY' }));
+ await submitEnum('database', 'table', 'city', data);
+
+ expect(sdk.forProject).toHaveBeenCalledWith('fra', 'project');
+ expect(createEnumColumn).toHaveBeenCalledExactlyOnceWith({
+ databaseId: 'database',
+ tableId: 'table',
+ key: 'city',
+ elements: ['New York, NY'],
+ required: false,
+ xdefault: 'New York, NY',
+ array: false
+ });
+});
+
+it.each([
+ ['city', undefined],
+ ['destination', 'destination']
+])('edits complete enum values with key %s and the original route key', async (key, newKey) => {
+ const user = userEvent.setup();
+ const updateEnumColumn = vi.fn().mockResolvedValue({});
+ vi.mocked(sdk.forProject).mockReturnValue({ tablesDB: { updateEnumColumn } } as never);
+ const data = {
+ key,
+ elements: ['New York, NY', 'Tokyo'],
+ default: 'New York, NY',
+ required: false,
+ array: false
+ };
+ render(EnumColumn, { data, editing: true });
+
+ await user.type(screen.getByRole('textbox', { name: /^Elements/ }), 'Nizhny Novgorod{Enter}');
+ await user.click(screen.getByRole('button', { name: 'Remove Tokyo' }));
+ await updateEnum('database', 'table', data, 'city');
+
+ expect(updateEnumColumn).toHaveBeenCalledExactlyOnceWith({
+ databaseId: 'database',
+ tableId: 'table',
+ key: 'city',
+ elements: ['New York, NY', 'Nizhny Novgorod'],
+ required: false,
+ xdefault: 'New York, NY',
+ newKey
+ });
+});
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte
new file mode 100644
index 0000000000..5d79b0379b
--- /dev/null
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte
@@ -0,0 +1,93 @@
+
+
+
+
+ (error = '')}
+ onkeydown={keydown}
+ onblur={blur}
+ oncompositionstart={() => (composing = true)}
+ oncompositionend={compositionEnd}>
+
+ Add
+
+
+
+ {#each elements as element, index}
+ remove(index)}>
+ {element}
+
+
+ {/each}
+
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte.test.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte.test.ts
new file mode 100644
index 0000000000..8839a64c89
--- /dev/null
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte.test.ts
@@ -0,0 +1,231 @@
+import { afterEach, expect, it, vi } from 'vitest';
+import { cleanup, fireEvent, render, screen } from '@testing-library/svelte';
+import userEvent from '@testing-library/user-event';
+import EnumElements from './enumElements.svelte';
+
+afterEach(cleanup);
+
+it('prevents edits while disabled and supports enabling the same control', async () => {
+ const user = userEvent.setup();
+ const { rerender } = render(EnumElements, { elements: ['New York'], disabled: true });
+ const input = screen.getByRole('textbox', { name: /^Elements/ });
+ const remove = screen.getByRole('button', { name: 'Remove New York' });
+
+ expect(input).toBeDisabled();
+ expect(remove).toBeDisabled();
+ expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled();
+ await user.click(remove);
+ expect(remove).toBeInTheDocument();
+
+ await rerender({ elements: ['New York'], disabled: false });
+ await user.type(input, 'Tokyo{Enter}');
+
+ expect(screen.getByRole('button', { name: 'Remove Tokyo' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Remove New York' })).toBeInTheDocument();
+});
+
+it('waits for composition to finish when focus leaves during IME input', async () => {
+ render(EnumElements);
+ const input = screen.getByRole('textbox', { name: 'Elements' });
+ await fireEvent.compositionStart(input);
+ await fireEvent.input(input, { target: { value: '東' } });
+ await fireEvent.blur(input);
+
+ expect(screen.queryByRole('button', { name: /^Remove / })).not.toBeInTheDocument();
+
+ await fireEvent.input(input, { target: { value: '東京' } });
+ await fireEvent.compositionEnd(input);
+
+ expect(screen.getByRole('button', { name: 'Remove 東京' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Remove 東' })).not.toBeInTheDocument();
+ expect(input).toHaveValue('');
+});
+
+it('removes the chosen value and restores input focus and required validation', async () => {
+ const user = userEvent.setup();
+ render(EnumElements, { elements: ['New York', 'Tokyo'] });
+ const input = screen.getByRole('textbox', { name: /^Elements/ });
+
+ await user.click(screen.getByRole('button', { name: 'Remove New York' }));
+
+ expect(screen.queryByRole('button', { name: 'Remove New York' })).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Remove Tokyo' })).toBeInTheDocument();
+ expect(input).toHaveFocus();
+
+ await user.click(screen.getByRole('button', { name: 'Remove Tokyo' }));
+
+ expect(input).toHaveFocus();
+ expect(input).toBeRequired();
+ expect(input).toBeInvalid();
+});
+
+it('does not commit the Enter key used to confirm an IME composition', async () => {
+ const user = userEvent.setup();
+ render(EnumElements);
+ const input = screen.getByRole('textbox', { name: 'Elements' });
+ await user.type(input, '東京');
+ await fireEvent.keyDown(input, { key: 'Enter', isComposing: true });
+
+ expect(input).toHaveValue('東京');
+ expect(screen.queryByRole('button', { name: /^Remove / })).not.toBeInTheDocument();
+
+ await user.keyboard('{Enter}');
+ expect(screen.getByRole('button', { name: 'Remove 東京' })).toBeInTheDocument();
+});
+
+it('commits a complete value on Tab without trapping keyboard focus', async () => {
+ const user = userEvent.setup();
+ render(EnumElements);
+ const input = screen.getByRole('textbox', { name: 'Elements' });
+
+ await user.type(input, 'New York');
+ await user.tab();
+
+ expect(screen.getByRole('button', { name: 'Remove New York' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Add' })).toHaveFocus();
+ expect(input).toHaveValue('');
+
+ await user.click(input);
+ await user.tab();
+ expect(screen.getAllByRole('button', { name: /^Remove / })).toHaveLength(1);
+});
+
+it('adds with Enter without submitting the surrounding form', async () => {
+ const user = userEvent.setup();
+ const form = document.createElement('form');
+ const submit = vi.fn((event: Event) => event.preventDefault());
+ form.addEventListener('submit', submit);
+ document.body.append(form);
+ const save = document.createElement('button');
+ save.type = 'submit';
+ save.textContent = 'Save';
+
+ try {
+ render(EnumElements, { target: form });
+ form.append(save);
+ await user.type(screen.getByRole('textbox', { name: 'Elements' }), 'New York{Enter}');
+
+ expect(screen.getByRole('button', { name: 'Remove New York' })).toBeInTheDocument();
+ expect(submit).not.toHaveBeenCalled();
+
+ await user.click(save);
+ expect(submit).toHaveBeenCalledOnce();
+ } finally {
+ form.remove();
+ }
+});
+
+it.each(['a', '界', '😀'])('enforces the 255-character boundary for %s', async (character) => {
+ const user = userEvent.setup();
+ render(EnumElements);
+ const input = screen.getByRole('textbox', { name: 'Elements' });
+ await user.click(input);
+ await user.paste(character.repeat(256));
+ expect(input).toBeInvalid();
+ await user.click(screen.getByRole('button', { name: 'Add' }));
+
+ expect(screen.getByText('Enum elements cannot exceed 255 characters.')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /^Remove / })).not.toBeInTheDocument();
+ expect(input).toBeInvalid();
+
+ await user.clear(input);
+ await user.paste(character.repeat(255));
+ expect(input).toBeValid();
+ await user.click(screen.getByRole('button', { name: 'Add' }));
+
+ expect(screen.getAllByRole('button', { name: /^Remove / })).toHaveLength(1);
+ expect(input).toBeValid();
+ expect(
+ screen.queryByText('Enum elements cannot exceed 255 characters.')
+ ).not.toBeInTheDocument();
+});
+
+it('applies native length validation to the trimmed value', async () => {
+ const user = userEvent.setup();
+ render(EnumElements);
+ const input = screen.getByRole('textbox', { name: 'Elements' });
+ await user.click(input);
+ await user.paste(` ${'界'.repeat(255)} `);
+
+ expect(input).toBeValid();
+ await user.keyboard('{Enter}');
+ expect(screen.getByRole('button', { name: /^Remove / })).toHaveAttribute(
+ 'aria-label',
+ `Remove ${'界'.repeat(255)}`
+ );
+});
+
+it('rejects whitespace-only elements and trims only outside the value', async () => {
+ const user = userEvent.setup();
+ render(EnumElements);
+ const input = screen.getByRole('textbox', { name: 'Elements' });
+
+ await user.type(input, ' {Enter}');
+
+ expect(screen.queryByRole('button', { name: /^Remove / })).not.toBeInTheDocument();
+ expect(input).toHaveValue('');
+ expect(input).toBeRequired();
+ expect(input).toBeInvalid();
+
+ await user.type(input, ' New York {Enter}');
+
+ expect(screen.getByRole('button', { name: 'Remove New York' })).toHaveAttribute(
+ 'aria-label',
+ 'Remove New York'
+ );
+ expect(input).not.toBeRequired();
+ expect(input).toBeValid();
+});
+
+it('ignores exact duplicates while preserving case-sensitive enum values', async () => {
+ const user = userEvent.setup();
+ render(EnumElements, { elements: ['Home'] });
+ const input = screen.getByRole('textbox', { name: /^Elements/ });
+
+ await user.type(input, 'Home{Enter}home{Enter}');
+
+ expect(screen.getAllByRole('button', { name: /^Remove / })).toHaveLength(2);
+ expect(screen.getByRole('button', { name: 'Remove Home' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Remove home' })).toBeInTheDocument();
+ expect(input).toHaveValue('');
+});
+
+it('preserves loaded elements exactly and follows changes to the edited column', async () => {
+ const { rerender } = render(EnumElements, { elements: ['New York, NY', '0', 'A B'] });
+
+ expect(
+ screen
+ .getAllByRole('button', { name: /^Remove / })
+ .map((button) => button.getAttribute('aria-label'))
+ ).toEqual(['Remove New York, NY', 'Remove 0', 'Remove A B']);
+
+ await rerender({ elements: ['Nizhny Novgorod'] });
+
+ expect(screen.getByRole('button', { name: 'Remove Nizhny Novgorod' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Remove New York, NY' })).not.toBeInTheDocument();
+});
+
+it('keeps a multi-word enum element together when submitted', async () => {
+ const user = userEvent.setup();
+ render(EnumElements);
+
+ await user.type(screen.getByRole('textbox', { name: 'Elements' }), 'Nizhny Novgorod{Enter}');
+
+ expect(screen.getByText('Nizhny Novgorod')).toBeInTheDocument();
+ expect(screen.queryByText('Nizhny')).not.toBeInTheDocument();
+});
+
+it.each(['New York, NY', 'MTS BS73 Home', 'North / South'])(
+ 'keeps pasted %s as one element',
+ async (value) => {
+ const user = userEvent.setup();
+ render(EnumElements);
+
+ await user.click(screen.getByRole('textbox', { name: 'Elements' }));
+ await user.paste(value);
+ await user.click(screen.getByRole('button', { name: 'Add' }));
+
+ expect(screen.getByRole('button', { name: `Remove ${value}` })).toBeInTheDocument();
+ expect(screen.getAllByRole('button', { name: /^Remove / })).toHaveLength(1);
+ }
+);