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
7 changes: 7 additions & 0 deletions app/http/endpoints/api/forms/createform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package forms

import (
"net/http"
"strings"

"github.com/TicketsBot-cloud/dashboard/app"
dbclient "github.com/TicketsBot-cloud/dashboard/database"
Expand All @@ -23,6 +24,12 @@ func CreateForm(c *gin.Context) {
return
}

// Validate title is not empty or whitespace-only
if len(strings.TrimSpace(data.Title)) == 0 {
c.JSON(400, utils.ErrorStr("Title is required"))
return
}

if len(data.Title) > 45 {
c.JSON(400, utils.ErrorStr("Title is too long"))
return
Expand Down
7 changes: 7 additions & 0 deletions app/http/endpoints/api/forms/updateform.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package forms
import (
"net/http"
"strconv"
"strings"

"github.com/TicketsBot-cloud/dashboard/app"
dbclient "github.com/TicketsBot-cloud/dashboard/database"
Expand All @@ -19,6 +20,12 @@ func UpdateForm(c *gin.Context) {
return
}

// Validate title is not empty or whitespace-only
if len(strings.TrimSpace(data.Title)) == 0 {
c.JSON(400, utils.ErrorStr("Title is required"))
return
}

if len(data.Title) > 45 {
c.JSON(400, utils.ErrorStr("Title is too long"))
return
Expand Down
61 changes: 61 additions & 0 deletions app/http/endpoints/api/forms/updateinputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"sort"
"strconv"
"strings"

"github.com/TicketsBot-cloud/dashboard/app"
dbclient "github.com/TicketsBot-cloud/dashboard/database"
Expand Down Expand Up @@ -158,6 +159,33 @@ func UpdateInputs(c *gin.Context) {
return
}

// Validate string select inputs have at least one option and unique option values
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(err.Error()))
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(err.Error()))
return
}
}
}

if err := saveInputs(c, formId, data, existingInputs); err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err))
return
Expand Down Expand Up @@ -197,6 +225,39 @@ func arePositionsCorrect(body updateInputsBody) bool {
return true
}

func validateUniqueOptionValues(options []inputOption) error {
if len(options) == 0 {
return nil
}

valueSet := make(map[string]bool)
duplicates := make(map[string]bool)

for _, opt := range options {
if opt.Value == "" {
continue
}
if valueSet[opt.Value] {
duplicates[opt.Value] = true
} else {
valueSet[opt.Value] = true
}
}

if len(duplicates) > 0 {
duplicateList := make([]string, 0, len(duplicates))
for value := range duplicates {
duplicateList = append(duplicateList, value)
}

sort.Strings(duplicateList)

return fmt.Errorf("Duplicate option values detected: %s. Each option must have a unique value", strings.Join(duplicateList, ", "))
}

return nil
}

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 Down
2 changes: 1 addition & 1 deletion frontend/src/components/form/DoubleRangeSlider.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
height: 5px;
border-radius: 5px;
background-color: #995df3;
width: 100%;
width: calc(100% - 10px);
margin-bottom: 12px;
}

Expand Down
169 changes: 141 additions & 28 deletions frontend/src/components/manage/FormInputRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
export let formId;

export let data = {};
export let hasValidationErrors = false;

// Initialize options if not present
$: if (data.type === 3 && !data.options) {
Expand Down Expand Up @@ -58,6 +59,42 @@
data.max_length = undefined;
}

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

data.options.forEach((opt, index) => {
if (opt.value && opt.value.trim()) {
if (valueMap.has(opt.value)) {
if (!duplicates.includes(opt.value)) {
duplicates.push(opt.value);
}
} else {
valueMap.set(opt.value, index);
}
}
});

return duplicates;
})();

// 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);

// Validate label (required, max 45 chars)
$: hasInvalidLabel = !data.label || data.label.trim().length === 0 || data.label.length > 45;

// Validate description (max 100 chars)
$: hasInvalidDescription = data.description && data.description.length > 100;

// Overall validation error flag
$: hasValidationErrors = hasDuplicateValues || hasNoOptions || hasInvalidLabel || hasInvalidDescription;

$: windowWidth = 0;

