Improve UX

This commit is contained in:
rxdn 2022-07-23 21:32:35 +01:00
parent 6cd41aa99a
commit e2e8270f86
11 changed files with 569 additions and 184 deletions

View File

@ -9,13 +9,6 @@ import (
"strconv" "strconv"
) )
type inputCreateBody struct {
Style component.TextStyleTypes `json:"style"`
Label string `json:"label"`
Placeholder *string `json:"placeholder"`
Optional bool `json:"optional"`
}
func CreateInput(ctx *gin.Context) { func CreateInput(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64) guildId := ctx.Keys["guildid"].(uint64)
@ -70,7 +63,7 @@ func CreateInput(ctx *gin.Context) {
// 2^30 chance of collision // 2^30 chance of collision
customId := utils.RandString(30) customId := utils.RandString(30)
formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, !data.Optional) formInputId, err := dbclient.Client.FormInput.Create(formId, customId, uint8(data.Style), data.Label, data.Placeholder, data.Required)
if err != nil { if err != nil {
ctx.JSON(500, utils.ErrorJson(err)) ctx.JSON(500, utils.ErrorJson(err))
return return
@ -83,7 +76,7 @@ func CreateInput(ctx *gin.Context) {
Style: uint8(data.Style), Style: uint8(data.Style),
Label: data.Label, Label: data.Label,
Placeholder: data.Placeholder, Placeholder: data.Placeholder,
Required: !data.Optional, Required: data.Required,
}) })
} }

View File

@ -16,7 +16,7 @@ func UpdateForm(ctx *gin.Context) {
return return
} }
if len(data.Title) > 255 { if len(data.Title) > 45 {
ctx.JSON(400, utils.ErrorStr("Title is too long")) ctx.JSON(400, utils.ErrorStr("Title is too long"))
return return
} }

View File

