This commit is contained in:
rxdn 2022-02-09 17:30:07 +00:00
parent 54cf2521e5
commit 116e5d6e05
22 changed files with 939 additions and 28 deletions

View File

@ -0,0 +1,45 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
)
type createFormBody struct {
Title string `json:"title"`
}
func CreateForm(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data createFormBody
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
if len(data.Title) > 255 {
ctx.JSON(400, utils.ErrorStr("Title is too long"))
return
}
// 26^50 chance of collision
customId := utils.RandString(50)
id, err := dbclient.Client.Forms.Create(guildId, data.Title, customId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
form := database.Form{
Id: id,
GuildId: guildId,
Title: data.Title,
CustomId: customId,
}
ctx.JSON(200, form)
}

View File

@ -0,0 +1,119 @@
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"
)
type inputCreateBody struct {
Style component.TextStyleTypes `json:"style"`
Label string `json:"label"`
Placeholder *string `json:"placeholder"`
}
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 := utils.RandString(30)
formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder)
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,
})
}
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) > 255 {
ctx.JSON(400, utils.ErrorStr("The input label must be between 1 and 255 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

@ -0,0 +1,41 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func DeleteForm(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
}
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
}
if err := dbclient.Client.Forms.Delete(formId); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -0,0 +1,63 @@
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

@ -0,0 +1,28 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
)
func GetForms(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
forms, err := dbclient.Client.Forms.GetForms(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
inputs, err := dbclient.Client.FormInput.GetInputsForGuild(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, gin.H{
"forms": forms,
"inputs": inputs,
})
}

View File

@ -0,0 +1,52 @@
package forms
import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/gin-gonic/gin"
"strconv"
)
func UpdateForm(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
var data createFormBody
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
if len(data.Title) > 255 {
ctx.JSON(400, utils.ErrorStr("Title is too long"))
return
}
formId, err := strconv.Atoi(ctx.Param("form_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
}
if err := dbclient.Client.Forms.UpdateTitle(formId, data.Title); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, utils.SuccessResponse)
}

View File

@ -0,0 +1,83 @@
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,
}
if err := dbclient.Client.FormInput.Update(newInput); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.JSON(200, newInput)
}

View File

