Discord: update UIs to use the new config
This commit is contained in:
@@ -210,6 +210,10 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
|
||||
@@ -34,7 +34,12 @@ import {
|
||||
type SignalForm,
|
||||
type TelegramForm,
|
||||
} from "./ui-types";
|
||||
import { loadChatHistory, sendChat, handleChatEvent } from "./controllers/chat";
|
||||
import {
|
||||
loadChatHistory,
|
||||
sendChat,
|
||||
handleChatEvent,
|
||||
type ChatEventPayload,
|
||||
} from "./controllers/chat";
|
||||
import { loadNodes } from "./controllers/nodes";
|
||||
import { loadConfig } from "./controllers/config";
|
||||
import {
|
||||
@@ -139,11 +144,15 @@ export class ClawdisApp extends LitElement {
|
||||
@state() discordForm: DiscordForm = {
|
||||
enabled: true,
|
||||
token: "",
|
||||
dmEnabled: true,
|
||||
allowFrom: "",
|
||||
groupEnabled: false,
|
||||
groupChannels: "",
|
||||
mediaMaxMb: "",
|
||||
historyLimit: "",
|
||||
textChunkLimit: "",
|
||||
replyToMode: "off",
|
||||
guilds: [],
|
||||
actions: { ...defaultDiscordActions },
|
||||
slashEnabled: false,
|
||||
slashName: "",
|
||||
@@ -350,7 +359,8 @@ export class ClawdisApp extends LitElement {
|
||||
].slice(0, 250);
|
||||
|
||||
if (evt.event === "chat") {
|
||||
const state = handleChatEvent(this, evt.payload as unknown);
|
||||
const payload = evt.payload as ChatEventPayload | undefined;
|
||||
const state = handleChatEvent(this, payload);
|
||||
if (state === "final") void loadChatHistory(this);
|
||||
return;
|
||||
}
|
||||
@@ -469,20 +479,26 @@ export class ClawdisApp extends LitElement {
|
||||
if (this.theme !== "system") return;
|
||||
this.applyResolvedTheme(event.matches ? "dark" : "light");
|
||||
};
|
||||
if ("addEventListener" in this.themeMedia) {
|
||||
if (typeof this.themeMedia.addEventListener === "function") {
|
||||
this.themeMedia.addEventListener("change", this.themeMediaHandler);
|
||||
} else {
|
||||
this.themeMedia.addListener(this.themeMediaHandler);
|
||||
return;
|
||||
}
|
||||
const legacy = this.themeMedia as MediaQueryList & {
|
||||
addListener: (cb: (event: MediaQueryListEvent) => void) => void;
|
||||
};
|
||||
legacy.addListener(this.themeMediaHandler);
|
||||
}
|
||||
|
||||
private detachThemeListener() {
|
||||
if (!this.themeMedia || !this.themeMediaHandler) return;
|
||||
if ("removeEventListener" in this.themeMedia) {
|
||||
if (typeof this.themeMedia.removeEventListener === "function") {
|
||||
this.themeMedia.removeEventListener("change", this.themeMediaHandler);
|
||||
} else {
|
||||
this.themeMedia.removeListener(this.themeMediaHandler);
|
||||
return;
|
||||
}
|
||||
const legacy = this.themeMedia as MediaQueryList & {
|
||||
removeListener: (cb: (event: MediaQueryListEvent) => void) => void;
|
||||
};
|
||||
legacy.removeListener(this.themeMediaHandler);
|
||||
this.themeMedia = null;
|
||||
this.themeMediaHandler = null;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export type ChatState = {
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
type ChatEventPayload = {
|
||||
export type ChatEventPayload = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
state: "delta" | "final" | "aborted" | "error";
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
defaultDiscordActions,
|
||||
type DiscordActionForm,
|
||||
type DiscordForm,
|
||||
type DiscordGuildChannelForm,
|
||||
type DiscordGuildForm,
|
||||
type IMessageForm,
|
||||
type SignalForm,
|
||||
type TelegramForm,
|
||||
@@ -96,6 +98,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
||||
const discordDm = (discord.dm ?? {}) as Record<string, unknown>;
|
||||
const slash = (discord.slashCommand ?? {}) as Record<string, unknown>;
|
||||
const discordActions = (discord.actions ?? {}) as Record<string, unknown>;
|
||||
const discordGuilds = discord.guilds;
|
||||
const readAction = (key: keyof DiscordActionForm) =>
|
||||
typeof discordActions[key] === "boolean"
|
||||
? (discordActions[key] as boolean)
|
||||
@@ -103,6 +106,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
||||
state.discordForm = {
|
||||
enabled: typeof discord.enabled === "boolean" ? discord.enabled : true,
|
||||
token: typeof discord.token === "string" ? discord.token : "",
|
||||
dmEnabled: typeof discordDm.enabled === "boolean" ? discordDm.enabled : true,
|
||||
allowFrom: toList(discordDm.allowFrom),
|
||||
groupEnabled:
|
||||
typeof discordDm.groupEnabled === "boolean" ? discordDm.groupEnabled : false,
|
||||
@@ -111,6 +115,57 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
||||
typeof discord.mediaMaxMb === "number" ? String(discord.mediaMaxMb) : "",
|
||||
historyLimit:
|
||||
typeof discord.historyLimit === "number" ? String(discord.historyLimit) : "",
|
||||
textChunkLimit:
|
||||
typeof discord.textChunkLimit === "number"
|
||||
? String(discord.textChunkLimit)
|
||||
: "",
|
||||
replyToMode:
|
||||
discord.replyToMode === "first" || discord.replyToMode === "all"
|
||||
? discord.replyToMode
|
||||
: "off",
|
||||
guilds: Array.isArray(discordGuilds)
|
||||
? []
|
||||
: typeof discordGuilds === "object" && discordGuilds
|
||||
? Object.entries(discordGuilds as Record<string, unknown>).map(
|
||||
([key, value]): DiscordGuildForm => {
|
||||
const entry =
|
||||
value && typeof value === "object"
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
const channelsRaw =
|
||||
entry.channels && typeof entry.channels === "object"
|
||||
? (entry.channels as Record<string, unknown>)
|
||||
: {};
|
||||
const channels = Object.entries(channelsRaw).map(
|
||||
([channelKey, channelValue]): DiscordGuildChannelForm => {
|
||||
const channel =
|
||||
channelValue && typeof channelValue === "object"
|
||||
? (channelValue as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
key: channelKey,
|
||||
allow:
|
||||
typeof channel.allow === "boolean" ? channel.allow : true,
|
||||
requireMention:
|
||||
typeof channel.requireMention === "boolean"
|
||||
? channel.requireMention
|
||||
: false,
|
||||
};
|
||||
},
|
||||
);
|
||||
return {
|
||||
key,
|
||||
slug: typeof entry.slug === "string" ? entry.slug : "",
|
||||
requireMention:
|
||||
typeof entry.requireMention === "boolean"
|
||||
? entry.requireMention
|
||||
: false,
|
||||
users: toList(entry.users),
|
||||
channels,
|
||||
};
|
||||
},
|
||||
)
|
||||
: [],
|
||||
actions: {
|
||||
reactions: readAction("reactions"),
|
||||
stickers: readAction("stickers"),
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
defaultDiscordActions,
|
||||
type DiscordActionForm,
|
||||
type DiscordForm,
|
||||
type DiscordGuildChannelForm,
|
||||
type DiscordGuildForm,
|
||||
type IMessageForm,
|
||||
type SignalForm,
|
||||
type TelegramForm,
|
||||
@@ -233,6 +235,8 @@ export async function saveDiscordConfig(state: ConnectionsState) {
|
||||
const allowFrom = parseList(form.allowFrom);
|
||||
const groupChannels = parseList(form.groupChannels);
|
||||
const dm = { ...(discord.dm ?? {}) } as Record<string, unknown>;
|
||||
if (form.dmEnabled) delete dm.enabled;
|
||||
else dm.enabled = false;
|
||||
if (allowFrom.length > 0) dm.allowFrom = allowFrom;
|
||||
else delete dm.allowFrom;
|
||||
if (form.groupEnabled) dm.groupEnabled = true;
|
||||
@@ -261,6 +265,51 @@ export async function saveDiscordConfig(state: ConnectionsState) {
|
||||
}
|
||||
}
|
||||
|
||||
const chunkLimitRaw = form.textChunkLimit.trim();
|
||||
if (chunkLimitRaw.length === 0) {
|
||||
delete discord.textChunkLimit;
|
||||
} else {
|
||||
const chunkLimit = Number(chunkLimitRaw);
|
||||
if (Number.isFinite(chunkLimit) && chunkLimit > 0) {
|
||||
discord.textChunkLimit = chunkLimit;
|
||||
} else {
|
||||
delete discord.textChunkLimit;
|
||||
}
|
||||
}
|
||||
|
||||
if (form.replyToMode === "off") {
|
||||
delete discord.replyToMode;
|
||||
} else {
|
||||
discord.replyToMode = form.replyToMode;
|
||||
}
|
||||
|
||||
const guildsForm = Array.isArray(form.guilds) ? form.guilds : [];
|
||||
const guilds: Record<string, unknown> = {};
|
||||
guildsForm.forEach((guild: DiscordGuildForm) => {
|
||||
const key = String(guild.key ?? "").trim();
|
||||
if (!key) return;
|
||||
const entry: Record<string, unknown> = {};
|
||||
const slug = String(guild.slug ?? "").trim();
|
||||
if (slug) entry.slug = slug;
|
||||
if (guild.requireMention) entry.requireMention = true;
|
||||
const users = parseList(guild.users);
|
||||
if (users.length > 0) entry.users = users;
|
||||
const channels: Record<string, unknown> = {};
|
||||
const channelForms = Array.isArray(guild.channels) ? guild.channels : [];
|
||||
channelForms.forEach((channel: DiscordGuildChannelForm) => {
|
||||
const channelKey = String(channel.key ?? "").trim();
|
||||
if (!channelKey) return;
|
||||
const channelEntry: Record<string, unknown> = {};
|
||||
if (channel.allow === false) channelEntry.allow = false;
|
||||
if (channel.requireMention) channelEntry.requireMention = true;
|
||||
channels[channelKey] = channelEntry;
|
||||
});
|
||||
if (Object.keys(channels).length > 0) entry.channels = channels;
|
||||
guilds[key] = entry;
|
||||
});
|
||||
if (Object.keys(guilds).length > 0) discord.guilds = guilds;
|
||||
else delete discord.guilds;
|
||||
|
||||
const actions: Partial<DiscordActionForm> = {};
|
||||
const applyAction = (key: keyof DiscordActionForm) => {
|
||||
const value = form.actions[key];
|
||||
|
||||
@@ -11,11 +11,15 @@ export type TelegramForm = {
|
||||
export type DiscordForm = {
|
||||
enabled: boolean;
|
||||
token: string;
|
||||
dmEnabled: boolean;
|
||||
allowFrom: string;
|
||||
groupEnabled: boolean;
|
||||
groupChannels: string;
|
||||
mediaMaxMb: string;
|
||||
historyLimit: string;
|
||||
textChunkLimit: string;
|
||||
replyToMode: "off" | "first" | "all";
|
||||
guilds: DiscordGuildForm[];
|
||||
actions: DiscordActionForm;
|
||||
slashEnabled: boolean;
|
||||
slashName: string;
|
||||
@@ -23,6 +27,20 @@ export type DiscordForm = {
|
||||
slashEphemeral: boolean;
|
||||
};
|
||||
|
||||
export type DiscordGuildForm = {
|
||||
key: string;
|
||||
slug: string;
|
||||
requireMention: boolean;
|
||||
users: string;
|
||||
channels: DiscordGuildChannelForm[];
|
||||
};
|
||||
|
||||
export type DiscordGuildChannelForm = {
|
||||
key: string;
|
||||
allow: boolean;
|
||||
requireMention: boolean;
|
||||
};
|
||||
|
||||
export type DiscordActionForm = {
|
||||
reactions: boolean;
|
||||
stickers: boolean;
|
||||
|
||||
@@ -10,6 +10,24 @@ import type {
|
||||
TelegramForm,
|
||||
} from "../ui-types";
|
||||
|
||||
const discordActionOptions = [
|
||||
{ key: "reactions", label: "Reactions" },
|
||||
{ key: "stickers", label: "Stickers" },
|
||||
{ key: "polls", label: "Polls" },
|
||||
{ key: "permissions", label: "Permissions" },
|
||||
{ key: "messages", label: "Messages" },
|
||||
{ key: "threads", label: "Threads" },
|
||||
{ key: "pins", label: "Pins" },
|
||||
{ key: "search", label: "Search" },
|
||||
{ key: "memberInfo", label: "Member info" },
|
||||
{ key: "roleInfo", label: "Role info" },
|
||||
{ key: "channelInfo", label: "Channel info" },
|
||||
{ key: "voiceStatus", label: "Voice status" },
|
||||
{ key: "events", label: "Events" },
|
||||
{ key: "roles", label: "Role changes" },
|
||||
{ key: "moderation", label: "Moderation" },
|
||||
] satisfies Array<{ key: keyof DiscordActionForm; label: string }>;
|
||||
|
||||
export type ConnectionsProps = {
|
||||
connected: boolean;
|
||||
loading: boolean;
|
||||
@@ -54,24 +72,6 @@ export function renderConnections(props: ConnectionsProps) {
|
||||
const discord = props.snapshot?.discord ?? null;
|
||||
const signal = props.snapshot?.signal ?? null;
|
||||
const imessage = props.snapshot?.imessage ?? null;
|
||||
const discordActionOptions: Array<{ key: keyof DiscordActionForm; label: string }> =
|
||||
[
|
||||
{ key: "reactions", label: "Reactions" },
|
||||
{ key: "stickers", label: "Stickers" },
|
||||
{ key: "polls", label: "Polls" },
|
||||
{ key: "permissions", label: "Permissions" },
|
||||
{ key: "messages", label: "Messages" },
|
||||
{ key: "threads", label: "Threads" },
|
||||
{ key: "pins", label: "Pins" },
|
||||
{ key: "search", label: "Search" },
|
||||
{ key: "memberInfo", label: "Member info" },
|
||||
{ key: "roleInfo", label: "Role info" },
|
||||
{ key: "channelInfo", label: "Channel info" },
|
||||
{ key: "voiceStatus", label: "Voice status" },
|
||||
{ key: "events", label: "Events" },
|
||||
{ key: "roles", label: "Role changes" },
|
||||
{ key: "moderation", label: "Moderation" },
|
||||
];
|
||||
const providerOrder: ProviderKey[] = [
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
@@ -500,6 +500,19 @@ function renderProvider(
|
||||
placeholder="123456789, username#1234"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>DMs enabled</span>
|
||||
<select
|
||||
.value=${props.discordForm.dmEnabled ? "yes" : "no"}
|
||||
@change=${(e: Event) =>
|
||||
props.onDiscordChange({
|
||||
dmEnabled: (e.target as HTMLSelectElement).value === "yes",
|
||||
})}
|
||||
>
|
||||
<option value="yes">Enabled</option>
|
||||
<option value="no">Disabled</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Group DMs</span>
|
||||
<select
|
||||
@@ -546,6 +559,268 @@ function renderProvider(
|
||||
placeholder="20"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Text chunk limit</span>
|
||||
<input
|
||||
.value=${props.discordForm.textChunkLimit}
|
||||
@input=${(e: Event) =>
|
||||
props.onDiscordChange({
|
||||
textChunkLimit: (e.target as HTMLInputElement).value,
|
||||
})}
|
||||
placeholder="2000"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Reply to mode</span>
|
||||
<select
|
||||
.value=${props.discordForm.replyToMode}
|
||||
@change=${(e: Event) =>
|
||||
props.onDiscordChange({
|
||||
replyToMode: (e.target as HTMLSelectElement).value as
|
||||
| "off"
|
||||
| "first"
|
||||
| "all",
|
||||
})}
|
||||
>
|
||||
<option value="off">Off</option>
|
||||
<option value="first">First</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="field full">
|
||||
<span>Guilds</span>
|
||||
<div class="card-sub">
|
||||
Add each guild (id or slug) and optional channel rules. Empty channel
|
||||
entries still allow that channel.
|
||||
</div>
|
||||
<div class="list">
|
||||
${props.discordForm.guilds.map(
|
||||
(guild, guildIndex) => html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span>Guild id / slug</span>
|
||||
<input
|
||||
.value=${guild.key}
|
||||
@input=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
key: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Slug</span>
|
||||
<input
|
||||
.value=${guild.slug}
|
||||
@input=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
slug: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Require mention</span>
|
||||
<select
|
||||
.value=${guild.requireMention ? "yes" : "no"}
|
||||
@change=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
requireMention:
|
||||
(e.target as HTMLSelectElement).value === "yes",
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
>
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">No</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Users allowlist</span>
|
||||
<input
|
||||
.value=${guild.users}
|
||||
@input=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
users: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
placeholder="123456789, username#1234"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
${guild.channels.length
|
||||
? html`
|
||||
<div class="form-grid" style="margin-top: 8px;">
|
||||
${guild.channels.map(
|
||||
(channel, channelIndex) => html`
|
||||
<label class="field">
|
||||
<span>Channel id / slug</span>
|
||||
<input
|
||||
.value=${channel.key}
|
||||
@input=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
const channels = [
|
||||
...(next[guildIndex].channels ?? []),
|
||||
];
|
||||
channels[channelIndex] = {
|
||||
...channels[channelIndex],
|
||||
key: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
channels,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Allow</span>
|
||||
<select
|
||||
.value=${channel.allow ? "yes" : "no"}
|
||||
@change=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
const channels = [
|
||||
...(next[guildIndex].channels ?? []),
|
||||
];
|
||||
channels[channelIndex] = {
|
||||
...channels[channelIndex],
|
||||
allow:
|
||||
(e.target as HTMLSelectElement).value ===
|
||||
"yes",
|
||||
};
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
channels,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
>
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">No</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Require mention</span>
|
||||
<select
|
||||
.value=${channel.requireMention ? "yes" : "no"}
|
||||
@change=${(e: Event) => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
const channels = [
|
||||
...(next[guildIndex].channels ?? []),
|
||||
];
|
||||
channels[channelIndex] = {
|
||||
...channels[channelIndex],
|
||||
requireMention:
|
||||
(e.target as HTMLSelectElement).value ===
|
||||
"yes",
|
||||
};
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
channels,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
>
|
||||
<option value="yes">Yes</option>
|
||||
<option value="no">No</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span> </span>
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => {
|
||||
const next = [
|
||||
...props.discordForm.guilds,
|
||||
];
|
||||
const channels = [
|
||||
...(next[guildIndex].channels ?? []),
|
||||
];
|
||||
channels.splice(channelIndex, 1);
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
channels,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</label>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<span>Channels</span>
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
const channels = [
|
||||
...(next[guildIndex].channels ?? []),
|
||||
{ key: "", allow: true, requireMention: false },
|
||||
];
|
||||
next[guildIndex] = {
|
||||
...next[guildIndex],
|
||||
channels,
|
||||
};
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
>
|
||||
Add channel
|
||||
</button>
|
||||
<button
|
||||
class="btn danger"
|
||||
@click=${() => {
|
||||
const next = [...props.discordForm.guilds];
|
||||
next.splice(guildIndex, 1);
|
||||
props.onDiscordChange({ guilds: next });
|
||||
}}
|
||||
>
|
||||
Remove guild
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
class="btn"
|
||||
style="margin-top: 8px;"
|
||||
@click=${() =>
|
||||
props.onDiscordChange({
|
||||
guilds: [
|
||||
...props.discordForm.guilds,
|
||||
{
|
||||
key: "",
|
||||
slug: "",
|
||||
requireMention: false,
|
||||
users: "",
|
||||
channels: [],
|
||||
},
|
||||
],
|
||||
})}
|
||||
>
|
||||
Add guild
|
||||
</button>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span>Slash command</span>
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user