Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 54 additions & 30 deletions app/http/endpoints/api/forms/updateinputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ 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"`
Type int `json:"type" validate:"required,oneof=3 4 5 6 7 8 21 22"`
Position int `json:"position" validate:"required,min=1,max=5"`
Style component.TextStyleTypes `json:"style" validate:"omitempty,required,min=1,max=2"`
Required bool `json:"required"`
Expand Down Expand Up @@ -161,30 +161,24 @@ func UpdateInputs(c *gin.Context) {
return
}

// Validate string select inputs have at least one option and unique option values
// Validate inputs that require options (String Select, RadioGroup, CheckboxGroup)
optionTypes := map[int]string{
3: "String select",
21: "Radio group",
22: "Checkbox group",
}

for _, input := range data.Create {
if input.Type == 3 {
if len(input.Options) == 0 {
c.JSON(400, utils.ErrorStr("String select inputs must have at least one option"))
return
}
if err := validateUniqueOptionValues(input.Options); err != nil {
c.JSON(400, utils.ErrorStr("%v", err))
return
}
if err := validateInputOptions(input, optionTypes); err != nil {
c.JSON(400, utils.ErrorStr("%v", err))
return
}
}

for _, input := range data.Update {
if input.Type == 3 {
if len(input.Options) == 0 {
c.JSON(400, utils.ErrorStr("String select inputs must have at least one option"))
return
}
if err := validateUniqueOptionValues(input.Options); err != nil {
c.JSON(400, utils.ErrorStr("%v", err))
return
}
if err := validateInputOptions(input.inputCreateBody, optionTypes); err != nil {
c.JSON(400, utils.ErrorStr("%v", err))
return
}
}

Expand Down Expand Up @@ -269,6 +263,36 @@ func validateUniqueOptionValues(options []inputOption) error {
return nil
}

func validateInputOptions(input inputCreateBody, optionTypes map[int]string) error {
typeName, requiresOptions := optionTypes[input.Type]
if !requiresOptions {
return nil
}

// Radio Group (type 21) requires 2-10 options, Checkbox Group (type 22) requires 1-10 options
if input.Type == 21 {
if len(input.Options) < 2 {
return fmt.Errorf("Radio group inputs must have at least 2 options")
}
if len(input.Options) > 10 {
return fmt.Errorf("Radio group inputs must have at most 10 options")
}
} else if input.Type == 22 {
if len(input.Options) == 0 {
return fmt.Errorf("%s inputs must have at least one option", typeName)
}
if len(input.Options) > 10 {
return fmt.Errorf("Checkbox group inputs must have at most 10 options")
}
} else {
if len(input.Options) == 0 {
return fmt.Errorf("%s inputs must have at least one option", typeName)
}
}

return validateUniqueOptionValues(input.Options)
}

func saveInputs(ctx context.Context, formId int, data updateInputsBody, existingInputs []database.FormInput) error {
// We can now update in the database
tx, err := dbclient.Client.BeginTx(ctx)
Expand All @@ -294,8 +318,8 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing
minLength := input.MinLength
maxLength := input.MaxLength

// Handle select types (3, 5-8)
if input.Type == 3 || (input.Type >= 5 && input.Type <= 8) {
// Handle select types (3, 5-8, 21, 22)
if input.Type == 3 || (input.Type >= 5 && input.Type <= 8) || input.Type == 21 || input.Type == 22 {
// Enforce min_length constraints (0-25)
if minLength < 0 {
minLength = 0
Expand All @@ -304,8 +328,8 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing
}

// Handle max_length based on type
if input.Type == 3 {
// String Select: use options length as max, can be lower but not higher
if input.Type == 3 || input.Type == 21 || input.Type == 22 {
// String Select, RadioGroup, CheckboxGroup: use options length as max, can be lower but not higher
optionsLength := uint16(len(input.Options))
if optionsLength > 0 {
if maxLength == 0 || maxLength > optionsLength {
Expand Down Expand Up @@ -354,7 +378,7 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing
return err
}

if wrapped.Type == 3 { // String Select
if wrapped.Type == 3 || wrapped.Type == 21 || wrapped.Type == 22 { // String Select, RadioGroup, CheckboxGroup
// Delete existing options
options, err := dbclient.Client.FormInputOption.GetOptions(ctx, wrapped.Id)
if err != nil {
Expand Down Expand Up @@ -394,8 +418,8 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing
minLength := input.MinLength
maxLength := input.MaxLength

// Handle select types (3, 5-8)
if input.Type == 3 || (input.Type >= 5 && input.Type <= 8) {
// Handle select types (3, 5-8, 21, 22)
if input.Type == 3 || (input.Type >= 5 && input.Type <= 8) || input.Type == 21 || input.Type == 22 {
// Enforce min_length constraints (0-25)
if minLength < 0 {
minLength = 0
Expand All @@ -404,8 +428,8 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing
}

// Handle max_length based on type
if input.Type == 3 {
// String Select: use options length as max, can be lower but not higher
if input.Type == 3 || input.Type == 21 || input.Type == 22 {
// String Select, RadioGroup, CheckboxGroup: use options length as max, can be lower but not higher
optionsLength := uint16(len(input.Options))
if optionsLength > 0 {
if maxLength == 0 || maxLength > optionsLength {
Expand Down Expand Up @@ -454,7 +478,7 @@ func saveInputs(ctx context.Context, formId int, data updateInputsBody, existing
return err
}

if input.Type == 3 { // String Select
if input.Type == 3 || input.Type == 21 || input.Type == 22 { // String Select, RadioGroup, CheckboxGroup
for i, opt := range input.Options {
option := database.FormInputOption{
FormInputId: formInputId,
Expand Down
76 changes: 50 additions & 26 deletions frontend/src/components/manage/FormInputRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
export let data = {};
export let hasValidationErrors = false;

// Initialize options if not present
$: if (data.type === 3 && !data.options) {
// Initialize options if not present (for types that need options: 3, 21, 22)
$: if ((data.type === 3 || data.type === 21 || data.type === 22) && !data.options) {
data.options = [];
}

Expand Down Expand Up @@ -59,9 +59,9 @@
data.max_length = undefined;
}

// Check for duplicate option values
// Check for duplicate option values (for types with options: 3, 21, 22)
$: duplicateValues = (() => {
if (!data.options || data.type !== 3) return [];
if (!data.options || (data.type !== 3 && data.type !== 21 && data.type !== 22)) return [];
const valueMap = new Map();
const duplicates = [];

Expand All @@ -83,8 +83,11 @@
// Flag for duplicate values
$: hasDuplicateValues = duplicateValues.length > 0;

// Check for no options in string select
$: hasNoOptions = data.type === 3 && (!data.options || data.options.length === 0);
// Check for invalid option count in types that require options (3, 21, 22)
// Radio Group (21) requires 2-10 options, Checkbox Group (22) requires 1-10, String Select (3) requires 1-25
$: minOptionsRequired = data.type === 21 ? 2 : 1;
$: maxOptionsAllowed = (data.type === 21 || data.type === 22) ? 10 : 25;
$: hasNoOptions = (data.type === 3 || data.type === 21 || data.type === 22) && (!data.options || data.options.length < minOptionsRequired || data.options.length > maxOptionsAllowed);

// Validate label (required, max 45 chars)
$: hasInvalidLabel = !data.label || data.label.trim().length === 0 || data.label.length > 45;
Expand Down Expand Up @@ -124,7 +127,7 @@
if (!data.options) {
data.options = [];
}
if (data.options.length < 25) {
if (data.options.length < maxOptionsAllowed) {
data.options = [
...data.options,
{
Expand Down Expand Up @@ -200,10 +203,15 @@
const oldType = data.type;
data.type = newType;

// Types that use options: 3, 21, 22
const optionTypes = [3, 21, 22];
const oldUsesOptions = optionTypes.includes(oldType);
const newUsesOptions = optionTypes.includes(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) {
// Clear options when switching away from option-based types
if (oldUsesOptions && !newUsesOptions) {
data.options = undefined;
data.allow_multiple = undefined;
}
Expand All @@ -221,11 +229,8 @@
data.max_length = 255; // Default max for short style
}
// Clear min/max for types that don't use them
if (
newType !== 3 &&
newType !== 4 &&
(newType < 5 || newType > 8)
) {
const typesWithMinMax = [3, 4, 5, 6, 7, 8, 21, 22];
if (!typesWithMinMax.includes(newType)) {
data.min_length = undefined;
data.max_length = undefined;
}
Expand All @@ -238,6 +243,8 @@
<option value={6}>Role Select</option>
<option value={7}>Mentionable Select</option>
<option value={8}>Channel Select</option>
<option value={21}>Radio Group</option>
<option value={22}>Checkbox Group</option>
</Dropdown>
</div>
{#if withDeleteButton}
Expand Down Expand Up @@ -283,14 +290,22 @@
</div>
{/if}

<!-- String Select Options (type 3 only) -->
{#if data.type == 3}
<!-- Options for String Select (3), Radio Group (21), Checkbox Group (22) -->
{#if data.type == 3 || data.type == 21 || data.type == 22}
<div class="row settings-row">
<div class="col-1">
<div class="dropdown-items-section">
<div class="dropdown-header">
<label class="form-label">String Select Options</label>
{#if !data.options || data.options.length < 25}
<label class="form-label">
{#if data.type == 3}
String Select Options
{:else if data.type == 21}
Radio Group Options
{:else if data.type == 22}
Checkbox Group Options
{/if}
</label>
{#if !data.options || data.options.length < maxOptionsAllowed}
<Button
icon="fas fa-plus"
on:click={addDropdownItem}
Expand Down Expand Up @@ -368,7 +383,11 @@
<div class="validation-error">
<i class="fas fa-exclamation-triangle"></i>
<span>
No dropdown options added yet. Click "Add Option" to create up to 25 options.
{#if data.options && data.options.length > maxOptionsAllowed}
Too many options. Maximum is {maxOptionsAllowed} (currently {data.options.length}).
{:else}
At least {minOptionsRequired} option{minOptionsRequired > 1 ? "s are" : " is"} required. Click "Add Option" to create up to {maxOptionsAllowed} options.
{/if}
</span>
</div>
{/if}
Expand Down Expand Up @@ -627,10 +646,15 @@
const oldType = data.type;
data.type = newType;

// Types that use options: 3, 21, 22
const optionTypes = [3, 21, 22];
const oldUsesOptions = optionTypes.includes(oldType);
const newUsesOptions = optionTypes.includes(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) {
// Clear options when switching away from option-based types
if (oldUsesOptions && !newUsesOptions) {
data.options = undefined;
data.allow_multiple = undefined;
}
Expand All @@ -648,11 +672,8 @@
data.max_length = 255; // Default max for short style
}
// Clear min/max for types that don't use them
if (
newType !== 3 &&
newType !== 4 &&
(newType < 5 || newType > 8)
) {
const typesWithMinMax = [3, 4, 5, 6, 7, 8, 21, 22];
if (!typesWithMinMax.includes(newType)) {
data.min_length = undefined;
data.max_length = undefined;
}
Expand All @@ -665,6 +686,8 @@
<option value={6}>Role Select</option>
<option value={7}>Channel Select</option>
<option value={8}>Mentionable Select</option>
<option value={21}>Radio Group</option>
<option value={22}>Checkbox Group</option>
</Dropdown>
</div>
</div>
Expand Down Expand Up @@ -805,6 +828,7 @@
font-size: 14px;
color: var(--text-secondary, #666);
flex-wrap: wrap;
padding-top: 15px;
}

.config-text {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704
github.com/TicketsBot-cloud/common v0.0.0-20260210203202-54154661338e
github.com/TicketsBot-cloud/database v0.0.0-20260214180435-acb54713814d
github.com/TicketsBot-cloud/gdl v0.0.0-20251129162044-695f8e5079eb
github.com/TicketsBot-cloud/gdl v0.0.0-20260213180045-11af01c262ca
github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc
github.com/TicketsBot-cloud/worker v0.0.0-20251212162840-a9cc9bbf5692
github.com/apex/log v1.1.2
Expand Down
10 changes: 2 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,12 @@ github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c
github.com/TicketsBot-cloud/analytics-client v0.0.0-20250604180646-6606dfc8fc8c/go.mod h1:zecIz09jVDSHyhV6NYgTko0NEN0QJGiZbzcxHRjQLzc=
github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704 h1:liLfvCrzoJ89DXFHzsd1iK3cyP8s4i0CnZPRFEj53zg=
github.com/TicketsBot-cloud/archiverclient v0.0.0-20251015181023-f0b66a074704/go.mod h1:Mux1bEPpOHwRw1wo6Fa6qJLJH9Erk9qv1yAIfLi1Wmw=
github.com/TicketsBot-cloud/common v0.0.0-20260210200939-e29bc465e46b h1:sHP4QMStL6Su9fz+QtK/XRDGjWNZzG6W6ZK94RMC5A8=
github.com/TicketsBot-cloud/common v0.0.0-20260210200939-e29bc465e46b/go.mod h1:tGrTHFz09OM3eDWF+62hIi9ELpT4igCFi868FKSvKBg=
github.com/TicketsBot-cloud/common v0.0.0-20260210203202-54154661338e h1:nFKV7yEm8MWbCP7dtsJ88+agcxDUD0YKIotVHMVvytw=
github.com/TicketsBot-cloud/common v0.0.0-20260210203202-54154661338e/go.mod h1:tGrTHFz09OM3eDWF+62hIi9ELpT4igCFi868FKSvKBg=
github.com/TicketsBot-cloud/database v0.0.0-20260210192531-2f4d3be7a6a1 h1:0W8p9eaZJ5a2UQ1s2FAesTFrGVrqFiy+29fdFgWeUAQ=
github.com/TicketsBot-cloud/database v0.0.0-20260210192531-2f4d3be7a6a1/go.mod h1:HQXAgmNSm7/FmBYwcsa6qpZqMrDhbLoEl+AyqFQ+RwY=
github.com/TicketsBot-cloud/database v0.0.0-20260214180435-acb54713814d h1:wMzN5PDtgbTSWOoy/JN8fNGn4suoSX2vtvrM2egBvNU=
github.com/TicketsBot-cloud/database v0.0.0-20260214180435-acb54713814d/go.mod h1:HQXAgmNSm7/FmBYwcsa6qpZqMrDhbLoEl+AyqFQ+RwY=
github.com/TicketsBot-cloud/gdl v0.0.0-20251129162044-695f8e5079eb h1:Efk1PmGyFH3/MmY3r24xCKNP1r5fMxedYTvnYSH3gc8=
github.com/TicketsBot-cloud/gdl v0.0.0-20251129162044-695f8e5079eb/go.mod h1:CdwBR2egPtxUXjD2CgC9ZwfuB8dz9HPePM8nuG6dt7Y=
github.com/TicketsBot-cloud/gdl v0.0.0-20260213180045-11af01c262ca h1:/HRqcgOPfv6d9NzE6CqHXN4U1QgElyJ9DcxNNT8kV6g=
github.com/TicketsBot-cloud/gdl v0.0.0-20260213180045-11af01c262ca/go.mod h1:CdwBR2egPtxUXjD2CgC9ZwfuB8dz9HPePM8nuG6dt7Y=
github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc h1:qTLNpCvIqM7UwZ6MdWQ9EztcDsIJfHh+VJdG+ULLEaA=
github.com/TicketsBot-cloud/logarchiver v0.0.0-20251018211319-7a7df5cacbdc/go.mod h1:pZqkzPNNTqnwKZvCT8kCaTHxrG7HJbxZV83S0p7mmzM=
github.com/TicketsBot-cloud/worker v0.0.0-20251212162840-a9cc9bbf5692 h1:80COkRlCghOngYJYHE6lyxQteldz30LpEfjxoPrFlxM=
Expand Down Expand Up @@ -276,7 +272,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
Expand All @@ -297,7 +292,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
Expand Down