feat(dashboard): add redirects page
This commit is contained in:
parent
32ddb9fae2
commit
cb425fe3cd
7 changed files with 400 additions and 62 deletions
|
|
@ -1,11 +1,40 @@
|
|||
export const TOKEN_KEY = "catalogger-token";
|
||||
|
||||
export default async function apiFetch<T>(
|
||||
method: "GET" | "POST" | "PATCH" | "DELETE",
|
||||
export type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";
|
||||
|
||||
export async function fastFetch(
|
||||
method: HttpMethod,
|
||||
path: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
body: any = null,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const headers = {
|
||||
...(body != null
|
||||
? { "Content-Type": "application/json; charset=utf-8" }
|
||||
: {}),
|
||||
...(token ? { Authorization: token } : {}),
|
||||
};
|
||||
|
||||
const reqBody = body ? JSON.stringify(body) : undefined;
|
||||
|
||||
console.debug("Sending", method, "request to", path, "with body", reqBody);
|
||||
|
||||
const resp = await fetch(path, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers,
|
||||
});
|
||||
if (resp.status < 200 || resp.status > 299)
|
||||
throw (await resp.json()) as ApiError;
|
||||
}
|
||||
|
||||
export default async function apiFetch<T>(
|
||||
method: HttpMethod,
|
||||
path: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
body: any = null,
|
||||
): Promise<T> {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const headers = {
|
||||
...(body != null
|
||||
|
|
|
|||
|
|
@ -8,7 +8,32 @@ export const load = async ({ params }) => {
|
|||
`/api/guilds/${params.guildId}`,
|
||||
);
|
||||
|
||||
return { guild };
|
||||
const options = [];
|
||||
|
||||
const channelsWithoutCategory = guild.channels_without_category.filter(
|
||||
(c) => c.can_log_to,
|
||||
);
|
||||
if (channelsWithoutCategory.length > 0)
|
||||
options.push({
|
||||
label: "(no category)",
|
||||
options: channelsWithoutCategory.map((c) => ({
|
||||
value: c.id,
|
||||
label: `#${c.name}`,
|
||||
})),
|
||||
});
|
||||
|
||||
options.push(
|
||||
...guild.categories
|
||||
.map((cat) => ({
|
||||
label: cat.name,
|
||||
options: cat.channels
|
||||
.filter((c) => c.can_log_to)
|
||||
.map((c) => ({ value: c.id, label: `#${c.name}` })),
|
||||
}))
|
||||
.filter((c) => c.options.length > 0),
|
||||
);
|
||||
|
||||
return { guild, options };
|
||||
} catch (e) {
|
||||
const err = e as ApiError;
|
||||
console.log("Fetching guild", params.guildId, ":", e);
|
||||
|
|
|
|||
|
|
@ -1,89 +1,85 @@
|
|||
<script lang="ts">
|
||||
import { Label } from "@sveltestrap/sveltestrap";
|
||||
import type { FullGuild, GuildChannelConfig } from "$lib/api";
|
||||
import type { GuildChannelConfig } from "$lib/api";
|
||||
import type { PageData } from "./$types";
|
||||
import ChannelSelect from "./ChannelSelect.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: channels = data.guild.config as GuildChannelConfig;
|
||||
|
||||
const logChannelOptions = (g: FullGuild) => {
|
||||
let options = [];
|
||||
|
||||
const channelsWithoutCategory = g.channels_without_category.filter(
|
||||
(c) => c.can_log_to,
|
||||
);
|
||||
if (channelsWithoutCategory.length > 0)
|
||||
options.push({
|
||||
label: "(no category)",
|
||||
options: channelsWithoutCategory.map((c) => ({
|
||||
value: c.id,
|
||||
label: `#${c.name}`,
|
||||
})),
|
||||
});
|
||||
|
||||
options.push(
|
||||
...data.guild.categories
|
||||
.map((cat) => ({
|
||||
label: cat.name,
|
||||
options: cat.channels
|
||||
.filter((c) => c.can_log_to)
|
||||
.map((c) => ({ value: c.id, label: `#${c.name}` })),
|
||||
}))
|
||||
.filter((c) => c.options.length > 0),
|
||||
);
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
$: options = logChannelOptions(data.guild);
|
||||
</script>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3">
|
||||
<div class="p-2">
|
||||
<Label><strong>Server changes</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_update} {options} />
|
||||
<ChannelSelect bind:value={channels.guild_update} options={data.options} />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Changes to emotes</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_emojis_update} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_emojis_update}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>New roles</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_role_create} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_role_create}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Changes to roles</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_role_update} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_role_update}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Deleted roles</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_role_delete} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_role_delete}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>New channels</strong></Label>
|
||||
<ChannelSelect bind:value={channels.channel_create} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.channel_create}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Changes to channels</strong></Label>
|
||||
<ChannelSelect bind:value={channels.channel_update} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.channel_update}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Deleted channels</strong></Label>
|
||||
<ChannelSelect bind:value={channels.channel_delete} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.channel_delete}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Users joining</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_member_add} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_member_add}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Users leaving</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_member_remove} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_member_remove}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Changes to key roles</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_update} {options} />
|
||||
<ChannelSelect bind:value={channels.guild_update} options={data.options} />
|
||||
<small>
|
||||
Key roles can be designated with the <code>/key-roles</code> command or the
|
||||
"Key roles" tab above.
|
||||
|
|
@ -91,48 +87,72 @@
|
|||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Member name changes</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_member_nick_update} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_member_nick_update}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Member icon changes</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_member_avatar_update} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_member_avatar_update}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Timeouts</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_member_timeout} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_member_timeout}
|
||||
options={data.options}
|
||||
/>
|
||||
<small> Note that this will not log timeouts naturally expiring. </small>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Kicked users</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_member_kick} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_member_kick}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Banned users</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_ban_add} {options} />
|
||||
<ChannelSelect bind:value={channels.guild_ban_add} options={data.options} />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Unbanned users</strong></Label>
|
||||
<ChannelSelect bind:value={channels.guild_ban_remove} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.guild_ban_remove}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>New invites</strong></Label>
|
||||
<ChannelSelect bind:value={channels.invite_create} {options} />
|
||||
<ChannelSelect bind:value={channels.invite_create} options={data.options} />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Deleted invites</strong></Label>
|
||||
<ChannelSelect bind:value={channels.invite_delete} {options} />
|
||||
<ChannelSelect bind:value={channels.invite_delete} options={data.options} />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Edited messages</strong></Label>
|
||||
<ChannelSelect bind:value={channels.message_update} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.message_update}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Deleted messages</strong></Label>
|
||||
<ChannelSelect bind:value={channels.message_delete} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.message_delete}
|
||||
options={data.options}
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<Label><strong>Bulk deleted messages</strong></Label>
|
||||
<ChannelSelect bind:value={channels.message_delete_bulk} {options} />
|
||||
<ChannelSelect
|
||||
bind:value={channels.message_delete_bulk}
|
||||
options={data.options}
|
||||
/>
|
||||
<small> This will only be triggered by bots, not normal users. </small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
type Group = { label: string; options: Option[] };
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
export let options: Group[];
|
||||
export let options: Array<Group | Option>;
|
||||
export let placeholder: string = "Select a channel";
|
||||
export let value: string;
|
||||
export let value: string | null;
|
||||
</script>
|
||||
|
||||
<Svelecte
|
||||
|
|
|
|||
|
|
@ -1 +1,173 @@
|
|||
<h1>aaaaaaaaa</h1>
|
||||
<script lang="ts">
|
||||
import type { ApiError, FullGuild } from "$lib/api";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
import ChannelSelect from "../ChannelSelect.svelte";
|
||||
import { fastFetch } from "$lib/api";
|
||||
import { addToast } from "$lib/toast";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: redirects = data.guild.config.redirects;
|
||||
|
||||
const makeSourceOptions = (
|
||||
guild: FullGuild,
|
||||
redirects: Record<string, string>,
|
||||
) => {
|
||||
const options = [];
|
||||
|
||||
// We shouldn't list channels that are already being redirected.
|
||||
const redirectedChannels = Object.keys(redirects);
|
||||
|
||||
options.push(
|
||||
...guild.categories
|
||||
.filter((cat) => !redirectedChannels.some((k) => k === cat.id))
|
||||
.map((cat) => ({
|
||||
label: `${cat.name} (category)`,
|
||||
value: cat.id,
|
||||
})),
|
||||
);
|
||||
|
||||
// Filter these channels
|
||||
const channelsWithoutCategory = guild.channels_without_category.filter(
|
||||
(c) => c.can_redirect_from && !redirectedChannels.some((k) => k === c.id),
|
||||
);
|
||||
if (channelsWithoutCategory.length > 0)
|
||||
options.push({
|
||||
label: "(no category)",
|
||||
options: channelsWithoutCategory.map((c) => ({
|
||||
value: c.id,
|
||||
label: `#${c.name}`,
|
||||
})),
|
||||
});
|
||||
|
||||
options.push(
|
||||
...guild.categories
|
||||
.map((cat) => ({
|
||||
label: cat.name,
|
||||
options: cat.channels
|
||||
.filter(
|
||||
(c) =>
|
||||
c.can_redirect_from &&
|
||||
!redirectedChannels.some((k) => k === c.id),
|
||||
)
|
||||
.map((c) => ({ value: c.id, label: `#${c.name}` })),
|
||||
}))
|
||||
.filter((c) => c.options.length > 0),
|
||||
);
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
$: allChannels = [
|
||||
...data.guild.channels_without_category.map((c) => ({
|
||||
id: c.id,
|
||||
name: `#${c.name}`,
|
||||
})),
|
||||
...data.guild.categories.map((cat) => ({ id: cat.id, name: cat.name })),
|
||||
...data.guild.categories.flatMap((cat) =>
|
||||
cat.channels.map((c) => ({ id: c.id, name: `#${c.name}` })),
|
||||
),
|
||||
];
|
||||
|
||||
const channelName = (id: string) =>
|
||||
allChannels.find((c) => c.id === id)?.name || `(unknown channel ${id})`;
|
||||
|
||||
$: sourceOptions = makeSourceOptions(data.guild, redirects);
|
||||
$: targetOptions = data.options;
|
||||
|
||||
let source: string | null = null;
|
||||
let target: string | null = null;
|
||||
|
||||
const addRedirect = async () => {
|
||||
if (!source || !target) {
|
||||
addToast({
|
||||
header: "Can't add redirect",
|
||||
body: "You haven't selected a source and target channel.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fastFetch("POST", `/api/guilds/${data.guild.id}/redirects`, {
|
||||
source,
|
||||
target,
|
||||
});
|
||||
|
||||
data.guild.config.redirects[source] = target;
|
||||
|
||||
addToast({ body: "Successfully added redirect." });
|
||||
} catch (e) {
|
||||
addToast({
|
||||
header: "Error adding redirect",
|
||||
body:
|
||||
(e as ApiError).message || "Unknown error. Please try again later.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeRedirect = async (id: string) => {
|
||||
try {
|
||||
await fastFetch("DELETE", `/api/guilds/${data.guild.id}/redirects/${id}`);
|
||||
|
||||
delete data.guild.config.redirects[id];
|
||||
data.guild.config.redirects = data.guild.config.redirects;
|
||||
|
||||
addToast({ body: "Successfully removed redirect." });
|
||||
} catch (e) {
|
||||
addToast({
|
||||
header: "Error removing redirect",
|
||||
body:
|
||||
(e as ApiError).message || "Unknown error. Please try again later.",
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<h3>Add new redirect</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<Label><strong>Source</strong></Label>
|
||||
<ChannelSelect bind:value={source} options={sourceOptions} />
|
||||
</div>
|
||||
|
||||
<div class="col-sm">
|
||||
<Label><strong>Target</strong></Label>
|
||||
<ChannelSelect bind:value={target} options={targetOptions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={!source || !target}
|
||||
on:click={() => addRedirect()}>Add redirect</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if Object.keys(redirects).length > 0}
|
||||
<h3>Existing redirects</h3>
|
||||
|
||||
<ListGroup>
|
||||
{#each Object.keys(redirects) as redirectSource}
|
||||
<ListGroupItem class="d-flex justify-content-between align-items-center">
|
||||
<span
|
||||
>{channelName(redirectSource)} ➜ {channelName(
|
||||
redirects[redirectSource],
|
||||
)}</span
|
||||
>
|
||||
<Button color="link" on:click={() => removeRedirect(redirectSource)}>
|
||||
Remove redirect
|
||||
</Button>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{:else}
|
||||
<p>There are no redirects configured.</p>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue