From ae3d2a27b772d2ead0170109ee9fa4068044fcf9 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 12 Sep 2025 20:56:15 +0100 Subject: [PATCH 1/6] RM-145 Add select menus to forms Signed-off-by: Ben --- app/http/endpoints/api/forms/getforms.go | 23 +- app/http/endpoints/api/forms/updateinputs.go | 63 +- .../src/components/manage/FormInputRow.svelte | 684 ++++++++++++- frontend/src/views/Forms.svelte | 930 ++++++++++-------- go.mod | 24 +- go.sum | 38 +- 6 files changed, 1264 insertions(+), 498 deletions(-) diff --git a/app/http/endpoints/api/forms/getforms.go b/app/http/endpoints/api/forms/getforms.go index 761ffb4..375a83f 100644 --- a/app/http/endpoints/api/forms/getforms.go +++ b/app/http/endpoints/api/forms/getforms.go @@ -9,9 +9,14 @@ import ( "github.com/gin-gonic/gin" ) +type embeddedFormInput struct { + database.FormInput + Options []database.FormInputOption `json:"options"` +} + type embeddedForm struct { database.Form - Inputs []database.FormInput `json:"inputs"` + Inputs []embeddedFormInput `json:"inputs"` } func GetForms(c *gin.Context) { @@ -29,6 +34,12 @@ func GetForms(c *gin.Context) { return } + options, err := dbclient.Client.FormInputOption.GetAllOptionsByGuild(c, guildId) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err)) + return + } + data := make([]embeddedForm, len(forms)) for i, form := range forms { formInputs, ok := inputs[form.Id] @@ -36,9 +47,17 @@ func GetForms(c *gin.Context) { formInputs = make([]database.FormInput, 0) } + inputs := make([]embeddedFormInput, len(formInputs)) + for j, input := range formInputs { + inputs[j] = embeddedFormInput{ + FormInput: input, + Options: options[input.Id], + } + } + data[i] = embeddedForm{ Form: form, - Inputs: formInputs, + Inputs: inputs, } } diff --git a/app/http/endpoints/api/forms/updateinputs.go b/app/http/endpoints/api/forms/updateinputs.go index 3ceee6c..f8a4af6 100644 --- a/app/http/endpoints/api/forms/updateinputs.go +++ b/app/http/endpoints/api/forms/updateinputs.go @@ -28,11 +28,19 @@ type ( Label string `json:"label" validate:"required,min=1,max=45"` Description *string `json:"description,omitempty" validate:"omitempty,max=100"` Placeholder *string `json:"placeholder,omitempty" validate:"omitempty,min=1,max=100"` + Type int `json:"type" validate:"required,min=3,max=8"` Position int `json:"position" validate:"required,min=1,max=5"` - Style component.TextStyleTypes `json:"style" validate:"required,min=1,max=2"` + Style component.TextStyleTypes `json:"style" validate:"omitempty,required,min=1,max=2"` Required bool `json:"required"` MinLength uint16 `json:"min_length" validate:"min=0,max=1024"` // validator interprets 0 as not set MaxLength uint16 `json:"max_length" validate:"min=0,max=1024"` + Options []inputOption `json:"options,omitempty" validate:"omitempty,dive,required,min=1,max=25"` + } + + inputOption struct { + Label string `json:"label" validate:"required,min=1,max=100"` + Description *string `json:"description,omitempty" validate:"omitempty,max=100"` + Value string `json:"value" validate:"required,min=1,max=100"` } inputUpdateBody struct { @@ -213,6 +221,7 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing wrapped := database.FormInput{ Id: input.Id, FormId: formId, + Type: input.Type, Position: input.Position, CustomId: existing.CustomId, Style: uint8(input.Style), @@ -227,6 +236,35 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing if err := dbclient.Client.FormInput.UpdateTx(ctx, tx, wrapped); err != nil { return err } + + if wrapped.Type == 3 { // String Select + // Delete existing options + options, err := dbclient.Client.FormInputOption.GetOptions(ctx, wrapped.Id) + if err != nil { + return err + } + + for _, option := range options { + if err := dbclient.Client.FormInputOption.DeleteTx(ctx, tx, option.Id); err != nil { + return err + } + } + + // Add new options + for i, opt := range input.Options { + option := database.FormInputOption{ + FormInputId: wrapped.Id, + Position: i + 1, + Label: opt.Label, + Description: opt.Description, + Value: opt.Value, + } + + if _, err := dbclient.Client.FormInputOption.CreateTx(ctx, tx, option); err != nil { + return err + } + } + } } for _, input := range data.Create { @@ -235,9 +273,10 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing return err } - if _, err := dbclient.Client.FormInput.CreateTx(ctx, + formInputId, err := dbclient.Client.FormInput.CreateTx(ctx, tx, formId, + input.Type, customId, input.Position, uint8(input.Style), @@ -247,9 +286,27 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing input.Required, &input.MinLength, &input.MaxLength, - ); err != nil { + ) + + if err != nil { return err } + + if input.Type == 3 { // String Select + for i, opt := range input.Options { + option := database.FormInputOption{ + FormInputId: formInputId, + Position: i + 1, + Label: opt.Label, + Description: opt.Description, + Value: opt.Value, + } + + if _, err := dbclient.Client.FormInputOption.CreateTx(ctx, tx, option); err != nil { + return err + } + } + } } return tx.Commit(context.Background()) diff --git a/frontend/src/components/manage/FormInputRow.svelte b/frontend/src/components/manage/FormInputRow.svelte index 65e7584..1b9b060 100644 --- a/frontend/src/components/manage/FormInputRow.svelte +++ b/frontend/src/components/manage/FormInputRow.svelte @@ -8,6 +8,7 @@ import Textarea from "../form/Textarea.svelte"; import Checkbox from "../form/Checkbox.svelte"; import DoubleRangeSlider from "../form/DoubleRangeSlider.svelte"; + import Number from "../form/Number.svelte"; export let withCreateButton = false; export let withDeleteButton = false; @@ -19,6 +20,44 @@ export let data = {}; + // Initialize options if not present + $: if (data.type === 3 && !data.options) { + data.options = []; + } + + // Validate min/max selections + $: if ( + data.min_length && + data.max_length && + data.min_length > data.max_length + ) { + data.min_length = data.max_length; + } + + // Ensure max selections doesn't exceed number of options + $: if ( + data.options && + data.max_length && + data.max_length > data.options.length + ) { + data.max_length = data.options.length; + } + + // Ensure min selections doesn't exceed number of options + $: if ( + data.options && + data.min_length && + data.min_length > data.options.length + ) { + data.min_length = data.options.length; + } + + // Clear selections if no options + $: if (data.options && data.options.length === 0) { + data.min_length = undefined; + data.max_length = undefined; + } + $: windowWidth = 0; function forwardCreate() { @@ -34,13 +73,55 @@ } function updateStyle(e) { - if (e.target.value == 1) { + const styleValue = parseInt(e.target.value, 10); + data.style = styleValue; + if (styleValue === 1) { // Short if (data.max_length > 255) { data.max_length = 255; } } } + + function addDropdownItem() { + if (!data.options) { + data.options = []; + } + if (data.options.length < 25) { + data.options = [ + ...data.options, + { + label: "", + value: "", + description: "", + }, + ]; + } + } + + function removeDropdownItem(index) { + data.options = data.options.filter((_, i) => i !== index); + + // Adjust constraints if they exceed the new number of options + if (data.options.length > 0) { + if (data.min_length && data.min_length > data.options.length) { + data.min_length = data.options.length; + } + if (data.max_length && data.max_length > data.options.length) { + data.max_length = data.options.length; + } + } else { + // Clear constraints if no options left + data.min_length = undefined; + data.max_length = undefined; + data.allow_multiple = false; + } + } + + function updateDropdownItem(index, field, value) { + data.options[index][field] = value; + data.options = [...data.options]; + }
@@ -73,6 +154,49 @@
{/if} +
+ { + const newType = parseInt(e.target.value, 10); + const oldType = data.type; + data.type = newType; + + // Clear type-specific fields when switching types + if (oldType !== newType) { + // Clear options only when switching away from string select (type 3) + if (oldType === 3 && newType !== 3) { + data.options = undefined; + data.allow_multiple = undefined; + } + // Clear text-specific fields when switching away from text input (type 4) + if (oldType === 4 && newType !== 4) { + data.placeholder = undefined; + data.style = undefined; + data.min_length = undefined; + data.max_length = undefined; + } + // Clear min/max for select types that don't use custom options + if (newType !== 3 && newType !== 4) { + data.min_length = undefined; + data.max_length = undefined; + } + + if (newType > 4 && newType < 9) { + data.max_length = 25; + } + } + }} + > + + + + + + + +
{#if withDeleteButton}
-
-