From 85ee77e2600e35ff18097dc11ba4ca6f41c6b17e Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:43:10 +0530 Subject: [PATCH 01/18] test(databases): reproduce enum values splitting on spaces --- .../columns/enumElements.svelte.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte.test.ts 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..ef9a58f900 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte.test.ts @@ -0,0 +1,16 @@ +import { afterEach, expect, it } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import InputTags from '$lib/elements/forms/inputTags.svelte'; + +afterEach(cleanup); + +it('keeps a multi-word enum element together when submitted', async () => { + const user = userEvent.setup(); + render(InputTags, { id: 'elements', label: 'Elements', required: true }); + + await user.type(screen.getByRole('textbox', { name: 'Elements' }), 'Nizhny Novgorod{Enter}'); + + expect(screen.getByText('Nizhny Novgorod')).toBeInTheDocument(); + expect(screen.queryByText('Nizhny')).not.toBeInTheDocument(); +}); From 6390145e80b0c071cbdd60765fcbf65a29c5145c Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:45:41 +0530 Subject: [PATCH 02/18] fix(databases): preserve complete values in enum elements input --- .../table-[table]/columns/enum.svelte | 19 ++----- .../table-[table]/columns/enumElements.svelte | 52 +++++++++++++++++++ .../columns/enumElements.svelte.test.ts | 4 +- 3 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enumElements.svelte diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte index f8d6894e82..bee8197544 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte @@ -40,10 +40,9 @@ - - - - - Enum elements have a maximum length of 255 characters. This limit can not be exceeded. - - - + + import { Button, Icon, Input, Layout, Tag } from '@appwrite.io/pink-svelte'; + import { IconX } from '@appwrite.io/pink-icons-svelte'; + + let { + elements = $bindable([]), + disabled = false + }: { elements?: string[]; disabled?: boolean } = $props(); + + let value = $state(''); + + function add() { + if (disabled || !value) return; + elements = [...elements, value]; + value = ''; + } + + function keydown(event: KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault(); + add(); + } + } + + + + + + Add + + + + {#each elements as element, index} + (elements = elements.filter((_, position) => position !== 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 index ef9a58f900..7bb3c052c9 100644 --- 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 @@ -1,13 +1,13 @@ import { afterEach, expect, it } from 'vitest'; import { cleanup, render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import InputTags from '$lib/elements/forms/inputTags.svelte'; +import EnumElements from './enumElements.svelte'; afterEach(cleanup); it('keeps a multi-word enum element together when submitted', async () => { const user = userEvent.setup(); - render(InputTags, { id: 'elements', label: 'Elements', required: true }); + render(EnumElements); await user.type(screen.getByRole('textbox', { name: 'Elements' }), 'Nizhny Novgorod{Enter}'); From 957834f0a684edfbdaa5b0bb077c85c9f7d9815e Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:46:17 +0530 Subject: [PATCH 03/18] test(databases): preserve punctuation in pasted enum elements --- .../columns/enumElements.svelte.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 index 7bb3c052c9..44d713fdf6 100644 --- 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 @@ -14,3 +14,18 @@ it('keeps a multi-word enum element together when submitted', async () => { 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); + } +); From 93507abf1e8200570f9e54b1683998c2256368e3 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:47:39 +0530 Subject: [PATCH 04/18] test(databases): preserve loaded enum elements while switching columns --- .../columns/enumElements.svelte.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 index 44d713fdf6..7630aad684 100644 --- 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 @@ -5,6 +5,21 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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); From edbc697bbef8662dc9740d7b777c0fa9e5841ffc Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:48:25 +0530 Subject: [PATCH 05/18] fix(databases): retain unique case-sensitive enum elements --- .../table-[table]/columns/enumElements.svelte | 2 +- .../columns/enumElements.svelte.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) 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 index 6b8cd08e25..33b2a8c487 100644 --- 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 @@ -11,7 +11,7 @@ function add() { if (disabled || !value) return; - elements = [...elements, value]; + if (!elements.includes(value)) elements = [...elements, value]; value = ''; } 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 index 7630aad684..881ec60f08 100644 --- 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 @@ -5,6 +5,19 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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'] }); From c4432240d15b07c46d4af3f29891f186fe811346 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:49:37 +0530 Subject: [PATCH 06/18] fix(databases): reject blank enum elements without trimming inner spaces --- .../table-[table]/columns/enumElements.svelte | 5 +++-- .../columns/enumElements.svelte.test.ts | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) 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 index 33b2a8c487..68ef1b198a 100644 --- 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 @@ -10,8 +10,9 @@ let value = $state(''); function add() { - if (disabled || !value) return; - if (!elements.includes(value)) elements = [...elements, value]; + if (disabled) return; + const element = value.trim(); + if (element && !elements.includes(element)) elements = [...elements, element]; value = ''; } 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 index 881ec60f08..8d059d2fac 100644 --- 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 @@ -5,6 +5,28 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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'] }); From 6d5a9810a506d4e69948366e28bff184c7f62639 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:51:03 +0530 Subject: [PATCH 07/18] fix(databases): validate enum element Unicode character limits --- .../table-[table]/columns/enumElements.svelte | 12 +++++++++- .../columns/enumElements.svelte.test.ts | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) 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 index 68ef1b198a..b4011e4d8a 100644 --- 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 @@ -8,12 +8,18 @@ }: { elements?: string[]; disabled?: boolean } = $props(); let value = $state(''); + let error = $state(''); function add() { if (disabled) return; const element = value.trim(); + if ([...element].length > 255) { + error = 'Enum elements cannot exceed 255 characters.'; + return; + } if (element && !elements.includes(element)) elements = [...elements, element]; value = ''; + error = ''; } function keydown(event: KeyboardEvent) { @@ -29,10 +35,14 @@ id="elements" label="Elements" placeholder="Add an element" - helper="Press Enter or choose Add to add a value. Elements can contain spaces." + helper={error || + 'Press Enter or choose Add to add a value. Maximum 255 characters per element.'} + state={error ? 'error' : 'default'} + pattern=".{(0, 255)}" required={!elements.length} {disabled} bind:value + on:input={() => (error = '')} onkeydown={keydown}> Add 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 index 8d059d2fac..bf8f77ed83 100644 --- 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 @@ -5,6 +5,29 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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)); + 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)); + 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('rejects whitespace-only elements and trims only outside the value', async () => { const user = userEvent.setup(); render(EnumElements); From e53ec15892c9c6100d4988239215f607d9cdb477 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:51:37 +0530 Subject: [PATCH 08/18] test(databases): separate enum confirmation from form submission --- .../columns/enumElements.svelte.test.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 index bf8f77ed83..11e6a450ba 100644 --- 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 @@ -1,10 +1,35 @@ -import { afterEach, expect, it } from 'vitest'; +import { afterEach, expect, it, vi } from 'vitest'; import { cleanup, render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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); From 10765218270d78ce6e2268487e0efc7701b44a31 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:52:10 +0530 Subject: [PATCH 09/18] fix(databases): commit complete enum drafts when leaving the input --- .../table-[table]/columns/enumElements.svelte | 3 ++- .../columns/enumElements.svelte.test.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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 index b4011e4d8a..68b2899c99 100644 --- 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 @@ -43,7 +43,8 @@ {disabled} bind:value on:input={() => (error = '')} - onkeydown={keydown}> + onkeydown={keydown} + onblur={add}> Add 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 index 11e6a450ba..4408af1dd8 100644 --- 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 @@ -5,6 +5,23 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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'); From 8019a16d0a4746e5afd3217e45045c400eb7d3a2 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:53:10 +0530 Subject: [PATCH 10/18] fix(databases): avoid confirming enum elements during IME input --- .../table-[table]/columns/enumElements.svelte | 2 +- .../columns/enumElements.svelte.test.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) 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 index 68b2899c99..74b3ba2491 100644 --- 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 @@ -23,7 +23,7 @@ } function keydown(event: KeyboardEvent) { - if (event.key === 'Enter') { + if (event.key === 'Enter' && !event.isComposing) { event.preventDefault(); add(); } 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 index 4408af1dd8..6e7664b3bb 100644 --- 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 @@ -1,10 +1,24 @@ import { afterEach, expect, it, vi } from 'vitest'; -import { cleanup, render, screen } from '@testing-library/svelte'; +import { cleanup, fireEvent, render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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); From fc8409adadbc676b6e099a09db464363c8761042 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:55:01 +0530 Subject: [PATCH 11/18] fix(databases): restore input focus after removing enum elements --- .../table-[table]/columns/enumElements.svelte | 71 +++++++++++-------- .../columns/enumElements.svelte.test.ts | 18 +++++ 2 files changed, 58 insertions(+), 31 deletions(-) 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 index 74b3ba2491..9a5c864236 100644 --- 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 @@ -9,6 +9,13 @@ let value = $state(''); let error = $state(''); + let container: HTMLDivElement; + + function remove(index: number) { + if (disabled) return; + elements = elements.filter((_, position) => position !== index); + container.querySelector('input')?.focus(); + } function add() { if (disabled) return; @@ -30,35 +37,37 @@ } - - (error = '')} - onkeydown={keydown} - onblur={add}> - - Add - - - - {#each elements as element, index} - (elements = elements.filter((_, position) => position !== index))}> - {element} - - - {/each} +
+ + (error = '')} + onkeydown={keydown} + onblur={add}> + + 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 index 6e7664b3bb..4cf52706f1 100644 --- 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 @@ -5,6 +5,24 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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); From 967ab74c3ef0bcbae3262d756e278fd86a501bd8 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:55:48 +0530 Subject: [PATCH 12/18] fix(databases): preserve the native enum validation pattern --- .../table-[table]/columns/enumElements.svelte | 2 +- .../columns/enumElements.svelte.test.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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 index 9a5c864236..b2f45029fb 100644 --- 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 @@ -46,7 +46,7 @@ helper={error || 'Press Enter or choose Add to add a value. Maximum 255 characters per element.'} state={error ? 'error' : 'default'} - pattern=".{(0, 255)}" + pattern={'\\s*.{0,255}\\s*'} required={!elements.length} {disabled} bind:value 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 index 4cf52706f1..7ed5fec907 100644 --- 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 @@ -85,6 +85,8 @@ it.each(['a', 'η•Œ', 'πŸ˜€'])('enforces the 255-character boundary for %s', asyn const input = screen.getByRole('textbox', { name: 'Elements' }); await user.click(input); await user.paste(character.repeat(256)); + expect(input).toHaveAttribute('pattern', '\\s*.{0,255}\\s*'); + expect(input).toBeInvalid(); await user.click(screen.getByRole('button', { name: 'Add' })); expect(screen.getByText('Enum elements cannot exceed 255 characters.')).toBeInTheDocument(); @@ -93,6 +95,7 @@ it.each(['a', 'η•Œ', 'πŸ˜€'])('enforces the 255-character boundary for %s', asyn 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); @@ -102,6 +105,21 @@ it.each(['a', 'η•Œ', 'πŸ˜€'])('enforces the 255-character boundary for %s', asyn ).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); From f221d50462a448c26cd8658781df1ee3e4c1411d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:57:30 +0530 Subject: [PATCH 13/18] fix(databases): defer blur confirmation until IME composition finishes --- .../table-[table]/columns/enumElements.svelte | 26 ++++++++++++++++--- .../columns/enumElements.svelte.test.ts | 17 ++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) 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 index b2f45029fb..5d79b0379b 100644 --- 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 @@ -9,6 +9,8 @@ let value = $state(''); let error = $state(''); + let composing = false; + let commitOnEnd = false; let container: HTMLDivElement; function remove(index: number) { @@ -18,7 +20,7 @@ } function add() { - if (disabled) return; + if (disabled || composing) return; const element = value.trim(); if ([...element].length > 255) { error = 'Enum elements cannot exceed 255 characters.'; @@ -30,11 +32,27 @@ } function keydown(event: KeyboardEvent) { - if (event.key === 'Enter' && !event.isComposing) { + if (event.key === 'Enter' && !event.isComposing && !composing) { event.preventDefault(); add(); } } + + function blur() { + if (composing) { + commitOnEnd = true; + return; + } + add(); + } + + function compositionEnd() { + composing = false; + if (commitOnEnd) { + commitOnEnd = false; + add(); + } + }
@@ -52,7 +70,9 @@ bind:value on:input={() => (error = '')} onkeydown={keydown} - onblur={add}> + onblur={blur} + oncompositionstart={() => (composing = true)} + oncompositionend={compositionEnd}> Add 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 index 7ed5fec907..87c7bb9594 100644 --- 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 @@ -5,6 +5,23 @@ import EnumElements from './enumElements.svelte'; afterEach(cleanup); +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'] }); From a1894cd524d9f7702c644d2a7ed86d462fe3c284 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 12:58:32 +0530 Subject: [PATCH 14/18] test(databases): cover disabled enum editing controls --- .../columns/enumElements.svelte.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 index 87c7bb9594..cca3fa2fc8 100644 --- 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 @@ -5,6 +5,25 @@ 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' }); From 6ae5f44e2c4a6dff12638bc8d5a3b8e7d2670d5d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 13:04:29 +0530 Subject: [PATCH 15/18] fix(databases): keep enum defaults consistent with allowed elements --- .../table-[table]/columns/enum.svelte | 9 +++ .../table-[table]/columns/enum.svelte.test.ts | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte index bee8197544..79d3f453fb 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte @@ -72,6 +72,15 @@ $: handleDefaultState($required || $array); + $: if (data.elements) { + if (typeof data.default === 'string' && !data.elements.includes(data.default)) { + data.default = null; + } + if (typeof savedDefault === 'string' && !data.elements.includes(savedDefault)) { + savedDefault = null; + } + } + $: options = [ ...(data?.elements ?? []).map((element) => { return { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts new file mode 100644 index 0000000000..ede0a9744e --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts @@ -0,0 +1,69 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import EnumColumn from './enum.svelte'; + +vi.mock('$lib/elements/forms', async () => ({ + 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(); +}); From 5fe8d9a2aea22d713aa6efbd9be2e81f48f27460 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 13:05:18 +0530 Subject: [PATCH 16/18] test(databases): preserve enum values through column creation --- .../table-[table]/columns/enum.svelte.test.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts index ede0a9744e..74102fa136 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts @@ -1,7 +1,8 @@ import { afterEach, expect, it, vi } from 'vitest'; import { cleanup, render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import EnumColumn from './enum.svelte'; +import EnumColumn, { submitEnum } from './enum.svelte'; +import { sdk } from '$lib/stores/sdk'; vi.mock('$lib/elements/forms', async () => ({ InputSelect: (await import('$lib/elements/forms/inputSelect.svelte')).default @@ -67,3 +68,27 @@ it.each(['Required', 'Array'])('restores a valid default after toggling %s', asy 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 + }); +}); From e735cc16e47dab64c6f6afb749982faa16b75bf7 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 13:06:28 +0530 Subject: [PATCH 17/18] test(databases): preserve enum values and defaults through column edits --- .../table-[table]/columns/enum.svelte.test.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts index 74102fa136..5623f9de2a 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/enum.svelte.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, it, vi } from 'vitest'; import { cleanup, render, screen } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; -import EnumColumn, { submitEnum } from './enum.svelte'; +import EnumColumn, { submitEnum, updateEnum } from './enum.svelte'; import { sdk } from '$lib/stores/sdk'; vi.mock('$lib/elements/forms', async () => ({ @@ -92,3 +92,34 @@ it('creates an enum with complete values and the selected multi-word default', a 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 + }); +}); From e4a136ef0779554d6151dedcd6e4e61952ee0c27 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 15 Sep 2026 13:33:09 +0530 Subject: [PATCH 18/18] test(databases): assert enum validation behavior without matching regex text --- .../table-[table]/columns/enumElements.svelte.test.ts | 1 - 1 file changed, 1 deletion(-) 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 index cca3fa2fc8..8839a64c89 100644 --- 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 @@ -121,7 +121,6 @@ it.each(['a', 'η•Œ', 'πŸ˜€'])('enforces the 255-character boundary for %s', asyn const input = screen.getByRole('textbox', { name: 'Elements' }); await user.click(input); await user.paste(character.repeat(256)); - expect(input).toHaveAttribute('pattern', '\\s*.{0,255}\\s*'); expect(input).toBeInvalid(); await user.click(screen.getByRole('button', { name: 'Add' }));