Tag overhaul
This commit is contained in:
parent
859de14f00
commit
dae9189052
@ -1,21 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/TicketsBot/GoPanel/database"
|
||||
"fmt"
|
||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/GoPanel/utils/types"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type tag struct {
|
||||
Id string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Id string `json:"id" validate:"required,min=1,max=16"`
|
||||
UseGuildCommand bool `json:"use_guild_command"` // Not yet implemented
|
||||
Content *string `json:"content" validate:"omitempty,min=1,max=4096"`
|
||||
UseEmbed bool `json:"use_embed"`
|
||||
Embed *types.CustomEmbed `json:"embed" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
var (
|
||||
validate = validator.New()
|
||||
slashCommandRegex = regexp.MustCompile(`^[-_a-zA-Z0-9]{1,32}$`)
|
||||
)
|
||||
|
||||
func CreateTag(ctx *gin.Context) {
|
||||
guildId := ctx.Keys["guildid"].(uint64)
|
||||
|
||||
// Max of 200 tags
|
||||
count, err := database.Client.Tag.GetTagCount(guildId)
|
||||
count, err := dbclient.Client.Tag.GetTagCount(guildId)
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
@ -32,27 +46,80 @@ func CreateTag(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !data.verifyIdLength() {
|
||||
ctx.JSON(400, utils.ErrorStr("Tag ID must be 1 - 16 characters in length"))
|
||||
if !data.UseEmbed {
|
||||
data.Embed = nil
|
||||
}
|
||||
|
||||
// TODO: Limit command amount
|
||||
if err := validate.Struct(data); err != nil {
|
||||
validationErrors, ok := err.(validator.ValidationErrors)
|
||||
if !ok {
|
||||
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
||||
return
|
||||
}
|
||||
|
||||
if !data.verifyContentLength() {
|
||||
ctx.JSON(400, utils.ErrorStr("Tag content must be 1 - 2000 characters in length"))
|
||||
formatted := "Your input contained the following errors:"
|
||||
for _, validationError := range validationErrors {
|
||||
formatted += fmt.Sprintf("\n%s", validationError.Error())
|
||||
}
|
||||
|
||||
formatted = strings.TrimSuffix(formatted, "\n")
|
||||
ctx.JSON(400, utils.ErrorStr(formatted))
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.Client.Tag.Set(guildId, data.Id, data.Content); err != nil {
|
||||
if !data.verifyContent() {
|
||||
ctx.JSON(400, utils.ErrorStr("You have not provided any content for the tag"))
|
||||
return
|
||||
}
|
||||
|
||||
var embed *database.CustomEmbedWithFields
|
||||
if data.Embed != nil {
|
||||
customEmbed, fields := data.Embed.IntoDatabaseStruct()
|
||||
embed = &database.CustomEmbedWithFields{
|
||||
CustomEmbed: customEmbed,
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
wrapped := database.Tag{
|
||||
Id: data.Id,
|
||||
GuildId: guildId,
|
||||
UseGuildCommand: data.UseGuildCommand,
|
||||
Content: data.Content,
|
||||
Embed: embed,
|
||||
}
|
||||
|
||||
if err := dbclient.Client.Tag.Set(wrapped); err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(204)
|
||||
}
|
||||
|
||||
func (t *tag) verifyId() bool {
|
||||
if len(t.Id) == 0 || len(t.Id) > 16 || strings.Contains(t.Id, " ") {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.UseGuildCommand {
|
||||
return slashCommandRegex.MatchString(t.Id)
|
||||
} else {
|
||||
ctx.JSON(200, utils.SuccessResponse)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tag) verifyIdLength() bool {
|
||||
return len(t.Id) > 0 && len(t.Id) <= 16
|
||||
func (t *tag) verifyContent() bool {
|
||||
if t.Content != nil { // validator ensures that if this is not nil, > 0 length
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *tag) verifyContentLength() bool {
|
||||
return len(t.Content) > 0 && len(t.Content) <= 2000
|
||||
if t.Embed != nil {
|
||||
if t.Embed.Description != nil || len(t.Embed.Fields) > 0 || t.Embed.ImageUrl != nil || t.Embed.ThumbnailUrl != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -6,14 +6,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DeleteTag(ctx *gin.Context) {
|
||||
guildId := ctx.Keys["guildid"].(uint64)
|
||||
|
||||
type Body struct {
|
||||
type deleteBody struct {
|
||||
TagId string `json:"tag_id"`
|
||||
}
|
||||
|
||||
var body Body
|
||||
func DeleteTag(ctx *gin.Context) {
|
||||
guildId := ctx.Keys["guildid"].(uint64)
|
||||
|
||||
var body deleteBody
|
||||
if err := ctx.BindJSON(&body); err != nil {
|
||||
ctx.JSON(400, utils.ErrorJson(err))
|
||||
return
|
||||
@ -26,7 +26,8 @@ func DeleteTag(ctx *gin.Context) {
|
||||
|
||||
if err := database.Client.Tag.Delete(guildId, body.TagId); err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
} else {
|
||||
ctx.JSON(200, utils.SuccessResponse)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(204)
|
||||
}
|
||||
|
@ -2,21 +2,35 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/GoPanel/utils/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TODO: Make client take new structure
|
||||
func TagsListHandler(ctx *gin.Context) {
|
||||
guildId := ctx.Keys["guildid"].(uint64)
|
||||
|
||||
tags, err := database.Client.Tag.GetByGuild(guildId)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(500, gin.H{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, tags)
|
||||
wrapped := make(map[string]tag)
|
||||
for id, data := range tags {
|
||||
var embed *types.CustomEmbed
|
||||
if data.Embed != nil {
|
||||
embed = types.NewCustomEmbed(data.Embed.CustomEmbed, data.Embed.Fields)
|
||||
}
|
||||
|
||||
wrapped[id] = tag{
|
||||
Id: data.Id,
|
||||
UseGuildCommand: data.UseGuildCommand,
|
||||
Content: data.Content,
|
||||
UseEmbed: data.Embed != nil,
|
||||
Embed: embed,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(200, wrapped)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/rxdn/gdl/objects/channel"
|
||||
"github.com/rxdn/gdl/objects/guild"
|
||||
"github.com/rxdn/gdl/objects/guild/emoji"
|
||||
"github.com/rxdn/gdl/objects/interaction"
|
||||
"github.com/rxdn/gdl/objects/member"
|
||||
"github.com/rxdn/gdl/objects/user"
|
||||
"github.com/rxdn/gdl/rest"
|
||||
@ -168,3 +169,11 @@ func (ctx BotContext) ListMembers(guildId uint64) (members []member.Member, err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctx BotContext) CreateGuildCommand(guildId uint64, data rest.CreateCommandData) (interaction.ApplicationCommand, error) {
|
||||
return rest.CreateGuildCommand(ctx.Token, ctx.RateLimiter, ctx.BotId, guildId, data)
|
||||
}
|
||||
|
||||
func (ctx BotContext) DeleteGuildCommand(guildId, commandId uint64) error {
|
||||
return rest.DeleteGuildCommand(ctx.Token, ctx.RateLimiter, ctx.BotId, guildId, commandId)
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
<div class="modal" transition:fade>
|
||||
<div class="modal-wrapper">
|
||||
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||
<span slot="title">Embed Builder</span>
|
||||
<span slot="title">
|
||||
<slot name="title">Embed Builder</slot>
|
||||
</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
<slot name="body"></slot>
|
||||
@ -51,6 +53,7 @@
|
||||
display: flex;
|
||||
width: 60%;
|
||||
margin: 10% auto auto auto;
|
||||
padding-bottom: 5%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1280px) {
|
||||
|
@ -4,71 +4,7 @@
|
||||
<span slot="title">Embed Builder</span>
|
||||
|
||||
<div slot="body" class="body-wrapper">
|
||||
<form class="form-wrapper" on:submit|preventDefault>
|
||||
<div class="row">
|
||||
<Colour col3 label="Embed Colour" bind:value={data.colour}/>
|
||||
<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>
|
||||
|
||||
<div class="row">
|
||||
<Textarea col1 label="Description" placeholder="Large text area, up to 4096 characters"
|
||||
bind:value={data.description}/>
|
||||
</div>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Author</span>
|
||||
|
||||
<div slot="content" class="row">
|
||||
<Input col3 label="Author Name" placeholder="Author Name" bind:value={data.author.name}/>
|
||||
<Input col3 label="Author Icon URL (Optional)" placeholder="https://example.com/image.png"
|
||||
tooltipText="Small icon displayed in the top left" bind:value={data.author.icon_url}/>
|
||||
<Input col3 label="Author URL (Optional)" placeholder="https://example.com"
|
||||
tooltipText="Hyperlink on the author's name" bind:value={data.author.url}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Images</span>
|
||||
<div slot="content" class="row">
|
||||
<Input col2 label="Large Image URL" placeholder="https://example.com/image.png"
|
||||
bind:value={data.image_url}/>
|
||||
<Input col2 label="Small Image URL" placeholder="https://example.com/image.png"
|
||||
bind:value={data.thumbnail_url}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Footer</span>
|
||||
<div slot="content" class="row">
|
||||
<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}/>
|
||||
<DateTimePicker col3 label="Footer Timestamp (Optional)" bind:value={data.timestamp}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Fields</span>
|
||||
<div slot="content" class="col-1">
|
||||
{#each data.fields as field, i}
|
||||
<div class="row" style="justify-content: flex-start; gap: 10px">
|
||||
<Input col2 label="Field Name" placeholder="Field Name" bind:value={field.name}/>
|
||||
<Checkbox label="Inline" bind:value={field.inline}/>
|
||||
|
||||
<div style="margin-top: 18px; display: flex; align-self: center">
|
||||
<Button danger icon="fas fa-trash-can" on:click={() => deleteField(i)}>Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Textarea col1 label="Field Value" placeholder="Large text area, up to 1024 characters"
|
||||
bind:value={field.value}/>
|
||||
</div>
|
||||
{/each}
|
||||
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</form>
|
||||
<EmbedForm bind:data />
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
@ -97,32 +33,12 @@
|
||||
import DateTimePicker from "./form/DateTimePicker.svelte";
|
||||
import Collapsible from "./Collapsible.svelte";
|
||||
import Checkbox from "./form/Checkbox.svelte";
|
||||
import EmbedForm from "./EmbedForm.svelte";
|
||||
|
||||
export let guildId;
|
||||
|
||||
export let data;
|
||||
|
||||
if (data === undefined || data === null) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
data.fields = [];
|
||||
data.colour = '#2ECC71';
|
||||
data.author = {};
|
||||
data.footer = {};
|
||||
}
|
||||
|
||||
function addField() {
|
||||
data.fields.push({name: '', value: '', inline: false});
|
||||
data = data;
|
||||
}
|
||||
|
||||
function deleteField(i) {
|
||||
data.fields.splice(i, 1);
|
||||
data = data;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function dispatchClose() {
|
||||
@ -190,19 +106,4 @@
|
||||
background-color: #000;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.form-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
116
frontend/src/components/EmbedForm.svelte
Normal file
116
frontend/src/components/EmbedForm.svelte
Normal file
@ -0,0 +1,116 @@
|
||||
<form class="form-wrapper" on:submit|preventDefault>
|
||||
<div class="row">
|
||||
<Colour col3 label="Embed Colour" bind:value={data.colour}/>
|
||||
<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>
|
||||
|
||||
<div class="row">
|
||||
<Textarea col1 label="Description" placeholder="Large text area, up to 4096 characters"
|
||||
bind:value={data.description}/>
|
||||
</div>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Author</span>
|
||||
|
||||
<div slot="content" class="row">
|
||||
<Input col3 label="Author Name" placeholder="Author Name" bind:value={data.author.name}/>
|
||||
<Input col3 label="Author Icon URL (Optional)" placeholder="https://example.com/image.png"
|
||||
tooltipText="Small icon displayed in the top left" bind:value={data.author.icon_url}/>
|
||||
<Input col3 label="Author URL (Optional)" placeholder="https://example.com"
|
||||
tooltipText="Hyperlink on the author's name" bind:value={data.author.url}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Images</span>
|
||||
<div slot="content" class="row">
|
||||
<Input col2 label="Large Image URL" placeholder="https://example.com/image.png"
|
||||
bind:value={data.image_url}/>
|
||||
<Input col2 label="Small Image URL" placeholder="https://example.com/image.png"
|
||||
bind:value={data.thumbnail_url}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Footer</span>
|
||||
<div slot="content" class="row">
|
||||
<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}/>
|
||||
<DateTimePicker col3 label="Footer Timestamp (Optional)" bind:value={data.timestamp}/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible>
|
||||
<span slot="header">Fields</span>
|
||||
<div slot="content" class="col-1">
|
||||
{#each data.fields as field, i}
|
||||
<div class="row" style="justify-content: flex-start; gap: 10px">
|
||||
<Input col2 label="Field Name" placeholder="Field Name" bind:value={field.name}/>
|
||||
<Checkbox label="Inline" bind:value={field.inline}/>
|
||||
|
||||
<div style="margin-top: 18px; display: flex; align-self: center">
|
||||
<Button danger icon="fas fa-trash-can" on:click={() => deleteField(i)}>Delete</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Textarea col1 label="Field Value" placeholder="Large text area, up to 1024 characters"
|
||||
bind:value={field.value}/>
|
||||
</div>
|
||||
{/each}
|
||||
<Button type="button" icon="fas fa-plus" fullWidth on:click={addField}>Add Field</Button>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.form-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Textarea from "./form/Textarea.svelte";
|
||||
import Colour from "./form/Colour.svelte";
|
||||
import Input from "./form/Input.svelte";
|
||||
import Collapsible from "./Collapsible.svelte";
|
||||
import DateTimePicker from "./form/DateTimePicker.svelte";
|
||||
import Checkbox from "./form/Checkbox.svelte";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
export let data;
|
||||
|
||||
if (data === undefined || data === null) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
data.fields = [];
|
||||
data.colour = '#2ECC71';
|
||||
data.author = {};
|
||||
data.footer = {};
|
||||
}
|
||||
|
||||
function addField() {
|
||||
data.fields.push({name: '', value: '', inline: false});
|
||||
data = data;
|
||||
}
|
||||
|
||||
function deleteField(i) {
|
||||
data.fields.splice(i, 1);
|
||||
data = data;
|
||||
}
|
||||
</script>
|
@ -18,5 +18,6 @@
|
||||
.form-checkbox {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||
{#if label !== undefined}
|
||||
<div class="label-wrapper">
|
||||
<label for="input" class="form-label">{label}</label>
|
||||
<div class="label-wrapper" class:no-margin={tooltipText !== undefined}>
|
||||
<label for="input" class="form-label" style="margin-bottom: 0">{label}</label>
|
||||
{#if badge !== undefined}
|
||||
<Badge>{badge}</Badge>
|
||||
{/if}
|
||||
{#if tooltipText !== undefined}
|
||||
<div style="margin-bottom: 5px">
|
||||
<div>
|
||||
<Tooltip tip={tooltipText} top color="#121212">
|
||||
{#if tooltipLink !== undefined}
|
||||
<a href={tooltipLink} target="_blank">
|
||||
@ -53,6 +53,11 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.tooltip-icon {
|
||||
|
@ -1,70 +0,0 @@
|
||||
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4} class="switch">
|
||||
<label for="input" class="form-label">{label}</label>
|
||||
<input id="input" type="checkbox" bind:checked={value} on:change={() => console.log('b')} />
|
||||
<span class="slider" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export let value;
|
||||
export let label;
|
||||
|
||||
export let col1 = false;
|
||||
export let col2 = false;
|
||||
export let col3 = false;
|
||||
export let col4 = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
box-shadow: 0 0 1px #2196f3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
</style>
|
@ -1,4 +1,4 @@
|
||||
<div>
|
||||
<div class:inline>
|
||||
{#if label !== undefined}
|
||||
<label class="form-label" style="margin-bottom: 0 !important;">{label}</label>
|
||||
{/if}
|
||||
@ -16,4 +16,17 @@
|
||||
|
||||
export let toggledColour = "#66bb6a";
|
||||
export let untoggledColour = "#ccc";
|
||||
export let inline = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inline {
|
||||
flex-direction: row !important;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
98
frontend/src/components/manage/TagEditor.svelte
Normal file
98
frontend/src/components/manage/TagEditor.svelte
Normal file
@ -0,0 +1,98 @@
|
||||
{#if data}
|
||||
<ConfirmationModal icon="fas fa-floppy-disk" on:confirm={() => dispatch("confirm", data)} on:cancel={() => dispatch("cancel", {})}>
|
||||
<span slot="title">Tag Editor</span>
|
||||
<div slot="body" class="body-wrapper">
|
||||
<div class="row">
|
||||
<Input col4 label="Tag ID" placeholder="docs" bind:value={data.id}
|
||||
tooltipText='If the command is "/tag docs", then the ID is "docs"'/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<Textarea col1 label="Message Content" bind:value={data.content} placeholder="Message content, outside of the embed"/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="inline">
|
||||
<Toggle inline label="Use Embed" bind:value={data.use_embed}/>
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
{#if data.use_embed}
|
||||
<EmbedForm bind:data={data.embed}/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="confirm">Save</span>
|
||||
</ConfirmationModal>
|
||||
{/if}
|
||||
|
||||
<svelte:window on:keydown={handleKeydown}/>
|
||||
|
||||
<script>
|
||||
import ConfirmationModal from "../ConfirmationModal.svelte";
|
||||
import Input from "../form/Input.svelte";
|
||||
import Checkbox from "../form/Checkbox.svelte";
|
||||
import Textarea from "../form/Textarea.svelte";
|
||||
import Toggle from "../form/Toggle.svelte";
|
||||
import EmbedForm from "../EmbedForm.svelte";
|
||||
import {createEventDispatcher, onMount} from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let data;
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === "Escape") {
|
||||
dispatch("cancel", {});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (data === undefined) {
|
||||
data = {
|
||||
use_embed: false,
|
||||
};
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2%;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 2vh;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid #777;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
@ -65,7 +65,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
z-index: 1001;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -83,7 +83,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 998;
|
||||
z-index: 1000;
|
||||
background-color: #000;
|
||||
opacity: .5;
|
||||
}
|
||||
|
@ -1,25 +1,28 @@
|
||||
{#if tagCreateModal}
|
||||
<TagEditor on:cancel={() => tagCreateModal = false} on:confirm={createTag}/>
|
||||
{:else if tagEditModal}
|
||||
<TagEditor bind:data={editData} on:cancel={cancelEdit} on:confirm={editTag}/>
|
||||
{/if}
|
||||
|
||||
<div class="parent">
|
||||
<div class="content">
|
||||
<div class="main-col">
|
||||
<Card footer={false}>
|
||||
<Card footer footerRight>
|
||||
<span slot="title">Tags</span>
|
||||
<div slot="body" class="body-wrapper">
|
||||
<table class="nice">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th>Edit</th>
|
||||
<th>Delete</th>
|
||||
<th style="text-align: right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(tags) as [id, content]}
|
||||
{#each Object.entries(tags) as [id, tag]}
|
||||
<tr>
|
||||
<td>{id}</td>
|
||||
<td>
|
||||
<Button type="button" on:click={() => editTag(id)}>Edit</Button>
|
||||
</td>
|
||||
<td>
|
||||
<td class="actions">
|
||||
<Button type="button" on:click={() => openEditModal(id)}>Edit</Button>
|
||||
<Button type="button" danger={true} on:click={() => deleteTag(id)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -27,27 +30,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="right-col">
|
||||
<Card footer={false}>
|
||||
<span slot="title">Create A Tag</span>
|
||||
<div slot="body" class="body-wrapper">
|
||||
<form class="body-wrapper" on:submit|preventDefault={createTag}>
|
||||
<div class="col" style="flex-direction: column">
|
||||
<Input label="Tag ID" placeholder="mytag" bind:value={createData.id}/>
|
||||
</div>
|
||||
<div class="col" style="flex-direction: column">
|
||||
<Textarea label="Tag Content" placeholder="Enter the text that the bot should respond with"
|
||||
bind:value={createData.content}/>
|
||||
</div>
|
||||
|
||||
<div class="row" style="justify-content: center">
|
||||
<div class="col-2">
|
||||
<Button fullWidth={true} icon="fas fa-plus">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div slot="footer">
|
||||
<Button icon="fas fa-plus" on:click={() => tagCreateModal = true}>Create Tag</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@ -61,30 +45,89 @@
|
||||
import axios from "axios";
|
||||
import {API_URL} from "../js/constants";
|
||||
import {setDefaultHeaders} from '../includes/Auth.svelte'
|
||||
import Input from "../components/form/Input.svelte";
|
||||
import Textarea from "../components/form/Textarea.svelte";
|
||||
import {fade} from "svelte/transition";
|
||||
import TagEditor from "../components/manage/TagEditor.svelte";
|
||||
|
||||
export let currentRoute;
|
||||
let guildId = currentRoute.namedParams.id;
|
||||
|
||||
let createData = {};
|
||||
let tags = {};
|
||||
let editData;
|
||||
let editId;
|
||||
|
||||
function editTag(id) {
|
||||
createData.id = id;
|
||||
createData.content = tags[id];
|
||||
let tagCreateModal = false;
|
||||
let tagEditModal = false;
|
||||
|
||||
function openEditModal(id) {
|
||||
editId = id;
|
||||
editData = tags[id];
|
||||
tagEditModal = true;
|
||||
}
|
||||
|
||||
async function createTag() {
|
||||
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, createData);
|
||||
if (res.status !== 200) {
|
||||
function cancelEdit() {
|
||||
editId = undefined;
|
||||
editData = undefined;
|
||||
tagEditModal = false;
|
||||
}
|
||||
|
||||
async function createTag(e) {
|
||||
const data = e.detail;
|
||||
if (!data.id || data.id.length === 0) {
|
||||
notifyError("Tag ID is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.content !== null && data.content.length === 0) {
|
||||
data.content = null;
|
||||
}
|
||||
|
||||
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, data);
|
||||
if (res.status !== 204) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
notifySuccess(`Tag ${createData.id} has been created`);
|
||||
tags[createData.id] = createData.content;
|
||||
createData = {};
|
||||
notifySuccess(`Tag ${data.id} has been created`);
|
||||
tagCreateModal = false;
|
||||
tags[data.id] = data;
|
||||
}
|
||||
|
||||
async function editTag(e) {
|
||||
const data = e.detail;
|
||||
|
||||
if (editId !== data.id) {
|
||||
// Delete old tag
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: {tag_id: editId}});
|
||||
if (res.status !== 204) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
delete tags[editId];
|
||||
}
|
||||
|
||||
if (!data.id || data.id.length === 0) {
|
||||
notifyError("Tag ID is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.content !== null && data.content.length === 0) {
|
||||
data.content = null;
|
||||
}
|
||||
|
||||
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, data);
|
||||
if (res.status !== 204) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
tags[data.id] = data;
|
||||
|
||||
tagEditModal = false;
|
||||
editData = undefined;
|
||||
editId = undefined;
|
||||
|
||||
notifySuccess("Tag edited successfully");
|
||||
}
|
||||
|
||||
async function deleteTag(id) {
|
||||
@ -93,14 +136,14 @@
|
||||
};
|
||||
|
||||
const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: data});
|
||||
if (res.status !== 200) {
|
||||
if (res.status !== 204) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
notifySuccess(`Tag deleted successfully`);
|
||||
delete tags[id];
|
||||
tags = tags; // svelte terrible
|
||||
tags = tags;
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
@ -111,6 +154,9 @@
|
||||
}
|
||||
|
||||
tags = res.data;
|
||||
for (const id in tags) {
|
||||
tags[id].use_embed = tags[id].embed !== null;
|
||||
}
|
||||
}
|
||||
|
||||
withLoadingScreen(async () => {
|
||||
@ -142,34 +188,23 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 34%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.body-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 2vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 2%;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 2%;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 950px) {
|
||||
@ -181,9 +216,5 @@
|
||||
width: 100%;
|
||||
margin-top: 4%;
|
||||
}
|
||||
|
||||
.right-col {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -288,6 +288,7 @@
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
:global(table.nice > thead > tr, table.nice > tbody > tr) {
|
||||
|
4
go.mod
4
go.mod
@ -6,9 +6,9 @@ require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
|
||||
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f
|
||||
github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
||||
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0
|
||||
github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8
|
||||
github.com/apex/log v1.1.2
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607
|
||||
|
4
go.sum
4
go.sum
@ -41,12 +41,16 @@ github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqS
|
||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
||||
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f h1:5tpytvC/I1eOgLXhhWvg4RA+vu40oXXzLFwfLde37gY=
|
||||
github.com/TicketsBot/database v0.0.0-20220731213519-9fc9b34ab06f/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
||||
github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347 h1:JVdXXxs6vxxldfesM/mmBNVBEVfkrFOpjcMdwxJQ4fw=
|
||||
github.com/TicketsBot/database v0.0.0-20220802140804-643695cd8347/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=
|
||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk=
|
||||
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0 h1:94TYfRYYoAoV3Vyg9hqjtyDFw/Nb5a5+X7ob5rEjjos=
|
||||
github.com/TicketsBot/worker v0.0.0-20220726162721-eb8978799cd0/go.mod h1:gThk0bKZTfcKgwpxrFN5BvOgQ6sXoRkz7ojj/9yiU28=
|
||||
github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8 h1:/hxUoNMsAD1HeG66iPDn0G5zQ8RNltaiL2h50LNplaA=
|
||||
github.com/TicketsBot/worker v0.0.0-20220802140902-30ca73aea6b8/go.mod h1:E8y+9Xu8el7QzHALhR/IFvITcJJkDeAxfsBFIEEWJuo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
|
Loading…
x
Reference in New Issue
Block a user