Tag overhaul

This commit is contained in:
rxdn 2022-08-02 15:09:55 +01:00
parent 859de14f00
commit dae9189052
17 changed files with 469 additions and 275 deletions

View File

@ -1,21 +1,35 @@
package api package api
import ( import (
"github.com/TicketsBot/GoPanel/database" "fmt"
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/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"regexp"
"strings"
) )
type tag struct { type tag struct {
Id string `json:"id"` Id string `json:"id" validate:"required,min=1,max=16"`
Content string `json:"content"` 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) { func CreateTag(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
// Max of 200 tags // Max of 200 tags
count, err := database.Client.Tag.GetTagCount(guildId) count, err := dbclient.Client.Tag.GetTagCount(guildId)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorJson(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
@ -32,27 +46,80 @@ func CreateTag(ctx *gin.Context) {
return return
} }
if !data.verifyIdLength() { if !data.UseEmbed {
ctx.JSON(400, utils.ErrorStr("Tag ID must be 1 - 16 characters in length")) 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
}
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 return
} }
if !data.verifyContentLength() { if !data.verifyContent() {
ctx.JSON(400, utils.ErrorStr("Tag content must be 1 - 2000 characters in length")) ctx.JSON(400, utils.ErrorStr("You have not provided any content for the tag"))
return return
} }
if err := database.Client.Tag.Set(guildId, data.Id, data.Content); err != nil { 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)) 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 { } else {
ctx.JSON(200, utils.SuccessResponse) return true
} }
} }
func (t *tag) verifyIdLength() bool { func (t *tag) verifyContent() bool {
return len(t.Id) > 0 && len(t.Id) <= 16 if t.Content != nil { // validator ensures that if this is not nil, > 0 length
} return true
}
func (t *tag) verifyContentLength() bool { if t.Embed != nil {
return len(t.Content) > 0 && len(t.Content) <= 2000 if t.Embed.Description != nil || len(t.Embed.Fields) > 0 || t.Embed.ImageUrl != nil || t.Embed.ThumbnailUrl != nil {
return true
}
}
return false
} }

View File

