Ticket panel rework
This commit is contained in:
parent
ab733dde8d
commit
274e2bfa78
@ -8,35 +8,31 @@ import (
|
||||
"github.com/TicketsBot/GoPanel/rpc"
|
||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/GoPanel/utils/types"
|
||||
"github.com/TicketsBot/common/premium"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/rxdn/gdl/objects/channel"
|
||||
"github.com/rxdn/gdl/rest/request"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type multiPanelCreateData struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Colour int32 `json:"colour"`
|
||||
ChannelId uint64 `json:"channel_id,string"`
|
||||
SelectMenu bool `json:"select_menu"`
|
||||
SelectMenuPlaceholder *string `json:"select_menu_placeholder,omitempty" validate:"omitempty,max=150"`
|
||||
Panels []int `json:"panels"`
|
||||
ImageUrl *string `json:"image_url,omitempty"`
|
||||
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
|
||||
Embed *types.CustomEmbed `json:"embed" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
func (d *multiPanelCreateData) IntoMessageData(isPremium bool) multiPanelMessageData {
|
||||
return multiPanelMessageData{
|
||||
ChannelId: d.ChannelId,
|
||||
Title: d.Title,
|
||||
Content: d.Content,
|
||||
Colour: int(d.Colour),
|
||||
SelectMenu: d.SelectMenu,
|
||||
IsPremium: isPremium,
|
||||
ImageUrl: d.ImageUrl,
|
||||
ThumbnailUrl: d.ThumbnailUrl,
|
||||
ChannelId: d.ChannelId,
|
||||
SelectMenu: d.SelectMenu,
|
||||
SelectMenuPlaceholder: d.SelectMenuPlaceholder,
|
||||
Embed: d.Embed.IntoDiscordEmbed(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +45,18 @@ func MultiPanelCreate(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := validate.Struct(data); err != nil {
|
||||
var validationErrors validator.ValidationErrors
|
||||
if ok := errors.As(err, &validationErrors); !ok {
|
||||
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the panel"))
|
||||
return
|
||||
}
|
||||
|
||||
formatted := "Your input contained the following errors:\n" + utils.FormatValidationErrors(validationErrors)
|
||||
ctx.JSON(400, utils.ErrorStr(formatted))
|
||||
return
|
||||
}
|
||||
|
||||
// validate body & get sub-panels
|
||||
panels, err := data.doValidations(guildId)
|
||||
if err != nil {
|
||||
@ -86,14 +94,17 @@ func MultiPanelCreate(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dbEmbed, dbEmbedFields := data.Embed.IntoDatabaseStruct()
|
||||
multiPanel := database.MultiPanel{
|
||||
MessageId: messageId,
|
||||
ChannelId: data.ChannelId,
|
||||
GuildId: guildId,
|
||||
Title: data.Title,
|
||||
Content: data.Content,
|
||||
Colour: int(data.Colour),
|
||||
SelectMenu: data.SelectMenu,
|
||||
SelectMenuPlaceholder: data.SelectMenuPlaceholder,
|
||||
Embed: &database.CustomEmbedWithFields{
|
||||
CustomEmbed: dbEmbed,
|
||||
Fields: dbEmbedFields,
|
||||
},
|
||||
}
|
||||
|
||||
multiPanel.Id, err = dbclient.Client.MultiPanels.Create(ctx, multiPanel)
|
||||
@ -123,10 +134,12 @@ func MultiPanelCreate(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func (d *multiPanelCreateData) doValidations(guildId uint64) (panels []database.Panel, err error) {
|
||||
if err := validateEmbed(d.Embed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group, _ := errgroup.WithContext(context.Background())
|
||||
|
||||
group.Go(d.validateTitle)
|
||||
group.Go(d.validateContent)
|
||||
group.Go(d.validateChannel(guildId))
|
||||
group.Go(func() (e error) {
|
||||
panels, e = d.validatePanels(guildId)
|
||||
@ -137,26 +150,6 @@ func (d *multiPanelCreateData) doValidations(guildId uint64) (panels []database.
|
||||
return
|
||||
}
|
||||
|
||||
func (d *multiPanelCreateData) validateTitle() (err error) {
|
||||
if len(d.Title) > 255 {
|
||||
err = errors.New("Embed title must be between 1 and 255 characters")
|
||||
} else if len(d.Title) == 0 {
|
||||
d.Title = "Click to open a ticket"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *multiPanelCreateData) validateContent() (err error) {
|
||||
if len(d.Content) > 4096 {
|
||||
err = errors.New("Embed content must be between 1 and 4096 characters")
|
||||
} else if len(d.Content) == 0 { // Fill default
|
||||
d.Content = "Click on the button corresponding to the type of ticket you wish to open"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *multiPanelCreateData) validateChannel(guildId uint64) func() error {
|
||||
return func() error {
|
||||
// TODO: Use proper context
|
||||
|
@ -13,46 +13,32 @@ import (
|
||||
)
|
||||
|
||||
type multiPanelMessageData struct {
|
||||
IsPremium bool
|
||||
|
||||
ChannelId uint64
|
||||
|
||||
Title string
|
||||
Content string
|
||||
Colour int
|
||||
SelectMenu bool
|
||||
IsPremium bool
|
||||
ImageUrl, ThumbnailUrl *string
|
||||
SelectMenuPlaceholder *string
|
||||
|
||||
Embed *embed.Embed
|
||||
}
|
||||
|
||||
func multiPanelIntoMessageData(panel database.MultiPanel, isPremium bool) multiPanelMessageData {
|
||||
return multiPanelMessageData{
|
||||
ChannelId: panel.ChannelId,
|
||||
Title: panel.Title,
|
||||
Content: panel.Content,
|
||||
Colour: panel.Colour,
|
||||
SelectMenu: panel.SelectMenu,
|
||||
IsPremium: isPremium,
|
||||
ImageUrl: panel.ImageUrl,
|
||||
ThumbnailUrl: panel.ThumbnailUrl,
|
||||
|
||||
ChannelId: panel.ChannelId,
|
||||
|
||||
SelectMenu: panel.SelectMenu,
|
||||
SelectMenuPlaceholder: panel.SelectMenuPlaceholder,
|
||||
Embed: types.NewCustomEmbed(panel.Embed.CustomEmbed, panel.Embed.Fields).IntoDiscordEmbed(),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *multiPanelMessageData) send(ctx *botcontext.BotContext, panels []database.Panel) (uint64, error) {
|
||||
e := embed.NewEmbed().
|
||||
SetTitle(d.Title).
|
||||
SetDescription(d.Content).
|
||||
SetColor(d.Colour)
|
||||
|
||||
if d.ImageUrl != nil {
|
||||
e.SetImage(*d.ImageUrl)
|
||||
}
|
||||
|
||||
if d.ThumbnailUrl != nil {
|
||||
e.SetThumbnail(*d.ThumbnailUrl)
|
||||
}
|
||||
|
||||
if !d.IsPremium {
|
||||
// TODO: Don't harcode
|
||||
e.SetFooter("Powered by ticketsbot.net", "https://ticketsbot.net/assets/img/logo.png")
|
||||
d.Embed.SetFooter("Powered by ticketsbot.net", "https://ticketsbot.net/assets/img/logo.png")
|
||||
}
|
||||
|
||||
var components []component.Component
|
||||
@ -68,13 +54,20 @@ func (d *multiPanelMessageData) send(ctx *botcontext.BotContext, panels []databa
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder string
|
||||
if d.SelectMenuPlaceholder == nil {
|
||||
placeholder = "Select a topic..."
|
||||
} else {
|
||||
placeholder = *d.SelectMenuPlaceholder
|
||||
}
|
||||
|
||||
components = []component.Component{
|
||||
component.BuildActionRow(
|
||||
component.BuildSelectMenu(
|
||||
component.SelectMenu{
|
||||
CustomId: "multipanel",
|
||||
Options: options,
|
||||
Placeholder: "Select a topic...",
|
||||
Placeholder: placeholder,
|
||||
MinValues: utils.IntPtr(1),
|
||||
MaxValues: utils.IntPtr(1),
|
||||
Disabled: false,
|
||||
@ -116,7 +109,7 @@ func (d *multiPanelMessageData) send(ctx *botcontext.BotContext, panels []databa
|
||||
}
|
||||
|
||||
data := rest.CreateMessageData{
|
||||
Embeds: []*embed.Embed{e},
|
||||
Embeds: []*embed.Embed{d.Embed},
|
||||
Components: components,
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/TicketsBot/common/premium"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/rxdn/gdl/rest"
|
||||
"github.com/rxdn/gdl/rest/request"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@ -53,6 +54,18 @@ func MultiPanelUpdate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := validate.Struct(data); err != nil {
|
||||
var validationErrors validator.ValidationErrors
|
||||
if ok := errors.As(err, &validationErrors); !ok {
|
||||
c.JSON(500, utils.ErrorStr("An error occurred while validating the panel"))
|
||||
return
|
||||
}
|
||||
|
||||
formatted := "Your input contained the following errors:\n" + utils.FormatValidationErrors(validationErrors)
|
||||
c.JSON(400, utils.ErrorStr(formatted))
|
||||
return
|
||||
}
|
||||
|
||||
// validate body & get sub-panels
|
||||
panels, err := data.doValidations(guildId)
|
||||
if err != nil {
|
||||
@ -115,17 +128,18 @@ func MultiPanelUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// update DB
|
||||
dbEmbed, dbEmbedFields := data.Embed.IntoDatabaseStruct()
|
||||
updated := database.MultiPanel{
|
||||
Id: multiPanel.Id,
|
||||
MessageId: messageId,
|
||||
ChannelId: data.ChannelId,
|
||||
GuildId: guildId,
|
||||
Title: data.Title,
|
||||
Content: data.Content,
|
||||
Colour: int(data.Colour),
|
||||
SelectMenu: data.SelectMenu,
|
||||
ImageUrl: data.ImageUrl,
|
||||
ThumbnailUrl: data.ThumbnailUrl,
|
||||
SelectMenuPlaceholder: data.SelectMenuPlaceholder,
|
||||
Embed: &database.CustomEmbedWithFields{
|
||||
CustomEmbed: dbEmbed,
|
||||
Fields: dbEmbedFields,
|
||||
},
|
||||
}
|
||||
|
||||
if err = dbclient.Client.MultiPanels.Update(c, multiPanel.Id, updated); err != nil {
|
||||
|
@ -95,14 +95,7 @@ func DeletePanel(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
messageData := multiPanelMessageData{
|
||||
Title: multiPanel.Title,
|
||||
Content: multiPanel.Content,
|
||||
Colour: multiPanel.Colour,
|
||||
ChannelId: multiPanel.ChannelId,
|
||||
SelectMenu: multiPanel.SelectMenu,
|
||||
IsPremium: premiumTier > premium.None,
|
||||
}
|
||||
messageData := multiPanelIntoMessageData(multiPanel, premiumTier > premium.None)
|
||||
|
||||
messageId, err := messageData.send(botContext, panels)
|
||||
if err != nil {
|
||||
|
@ -306,16 +306,7 @@ func UpdatePanel(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
messageData := multiPanelMessageData{
|
||||
Title: multiPanel.Title,
|
||||
Content: multiPanel.Content,
|
||||
Colour: multiPanel.Colour,
|
||||
ChannelId: multiPanel.ChannelId,
|
||||
SelectMenu: multiPanel.SelectMenu,
|
||||
IsPremium: premiumTier > premium.None,
|
||||
ImageUrl: multiPanel.ImageUrl,
|
||||
ThumbnailUrl: multiPanel.ThumbnailUrl,
|
||||
}
|
||||
messageData := multiPanelIntoMessageData(multiPanel, premiumTier > premium.None)
|
||||
|
||||
messageId, err := messageData.send(botContext, panels)
|
||||
if err != nil {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/TicketsBot/GoPanel/botcontext"
|
||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/GoPanel/utils/types"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/rxdn/gdl/objects/channel"
|
||||
"github.com/rxdn/gdl/objects/guild"
|
||||
@ -289,13 +290,7 @@ func validateNamingScheme(ctx PanelValidationContext) validation.ValidationFunc
|
||||
|
||||
func validateWelcomeMessage(ctx PanelValidationContext) validation.ValidationFunc {
|
||||
return func() error {
|
||||
wm := ctx.Data.WelcomeMessage
|
||||
|
||||
if wm == nil || wm.Title != nil || wm.Description != nil || len(wm.Fields) > 0 || wm.ImageUrl != nil || wm.ThumbnailUrl != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validation.NewInvalidInputError("Welcome message has no content")
|
||||
return validateEmbed(ctx.Data.WelcomeMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,3 +334,11 @@ func validateAccessControlList(ctx PanelValidationContext) validation.Validation
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateEmbed(e *types.CustomEmbed) error {
|
||||
if e == nil || e.Title != nil || e.Description != nil || len(e.Fields) > 0 || e.ImageUrl != nil || e.ThumbnailUrl != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validation.NewInvalidInputError("Your embed message does not contain any content")
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TicketsBot/GoPanel/botcontext"
|
||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||
@ -59,8 +60,8 @@ func CreateTag(ctx *gin.Context) {
|
||||
|
||||
// TODO: Limit command amount
|
||||
if err := validate.Struct(data); err != nil {
|
||||
validationErrors, ok := err.(validator.ValidationErrors)
|
||||
if !ok {
|
||||
var validationErrors validator.ValidationErrors
|
||||
if ok := errors.As(err, &validationErrors); !ok {
|
||||
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
||||
return
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
<svelte:window bind:innerWidth />
|
||||
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import {afterUpdate, onMount} from "svelte";
|
||||
import Tooltip from "svelte-tooltip";
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
@ -89,9 +89,6 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// content.addEventListener('DOMNodeInserted', updateIfExpanded);
|
||||
// content.addEventListener('DOMNodeRemoved', updateIfExpanded);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
updateIfExpanded();
|
||||
setTimeout(updateIfExpanded, 300); // TODO: Move with transition height
|
||||
@ -101,6 +98,8 @@
|
||||
|
||||
if (defaultOpen || forceAlwaysOpen) toggle(true);
|
||||
});
|
||||
|
||||
afterUpdate(updateIfExpanded);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
{#if data}
|
||||
{#if data && appliedOverrides}
|
||||
<form class="form-wrapper" on:submit|preventDefault>
|
||||
<div class="row">
|
||||
<Colour col3 label="Embed Colour" bind:value={data.colour}/>
|
||||
<Colour col3 label="Embed Colour" on:change={updateColour} bind:value={tempColour}/>
|
||||
<Input col3 label="Title" placeholder="Embed Title" bind:value={data.title}/>
|
||||
<Input col3 label="Title URL (Optional)" placeholder="https://example.com" bind:value={data.url}/>
|
||||
</div>
|
||||
@ -37,8 +37,10 @@
|
||||
<span slot="header">Footer</span>
|
||||
<div slot="content" class="row">
|
||||
{#if footerPremiumOnly}
|
||||
<Input col3 label="Footer Text" placeholder="Footer Text" badge="Premium" bind:value={data.footer.text}/>
|
||||
<Input col3 label="Footer Icon URL (Optional)" badge="Premium" placeholder="https://example.com/image.png"
|
||||
<Input col3 label="Footer Text" placeholder="Footer Text" badge="Premium"
|
||||
bind:value={data.footer.text}/>
|
||||
<Input col3 label="Footer Icon URL (Optional)" badge="Premium"
|
||||
placeholder="https://example.com/image.png"
|
||||
bind:value={data.footer.icon_url}/>
|
||||
{:else}
|
||||
<Input col3 label="Footer Text" placeholder="Footer Text" bind:value={data.footer.text}/>
|
||||
@ -66,8 +68,11 @@
|
||||
bind:value={field.value}/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="add-field-wrapper">
|
||||
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</form>
|
||||
{/if}
|
||||
@ -77,7 +82,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
@ -86,7 +90,11 @@
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-field-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -98,12 +106,14 @@
|
||||
import DateTimePicker from "./form/DateTimePicker.svelte";
|
||||
import Checkbox from "./form/Checkbox.svelte";
|
||||
import Button from "./Button.svelte";
|
||||
import {onMount} from "svelte";
|
||||
import {intToColour, colourToInt} from "../js/util";
|
||||
|
||||
export let data;
|
||||
|
||||
$: data = data ?? {
|
||||
fields: [],
|
||||
colour: '#2ECC71',
|
||||
colour: 0x2ECC71,
|
||||
author: {},
|
||||
footer: {},
|
||||
};
|
||||
@ -119,4 +129,28 @@
|
||||
data.fields.splice(i, 1);
|
||||
data = data;
|
||||
}
|
||||
|
||||
let tempColour = "#2ecc71";
|
||||
function updateColour() {
|
||||
data.colour = colourToInt(tempColour);
|
||||
}
|
||||
|
||||
let appliedOverrides = false;
|
||||
onMount(() => {
|
||||
data.author = data.author ?? {};
|
||||
data.footer = data.footer ?? {};
|
||||
data.fields = data.fields ?? [];
|
||||
|
||||
if (!data.colour) {
|
||||
data.colour = 0x2ECC71;
|
||||
} else {
|
||||
if (typeof data.colour === "string" && data.colour.startsWith('#')) {
|
||||
data.colour = parseInt(data.colour.slice(1), 16)
|
||||
}
|
||||
|
||||
tempColour = intToColour(data.colour);
|
||||
}
|
||||
|
||||
appliedOverrides = true;
|
||||
});
|
||||
</script>
|
||||
|
@ -24,8 +24,6 @@
|
||||
selectedRaw = [];
|
||||
}
|
||||
|
||||
console.log(selectedRaw)
|
||||
|
||||
if (isMulti) {
|
||||
selected = selectedRaw.map((panel) => panel.panel_id);
|
||||
} else {
|
||||
|
@ -1,59 +1,41 @@
|
||||
<form on:submit|preventDefault>
|
||||
<div class="row">
|
||||
<Input col1={true} label="Panel Title" placeholder="Click to open a ticket" bind:value={data.title}/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Textarea col1={true} label="Panel Content" bind:value={data.content}
|
||||
placeholder="Click on the button corresponding to the type of ticket you wish to open. Let users know which button responds to which category. You are able to use emojis here."/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1-3">
|
||||
<Colour col1={true} label="Panel Colour" on:change={updateColour} bind:value={tempColour}/>
|
||||
</div>
|
||||
<div class="col-2-3">
|
||||
<ChannelDropdown col1 allowAnnouncementChannel {channels} label="Panel Channel" bind:value={data.channel_id}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3-4" style="padding-right: 10px">
|
||||
<PanelDropdown label="Panels" {panels} bind:selected={data.panels} />
|
||||
</div>
|
||||
|
||||
<div class="col-1-4">
|
||||
<Checkbox label="Use Dropdown Menu" bind:value={data.select_menu} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="justify-content: center; padding-top: 10px">
|
||||
<Collapsible defaultOpen>
|
||||
<span slot="header">Properties</span>
|
||||
<div slot="content" class="col-1">
|
||||
<div class="col-1">
|
||||
<Button icon="fas fa-sliders-h" fullWidth=true type="button"
|
||||
on:click={toggleAdvancedSettings}>Toggle Advanced Settings
|
||||
</Button>
|
||||
<ChannelDropdown col1 allowAnnouncementChannel {channels} label="Panel Channel"
|
||||
bind:value={data.channel_id}/>
|
||||
</div>
|
||||
<div class="col-1" style="padding-right: 10px">
|
||||
<PanelDropdown label="Panels (Minimum 2)" {panels} bind:selected={data.panels}/>
|
||||
</div>
|
||||
<div class="row advanced-settings" class:advanced-settings-show={advancedSettings}
|
||||
class:advanced-settings-hide={!advancedSettings} class:show-overflow={overflowShow}>
|
||||
<div class="inner" class:inner-show={advancedSettings} class:absolute={advancedSettings && !overflowShow} >
|
||||
<div class="row">
|
||||
<Input col1={true} label="Large Image URL" bind:value={data.image_url} placeholder="https://example.com/image.png" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<Input col1={true} label="Small Image URL" bind:value={data.thumbnail_url} placeholder="https://example.com/image.png" />
|
||||
<div class="col-1">
|
||||
<div class="row dropdown-menu-settings">
|
||||
<Checkbox label="Use Dropdown Menu" bind:value={data.select_menu}/>
|
||||
<div class="placeholder-input">
|
||||
<Input label="Dropdown Menu Placeholder" col1 placeholder="Select a topic..."
|
||||
bind:value={data.select_menu_placeholder} disabled={!data.select_menu} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible defaultOpen>
|
||||
<span slot="header">Message</span>
|
||||
<div slot="content" class="col-1">
|
||||
<EmbedForm footerPremiumOnly={true} bind:data={data.embed}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
import Input from "../form/Input.svelte";
|
||||
import Textarea from "../form/Textarea.svelte";
|
||||
import Colour from "../form/Colour.svelte";
|
||||
import {colourToInt, intToColour} from "../../js/util";
|
||||
import ChannelDropdown from "../ChannelDropdown.svelte";
|
||||
import PanelDropdown from "../PanelDropdown.svelte";
|
||||
import {onMount} from "svelte";
|
||||
import Checkbox from "../form/Checkbox.svelte";
|
||||
import Button from "../Button.svelte";
|
||||
import Collapsible from "../Collapsible.svelte";
|
||||
import EmbedForm from "../EmbedForm.svelte";
|
||||
import Input from "../form/Input.svelte";
|
||||
|
||||
export let data;
|
||||
|
||||
@ -66,42 +48,17 @@
|
||||
const firstChannel = channels[0];
|
||||
|
||||
data = {
|
||||
colour: 0x7289da,
|
||||
channels: firstChannel ? firstChannel.id : undefined,
|
||||
panels: [],
|
||||
embed: {
|
||||
title: 'Open a ticket!',
|
||||
fields: [],
|
||||
colour: 0x2ECC71,
|
||||
author: {},
|
||||
footer: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mounted = false;
|
||||
let advancedSettings = false;
|
||||
let overflowShow = false;
|
||||
|
||||
function toggleAdvancedSettings() {
|
||||
advancedSettings = !advancedSettings;
|
||||
if (advancedSettings) {
|
||||
setTimeout(() => {
|
||||
overflowShow = true;
|
||||
}, 300);
|
||||
} else {
|
||||
overflowShow = false;
|
||||
}
|
||||
}
|
||||
|
||||
let tempColour = '#7289da';
|
||||
|
||||
function updateColour() {
|
||||
data.colour = colourToInt(tempColour);
|
||||
}
|
||||
|
||||
function applyOverrides() {
|
||||
tempColour = intToColour(data.colour);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!seedDefault) {
|
||||
applyOverrides();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -109,7 +66,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
@ -119,6 +75,15 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-menu-settings {
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dropdown-menu-settings > .placeholder-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 950px) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
@ -140,24 +105,4 @@
|
||||
width: 75%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.advanced-settings-show {
|
||||
visibility: visible;
|
||||
min-height: 142px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,91 +0,0 @@
|
||||
<div class="modal" transition:fade>
|
||||
<div class="modal-wrapper">
|
||||
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||
<span slot="title">Edit Multi-Panel</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
<MultiPanelCreationForm {guildId} {channels} {panels} bind:data seedDefault={false} />
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<Button danger={true} on:click={dispatchClose}>Cancel</Button>
|
||||
<div style="margin-left: 12px">
|
||||
<Button icon="fas fa-paper-plane" on:click={dispatchConfirm}>Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" transition:fade>
|
||||
</div>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown}/>
|
||||
|
||||
<script>
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
import {fade} from 'svelte/transition'
|
||||
import Card from "../Card.svelte";
|
||||
import Button from "../Button.svelte";
|
||||
import MultiPanelCreationForm from "./MultiPanelCreationForm.svelte";
|
||||
|
||||
export let guildId;
|
||||
export let data;
|
||||
export let channels = [];
|
||||
export let panels = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function dispatchClose() {
|
||||
dispatch('close', {});
|
||||
}
|
||||
|
||||
// Dispatch with data
|
||||
function dispatchConfirm() {
|
||||
dispatch('confirm', data);
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === "Escape") {
|
||||
dispatchClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 501;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
display: flex;
|
||||
width: 75%;
|
||||
margin: 2% auto auto auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
.modal-wrapper {
|
||||
width: 96%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 500;
|
||||
background-color: #000;
|
||||
opacity: .5;
|
||||
}
|
||||
</style>
|
@ -309,6 +309,10 @@
|
||||
|
||||
data.emote = data.emote;
|
||||
|
||||
if (!data.colour) {
|
||||
data.colour = 0x2ECC71;
|
||||
}
|
||||
|
||||
tempColour = intToColour(data.colour);
|
||||
}
|
||||
|
||||
|
@ -1,100 +0,0 @@
|
||||
<div class="modal" transition:fade bind:this={modal}>
|
||||
<div class="modal-wrapper">
|
||||
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||
<span slot="title">Edit Panel</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
<PanelCreationForm {guildId} {channels} {roles} {emojis} {teams} {forms} {isPremium} bind:data={panel} seedDefault={false} />
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<Button danger={true} on:click={dispatchClose}>Cancel</Button>
|
||||
<div style="margin-left: 12px">
|
||||
<Button icon="fas fa-paper-plane" on:click={dispatchConfirm}>Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" transition:fade>
|
||||
</div>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown}/>
|
||||
|
||||
<script>
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
import {fade} from 'svelte/transition'
|
||||
import PanelCreationForm from "./PanelCreationForm.svelte";
|
||||
import Card from "../Card.svelte";
|
||||
import Button from "../Button.svelte";
|
||||
|
||||
export let modal;
|
||||
|
||||
export let guildId;
|
||||
export let panel = {};
|
||||
export let channels = [];
|
||||
export let forms = [];
|
||||
export let roles = [];
|
||||
export let emojis = [];
|
||||
export let teams = []
|
||||
export let isPremium = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function dispatchClose() {
|
||||
dispatch('close', {});
|
||||
}
|
||||
|
||||
// Dispatch with data
|
||||
function dispatchConfirm() {
|
||||
let form_id = (panel.form_id === null || panel.form_id === "null") ? null : parseInt(panel.form_id);
|
||||
let mapped = {...panel, form_id: form_id};
|
||||
dispatch('confirm', mapped);
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === "Escape") {
|
||||
dispatchClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 501;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
display: flex;
|
||||
width: 75%;
|
||||
margin: 2% auto auto auto;
|
||||
padding-bottom: 5%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
.modal-wrapper {
|
||||
width: 96%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 500;
|
||||
background-color: #000;
|
||||
opacity: .5;
|
||||
}
|
||||
</style>
|
@ -16,7 +16,7 @@
|
||||
</NavElement>
|
||||
|
||||
{#if isAdmin}
|
||||
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Reaction Panels</NavElement>
|
||||
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Ticket 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}>Staff Teams</NavElement>
|
||||
<NavElement icon="fas fa-robot" link="/manage/{guildId}/integrations" on:click={closeDropdown}>
|
||||
|
78
frontend/src/js/common.js
Normal file
78
frontend/src/js/common.js
Normal file
@ -0,0 +1,78 @@
|
||||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
|
||||
export async function loadPremium(guildId, includeVoting = false) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/premium?include_voting=${includeVoting}`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load premium status: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data.premium;
|
||||
}
|
||||
|
||||
export async function loadChannels(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/channels`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load channels: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function loadPanels(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/panels`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load panels: ${res.data.error}`);
|
||||
}
|
||||
|
||||
// convert button_style and form_id to string
|
||||
return res.data.map((p) => Object.assign({}, p, {
|
||||
button_style: p.button_style.toString(),
|
||||
form_id: p.form_id === null ? "null" : p.form_id
|
||||
}));
|
||||
}
|
||||
|
||||
export async function loadMultiPanels(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/multipanels`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load multi-panels: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
export async function loadTeams(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/team`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load teams: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function loadRoles(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/roles`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load roles: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data.roles;
|
||||
}
|
||||
|
||||
export async function loadEmojis(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/emojis`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load emojis: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function loadForms(guildId) {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/forms`);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Failed to load forms: ${res.data.error}`);
|
||||
}
|
||||
|
||||
return res.data || [];
|
||||
}
|
@ -38,9 +38,59 @@ export function colourToInt(colour) {
|
||||
}
|
||||
|
||||
export function intToColour(i) {
|
||||
return `#${i.toString(16)}`
|
||||
return `#${i.toString(16).padStart(6, '0')}`
|
||||
}
|
||||
|
||||
export function nullIfBlank(s) {
|
||||
return s === '' ? null : s;
|
||||
}
|
||||
|
||||
export function setBlankStringsToNull(obj) {
|
||||
// Set all blank strings in the object, including nested objects, to null
|
||||
for (const key in obj) {
|
||||
if (obj[key] === "" || obj[key] === "null") {
|
||||
obj[key] = null;
|
||||
} else if (typeof obj[key] === "object") {
|
||||
setBlankStringsToNull(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function removeBlankEmbedFields(obj) {
|
||||
for (const key in obj) {
|
||||
if (obj[key] === null || obj[key] === undefined) {
|
||||
delete obj[key];
|
||||
}
|
||||
|
||||
if (typeof obj[key] === "string" && obj[key] === "") {
|
||||
delete obj[key];
|
||||
}
|
||||
|
||||
if (typeof obj[key] === "object") {
|
||||
removeBlankEmbedFields(obj[key]);
|
||||
}
|
||||
|
||||
if (Array.isArray(obj[key]) && obj[key].length === 0) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === "object" && Object.keys(obj[key]).length === 0) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function checkForParamAndRewrite(param) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get(param) === "true") {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.delete(param);
|
||||
|
||||
window.history.pushState(null, '', newUrl.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import Error404 from './views/Error404.svelte'
|
||||
import Transcripts from './views/Transcripts.svelte'
|
||||
import TranscriptView from './views/TranscriptView.svelte'
|
||||
import Blacklist from './views/Blacklist.svelte'
|
||||
import Panels from './views/Panels.svelte'
|
||||
import Panels from './views/panels/Panels.svelte'
|
||||
import Tags from './views/Tags.svelte'
|
||||
import Teams from './views/Teams.svelte'
|
||||
import Tickets from './views/Tickets.svelte'
|
||||
@ -30,6 +30,10 @@ import IntegrationCreate from "./views/integrations/Create.svelte";
|
||||
import IntegrationConfigure from "./views/integrations/Configure.svelte";
|
||||
import IntegrationActivate from "./views/integrations/Activate.svelte";
|
||||
import IntegrationManage from "./views/integrations/Manage.svelte";
|
||||
import CreatePanel from "./views/panels/CreatePanel.svelte";
|
||||
import CreateMultiPanel from "./views/panels/CreateMultiPanel.svelte";
|
||||
import EditPanel from "./views/panels/EditPanel.svelte";
|
||||
import EditMultiPanel from "./views/panels/EditMultiPanel.svelte";
|
||||
|
||||
export const routes = [
|
||||
{name: '/', component: Index, layout: IndexLayout},
|
||||
@ -77,7 +81,36 @@ export const routes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{name: 'panels', component: Panels, layout: ManageLayout},
|
||||
{
|
||||
name: 'panels',
|
||||
nestedRoutes: [
|
||||
{
|
||||
name: 'index',
|
||||
component: Panels,
|
||||
layout: ManageLayout
|
||||
},
|
||||
{
|
||||
name: 'create',
|
||||
component: CreatePanel,
|
||||
layout: ManageLayout
|
||||
},
|
||||
{
|
||||
name: 'create-multi',
|
||||
component: CreateMultiPanel,
|
||||
layout: ManageLayout
|
||||
},
|
||||
{
|
||||
name: 'edit/:panelid',
|
||||
component: EditPanel,
|
||||
layout: ManageLayout
|
||||
},
|
||||
{
|
||||
name: 'edit-multi/:panelid',
|
||||
component: EditMultiPanel,
|
||||
layout: ManageLayout
|
||||
}
|
||||
]
|
||||
},
|
||||
{name: 'blacklist', component: Blacklist, layout: ManageLayout},
|
||||
{name: 'tags', component: Tags, layout: ManageLayout},
|
||||
{name: 'teams', component: Teams, layout: ManageLayout},
|
||||
|
@ -1,490 +0,0 @@
|
||||
{#if editModal}
|
||||
<PanelEditModal bind:modal={editModalElement} {guildId} {channels} {roles} {emojis} {teams} {forms} {isPremium} bind:panel={editData}
|
||||
on:close={() => editModal = false} on:confirm={submitEdit}/>
|
||||
{/if}
|
||||
|
||||
{#if multiEditModal}
|
||||
<MultiPanelEditModal {guildId} {channels} {panels} data={multiPanelEditData}
|
||||
on:close={() => multiEditModal = false} on:confirm={submitMultiPanelEdit}/>
|
||||
{/if}
|
||||
|
||||
{#if panelToDelete !== null}
|
||||
<ConfirmationModal icon="fas fa-trash-can" isDangerous on:cancel={() => panelToDelete = null}
|
||||
on:confirm={() => deletePanel(panelToDelete.panel_id)}>
|
||||
<span slot="body">Are you sure you want to delete the panel {panelToDelete.title}?</span>
|
||||
<span slot="confirm">Delete</span>
|
||||
</ConfirmationModal>
|
||||
{/if}
|
||||
|
||||
{#if multiPanelToDelete !== null}
|
||||
<ConfirmationModal icon="fas fa-trash-can" isDangerous on:cancel={() => multiPanelToDelete = null}
|
||||
on:confirm={() => deleteMultiPanel(multiPanelToDelete.id)}>
|
||||
<span slot="body">Are you sure you want to delete the multi-panel {multiPanelToDelete.title}?</span>
|
||||
<span slot="confirm">Delete</span>
|
||||
</ConfirmationModal>
|
||||
{/if}
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="col-main">
|
||||
<div class="row">
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Your Reaction Panels</span>
|
||||
<div slot="body" class="card-body">
|
||||
<p>Your panel quota: <b>{panels.length} / {isPremium ? '∞' : '3'}</b></p>
|
||||
|
||||
<table style="margin-top: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th>Panel Title</th>
|
||||
<th class="category-col">Ticket Channel Category</th>
|
||||
<th>Resend</th>
|
||||
<th>Edit</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each panels as panel}
|
||||
<tr>
|
||||
<td>#{channels.find((c) => c.id === panel.channel_id)?.name ?? 'Unknown Channel'}</td>
|
||||
<td>{panel.title}</td>
|
||||
<td class="category-col">{channels.find((c) => c.id === panel.category_id)?.name ?? 'Unknown Category'}</td>
|
||||
<td>
|
||||
<Button disabled={panel.force_disabled} on:click={() => resendPanel(panel.panel_id)}>Resend</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Button disabled={panel.force_disabled} on:click={() => openEditModal(panel.panel_id)}>Edit</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Button on:click={() => panelToDelete = panel}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Create Panel</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
{#if !$loadingScreen}
|
||||
<PanelCreationForm {guildId} {channels} {roles} {emojis} {teams} {forms} {isPremium} 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>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-small">
|
||||
<div class="row">
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Your Multi-Panels</span>
|
||||
<div slot="body" class="card-body">
|
||||
<table style="margin-top: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Embed Title</th>
|
||||
<th>Resend</th>
|
||||
<th>Edit</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each multiPanels as panel}
|
||||
<tr>
|
||||
<td>{panel.title}</td>
|
||||
<td>
|
||||
<Button on:click={() => resendMultiPanel(panel.id)}>Resend</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Button on:click={() => openMultiEditModal(panel.id)}>Edit</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Button on:click={() => multiPanelToDelete = panel}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Card footer={false}>
|
||||
<span slot="title">Create Multi-Panel</span>
|
||||
<div slot="body" class="card-body">
|
||||
<p>Note: The panels which you wish to combine into a multi-panel must already exist</p>
|
||||
|
||||
{#if !$loadingScreen}
|
||||
<div style="margin-top: 10px">
|
||||
<MultiPanelCreationForm {guildId} {channels} {panels} bind:data={multiPanelCreateData}/>
|
||||
<div style="display: flex; justify-content: center; margin-top: 2%">
|
||||
<Button icon="fas fa-paper-plane" fullWidth={true} on:click={createMultiPanel}>Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import Card from "../components/Card.svelte";
|
||||
import {notifyError, notifySuccess, withLoadingScreen} from "../js/util";
|
||||
import {loadingScreen} from "../js/stores";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../js/constants";
|
||||
import {setDefaultHeaders} from '../includes/Auth.svelte'
|
||||
import Button from "../components/Button.svelte";
|
||||
import PanelEditModal from "../components/manage/PanelEditModal.svelte";
|
||||
import PanelCreationForm from "../components/manage/PanelCreationForm.svelte";
|
||||
import MultiPanelCreationForm from '../components/manage/MultiPanelCreationForm.svelte';
|
||||
import MultiPanelEditModal from "../components/manage/MultiPanelEditModal.svelte";
|
||||
import ConfirmationModal from "../components/ConfirmationModal.svelte";
|
||||
import {afterUpdate} from "svelte";
|
||||
|
||||
export let currentRoute;
|
||||
export let params = {};
|
||||
|
||||
setDefaultHeaders()
|
||||
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
|
||||
let channels = [];
|
||||
let roles = [];
|
||||
let emojis = [];
|
||||
let teams = [];
|
||||
let forms = [];
|
||||
let panels = [];
|
||||
let multiPanels = [];
|
||||
let isPremium = false;
|
||||
|
||||
let editModal = false;
|
||||
let multiEditModal = false;
|
||||
let panelToDelete = null;
|
||||
let multiPanelToDelete = null;
|
||||
|
||||
let panelCreateData;
|
||||
let editData;
|
||||
let multiPanelCreateData;
|
||||
let multiPanelEditData;
|
||||
|
||||
let editModalElement;
|
||||
|
||||
function openEditModal(panelId) {
|
||||
editData = panels.find((p) => p.panel_id === panelId);
|
||||
editModal = true;
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function openMultiEditModal(id) {
|
||||
multiPanelEditData = multiPanels.find((mp) => mp.id === id);
|
||||
multiEditModal = true;
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function resendPanel(panelId) {
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/panels/${panelId}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
notifySuccess("Panel resent successfully");
|
||||
}
|
||||
|
||||
async function deletePanel(panelId) {
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/panels/${panelId}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
panels = panels.filter((p) => p.panel_id !== panelId);
|
||||
panelToDelete = null;
|
||||
}
|
||||
|
||||
async function resendMultiPanel(id) {
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/multipanels/${id}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
notifySuccess("Multipanel resent successfully")
|
||||
}
|
||||
|
||||
async function deleteMultiPanel(id) {
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/multipanels/${id}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
multiPanels = multiPanels.filter((p) => p.id !== id);
|
||||
multiPanelToDelete = null;
|
||||
}
|
||||
|
||||
async function createPanel() {
|
||||
setBlankStringsToNull(panelCreateData);
|
||||
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/panels`, panelCreateData);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
await loadPanels();
|
||||
notifySuccess('Panel created successfully');
|
||||
}
|
||||
|
||||
async function submitEdit(e) {
|
||||
let data = e.detail;
|
||||
setBlankStringsToNull(data);
|
||||
|
||||
const res = await axios.patch(`${API_URL}/api/${guildId}/panels/${data.panel_id}`, data);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
} else {
|
||||
editModal = false;
|
||||
editData = {};
|
||||
notifySuccess('Panel updated successfully');
|
||||
}
|
||||
|
||||
await loadPanels();
|
||||
}
|
||||
|
||||
function setBlankStringsToNull(obj) {
|
||||
// Set all blank strings in the object, including nested objects, to null
|
||||
for (const key in obj) {
|
||||
if (obj[key] === "" || obj[key] === "null") {
|
||||
obj[key] = null;
|
||||
} else if (typeof obj[key] === "object") {
|
||||
setBlankStringsToNull(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function submitMultiPanelEdit(e) {
|
||||
let data = e.detail;
|
||||
|
||||
const res = await axios.patch(`${API_URL}/api/${guildId}/multipanels/${data.id}`, data);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
} else {
|
||||
multiEditModal = false;
|
||||
multiPanelEditData = {};
|
||||
notifySuccess('Multi-panel updated successfully');
|
||||
}
|
||||
|
||||
await loadPanels();
|
||||
}
|
||||
|
||||
async function createMultiPanel() {
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/multipanels`, multiPanelCreateData);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
} else {
|
||||
notifySuccess('Multi-panel created successfully');
|
||||
}
|
||||
|
||||
await loadMultiPanels();
|
||||
}
|
||||
|
||||
async function loadPremium() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/premium?include_voting=false`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
isPremium = res.data.premium;
|
||||
}
|
||||
|
||||
async function loadChannels() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/channels`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
channels = res.data;
|
||||
}
|
||||
|
||||
async function loadPanels() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/panels`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// convert button_style and form_id to string
|
||||
panels = res.data.map((p) => Object.assign({}, p, {
|
||||
button_style: p.button_style.toString(),
|
||||
form_id: p.form_id === null ? "null" : p.form_id
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadMultiPanels() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/multipanels`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
multiPanels = res.data.data;
|
||||
}
|
||||
|
||||
async function loadTeams() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/team`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
teams = res.data;
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/roles`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
roles = res.data.roles;
|
||||
}
|
||||
|
||||
async function loadEmojis() {
|
||||
const res = await axios.get(`${API_URL}/api/${guildId}/emojis`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
emojis = res.data;
|
||||
}
|
||||
|
||||
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 || [];
|
||||
}
|
||||
|
||||
withLoadingScreen(async () => {
|
||||
await Promise.all([
|
||||
loadPremium(),
|
||||
loadChannels(),
|
||||
loadTeams(),
|
||||
loadForms(),
|
||||
loadRoles(),
|
||||
loadEmojis(),
|
||||
loadPanels(),
|
||||
loadMultiPanels()
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.col-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 65%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.col-small {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 35%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
width: 96%;
|
||||
height: 100%;
|
||||
margin-bottom: 2%;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1100px) {
|
||||
.wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col-main, .col-small {
|
||||
width: 100%;
|
||||
margin-bottom: 4%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.category-col {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 0 10px 10px;
|
||||
}
|
||||
</style>
|
95
frontend/src/views/panels/CreateMultiPanel.svelte
Normal file
95
frontend/src/views/panels/CreateMultiPanel.svelte
Normal file
@ -0,0 +1,95 @@
|
||||
<main>
|
||||
<a href="/manage/{guildId}/panels" class="link">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Panels
|
||||
</a>
|
||||
<Card footer={false}>
|
||||
<span slot="title">Create Multi-Panel</span>
|
||||
<div slot="body" class="card-body">
|
||||
<p>Note: The panels which you wish to combine into a multi-panel must already exist</p>
|
||||
|
||||
{#if !$loadingScreen}
|
||||
<div style="margin-top: 10px">
|
||||
<MultiPanelCreationForm {guildId} {channels} {panels} bind:data={multiPanelCreateData}/>
|
||||
|
||||
<div class="submit-wrapper">
|
||||
<Button icon="fas fa-paper-plane" fullWidth={true} on:click={createMultiPanel}>Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2% 10% 4% 10%;
|
||||
width: 100%;
|
||||
row-gap: 1vh;
|
||||
}
|
||||
|
||||
main > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-wrapper {
|
||||
margin: 1vh auto auto;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {loadingScreen} from "../../js/stores";
|
||||
import MultiPanelCreationForm from "../../components/manage/MultiPanelCreationForm.svelte";
|
||||
import Button from "../../components/Button.svelte";
|
||||
import Card from "../../components/Card.svelte";
|
||||
import {onMount} from "svelte";
|
||||
import {notifyError, removeBlankEmbedFields, setBlankStringsToNull, withLoadingScreen} from "../../js/util";
|
||||
import {loadChannels, loadPanels} from "../../js/common";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../../js/constants";
|
||||
import {navigateTo} from "svelte-router-spa";
|
||||
|
||||
export let currentRoute;
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
|
||||
let channels = [];
|
||||
let panels = [];
|
||||
|
||||
let multiPanelCreateData;
|
||||
|
||||
async function createMultiPanel() {
|
||||
const data = structuredClone(multiPanelCreateData);
|
||||
|
||||
setBlankStringsToNull(data);
|
||||
removeBlankEmbedFields(data);
|
||||
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/multipanels`, data);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
} else {
|
||||
navigateTo(`/manage/${guildId}/panels?created=true`)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await withLoadingScreen(async () => {
|
||||
await Promise.all([
|
||||
loadChannels(guildId).then(r => channels = r).catch(e => notifyError(e)),
|
||||
loadPanels(guildId).then(r => panels = r).catch(e => notifyError(e)),
|
||||
])
|
||||
});
|
||||
});
|
||||
</script>
|
99
frontend/src/views/panels/CreatePanel.svelte
Normal file
99
frontend/src/views/panels/CreatePanel.svelte
Normal file
@ -0,0 +1,99 @@
|
||||
<main>
|
||||
<a href="/manage/{guildId}/panels" class="link">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Panels
|
||||
</a>
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Create Panel</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
{#if !$loadingScreen}
|
||||
<PanelCreationForm {guildId} {channels} {roles} {emojis} {teams} {forms} {isPremium}
|
||||
bind:data={panelCreateData}/>
|
||||
<div class="submit-wrapper">
|
||||
<Button icon="fas fa-paper-plane" fullWidth={true} on:click={createPanel}>Create</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2% 10% 4% 10%;
|
||||
width: 100%;
|
||||
row-gap: 1vh;
|
||||
}
|
||||
|
||||
main > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.submit-wrapper {
|
||||
margin: 1vh auto auto;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {loadingScreen} from "../../js/stores";
|
||||
import Button from "../../components/Button.svelte";
|
||||
import Card from "../../components/Card.svelte";
|
||||
import PanelCreationForm from "../../components/manage/PanelCreationForm.svelte";
|
||||
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||
import {notifyError, setBlankStringsToNull, withLoadingScreen} from "../../js/util";
|
||||
import {onMount} from "svelte";
|
||||
import {loadChannels, loadEmojis, loadForms, loadPremium, loadRoles, loadTeams} from "../../js/common";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../../js/constants";
|
||||
import {Navigate, navigateTo} from "svelte-router-spa";
|
||||
|
||||
setDefaultHeaders();
|
||||
|
||||
export let currentRoute;
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
|
||||
let channels = [];
|
||||
let roles = [];
|
||||
let emojis = [];
|
||||
let teams = [];
|
||||
let forms = [];
|
||||
let isPremium = false;
|
||||
|
||||
let panelCreateData;
|
||||
|
||||
async function createPanel() {
|
||||
setBlankStringsToNull(panelCreateData);
|
||||
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/panels`, panelCreateData);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo(`/manage/${guildId}/panels?created=true`);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await withLoadingScreen(async () => {
|
||||
await Promise.all([
|
||||
loadChannels(guildId).then(r => channels = r).catch(e => notifyError(e)),
|
||||
loadRoles(guildId).then(r => roles = r).catch(e => notifyError(e)),
|
||||
loadEmojis(guildId).then(r => emojis = r).catch(e => notifyError(e)),
|
||||
loadTeams(guildId).then(r => teams = r).catch(e => notifyError(e)),
|
||||
loadForms(guildId).then(r => forms = r).catch(e => notifyError(e)),
|
||||
loadPremium(guildId, false).then(r => isPremium = r).catch(e => notifyError(e)),
|
||||
])
|
||||
});
|
||||
});
|
||||
</script>
|
104
frontend/src/views/panels/EditMultiPanel.svelte
Normal file
104
frontend/src/views/panels/EditMultiPanel.svelte
Normal file
@ -0,0 +1,104 @@
|
||||
<main>
|
||||
<a href="/manage/{guildId}/panels" class="link">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Panels
|
||||
</a>
|
||||
<Card footer={false}>
|
||||
<span slot="title">Create Multi-Panel</span>
|
||||
<div slot="body" class="card-body">
|
||||
<p>Note: The panels which you wish to combine into a multi-panel must already exist</p>
|
||||
|
||||
{#if multiPanelData && !$loadingScreen}
|
||||
<div style="margin-top: 10px">
|
||||
<MultiPanelCreationForm {guildId} {channels} {panels} bind:data={multiPanelData} seedDefault={false} />
|
||||
|
||||
<div class="submit-wrapper">
|
||||
<Button icon="fas fa-floppy-disk" fullWidth={true} on:click={editMultiPanel}>Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2% 10% 4% 10%;
|
||||
width: 100%;
|
||||
row-gap: 1vh;
|
||||
}
|
||||
|
||||
main > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-wrapper {
|
||||
margin: 1vh auto auto;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {loadingScreen} from "../../js/stores";
|
||||
import MultiPanelCreationForm from "../../components/manage/MultiPanelCreationForm.svelte";
|
||||
import Button from "../../components/Button.svelte";
|
||||
import Card from "../../components/Card.svelte";
|
||||
import {onMount} from "svelte";
|
||||
import {notifyError, removeBlankEmbedFields, setBlankStringsToNull, withLoadingScreen} from "../../js/util";
|
||||
import {loadChannels, loadPanels, loadMultiPanels} from "../../js/common";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../../js/constants";
|
||||
import {navigateTo} from "svelte-router-spa";
|
||||
|
||||
export let currentRoute;
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
let multiPanelId = parseInt(currentRoute.namedParams.panelid);
|
||||
|
||||
let channels = [];
|
||||
let panels = [];
|
||||
|
||||
let multiPanelData;
|
||||
|
||||
async function editMultiPanel() {
|
||||
const data = structuredClone(multiPanelData);
|
||||
|
||||
setBlankStringsToNull(data);
|
||||
removeBlankEmbedFields(data);
|
||||
|
||||
const res = await axios.patch(`${API_URL}/api/${guildId}/multipanels/${multiPanelId}`, data);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
} else {
|
||||
navigateTo(`/manage/${guildId}/panels?edited=true`)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await withLoadingScreen(async () => {
|
||||
let multiPanels = [];
|
||||
|
||||
await Promise.all([
|
||||
loadChannels(guildId).then(r => channels = r).catch(e => notifyError(e)),
|
||||
loadPanels(guildId).then(r => panels = r).catch(e => notifyError(e)),
|
||||
loadMultiPanels(guildId).then(r => multiPanels = r).catch(e => notifyError(e))
|
||||
]);
|
||||
|
||||
multiPanelData = multiPanels.find(mp => mp.id === multiPanelId);
|
||||
if (!multiPanelData) {
|
||||
navigateTo(`/manage/${guildId}/panels?notfound=true`)
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
108
frontend/src/views/panels/EditPanel.svelte
Normal file
108
frontend/src/views/panels/EditPanel.svelte
Normal file
@ -0,0 +1,108 @@
|
||||
<main>
|
||||
<a href="/manage/{guildId}/panels" class="link">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Panels
|
||||
</a>
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Create Panel</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
{#if !$loadingScreen}
|
||||
<PanelCreationForm {guildId} {channels} {roles} {emojis} {teams} {forms} {isPremium}
|
||||
bind:data={panelData} seedDefault={false} />
|
||||
<div class="submit-wrapper">
|
||||
<Button icon="fas fa-floppy-disk" fullWidth={true} on:click={editPanel}>Save</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2% 10% 4% 10%;
|
||||
width: 100%;
|
||||
row-gap: 1vh;
|
||||
}
|
||||
|
||||
main > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.submit-wrapper {
|
||||
margin: 1vh auto auto;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {loadingScreen} from "../../js/stores";
|
||||
import Button from "../../components/Button.svelte";
|
||||
import Card from "../../components/Card.svelte";
|
||||
import PanelCreationForm from "../../components/manage/PanelCreationForm.svelte";
|
||||
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||
import {notifyError, notifySuccess, setBlankStringsToNull, withLoadingScreen} from "../../js/util";
|
||||
import {onMount} from "svelte";
|
||||
import {loadChannels, loadEmojis, loadForms, loadPanels, loadPremium, loadRoles, loadTeams} from "../../js/common";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../../js/constants";
|
||||
import {Navigate, navigateTo} from "svelte-router-spa";
|
||||
|
||||
setDefaultHeaders();
|
||||
|
||||
export let currentRoute;
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
let panelId = parseInt(currentRoute.namedParams.panelid);
|
||||
|
||||
let channels = [];
|
||||
let roles = [];
|
||||
let emojis = [];
|
||||
let teams = [];
|
||||
let forms = [];
|
||||
let isPremium = false;
|
||||
|
||||
let panelData;
|
||||
|
||||
async function editPanel() {
|
||||
setBlankStringsToNull(panelData);
|
||||
|
||||
const res = await axios.patch(`${API_URL}/api/${guildId}/panels/${panelId}`, panelData);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo(`/manage/${guildId}/panels?edited=true`);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await withLoadingScreen(async () => {
|
||||
let panels = [];
|
||||
|
||||
await Promise.all([
|
||||
loadChannels(guildId).then(r => channels = r).catch(e => notifyError(e)),
|
||||
loadRoles(guildId).then(r => roles = r).catch(e => notifyError(e)),
|
||||
loadEmojis(guildId).then(r => emojis = r).catch(e => notifyError(e)),
|
||||
loadTeams(guildId).then(r => teams = r).catch(e => notifyError(e)),
|
||||
loadForms(guildId).then(r => forms = r).catch(e => notifyError(e)),
|
||||
loadPremium(guildId, false).then(r => isPremium = r).catch(e => notifyError(e)),
|
||||
loadPanels(guildId).then(r => panels = r).catch(e => notifyError(e))
|
||||
]);
|
||||
|
||||
panelData = panels.find(p => p.panel_id === panelId);
|
||||
if (!panelData) {
|
||||
navigateTo(`/manage/${guildId}/panels?notfound=true`);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
295
frontend/src/views/panels/Panels.svelte
Normal file
295
frontend/src/views/panels/Panels.svelte
Normal file
@ -0,0 +1,295 @@
|
||||
{#if panelToDelete !== null}
|
||||
<ConfirmationModal icon="fas fa-trash-can" isDangerous on:cancel={() => panelToDelete = null}
|
||||
on:confirm={() => deletePanel(panelToDelete.panel_id)}>
|
||||
<span slot="body">Are you sure you want to delete the panel {panelToDelete.title}?</span>
|
||||
<span slot="confirm">Delete</span>
|
||||
</ConfirmationModal>
|
||||
{/if}
|
||||
|
||||
{#if multiPanelToDelete !== null}
|
||||
<ConfirmationModal icon="fas fa-trash-can" isDangerous on:cancel={() => multiPanelToDelete = null}
|
||||
on:confirm={() => deleteMultiPanel(multiPanelToDelete.id)}>
|
||||
<span slot="body">Are you sure you want to delete the multi-panel
|
||||
{multiPanelToDelete.embed?.title || "Open a ticket!"}?</span>
|
||||
<span slot="confirm">Delete</span>
|
||||
</ConfirmationModal>
|
||||
{/if}
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Ticket Panels</span>
|
||||
<div slot="body" class="card-body panels">
|
||||
<div class="controls">
|
||||
<p>Your panel quota: <b>{panels.length} / {isPremium ? '∞' : '3'}</b></p>
|
||||
<Navigate to="/manage/{guildId}/panels/create" styles="link">
|
||||
<Button icon="fas fa-plus">New Panel</Button>
|
||||
</Navigate>
|
||||
</div>
|
||||
|
||||
<table style="margin-top: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Channel</th>
|
||||
<th class="max">Panel Title</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each panels as panel}
|
||||
<tr>
|
||||
<td>#{channels.find((c) => c.id === panel.channel_id)?.name ?? 'Unknown Channel'}</td>
|
||||
<td class="max">{panel.title}</td>
|
||||
<td>
|
||||
<Button disabled={panel.force_disabled}
|
||||
on:click={() => resendPanel(panel.panel_id)}>Resend
|
||||
</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Navigate to="/manage/{guildId}/panels/edit/{panel.panel_id}" styles="link">
|
||||
<Button disabled={panel.force_disabled}>Edit</Button>
|
||||
</Navigate>
|
||||
</td>
|
||||
<td>
|
||||
<Button danger on:click={() => panelToDelete = panel}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<Card footer="{false}">
|
||||
<span slot="title">Multi-Panels</span>
|
||||
<div slot="body" class="card-body">
|
||||
<div class="controls">
|
||||
<Navigate to="/manage/{guildId}/panels/create-multi" styles="link">
|
||||
<Button icon="fas fa-plus">New Multi-Panel</Button>
|
||||
</Navigate>
|
||||
</div>
|
||||
|
||||
<table style="margin-top: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="max">Panel Title</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each multiPanels as panel}
|
||||
<tr>
|
||||
<td class="max">{panel.title || 'Open a ticket!'}</td>
|
||||
<td>
|
||||
<Button on:click={() => resendMultiPanel(panel.id)}>Resend</Button>
|
||||
</td>
|
||||
<td>
|
||||
<Navigate to="/manage/{guildId}/panels/edit-multi/{panel.id}" styles="link">
|
||||
<Button>Edit</Button>
|
||||
</Navigate>
|
||||
</td>
|
||||
<td>
|
||||
<Button danger on:click={() => multiPanelToDelete = panel}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import Card from "../../components/Card.svelte";
|
||||
import {checkForParamAndRewrite, notifyError, notifySuccess, withLoadingScreen} from "../../js/util";
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../../js/constants";
|
||||
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||
import Button from "../../components/Button.svelte";
|
||||
import ConfirmationModal from "../../components/ConfirmationModal.svelte";
|
||||
import {Navigate} from "svelte-router-spa";
|
||||
import {loadChannels, loadMultiPanels, loadPanels, loadPremium} from "../../js/common";
|
||||
|
||||
export let currentRoute;
|
||||
|
||||
setDefaultHeaders()
|
||||
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
|
||||
let channels = [];
|
||||
let panels = [];
|
||||
let multiPanels = [];
|
||||
let isPremium = false;
|
||||
|
||||
let panelToDelete = null;
|
||||
let multiPanelToDelete = null;
|
||||
|
||||
async function resendPanel(panelId) {
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/panels/${panelId}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
notifySuccess("Panel resent successfully");
|
||||
}
|
||||
|
||||
async function deletePanel(panelId) {
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/panels/${panelId}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
panels = panels.filter((p) => p.panel_id !== panelId);
|
||||
panelToDelete = null;
|
||||
}
|
||||
|
||||
async function resendMultiPanel(id) {
|
||||
const res = await axios.post(`${API_URL}/api/${guildId}/multipanels/${id}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
notifySuccess("Multipanel resent successfully")
|
||||
}
|
||||
|
||||
async function deleteMultiPanel(id) {
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/multipanels/${id}`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
multiPanels = multiPanels.filter((p) => p.id !== id);
|
||||
multiPanelToDelete = null;
|
||||
}
|
||||
|
||||
withLoadingScreen(async () => {
|
||||
await Promise.all([
|
||||
loadChannels(guildId).then(r => channels = r).catch(e => notifyError(e)),
|
||||
loadPremium(guildId, false).then(r => isPremium = r).catch(e => notifyError(e)),
|
||||
loadPanels(guildId).then(r => panels = r).catch(e => notifyError(e)),
|
||||
loadMultiPanels(guildId).then(r => multiPanels = r).catch(e => notifyError(e))
|
||||
])
|
||||
|
||||
if (checkForParamAndRewrite("created")) {
|
||||
notifySuccess("Panel created successfully");
|
||||
}
|
||||
|
||||
if (checkForParamAndRewrite("edited")) {
|
||||
notifySuccess("Panel edited successfully");
|
||||
}
|
||||
|
||||
if (checkForParamAndRewrite("notfound")) {
|
||||
notifyError("Panel not found");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
width: 96%;
|
||||
margin-bottom: 2%;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-body.panels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 4%;
|
||||
}
|
||||
|
||||
.card-body > .controls {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
gap: 2%;
|
||||
}
|
||||
|
||||
.card-body.panels > .controls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1100px) {
|
||||
.wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
th:not(.max), td:not(.max) {
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
2
go.mod
2
go.mod
@ -8,7 +8,7 @@ require (
|
||||
github.com/BurntSushi/toml v1.2.1
|
||||
github.com/TicketsBot/archiverclient v0.0.0-20240613013458-accc062facc2
|
||||
github.com/TicketsBot/common v0.0.0-20240710005307-9cc26f78d8e3
|
||||
github.com/TicketsBot/database v0.0.0-20240720222825-35466fd5fc96
|
||||
github.com/TicketsBot/database v0.0.0-20240729222446-2c671b9b9366
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
||||
github.com/TicketsBot/worker v0.0.0-20240720223640-84817ecc3309
|
||||
github.com/apex/log v1.1.2
|
||||
|
2
go.sum
2
go.sum
@ -54,6 +54,8 @@ github.com/TicketsBot/database v0.0.0-20240622123318-f2e43bd962cb/go.mod h1:gAtO
|
||||
github.com/TicketsBot/database v0.0.0-20240715172214-86ee61a85834/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
||||
github.com/TicketsBot/database v0.0.0-20240720222825-35466fd5fc96 h1:iOf8LxG3y7v5d5FErpc2i9gRzYN5wJzbd63iBTIllYY=
|
||||
github.com/TicketsBot/database v0.0.0-20240720222825-35466fd5fc96/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
||||
github.com/TicketsBot/database v0.0.0-20240729222446-2c671b9b9366 h1:KC7NWVXk8xX2M4rQUVoHTuTjkXumGIDRrUS741lfcD4=
|
||||
github.com/TicketsBot/database v0.0.0-20240729222446-2c671b9b9366/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
||||
|
@ -17,6 +17,12 @@ func (c Colour) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (c *Colour) UnmarshalJSON(b []byte) error {
|
||||
// Try to parse as int first
|
||||
if parsed, err := strconv.ParseUint(string(b), 10, 32); err == nil {
|
||||
*c = Colour(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(b) < 2 {
|
||||
return fmt.Errorf("invalid colour")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user