invites: editable label, /invites/edit route

PATCH /invite/edit lets you edit an invite by giving new values for a
subset of inviteDTO (EditableInviteDTO). Replaces /invite/profile and
/invite/notify, and allows changing (user)label and user expiry as well
as the previously customizable values through other routes. An edit
button next to the code/label allows changing on the invites tab.
This commit is contained in:
Harvey Tindall
2025-12-06 15:38:06 +00:00
parent 4bb116417e
commit 44e4b5fce2
6 changed files with 232 additions and 120 deletions

View File

@@ -142,6 +142,8 @@ export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHt
export const _put = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("PUT", url, data, onreadystatechange, response, statusHandler, noConnectionError);
export const _patch = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean, statusHandler?: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void => _req("PATCH", url, data, onreadystatechange, response, statusHandler, noConnectionError);
export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, noConnectionError: boolean = false): void {
let req = new XMLHttpRequest();
if (window.pages) { url = window.pages.Base + url; }

View File

@@ -1,6 +1,7 @@
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString, SetupCopyButton, addLoader, removeLoader, DateCountdown } from "../modules/common.js";
import { _get, _post, _delete, _patch, toClipboard, toggleLoader, toDateString, SetupCopyButton, addLoader, removeLoader, DateCountdown } from "../modules/common.js";
import { DiscordSearch, DiscordUser, newDiscordSearch } from "../modules/discord.js";
import { reloadProfileNames } from "../modules/profiles.js";
import { HiddenInputField } from "./ui.js";
declare var window: GlobalWindow;
@@ -14,16 +15,18 @@ export const generateCodeLink = (code: string): string => {
class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => {
let state: { [code: string]: { [type: string]: boolean } } = {};
let state = {
code: this.code,
notify_expiry: this.notify_expiry,
notify_creation: this.notify_creation
};
let revertChanges: () => void;
if (checkbox.classList.contains("inv-notify-expiry")) {
revertChanges = () => { this.notify_expiry = !this.notify_expiry };
state[this.code] = { "notify-expiry": this.notify_expiry };
} else {
revertChanges = () => { this.notify_creation = !this.notify_creation };
state[this.code] = { "notify-creation": this.notify_creation };
}
_post("/invites/notify", state, (req: XMLHttpRequest) => {
_patch("/invites/edit", state, (req: XMLHttpRequest) => {
if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) {
revertChanges();
}
@@ -38,18 +41,6 @@ class DOMInvite implements Invite {
}
})
private _label: string = "";
get label(): string { return this._label; }
set label(label: string) {
this._label = label;
const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement;
if (label == "") {
linkEl.textContent = this.code.replace(/-/g, '-');
} else {
linkEl.textContent = label;
}
}
private _userLabel: string = "";
get user_label(): string { return this._userLabel; }
set user_label(label: string) {
@@ -67,16 +58,26 @@ class DOMInvite implements Invite {
}
}
private _label: string = "";
get label(): string { return this._label; }
set label(label: string) {
this._label = label;
if (label == "") {
this.code = this.code;
} else {
this._labelEditor.value = label;
}
}
private _code: string = "None";
get code(): string { return this._code; }
set code(code: string) {
this._code = code;
this._codeLink = generateCodeLink(code);
const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement;
if (this.label == "") {
linkEl.textContent = code.replace(/-/g, '-');
this._labelEditor.value = code.replace(/-/g, '-');
}
linkEl.href = this._codeLink;
this._linkEl.href = this._codeLink;
}
private _codeLink: string;
@@ -311,8 +312,9 @@ class DOMInvite implements Invite {
const select = this._left.querySelector("select") as HTMLSelectElement;
const previous = this.profile;
let profile = select.value;
if (profile == "noProfile") { profile = ""; }
_post("/invites/profile", { "invite": this.code, "profile": profile }, (req: XMLHttpRequest) => {
let state = {code: this.code};
if (profile != "noProfile") { state["profile"] = profile };
_patch("/invites/edit", state, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (!(req.status == 200 || req.status == 204)) {
select.value = previous || "noProfile";
@@ -323,6 +325,22 @@ class DOMInvite implements Invite {
});
}
private _setLabel = () => {
const newLabel = this._labelEditor.value.trim();
const old = this.label;
this.label = newLabel;
let state = {
code: this.code,
label: newLabel
};
_patch("/invites/edit", state, (req: XMLHttpRequest) => {
if (req.readyState != 4) return;
if (req.status != 200 && req.status != 204) {
this.label = old;
}
});
}
private _container: HTMLDivElement;
private _header: HTMLDivElement;
@@ -335,6 +353,10 @@ class DOMInvite implements Invite {
private _right: HTMLDivElement;
private _userTable: HTMLDivElement;
private _linkContainer: HTMLElement;
private _linkEl: HTMLAnchorElement;
private _labelEditor: HiddenInputField;
private _detailsToggle: HTMLInputElement;
// whether the details card is expanded.
@@ -413,11 +435,22 @@ class DOMInvite implements Invite {
this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-full", "items-center", "gap-2", "truncate");
this._codeArea.innerHTML = `
<div class="flex items-center gap-x-4 gap-y-2 truncate">
<a class="invite-link text-black dark:text-white font-mono bg-inherit truncate" href=""></a>
<a class="invite-link-container text-black dark:text-white font-mono bg-inherit truncate"></a>
<button class="invite-copy-button"></button>
</div>
<span>${window.lang.var("strings", "inviteExpiresInTime", "<span class=\"inv-duration\"></span>")}</span>
`;
this._linkContainer = this._codeArea.getElementsByClassName("invite-link-container")[0] as HTMLElement;
this._labelEditor = new HiddenInputField({
container: this._linkContainer,
buttonOnLeft: false,
customContainerHTML: `<a class="hidden-input-content invite-link text-black dark:text-white font-mono bg-inherit truncate"></a>`,
clickAwayShouldSave: true,
onSet: this._setLabel
});
this._linkEl = this._linkContainer.getElementsByClassName("invite-link")[0] as HTMLAnchorElement;
const copyButton = this._codeArea.getElementsByClassName("invite-copy-button")[0] as HTMLButtonElement;
SetupCopyButton(copyButton, this._codeLink);

View File

@@ -44,6 +44,14 @@ export class HiddenInputField {
this._toggle.onclick = () => {
this.editing = !this.editing;
};
this._input.addEventListener("keypress", (e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
this._toggle.click();
}
})
this.setEditing(false, true);
}
@@ -66,6 +74,7 @@ export class HiddenInputField {
this._toggle.classList.add(HiddenInputField.saveClass);
this._toggle.classList.remove(HiddenInputField.editClass);
this._input.classList.remove("hidden");
this._input.focus();
this._content.classList.add("hidden");
} else {
document.removeEventListener("click", this.outerClickListener);