function forwardCreate() {
Expand Down Expand Up @@ -126,9 +163,9 @@

<form on:submit|preventDefault={forwardCreate} class="input-form">
<div class="row">
<div class="sub-row" style="flex: 1">
<div class="sub-row" style="flex: 1; margin-right: 10px;">
<Input
col3={true}
col1={true}
label="Label"
bind:value={data.label}
placeholder="Name of the field"
Expand Down Expand Up @@ -215,16 +252,36 @@
{/if}
</div>
</div>
{#if hasInvalidLabel}
<div class="validation-error" style="margin-top: 8px; margin-bottom: 8px;">
<i class="fas fa-exclamation-triangle"></i>
<span>
{#if !data.label || data.label.trim().length === 0}
Label is required
{:else}
Label must be 45 characters or less (currently {data.label.length})
{/if}
</span>
</div>
{/if}
<div class="row">
<div class="sub-row" style="flex: 1">
<Input
col3={true}
label="Description"
col1={true}
label="Description (Optional)"
bind:value={data.description}
placeholder="Description for the field"
/>
</div>
</div>
{#if hasInvalidDescription}
<div class="validation-error" style="margin-top: 8px; margin-bottom: 8px;">
<i class="fas fa-exclamation-triangle"></i>
<span>
Description must be 100 characters or less (currently {data.description.length})
</span>
</div>
{/if}

<!-- String Select Options (type 3 only) -->
{#if data.type == 3}
Expand Down Expand Up @@ -307,6 +364,23 @@
{/if}
</div>
</div>
{#if hasNoOptions}
<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.
</span>
</div>
{/if}
{#if hasDuplicateValues}
<div class="validation-error">
<i class="fas fa-exclamation-triangle"></i>
<span>
Duplicate option values detected: {duplicateValues.join(", ")}.
Each option must have a unique value.
</span>
</div>
{/if}
{#if data.options && data.options.length > 0}
<div class="dropdown-items-list">
{#each data.options as item, i}
Expand Down Expand Up @@ -339,24 +413,29 @@
e.target.value,
)}
/>
<Input
col2={true}
label="Value"
placeholder="Internal value"
value={item.value}
on:input={(e) =>
updateDropdownItem(
i,
"value",
e.target.value,
)}
/>
<div class="value-input-wrapper" class:has-duplicate={item.value && duplicateValues.includes(item.value)}>
<Input
col1={true}
label="Value"
placeholder="Internal value"
value={item.value}
on:input={(e) =>
updateDropdownItem(
i,
"value",
e.target.value,
)}
/>
{#if item.value && duplicateValues.includes(item.value)}
<span class="duplicate-indicator">Duplicate</span>
{/if}
</div>
</div>
<div class="dropdown-field-row">
<Input
col1={true}
label="Description"
placeholder="Optional description for this option"
label="Description (Optional)"
placeholder="Description for this option"
value={item.description}
on:input={(e) =>
updateDropdownItem(
Expand All @@ -370,13 +449,6 @@
</div>
{/each}
</div>
{:else}
<div class="empty-state">
<p>
No dropdown options added yet. Click "Add
Option" to create up to 25 options.
</p>
</div>
{/if}
</div>
</div>
Expand Down Expand Up @@ -495,15 +567,15 @@
<div class="row settings-row">
<Textarea
col2={true}
label="Placeholder"
label="Placeholder (Optional)"
bind:value={data.placeholder}
minHeight="120px"
placeholder="Placeholder text for the field, just like this text"
/>
<div class="col-2 properties-group">
<div class="row">
<Dropdown
col2={true}
col1={true}
label="Style"
value={data.style || 1}
on:change={updateStyle}
Expand Down Expand Up @@ -624,7 +696,7 @@
</div>
{/if}
<div class="row">
<div class="col-2-force">
<div class="col-1-force">
{#if withDeleteButton}
<form
on:submit|preventDefault={forwardDelete}
Expand Down Expand Up @@ -846,6 +918,47 @@
font-size: 14px;
}

.validation-error {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 15px;
background: rgba(220, 53, 69, 0.1);
border: 1px solid rgba(220, 53, 69, 0.3);
border-radius: 6px;
color: #dc3545;
font-size: 14px;
margin-bottom: 15px;
}

.validation-error i {
font-size: 16px;
}

.value-input-wrapper {
position: relative;
flex: 1;
}

.value-input-wrapper.has-duplicate :global(input) {
border-color: #dc3545 !important;
background-color: rgba(220, 53, 69, 0.05) !important;
}

.duplicate-indicator {
position: absolute;
top: -1px;
right: 0;
font-size: 11px;
color: #dc3545;
font-weight: 600;
text-transform: uppercase;
background: rgba(220, 53, 69, 0.1);
padding: 2px 6px;
border-radius: 3px;
pointer-events: none;
}

@media only screen and (max-width: 950px) {
.settings-row {
flex-direction: column-reverse !important;
Expand Down
Loading