accounts: show table row when clicking into details, more usage of

RadioBasedTabs

when clicking a username in the accounts tab, the details comes up with
a copy of the row you clicked, so you can view logs and edit the user.
Also decided not to use a RadioBasedTab here, instead providing a link
to the activity tab with a search for the user (added the ability to do
that, ?user=username). FIXME: handle deleting the user or just generally
editing them.
This commit is contained in:
Harvey Tindall
2025-12-23 20:37:29 +00:00
parent c3bac6c51c
commit 748acc13c0
6 changed files with 140 additions and 142 deletions

View File

@@ -0,0 +1,24 @@
<thead id="accounts-table-header">
<tr>
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th class="table-inline my-2 grid gap-4 place-items-stretch accounts-header-username">{{ .strings.username }}</th>
{{ if .jellyfinLogin }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-access-jfa">{{ .strings.accessJFA }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-email">{{ .strings.emailAddress }}</th>
{{ if .telegramEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-telegram">Telegram</th>
{{ end }}
{{ if .matrixEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-matrix">Matrix</th>
{{ end }}
{{ if .discordEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
{{ end }}
{{ if .referralsEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-referrals">{{ .strings.referrals }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
</tr>
</thead>

View File

@@ -86,16 +86,7 @@
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">&times;</span></span>
<p class="content">{{ .strings.modifySettingsDescription }}</p>
<div class="flex flex-col gap-4">
<div class="flex flex-row gap-2">
<label class="grow">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="grow">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
</label>
</div>
<div id="modify-user-profile-source"></div>
<div class="select ~neutral @low">
<select id="modify-user-profiles"></select>
</div>
@@ -134,16 +125,7 @@
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-4" id="form-enable-referrals-user" href="">
<span class="heading"><span id="header-enable-referrals-user"></span> <span class="modal-close">&times;</span></span>
<p class="content">{{ .strings.enableReferralsDescription }}</p>
<div class="flex flex-row gap-2">
<label class="grow">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="grow">
<input type="radio" name="enable-referrals-user-source" class="unfocused" id="radio-referrals-use-invite">
<span class="button ~neutral @low supra full-width center">{{ .strings.invite }}</span>
</label>
</div>
<div id="enable-referrals-user-source"></div>
<div class="select ~neutral @low">
<select id="enable-referrals-user-profiles"></select>
</div>
@@ -804,30 +786,7 @@
</div>
<div class="card @low accounts-header overflow-x-scroll transition-opacity transition-display" id="accounts-table">
<table class="table text-base leading-5">
<thead>
<tr>
<th><input type="checkbox" value="" id="accounts-select-all"></th>
<th class="table-inline my-2 grid gap-4 place-items-stretch accounts-header-username">{{ .strings.username }}</th>
{{ if .jellyfinLogin }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-access-jfa">{{ .strings.accessJFA }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-email">{{ .strings.emailAddress }}</th>
{{ if .telegramEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-telegram">Telegram</th>
{{ end }}
{{ if .matrixEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-matrix">Matrix</th>
{{ end }}
{{ if .discordEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
{{ end }}
{{ if .referralsEnabled }}
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-referrals">{{ .strings.referrals }}</th>
{{ end }}
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
</tr>
</thead>
{{ template "accounts-table-header.txt" . }}
<tbody id="accounts-list"></tbody>
</table>
<div id="accounts-loader"></div>

View File

@@ -7,6 +7,8 @@
"invite": "Invite",
"accounts": "Accounts",
"activity": "Activity",
"activityFromJF": "Activity (from Jellyfin)",
"activityFromJfa": "Activity (from jfa-go)",
"settings": "Settings",
"inviteMonths": "Months",
"inviteDays": "Days",

View File

@@ -159,6 +159,7 @@ window.notifications = new notificationBox(document.getElementById("notification
// Determine if url references an invite or account
let isInviteURL = window.invites.isInviteURL();
let isAccountURL = accounts.isAccountURL();
let isActivityURL = activity.isActivityURL();
// load tabs
const tabs: { id: string; url: string; reloader: () => void; unloader?: () => void }[] = [
@@ -183,6 +184,8 @@ const tabs: { id: string; url: string; reloader: () => void; unloader?: () => vo
accounts.loadAccountURL();
// Don't keep loading the same item on every tab refresh
isAccountURL = false;
// Since accounts and activity accept ?user=x, wipe the other one.
isActivityURL = false;
}
accounts.bindPageEvents();
}),
@@ -192,7 +195,15 @@ const tabs: { id: string; url: string; reloader: () => void; unloader?: () => vo
id: "activity",
url: "activity",
reloader: () => {
activity.reload();
activity.reload(() => {
if (isActivityURL) {
activity.loadActivityURL();
// Don't keep loading the same item on every tab refresh
isActivityURL = false;
// Since accounts and activity accept ?user=x, wipe the other one.
isAccountURL = false;
}
});
activity.bindPageEvents();
},
unloader: activity.unbindPageEvents,

View File

@@ -14,7 +14,7 @@ import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd";
import { DiscordUser, newDiscordSearch } from "../modules/discord";
import { SearchConfiguration, QueryType, SearchableItem, SearchableItemDataAttribute } from "../modules/search";
import { HiddenInputField } from "./ui";
import { HiddenInputField, RadioBasedTabSelector } from "./ui";
import { PaginatedList } from "./list";
import { TableRow } from "./row";
@@ -211,6 +211,8 @@ class User extends TableRow implements UserDTO, SearchableItem {
private _referralsEnabled: boolean;
private _referralsEnabledCheck: HTMLElement;
notInList: boolean = false;
focus = () => this._row.scrollIntoView({ behavior: "smooth", block: "center" });
lastNotifyMethod = (): string => {
@@ -244,7 +246,7 @@ class User extends TableRow implements UserDTO, SearchableItem {
setSelected(state: boolean, dispatchEvent: boolean) {
this._selected = state;
this._check.checked = state;
if (dispatchEvent)
if (dispatchEvent && !(this.notInList))
state ? document.dispatchEvent(this._checkEvent()) : document.dispatchEvent(this._uncheckEvent());
}
@@ -931,7 +933,7 @@ class User extends TableRow implements UserDTO, SearchableItem {
};
remove = () => {
if (this.selected) {
if (this.selected && !(this.notInList)) {
document.dispatchEvent(this._uncheckEvent());
}
super.remove();
@@ -984,11 +986,13 @@ export class accountsList extends PaginatedList {
private _enableExpiryNotify = document.getElementById("expiry-extend-enable") as HTMLInputElement;
private _enableExpiryReason = document.getElementById("textarea-extend-enable") as HTMLTextAreaElement;
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;
/*private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;*/
private _modifySettingsProfileSource: RadioBasedTabSelector;
private _enableReferrals = document.getElementById("accounts-enable-referrals") as HTMLSpanElement;
private _enableReferralsProfile = document.getElementById("radio-referrals-use-profile") as HTMLInputElement;
private _enableReferralsInvite = document.getElementById("radio-referrals-use-invite") as HTMLInputElement;
/*private _enableReferralsProfile = document.getElementById("radio-referrals-use-profile") as HTMLInputElement;
private _enableReferralsInvite = document.getElementById("radio-referrals-use-invite") as HTMLInputElement;*/
private _enableReferralsSource: RadioBasedTabSelector;
private _sendPWR = document.getElementById("accounts-send-pwr") as HTMLSpanElement;
private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement;
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
@@ -1052,8 +1056,7 @@ export class accountsList extends PaginatedList {
this.unbindPageEvents();
console.debug("Loading details for ", username, jfId);
this._details.load(
username,
jfId,
this.users.get(jfId),
() => {
this.unbindPageEvents();
this._inDetails = true;
@@ -1164,9 +1167,6 @@ export class accountsList extends PaginatedList {
this.initSearch(searchConfig);
// FIXME: Remove!
(window as any).accs = this;
this._selectAll.checked = false;
this._selectAllState = SelectAllState.None;
this._selectAll.onchange = () => this.cycleSelectAll();
@@ -1195,66 +1195,47 @@ export class accountsList extends PaginatedList {
this._modifySettings.onclick = this.modifyUsers;
this._modifySettings.classList.add("unfocused");
const checkSource = () => {
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
if (this._modifySettingsProfile.checked) {
this._userSelect.parentElement.classList.add("unfocused");
this._profileSelect.parentElement.classList.remove("unfocused");
profileSpan.classList.add("@high");
profileSpan.classList.remove("@low");
userSpan.classList.remove("@high");
userSpan.classList.add("@low");
this._modifySettingsProfileSource = new RadioBasedTabSelector(
document.getElementById("modify-user-profile-source"),
"modify-user-profile-source",
{
name: window.lang.strings("profile"),
id: "profile",
content: this._profileSelect.parentElement,
onShow: () => {
this._applyOmbi?.parentElement.classList.remove("unfocused");
this._applyJellyseerr?.parentElement.classList.remove("unfocused");
} else {
this._userSelect.parentElement.classList.remove("unfocused");
this._profileSelect.parentElement.classList.add("unfocused");
userSpan.classList.add("@high");
userSpan.classList.remove("@low");
profileSpan.classList.remove("@high");
profileSpan.classList.add("@low");
},
},
{
name: window.lang.strings("user"),
id: "user",
content: this._userSelect.parentElement,
onShow: () => {
this._applyOmbi?.parentElement.classList.add("unfocused");
this._applyJellyseerr?.parentElement.classList.add("unfocused");
}
};
this._modifySettingsProfile.onchange = checkSource;
this._modifySettingsUser.onchange = checkSource;
},
},
)
if (window.referralsEnabled) {
const profileSpan = this._enableReferralsProfile.nextElementSibling as HTMLSpanElement;
const inviteSpan = this._enableReferralsInvite.nextElementSibling as HTMLSpanElement;
const checkReferralSource = () => {
console.debug("States:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
if (this._enableReferralsProfile.checked) {
this._referralsInviteSelect.parentElement.classList.add("unfocused");
this._referralsProfileSelect.parentElement.classList.remove("unfocused");
profileSpan.classList.add("@high");
profileSpan.classList.remove("@low");
inviteSpan.classList.remove("@high");
inviteSpan.classList.add("@low");
} else {
this._referralsInviteSelect.parentElement.classList.remove("unfocused");
this._referralsProfileSelect.parentElement.classList.add("unfocused");
inviteSpan.classList.add("@high");
inviteSpan.classList.remove("@low");
profileSpan.classList.remove("@high");
profileSpan.classList.add("@low");
}
};
profileSpan.onclick = () => {
this._enableReferralsProfile.checked = true;
this._enableReferralsInvite.checked = false;
checkReferralSource();
};
inviteSpan.onclick = () => {
this._enableReferralsInvite.checked = true;
this._enableReferralsProfile.checked = false;
checkReferralSource();
};
this._enableReferralsSource = new RadioBasedTabSelector(
document.getElementById("enable-referrals-user-source"),
"enable-referrals-user-source",
{
name: window.lang.strings("profile"),
id: "profile",
content: this._referralsProfileSelect.parentElement,
},
{
name: window.lang.strings("invite"),
id: "invite",
content: this._referralsInviteSelect.parentElement,
},
);
this._enableReferrals.onclick = () => {
this.enableReferrals();
profileSpan.onclick(null);
this._enableReferralsSource.selected = 0;
};
}
@@ -2125,8 +2106,7 @@ export class accountsList extends PaginatedList {
const form = document.getElementById("form-modify-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
this._modifySettingsProfile.checked = true;
this._modifySettingsUser.checked = false;
this._modifySettingsProfileSource.selected = 0;
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
@@ -2141,10 +2121,10 @@ export class accountsList extends PaginatedList {
if (window.jellyseerrEnabled) {
send["jellyseerr"] = this._applyJellyseerr.checked;
}
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
if (this._modifySettingsProfileSource.selected == "profile") {
send["from"] = "profile";
send["profile"] = this._profileSelect.value;
} else if (this._modifySettingsUser.checked && !this._modifySettingsProfile.checked) {
} else if (this._modifySettingsProfileSource.selected == "user") {
send["from"] = "user";
send["id"] = this._userSelect.value;
}
@@ -2216,12 +2196,11 @@ export class accountsList extends PaginatedList {
}
innerHTML += `<option value="${inv.code}">${name}</option>`;
}
this._enableReferralsInvite.checked = true;
this._enableReferralsSource.selected = "invite";
} else {
this._enableReferralsInvite.checked = false;
this._enableReferralsSource.selected = "profile";
innerHTML += `<option>${window.lang.strings("inviteNoInvites")}</option>`;
}
this._enableReferralsProfile.checked = !this._enableReferralsInvite.checked;
this._referralsInviteSelect.innerHTML = innerHTML;
// 2. Profiles
@@ -2243,10 +2222,10 @@ export class accountsList extends PaginatedList {
users: list,
};
// console.log("profile:", this._enableReferralsProfile.checked, this._enableReferralsInvite.checked);
if (this._enableReferralsProfile.checked && !this._enableReferralsInvite.checked) {
if (this._enableReferralsSource.selected == "profile") {
send["from"] = "profile";
send["profile"] = this._referralsProfileSelect.value;
} else if (this._enableReferralsInvite.checked && !this._enableReferralsProfile.checked) {
} else if (this._enableReferralsSource.selected == "invite") {
send["from"] = "invite";
send["id"] = this._referralsInviteSelect.value;
}
@@ -2278,8 +2257,7 @@ export class accountsList extends PaginatedList {
},
);
};
this._enableReferralsProfile.checked = true;
this._enableReferralsInvite.checked = false;
this._enableReferralsSource.selected = 0;
this._referralsExpiry.checked = false;
window.modals.enableReferralsUser.show();
};
@@ -2856,7 +2834,9 @@ class UserInfo extends PaginatedList {
private _hidden: boolean = true;
private _table: HTMLElement;
private _card: HTMLElement;
private _rowArea: HTMLElement;
private _noResults: HTMLElement;
private _link: HTMLAnchorElement;
get entries(): Map<string, ActivityLogEntry> {
return this._search.items as Map<string, ActivityLogEntry>;
}
@@ -2867,20 +2847,14 @@ class UserInfo extends PaginatedList {
card.classList.add("unfocused");
card.innerHTML = `
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-2 justify-start">
</div>
<div class="flex flex-row gap-2">
</div>
<div class="flex flex-col gap-1">
<div class="flex flex-row gap-2">
<label class="grow">
<input type="radio" name="jf-activity-source" class="jf-activity-source-jellyfin unfocused" checked>
<span class="button ~neutral @high supra w-full text-center">${window.lang.strings("jellyfin")}</span>
</label>
<label class="grow">
<input type="radio" name="jf-activity-source" class="jf-activity-source-jfa-go unfocused">
<span class="button ~neutral @low supra w-full text-center">jfa-go</span>
</label>
<table class="table text-base leading-5">
<thead class="user-info-table-header"></thead>
<tbody class="user-info-row"></tbody>
</table>
<div class="flex flex-col gap-2">
<div class="jf-activity-source flex flex-row justify-between gap-2">
<h3 class="heading text-lg">${window.lang.strings("activityFromJF")}</h3>
<a role="link" tabindex="0" class="jf-activity-jfa-link hover:underline cursor-pointer button ~info @high flex flex-row gap-1 items-baseline">${window.lang.strings("activityFromJfa")}<i class="icon ri-external-link-line text-current"></i></a>
</div>
<!-- <h2 class="heading text-2xl">${window.lang.strings("activity")}</h2> -->
<div class="card @low overflow-x-scroll jf-activity-table">
@@ -2962,11 +2936,18 @@ class UserInfo extends PaginatedList {
});
this._hidden = true;
this._card = card;
this._rowArea = this._card.getElementsByClassName("user-info-row")[0] as HTMLElement;
const realHead = document.getElementById("accounts-table-header") as HTMLElement;
const cloneHead = realHead.cloneNode(true) as HTMLElement;
// Remove "select all" check from header
cloneHead.querySelector("input[type=checkbox]").remove();
const head = this._card.getElementsByClassName("user-info-table-header")[0] as HTMLElement;
head.replaceWith(cloneHead);
this._table = this._card.getElementsByClassName("jf-activity-table")[0] as HTMLElement;
this._container = this._table.getElementsByClassName("jf-activity-table-content")[0] as HTMLElement;
this._back = document.getElementById("user-details-back") as HTMLButtonElement;
this._noResults = this._card.getElementsByClassName("jf-activity-no-activity")[0] as HTMLElement;
this._link = this._card.getElementsByClassName("jf-activity-jfa-link")[0] as HTMLAnchorElement;
let searchConfig: SearchConfiguration = {
queries: {},
setVisibility: null,
@@ -2995,9 +2976,16 @@ class UserInfo extends PaginatedList {
this._reload(callback);
};
load = (username: string, jfId: string, onLoad?: () => void, onBack?: () => void) => {
this.username = username;
this.jfId = jfId;
load = (user: User, onLoad?: () => void, onBack?: () => void) => {
this.username = user.name;
this.jfId = user.id;
const clone = new User(user);
clone.notInList = true;
clone.setSelected(true, false)
this._rowArea.replaceChildren(clone.asElement());
this._link.setAttribute("data-id", this.jfId)
this._link.href = `${window.pages.Base}${window.pages.Admin}/activity?user=${this.username}`
if (onBack) {
this._back.classList.remove("unfocused");
this._back.onclick = () => onBack();

View File

@@ -687,4 +687,18 @@ export class activityList extends PaginatedList {
this._keepSearchingDescription.classList.add("unfocused");
}
};*/
isActivityURL = () => {
const urlParams = new URLSearchParams(window.location.search);
const username = urlParams.get("user");
return Boolean(username);
};
loadActivityURL = () => {
const urlParams = new URLSearchParams(window.location.search);
const username = urlParams.get("user");
this._c.searchBox.value = `user:"${username}"`;
this._search.onSearchBoxChange();
this._search.onServerSearch();
};
}