Improve UX
This commit is contained in:
parent
6cd41aa99a
commit
e2e8270f86
@ -9,13 +9,6 @@ import (
|
||||
"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) {
|
||||
guildId := ctx.Keys["guildid"].(uint64)
|
||||
|
||||
@ -70,7 +63,7 @@ func CreateInput(ctx *gin.Context) {
|
||||
// 2^30 chance of collision
|
||||
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 {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
@ -83,7 +76,7 @@ func CreateInput(ctx *gin.Context) {
|
||||
Style: uint8(data.Style),
|
||||
Label: data.Label,
|
||||
Placeholder: data.Placeholder,
|
||||
Required: !data.Optional,
|
||||
Required: data.Required,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ func UpdateForm(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Title) > 255 {
|
||||
if len(data.Title) > 45 {
|
||||
ctx.JSON(400, utils.ErrorStr("Title is too long"))
|
||||
return
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func UpdateInput(ctx *gin.Context) {
|
||||
Style: uint8(data.Style),
|
||||
Label: data.Label,
|
||||
Placeholder: data.Placeholder,
|
||||
Required: !data.Optional,
|
||||
Required: data.Required,
|
||||
}
|
||||
|
||||
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.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/: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)
|
||||
|
@ -17,11 +17,6 @@
|
||||
</Button>
|
||||
</form>
|
||||
{/if}
|
||||
{#if withSaveButton}
|
||||
<form on:submit|preventDefault={forwardSave} class="button-form">
|
||||
<Button icon="fas fa-save">Save</Button>
|
||||
</form>
|
||||
{/if}
|
||||
{#if withDeleteButton}
|
||||
<form on:submit|preventDefault={forwardDelete} class="button-form">
|
||||
<Button icon="fas fa-trash" danger={true}>Delete</Button>
|
||||
@ -41,7 +36,7 @@
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Checkbox label="Optional" bind:value={data.optional}/>
|
||||
<Checkbox label="Required" bind:value={data.required}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,13 +62,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
<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">
|
||||
{#if withDeleteButton}
|
||||
<form on:submit|preventDefault={forwardDelete} class="button-form">
|
||||
@ -85,7 +73,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if withCreateButton}
|
||||
{#if withCreateButton && false}
|
||||
<div class="row" style="justify-content: center; margin-top: 10px">
|
||||
<Button type="submit" icon="fas fa-plus" {disabled}>Add Input</Button>
|
||||
</div>
|
||||
@ -105,7 +93,6 @@
|
||||
import Checkbox from "../form/Checkbox.svelte";
|
||||
|
||||
export let withCreateButton = false;
|
||||
export let withSaveButton = false;
|
||||
export let withDeleteButton = false;
|
||||
export let withDirectionButtons = false;
|
||||
export let disabled = false;
|
||||
@ -121,10 +108,6 @@
|
||||
dispatch('create', data);
|
||||
}
|
||||
|
||||
function forwardSave() {
|
||||
dispatch('save', data);
|
||||
}
|
||||
|
||||
function forwardDelete() {
|
||||
dispatch('delete', {});
|
||||
}
|
||||
|
@ -40,3 +40,7 @@ export function colourToInt(colour) {
|
||||
export function intToColour(i) {
|
||||
return `#${i.toString(16)}`
|
||||
}
|
||||
|
||||
export function nullIfBlank(s) {
|
||||
return s === '' ? null : s;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="parent">
|
||||
<div class="content">
|
||||
<Card footer={false}>
|
||||
<Card footer footerRight>
|
||||
<span slot="title">Forms</span>
|
||||
<div slot="body" class="body-wrapper">
|
||||
<div class="section">
|
||||
@ -18,25 +18,31 @@
|
||||
<div class="section">
|
||||
<h2 class="section-title">Manage Forms</h2>
|
||||
|
||||
<div class="col-1" style="flex-direction: row">
|
||||
<div class="col-4" style="margin-right: 12px">
|
||||
{#if editingTitle && activeFormId !== null}
|
||||
<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">
|
||||
<Dropdown col1={true} bind:value={activeFormId}>
|
||||
<Dropdown col1 bind:value={activeFormId}>
|
||||
<option value={null}>Select a form...</option>
|
||||
{#each forms as form}
|
||||
<option value="{form.form_id}">{form.title}</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if activeFormId !== null}
|
||||
<div class="col-4">
|
||||
<Button danger={true} type="button"
|
||||
<Button on:click={() => editingTitle = true}>Rename Form</Button>
|
||||
<Button danger type="button"
|
||||
on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="manage">
|
||||
{#if activeFormId !== null}
|
||||
@ -45,29 +51,40 @@
|
||||
<FormInputRow data={input} formId={activeFormId}
|
||||
withSaveButton={true} withDeleteButton={true} withDirectionButtons={true}
|
||||
index={i} {formLength}
|
||||
on:save={(e) => editInput(activeFormId, input.id, e.detail)}
|
||||
on:delete={() => deleteInput(activeFormId, input.id)}
|
||||
on:move={(e) => changePosition(activeFormId, input.id, e.detail.direction)} />
|
||||
on:delete={() => deleteInput(activeFormId, input)}
|
||||
on:move={(e) => changePosition(activeFormId, input, e.detail.direction)}/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if activeFormId !== null}
|
||||
<FormInputRow bind:data={inputCreationData} withCreateButton={true} disabled={formLength >= 5}
|
||||
on:create={(e) => createInput(e.detail)}/>
|
||||
<div class="row" style="justify-content: center; align-items: center; gap: 10px; margin-top: 10px">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<Button type="submit" icon="fas fa-floppy-disk" disabled={formLength === 0} on:click={saveInputs}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:window bind:innerWidth={windowWidth} />
|
||||
<svelte:window bind:innerWidth={windowWidth}/>
|
||||
|
||||
<script>
|
||||
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 axios from "axios";
|
||||
import {API_URL} from "../js/constants";
|
||||
@ -75,7 +92,7 @@
|
||||
import Input from "../components/form/Input.svelte";
|
||||
import Dropdown from "../components/form/Dropdown.svelte";
|
||||
import FormInputRow from "../components/manage/FormInputRow.svelte";
|
||||
import { flip } from "svelte/animate";
|
||||
import {flip} from "svelte/animate";
|
||||
|
||||
export let currentRoute;
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
@ -84,18 +101,40 @@
|
||||
|
||||
let newTitle;
|
||||
let forms = [];
|
||||
let toDelete = {};
|
||||
let activeFormId = null;
|
||||
$: 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;
|
||||
|
||||
let editingTitle = false;
|
||||
let renamedTitle = "";
|
||||
$: activeFormId, reflectTitle();
|
||||
|
||||
function reflectTitle() {
|
||||
renamedTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : null;
|
||||
}
|
||||
|
||||
$: windowWidth = 0;
|
||||
|
||||
function getForm(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() {
|
||||
let data = {
|
||||
title: newTitle,
|
||||
@ -116,6 +155,8 @@
|
||||
activeFormId = null; // Error thrown from {#each forms.find} if we don't temporarily set this to null?
|
||||
forms = [...forms, form];
|
||||
activeFormId = form.form_id;
|
||||
|
||||
addInput();
|
||||
}
|
||||
|
||||
async function deleteForm(id) {
|
||||
@ -135,21 +176,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function createInput(data) {
|
||||
let mapped = {...data, style: parseInt(data.style)};
|
||||
function addInput() {
|
||||
if (formLength >= 5) return;
|
||||
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/forms/${activeFormId}`, mapped);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
const input = {
|
||||
form_id: activeFormId,
|
||||
position: formLength + 1,
|
||||
style: "1",
|
||||
label: "",
|
||||
placeholder: "",
|
||||
required: true,
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
let form = getForm(res.data.form_id);
|
||||
form.inputs = [...form.inputs, res.data];
|
||||
const form = getForm(activeFormId);
|
||||
form.inputs = [...form.inputs, input];
|
||||
forms = forms;
|
||||
inputCreationData = {"style": "1"};
|
||||
|
||||
notifySuccess('Form input created successfully');
|
||||
}
|
||||
|
||||
async function editInput(formId, inputId, data) {
|
||||
@ -168,42 +210,66 @@
|
||||
notifySuccess('Form input updated successfully');
|
||||
}
|
||||
|
||||
async function deleteInput(formId, inputId) {
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
async function deleteInput(formId, input) {
|
||||
let form = getForm(formId);
|
||||
|
||||
let idx = form.inputs.findIndex((i) => i === input);
|
||||
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;
|
||||
|
||||
notifySuccess('Form input deleted successfully');
|
||||
if (!input.is_new) {
|
||||
if (toDelete[formId] === undefined) {
|
||||
toDelete[formId] = [];
|
||||
}
|
||||
|
||||
async function changePosition(formId, inputId, direction) {
|
||||
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}/${direction}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
toDelete[formId] = [...toDelete[formId], input.id];
|
||||
}
|
||||
}
|
||||
|
||||
if (res.data.success) {
|
||||
//let form = getForm(formId);
|
||||
let form = forms.find(form => form.form_id === activeFormId);
|
||||
let idx = form.inputs.findIndex((input) => input.id === inputId);
|
||||
function changePosition(formId, input, direction) {
|
||||
const form = getForm(formId);
|
||||
let idx = form.inputs.findIndex((i) => i === input);
|
||||
|
||||
let inputs = form.inputs;
|
||||
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") {
|
||||
[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;
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -215,7 +281,6 @@
|
||||
|
||||
forms = res.data || [];
|
||||
forms.flatMap(f => f.inputs).forEach(i => {
|
||||
i.optional = !i.required;
|
||||
i.style = i.style.toString();
|
||||
});
|
||||
|
||||
@ -276,6 +341,14 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
hr.fill {
|
||||
border-top: 1px solid #777;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -284,6 +357,26 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -293,6 +386,22 @@
|
||||
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 {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
@ -314,5 +423,22 @@
|
||||
#create-button-wrapper {
|
||||
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>
|
||||
|
4
go.mod
4
go.mod
@ -83,3 +83,7 @@ require (
|
||||
gopkg.in/yaml.v2 v2.4.0 // 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/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/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/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
Loading…
x
Reference in New Issue
Block a user