Improve UX
This commit is contained in:
parent
6cd41aa99a
commit
e2e8270f86
@ -9,13 +9,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inputCreateBody struct {
|
|
||||||
Style component.TextStyleTypes `json:"style"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Placeholder *string `json:"placeholder"`
|
|
||||||
Optional bool `json:"optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateInput(ctx *gin.Context) {
|
func CreateInput(ctx *gin.Context) {
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
@ -70,7 +63,7 @@ func CreateInput(ctx *gin.Context) {
|
|||||||
// 2^30 chance of collision
|
// 2^30 chance of collision
|
||||||
customId := utils.RandString(30)
|
customId := utils.RandString(30)
|
||||||
|
|
||||||
formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, !data.Optional)
|
formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, data.Required)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
@ -83,7 +76,7 @@ func CreateInput(ctx *gin.Context) {
|
|||||||
Style: uint8(data.Style),
|
Style: uint8(data.Style),
|
||||||
Label: data.Label,
|
Label: data.Label,
|
||||||
Placeholder: data.Placeholder,
|
Placeholder: data.Placeholder,
|
||||||
Required: !data.Optional,
|
Required: data.Required,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ func UpdateForm(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data.Title) > 255 {
|
if len(data.Title) > 45 {
|
||||||
ctx.JSON(400, utils.ErrorStr("Title is too long"))
|
ctx.JSON(400, utils.ErrorStr("Title is too long"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ func UpdateInput(ctx *gin.Context) {
|
|||||||
Style: uint8(data.Style),
|
Style: uint8(data.Style),
|
||||||
Label: data.Label,
|
Label: data.Label,
|
||||||
Placeholder: data.Placeholder,
|
Placeholder: data.Placeholder,
|
||||||
Required: !data.Optional,
|
Required: data.Required,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbclient.Client.FormInput.Update(newInput); err != nil {
|
if err := dbclient.Client.FormInput.Update(newInput); err != nil {
|
||||||
|
244
app/http/endpoints/api/forms/updateinputs.go
Normal file
244
app/http/endpoints/api/forms/updateinputs.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package forms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/rxdn/gdl/objects/interaction/component"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
updateInputsBody struct {
|
||||||
|
Create []inputCreateBody `json:"create" validate:"omitempty,dive"`
|
||||||
|
Update []inputUpdateBody `json:"update" validate:"omitempty,dive"`
|
||||||
|
Delete []int `json:"delete" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
inputCreateBody struct {
|
||||||
|
Label string `json:"label" validate:"required,min=1,max=45"`
|
||||||
|
Placeholder *string `json:"placeholder,omitempty" validate:"omitempty,min=1,max=100"`
|
||||||
|
Position int `json:"position" validate:"required,min=1,max=5"`
|
||||||
|
Style component.TextStyleTypes `json:"style" validate:"required,min=1,max=2"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
inputUpdateBody struct {
|
||||||
|
Id int `json:"id" validate:"required"`
|
||||||
|
inputCreateBody `validate:"required,dive"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
func UpdateInputs(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
|
||||||
|
}
|
||||||
|
|
||||||
|
var data updateInputsBody
|
||||||
|
if err := ctx.BindJSON(&data); err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate.Struct(data); err != nil {
|
||||||
|
validationErrors, ok := err.(validator.ValidationErrors)
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted := "Your input contained the following errors:"
|
||||||
|
for _, validationError := range validationErrors {
|
||||||
|
formatted += fmt.Sprintf("\n%s", validationError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted = strings.TrimSuffix(formatted, "\n")
|
||||||
|
ctx.JSON(400, utils.ErrorStr(formatted))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldCount := len(data.Create) + len(data.Update)
|
||||||
|
if fieldCount <= 0 || fieldCount > 5 {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Forms must have between 1 and 5 inputs"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify form exists and is from the right 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
|
||||||
|
}
|
||||||
|
|
||||||
|
existingInputs, err := dbclient.Client.FormInput.GetInputs(formId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the UPDATE inputs exist
|
||||||
|
for _, input := range data.Update {
|
||||||
|
if !utils.ExistsMap(existingInputs, input.Id, idMapper) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Input (to be updated) not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the DELETE inputs exist
|
||||||
|
for _, id := range data.Delete {
|
||||||
|
if !utils.ExistsMap(existingInputs, id, idMapper) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Input (to be deleted) not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no overlap between DELETE and UPDATE
|
||||||
|
for _, id := range data.Delete {
|
||||||
|
if utils.ExistsMap(data.Update, id, idMapperBody) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Delete and update overlap"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that we are updating ALL inputs, excluding the ones to be deleted
|
||||||
|
var remainingExisting []int
|
||||||
|
for _, input := range existingInputs {
|
||||||
|
if !utils.Exists(data.Delete, input.Id) {
|
||||||
|
remainingExisting = append(remainingExisting, input.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now verify that the contents match exactly
|
||||||
|
if len(remainingExisting) != len(data.Update) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("All inputs must be included in the update array"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range data.Update {
|
||||||
|
if !utils.Exists(remainingExisting, input.Id) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("All inputs must be included in the update array"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the positions are unique, and are in ascending order
|
||||||
|
if !arePositionsCorrect(data) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Positions must be unique and in ascending order"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := saveInputs(formId, data, existingInputs); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(204)
|
||||||
|
}
|
||||||
|
|
||||||
|
func idMapper(input database.FormInput) int {
|
||||||
|
return input.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func idMapperBody(input inputUpdateBody) int {
|
||||||
|
return input.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func arePositionsCorrect(body updateInputsBody) bool {
|
||||||
|
var positions []int
|
||||||
|
for _, input := range body.Create {
|
||||||
|
positions = append(positions, input.Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range body.Update {
|
||||||
|
positions = append(positions, input.Position)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(positions, func(i, j int) bool {
|
||||||
|
return positions[i] < positions[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, position := range positions {
|
||||||
|
if i+1 != position {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveInputs(formId int, data updateInputsBody, existingInputs []database.FormInput) error {
|
||||||
|
// We can now update in the database
|
||||||
|
tx, err := dbclient.Client.BeginTx()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback(context.Background())
|
||||||
|
|
||||||
|
for _, id := range data.Delete {
|
||||||
|
if err := dbclient.Client.FormInput.DeleteTx(tx, id, formId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range data.Update {
|
||||||
|
existing := utils.FindMap(existingInputs, input.Id, idMapper)
|
||||||
|
if existing == nil {
|
||||||
|
return fmt.Errorf("input %d does not exist", input.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped := database.FormInput{
|
||||||
|
Id: input.Id,
|
||||||
|
FormId: formId,
|
||||||
|
Position: input.Position,
|
||||||
|
CustomId: existing.CustomId,
|
||||||
|
Style: uint8(input.Style),
|
||||||
|
Label: input.Label,
|
||||||
|
Placeholder: input.Placeholder,
|
||||||
|
Required: input.Required,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.FormInput.UpdateTx(tx, wrapped); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range data.Create {
|
||||||
|
if _, err := dbclient.Client.FormInput.CreateTx(
|
||||||
|
tx,
|
||||||
|
formId,
|
||||||
|
utils.RandString(30),
|
||||||
|
input.Position,
|
||||||
|
uint8(input.Style),
|
||||||
|
input.Label,
|
||||||
|
input.Placeholder,
|
||||||
|
input.Required,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit(context.Background())
|
||||||
|
}
|
@ -123,6 +123,7 @@ func StartServer() {
|
|||||||
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.POST("/forms/:form_id", api_forms.CreateInput)
|
||||||
|
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", api_forms.UpdateInput)
|
||||||
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id/:direction", api_forms.SwapInput)
|
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id/:direction", api_forms.SwapInput)
|
||||||
guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput)
|
guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput)
|
||||||
|
@ -17,11 +17,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
{#if withSaveButton}
|
|
||||||
<form on:submit|preventDefault={forwardSave} class="button-form">
|
|
||||||
<Button icon="fas fa-save">Save</Button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
{#if withDeleteButton}
|
{#if withDeleteButton}
|
||||||
<form on:submit|preventDefault={forwardDelete} class="button-form">
|
<form on:submit|preventDefault={forwardDelete} class="button-form">
|
||||||
<Button icon="fas fa-trash" danger={true}>Delete</Button>
|
<Button icon="fas fa-trash" danger={true}>Delete</Button>
|
||||||
@ -41,7 +36,7 @@
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Checkbox label="Optional" bind:value={data.optional}/>
|
<Checkbox label="Required" bind:value={data.required}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,13 +62,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2-force">
|
|
||||||
{#if withSaveButton}
|
|
||||||
<form on:submit|preventDefault={forwardSave} class="button-form">
|
|
||||||
<Button icon="fas fa-save">Save</Button>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="col-2-force">
|
<div class="col-2-force">
|
||||||
{#if withDeleteButton}
|
{#if withDeleteButton}
|
||||||
<form on:submit|preventDefault={forwardDelete} class="button-form">
|
<form on:submit|preventDefault={forwardDelete} class="button-form">
|
||||||
@ -85,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if withCreateButton}
|
{#if withCreateButton && false}
|
||||||
<div class="row" style="justify-content: center; margin-top: 10px">
|
<div class="row" style="justify-content: center; margin-top: 10px">
|
||||||
<Button type="submit" icon="fas fa-plus" {disabled}>Add Input</Button>
|
<Button type="submit" icon="fas fa-plus" {disabled}>Add Input</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -105,7 +93,6 @@
|
|||||||
import Checkbox from "../form/Checkbox.svelte";
|
import Checkbox from "../form/Checkbox.svelte";
|
||||||
|
|
||||||
export let withCreateButton = false;
|
export let withCreateButton = false;
|
||||||
export let withSaveButton = false;
|
|
||||||
export let withDeleteButton = false;
|
export let withDeleteButton = false;
|
||||||
export let withDirectionButtons = false;
|
export let withDirectionButtons = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
@ -121,10 +108,6 @@
|
|||||||
dispatch('create', data);
|
dispatch('create', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function forwardSave() {
|
|
||||||
dispatch('save', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function forwardDelete() {
|
function forwardDelete() {
|
||||||
dispatch('delete', {});
|
dispatch('delete', {});
|
||||||
}
|
}
|
||||||
|
@ -40,3 +40,7 @@ export function colourToInt(colour) {
|
|||||||
export function intToColour(i) {
|
export function intToColour(i) {
|
||||||
return `#${i.toString(16)}`
|
return `#${i.toString(16)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function nullIfBlank(s) {
|
||||||
|
return s === '' ? null : s;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="parent">
|
<div class="parent">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Card footer={false}>
|
<Card footer footerRight>
|
||||||
<span slot="title">Forms</span>
|
<span slot="title">Forms</span>
|
||||||
<div slot="body" class="body-wrapper">
|
<div slot="body" class="body-wrapper">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@ -18,25 +18,31 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h2 class="section-title">Manage Forms</h2>
|
<h2 class="section-title">Manage Forms</h2>
|
||||||
|
|
||||||
<div class="col-1" style="flex-direction: row">
|
{#if editingTitle && activeFormId !== null}
|
||||||
<div class="col-4" style="margin-right: 12px">
|
<div class="row form-name-edit-wrapper">
|
||||||
|
<Input col4 label="Form Title" placeholder="Form Title" bind:value={renamedTitle}/>
|
||||||
|
<div class="form-name-save-wrapper">
|
||||||
|
<Button icon="fas fa-floppy-disk" fullWidth={windowWidth <= 950} on:click={updateTitle}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="row form-select-row">
|
||||||
<div class="multiselect-super">
|
<div class="multiselect-super">
|
||||||
<Dropdown col1={true} bind:value={activeFormId}>
|
<Dropdown col1 bind:value={activeFormId}>
|
||||||
<option value={null}>Select a form...</option>
|
<option value={null}>Select a form...</option>
|
||||||
{#each forms as form}
|
{#each forms as form}
|
||||||
<option value="{form.form_id}">{form.title}</option>
|
<option value="{form.form_id}">{form.title}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if activeFormId !== null}
|
{#if activeFormId !== null}
|
||||||
<div class="col-4">
|
<Button on:click={() => editingTitle = true}>Rename Form</Button>
|
||||||
<Button danger={true} type="button"
|
<Button danger type="button"
|
||||||
on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button>
|
on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="manage">
|
<div class="manage">
|
||||||
{#if activeFormId !== null}
|
{#if activeFormId !== null}
|
||||||
@ -45,20 +51,31 @@
|
|||||||
<FormInputRow data={input} formId={activeFormId}
|
<FormInputRow data={input} formId={activeFormId}
|
||||||
withSaveButton={true} withDeleteButton={true} withDirectionButtons={true}
|
withSaveButton={true} withDeleteButton={true} withDirectionButtons={true}
|
||||||
index={i} {formLength}
|
index={i} {formLength}
|
||||||
on:save={(e) => editInput(activeFormId, input.id, e.detail)}
|
on:delete={() => deleteInput(activeFormId, input)}
|
||||||
on:delete={() => deleteInput(activeFormId, input.id)}
|
on:move={(e) => changePosition(activeFormId, input, e.detail.direction)}/>
|
||||||
on:move={(e) => changePosition(activeFormId, input.id, e.detail.direction)} />
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if activeFormId !== null}
|
{#if activeFormId !== null}
|
||||||
<FormInputRow bind:data={inputCreationData} withCreateButton={true} disabled={formLength >= 5}
|
<div class="row" style="justify-content: center; align-items: center; gap: 10px; margin-top: 10px">
|
||||||
on:create={(e) => createInput(e.detail)}/>
|
<hr class="fill">
|
||||||
|
<div class="row add-input-container" class:add-input-disabled={formLength >= 5}>
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<a on:click={addInput}>Add Input</a>
|
||||||
|
</div>
|
||||||
|
<hr class="fill">
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<Button type="submit" icon="fas fa-floppy-disk" disabled={formLength === 0} on:click={saveInputs}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,7 +84,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Card from "../components/Card.svelte";
|
import Card from "../components/Card.svelte";
|
||||||
import {notifyError, notifySuccess, withLoadingScreen} from '../js/util'
|
import {notifyError, notifySuccess, nullIfBlank, withLoadingScreen} from '../js/util'
|
||||||
import Button from "../components/Button.svelte";
|
import Button from "../components/Button.svelte";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {API_URL} from "../js/constants";
|
import {API_URL} from "../js/constants";
|
||||||
@ -84,18 +101,40 @@
|
|||||||
|
|
||||||
let newTitle;
|
let newTitle;
|
||||||
let forms = [];
|
let forms = [];
|
||||||
|
let toDelete = {};
|
||||||
let activeFormId = null;
|
let activeFormId = null;
|
||||||
$: activeFormTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : 'Unknown';
|
$: activeFormTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : 'Unknown';
|
||||||
let inputCreationData = {};
|
|
||||||
|
|
||||||
$: formLength = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).inputs.length : 0;
|
$: formLength = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).inputs.length : 0;
|
||||||
|
|
||||||
|
let editingTitle = false;
|
||||||
|
let renamedTitle = "";
|
||||||
|
$: activeFormId, reflectTitle();
|
||||||
|
|
||||||
|
function reflectTitle() {
|
||||||
|
renamedTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : null;
|
||||||
|
}
|
||||||
|
|
||||||
$: windowWidth = 0;
|
$: windowWidth = 0;
|
||||||
|
|
||||||
function getForm(formId) {
|
function getForm(formId) {
|
||||||
return forms.find(form => form.form_id === formId);
|
return forms.find(form => form.form_id === formId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateTitle() {
|
||||||
|
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${activeFormId}`, {title: renamedTitle});
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError('Failed to update form title');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editingTitle = false;
|
||||||
|
getForm(activeFormId).title = renamedTitle;
|
||||||
|
forms = forms;
|
||||||
|
|
||||||
|
notifySuccess('Form title updated');
|
||||||
|
}
|
||||||
|
|
||||||
async function createForm() {
|
async function createForm() {
|
||||||
let data = {
|
let data = {
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
@ -116,6 +155,8 @@
|
|||||||
activeFormId = null; // Error thrown from {#each forms.find} if we don't temporarily set this to null?
|
activeFormId = null; // Error thrown from {#each forms.find} if we don't temporarily set this to null?
|
||||||
forms = [...forms, form];
|
forms = [...forms, form];
|
||||||
activeFormId = form.form_id;
|
activeFormId = form.form_id;
|
||||||
|
|
||||||
|
addInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteForm(id) {
|
async function deleteForm(id) {
|
||||||
@ -135,21 +176,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInput(data) {
|
function addInput() {
|
||||||
let mapped = {...data, style: parseInt(data.style)};
|
if (formLength >= 5) return;
|
||||||
|
|
||||||
const res = await axios.post(`${API_URL}/api/${guildId}/forms/${activeFormId}`, mapped);
|
const input = {
|
||||||
if (res.status !== 200) {
|
form_id: activeFormId,
|
||||||
notifyError(res.data.error);
|
position: formLength + 1,
|
||||||
return;
|
style: "1",
|
||||||
}
|
label: "",
|
||||||
|
placeholder: "",
|
||||||
|
required: true,
|
||||||
|
is_new: true,
|
||||||
|
};
|
||||||
|
|
||||||
let form = getForm(res.data.form_id);
|
const form = getForm(activeFormId);
|
||||||
form.inputs = [...form.inputs, res.data];
|
form.inputs = [...form.inputs, input];
|
||||||
forms = forms;
|
forms = forms;
|
||||||
inputCreationData = {"style": "1"};
|
|
||||||
|
|
||||||
notifySuccess('Form input created successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editInput(formId, inputId, data) {
|
async function editInput(formId, inputId, data) {
|
||||||
@ -168,42 +210,66 @@
|
|||||||
notifySuccess('Form input updated successfully');
|
notifySuccess('Form input updated successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteInput(formId, inputId) {
|
async function deleteInput(formId, input) {
|
||||||
const res = await axios.delete(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}`);
|
let form = getForm(formId);
|
||||||
if (res.status !== 200) {
|
|
||||||
notifyError(res.data.error);
|
let idx = form.inputs.findIndex((i) => i === input);
|
||||||
return;
|
form.inputs.splice(idx, 1);
|
||||||
|
for (let i = idx; i < form.inputs.length; i++) {
|
||||||
|
form.inputs[i].position--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: delete keyword?
|
|
||||||
let form = getForm(formId);
|
|
||||||
form.inputs = form.inputs.filter(input => input.id !== inputId);
|
|
||||||
forms = forms;
|
forms = forms;
|
||||||
|
|
||||||
notifySuccess('Form input deleted successfully');
|
if (!input.is_new) {
|
||||||
|
if (toDelete[formId] === undefined) {
|
||||||
|
toDelete[formId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changePosition(formId, inputId, direction) {
|
toDelete[formId] = [...toDelete[formId], input.id];
|
||||||
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}/${direction}`);
|
}
|
||||||
if (res.status !== 200) {
|
|
||||||
notifyError(res.data.error);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.data.success) {
|
function changePosition(formId, input, direction) {
|
||||||
//let form = getForm(formId);
|
const form = getForm(formId);
|
||||||
let form = forms.find(form => form.form_id === activeFormId);
|
let idx = form.inputs.findIndex((i) => i === input);
|
||||||
let idx = form.inputs.findIndex((input) => input.id === inputId);
|
|
||||||
|
|
||||||
let inputs = form.inputs;
|
let inputs = form.inputs;
|
||||||
if (direction === "up") {
|
if (direction === "up") {
|
||||||
[inputs[idx-1], inputs[idx]] = [inputs[idx], inputs[idx-1]]
|
[inputs[idx - 1].position, inputs[idx].position] = [inputs[idx].position, inputs[idx - 1].position];
|
||||||
|
[inputs[idx - 1], inputs[idx]] = [inputs[idx], inputs[idx - 1]];
|
||||||
} else if (direction === "down") {
|
} else if (direction === "down") {
|
||||||
[inputs[idx+1], inputs[idx]] = [form.inputs[idx], form.inputs[idx+1]]
|
[inputs[idx + 1].position, inputs[idx].position] = [form.inputs[idx].position, form.inputs[idx + 1].position];
|
||||||
|
[inputs[idx + 1], inputs[idx]] = [form.inputs[idx], form.inputs[idx + 1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
forms = forms;
|
forms = forms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveInputs() {
|
||||||
|
const form = getForm(activeFormId);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
"create": form.inputs.filter(i => i.is_new === true)
|
||||||
|
.map(i => ({...i, style: parseInt(i.style), placeholder: nullIfBlank(i.placeholder)})),
|
||||||
|
"update": form.inputs.filter(i => !i.is_new)
|
||||||
|
.map(i => ({...i, style: parseInt(i.style), placeholder: nullIfBlank(i.placeholder)})),
|
||||||
|
"delete": toDelete[activeFormId] || [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${activeFormId}/inputs`, data);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDelete = {};
|
||||||
|
|
||||||
|
const formId = activeFormId;
|
||||||
|
await loadForms();
|
||||||
|
activeFormId = formId;
|
||||||
|
|
||||||
|
notifySuccess('Form updated successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadForms() {
|
async function loadForms() {
|
||||||
@ -215,7 +281,6 @@
|
|||||||
|
|
||||||
forms = res.data || [];
|
forms = res.data || [];
|
||||||
forms.flatMap(f => f.inputs).forEach(i => {
|
forms.flatMap(f => f.inputs).forEach(i => {
|
||||||
i.optional = !i.required;
|
|
||||||
i.style = i.style.toString();
|
i.style = i.style.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -276,6 +341,14 @@
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr.fill {
|
||||||
|
border-top: 1px solid #777;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -284,6 +357,26 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-select-row {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-name-edit-wrapper {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-name-save-wrapper {
|
||||||
|
height: 48px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-super {
|
||||||
|
width: 31%;
|
||||||
|
}
|
||||||
|
|
||||||
.manage {
|
.manage {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -293,6 +386,22 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input-container > * {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input-disabled > *{
|
||||||
|
cursor: default !important;
|
||||||
|
color: #777 !important;
|
||||||
|
}
|
||||||
|
|
||||||
#creation-row {
|
#creation-row {
|
||||||
justify-content: flex-start !important;
|
justify-content: flex-start !important;
|
||||||
}
|
}
|
||||||
@ -314,5 +423,22 @@
|
|||||||
#create-button-wrapper {
|
#create-button-wrapper {
|
||||||
margin-left: unset;
|
margin-left: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-select-row {
|
||||||
|
max-height: unset;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-super {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-name-edit-wrapper {
|
||||||
|
gap: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-name-save-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
4
go.mod
4
go.mod
@ -83,3 +83,7 @@ require (
|
|||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
nhooyr.io/websocket v1.8.4 // indirect
|
nhooyr.io/websocket v1.8.4 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace (
|
||||||
|
github.com/TicketsBot/database => "../database"
|
||||||
|
)
|
||||||
|
2
go.sum
2
go.sum
@ -5,8 +5,6 @@ github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc h1:n15W8
|
|||||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
|
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
|
||||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqSbjJ4o7WKkdoPngmhjAGEXFwteEjpqs=
|
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqSbjJ4o7WKkdoPngmhjAGEXFwteEjpqs=
|
||||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
||||||
github.com/TicketsBot/database v0.0.0-20220712212403-61804b8beb18 h1:p3rr325yK5CqWQMBML1SzkC+mXx+SSaBq4PnyxeBYXA=
|
|
||||||
github.com/TicketsBot/database v0.0.0-20220712212403-61804b8beb18/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
|
||||||
github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c h1:eyAFQuKihRkfkSNg1xeIm9nHQZ1z2Qg46kS7LcLZNxk=
|
github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c h1:eyAFQuKihRkfkSNg1xeIm9nHQZ1z2Qg46kS7LcLZNxk=
|
||||||
github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
||||||
|
@ -16,6 +16,38 @@ func Slice[T any](v ...T) []T {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Exists[T comparable](v []T, el T) bool {
|
||||||
|
for _, e := range v {
|
||||||
|
if e == el {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExistsMap[T any, U comparable](v []T, el U, mapper func(T) U) bool {
|
||||||
|
for _, e := range v {
|
||||||
|
mapped := mapper(e)
|
||||||
|
if mapped == el {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindMap[T any, U comparable](v []T, el U, mapper func(T) U) *T {
|
||||||
|
for _, e := range v {
|
||||||
|
mapped := mapper(e)
|
||||||
|
if mapped == el {
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Must(err error) {
|
func Must(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user