Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
85ee77e
test(databases): reproduce enum values splitting on spaces
HarshMN2345 Sep 15, 2026
6390145
fix(databases): preserve complete values in enum elements input
HarshMN2345 Sep 15, 2026
957834f
test(databases): preserve punctuation in pasted enum elements
HarshMN2345 Sep 15, 2026
93507ab
test(databases): preserve loaded enum elements while switching columns
HarshMN2345 Sep 15, 2026
edbc697
fix(databases): retain unique case-sensitive enum elements
HarshMN2345 Sep 15, 2026
c443224
fix(databases): reject blank enum elements without trimming inner spaces
HarshMN2345 Sep 15, 2026
6d5a981
fix(databases): validate enum element Unicode character limits
HarshMN2345 Sep 15, 2026
e53ec15
test(databases): separate enum confirmation from form submission
HarshMN2345 Sep 15, 2026
1076521
fix(databases): commit complete enum drafts when leaving the input
HarshMN2345 Sep 15, 2026
8019a16
fix(databases): avoid confirming enum elements during IME input
HarshMN2345 Sep 15, 2026
fc8409a
fix(databases): restore input focus after removing enum elements
HarshMN2345 Sep 15, 2026
967ab74
fix(databases): preserve the native enum validation pattern
HarshMN2345 Sep 15, 2026
f221d50
fix(databases): defer blur confirmation until IME composition finishes
HarshMN2345 Sep 15, 2026
a1894cd
test(databases): cover disabled enum editing controls
HarshMN2345 Sep 15, 2026
6ae5f44
fix(databases): keep enum defaults consistent with allowed elements
HarshMN2345 Sep 15, 2026
5fe8d9a
test(databases): preserve enum values through column creation
HarshMN2345 Sep 15, 2026
e735cc1
test(databases): preserve enum values and defaults through column edits
HarshMN2345 Sep 15, 2026
e4a136e
test(databases): assert enum validation behavior without matching reg…
HarshMN2345 Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@

<script lang="ts">
import { createConservative } from '$lib/helpers/stores';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { InputSelect, InputTags } from '$lib/elements/forms';
import { Icon, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { InputSelect } from '$lib/elements/forms';
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
import EnumElements from './enumElements.svelte';

export let editing = false;
export let disabled = false;
Expand Down Expand Up @@ -73,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 {
Expand All @@ -88,19 +96,7 @@
].filter(Boolean);
</script>

<InputTags
required
id="elements"
label="Elements"
bind:tags={data.elements}
placeholder="Add elements here">
<Tooltip slot="info" maxWidth="15rem">
<Icon icon={IconInfo} size="s" />
<Typography.Caption variant="400" slot="tooltip">
Enum elements have a maximum length of 255 characters. This limit can not be exceeded.
</Typography.Caption>
</Tooltip>
</InputTags>
<EnumElements bind:elements={data.elements} {disabled} />

<InputSelect
id="default"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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, updateEnum } from './enum.svelte';
import { sdk } from '$lib/stores/sdk';

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();
});

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
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<script lang="ts">
import { Button, Icon, Input, Layout, Tag } from '@appwrite.io/pink-svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';

let {
elements = $bindable<string[]>([]),
disabled = false
}: { elements?: string[]; disabled?: boolean } = $props();

let value = $state('');
let error = $state('');
let composing = false;
let commitOnEnd = false;
let container: HTMLDivElement;

function remove(index: number) {
if (disabled) return;
elements = elements.filter((_, position) => position !== index);
container.querySelector('input')?.focus();
}

function add() {
if (disabled || composing) 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) {
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();
}
}
</script>

<div bind:this={container}>
<Layout.Stack gap="s">
<Input.Text
id="elements"
label="Elements"
placeholder="Add an element"
helper={error ||
'Press Enter or choose Add to add a value. Maximum 255 characters per element.'}
state={error ? 'error' : 'default'}
pattern={'\\s*.{0,255}\\s*'}
required={!elements.length}
{disabled}
bind:value
on:input={() => (error = '')}
onkeydown={keydown}
onblur={blur}
oncompositionstart={() => (composing = true)}
oncompositionend={compositionEnd}>
<Button.Button slot="end" type="button" variant="text" {disabled} on:click={add}>
Add
</Button.Button>
</Input.Text>
<Layout.Stack direction="row" wrap="wrap" gap="s">
{#each elements as element, index}
<Tag
size="s"
{disabled}
aria-label={`Remove ${element}`}
on:click={() => remove(index)}>
{element}
<Icon slot="end" icon={IconX} size="s" />
</Tag>
{/each}
</Layout.Stack>
</Layout.Stack>
</div>
Loading