@ -38,6 +38,7 @@ type panelBody struct {
ImageUrl *string `json:"image_url,omitempty"`
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
ButtonStyle component.ButtonStyle `json:"button_style,string"`
FormId int `json:"form_id"`
}
func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelMessageData {
@ -138,6 +139,11 @@ func CreatePanel(ctx *gin.Context) {
return
}
var formId *int
if data.FormId != 0 { // Already validated
formId = &data.FormId
}
// Store in DB
panel := database.Panel{
MessageId: msgId,
@ -154,6 +160,7 @@ func CreatePanel(ctx *gin.Context) {
ImageUrl: data.ImageUrl,
ThumbnailUrl: data.ThumbnailUrl,
ButtonStyle: int(data.ButtonStyle),
FormId: formId,
}
panelId, err := dbclient.Client.Panel.Create(panel)
@ -305,6 +312,19 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool {
return false
}
{
ok, err := p.verifyFormId(guildId)
if err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return false
}
if !ok {
ctx.AbortWithStatusJSON(400, utils.ErrorStr("Guild ID for form does not match"))
return false
}
}
return true
}
@ -383,3 +403,24 @@ func (p *panelBody) verifyThumbnailUrl() bool {
func (p *panelBody) verifyButtonStyle() bool {
return p.ButtonStyle >= component.ButtonStylePrimary && p.ButtonStyle <= component.ButtonStyleDanger
}
func (p *panelBody) verifyFormId(guildId uint64) (bool, error) {
if p.FormId == 0 { // TODO: Use nil
return true, nil
} else {
form, ok, err := dbclient.Client.Forms.Get(p.FormId)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
if form.GuildId != guildId {
return false, nil
}
return true, nil
}
}

View File

@ -142,6 +142,12 @@ func UpdatePanel(ctx *gin.Context) {
}
}
// Already validated
var formId *int
if data.FormId != 0 {
formId = &data.FormId
}
// Store in DB
panel := database.Panel{
PanelId: panelId,
@ -159,6 +165,7 @@ func UpdatePanel(ctx *gin.Context) {
ImageUrl: data.ImageUrl,
ThumbnailUrl: data.ThumbnailUrl,
ButtonStyle: int(data.ButtonStyle),
FormId: formId,
}
if err = dbclient.Client.Panel.Update(panel); err != nil {

View File

@ -5,6 +5,7 @@ import (
api_autoclose "github.com/TicketsBot/GoPanel/app/http/endpoints/api/autoclose"
api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist"
api_customisation "github.com/TicketsBot/GoPanel/app/http/endpoints/api/customisation"
api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms"
api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel"
api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings"
api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags"
@ -102,6 +103,14 @@ func StartServer() {
guildAuthApiAdmin.PATCH("/multipanels/:panelid", api_panels.MultiPanelUpdate)
guildAuthApiAdmin.DELETE("/multipanels/:panelid", api_panels.MultiPanelDelete)
guildAuthApiSupport.GET("/forms", api_forms.GetForms)
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.DELETE("/forms/:form_id", api_forms.DeleteForm)
guildAuthApiAdmin.POST("/forms/:form_id", api_forms.CreateInput)
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id", api_forms.UpdateInput)
guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput)
// Should be a GET, but easier to take a body for development purposes
guildAuthApiSupport.POST("/transcripts",
rl(middleware.RateLimitTypeUser, 5, 5*time.Second),

View File

@ -1,5 +1,7 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
{#if label !== undefined}
<label for="input" class="form-label">{label}</label>
{/if}
<select id="input" class="form-input" on:change bind:value={value}>
<slot />
</select>

View File

@ -1,4 +1,4 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4} class:col-3-4={col3_4}>
<label for="input" class="form-label">{label}</label>
<textarea id="input" class="form-input" placeholder="{placeholder}" bind:value on:change on:input></textarea>
</div>
@ -12,6 +12,8 @@
export let col2 = false;
export let col3 = false;
export let col4 = false;
export let col3_4 = false;
</script>
<style>

View File

@ -0,0 +1,126 @@
<form on:submit|preventDefault={forwardCreate} class="input-form">
<div class="row">
<div class="sub-row" style="flex: 1">
<Input col4={true} label="Label" bind:value={data.label} placeholder="Name of the field" />
</div>
<div class="sub-row buttons-row">
{#if windowWidth > 950}
{#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>
</form>
{/if}
{/if}
</div>
</div>
<div class="row settings-row">
<Textarea col3_4={true} label="Placeholder" bind:value={data.placeholder}
placeholder="Placeholder text for the field, just like this text" />
<Dropdown col4={true} label="Style" bind:value={data.style}>
<option value=1 selected>Short</option>
<option value=2>Paragraph</option>
</Dropdown>
</div>
{#if windowWidth <= 950}
<div class="row">
{#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>
</form>
{/if}
</div>
{/if}
{#if withCreateButton}
<div class="row" style="justify-content: center; margin-top: 10px">
<Button type="submit" icon="fas fa-plus">Add Input</Button>
</div>
{/if}
</form>
<svelte:window bind:innerWidth={windowWidth} />
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Input from "../form/Input.svelte";
import Dropdown from "../form/Dropdown.svelte";
import Button from "../Button.svelte";
import Textarea from "../form/Textarea.svelte";
export let withCreateButton = false;
export let withSaveButton = false;
export let withDeleteButton = false;
export let data = {};
$: windowWidth = 0;
function forwardCreate() {
dispatch('create', data);
}
function forwardSave() {
dispatch('save', data);
}
function forwardDelete() {
dispatch('delete', {});
}
</script>
<style>
.input-form {
display: flex;
flex-direction: column;
width: 100%;
border-top: 1px solid rgba(0, 0, 0, .25);
padding-top: 10px;
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
}
.sub-row {
display: flex;
flex-direction: row;
}
.button-form {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 0.5em;
}
.buttons-row > form:first-of-type {
margin-right: 10px;
}
@media only screen and (max-width: 950px) {
.row {
flex-direction: column;
}
.settings-row {
flex-direction: column-reverse !important;
}
}
</style>

View File

@ -21,6 +21,13 @@
<option value="3">Green</option>
<option value="4">Red</option>
</Dropdown>
<Dropdown col4=true label="Form" bind:value={data.form_id}>
<option value=0>Disabled</option>
{#each forms as form}
<option value={form.form_id}>{form.title}</option>
{/each}
</Dropdown>
</div>
<div class="row" style="justify-content: center">
<div class="col-3">
@ -94,12 +101,14 @@
default_team: true,
teams: [],
button_style: "1",
form_id: 0,
};
}
export let channels = [];
export let roles = [];
export let teams = [];
export let forms = [];
let advancedSettings = false;
let overflowShow = false;

View File

@ -4,7 +4,7 @@
<span slot="title">Edit Panel</span>
<div slot="body" class="body-wrapper">
<PanelCreationForm {guildId} {channels} {roles} {teams} bind:data={panel} seedDefault={false} />
<PanelCreationForm {guildId} {channels} {roles} {teams} {forms} bind:data={panel} seedDefault={false} />
</div>
<div slot="footer">
@ -30,6 +30,7 @@
export let guildId;
export let panel = {};
export let channels = [];
export let forms = [];
export let roles = [];
export let teams = [];

View File

@ -227,7 +227,7 @@
justify-content: center;
}
:global(.col-4, .col-3, .col-2) {
:global(.col-4, .col-3, .col-2, .col-3-4) {
width: 100% !important;
}
}

View File

@ -9,6 +9,7 @@
<NavElement icon="fas fa-cogs" link="/manage/{guildId}/settings" on:click={closeDropdown}>Settings</NavElement>
<NavElement icon="fas fa-copy" link="/manage/{guildId}/transcripts" on:click={closeDropdown}>Transcripts</NavElement>
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Reaction Panels</NavElement>
<NavElement icon="fas fa-poll-h" link="/manage/{guildId}/forms" on:click={closeDropdown}>Forms</NavElement>
<NavElement icon="fas fa-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Teams</NavElement>
<NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement>
<NavElement icon="fas fa-ban" link="/manage/{guildId}/blacklist" on:click={closeDropdown}>Blacklist</NavElement>

View File

@ -20,6 +20,7 @@ import Teams from './views/Teams.svelte'
import Tickets from './views/Tickets.svelte'
import TicketView from './views/TicketView.svelte'
import Appearance from './views/Appearance.svelte';
import Forms from './views/Forms.svelte';
export const routes = [
{name: '/', component: Index, layout: IndexLayout},
@ -65,6 +66,7 @@ export const routes = [
{name: 'blacklist', component: Blacklist, layout: ManageLayout},
{name: 'tags', component: Tags, layout: ManageLayout},
{name: 'teams', component: Teams, layout: ManageLayout},
{name: 'forms', component: Forms, layout: ManageLayout},
{
name: 'tickets',
nestedRoutes: [

View File

@ -0,0 +1,279 @@
<div class="parent">
<div class="content">
<Card footer={false}>
<span slot="title">Forms</span>
<div slot="body" class="body-wrapper">
<div class="section">
<h2 class="section-title">Create New Form</h2>
<form on:submit|preventDefault={createForm}>
<div class="row" id="creation-row">
<Input placeholder="Form Title" col3={true} bind:value={newTitle}/>
<div id="create-button-wrapper">
<Button icon="fas fa-paper-plane" fullWidth={windowWidth <= 950}>Create</Button>
</div>
</div>
</form>
</div>
<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">
<div class="multiselect-super">
<Dropdown col1={true} 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"
on:click={() => deleteForm(activeFormId)}>Delete {getActiveFormTitle()}</Button>
</div>
{/if}
</div>
<div class="manage">
{#if activeFormId !== null && inputs[activeFormId.toString()]}
{#each inputs[activeFormId.toString()] as input}
<FormInputRow data={input} formId={activeFormId} withSaveButton={true} withDeleteButton={true}
on:save={(e) => editInput(activeFormId, input.id, e.detail)}
on:delete={() => deleteInput(activeFormId, input.id)}/>
{/each}
{/if}
{#if activeFormId !== null}
<FormInputRow bind:data={inputCreationData} withCreateButton={true}
on:create={(e) => createInput(e.detail)}/>
{/if}
</div>
</div>
</div>
</Card>
</div>
</div>
<svelte:window bind:innerWidth={windowWidth} />
<script>
import Card from "../components/Card.svelte";
import {notifyError, notifySuccess, withLoadingScreen} from '../js/util'
import Button from "../components/Button.svelte";
import axios from "axios";
import {API_URL} from "../js/constants";
import {setDefaultHeaders} from '../includes/Auth.svelte'
import Input from "../components/form/Input.svelte";
import Dropdown from "../components/form/Dropdown.svelte";
import FormInputRow from "../components/manage/FormInputRow.svelte";
export let currentRoute;
let guildId = currentRoute.namedParams.id;
let defaultTeam = {id: 'default', name: 'Default'};
let newTitle;
let forms = [];
let inputs = {};
let activeFormId = null;
let inputCreationData = {};
$: windowWidth = 0;
async function createForm() {
let data = {
title: newTitle,
};
const res = await axios.post(`${API_URL}/api/${guildId}/forms`, data);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
notifySuccess(`Form ${newTitle} has been created`);
newTitle = '';
inputs[res.data.form_id] = {};
forms = [...forms, res.data];
activeFormId = res.data.form_id;
}
async function deleteForm(id) {
const res = await axios.delete(`${API_URL}/api/${guildId}/forms/${id}`);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
notifySuccess(`Form deleted successfully`);
forms = forms.filter(form => form.form_id !== id);
delete inputs[id.toString()];
if (forms.length > 0) {
activeFormId = forms[0].form_id;
} else {
activeFormId = null;
}
}
async function createInput(data) {
let mapped = {...data, style: parseInt(data.style)};
const res = await axios.post(`${API_URL}/api/${guildId}/forms/${activeFormId}`, mapped);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
inputs[res.data.form_id] = [...inputs[res.data.form_id], res.data];
inputCreationData = {};
notifySuccess('Form input created successfully');
}
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;
}
inputs[res.data.form_id] = inputs[res.data.form_id].filter(input => input.id !== inputId);
inputs[res.data.form_id] = [...inputs[res.data.form_id], res.data];
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;
}
inputs[formId] = inputs[formId].filter(input => input.id !== inputId);
notifySuccess('Form input deleted successfully');
}
async function loadForms() {
const res = await axios.get(`${API_URL}/api/${guildId}/forms`);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
forms = res.data.forms || [];
for (const form of forms) {
inputs[form.form_id] = {};
}
for (const [key, value] of Object.entries(res.data.inputs)) {
inputs[key] = value.map(input => ({...input, style: input.style.toString()}));
}
if (forms.length > 0) {
activeFormId = forms[0].form_id;
}
}
function getActiveFormTitle() {
console.log(forms);
return activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : 'Unknown';
}
withLoadingScreen(async () => {
setDefaultHeaders();
await loadForms();
});
</script>
<style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.content {
display: flex;
justify-content: space-between;
width: 96%;
height: 100%;
margin-top: 30px;
margin-bottom: 50px;
}
.body-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 1%;
}
.section {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.section:not(:first-child) {
margin-top: 2%;
}
.section-title {
font-size: 36px;
font-weight: bolder !important;
}
h3 {
font-size: 28px;
margin-bottom: 4px;
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
}
.manage {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 100%;
margin-top: 12px;
}
#creation-row {
justify-content: flex-start !important;
}
#create-button-wrapper {
margin-left: 15px;
height: 40px;
}
@media only screen and (max-width: 950px) {
.manage {
flex-direction: column;
}
.row {
flex-direction: column;
}
#create-button-wrapper {
margin-left: unset;
}
}
</style>

View File

@ -1,5 +1,5 @@
{#if editModal}
<PanelEditModal {guildId} {channels} {roles} {teams} bind:panel={editData}
<PanelEditModal {guildId} {channels} {roles} {teams} {forms} bind:panel={editData}
on:close={() => editModal = false} on:confirm={submitEdit}/>
{/if}
@ -55,7 +55,7 @@
<div slot="body" class="body-wrapper">
{#if !$loadingScreen}
<PanelCreationForm {guildId} {channels} {roles} {teams} bind:data={panelCreateData}/>
<PanelCreationForm {guildId} {channels} {roles} {teams} {forms} bind:data={panelCreateData}/>
<div style="display: flex; justify-content: center">
<div class="col-3">
<Button icon="fas fa-paper-plane" fullWidth={true} on:click={createPanel}>Submit</Button>
@ -143,6 +143,7 @@
let channels = [];
let roles = [];
let teams = [];
let forms = [];
let panels = [];
let multiPanels = [];
let isPremium = false;
@ -320,10 +321,21 @@
roles = res.data.roles;
}
async function loadForms() {
const res = await axios.get(`${API_URL}/api/${guildId}/forms`);
if (res.status !== 200) {
notifyError(res.data.error);
return;
}
forms = res.data.forms || [];
}
withLoadingScreen(async () => {
await loadPremium();
await loadChannels();
await loadTeams();
await loadForms();
await loadRoles();
await loadPanels();
await loadMultiPanels();

4
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20210220155137-a562b2f1bbbb
github.com/TicketsBot/common v0.0.0-20210910205523-7ce93fba6fa5
github.com/TicketsBot/database v0.0.0-20211109153802-24100e383d78
github.com/TicketsBot/database v0.0.0-20211202174040-bd189305f898
github.com/TicketsBot/worker v0.0.0-20211108224403-97ac8e44b789
github.com/apex/log v1.1.2
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
@ -23,7 +23,7 @@ require (
github.com/jackc/pgx/v4 v4.7.1
github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c
github.com/pkg/errors v0.9.1
github.com/rxdn/gdl v0.0.0-20211030160619-a8772c268ca4
github.com/rxdn/gdl v0.0.0-20220209151849-19fd8c86af50
github.com/sirupsen/logrus v1.5.0
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
)

19
go.sum
View File

@ -14,8 +14,8 @@ github.com/TicketsBot/common v0.0.0-20210910205523-7ce93fba6fa5/go.mod h1:SVwX6g
github.com/TicketsBot/database v0.0.0-20200516170158-fd8a949aec2c/go.mod h1:eky4tBL+IZ0svPgTT0N/9i6j7ygHDQH3784DW+HgfcA=
github.com/TicketsBot/database v0.0.0-20210902172951-4e1f8ced84b7/go.mod h1:A4T2uQFIWC/ttCYpfgv7AkPjR09mMRgzG13lgoV/+aI=
github.com/TicketsBot/database v0.0.0-20211108142700-c406ab0fc1bb/go.mod h1:72oWvH/Gq1iKeXCZhVRZn1JFbNVC5iAgERZWTrEarEo=
github.com/TicketsBot/database v0.0.0-20211109153802-24100e383d78 h1:zzjOyxCdXN1fGDL2Na6Q82EDU96Cfd1vnlafeY1utUQ=
github.com/TicketsBot/database v0.0.0-20211109153802-24100e383d78/go.mod h1:72oWvH/Gq1iKeXCZhVRZn1JFbNVC5iAgERZWTrEarEo=
github.com/TicketsBot/database v0.0.0-20211202174040-bd189305f898 h1:bbDirB6NmLjJ+9Zbw+Zy3073I7vRGxlKOb+jTRXm3vE=
github.com/TicketsBot/database v0.0.0-20211202174040-bd189305f898/go.mod h1:72oWvH/Gq1iKeXCZhVRZn1JFbNVC5iAgERZWTrEarEo=
github.com/TicketsBot/logarchiver v0.0.0-20200423221245-a3f92edf8c14/go.mod h1:whts8TRxrAF4WuDuEAMllkWA/inKem0NhDEFeyuoOvE=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk=
@ -106,7 +106,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I=
github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
@ -115,7 +114,6 @@ github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCx
github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc=
github.com/go-redis/redis_rate/v9 v9.1.1 h1:7SIrbnhQ7zsTNEgIvprFhJf7/+l3wSpZc2iRVwUmaq8=
github.com/go-redis/redis_rate/v9 v9.1.1/go.mod h1:jjU9YxOSZ3cz0yj1QJVAJiy5ueKmL9o4AySJHcKyTSE=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
@ -237,7 +235,6 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv
github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -294,7 +291,6 @@ github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1y
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@ -323,14 +319,12 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
@ -353,10 +347,8 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/rxdn/gdl v0.0.0-20200522202912-4ae241eb98c1/go.mod h1:2gPBB++1s9Zh11AGM/y84KpmqTyLCnjGwEnt6xRRKL4=
github.com/rxdn/gdl v0.0.0-20210527173953-25dde613ff0a/go.mod h1:jvcb1N6AdaGx3/e8MoLedO6qSo/+UdA5GGHOA8cnAeU=
github.com/rxdn/gdl v0.0.0-20211030160619-a8772c268ca4 h1:vHSTqcCCZZwlj6trBUj3tqys5hnKbBf9J6mtuG7DvgM=
github.com/rxdn/gdl v0.0.0-20211030160619-a8772c268ca4/go.mod h1:rENs8TxMsoYSJRssegNS/+fy18NCI9EUdCJX8R83PlY=
github.com/rxdn/gdl v0.0.0-20220209151849-19fd8c86af50 h1:xkWpJfblzNEzt/wQ0OB/vqJdLBnSCYIcuVE6z396MWY=
github.com/rxdn/gdl v0.0.0-20220209151849-19fd8c86af50/go.mod h1:rENs8TxMsoYSJRssegNS/+fy18NCI9EUdCJX8R83PlY=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
@ -445,7 +437,6 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@ -461,7 +452,6 @@ golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -532,7 +522,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=