@ -6,14 +6,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type deleteBody struct {
TagId string `json:"tag_id"`
}
func DeleteTag(ctx *gin.Context) { func DeleteTag(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
type Body struct { var body deleteBody
TagId string `json:"tag_id"`
}
var body Body
if err := ctx.BindJSON(&body); err != nil { if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(400, utils.ErrorJson(err)) ctx.JSON(400, utils.ErrorJson(err))
return return
@ -26,7 +26,8 @@ func DeleteTag(ctx *gin.Context) {
if err := database.Client.Tag.Delete(guildId, body.TagId); err != nil { if err := database.Client.Tag.Delete(guildId, body.TagId); err != nil {
ctx.JSON(500, utils.ErrorJson(err)) ctx.JSON(500, utils.ErrorJson(err))
} else { return
ctx.JSON(200, utils.SuccessResponse)
} }
ctx.Status(204)
} }

View File

@ -2,21 +2,35 @@ package api
import ( import (
"github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// TODO: Make client take new structure
func TagsListHandler(ctx *gin.Context) { func TagsListHandler(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
tags, err := database.Client.Tag.GetByGuild(guildId) tags, err := database.Client.Tag.GetByGuild(guildId)
if err != nil { if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{ ctx.JSON(500, utils.ErrorJson(err))
"success": false,
"error": err.Error(),
})
return 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)
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/rxdn/gdl/objects/channel" "github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild" "github.com/rxdn/gdl/objects/guild"
"github.com/rxdn/gdl/objects/guild/emoji" "github.com/rxdn/gdl/objects/guild/emoji"
"github.com/rxdn/gdl/objects/interaction"
"github.com/rxdn/gdl/objects/member" "github.com/rxdn/gdl/objects/member"
"github.com/rxdn/gdl/objects/user" "github.com/rxdn/gdl/objects/user"
"github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest"
@ -168,3 +169,11 @@ func (ctx BotContext) ListMembers(guildId uint64) (members []member.Member, err
return 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)
}

View File

@ -1,7 +1,9 @@
<div class="modal" transition:fade> <div class="modal" transition:fade>
<div class="modal-wrapper"> <div class="modal-wrapper">
<Card footer="{true}" footerRight="{true}" fill="{false}"> <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"> <div slot="body" class="body-wrapper">
<slot name="body"></slot> <slot name="body"></slot>
@ -51,6 +53,7 @@
display: flex; display: flex;
width: 60%; width: 60%;
margin: 10% auto auto auto; margin: 10% auto auto auto;
padding-bottom: 5%;
} }
@media only screen and (max-width: 1280px) { @media only screen and (max-width: 1280px) {

View File

@ -4,71 +4,7 @@
<span slot="title">Embed Builder</span> <span slot="title">Embed Builder</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<form class="form-wrapper" on:submit|preventDefault> <EmbedForm bind:data />
<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>
</div> </div>
<div slot="footer"> <div slot="footer">
@ -97,32 +33,12 @@
import DateTimePicker from "./form/DateTimePicker.svelte"; import DateTimePicker from "./form/DateTimePicker.svelte";
import Collapsible from "./Collapsible.svelte"; import Collapsible from "./Collapsible.svelte";
import Checkbox from "./form/Checkbox.svelte"; import Checkbox from "./form/Checkbox.svelte";
import EmbedForm from "./EmbedForm.svelte";
export let guildId; export let guildId;
export let data; 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(); const dispatch = createEventDispatcher();
function dispatchClose() { function dispatchClose() {
@ -190,19 +106,4 @@
background-color: #000; background-color: #000;
opacity: .5; 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> </style>

View 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>

View File

@ -18,5 +18,6 @@
.form-checkbox { .form-checkbox {
height: 40px; height: 40px;
width: 40px; width: 40px;
margin: 0 !important;
} }
</style> </style>

View File

@ -1,12 +1,12 @@
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}> <div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
{#if label !== undefined} {#if label !== undefined}
<div class="label-wrapper"> <div class="label-wrapper" class:no-margin={tooltipText !== undefined}>
<label for="input" class="form-label">{label}</label> <label for="input" class="form-label" style="margin-bottom: 0">{label}</label>
{#if badge !== undefined} {#if badge !== undefined}
<Badge>{badge}</Badge> <Badge>{badge}</Badge>
{/if} {/if}
{#if tooltipText !== undefined} {#if tooltipText !== undefined}
<div style="margin-bottom: 5px"> <div>
<Tooltip tip={tooltipText} top color="#121212"> <Tooltip tip={tooltipText} top color="#121212">
{#if tooltipLink !== undefined} {#if tooltipLink !== undefined}
<a href={tooltipLink} target="_blank"> <a href={tooltipLink} target="_blank">
@ -53,6 +53,11 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
margin-bottom: 5px;
}
.no-margin {
margin-bottom: 0 !important;
} }
.tooltip-icon { .tooltip-icon {

View File

@ -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>

View File

@ -1,4 +1,4 @@
<div> <div class:inline>
{#if label !== undefined} {#if label !== undefined}
<label class="form-label" style="margin-bottom: 0 !important;">{label}</label> <label class="form-label" style="margin-bottom: 0 !important;">{label}</label>
{/if} {/if}
@ -16,4 +16,17 @@
export let toggledColour = "#66bb6a"; export let toggledColour = "#66bb6a";
export let untoggledColour = "#ccc"; export let untoggledColour = "#ccc";
export let inline = false;
</script> </script>
<style>
div {
display: flex;
flex-direction: column;
}
.inline {
flex-direction: row !important;
gap: 4px;
}
</style>

View 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>

View File

@ -65,7 +65,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 999; z-index: 1001;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -83,7 +83,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 998; z-index: 1000;
background-color: #000; background-color: #000;
opacity: .5; opacity: .5;
} }

View File

@ -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="parent">
<div class="content"> <div class="content">
<div class="main-col"> <div class="main-col">
<Card footer={false}> <Card footer footerRight>
<span slot="title">Tags</span> <span slot="title">Tags</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<table class="nice"> <table class="nice">
<thead> <thead>
<tr> <tr>
<th>Tag</th> <th>Tag</th>
<th>Edit</th> <th style="text-align: right">Actions</th>
<th>Delete</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each Object.entries(tags) as [id, content]} {#each Object.entries(tags) as [id, tag]}
<tr> <tr>
<td>{id}</td> <td>{id}</td>
<td> <td class="actions">
<Button type="button" on:click={() => editTag(id)}>Edit</Button> <Button type="button" on:click={() => openEditModal(id)}>Edit</Button>
</td>
<td>
<Button type="button" danger={true} on:click={() => deleteTag(id)}>Delete</Button> <Button type="button" danger={true} on:click={() => deleteTag(id)}>Delete</Button>
</td> </td>
</tr> </tr>
@ -27,27 +30,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</Card> <div slot="footer">
</div> <Button icon="fas fa-plus" on:click={() => tagCreateModal = true}>Create Tag</Button>
<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> </div>
</Card> </Card>
</div> </div>
@ -61,30 +45,89 @@
import axios from "axios"; import axios from "axios";
import {API_URL} from "../js/constants"; import {API_URL} from "../js/constants";
import {setDefaultHeaders} from '../includes/Auth.svelte' import {setDefaultHeaders} from '../includes/Auth.svelte'
import Input from "../components/form/Input.svelte"; import {fade} from "svelte/transition";
import Textarea from "../components/form/Textarea.svelte"; import TagEditor from "../components/manage/TagEditor.svelte";
export let currentRoute; export let currentRoute;
let guildId = currentRoute.namedParams.id; let guildId = currentRoute.namedParams.id;
let createData = {};
let tags = {}; let tags = {};
let editData;
let editId;
function editTag(id) { let tagCreateModal = false;
createData.id = id; let tagEditModal = false;
createData.content = tags[id];
function openEditModal(id) {
editId = id;
editData = tags[id];
tagEditModal = true;
} }
async function createTag() { function cancelEdit() {
const res = await axios.put(`${API_URL}/api/${guildId}/tags`, createData); editId = undefined;
if (res.status !== 200) { 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); notifyError(res.data.error);
return; return;
} }
notifySuccess(`Tag ${createData.id} has been created`); notifySuccess(`Tag ${data.id} has been created`);
tags[createData.id] = createData.content; tagCreateModal = false;
createData = {}; 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) { async function deleteTag(id) {
@ -93,14 +136,14 @@
}; };
const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: data}); const res = await axios.delete(`${API_URL}/api/${guildId}/tags`, {data: data});
if (res.status !== 200) { if (res.status !== 204) {
notifyError(res.data.error); notifyError(res.data.error);
return; return;
} }
notifySuccess(`Tag deleted successfully`); notifySuccess(`Tag deleted successfully`);
delete tags[id]; delete tags[id];
tags = tags; // svelte terrible tags = tags;
} }
async function loadTags() { async function loadTags() {
@ -111,6 +154,9 @@
} }
tags = res.data; tags = res.data;
for (const id in tags) {
tags[id].use_embed = tags[id].embed !== null;
}
} }
withLoadingScreen(async () => { withLoadingScreen(async () => {
@ -142,34 +188,23 @@
height: 100%; height: 100%;
} }
.right-col {
display: flex;
flex-direction: column;
width: 34%;
height: 100%;
}
.body-wrapper { .body-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
row-gap: 2vh;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.row { table {
width: 100%;
}
.actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; gap: 10px;
height: 100%; justify-content: flex-end;
margin-bottom: 2%;
}
.col {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
margin-bottom: 2%;
} }
@media only screen and (max-width: 950px) { @media only screen and (max-width: 950px) {
@ -181,9 +216,5 @@
width: 100%; width: 100%;
margin-top: 4%; margin-top: 4%;
} }
.right-col {
width: 100%;
}
} }
</style> </style>

View File

@ -288,6 +288,7 @@
font-weight: normal; font-weight: normal;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #dee2e6;
padding-left: 10px; padding-left: 10px;
padding-right: 10px;
} }
:global(table.nice > thead > tr, table.nice > tbody > tr) { :global(table.nice > thead > tr, table.nice > tbody > tr) {

4
go.mod
View File

@ -6,9 +6,9 @@ require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 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/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/apex/log v1.1.2
github.com/getsentry/sentry-go v0.13.0 github.com/getsentry/sentry-go v0.13.0
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607 github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607

4
go.sum
View File

@ -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/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 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-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 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=
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk= 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 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-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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=