mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user