fix(frontend): add folder picker validation, error handling, and loading state (#7987)

* fix(frontend): add folder name validation and error handling to folder picker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(frontend): add loading state to folder picker select

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): add error toast for folder list loading failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): reassign userStore folders array to trigger reactivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-02-17 17:03:23 +00:00
committed by GitHub
parent 90fa5b3ced
commit 4ea1692ee2
+87 -42
View File
@@ -7,7 +7,11 @@
import Select from './select/Select.svelte'
import TextInput from './text_input/TextInput.svelte'
import Label from './Label.svelte'
import InputError from './InputError.svelte'
import { tick } from 'svelte'
import { sendUserToast } from '$lib/toast'
const VALID_FOLDER_NAME = /^[a-zA-Z_0-9]+$/
let folders: { name: string; write: boolean }[] = $state([])
let filterText: string = $state('')
@@ -17,6 +21,8 @@
let viewFolder: Drawer | null = $state(null)
let newFolderName: string = $state('')
let folderCreated: string | undefined = $state(undefined)
let creating: boolean = $state(false)
let loadingFolders: boolean = $state(true)
type Props = {
folderName: string
@@ -37,30 +43,37 @@
}: Props = $props()
async function loadFolders(): Promise<void> {
let initialFolders: { name: string; write: boolean }[] = []
let initialFolder = ''
if (initialPath?.split('/')?.[0] == 'f') {
initialFolder = initialPath?.split('/')?.[1]
initialFolders.push({ name: initialFolder, write: true })
}
loadingFolders = true
try {
let initialFolders: { name: string; write: boolean }[] = []
let initialFolder = ''
if (initialPath?.split('/')?.[0] == 'f') {
initialFolder = initialPath?.split('/')?.[1]
initialFolders.push({ name: initialFolder, write: true })
}
const excludedFolders = [initialFolder, 'app_groups', 'app_custom', 'app_themes']
const excludedFolders = [initialFolder, 'app_groups', 'app_custom', 'app_themes']
folders = initialFolders.concat(
(
await FolderService.listFolderNames({
workspace: $workspaceStore!
})
folders = initialFolders.concat(
(
await FolderService.listFolderNames({
workspace: $workspaceStore!
})
)
.filter((x) => !excludedFolders.includes(x))
.map((x) => ({
name: x,
write:
$userStore?.folders?.includes(x) == true ||
($userStore?.is_admin ?? false) ||
($userStore?.is_super_admin ?? false)
}))
)
.filter((x) => !excludedFolders.includes(x))
.map((x) => ({
name: x,
write:
$userStore?.folders?.includes(x) == true ||
($userStore?.is_admin ?? false) ||
($userStore?.is_super_admin ?? false)
}))
)
} catch (e) {
sendUserToast(`Could not load folders: ${e}`, true)
} finally {
loadingFolders = false
}
}
async function openCreateFolder() {
@@ -72,14 +85,24 @@
}
async function addFolder() {
await FolderService.createFolder({
workspace: $workspaceStore ?? '',
requestBody: { name: newFolderName }
})
folderCreated = newFolderName
$userStore?.folders?.push(newFolderName)
folderName = newFolderName
loadFolders()
if (nameError || !newFolderName || creating) return
creating = true
try {
await FolderService.createFolder({
workspace: $workspaceStore ?? '',
requestBody: { name: newFolderName }
})
folderCreated = newFolderName
if ($userStore) {
$userStore.folders = [...($userStore.folders ?? []), newFolderName]
}
folderName = newFolderName
loadFolders()
} catch (e) {
sendUserToast(`Could not create folder: ${e}`, true)
} finally {
creating = false
}
}
let selectItems = $derived(
@@ -90,6 +113,16 @@
}))
)
let nameError = $derived(
!newFolderName
? ''
: !VALID_FOLDER_NAME.test(newFolderName)
? 'Folder name can only contain alphanumeric characters and underscores'
: folders.some((f) => f.name === newFolderName)
? 'A folder with this name already exists'
: ''
)
let noMatchingItems = $derived(
filterText &&
!selectItems.some((item) => item.label.toLowerCase().includes(filterText.toLowerCase()))
@@ -120,20 +153,29 @@
<div class="flex flex-col gap-4">
<Label label="Folder name">
<TextInput
bind:this={nameInput}
bind:value={newFolderName}
inputProps={{
placeholder: 'folder_name',
onkeydown: (e: KeyboardEvent) => {
if (e.key === 'Enter' && newFolderName) {
e.preventDefault()
addFolder()
bind:this={nameInput}
bind:value={newFolderName}
error={!!nameError}
inputProps={{
placeholder: 'folder_name',
onkeydown: (e: KeyboardEvent) => {
if (e.key === 'Enter' && newFolderName) {
e.preventDefault()
addFolder()
}
}
}
}}
/>
}}
/>
<InputError error={nameError} />
</Label>
<Button variant="accent" disabled={!newFolderName} on:click={addFolder}>Create</Button>
<Button
variant="accent"
disabled={!newFolderName || !!nameError || creating}
loading={creating}
on:click={addFolder}
>
Create
</Button>
</div>
{/if}
</DrawerContent>
@@ -153,12 +195,15 @@
bind:open={selectOpen}
items={selectItems}
disabled={disabled || disableEditing}
loading={loadingFolders}
{size}
placeholder="Select folder"
>
{#snippet bottomSnippet({ close })}
<button
class="sticky py-2 px-4 w-full text-left text-xs font-semibold text-medium hover:bg-surface-hover flex items-center justify-center gap-2 border-t border-border-light {noMatchingItems ? 'bg-surface-hover' : ''}"
class="sticky py-2 px-4 w-full text-left text-xs font-medium hover:bg-surface-hover flex items-center justify-center gap-2 border-t border-border-light {noMatchingItems
? 'bg-surface-hover'
: ''}"
onclick={() => {
close()
openCreateFolder()