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"
|
||||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils/types"
|
||||||
"github.com/TicketsBot/common/premium"
|
"github.com/TicketsBot/common/premium"
|
||||||
"github.com/TicketsBot/database"
|
"github.com/TicketsBot/database"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/rxdn/gdl/objects/channel"
|
"github.com/rxdn/gdl/objects/channel"
|
||||||
"github.com/rxdn/gdl/rest/request"
|
"github.com/rxdn/gdl/rest/request"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type multiPanelCreateData struct {
|
type multiPanelCreateData struct {
|
||||||
Title string `json:"title"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Colour int32 `json:"colour"`
|
|
||||||
ChannelId uint64 `json:"channel_id,string"`
|
ChannelId uint64 `json:"channel_id,string"`
|
||||||
SelectMenu bool `json:"select_menu"`
|
SelectMenu bool `json:"select_menu"`
|
||||||
|
SelectMenuPlaceholder *string `json:"select_menu_placeholder,omitempty" validate:"omitempty,max=150"`
|
||||||
Panels []int `json:"panels"`
|
Panels []int `json:"panels"`
|
||||||
ImageUrl *string `json:"image_url,omitempty"`
|
Embed *types.CustomEmbed `json:"embed" validate:"omitempty,dive"`
|
||||||
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *multiPanelCreateData) IntoMessageData(isPremium bool) multiPanelMessageData {
|
func (d *multiPanelCreateData) IntoMessageData(isPremium bool) multiPanelMessageData {
|
||||||
return multiPanelMessageData{
|
return multiPanelMessageData{
|
||||||
ChannelId: d.ChannelId,
|
|
||||||
Title: d.Title,
|
|
||||||
Content: d.Content,
|
|
||||||
Colour: int(d.Colour),
|
|
||||||
SelectMenu: d.SelectMenu,
|
|
||||||
IsPremium: isPremium,
|
IsPremium: isPremium,
|
||||||
ImageUrl: d.ImageUrl,
|
ChannelId: d.ChannelId,
|
||||||
ThumbnailUrl: d.ThumbnailUrl,
|
SelectMenu: d.SelectMenu,
|
||||||
|
SelectMenuPlaceholder: d.SelectMenuPlaceholder,
|
||||||
|
Embed: d.Embed.IntoDiscordEmbed(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +45,18 @@ func MultiPanelCreate(ctx *gin.Context) {
|
|||||||
return
|
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
|
// validate body & get sub-panels
|
||||||
panels, err := data.doValidations(guildId)
|
panels, err := data.doValidations(guildId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -86,14 +94,17 @@ func MultiPanelCreate(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbEmbed, dbEmbedFields := data.Embed.IntoDatabaseStruct()
|
||||||
multiPanel := database.MultiPanel{
|
multiPanel := database.MultiPanel{
|
||||||
MessageId: messageId,
|
MessageId: messageId,
|
||||||
ChannelId: data.ChannelId,
|
ChannelId: data.ChannelId,
|
||||||
GuildId: guildId,
|
GuildId: guildId,
|
||||||
Title: data.Title,
|
|
||||||
Content: data.Content,
|
|
||||||
Colour: int(data.Colour),
|
|
||||||
SelectMenu: data.SelectMenu,
|
SelectMenu: data.SelectMenu,
|
||||||
|
SelectMenuPlaceholder: data.SelectMenuPlaceholder,
|
||||||
|
Embed: &database.CustomEmbedWithFields{
|
||||||
|
CustomEmbed: dbEmbed,
|
||||||
|
Fields: dbEmbedFields,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
multiPanel.Id, err = dbclient.Client.MultiPanels.Create(ctx, multiPanel)
|
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) {
|
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, _ := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
group.Go(d.validateTitle)
|
|
||||||
group.Go(d.validateContent)
|
|
||||||
group.Go(d.validateChannel(guildId))
|
group.Go(d.validateChannel(guildId))
|
||||||
group.Go(func() (e error) {
|
group.Go(func() (e error) {
|
||||||
panels, e = d.validatePanels(guildId)
|
panels, e = d.validatePanels(guildId)
|
||||||
@ -137,26 +150,6 @@ func (d *multiPanelCreateData) doValidations(guildId uint64) (panels []database.
|
|||||||
return
|
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 {
|
func (d *multiPanelCreateData) validateChannel(guildId uint64) func() error {
|
||||||
return func() error {
|
return func() error {
|
||||||
// TODO: Use proper context
|
// TODO: Use proper context
|
||||||
|
@ -13,46 +13,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type multiPanelMessageData struct {
|
type multiPanelMessageData struct {
|
||||||
|
IsPremium bool
|
||||||
|
|
||||||
ChannelId uint64
|
ChannelId uint64
|
||||||
|
|
||||||
Title string
|
|
||||||
Content string
|
|
||||||
Colour int
|
|
||||||
SelectMenu bool
|
SelectMenu bool
|
||||||
IsPremium bool
|
SelectMenuPlaceholder *string
|
||||||
ImageUrl, ThumbnailUrl *string
|
|
||||||
|
Embed *embed.Embed
|
||||||
}
|
}
|
||||||
|
|
||||||
func multiPanelIntoMessageData(panel database.MultiPanel, isPremium bool) multiPanelMessageData {
|
func multiPanelIntoMessageData(panel database.MultiPanel, isPremium bool) multiPanelMessageData {
|
||||||
return multiPanelMessageData{
|
return multiPanelMessageData{
|
||||||
ChannelId: panel.ChannelId,
|
|
||||||
Title: panel.Title,
|
|
||||||
Content: panel.Content,
|
|
||||||
Colour: panel.Colour,
|
|
||||||
SelectMenu: panel.SelectMenu,
|
|
||||||
IsPremium: isPremium,
|
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) {
|
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 {
|
if !d.IsPremium {
|
||||||
// TODO: Don't harcode
|
// 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
|
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{
|
components = []component.Component{
|
||||||
component.BuildActionRow(
|
component.BuildActionRow(
|
||||||
component.BuildSelectMenu(
|
component.BuildSelectMenu(
|
||||||
component.SelectMenu{
|
component.SelectMenu{
|
||||||
CustomId: "multipanel",
|
CustomId: "multipanel",
|
||||||
Options: options,
|
Options: options,
|
||||||
Placeholder: "Select a topic...",
|
Placeholder: placeholder,
|
||||||
MinValues: utils.IntPtr(1),
|
MinValues: utils.IntPtr(1),
|
||||||
MaxValues: utils.IntPtr(1),
|
MaxValues: utils.IntPtr(1),
|
||||||
Disabled: false,
|
Disabled: false,
|
||||||
@ -116,7 +109,7 @@ func (d *multiPanelMessageData) send(ctx *botcontext.BotContext, panels []databa
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := rest.CreateMessageData{
|
data := rest.CreateMessageData{
|
||||||
Embeds: []*embed.Embed{e},
|
Embeds: []*embed.Embed{d.Embed},
|
||||||
Components: components,
|
Components: components,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/TicketsBot/common/premium"
|
"github.com/TicketsBot/common/premium"
|
||||||
"github.com/TicketsBot/database"
|
"github.com/TicketsBot/database"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/rxdn/gdl/rest"
|
"github.com/rxdn/gdl/rest"
|
||||||
"github.com/rxdn/gdl/rest/request"
|
"github.com/rxdn/gdl/rest/request"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
@ -53,6 +54,18 @@ func MultiPanelUpdate(c *gin.Context) {
|
|||||||
return
|
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
|
// validate body & get sub-panels
|
||||||
panels, err := data.doValidations(guildId)
|
panels, err := data.doValidations(guildId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -115,17 +128,18 @@ func MultiPanelUpdate(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update DB
|
// update DB
|
||||||
|
dbEmbed, dbEmbedFields := data.Embed.IntoDatabaseStruct()
|
||||||
updated := database.MultiPanel{
|
updated := database.MultiPanel{
|
||||||
Id: multiPanel.Id,
|
Id: multiPanel.Id,
|
||||||
MessageId: messageId,
|
MessageId: messageId,
|
||||||
ChannelId: data.ChannelId,
|
ChannelId: data.ChannelId,
|
||||||
GuildId: guildId,
|
GuildId: guildId,
|
||||||
Title: data.Title,
|
|
||||||
Content: data.Content,
|
|
||||||
Colour: int(data.Colour),
|
|
||||||
SelectMenu: data.SelectMenu,
|
SelectMenu: data.SelectMenu,
|
||||||
ImageUrl: data.ImageUrl,
|
SelectMenuPlaceholder: data.SelectMenuPlaceholder,
|
||||||
ThumbnailUrl: data.ThumbnailUrl,
|
Embed: &database.CustomEmbedWithFields{
|
||||||
|
CustomEmbed: dbEmbed,
|
||||||
|
Fields: dbEmbedFields,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = dbclient.Client.MultiPanels.Update(c, multiPanel.Id, updated); err != nil {
|
if err = dbclient.Client.MultiPanels.Update(c, multiPanel.Id, updated); err != nil {
|
||||||
|
@ -95,14 +95,7 @@ func DeletePanel(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
messageData := multiPanelMessageData{
|
messageData := multiPanelIntoMessageData(multiPanel, premiumTier > premium.None)
|
||||||
Title: multiPanel.Title,
|
|
||||||
Content: multiPanel.Content,
|
|
||||||
Colour: multiPanel.Colour,
|
|
||||||
ChannelId: multiPanel.ChannelId,
|
|
||||||
SelectMenu: multiPanel.SelectMenu,
|
|
||||||
IsPremium: premiumTier > premium.None,
|
|
||||||
}
|
|
||||||
|
|
||||||
messageId, err := messageData.send(botContext, panels)
|
messageId, err := messageData.send(botContext, panels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -306,16 +306,7 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
messageData := multiPanelMessageData{
|
messageData := multiPanelIntoMessageData(multiPanel, premiumTier > premium.None)
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
messageId, err := messageData.send(botContext, panels)
|
messageId, err := messageData.send(botContext, panels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils/types"
|
||||||
"github.com/TicketsBot/database"
|
"github.com/TicketsBot/database"
|
||||||
"github.com/rxdn/gdl/objects/channel"
|
"github.com/rxdn/gdl/objects/channel"
|
||||||
"github.com/rxdn/gdl/objects/guild"
|
"github.com/rxdn/gdl/objects/guild"
|
||||||
@ -289,13 +290,7 @@ func validateNamingScheme(ctx PanelValidationContext) validation.ValidationFunc
|
|||||||
|
|
||||||
func validateWelcomeMessage(ctx PanelValidationContext) validation.ValidationFunc {
|
func validateWelcomeMessage(ctx PanelValidationContext) validation.ValidationFunc {
|
||||||
return func() error {
|
return func() error {
|
||||||
wm := ctx.Data.WelcomeMessage
|
return validateEmbed(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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,3 +334,11 @@ func validateAccessControlList(ctx PanelValidationContext) validation.Validation
|
|||||||
return nil
|
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
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
@ -59,8 +60,8 @@ func CreateTag(ctx *gin.Context) {
|
|||||||
|
|
||||||
// TODO: Limit command amount
|
// TODO: Limit command amount
|
||||||
if err := validate.Struct(data); err != nil {
|
if err := validate.Struct(data); err != nil {
|
||||||
validationErrors, ok := err.(validator.ValidationErrors)
|
var validationErrors validator.ValidationErrors
|
||||||
if !ok {
|
if ok := errors.As(err, &validationErrors); !ok {
|
||||||
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<svelte:window bind:innerWidth />
|
<svelte:window bind:innerWidth />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {onMount} from "svelte";
|
import {afterUpdate, onMount} from "svelte";
|
||||||
import Tooltip from "svelte-tooltip";
|
import Tooltip from "svelte-tooltip";
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
@ -89,9 +89,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// content.addEventListener('DOMNodeInserted', updateIfExpanded);
|
|
||||||
// content.addEventListener('DOMNodeRemoved', updateIfExpanded);
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
updateIfExpanded();
|
updateIfExpanded();
|
||||||
setTimeout(updateIfExpanded, 300); // TODO: Move with transition height
|
setTimeout(updateIfExpanded, 300); // TODO: Move with transition height
|
||||||
@ -101,6 +98,8 @@
|
|||||||
|
|
||||||
if (defaultOpen || forceAlwaysOpen) toggle(true);
|
if (defaultOpen || forceAlwaysOpen) toggle(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterUpdate(updateIfExpanded);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{#if data}
|
{#if data && appliedOverrides}
|
||||||
<form class="form-wrapper" on:submit|preventDefault>
|
<form class="form-wrapper" on:submit|preventDefault>
|
||||||
<div class="row">
|
<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" placeholder="Embed Title" bind:value={data.title}/>
|
||||||
<Input col3 label="Title URL (Optional)" placeholder="https://example.com" bind:value={data.url}/>
|
<Input col3 label="Title URL (Optional)" placeholder="https://example.com" bind:value={data.url}/>
|
||||||
</div>
|
</div>
|
||||||
@ -37,8 +37,10 @@
|
|||||||
<span slot="header">Footer</span>
|
<span slot="header">Footer</span>
|
||||||
<div slot="content" class="row">
|
<div slot="content" class="row">
|
||||||
{#if footerPremiumOnly}
|
{#if footerPremiumOnly}
|
||||||
<Input col3 label="Footer Text" placeholder="Footer Text" badge="Premium" bind:value={data.footer.text}/>
|
<Input col3 label="Footer Text" placeholder="Footer Text" badge="Premium"
|
||||||
<Input col3 label="Footer Icon URL (Optional)" badge="Premium" placeholder="https://example.com/image.png"
|
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}/>
|
bind:value={data.footer.icon_url}/>
|
||||||
{:else}
|
{:else}
|
||||||
<Input col3 label="Footer Text" placeholder="Footer Text" bind:value={data.footer.text}/>
|
<Input col3 label="Footer Text" placeholder="Footer Text" bind:value={data.footer.text}/>
|
||||||
@ -66,8 +68,11 @@
|
|||||||
bind:value={field.value}/>
|
bind:value={field.value}/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<div class="add-field-wrapper">
|
||||||
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
|
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
@ -77,7 +82,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
@ -86,7 +90,11 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
}
|
||||||
|
|
||||||
|
.add-field-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@ -98,12 +106,14 @@
|
|||||||
import DateTimePicker from "./form/DateTimePicker.svelte";
|
import DateTimePicker from "./form/DateTimePicker.svelte";
|
||||||
import Checkbox from "./form/Checkbox.svelte";
|
import Checkbox from "./form/Checkbox.svelte";
|
||||||
import Button from "./Button.svelte";
|
import Button from "./Button.svelte";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import {intToColour, colourToInt} from "../js/util";
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
$: data = data ?? {
|
$: data = data ?? {
|
||||||
fields: [],
|
fields: [],
|
||||||
colour: '#2ECC71',
|
colour: 0x2ECC71,
|
||||||
author: {},
|
author: {},
|
||||||
footer: {},
|
footer: {},
|
||||||
};
|
};
|
||||||
@ -119,4 +129,28 @@
|
|||||||
data.fields.splice(i, 1);
|
data.fields.splice(i, 1);
|
||||||
data = data;
|
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>
|
</script>
|
||||||
|
@ -24,8 +24,6 @@
|
|||||||
selectedRaw = [];
|
selectedRaw = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(selectedRaw)
|
|
||||||
|
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
selected = selectedRaw.map((panel) => panel.panel_id);
|
selected = selectedRaw.map((panel) => panel.panel_id);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,59 +1,41 @@
|
|||||||
<form on:submit|preventDefault>
|
<form on:submit|preventDefault>
|
||||||
<div class="row">
|
<Collapsible defaultOpen>
|
||||||
<Input col1={true} label="Panel Title" placeholder="Click to open a ticket" bind:value={data.title}/>
|
<span slot="header">Properties</span>
|
||||||
</div>
|
<div slot="content" class="col-1">
|
||||||
<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">
|
|
||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
<Button icon="fas fa-sliders-h" fullWidth=true type="button"
|
<ChannelDropdown col1 allowAnnouncementChannel {channels} label="Panel Channel"
|
||||||
on:click={toggleAdvancedSettings}>Toggle Advanced Settings
|
bind:value={data.channel_id}/>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-1" style="padding-right: 10px">
|
||||||
|
<PanelDropdown label="Panels (Minimum 2)" {panels} bind:selected={data.panels}/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row advanced-settings" class:advanced-settings-show={advancedSettings}
|
<div class="col-1">
|
||||||
class:advanced-settings-hide={!advancedSettings} class:show-overflow={overflowShow}>
|
<div class="row dropdown-menu-settings">
|
||||||
<div class="inner" class:inner-show={advancedSettings} class:absolute={advancedSettings && !overflowShow} >
|
<Checkbox label="Use Dropdown Menu" bind:value={data.select_menu}/>
|
||||||
<div class="row">
|
<div class="placeholder-input">
|
||||||
<Input col1={true} label="Large Image URL" bind:value={data.image_url} placeholder="https://example.com/image.png" />
|
<Input label="Dropdown Menu Placeholder" col1 placeholder="Select a topic..."
|
||||||
</div>
|
bind:value={data.select_menu_placeholder} disabled={!data.select_menu} />
|
||||||
<div class="row">
|
|
||||||
<Input col1={true} label="Small Image URL" bind:value={data.thumbnail_url} placeholder="https://example.com/image.png" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
|
|
||||||
<script>
|
<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 ChannelDropdown from "../ChannelDropdown.svelte";
|
||||||
import PanelDropdown from "../PanelDropdown.svelte";
|
import PanelDropdown from "../PanelDropdown.svelte";
|
||||||
import {onMount} from "svelte";
|
|
||||||
import Checkbox from "../form/Checkbox.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;
|
export let data;
|
||||||
|
|
||||||
@ -66,42 +48,17 @@
|
|||||||
const firstChannel = channels[0];
|
const firstChannel = channels[0];
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
colour: 0x7289da,
|
|
||||||
channels: firstChannel ? firstChannel.id : undefined,
|
channels: firstChannel ? firstChannel.id : undefined,
|
||||||
panels: [],
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -109,7 +66,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
@ -119,6 +75,15 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-settings {
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-settings > .placeholder-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 950px) {
|
@media only screen and (max-width: 950px) {
|
||||||
.row {
|
.row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -140,24 +105,4 @@
|
|||||||
width: 75%;
|
width: 75%;
|
||||||
height: 100%;
|
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>
|
</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;
|
data.emote = data.emote;
|
||||||
|
|
||||||
|
if (!data.colour) {
|
||||||
|
data.colour = 0x2ECC71;
|
||||||
|
}
|
||||||
|
|
||||||
tempColour = intToColour(data.colour);
|
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>
|
</NavElement>
|
||||||
|
|
||||||
{#if isAdmin}
|
{#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-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-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Staff Teams</NavElement>
|
||||||
<NavElement icon="fas fa-robot" link="/manage/{guildId}/integrations" on:click={closeDropdown}>
|
<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) {
|
export function intToColour(i) {
|
||||||
return `#${i.toString(16)}`
|
return `#${i.toString(16).padStart(6, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nullIfBlank(s) {
|
export function nullIfBlank(s) {
|
||||||
return s === '' ? null : 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 Transcripts from './views/Transcripts.svelte'
|
||||||
import TranscriptView from './views/TranscriptView.svelte'
|
import TranscriptView from './views/TranscriptView.svelte'
|
||||||
import Blacklist from './views/Blacklist.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 Tags from './views/Tags.svelte'
|
||||||
import Teams from './views/Teams.svelte'
|
import Teams from './views/Teams.svelte'
|
||||||
import Tickets from './views/Tickets.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 IntegrationConfigure from "./views/integrations/Configure.svelte";
|
||||||
import IntegrationActivate from "./views/integrations/Activate.svelte";
|
import IntegrationActivate from "./views/integrations/Activate.svelte";
|
||||||
import IntegrationManage from "./views/integrations/Manage.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 = [
|
export const routes = [
|
||||||
{name: '/', component: Index, layout: IndexLayout},
|
{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: 'blacklist', component: Blacklist, layout: ManageLayout},
|
||||||
{name: 'tags', component: Tags, layout: ManageLayout},
|
{name: 'tags', component: Tags, layout: ManageLayout},
|
||||||
{name: 'teams', component: Teams, 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/BurntSushi/toml v1.2.1
|
||||||
github.com/TicketsBot/archiverclient v0.0.0-20240613013458-accc062facc2
|
github.com/TicketsBot/archiverclient v0.0.0-20240613013458-accc062facc2
|
||||||
github.com/TicketsBot/common v0.0.0-20240710005307-9cc26f78d8e3
|
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/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
||||||
github.com/TicketsBot/worker v0.0.0-20240720223640-84817ecc3309
|
github.com/TicketsBot/worker v0.0.0-20240720223640-84817ecc3309
|
||||||
github.com/apex/log v1.1.2
|
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-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 h1:iOf8LxG3y7v5d5FErpc2i9gRzYN5wJzbd63iBTIllYY=
|
||||||
github.com/TicketsBot/database v0.0.0-20240720222825-35466fd5fc96/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
|
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 h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
||||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
||||||
|
@ -17,6 +17,12 @@ func (c Colour) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Colour) UnmarshalJSON(b []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 {
|
if len(b) < 2 {
|
||||||
return fmt.Errorf("invalid colour")
|
return fmt.Errorf("invalid colour")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user