2025-10-13 16:05:50 +02:00
|
|
|
<script lang="ts" module>
|
2025-03-25 17:58:26 +01:00
|
|
|
export type FormConfig = {
|
|
|
|
|
fields: FormField[],
|
|
|
|
|
submit_label: string
|
|
|
|
|
submit_red?: boolean,
|
|
|
|
|
on_submit: (values: {[key: string]: string}) => Promise<SubmitResult>,
|
2025-10-13 16:05:50 +02:00
|
|
|
};
|
2025-03-25 17:58:26 +01:00
|
|
|
export type FormField = {
|
|
|
|
|
type: string,
|
|
|
|
|
name?: string,
|
|
|
|
|
label?: string,
|
|
|
|
|
default_value?: string,
|
|
|
|
|
description?: string,
|
|
|
|
|
separator?: boolean,
|
|
|
|
|
radio_values?: string[], // Options to choose from when type is "radio"
|
|
|
|
|
pattern?: string, // Used for pattern matching on input fields
|
|
|
|
|
binding?: any
|
2025-10-13 16:05:50 +02:00
|
|
|
};
|
2025-03-25 17:58:26 +01:00
|
|
|
export type SubmitResult = {
|
|
|
|
|
success: boolean,
|
|
|
|
|
message?: string,
|
|
|
|
|
messages?: string[],
|
|
|
|
|
error_json?: GenericResponse,
|
2025-10-13 16:05:50 +02:00
|
|
|
};
|
2025-03-25 17:58:26 +01:00
|
|
|
</script>
|
|
|
|
|
<script lang="ts">
|
2021-09-21 21:39:28 +02:00
|
|
|
import { onMount } from "svelte";
|
|
|
|
|
import Spinner from "./Spinner.svelte";
|
2026-06-10 23:53:03 +02:00
|
|
|
import type { GenericResponse } from "lib/NovaAPI";
|
2021-09-21 21:39:28 +02:00
|
|
|
|
2025-10-13 16:05:50 +02:00
|
|
|
let { config }: {
|
|
|
|
|
config: FormConfig;
|
|
|
|
|
} = $props();
|
2021-09-21 21:39:28 +02:00
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
config.fields.forEach(field => {
|
|
|
|
|
if(field.default_value === undefined) {
|
|
|
|
|
field.default_value = ""
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2025-10-13 16:05:50 +02:00
|
|
|
let loading = $state(false)
|
|
|
|
|
let submitted = $state(false)
|
|
|
|
|
let submit_result: SubmitResult = $state()
|
2024-06-26 23:28:17 +02:00
|
|
|
|
2025-03-25 17:58:26 +01:00
|
|
|
const submit = async (event: SubmitEvent) => {
|
2021-09-21 21:39:28 +02:00
|
|
|
loading = true
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
|
|
let field_values = {}
|
|
|
|
|
|
2021-09-23 22:16:53 +02:00
|
|
|
config.fields.forEach(field => {
|
|
|
|
|
if (field.type === "radio") {
|
|
|
|
|
if (field.binding === undefined) {
|
|
|
|
|
field_values[field.name] = ""
|
|
|
|
|
} else {
|
|
|
|
|
field_values[field.name] = field.binding
|
|
|
|
|
}
|
2021-10-19 15:59:22 +02:00
|
|
|
} else if (field.type === "description") {
|
|
|
|
|
field_values[field.name] = ""
|
2021-09-23 22:16:53 +02:00
|
|
|
} else {
|
|
|
|
|
field_values[field.name] = field.binding.value
|
|
|
|
|
}
|
2021-09-21 21:39:28 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
submit_result = await config.on_submit(field_values)
|
2025-03-25 17:58:26 +01:00
|
|
|
if (submit_result && submit_result.error_json) {
|
2021-09-21 21:39:28 +02:00
|
|
|
submit_result = handle_errors(submit_result.error_json)
|
|
|
|
|
}
|
|
|
|
|
submitted = true
|
|
|
|
|
|
|
|
|
|
loading = false
|
|
|
|
|
return false
|
|
|
|
|
}
|
2025-03-25 17:58:26 +01:00
|
|
|
|
|
|
|
|
const field_label = (field: string) => {
|
2021-09-21 21:39:28 +02:00
|
|
|
let label = ""
|
|
|
|
|
config.fields.forEach(val => {
|
|
|
|
|
if (val.name === field) {
|
|
|
|
|
label = val.label
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return label
|
|
|
|
|
}
|
2025-03-25 17:58:26 +01:00
|
|
|
|
|
|
|
|
const handle_errors = (response: GenericResponse) => {
|
2021-09-21 21:39:28 +02:00
|
|
|
console.log(response)
|
|
|
|
|
let result = {success: false, message: "", messages: null}
|
|
|
|
|
|
|
|
|
|
if (response.value === "multiple_errors") {
|
|
|
|
|
result.messages = []
|
|
|
|
|
|
|
|
|
|
response.errors.forEach(err => {
|
|
|
|
|
if (err.value === "string_out_of_range") {
|
|
|
|
|
result.messages.push(
|
2025-03-25 17:58:26 +01:00
|
|
|
`${field_label(<string>err.extra.field)} is too long or too short.
|
2021-09-21 21:39:28 +02:00
|
|
|
It should be between ${err.extra.min_len} and
|
|
|
|
|
${err.extra.max_len} characters. Current length:
|
|
|
|
|
${err.extra.len}`
|
|
|
|
|
)
|
|
|
|
|
} else if (err.value === "field_contains_illegal_character") {
|
|
|
|
|
result.messages.push(
|
2025-03-25 17:58:26 +01:00
|
|
|
`Character '${err.extra.char}' is not allowed in ${field_label(<string>err.extra.field)}`
|
2021-09-21 21:39:28 +02:00
|
|
|
)
|
|
|
|
|
} else if (err.value === "missing_field") {
|
|
|
|
|
result.messages.push(
|
2025-03-25 17:58:26 +01:00
|
|
|
`${field_label(<string>err.extra.field)} is required`
|
2021-09-21 21:39:28 +02:00
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
result.messages.push(err.message)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
result.message = response.message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
2025-10-13 16:05:50 +02:00
|
|
|
<form method="POST" onsubmit={submit}>
|
2025-03-25 17:58:26 +01:00
|
|
|
{#if submitted && submit_result !== undefined}
|
2021-09-21 21:39:28 +02:00
|
|
|
{#if submit_result.messages}
|
|
|
|
|
<div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}>
|
|
|
|
|
Something went wrong, please correct these errors before continuing:<br/>
|
|
|
|
|
<ul>
|
|
|
|
|
{#each submit_result.messages as message}
|
|
|
|
|
<li>{message}</li>
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}>
|
2021-09-23 22:16:53 +02:00
|
|
|
{@html submit_result.message}
|
2021-09-21 21:39:28 +02:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{/if}
|
|
|
|
|
|
2022-08-04 20:19:30 +02:00
|
|
|
<div class="form">
|
2021-09-21 21:39:28 +02:00
|
|
|
{#each config.fields as field}
|
2022-08-04 20:19:30 +02:00
|
|
|
{#if field.type !== "description"}
|
|
|
|
|
<label for="input_{field.name}">
|
|
|
|
|
{field.label}
|
|
|
|
|
</label>
|
|
|
|
|
{#if field.type === "text"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="text"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "text_area"}
|
|
|
|
|
<textarea bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
class="form_input"
|
|
|
|
|
style="width: 100%; height: 10em; resize: vertical;"
|
|
|
|
|
>{field.default_value}</textarea>
|
|
|
|
|
{:else if field.type === "number"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="number"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "decimal"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="number"
|
|
|
|
|
step="0.1"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2023-02-15 19:39:31 +01:00
|
|
|
{:else if field.type === "datetime-local"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2023-02-15 19:39:31 +01:00
|
|
|
type="datetime-local"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "username"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="text"
|
|
|
|
|
autocomplete="username"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "email"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="email"
|
|
|
|
|
autocomplete="email"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "current_password"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="password"
|
|
|
|
|
autocomplete="current-password"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "new_password"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value}"
|
2025-03-25 17:58:26 +01:00
|
|
|
pattern={field.pattern}
|
2022-08-04 20:19:30 +02:00
|
|
|
type="password"
|
|
|
|
|
autocomplete="new-password"
|
2025-03-25 17:58:26 +01:00
|
|
|
class="form_input"
|
|
|
|
|
/>
|
|
|
|
|
{:else if field.type === "totp"}
|
|
|
|
|
<input bind:this={field.binding}
|
|
|
|
|
id="input_{field.name}"
|
|
|
|
|
name="{field.name}"
|
|
|
|
|
value="{field.default_value?field.default_value:""}"
|
|
|
|
|
type="text"
|
|
|
|
|
autocomplete="one-time-code"
|
|
|
|
|
pattern={"[0-9]{6}"}
|
|
|
|
|
class="form_input"
|
|
|
|
|
/>
|
2022-08-04 20:19:30 +02:00
|
|
|
{:else if field.type === "radio"}
|
|
|
|
|
<div>
|
|
|
|
|
{#each field.radio_values as val}
|
|
|
|
|
<input bind:group={field.binding}
|
|
|
|
|
id="input_{field.name}_choice_{val}"
|
2021-09-23 22:16:53 +02:00
|
|
|
name="{field.name}"
|
2022-08-04 20:19:30 +02:00
|
|
|
value={val}
|
|
|
|
|
type="radio"
|
|
|
|
|
checked={val === field.default_value}/>
|
|
|
|
|
<label for="input_{field.name}_choice_{val}">{val}</label><br/>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
2021-09-21 21:39:28 +02:00
|
|
|
{/if}
|
2022-08-04 20:19:30 +02:00
|
|
|
{/if}
|
2021-09-21 21:39:28 +02:00
|
|
|
{#if field.description}
|
2022-08-04 20:19:30 +02:00
|
|
|
<div>
|
|
|
|
|
{@html field.description}
|
|
|
|
|
</div>
|
2021-09-21 21:39:28 +02:00
|
|
|
{/if}
|
|
|
|
|
{#if field.separator}
|
2022-08-04 20:19:30 +02:00
|
|
|
<hr/>
|
2021-09-21 21:39:28 +02:00
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
|
|
<!-- Submit button -->
|
2022-08-04 20:19:30 +02:00
|
|
|
{#if config.submit_red}
|
|
|
|
|
<button type="submit" class="button_red">{@html config.submit_label}</button>
|
|
|
|
|
{:else}
|
|
|
|
|
<button type="submit" class="button_highlight">{@html config.submit_label}</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
2023-05-27 15:50:44 +02:00
|
|
|
|
|
|
|
|
{#if loading}
|
|
|
|
|
<div class="spinner_container">
|
|
|
|
|
<Spinner></Spinner>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2021-09-21 21:39:28 +02:00
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.spinner_container {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 10px;
|
2023-05-27 15:50:44 +02:00
|
|
|
right: 10px;
|
2021-09-21 21:39:28 +02:00
|
|
|
height: 100px;
|
|
|
|
|
width: 100px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|