Add length slider

This commit is contained in:
rxdn 2024-06-20 16:41:13 +01:00
parent a7b05af1f0
commit dc564d37f0
10 changed files with 25 additions and 369 deletions

View File

@ -1,118 +0,0 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/objects/interaction/component"
"strconv"
)
func CreateInput(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data inputCreateBody
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
// Validate body
if !data.Validate(ctx) {
return
}
// Parse form ID from URL
formId, err := strconv.Atoi(ctx.Param("form_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
// Get form and validate it belongs to the guild
form, ok, err := dbclient.Client.Forms.Get(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Form not found"))
return
}
if form.GuildId != guildId {
ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild"))
return
}
// Check there are not more than 25 inputs already
// TODO: This is vulnerable to a race condition
inputCount, err := getFormInputCount(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if inputCount >= 5 {
ctx.JSON(400, utils.ErrorStr("A form cannot have more than 5 inputs"))
return
}
// 2^30 chance of collision
customId, err := utils.RandString(30)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, data.Required)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, database.FormInput{
Id: formInputId,
FormId: formId,
CustomId: customId,
Style: uint8(data.Style),
Label: data.Label,
Placeholder: data.Placeholder,
Required: data.Required,
})
}
func (b *inputCreateBody) Validate(ctx *gin.Context) bool {
if b.Style != component.TextStyleShort && b.Style != component.TextStyleParagraph {
ctx.JSON(400, utils.ErrorStr("Invalid style"))
return false
}
if len(b.Label) == 0 || len(b.Label) > 45 {
ctx.JSON(400, utils.ErrorStr("The input label must be between 1 and 45 characters"))
return false
}
if b.Placeholder != nil && len(*b.Placeholder) == 0 {
b.Placeholder = nil
}
if b.Placeholder != nil && len(*b.Placeholder) > 100 {
ctx.JSON(400, utils.ErrorStr("The placeholder cannot be more than 100 characters"))
return false
}
return true
}
// TODO: Use select count()
func getFormInputCount(formId int) (int, error) {
inputs, err := dbclient.Client.FormInput.GetInputs(formId)
if err != nil {
return 0, err
}
return len(inputs), nil
}

View File

@ -1,63 +0,0 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func DeleteInput(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
formId, err := strconv.Atoi(ctx.Param("form_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
inputId, err := strconv.Atoi(ctx.Param("input_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
form, ok, err := dbclient.Client.Forms.Get(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Form not found"))
return
}
if form.GuildId != guildId {
ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild"))
return
}
input, ok, err := dbclient.Client.FormInput.Get(inputId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Input not found"))
return
}
if input.FormId != formId {
ctx.JSON(403, utils.ErrorStr("Input does not belong to this form"))
return
}
if err := dbclient.Client.FormInput.Delete(input.Id, input.FormId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -1,77 +0,0 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"strconv"
)
func SwapInput(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
formId, err := strconv.Atoi(ctx.Param("form_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
inputId, err := strconv.Atoi(ctx.Param("input_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
form, ok, err := dbclient.Client.Forms.Get(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Form not found"))
return
}
if form.GuildId != guildId {
ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild"))
return
}
input, ok, err := dbclient.Client.FormInput.Get(inputId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Input not found"))
return
}
if input.FormId != formId {
ctx.JSON(403, utils.ErrorStr("Input does not belong to this form"))
return
}
var direction database.InputSwapDirection
{
directionRaw := ctx.Param("direction")
if directionRaw == "up" {
direction = database.SwapDirectionUp
} else if directionRaw == "down" {
direction = database.SwapDirectionDown
} else {
ctx.JSON(400, utils.ErrorStr("Invalid swap direction"))
return
}
}
if err := dbclient.Client.FormInput.SwapDirection(inputId, formId, direction); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -1,84 +0,0 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"strconv"
)
func UpdateInput(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data inputCreateBody
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
if !data.Validate(ctx) {
return
}
formId, err := strconv.Atoi(ctx.Param("form_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
inputId, err := strconv.Atoi(ctx.Param("input_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
form, ok, err := dbclient.Client.Forms.Get(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Form not found"))
return
}
if form.GuildId != guildId {
ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild"))
return
}
input, ok, err := dbclient.Client.FormInput.Get(inputId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Input not found"))
return
}
if input.FormId != formId {
ctx.JSON(403, utils.ErrorStr("Input does not belong to this form"))
return
}
newInput := database.FormInput{
Id: inputId,
FormId: formId,
CustomId: input.CustomId,
Style: uint8(data.Style),
Label: data.Label,
Placeholder: data.Placeholder,
Required: data.Required,
}
if err := dbclient.Client.FormInput.Update(newInput); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, newInput)
}

View File

@ -26,6 +26,8 @@ type (
Position int `json:"position" validate:"required,min=1,max=5"` 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:"required,min=1,max=2"`
Required bool `json:"required"` 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"`
} }
inputUpdateBody struct { inputUpdateBody struct {
@ -212,6 +214,8 @@ func saveInputs(formId int, data updateInputsBody, existingInputs []database.For
Label: input.Label, Label: input.Label,
Placeholder: input.Placeholder, Placeholder: input.Placeholder,
Required: input.Required, Required: input.Required,
MinLength: &input.MinLength,
MaxLength: &input.MaxLength,
} }
if err := dbclient.Client.FormInput.UpdateTx(tx, wrapped); err != nil { if err := dbclient.Client.FormInput.UpdateTx(tx, wrapped); err != nil {
@ -234,6 +238,8 @@ func saveInputs(formId int, data updateInputsBody, existingInputs []database.For
input.Label, input.Label,
input.Placeholder, input.Placeholder,
input.Required, input.Required,
&input.MinLength,
&input.MaxLength,
); err != nil { ); err != nil {
return err return err
} }

View File

@ -132,11 +132,7 @@ func StartServer(sm *livechat.SocketManager) {
guildAuthApiAdmin.POST("/forms", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.CreateForm) guildAuthApiAdmin.POST("/forms", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.CreateForm)
guildAuthApiAdmin.PATCH("/forms/:form_id", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.UpdateForm) guildAuthApiAdmin.PATCH("/forms/:form_id", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.UpdateForm)
guildAuthApiAdmin.DELETE("/forms/:form_id", api_forms.DeleteForm) guildAuthApiAdmin.DELETE("/forms/:form_id", api_forms.DeleteForm)
guildAuthApiAdmin.POST("/forms/:form_id", api_forms.CreateInput)
guildAuthApiAdmin.PATCH("/forms/:form_id/inputs", api_forms.UpdateInputs) guildAuthApiAdmin.PATCH("/forms/:form_id/inputs", api_forms.UpdateInputs)
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id", api_forms.UpdateInput)
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id/:direction", api_forms.SwapInput)
guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput)
// Should be a GET, but easier to take a body for development purposes // Should be a GET, but easier to take a body for development purposes
guildAuthApiSupport.POST("/transcripts", guildAuthApiSupport.POST("/transcripts",

View File

@ -26,17 +26,18 @@
</div> </div>
</div> </div>
<div class="row settings-row"> <div class="row settings-row">
<Textarea col3_4={true} label="Placeholder" bind:value={data.placeholder} minHeight="120px" <Textarea col2={true} label="Placeholder" bind:value={data.placeholder} minHeight="120px"
placeholder="Placeholder text for the field, just like this text" /> placeholder="Placeholder text for the field, just like this text" />
<div class="col-4"> <div class="col-2 properties-group">
<div class="row"> <div class="row">
<Dropdown col1={true} label="Style" bind:value={data.style}> <Dropdown col2={true} label="Style" bind:value={data.style}>
<option value=1 selected>Short</option> <option value=1 selected>Short</option>
<option value=2>Paragraph</option> <option value=2>Multi-line</option>
</Dropdown> </Dropdown>
</div> </div>
<div class="row"> <div class="row" style="gap: 10px">
<Checkbox label="Required" bind:value={data.required}/> <Checkbox label="Required" bind:value={data.required}/>
<DoubleRangeSlider label="Answer Length Range" bind:start={data.min_length} bind:end={data.max_length} min={0} max={1024} />
</div> </div>
</div> </div>
</div> </div>
@ -91,6 +92,7 @@
import Button from "../Button.svelte"; import Button from "../Button.svelte";
import Textarea from "../form/Textarea.svelte"; import Textarea from "../form/Textarea.svelte";
import Checkbox from "../form/Checkbox.svelte"; import Checkbox from "../form/Checkbox.svelte";
import DoubleRangeSlider from "../form/DoubleRangeSlider.svelte";
export let withCreateButton = false; export let withCreateButton = false;
export let withDeleteButton = false; export let withDeleteButton = false;
@ -159,4 +161,10 @@
width: 100%; width: 100%;
} }
} }
@media only screen and (max-width: 576px) {
.properties-group > div:nth-child(2) {
flex-direction: column;
}
}
</style> </style>

View File

@ -62,7 +62,7 @@
<hr class="fill"> <hr class="fill">
<div class="row add-input-container" class:add-input-disabled={formLength >= 5}> <div class="row add-input-container" class:add-input-disabled={formLength >= 5}>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<a on:click={addInput}>Add Input</a> <a on:click={addInput}>New Field</a>
</div> </div>
<hr class="fill"> <hr class="fill">
</div> </div>
@ -187,6 +187,8 @@
label: "", label: "",
placeholder: "", placeholder: "",
required: true, required: true,
min_length: 0,
max_length: 1024,
is_new: true, is_new: true,
}; };
@ -194,22 +196,6 @@
forms = forms; forms = forms;
} }
async function editInput(formId, inputId, data) {
let mapped = {...data, style: parseInt(data.style)};
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}`, mapped);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
let form = getForm(formId);
form.inputs = form.inputs.filter(input => input.id !== inputId);
form.inputs = [...form.inputs, res.data];
notifySuccess('Form input updated successfully');
}
async function deleteInput(formId, input) { async function deleteInput(formId, input) {
let form = getForm(formId); let form = getForm(formId);

2
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/BurntSushi/toml v1.2.1 github.com/BurntSushi/toml v1.2.1
github.com/TicketsBot/archiverclient v0.0.0-20240613013458-accc062facc2 github.com/TicketsBot/archiverclient v0.0.0-20240613013458-accc062facc2
github.com/TicketsBot/common v0.0.0-20240613013221-1e27eb8bfe37 github.com/TicketsBot/common v0.0.0-20240613013221-1e27eb8bfe37
github.com/TicketsBot/database v0.0.0-20240614143550-e9b219d41743 github.com/TicketsBot/database v0.0.0-20240620154005-fdf7932c6a00
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
github.com/TicketsBot/worker v0.0.0-20240615173640-85185c239fd0 github.com/TicketsBot/worker v0.0.0-20240615173640-85185c239fd0
github.com/apex/log v1.1.2 github.com/apex/log v1.1.2

2
go.sum
View File

@ -49,6 +49,8 @@ github.com/TicketsBot/common v0.0.0-20240613013221-1e27eb8bfe37 h1:NC5fn+uAup0Jx
github.com/TicketsBot/common v0.0.0-20240613013221-1e27eb8bfe37/go.mod h1:UZ6Kzobh9akWyon7iGLPb4w/9gmKV+sLuR6PmthsS+U= github.com/TicketsBot/common v0.0.0-20240613013221-1e27eb8bfe37/go.mod h1:UZ6Kzobh9akWyon7iGLPb4w/9gmKV+sLuR6PmthsS+U=
github.com/TicketsBot/database v0.0.0-20240614143550-e9b219d41743 h1:wTGntdybAv9GcWYE3RJiAIo4I6rYrfG9uSpHaF0KUXY= github.com/TicketsBot/database v0.0.0-20240614143550-e9b219d41743 h1:wTGntdybAv9GcWYE3RJiAIo4I6rYrfG9uSpHaF0KUXY=
github.com/TicketsBot/database v0.0.0-20240614143550-e9b219d41743/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI= github.com/TicketsBot/database v0.0.0-20240614143550-e9b219d41743/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/database v0.0.0-20240620154005-fdf7932c6a00 h1:2qU/ixn0SEaDxiTQ7S/a4bTGQOlV920Sab/MlqocGzs=
github.com/TicketsBot/database v0.0.0-20240620154005-fdf7932c6a00/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=