@ -72,7 +72,7 @@ func UpdateInput(ctx *gin.Context) {
Style: uint8(data.Style), Style: uint8(data.Style),
Label: data.Label, Label: data.Label,
Placeholder: data.Placeholder, Placeholder: data.Placeholder,
Required: !data.Optional, Required: data.Required,
} }
if err := dbclient.Client.FormInput.Update(newInput); err != nil { if err := dbclient.Client.FormInput.Update(newInput); err != nil {

View File

@ -0,0 +1,244 @@
package forms
import (
"context"
"fmt"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/rxdn/gdl/objects/interaction/component"
"sort"
"strconv"
"strings"
)
type (
updateInputsBody struct {
Create []inputCreateBody `json:"create" validate:"omitempty,dive"`
Update []inputUpdateBody `json:"update" validate:"omitempty,dive"`
Delete []int `json:"delete" validate:"omitempty"`
}
inputCreateBody struct {
Label string `json:"label" validate:"required,min=1,max=45"`
Placeholder *string `json:"placeholder,omitempty" validate:"omitempty,min=1,max=100"`
Position int `json:"position" validate:"required,min=1,max=5"`
Style component.TextStyleTypes `json:"style" validate:"required,min=1,max=2"`
Required bool `json:"required"`
}
inputUpdateBody struct {
Id int `json:"id" validate:"required"`
inputCreateBody `validate:"required,dive"`
}
)
var validate = validator.New()
func UpdateInputs(ctx *gin.Context) {
guildId := ctx.Keys["guildid"].(uint64)
formId, err := strconv.Atoi(ctx.Param("form_id"))
if err != nil {
ctx.JSON(400, utils.ErrorStr("Invalid form ID"))
return
}
var data updateInputsBody
if err := ctx.BindJSON(&data); err != nil {
ctx.JSON(400, utils.ErrorJson(err))
return
}
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
}
fieldCount := len(data.Create) + len(data.Update)
if fieldCount <= 0 || fieldCount > 5 {
ctx.JSON(400, utils.ErrorStr("Forms must have between 1 and 5 inputs"))
return
}
// Verify form exists and is from the right guild
form, ok, err := dbclient.Client.Forms.Get(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
if !ok {
ctx.JSON(404, utils.ErrorStr("Form not found"))
return
}
if form.GuildId != guildId {
ctx.JSON(403, utils.ErrorStr("Form does not belong to this guild"))
return
}
existingInputs, err := dbclient.Client.FormInput.GetInputs(formId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Verify that the UPDATE inputs exist
for _, input := range data.Update {
if !utils.ExistsMap(existingInputs, input.Id, idMapper) {
ctx.JSON(400, utils.ErrorStr("Input (to be updated) not found"))
return
}
}
// Verify that the DELETE inputs exist
for _, id := range data.Delete {
if !utils.ExistsMap(existingInputs, id, idMapper) {
ctx.JSON(400, utils.ErrorStr("Input (to be deleted) not found"))
return
}
}
// Ensure no overlap between DELETE and UPDATE
for _, id := range data.Delete {
if utils.ExistsMap(data.Update, id, idMapperBody) {
ctx.JSON(400, utils.ErrorStr("Delete and update overlap"))
return
}
}
// Verify that we are updating ALL inputs, excluding the ones to be deleted
var remainingExisting []int
for _, input := range existingInputs {
if !utils.Exists(data.Delete, input.Id) {
remainingExisting = append(remainingExisting, input.Id)
}
}
// Now verify that the contents match exactly
if len(remainingExisting) != len(data.Update) {
ctx.JSON(400, utils.ErrorStr("All inputs must be included in the update array"))
return
}
for _, input := range data.Update {
if !utils.Exists(remainingExisting, input.Id) {
ctx.JSON(400, utils.ErrorStr("All inputs must be included in the update array"))
return
}
}
// Verify that the positions are unique, and are in ascending order
if !arePositionsCorrect(data) {
ctx.JSON(400, utils.ErrorStr("Positions must be unique and in ascending order"))
return
}
if err := saveInputs(formId, data, existingInputs); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
ctx.Status(204)
}
func idMapper(input database.FormInput) int {
return input.Id
}
func idMapperBody(input inputUpdateBody) int {
return input.Id
}
func arePositionsCorrect(body updateInputsBody) bool {
var positions []int
for _, input := range body.Create {
positions = append(positions, input.Position)
}
for _, input := range body.Update {
positions = append(positions, input.Position)
}
sort.Slice(positions, func(i, j int) bool {
return positions[i] < positions[j]
})
for i, position := range positions {
if i+1 != position {
return false
}
}
return true
}
func saveInputs(formId int, data updateInputsBody, existingInputs []database.FormInput) error {
// We can now update in the database
tx, err := dbclient.Client.BeginTx()
if err != nil {
return err
}
defer tx.Rollback(context.Background())
for _, id := range data.Delete {
if err := dbclient.Client.FormInput.DeleteTx(tx, id, formId); err != nil {
return err
}
}
for _, input := range data.Update {
existing := utils.FindMap(existingInputs, input.Id, idMapper)
if existing == nil {
return fmt.Errorf("input %d does not exist", input.Id)
}
wrapped := database.FormInput{
Id: input.Id,
FormId: formId,
Position: input.Position,
CustomId: existing.CustomId,
Style: uint8(input.Style),
Label: input.Label,
Placeholder: input.Placeholder,
Required: input.Required,
}
if err := dbclient.Client.FormInput.UpdateTx(tx, wrapped); err != nil {
return err
}
}
for _, input := range data.Create {
if _, err := dbclient.Client.FormInput.CreateTx(
tx,
formId,
utils.RandString(30),
input.Position,
uint8(input.Style),
input.Label,
input.Placeholder,
input.Required,
); err != nil {
return err
}
}
return tx.Commit(context.Background())
}

View File

@ -123,6 +123,7 @@ func StartServer() {
guildAuthApiAdmin.PATCH("/forms/:form_id", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.UpdateForm) guildAuthApiAdmin.PATCH("/forms/:form_id", rl(middleware.RateLimitTypeGuild, 30, time.Hour), api_forms.UpdateForm)
guildAuthApiAdmin.DELETE("/forms/:form_id", api_forms.DeleteForm) guildAuthApiAdmin.DELETE("/forms/:form_id", api_forms.DeleteForm)
guildAuthApiAdmin.POST("/forms/:form_id", api_forms.CreateInput) guildAuthApiAdmin.POST("/forms/:form_id", api_forms.CreateInput)
guildAuthApiAdmin.PATCH("/forms/:form_id/inputs", api_forms.UpdateInputs)
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id", api_forms.UpdateInput) guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id", api_forms.UpdateInput)
guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id/:direction", api_forms.SwapInput) guildAuthApiAdmin.PATCH("/forms/:form_id/:input_id/:direction", api_forms.SwapInput)
guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput) guildAuthApiAdmin.DELETE("/forms/:form_id/:input_id", api_forms.DeleteInput)

View File

@ -17,11 +17,6 @@
</Button> </Button>
</form> </form>
{/if} {/if}
{#if withSaveButton}
<form on:submit|preventDefault={forwardSave} class="button-form">
<Button icon="fas fa-save">Save</Button>
</form>
{/if}
{#if withDeleteButton} {#if withDeleteButton}
<form on:submit|preventDefault={forwardDelete} class="button-form"> <form on:submit|preventDefault={forwardDelete} class="button-form">
<Button icon="fas fa-trash" danger={true}>Delete</Button> <Button icon="fas fa-trash" danger={true}>Delete</Button>
@ -41,7 +36,7 @@
</Dropdown> </Dropdown>
</div> </div>
<div class="row"> <div class="row">
<Checkbox label="Optional" bind:value={data.optional}/> <Checkbox label="Required" bind:value={data.required}/>
</div> </div>
</div> </div>
</div> </div>
@ -67,13 +62,6 @@
</div> </div>
{/if} {/if}
<div class="row"> <div class="row">
<div class="col-2-force">
{#if withSaveButton}
<form on:submit|preventDefault={forwardSave} class="button-form">
<Button icon="fas fa-save">Save</Button>
</form>
{/if}
</div>
<div class="col-2-force"> <div class="col-2-force">
{#if withDeleteButton} {#if withDeleteButton}
<form on:submit|preventDefault={forwardDelete} class="button-form"> <form on:submit|preventDefault={forwardDelete} class="button-form">
@ -85,7 +73,7 @@
</div> </div>
{/if} {/if}
{#if withCreateButton} {#if withCreateButton && false}
<div class="row" style="justify-content: center; margin-top: 10px"> <div class="row" style="justify-content: center; margin-top: 10px">
<Button type="submit" icon="fas fa-plus" {disabled}>Add Input</Button> <Button type="submit" icon="fas fa-plus" {disabled}>Add Input</Button>
</div> </div>
@ -105,7 +93,6 @@
import Checkbox from "../form/Checkbox.svelte"; import Checkbox from "../form/Checkbox.svelte";
export let withCreateButton = false; export let withCreateButton = false;
export let withSaveButton = false;
export let withDeleteButton = false; export let withDeleteButton = false;
export let withDirectionButtons = false; export let withDirectionButtons = false;
export let disabled = false; export let disabled = false;
@ -121,10 +108,6 @@
dispatch('create', data); dispatch('create', data);
} }
function forwardSave() {
dispatch('save', data);
}
function forwardDelete() { function forwardDelete() {
dispatch('delete', {}); dispatch('delete', {});
} }

View File

@ -40,3 +40,7 @@ export function colourToInt(colour) {
export function intToColour(i) { export function intToColour(i) {
return `#${i.toString(16)}` return `#${i.toString(16)}`
} }
export function nullIfBlank(s) {
return s === '' ? null : s;
}

View File

@ -1,6 +1,6 @@
<div class="parent"> <div class="parent">
<div class="content"> <div class="content">
<Card footer={false}> <Card footer footerRight>
<span slot="title">Forms</span> <span slot="title">Forms</span>
<div slot="body" class="body-wrapper"> <div slot="body" class="body-wrapper">
<div class="section"> <div class="section">
@ -18,25 +18,31 @@
<div class="section"> <div class="section">
<h2 class="section-title">Manage Forms</h2> <h2 class="section-title">Manage Forms</h2>
<div class="col-1" style="flex-direction: row"> {#if editingTitle && activeFormId !== null}
<div class="col-4" style="margin-right: 12px"> <div class="row form-name-edit-wrapper">
<Input col4 label="Form Title" placeholder="Form Title" bind:value={renamedTitle}/>
<div class="form-name-save-wrapper">
<Button icon="fas fa-floppy-disk" fullWidth={windowWidth <= 950} on:click={updateTitle}>Save</Button>
</div>
</div>
{:else}
<div class="row form-select-row">
<div class="multiselect-super"> <div class="multiselect-super">
<Dropdown col1={true} bind:value={activeFormId}> <Dropdown col1 bind:value={activeFormId}>
<option value={null}>Select a form...</option> <option value={null}>Select a form...</option>
{#each forms as form} {#each forms as form}
<option value="{form.form_id}">{form.title}</option> <option value="{form.form_id}">{form.title}</option>
{/each} {/each}
</Dropdown> </Dropdown>
</div> </div>
</div>
{#if activeFormId !== null} {#if activeFormId !== null}
<div class="col-4"> <Button on:click={() => editingTitle = true}>Rename Form</Button>
<Button danger={true} type="button" <Button danger type="button"
on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button> on:click={() => deleteForm(activeFormId)}>Delete {activeFormTitle}</Button>
</div>
{/if} {/if}
</div> </div>
{/if}
<div class="manage"> <div class="manage">
{#if activeFormId !== null} {#if activeFormId !== null}
@ -45,20 +51,31 @@
<FormInputRow data={input} formId={activeFormId} <FormInputRow data={input} formId={activeFormId}
withSaveButton={true} withDeleteButton={true} withDirectionButtons={true} withSaveButton={true} withDeleteButton={true} withDirectionButtons={true}
index={i} {formLength} index={i} {formLength}
on:save={(e) => editInput(activeFormId, input.id, e.detail)} on:delete={() => deleteInput(activeFormId, input)}
on:delete={() => deleteInput(activeFormId, input.id)} on:move={(e) => changePosition(activeFormId, input, e.detail.direction)}/>
on:move={(e) => changePosition(activeFormId, input.id, e.detail.direction)} />
</div> </div>
{/each} {/each}
{/if} {/if}
{#if activeFormId !== null} {#if activeFormId !== null}
<FormInputRow bind:data={inputCreationData} withCreateButton={true} disabled={formLength >= 5} <div class="row" style="justify-content: center; align-items: center; gap: 10px; margin-top: 10px">
on:create={(e) => createInput(e.detail)}/> <hr class="fill">
<div class="row add-input-container" class:add-input-disabled={formLength >= 5}>
<i class="fas fa-plus"></i>
<a on:click={addInput}>Add Input</a>
</div>
<hr class="fill">
</div>
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
<div slot="footer">
<Button type="submit" icon="fas fa-floppy-disk" disabled={formLength === 0} on:click={saveInputs}>
Save
</Button>
</div>
</Card> </Card>
</div> </div>
</div> </div>
@ -67,7 +84,7 @@
<script> <script>
import Card from "../components/Card.svelte"; import Card from "../components/Card.svelte";
import {notifyError, notifySuccess, withLoadingScreen} from '../js/util' import {notifyError, notifySuccess, nullIfBlank, withLoadingScreen} from '../js/util'
import Button from "../components/Button.svelte"; import Button from "../components/Button.svelte";
import axios from "axios"; import axios from "axios";
import {API_URL} from "../js/constants"; import {API_URL} from "../js/constants";
@ -84,18 +101,40 @@
let newTitle; let newTitle;
let forms = []; let forms = [];
let toDelete = {};
let activeFormId = null; let activeFormId = null;
$: activeFormTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : 'Unknown'; $: activeFormTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : 'Unknown';
let inputCreationData = {};
$: formLength = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).inputs.length : 0; $: formLength = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).inputs.length : 0;
let editingTitle = false;
let renamedTitle = "";
$: activeFormId, reflectTitle();
function reflectTitle() {
renamedTitle = activeFormId !== null ? forms.find(f => f.form_id === activeFormId).title : null;
}
$: windowWidth = 0; $: windowWidth = 0;
function getForm(formId) { function getForm(formId) {
return forms.find(form => form.form_id === formId); return forms.find(form => form.form_id === formId);
} }
async function updateTitle() {
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${activeFormId}`, {title: renamedTitle});
if (res.status !== 200) {
notifyError('Failed to update form title');
return;
}
editingTitle = false;
getForm(activeFormId).title = renamedTitle;
forms = forms;
notifySuccess('Form title updated');
}
async function createForm() { async function createForm() {
let data = { let data = {
title: newTitle, title: newTitle,
@ -116,6 +155,8 @@
activeFormId = null; // Error thrown from {#each forms.find} if we don't temporarily set this to null? activeFormId = null; // Error thrown from {#each forms.find} if we don't temporarily set this to null?
forms = [...forms, form]; forms = [...forms, form];
activeFormId = form.form_id; activeFormId = form.form_id;
addInput();
} }
async function deleteForm(id) { async function deleteForm(id) {
@ -135,21 +176,22 @@
} }
} }
async function createInput(data) { function addInput() {
let mapped = {...data, style: parseInt(data.style)}; if (formLength >= 5) return;
const res = await axios.post(`${API_URL}/api/${guildId}/forms/${activeFormId}`, mapped); const input = {
if (res.status !== 200) { form_id: activeFormId,
notifyError(res.data.error); position: formLength + 1,
return; style: "1",
} label: "",
placeholder: "",
required: true,
is_new: true,
};
let form = getForm(res.data.form_id); const form = getForm(activeFormId);
form.inputs = [...form.inputs, res.data]; form.inputs = [...form.inputs, input];
forms = forms; forms = forms;
inputCreationData = {"style": "1"};
notifySuccess('Form input created successfully');
} }
async function editInput(formId, inputId, data) { async function editInput(formId, inputId, data) {
@ -168,42 +210,66 @@
notifySuccess('Form input updated successfully'); notifySuccess('Form input updated successfully');
} }
async function deleteInput(formId, inputId) { async function deleteInput(formId, input) {
const res = await axios.delete(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}`); let form = getForm(formId);
if (res.status !== 200) {
notifyError(res.data.error); let idx = form.inputs.findIndex((i) => i === input);
return; form.inputs.splice(idx, 1);
for (let i = idx; i < form.inputs.length; i++) {
form.inputs[i].position--;
} }
// TODO: delete keyword?
let form = getForm(formId);
form.inputs = form.inputs.filter(input => input.id !== inputId);
forms = forms; forms = forms;
notifySuccess('Form input deleted successfully'); if (!input.is_new) {
if (toDelete[formId] === undefined) {
toDelete[formId] = [];
} }
async function changePosition(formId, inputId, direction) { toDelete[formId] = [...toDelete[formId], input.id];
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${formId}/${inputId}/${direction}`); }
if (res.status !== 200) {
notifyError(res.data.error);
return;
} }
if (res.data.success) { function changePosition(formId, input, direction) {
//let form = getForm(formId); const form = getForm(formId);
let form = forms.find(form => form.form_id === activeFormId); let idx = form.inputs.findIndex((i) => i === input);
let idx = form.inputs.findIndex((input) => input.id === inputId);
let inputs = form.inputs; let inputs = form.inputs;
if (direction === "up") { if (direction === "up") {
[inputs[idx-1], inputs[idx]] = [inputs[idx], inputs[idx-1]] [inputs[idx - 1].position, inputs[idx].position] = [inputs[idx].position, inputs[idx - 1].position];
[inputs[idx - 1], inputs[idx]] = [inputs[idx], inputs[idx - 1]];
} else if (direction === "down") { } else if (direction === "down") {
[inputs[idx+1], inputs[idx]] = [form.inputs[idx], form.inputs[idx+1]] [inputs[idx + 1].position, inputs[idx].position] = [form.inputs[idx].position, form.inputs[idx + 1].position];
[inputs[idx + 1], inputs[idx]] = [form.inputs[idx], form.inputs[idx + 1]];
} }
forms = forms; forms = forms;
} }
async function saveInputs() {
const form = getForm(activeFormId);
const data = {
"create": form.inputs.filter(i => i.is_new === true)
.map(i => ({...i, style: parseInt(i.style), placeholder: nullIfBlank(i.placeholder)})),
"update": form.inputs.filter(i => !i.is_new)
.map(i => ({...i, style: parseInt(i.style), placeholder: nullIfBlank(i.placeholder)})),
"delete": toDelete[activeFormId] || [],
}
const res = await axios.patch(`${API_URL}/api/${guildId}/forms/${activeFormId}/inputs`, data);
if (res.status !== 204) {
notifyError(res.data.error);
return;
}
toDelete = {};
const formId = activeFormId;
await loadForms();
activeFormId = formId;
notifySuccess('Form updated successfully');
} }
async function loadForms() { async function loadForms() {
@ -215,7 +281,6 @@
forms = res.data || []; forms = res.data || [];
forms.flatMap(f => f.inputs).forEach(i => { forms.flatMap(f => f.inputs).forEach(i => {
i.optional = !i.required;
i.style = i.style.toString(); i.style = i.style.toString();
}); });
@ -276,6 +341,14 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
hr.fill {
border-top: 1px solid #777;
border-bottom: 0;
border-left: 0;
border-right: 0;
flex: 1;
}
.row { .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -284,6 +357,26 @@
height: 100%; height: 100%;
} }
.form-select-row {
justify-content: flex-start;
gap: 12px;
max-height: 40px;
}
.form-name-edit-wrapper {
justify-content: flex-start;
gap: 12px;
}
.form-name-save-wrapper {
height: 48px;
align-self: flex-end;
}
.multiselect-super {
width: 31%;
}
.manage { .manage {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -293,6 +386,22 @@
margin-top: 12px; margin-top: 12px;
} }
.add-input-container {
display: flex;
align-items: center;
gap: 4px;
width: unset !important;
}
.add-input-container > * {
cursor: pointer;
}
.add-input-disabled > *{
cursor: default !important;
color: #777 !important;
}
#creation-row { #creation-row {
justify-content: flex-start !important; justify-content: flex-start !important;
} }
@ -314,5 +423,22 @@
#create-button-wrapper { #create-button-wrapper {
margin-left: unset; margin-left: unset;
} }
.form-select-row {
max-height: unset;
gap: 8px;
}
.multiselect-super {
width: 100%;
}
.form-name-edit-wrapper {
gap: unset;
}
.form-name-save-wrapper {
width: 100%;
}
} }
</style> </style>

4
go.mod
View File

@ -83,3 +83,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
nhooyr.io/websocket v1.8.4 // indirect nhooyr.io/websocket v1.8.4 // indirect
) )
replace (
github.com/TicketsBot/database => "../database"
)

2
go.sum
View File

@ -5,8 +5,6 @@ github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc h1:n15W8
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc= github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqSbjJ4o7WKkdoPngmhjAGEXFwteEjpqs= github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqSbjJ4o7WKkdoPngmhjAGEXFwteEjpqs=
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-20220712212403-61804b8beb18 h1:p3rr325yK5CqWQMBML1SzkC+mXx+SSaBq4PnyxeBYXA=
github.com/TicketsBot/database v0.0.0-20220712212403-61804b8beb18/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c h1:eyAFQuKihRkfkSNg1xeIm9nHQZ1z2Qg46kS7LcLZNxk= github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c h1:eyAFQuKihRkfkSNg1xeIm9nHQZ1z2Qg46kS7LcLZNxk=
github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw= github.com/TicketsBot/database v0.0.0-20220721214509-131e86b1a06c/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=

View File

@ -16,6 +16,38 @@ func Slice[T any](v ...T) []T {
return v return v
} }
func Exists[T comparable](v []T, el T) bool {
for _, e := range v {
if e == el {
return true
}
}
return false
}
func ExistsMap[T any, U comparable](v []T, el U, mapper func(T) U) bool {
for _, e := range v {
mapped := mapper(e)
if mapped == el {
return true
}
}
return false
}
func FindMap[T any, U comparable](v []T, el U, mapper func(T) U) *T {
for _, e := range v {
mapped := mapper(e)
if mapped == el {
return &e
}
}
return nil
}
func Must(err error) { func Must(err error) {
if err != nil { if err != nil {
panic(err) panic(err)