mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-03-18 13:40:31 +01:00
jf_activity: paginated list in ui
added POST route for pagination to activity route, a count route, and modified Search and PaginatedList a bit to support lists without search fields (essentially just running an empty search). Visible by clicking on a user's name in the accounts tab.
This commit is contained in:
74
api-users.go
74
api-users.go
@@ -1420,7 +1420,30 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
gc.JSON(code, errors)
|
||||
}
|
||||
|
||||
// @Summary Get the latest Jellyfin/Emby activities related to the given user ID. Returns as many as the server has recorded.
|
||||
// @Summary Gets the number of Jellyfin/Emby activities stored by jfa-go related to the given user ID. As the total collected by jfa-go is limited, this may not include all those held by Jellyfin.
|
||||
// @Produce json
|
||||
// @Success 200 {object} PageCountDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Param id path string true "id of user to fetch activities of."
|
||||
// @Router /users/{id}/activities/jellyfin/count [get]
|
||||
// @Security Bearer
|
||||
// @tags Users
|
||||
func (app *appContext) CountJFActivitesForUser(gc *gin.Context) {
|
||||
userID := gc.Param("id")
|
||||
if userID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
activities, err := app.jf.activity.ByUserID(userID)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetJFActivities, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
gc.JSON(200, PageCountDTO{Count: uint64(len(activities))})
|
||||
}
|
||||
|
||||
// @Summary Get the latest Jellyfin/Emby activities related to the given user ID. Returns as many as the server has recorded. As the total collected by jfa-go is limited, this may not include all those held by Jellyfin.
|
||||
// @Produce json
|
||||
// @Success 200 {object} ActivityLogEntriesDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
@@ -1429,12 +1452,12 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Users
|
||||
func (app *appContext) GetJFActivitesForUser(gc *gin.Context) {
|
||||
userId := gc.Param("id")
|
||||
if userId == "" {
|
||||
userID := gc.Param("id")
|
||||
if userID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
activities, err := app.jf.activity.ByUserID(userId)
|
||||
activities, err := app.jf.activity.ByUserID(userID)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetJFActivities, err)
|
||||
respondBool(400, false, gc)
|
||||
@@ -1450,3 +1473,46 @@ func (app *appContext) GetJFActivitesForUser(gc *gin.Context) {
|
||||
app.debug.Printf(lm.GotNEntries, len(activities))
|
||||
gc.JSON(200, out)
|
||||
}
|
||||
|
||||
// @Summary Get the latest Jellyfin/Emby activities related to the given user ID, paginated. As the total collected by jfa-go is limited, this may not include all those held by Jellyfin.
|
||||
// @Produce json
|
||||
// @Param PaginatedReqDTO body PaginatedReqDTO true "pagination parameters"
|
||||
// @Success 200 {object} PaginatedActivityLogEntriesDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Param id path string true "id of user to fetch activities of."
|
||||
// @Router /users/{id}/activities/jellyfin [post]
|
||||
// @Security Bearer
|
||||
// @tags Users
|
||||
func (app *appContext) GetPaginatedJFActivitesForUser(gc *gin.Context) {
|
||||
var req PaginatedReqDTO
|
||||
gc.BindJSON(&req)
|
||||
userID := gc.Param("id")
|
||||
if userID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
activities, err := app.jf.activity.ByUserID(userID)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetJFActivities, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
out := PaginatedActivityLogEntriesDTO{}
|
||||
startIndex := req.Page * req.Limit
|
||||
if startIndex >= len(activities) {
|
||||
out.LastPage = true
|
||||
gc.JSON(200, out)
|
||||
return
|
||||
}
|
||||
endIndex := min(startIndex+req.Limit, len(activities))
|
||||
activities = activities[startIndex:endIndex]
|
||||
|
||||
out.Entries = make([]ActivityLogEntryDTO, len(activities))
|
||||
out.LastPage = len(activities) != req.Limit
|
||||
for i := range activities {
|
||||
out.Entries[i].ActivityLogEntry = activities[i]
|
||||
out.Entries[i].Date = activities[i].Date.Unix()
|
||||
}
|
||||
app.debug.Printf(lm.GotNEntries, len(activities))
|
||||
gc.JSON(200, out)
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -42,7 +42,7 @@ require (
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-20251123165523-7c9f91711460
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20251123165523-7c9f91711460
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20251123165523-7c9f91711460
|
||||
github.com/hrfee/mediabrowser v0.3.35
|
||||
github.com/hrfee/mediabrowser v0.3.36
|
||||
github.com/hrfee/simple-template v1.1.0
|
||||
github.com/itchyny/timefmt-go v0.1.7
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
|
||||
2
go.sum
2
go.sum
@@ -200,6 +200,8 @@ github.com/hrfee/mediabrowser v0.3.34 h1:AKnd1V9wt+KWZmHDjj1GMkCgcgcpBKxPw5iUcYg
|
||||
github.com/hrfee/mediabrowser v0.3.34/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/hrfee/mediabrowser v0.3.35 h1:xEq4cL96Di0G+S3ONBH1HHeQJU6IfUMZiaeGeuJSFS8=
|
||||
github.com/hrfee/mediabrowser v0.3.35/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/hrfee/mediabrowser v0.3.36 h1:erYWzmaz4b6wfxEfeEWQHLMI9fSpGDy1m7NxkQmIVsw=
|
||||
github.com/hrfee/mediabrowser v0.3.36/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/hrfee/simple-template v1.1.0 h1:PNQDTgc2H0s19/pWuhRh4bncuNJjPrW0fIX77YtY78M=
|
||||
github.com/hrfee/simple-template v1.1.0/go.mod h1:s9a5QgfqbmT7j9WCC3GD5JuEqvihBEohyr+oYZmr4bA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
|
||||
@@ -806,54 +806,59 @@
|
||||
<span class="button ~info @low center unfocused " id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
||||
<span class="button ~critical @low center " id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
<div class="card @low accounts-header overflow-x-scroll">
|
||||
<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>
|
||||
<tbody id="accounts-list"></tbody>
|
||||
</table>
|
||||
<div id="accounts-loader"></div>
|
||||
<div class="unfocused h-[100%] my-3" id="accounts-not-found">
|
||||
<div class="flex flex-col gap-2 h-[100%] justify-center items-center">
|
||||
<span class="text-2xl font-medium italic text-center">{{ .strings.noResultsFound }}</span>
|
||||
<span class="text-sm font-light italic unfocused text-center" id="accounts-no-local-results">{{ .strings.noResultsFoundLocally }}</span>
|
||||
<div class="flex flex-row">
|
||||
<button class="button ~neutral @low accounts-search-clear gap-1">
|
||||
<i class="ri-close-line"></i>
|
||||
<span>{{ .strings.clearSearch }}</span>
|
||||
</button>
|
||||
<div id="accounts-table-details-container">
|
||||
<div class="card @low accounts-details unfocused">
|
||||
<div id="accounts-loader"></div>
|
||||
</div>
|
||||
<div class="card @low accounts-header overflow-x-scroll" 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>
|
||||
<tbody id="accounts-list"></tbody>
|
||||
</table>
|
||||
<div id="accounts-loader"></div>
|
||||
<div class="unfocused h-[100%] my-3" id="accounts-not-found">
|
||||
<div class="flex flex-col gap-2 h-[100%] justify-center items-center">
|
||||
<span class="text-2xl font-medium italic text-center">{{ .strings.noResultsFound }}</span>
|
||||
<span class="text-sm font-light italic unfocused text-center" id="accounts-no-local-results">{{ .strings.noResultsFoundLocally }}</span>
|
||||
<div class="flex flex-row">
|
||||
<button class="button ~neutral @low accounts-search-clear gap-1">
|
||||
<i class="ri-close-line"></i>
|
||||
<span>{{ .strings.clearSearch }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 my-3 justify-center">
|
||||
<button class="button ~neutral @low" id="accounts-load-more">{{ .strings.loadMore }}</button>
|
||||
<button class="button ~neutral @low accounts-load-all">{{ .strings.loadAll }}</button>
|
||||
<button class="button ~info @low center accounts-search-server gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
|
||||
<i class="ri-search-line"></i>
|
||||
<span>{{ .strings.searchAllRecords }}</span>
|
||||
</button>
|
||||
<div class="flex flex-row gap-2 my-3 justify-center">
|
||||
<button class="button ~neutral @low" id="accounts-load-more">{{ .strings.loadMore }}</button>
|
||||
<button class="button ~neutral @low accounts-load-all">{{ .strings.loadAll }}</button>
|
||||
<button class="button ~info @low center accounts-search-server gap-1" aria-label="{{ .strings.searchAllRecords }}" text="{{ .strings.searchAllRecords }}">
|
||||
<i class="ri-search-line"></i>
|
||||
<span>{{ .strings.searchAllRecords }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<span class="button ~warning h-min" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
</div>
|
||||
<a class="button ~info unfocused h-min flex flex-row gap-2" href="/" id="admin-back-button"><i class="ri-arrow-left-fill"></i>{{ .strings.admin }}</a>
|
||||
<a class="button ~info unfocused h-min flex flex-row gap-1" href="/" id="admin-back-button"><i class="ri-arrow-left-fill"></i>{{ .strings.admin }}</a>
|
||||
</div>
|
||||
<div class="card @low dark:~d_neutral" id="card-user">
|
||||
<span class="heading flex flex-row gap-4"></span>
|
||||
|
||||
@@ -224,7 +224,13 @@
|
||||
"restartRequired": "Restart required",
|
||||
"required": "Required",
|
||||
"syntax": "Syntax",
|
||||
"syntaxDescription": "Variables denoted as {variable}. If statements can evaluate truthfulness (e.g. {ifTruth}) or make basic comparisons (e.g. {ifCompare})"
|
||||
"syntaxDescription": "Variables denoted as {variable}. If statements can evaluate truthfulness (e.g. {ifTruth}) or make basic comparisons (e.g. {ifCompare})",
|
||||
"info": "Info",
|
||||
"debug": "Debug",
|
||||
"warn": "Warn",
|
||||
"error": "Error",
|
||||
"fatal": "Fatal",
|
||||
"severity": "Severity"
|
||||
},
|
||||
"notifications": {
|
||||
"pathCopied": "Full path copied to clipboard.",
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
"inviteRemainingUses": "Remaining uses",
|
||||
"internal": "Internal",
|
||||
"external": "External",
|
||||
"failed": "Failed"
|
||||
"failed": "Failed",
|
||||
"details": "Details",
|
||||
"type": "Type",
|
||||
"other": "Other",
|
||||
"back": "Back",
|
||||
"none": "None"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "The username and/or password were left blank.",
|
||||
|
||||
27
models.go
27
models.go
@@ -174,15 +174,31 @@ type respUser struct {
|
||||
ReferralsEnabled bool `json:"referrals_enabled"`
|
||||
}
|
||||
|
||||
// ServerSearchReqDTO is a usual SortablePaginatedReqDTO with added fields for searching and filtering.
|
||||
type ServerSearchReqDTO struct {
|
||||
SortablePaginatedReqDTO
|
||||
ServerFilterReqDTO
|
||||
}
|
||||
|
||||
// ServerFilterReqDTO provides search terms and queries to a search or count route.
|
||||
type ServerFilterReqDTO struct {
|
||||
SearchTerms []string `json:"searchTerms"`
|
||||
Queries []QueryDTO `json:"queries"`
|
||||
}
|
||||
|
||||
type PaginatedDTO struct {
|
||||
LastPage bool `json:"last_page"`
|
||||
}
|
||||
|
||||
type PaginatedReqDTO struct {
|
||||
Limit int `json:"limit"`
|
||||
Page int `json:"page"` // zero-indexed
|
||||
type SortablePaginatedReqDTO struct {
|
||||
SortByField string `json:"sortByField"`
|
||||
Ascending bool `json:"ascending"`
|
||||
PaginatedReqDTO
|
||||
}
|
||||
|
||||
type PaginatedReqDTO struct {
|
||||
Limit int `json:"limit"`
|
||||
Page int `json:"page"` // zero-indexed
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
@@ -520,6 +536,11 @@ type ActivityLogEntriesDTO struct {
|
||||
Entries []ActivityLogEntryDTO `json:"entries"`
|
||||
}
|
||||
|
||||
type PaginatedActivityLogEntriesDTO struct {
|
||||
ActivityLogEntriesDTO
|
||||
PaginatedDTO
|
||||
}
|
||||
|
||||
type ActivityLogEntryDTO struct {
|
||||
mediabrowser.ActivityLogEntry
|
||||
Date int64 `json:"Date"`
|
||||
|
||||
@@ -207,6 +207,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.POST(p+"/users/extend", app.ExtendExpiry)
|
||||
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
||||
api.GET(p+"/users/:id/activities/jellyfin", app.GetJFActivitesForUser)
|
||||
api.GET(p+"/users/:id/activities/jellyfin/count", app.CountJFActivitesForUser)
|
||||
api.POST(p+"/users/:id/activities/jellyfin", app.GetPaginatedJFActivitesForUser)
|
||||
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
||||
api.POST(p+"/invites", app.GenerateInvite)
|
||||
api.GET(p+"/invites", app.GetInvites)
|
||||
|
||||
@@ -16,6 +16,7 @@ import { DiscordUser, newDiscordSearch } from "../modules/discord";
|
||||
import { SearchConfiguration, QueryType, SearchableItem, SearchableItemDataAttribute } from "../modules/search";
|
||||
import { HiddenInputField } from "./ui";
|
||||
import { PaginatedList } from "./list";
|
||||
import { TableRow } from "./row";
|
||||
|
||||
declare var window: GlobalWindow;
|
||||
|
||||
@@ -177,9 +178,8 @@ const queries = (): { [field: string]: QueryType } => {
|
||||
};
|
||||
};
|
||||
|
||||
class User implements UserDTO, SearchableItem {
|
||||
class User extends TableRow implements UserDTO, SearchableItem {
|
||||
private _id = "";
|
||||
private _row: HTMLTableRowElement;
|
||||
private _check: HTMLInputElement;
|
||||
private _username: HTMLSpanElement;
|
||||
private _admin: HTMLSpanElement;
|
||||
@@ -692,12 +692,11 @@ class User implements UserDTO, SearchableItem {
|
||||
private _uncheckEvent = () => new CustomEvent("accountUncheckEvent", { detail: this.id });
|
||||
|
||||
constructor(user: UserDTO) {
|
||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||
this._row.classList.add("border-b", "border-dashed", "dark:border-dotted", "dark:border-stone-700");
|
||||
super();
|
||||
let innerHTML = `
|
||||
<td><input type="checkbox" class="accounts-select-user" value=""></td>
|
||||
<td><div class="flex flex-row gap-2 items-center">
|
||||
<span class="accounts-username"></span>
|
||||
<span class="accounts-username hover:underline hover:cursor-pointer"></span>
|
||||
<div class="flex flex-row gap-2 items-baseline">
|
||||
<span class="accounts-label-container" title="${window.lang.strings("label")}"></span>
|
||||
<span class="accounts-admin chip ~info hidden"></span>
|
||||
@@ -743,6 +742,15 @@ class User implements UserDTO, SearchableItem {
|
||||
this._check = this._row.querySelector("input[type=checkbox].accounts-select-user") as HTMLInputElement;
|
||||
this._accounts_admin = this._row.querySelector("input[type=checkbox].accounts-access-jfa") as HTMLInputElement;
|
||||
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
||||
this._username.onclick = () =>
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("accounts-show-details", {
|
||||
detail: {
|
||||
username: this.name,
|
||||
id: this.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
|
||||
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
|
||||
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
|
||||
@@ -922,14 +930,11 @@ class User implements UserDTO, SearchableItem {
|
||||
this.referrals_enabled = user.referrals_enabled;
|
||||
};
|
||||
|
||||
asElement = (): HTMLTableRowElement => {
|
||||
return this._row;
|
||||
};
|
||||
remove = () => {
|
||||
if (this.selected) {
|
||||
document.dispatchEvent(this._uncheckEvent());
|
||||
}
|
||||
this._row.remove();
|
||||
super.remove();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -950,6 +955,8 @@ declare interface ExtendExpiryDTO {
|
||||
}
|
||||
|
||||
export class accountsList extends PaginatedList {
|
||||
private _details: UserInfo;
|
||||
private _table = document.getElementById("accounts-table") as HTMLTableElement;
|
||||
protected _container = document.getElementById("accounts-list") as HTMLTableSectionElement;
|
||||
|
||||
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
|
||||
@@ -1000,8 +1007,8 @@ export class accountsList extends PaginatedList {
|
||||
private _selectAllState: SelectAllState = SelectAllState.None;
|
||||
// private _users: { [id: string]: user };
|
||||
// private _ordering: string[] = [];
|
||||
get users(): { [id: string]: User } {
|
||||
return this._search.items as { [id: string]: User };
|
||||
get users(): Map<string, User> {
|
||||
return this._search.items as Map<string, User>;
|
||||
}
|
||||
// set users(v: { [id: string]: user }) { this._search.items = v as SearchableItems; }
|
||||
|
||||
@@ -1040,6 +1047,27 @@ export class accountsList extends PaginatedList {
|
||||
}
|
||||
};
|
||||
|
||||
details(username: string, jfId: string) {
|
||||
this.unbindPageEvents();
|
||||
console.debug("Loading details for ", username, jfId);
|
||||
this._details.load(
|
||||
username,
|
||||
jfId,
|
||||
() => {
|
||||
this.unbindPageEvents();
|
||||
this._table.classList.add("unfocused");
|
||||
this._details.hidden = false;
|
||||
this.processSelectedAccounts();
|
||||
},
|
||||
() => {
|
||||
this._details.hidden = true;
|
||||
this._table.classList.remove("unfocused");
|
||||
this.processSelectedAccounts();
|
||||
this.bindPageEvents();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
loader: document.getElementById("accounts-loader"),
|
||||
@@ -1060,8 +1088,8 @@ export class accountsList extends PaginatedList {
|
||||
maxItemsLoadedForSearch: 200,
|
||||
appendNewItems: (resp: paginatedDTO) => {
|
||||
for (let u of (resp as UsersDTO).users || []) {
|
||||
if (u.id in this.users) {
|
||||
this.users[u.id].update(u);
|
||||
if (this.users.has(u.id)) {
|
||||
this.users.get(u.id).update(u);
|
||||
} else {
|
||||
this.add(u);
|
||||
}
|
||||
@@ -1074,15 +1102,15 @@ export class accountsList extends PaginatedList {
|
||||
);
|
||||
},
|
||||
replaceWithNewItems: (resp: paginatedDTO) => {
|
||||
let accountsOnDOM: { [id: string]: boolean } = {};
|
||||
let accountsOnDOM = new Map<string, boolean>();
|
||||
|
||||
for (let id of Object.keys(this.users)) {
|
||||
accountsOnDOM[id] = true;
|
||||
for (let id of this.users.keys()) {
|
||||
accountsOnDOM.set(id, true);
|
||||
}
|
||||
for (let u of (resp as UsersDTO).users || []) {
|
||||
if (u.id in accountsOnDOM) {
|
||||
this.users[u.id].update(u);
|
||||
delete accountsOnDOM[u.id];
|
||||
if (accountsOnDOM.has(u.id)) {
|
||||
this.users.get(u.id).update(u);
|
||||
accountsOnDOM.delete(u.id);
|
||||
} else {
|
||||
this.add(u);
|
||||
}
|
||||
@@ -1090,9 +1118,9 @@ export class accountsList extends PaginatedList {
|
||||
|
||||
// Delete accounts w/ remaining IDs (those not in resp.users)
|
||||
// console.log("Removing", Object.keys(accountsOnDOM).length, "from DOM");
|
||||
for (let id in accountsOnDOM) {
|
||||
this.users[id].remove();
|
||||
delete this.users[id];
|
||||
for (let id of accountsOnDOM.keys()) {
|
||||
this.users.get(id).remove();
|
||||
this.users.delete(id);
|
||||
}
|
||||
|
||||
this._search.setOrdering(
|
||||
@@ -1404,6 +1432,11 @@ export class accountsList extends PaginatedList {
|
||||
|
||||
this.registerURLListener();
|
||||
|
||||
this._details = new UserInfo(document.getElementsByClassName("accounts-details")[0] as HTMLElement);
|
||||
document.addEventListener("accounts-show-details", (ev: ShowDetailsEvent) => {
|
||||
this.details(ev.detail.username, ev.detail.id);
|
||||
});
|
||||
|
||||
// Get rid of nasty CSS
|
||||
window.modals.announce.onclose = () => {
|
||||
const preview = document.getElementById("announce-preview") as HTMLDivElement;
|
||||
@@ -1448,8 +1481,8 @@ export class accountsList extends PaginatedList {
|
||||
|
||||
if (next == SelectAllState.None) {
|
||||
// Deselect -all- users, rather than just visible ones, to be safe.
|
||||
for (let id in this.users) {
|
||||
this.users[id].setSelected(false, false);
|
||||
for (let id of this.users.keys()) {
|
||||
this.users.get(id).setSelected(false, false);
|
||||
}
|
||||
this._selectAll.checked = false;
|
||||
this._selectAll.indeterminate = false;
|
||||
@@ -1461,7 +1494,7 @@ export class accountsList extends PaginatedList {
|
||||
const selectAllVisible = () => {
|
||||
let count = 0;
|
||||
for (let id of this._visible) {
|
||||
this.users[id].setSelected(true, false);
|
||||
this.users.get(id).setSelected(true, false);
|
||||
count++;
|
||||
}
|
||||
console.debug("Selected", count);
|
||||
@@ -1495,15 +1528,14 @@ export class accountsList extends PaginatedList {
|
||||
for (let id of this._search.ordering) {
|
||||
if (!(inRange || id == startID)) continue;
|
||||
inRange = true;
|
||||
if (!this._container.contains(this.users[id].asElement())) continue;
|
||||
this.users[id].selected = true;
|
||||
if (!this._container.contains(this.users.get(id).asElement())) continue;
|
||||
this.users.get(id).selected = true;
|
||||
if (id == endID) return;
|
||||
}
|
||||
};
|
||||
|
||||
add = (u: UserDTO) => {
|
||||
let domAccount = new User(u);
|
||||
this.users[u.id] = domAccount;
|
||||
this.users.set(u.id, new User(u));
|
||||
// console.log("after appending lengths:", Object.keys(this.users).length, Object.keys(this._search.items).length);
|
||||
};
|
||||
|
||||
@@ -1551,35 +1583,36 @@ export class accountsList extends PaginatedList {
|
||||
if (window.emailEnabled || window.telegramEnabled) {
|
||||
this._announceButton.parentElement.classList.remove("unfocused");
|
||||
}
|
||||
|
||||
let anyNonExpiries = list.length == 0 ? true : false;
|
||||
let allNonExpiries = true;
|
||||
let noContactCount = 0;
|
||||
let referralState = Number(this.users[list[0]].referrals_enabled); // -1 = hide, 0 = show "enable", 1 = show "disable"
|
||||
let referralState = Number(this.users.get(list[0]).referrals_enabled); // -1 = hide, 0 = show "enable", 1 = show "disable"
|
||||
// Only show enable/disable button if all selected have the same state.
|
||||
this._shouldEnable = this.users[list[0]].disabled;
|
||||
this._shouldEnable = this.users.get(list[0]).disabled;
|
||||
let showDisableEnable = true;
|
||||
for (let id of list) {
|
||||
if (!anyNonExpiries && !this.users[id].expiry) {
|
||||
if (!anyNonExpiries && !this.users.get(id).expiry) {
|
||||
anyNonExpiries = true;
|
||||
this._expiryDropdown.classList.add("unfocused");
|
||||
}
|
||||
if (this.users[id].expiry) {
|
||||
if (this.users.get(id).expiry) {
|
||||
allNonExpiries = false;
|
||||
}
|
||||
if (showDisableEnable && this.users[id].disabled != this._shouldEnable) {
|
||||
if (showDisableEnable && this.users.get(id).disabled != this._shouldEnable) {
|
||||
showDisableEnable = false;
|
||||
this._disableEnable.parentElement.classList.add("unfocused");
|
||||
}
|
||||
if (!showDisableEnable && anyNonExpiries) {
|
||||
break;
|
||||
}
|
||||
if (!this.users[id].lastNotifyMethod()) {
|
||||
if (!this.users.get(id).lastNotifyMethod()) {
|
||||
noContactCount++;
|
||||
}
|
||||
if (
|
||||
window.referralsEnabled &&
|
||||
referralState != -1 &&
|
||||
Number(this.users[id].referrals_enabled) != referralState
|
||||
Number(this.users.get(id).referrals_enabled) != referralState
|
||||
) {
|
||||
referralState = -1;
|
||||
}
|
||||
@@ -1638,9 +1671,10 @@ export class accountsList extends PaginatedList {
|
||||
};
|
||||
|
||||
private _collectUsers = (): string[] => {
|
||||
if (!this._details.hidden && this._details.jfId != "") return [this._details.jfId];
|
||||
let list: string[] = [];
|
||||
for (let id of this._visible) {
|
||||
if (this.users[id].selected) {
|
||||
if (this.users.get(id).selected) {
|
||||
list.push(id);
|
||||
}
|
||||
}
|
||||
@@ -1993,7 +2027,7 @@ export class accountsList extends PaginatedList {
|
||||
let list = this._collectUsers();
|
||||
let manualUser: User;
|
||||
for (let id of list) {
|
||||
let user = this.users[id];
|
||||
let user = this.users.get(id);
|
||||
if (!user.lastNotifyMethod() && !user.email) {
|
||||
manualUser = user;
|
||||
break;
|
||||
@@ -2065,8 +2099,8 @@ export class accountsList extends PaginatedList {
|
||||
|
||||
(() => {
|
||||
let innerHTML = "";
|
||||
for (let id in this.users) {
|
||||
innerHTML += `<option value="${id}">${this.users[id].name}</option>`;
|
||||
for (let id of this.users.keys()) {
|
||||
innerHTML += `<option value="${id}">${this.users.get(id).name}</option>`;
|
||||
}
|
||||
this._userSelect.innerHTML = innerHTML;
|
||||
})();
|
||||
@@ -2137,7 +2171,7 @@ export class accountsList extends PaginatedList {
|
||||
let list = this._collectUsers();
|
||||
|
||||
// Check if we're disabling or enabling
|
||||
if (this.users[list[0]].referrals_enabled) {
|
||||
if (this.users.get(list[0]).referrals_enabled) {
|
||||
_delete("/users/referral", { users: list }, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4 || req.status != 200) return;
|
||||
window.notifications.customSuccess(
|
||||
@@ -2285,8 +2319,8 @@ export class accountsList extends PaginatedList {
|
||||
let id = users.length > 0 ? users[0] : "";
|
||||
if (!id) invalid = true;
|
||||
else {
|
||||
date = new Date(this.users[id].expiry * 1000);
|
||||
if (this.users[id].expiry == 0) date = new Date();
|
||||
date = new Date(this.users.get(id).expiry * 1000);
|
||||
if (this.users.get(id).expiry == 0) date = new Date();
|
||||
date.setMonth(date.getMonth() + +fields[0].value);
|
||||
date.setDate(date.getDate() + +fields[1].value);
|
||||
date.setHours(date.getHours() + +fields[2].value);
|
||||
@@ -2429,7 +2463,7 @@ export class accountsList extends PaginatedList {
|
||||
this._c.searchBox.value = `id:"${userID}"`;
|
||||
this._search.onSearchBoxChange();
|
||||
this._search.onServerSearch();
|
||||
if (userID in this.users) this.users[userID].focus();
|
||||
if (userID in this.users) this.users.get(userID).focus();
|
||||
};
|
||||
|
||||
public static readonly _accountURLEvent = "account-url";
|
||||
@@ -2545,12 +2579,12 @@ class Column {
|
||||
}
|
||||
|
||||
// Sorts the user list. previouslyActive is whether this column was previously sorted by, indicating that the direction should change.
|
||||
sort = (users: { [id: string]: User }): string[] => {
|
||||
let userIDs = Object.keys(users);
|
||||
sort = (users: Map<string, User>): string[] => {
|
||||
let userIDs = Array.from(users.keys());
|
||||
userIDs.sort((a: string, b: string): number => {
|
||||
let av: GetterReturnType = this._getter.call(users[a]);
|
||||
let av: GetterReturnType = this._getter.call(users.get(a));
|
||||
if (typeof av === "string") av = av.toLowerCase();
|
||||
let bv: GetterReturnType = this._getter.call(users[b]);
|
||||
let bv: GetterReturnType = this._getter.call(users.get(b));
|
||||
if (typeof bv === "string") bv = bv.toLowerCase();
|
||||
if (av < bv) return this._ascending ? -1 : 1;
|
||||
if (av > bv) return this._ascending ? 1 : -1;
|
||||
@@ -2561,7 +2595,17 @@ class Column {
|
||||
};
|
||||
}
|
||||
|
||||
type ActivitySeverity = "Info" | "Debug" | "Warn" | "Error" | "Fatal";
|
||||
type ActivitySeverity =
|
||||
| "Fatal"
|
||||
| "None"
|
||||
| "Trace"
|
||||
| "Debug"
|
||||
| "Information"
|
||||
| "Info"
|
||||
| "Warn"
|
||||
| "Warning"
|
||||
| "Error"
|
||||
| "Critical";
|
||||
interface ActivityLogEntryDTO {
|
||||
Id: number;
|
||||
Name: string;
|
||||
@@ -2574,3 +2618,380 @@ interface ActivityLogEntryDTO {
|
||||
UserPrimaryImageTag: string;
|
||||
Severity: ActivitySeverity;
|
||||
}
|
||||
interface PaginatedActivityLogEntriesDTO {
|
||||
entries: ActivityLogEntryDTO[];
|
||||
last_page: boolean;
|
||||
}
|
||||
|
||||
class ActivityLogEntry extends TableRow implements ActivityLogEntryDTO, SearchableItem {
|
||||
private _e: ActivityLogEntryDTO;
|
||||
private _username: string;
|
||||
private _severity: HTMLElement;
|
||||
private _user: HTMLElement;
|
||||
private _name: HTMLElement;
|
||||
private _type: HTMLElement;
|
||||
private _overview: HTMLElement;
|
||||
private _time: HTMLElement;
|
||||
|
||||
update = (user: string | null, entry: ActivityLogEntryDTO) => {
|
||||
this._e = entry;
|
||||
if (user != null) this.User = user;
|
||||
this.Id = entry.Id;
|
||||
this.Name = entry.Name;
|
||||
this.Overview = entry.Overview;
|
||||
this.ShortOverview = entry.ShortOverview;
|
||||
this.Type = entry.Type;
|
||||
this.ItemId = entry.ItemId;
|
||||
this.Date = entry.Date;
|
||||
this.UserId = entry.UserId;
|
||||
this.UserPrimaryImageTag = entry.UserPrimaryImageTag;
|
||||
this.Severity = entry.Severity;
|
||||
};
|
||||
|
||||
constructor(user: string, entry: ActivityLogEntryDTO) {
|
||||
super();
|
||||
this._row.innerHTML = `
|
||||
<td class="text-center-i"><span class="jf-activity-log-severity chip ~info dark:~d_info unfocused"></span></td>
|
||||
<td class="jf-activity-log-user-name-combined max-w-96 truncate"><div class="flex flex-row gap-2 items-baseline"><span class="chip ~gray dark:~d_gray jf-activity-log-user unfocused"></span><span class="jf-activity-log-name truncate"></span></div></td>
|
||||
<td class="jf-activity-log-type italic"></td>
|
||||
<td class="jf-activity-log-overview"></td>
|
||||
<td class="jf-activity-log-time"></td>
|
||||
`;
|
||||
this._severity = this._row.getElementsByClassName("jf-activity-log-severity")[0] as HTMLElement;
|
||||
this._user = this._row.getElementsByClassName("jf-activity-log-user")[0] as HTMLElement;
|
||||
this._name = this._row.getElementsByClassName("jf-activity-log-name")[0] as HTMLElement;
|
||||
this._type = this._row.getElementsByClassName("jf-activity-log-type")[0] as HTMLElement;
|
||||
this._overview = this._row.getElementsByClassName("jf-activity-log-overview")[0] as HTMLElement;
|
||||
this._time = this._row.getElementsByClassName("jf-activity-log-time")[0] as HTMLElement;
|
||||
this.update(user, entry);
|
||||
}
|
||||
|
||||
matchesSearch = (query: string): boolean => {
|
||||
return (
|
||||
("" + this.Id).includes(query) ||
|
||||
this.User.includes(query) ||
|
||||
this.UserId.includes(query) ||
|
||||
this.Name.includes(query) ||
|
||||
this.Overview.includes(query) ||
|
||||
this.ShortOverview.includes(query) ||
|
||||
this.Type.includes(query) ||
|
||||
this.ItemId.includes(query) ||
|
||||
this.UserId.includes(query) ||
|
||||
this.Severity.includes(query)
|
||||
);
|
||||
};
|
||||
|
||||
get Id(): number {
|
||||
return this._e.Id;
|
||||
}
|
||||
set Id(v: number) {
|
||||
this._e.Id = v;
|
||||
}
|
||||
|
||||
private setName() {
|
||||
const space = this.Name.indexOf(" ");
|
||||
let nameContent = this.Name;
|
||||
let endOfUserBadge = ":";
|
||||
if (space != -1 && this.User != "" && nameContent.substring(0, space) == this.User) {
|
||||
endOfUserBadge = "";
|
||||
nameContent = nameContent.substring(space + 1, nameContent.length);
|
||||
}
|
||||
if (this.User == "") this._user.classList.add("unfocused");
|
||||
else this._user.classList.remove("unfocused");
|
||||
this._user.textContent = this.User + endOfUserBadge;
|
||||
this._name.textContent = nameContent;
|
||||
this._name.title = nameContent;
|
||||
}
|
||||
|
||||
// User is the username of the user. It is not part of an ActivityLogEntryDTO, rather the UserId is, but in most contexts the parent should know it anyway.
|
||||
get User(): string {
|
||||
return this._username;
|
||||
}
|
||||
set User(v: string) {
|
||||
this._username = v;
|
||||
this.setName();
|
||||
}
|
||||
|
||||
// Name is a description of the entry. It often starts with the name of the user, so
|
||||
get Name(): string {
|
||||
return this._e.Name;
|
||||
}
|
||||
set Name(v: string) {
|
||||
this._e.Name = v;
|
||||
this.setName();
|
||||
}
|
||||
|
||||
// "Overview" doesn't seem to be used, but we'll let it take precedence anyway.
|
||||
private setOverview() {
|
||||
this._overview.textContent = this.Overview || this.ShortOverview;
|
||||
}
|
||||
|
||||
// Overview is something, but I haven't seen it actually used.
|
||||
get Overview(): string {
|
||||
return this._e.Overview;
|
||||
}
|
||||
|
||||
set Overview(v: string) {
|
||||
this._e.Overview = v;
|
||||
this.setOverview();
|
||||
}
|
||||
|
||||
// ShortOverview usually seems to be the IP address of the user in applicable entries.
|
||||
get ShortOverview(): string {
|
||||
return this._e.ShortOverview;
|
||||
}
|
||||
set ShortOverview(v: string) {
|
||||
this._e.ShortOverview = v;
|
||||
this.setOverview();
|
||||
}
|
||||
|
||||
get Type(): string {
|
||||
return this._e.Type;
|
||||
}
|
||||
set Type(v: string) {
|
||||
this._e.Type = v;
|
||||
this._type.textContent = v;
|
||||
}
|
||||
|
||||
get ItemId(): string {
|
||||
return this._e.ItemId;
|
||||
}
|
||||
set ItemId(v: string) {
|
||||
this._e.ItemId = v;
|
||||
}
|
||||
|
||||
get Date(): number {
|
||||
return this._e.Date;
|
||||
}
|
||||
set Date(v: number) {
|
||||
this._e.Date = v;
|
||||
this._time.textContent = toDateString(new Date(v * 1000));
|
||||
}
|
||||
|
||||
get UserId(): string {
|
||||
return this._e.UserId;
|
||||
}
|
||||
set UserId(v: string) {
|
||||
this._e.UserId = v;
|
||||
}
|
||||
|
||||
get UserPrimaryImageTag(): string {
|
||||
return this._e.UserPrimaryImageTag;
|
||||
}
|
||||
set UserPrimaryImageTag(v: string) {
|
||||
this._e.UserPrimaryImageTag = v;
|
||||
}
|
||||
|
||||
get Severity(): ActivitySeverity {
|
||||
return this._e.Severity;
|
||||
}
|
||||
set Severity(v: ActivitySeverity) {
|
||||
this._e.Severity = v;
|
||||
if (v) this._severity.classList.remove("unfocused");
|
||||
else this._severity.classList.add("unfocused");
|
||||
["~neutral", "~positive", "~warning", "~critical", "~info", "~urge"].forEach((c) =>
|
||||
this._severity.classList.remove(c),
|
||||
);
|
||||
switch (v) {
|
||||
case "Info":
|
||||
case "Information":
|
||||
this._severity.textContent = window.lang.strings("info");
|
||||
this._severity.classList.add("~info");
|
||||
break;
|
||||
case "Debug":
|
||||
case "Trace":
|
||||
this._severity.textContent = window.lang.strings("debug");
|
||||
this._severity.classList.add("~urge");
|
||||
break;
|
||||
case "Warn":
|
||||
case "Warning":
|
||||
this._severity.textContent = window.lang.strings("warn");
|
||||
this._severity.classList.add("~warning");
|
||||
break;
|
||||
case "Error":
|
||||
this._severity.textContent = window.lang.strings("error");
|
||||
this._severity.classList.add("~critical");
|
||||
break;
|
||||
case "Critical":
|
||||
case "Fatal":
|
||||
this._severity.textContent = window.lang.strings("fatal");
|
||||
this._severity.classList.add("~critical");
|
||||
break;
|
||||
case "None":
|
||||
this._severity.textContent = window.lang.strings("none");
|
||||
this._severity.classList.add("~neutral");
|
||||
default:
|
||||
console.warn("Unknown key in activity severity:", v);
|
||||
this._severity.textContent = v;
|
||||
this._severity.classList.add("~neutral");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ShowDetailsEvent extends Event {
|
||||
detail: {
|
||||
username: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
class UserInfo extends PaginatedList {
|
||||
private _card: HTMLElement;
|
||||
get entries(): Map<string, ActivityLogEntry> {
|
||||
return this._search.items as Map<string, ActivityLogEntry>;
|
||||
}
|
||||
private _back: HTMLButtonElement;
|
||||
username: string;
|
||||
jfId: string;
|
||||
constructor(card: HTMLElement) {
|
||||
card.classList.add("unfocused");
|
||||
card.innerHTML = `
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-2 justify-start">
|
||||
<button type="button" title="${window.lang.strings("back")}" class="button ~neutral @low inline-flex gap-1 user-details-back" aria-label="${window.lang.strings("back")}"><i class="icon ri-arrow-left-fill"></i>${window.lang.strings("back")}</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
</div>
|
||||
<div class="card @low overflow-x-scroll">
|
||||
<table class="table text-xs leading-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${window.lang.strings("severity")}</th>
|
||||
<th>${window.lang.strings("details")}</th>
|
||||
<th>${window.lang.strings("type")}</th>
|
||||
<th>${window.lang.strings("other")}</th>
|
||||
<th>${window.lang.strings("date")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="jf-activity-table-content"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 justify-center">
|
||||
<button class="button ~neutral @low jf-activity-load-more">${window.lang.strings("loadMore")}</button>
|
||||
<button class="button ~neutral @low accounts-load-all jf-activity-load-all">${window.lang.strings("loadAll")}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
super({
|
||||
loader: card.getElementsByClassName("jf-activity-loader")[0] as HTMLElement,
|
||||
loadMoreButtons: Array.from(
|
||||
document.getElementsByClassName("jf-activity-load-more"),
|
||||
) as Array<HTMLButtonElement>,
|
||||
loadAllButtons: Array.from(
|
||||
document.getElementsByClassName("jf-activity-load-all"),
|
||||
) as Array<HTMLButtonElement>,
|
||||
totalEndpoint: () => "/users/" + this.jfId + "/activities/jellyfin/count",
|
||||
getPageEndpoint: () => "/users/" + this.jfId + "/activities/jellyfin",
|
||||
itemsPerPage: 20,
|
||||
maxItemsLoadedForSearch: 200,
|
||||
disableSearch: true,
|
||||
appendNewItems: (resp: paginatedDTO) => {
|
||||
for (let entry of (resp as PaginatedActivityLogEntriesDTO).entries || []) {
|
||||
if (this.entries.has("" + entry.Id)) {
|
||||
this.entries.get("" + entry.Id).update(null, entry);
|
||||
} else {
|
||||
this.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
this._search.setOrdering(Array.from(this.entries.keys()), "Date", true);
|
||||
},
|
||||
replaceWithNewItems: (resp: paginatedDTO) => {
|
||||
let entriesOnDOM = new Map<string, boolean>();
|
||||
|
||||
for (let id of this.entries.keys()) {
|
||||
entriesOnDOM.set(id, true);
|
||||
}
|
||||
for (let entry of (resp as PaginatedActivityLogEntriesDTO).entries || []) {
|
||||
if (entriesOnDOM.has("" + entry.Id)) {
|
||||
this.entries.get("" + entry.Id).update(null, entry);
|
||||
entriesOnDOM.delete("" + entry.Id);
|
||||
} else {
|
||||
this.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete entries w/ remaining IDs (those not in resp.entries)
|
||||
// console.log("Removing", Object.keys(entriesOnDOM).length, "from DOM");
|
||||
for (let id of entriesOnDOM.keys()) {
|
||||
this.entries.get(id).remove();
|
||||
this.entries.delete(id);
|
||||
}
|
||||
|
||||
this._search.setOrdering(Array.from(this.entries.keys()), "Date", true);
|
||||
},
|
||||
});
|
||||
this._card = card;
|
||||
this._container = this._card.getElementsByClassName("jf-activity-table-content")[0] as HTMLElement;
|
||||
this._back = this._card.getElementsByClassName("user-details-back")[0] as HTMLButtonElement;
|
||||
|
||||
let searchConfig: SearchConfiguration = {
|
||||
queries: {},
|
||||
setVisibility: null,
|
||||
searchServer: null,
|
||||
clearServerSearch: null,
|
||||
onSearchCallback: (_0: boolean, _1: boolean) => {},
|
||||
};
|
||||
|
||||
this.initSearch(searchConfig);
|
||||
}
|
||||
|
||||
add = (entry: ActivityLogEntryDTO) => {
|
||||
this.entries.set("" + entry.Id, new ActivityLogEntry(this.username, entry));
|
||||
};
|
||||
|
||||
loadMore = (loadAll: boolean = false, callback?: (resp?: paginatedDTO) => void) => {
|
||||
this._loadMore(loadAll, callback);
|
||||
};
|
||||
|
||||
loadAll = (callback?: (resp?: paginatedDTO) => void) => {
|
||||
this._loadAll(callback);
|
||||
};
|
||||
|
||||
reload = (callback?: (resp: paginatedDTO) => void) => {
|
||||
this._reload(callback);
|
||||
};
|
||||
|
||||
load = (username: string, jfId: string, onLoad?: () => void, onBack?: () => void) => {
|
||||
this.username = username;
|
||||
this.jfId = jfId;
|
||||
if (onBack) {
|
||||
this._back.classList.remove("unfocused");
|
||||
this._back.onclick = () => onBack();
|
||||
} else {
|
||||
this._back.classList.add("unfocused");
|
||||
this._back.onclick = null;
|
||||
}
|
||||
this.reload(onLoad);
|
||||
/*_get("/users/" + jfId + "/activities/jellyfin", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorLoadJFActivities", window.lang.notif("errorLoadActivities"));
|
||||
return;
|
||||
}
|
||||
// FIXME: Lazy loading table
|
||||
this._table.textContent = ``;
|
||||
let entries = (req.response as PaginatedActivityLogEntriesDTO).entries;
|
||||
for (let entry of entries) {
|
||||
const row = new ActivityLogEntry(username, entry);
|
||||
this._table.appendChild(row.asElement());
|
||||
}
|
||||
if (onLoad) onLoad();
|
||||
});*/
|
||||
};
|
||||
|
||||
get hidden(): boolean {
|
||||
return this._card.classList.contains("unfocused");
|
||||
}
|
||||
set hidden(v: boolean) {
|
||||
if (v) {
|
||||
this.unbindPageEvents();
|
||||
this._card.classList.add("unfocused");
|
||||
this.username = "";
|
||||
this.jfId = "";
|
||||
} else {
|
||||
this.bindPageEvents();
|
||||
this._card.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,8 +564,8 @@ export class activityList extends PaginatedList {
|
||||
|
||||
protected _ascending: boolean;
|
||||
|
||||
get activities(): { [id: string]: Activity } {
|
||||
return this._search.items as { [id: string]: Activity };
|
||||
get activities(): Map<string, Activity> {
|
||||
return this._search.items as Map<string, Activity>;
|
||||
}
|
||||
// set activities(v: { [id: string]: Activity }) { this._search.items = v as SearchableItems; }
|
||||
|
||||
@@ -590,7 +590,7 @@ export class activityList extends PaginatedList {
|
||||
appendNewItems: (resp: paginatedDTO) => {
|
||||
let ordering: string[] = this._search.ordering;
|
||||
for (let act of (resp as ActivitiesDTO).activities || []) {
|
||||
this.activities[act.id] = new Activity(act);
|
||||
this.activities.set(act.id, new Activity(act));
|
||||
ordering.push(act.id);
|
||||
}
|
||||
this._search.setOrdering(ordering, this._c.defaultSortField, this.ascending);
|
||||
@@ -599,9 +599,7 @@ export class activityList extends PaginatedList {
|
||||
// FIXME: Implement updates to existing elements, rather than just wiping each time.
|
||||
|
||||
// Remove existing items
|
||||
for (let id of Object.keys(this.activities)) {
|
||||
delete this.activities[id];
|
||||
}
|
||||
this.activities.clear();
|
||||
// And wipe their ordering
|
||||
this._search.setOrdering([], this._c.defaultSortField, this.ascending);
|
||||
this._c.appendNewItems(resp);
|
||||
|
||||
@@ -18,18 +18,20 @@ export class RecordCounter {
|
||||
private _loaded: number;
|
||||
private _shown: number;
|
||||
private _selected: number;
|
||||
constructor(container: HTMLElement) {
|
||||
this._container = container;
|
||||
this._container.innerHTML = `
|
||||
constructor(container?: HTMLElement) {
|
||||
if (container) {
|
||||
this._container = container;
|
||||
this._container.innerHTML = `
|
||||
<span class="records-total"></span>
|
||||
<span class="records-loaded"></span>
|
||||
<span class="records-shown"></span>
|
||||
<span class="records-selected"></span>
|
||||
`;
|
||||
this._totalRecords = this._container.getElementsByClassName("records-total")[0] as HTMLElement;
|
||||
this._loadedRecords = this._container.getElementsByClassName("records-loaded")[0] as HTMLElement;
|
||||
this._shownRecords = this._container.getElementsByClassName("records-shown")[0] as HTMLElement;
|
||||
this._selectedRecords = this._container.getElementsByClassName("records-selected")[0] as HTMLElement;
|
||||
this._totalRecords = this._container.getElementsByClassName("records-total")[0] as HTMLElement;
|
||||
this._loadedRecords = this._container.getElementsByClassName("records-loaded")[0] as HTMLElement;
|
||||
this._shownRecords = this._container.getElementsByClassName("records-shown")[0] as HTMLElement;
|
||||
this._selectedRecords = this._container.getElementsByClassName("records-selected")[0] as HTMLElement;
|
||||
}
|
||||
this.total = 0;
|
||||
this.loaded = 0;
|
||||
this.shown = 0;
|
||||
@@ -55,7 +57,7 @@ export class RecordCounter {
|
||||
}
|
||||
set total(v: number) {
|
||||
this._total = v;
|
||||
this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`);
|
||||
if (this._totalRecords) this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`);
|
||||
}
|
||||
|
||||
get loaded(): number {
|
||||
@@ -63,7 +65,7 @@ export class RecordCounter {
|
||||
}
|
||||
set loaded(v: number) {
|
||||
this._loaded = v;
|
||||
this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`);
|
||||
if (this._loadedRecords) this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`);
|
||||
}
|
||||
|
||||
get shown(): number {
|
||||
@@ -71,7 +73,7 @@ export class RecordCounter {
|
||||
}
|
||||
set shown(v: number) {
|
||||
this._shown = v;
|
||||
this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`);
|
||||
if (this._shownRecords) this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`);
|
||||
}
|
||||
|
||||
get selected(): number {
|
||||
@@ -79,8 +81,10 @@ export class RecordCounter {
|
||||
}
|
||||
set selected(v: number) {
|
||||
this._selected = v;
|
||||
if (v == 0) this._selectedRecords.textContent = ``;
|
||||
else this._selectedRecords.textContent = window.lang.var("strings", "selectedRecords", `${v}`);
|
||||
if (this._selectedRecords) {
|
||||
if (v == 0) this._selectedRecords.textContent = ``;
|
||||
else this._selectedRecords.textContent = window.lang.var("strings", "selectedRecords", `${v}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,20 +92,21 @@ export interface PaginatedListConfig {
|
||||
loader: HTMLElement;
|
||||
loadMoreButtons: Array<HTMLButtonElement>;
|
||||
loadAllButtons: Array<HTMLButtonElement>;
|
||||
refreshButton: HTMLButtonElement;
|
||||
filterArea: HTMLElement;
|
||||
searchOptionsHeader: HTMLElement;
|
||||
searchBox: HTMLInputElement;
|
||||
recordCounter: HTMLElement;
|
||||
totalEndpoint: string;
|
||||
getPageEndpoint: string;
|
||||
refreshButton?: HTMLButtonElement;
|
||||
filterArea?: HTMLElement;
|
||||
searchOptionsHeader?: HTMLElement;
|
||||
searchBox?: HTMLInputElement;
|
||||
recordCounter?: HTMLElement;
|
||||
totalEndpoint: string | (() => string);
|
||||
getPageEndpoint: string | (() => string);
|
||||
itemsPerPage: number;
|
||||
maxItemsLoadedForSearch: number;
|
||||
appendNewItems: (resp: paginatedDTO) => void;
|
||||
replaceWithNewItems: (resp: paginatedDTO) => void;
|
||||
defaultSortField: string;
|
||||
defaultSortAscending: boolean;
|
||||
defaultSortField?: string;
|
||||
defaultSortAscending?: boolean;
|
||||
pageLoadCallback?: (req: XMLHttpRequest) => void;
|
||||
disableSearch?: boolean;
|
||||
}
|
||||
|
||||
export abstract class PaginatedList {
|
||||
@@ -186,10 +191,11 @@ export abstract class PaginatedList {
|
||||
this.loadMore(() => removeLoader(this._keepSearchingButton, true));
|
||||
}; */
|
||||
// Since this.reload doesn't exist, we need an arrow function to wrap it.
|
||||
this._c.refreshButton.onclick = () => this.reload();
|
||||
if (this._c.refreshButton) this._c.refreshButton.onclick = () => this.reload();
|
||||
}
|
||||
|
||||
autoSetServerSearchButtonsDisabled = () => {
|
||||
if (!this._search) return;
|
||||
const serverSearchSortChanged =
|
||||
this._search.inServerSearch &&
|
||||
(this._searchParams.sortByField != this._search.sortField ||
|
||||
@@ -259,7 +265,7 @@ export abstract class PaginatedList {
|
||||
};
|
||||
searchConfig.setVisibility = this.setVisibility;
|
||||
this._search = new Search(searchConfig);
|
||||
this._search.generateFilterList();
|
||||
if (!this._c.disableSearch) this._search.generateFilterList();
|
||||
this.lastPage = false;
|
||||
};
|
||||
|
||||
@@ -270,7 +276,7 @@ export abstract class PaginatedList {
|
||||
// else this._visible = this._search.ordering.filter(v => !elements.includes(v));
|
||||
// const frag = document.createDocumentFragment()
|
||||
// for (let i = 0; i < this._visible.length; i++) {
|
||||
// frag.appendChild(this._search.items[this._visible[i]].asElement())
|
||||
// frag.appendChild(this._search.items.get(this._visible[i]).asElement())
|
||||
// }
|
||||
// this._container.replaceChildren(frag);
|
||||
// if (this._search.timeSearches) {
|
||||
@@ -295,7 +301,7 @@ export abstract class PaginatedList {
|
||||
|
||||
if (!appendedItems) {
|
||||
// Wipe old elements and render 1 new one, so we can take the element height.
|
||||
this._container.replaceChildren(this._search.items[this._visible[0]].asElement());
|
||||
this._container.replaceChildren(this._search.items.get(this._visible[0]).asElement());
|
||||
}
|
||||
|
||||
this._computeScrollInfo();
|
||||
@@ -317,7 +323,7 @@ export abstract class PaginatedList {
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = baseIndex; i < this._scroll.initialRenderCount; i++) {
|
||||
frag.appendChild(this._search.items[this._visible[i]].asElement());
|
||||
frag.appendChild(this._search.items.get(this._visible[i]).asElement());
|
||||
}
|
||||
this._scroll.rendered = Math.max(baseIndex, this._scroll.initialRenderCount);
|
||||
// appendChild over replaceChildren because there's already elements on the DOM
|
||||
@@ -335,7 +341,7 @@ export abstract class PaginatedList {
|
||||
|
||||
this._scroll.screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
|
||||
|
||||
this._scroll.rowHeight = this._search.items[this._visible[0]].asElement().offsetHeight;
|
||||
this._scroll.rowHeight = this._search.items.get(this._visible[0]).asElement().offsetHeight;
|
||||
};
|
||||
|
||||
// returns the item index to render up to for the given scroll position.
|
||||
@@ -364,7 +370,7 @@ export abstract class PaginatedList {
|
||||
}
|
||||
|
||||
_post(
|
||||
this._c.getPageEndpoint,
|
||||
typeof this._c.getPageEndpoint === "string" ? this._c.getPageEndpoint : this._c.getPageEndpoint(),
|
||||
params,
|
||||
(req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
@@ -397,7 +403,9 @@ export abstract class PaginatedList {
|
||||
protected _reload = (callback?: (resp: paginatedDTO) => void) => {
|
||||
this.lastPage = false;
|
||||
this._counter.reset();
|
||||
this._counter.getTotal(this._c.totalEndpoint);
|
||||
this._counter.getTotal(
|
||||
typeof this._c.totalEndpoint === "string" ? this._c.totalEndpoint : this._c.totalEndpoint(),
|
||||
);
|
||||
// Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)).
|
||||
let limit = this._c.itemsPerPage;
|
||||
if (this._page != 0) {
|
||||
@@ -409,8 +417,10 @@ export abstract class PaginatedList {
|
||||
this._c.replaceWithNewItems,
|
||||
(_0: paginatedDTO) => {
|
||||
// Allow refreshes every 15s
|
||||
this._c.refreshButton.disabled = true;
|
||||
setTimeout(() => (this._c.refreshButton.disabled = false), 15000);
|
||||
if (this._c.refreshButton) {
|
||||
this._c.refreshButton.disabled = true;
|
||||
setTimeout(() => (this._c.refreshButton.disabled = false), 15000);
|
||||
}
|
||||
},
|
||||
(resp: paginatedDTO) => {
|
||||
this._search.onSearchBoxChange(true, false, false);
|
||||
@@ -507,7 +517,7 @@ export abstract class PaginatedList {
|
||||
const realEndIdx = Math.min(endIdx, this._visible.length);
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = this._scroll.rendered; i < realEndIdx; i++) {
|
||||
frag.appendChild(this._search.items[this._visible[i]].asElement());
|
||||
frag.appendChild(this._search.items.get(this._visible[i]).asElement());
|
||||
}
|
||||
this._scroll.rendered = realEndIdx;
|
||||
this._container.appendChild(frag);
|
||||
|
||||
15
ts/modules/row.ts
Normal file
15
ts/modules/row.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export abstract class TableRow {
|
||||
protected _row: HTMLTableRowElement;
|
||||
|
||||
remove() {
|
||||
this._row.remove();
|
||||
}
|
||||
asElement(): HTMLTableRowElement {
|
||||
return this._row;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._row = document.createElement("tr");
|
||||
this._row.classList.add("border-b", "border-dashed", "dark:border-dotted", "dark:border-stone-700");
|
||||
}
|
||||
}
|
||||
@@ -33,16 +33,16 @@ export interface QueryType {
|
||||
}
|
||||
|
||||
export interface SearchConfiguration {
|
||||
filterArea: HTMLElement;
|
||||
filterArea?: HTMLElement;
|
||||
sortingByButton?: HTMLButtonElement;
|
||||
searchOptionsHeader: HTMLElement;
|
||||
notFoundPanel: HTMLElement;
|
||||
notFoundLocallyText: HTMLElement;
|
||||
searchOptionsHeader?: HTMLElement;
|
||||
notFoundPanel?: HTMLElement;
|
||||
notFoundLocallyText?: HTMLElement;
|
||||
notFoundCallback?: (notFound: boolean) => void;
|
||||
filterList: HTMLElement;
|
||||
clearSearchButtonSelector: string;
|
||||
serverSearchButtonSelector: string;
|
||||
search: HTMLInputElement;
|
||||
filterList?: HTMLElement;
|
||||
clearSearchButtonSelector?: string;
|
||||
serverSearchButtonSelector?: string;
|
||||
search?: HTMLInputElement;
|
||||
queries: { [field: string]: QueryType };
|
||||
setVisibility: (items: string[], visible: boolean, appendedItems: boolean) => void;
|
||||
onSearchCallback: (newItems: boolean, loadAll: boolean, callback?: (resp: paginatedDTO) => void) => void;
|
||||
@@ -276,14 +276,14 @@ export interface SearchableItem extends ListItem {
|
||||
|
||||
export const SearchableItemDataAttribute = "data-search-item";
|
||||
|
||||
export type SearchableItems = { [id: string]: SearchableItem };
|
||||
export type SearchableItems = Map<string, SearchableItem>;
|
||||
|
||||
export class Search {
|
||||
private _c: SearchConfiguration;
|
||||
private _sortField: string = "";
|
||||
private _ascending: boolean = true;
|
||||
private _ordering: string[] = [];
|
||||
private _items: SearchableItems = {};
|
||||
private _items: SearchableItems = new Map<string, SearchableItem>();
|
||||
// Search queries (filters)
|
||||
private _queries: Query[] = [];
|
||||
// Plain-text search terms
|
||||
@@ -435,7 +435,7 @@ export class Search {
|
||||
for (let term of searchTerms) {
|
||||
let cachedResult = [...result];
|
||||
for (let id of cachedResult) {
|
||||
const u = this.items[id];
|
||||
const u = this.items.get(id);
|
||||
if (!u.matchesSearch(term)) {
|
||||
result.splice(result.indexOf(id), 1);
|
||||
}
|
||||
@@ -444,14 +444,14 @@ export class Search {
|
||||
}
|
||||
|
||||
for (let q of queries) {
|
||||
this._c.filterArea.appendChild(q.asElement());
|
||||
this._c.filterArea?.appendChild(q.asElement());
|
||||
// Skip if this query has already been performed by the server.
|
||||
if (this.inServerSearch && !q.localOnly) continue;
|
||||
|
||||
let cachedResult = [...result];
|
||||
if (q.type == "bool") {
|
||||
for (let id of cachedResult) {
|
||||
const u = this.items[id];
|
||||
const u = this.items.get(id);
|
||||
// Remove from result if not matching query
|
||||
if (!q.compareItem(u)) {
|
||||
// console.log("not matching, result is", result);
|
||||
@@ -460,7 +460,7 @@ export class Search {
|
||||
}
|
||||
} else if (q.type == "string") {
|
||||
for (let id of cachedResult) {
|
||||
const u = this.items[id];
|
||||
const u = this.items.get(id);
|
||||
// We want to compare case-insensitively, so we get value, lower-case it then compare,
|
||||
// rather than doing both with compareItem.
|
||||
const value = q.getValueFromItem(u).toLowerCase();
|
||||
@@ -470,7 +470,7 @@ export class Search {
|
||||
}
|
||||
} else if (q.type == "date") {
|
||||
for (let id of cachedResult) {
|
||||
const u = this.items[id];
|
||||
const u = this.items.get(id);
|
||||
// Getter here returns a unix timestamp rather than a date, so we can't use compareItem.
|
||||
const unixValue = q.getValueFromItem(u);
|
||||
if (unixValue == 0) {
|
||||
@@ -491,7 +491,7 @@ export class Search {
|
||||
// Returns a list of identifiers (used as keys in items, values in ordering).
|
||||
search = (query: string): string[] => {
|
||||
let timer = this.timeSearches ? performance.now() : null;
|
||||
this._c.filterArea.textContent = "";
|
||||
if (this._c.filterArea) this._c.filterArea.textContent = "";
|
||||
|
||||
const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query));
|
||||
|
||||
@@ -515,16 +515,16 @@ export class Search {
|
||||
showHideSearchOptionsHeader = () => {
|
||||
let sortingBy = false;
|
||||
if (this._c.sortingByButton) sortingBy = !this._c.sortingByButton.classList.contains("hidden");
|
||||
const hasFilters = this._c.filterArea.textContent != "";
|
||||
const hasFilters = this._c.filterArea ? this._c.filterArea.textContent != "" : false;
|
||||
if (sortingBy || hasFilters) {
|
||||
this._c.searchOptionsHeader.classList.remove("hidden");
|
||||
this._c.searchOptionsHeader?.classList.remove("hidden");
|
||||
} else {
|
||||
this._c.searchOptionsHeader.classList.add("hidden");
|
||||
this._c.searchOptionsHeader?.classList.add("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
// -all- elements.
|
||||
get items(): { [id: string]: SearchableItem } {
|
||||
get items(): Map<string, SearchableItem> {
|
||||
return this._items;
|
||||
}
|
||||
// set items(v: { [id: string]: SearchableItem }) {
|
||||
@@ -585,14 +585,14 @@ export class Search {
|
||||
|
||||
setNotFoundPanelVisibility = (visible: boolean) => {
|
||||
if (this._inServerSearch || !this.inSearch) {
|
||||
this._c.notFoundLocallyText.classList.add("unfocused");
|
||||
this._c.notFoundLocallyText?.classList.add("unfocused");
|
||||
} else if (this.inSearch) {
|
||||
this._c.notFoundLocallyText.classList.remove("unfocused");
|
||||
this._c.notFoundLocallyText?.classList.remove("unfocused");
|
||||
}
|
||||
if (visible) {
|
||||
this._c.notFoundPanel.classList.remove("unfocused");
|
||||
this._c.notFoundPanel?.classList.remove("unfocused");
|
||||
} else {
|
||||
this._c.notFoundPanel.classList.add("unfocused");
|
||||
this._c.notFoundPanel?.classList.add("unfocused");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -606,6 +606,7 @@ export class Search {
|
||||
};
|
||||
|
||||
generateFilterList = () => {
|
||||
if (!this._c.filterList) return;
|
||||
const filterListContainer = document.createElement("div");
|
||||
filterListContainer.classList.add("flex", "flex-row", "flex-wrap", "gap-2");
|
||||
// Generate filter buttons
|
||||
@@ -723,6 +724,10 @@ export class Search {
|
||||
|
||||
constructor(c: SearchConfiguration) {
|
||||
this._c = c;
|
||||
if (!this._c.search) {
|
||||
// Make a dummy one
|
||||
this._c.search = document.createElement("input") as HTMLInputElement;
|
||||
}
|
||||
|
||||
this._c.search.oninput = () => {
|
||||
this.inServerSearch = false;
|
||||
@@ -734,20 +739,22 @@ export class Search {
|
||||
}
|
||||
});
|
||||
|
||||
const clearSearchButtons = Array.from(
|
||||
document.querySelectorAll(this._c.clearSearchButtonSelector),
|
||||
) as Array<HTMLSpanElement>;
|
||||
for (let b of clearSearchButtons) {
|
||||
b.addEventListener("click", () => {
|
||||
this._c.search.value = "";
|
||||
this.inServerSearch = false;
|
||||
this.onSearchBoxChange();
|
||||
});
|
||||
if (this._c.clearSearchButtonSelector) {
|
||||
const clearSearchButtons = Array.from(
|
||||
document.querySelectorAll(this._c.clearSearchButtonSelector),
|
||||
) as Array<HTMLSpanElement>;
|
||||
for (let b of clearSearchButtons) {
|
||||
b.addEventListener("click", () => {
|
||||
this._c.search.value = "";
|
||||
this.inServerSearch = false;
|
||||
this.onSearchBoxChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._serverSearchButtons = Array.from(
|
||||
document.querySelectorAll(this._c.serverSearchButtonSelector),
|
||||
) as Array<HTMLSpanElement>;
|
||||
this._serverSearchButtons = this._c.serverSearchButtonSelector
|
||||
? (Array.from(document.querySelectorAll(this._c.serverSearchButtonSelector)) as Array<HTMLSpanElement>)
|
||||
: [];
|
||||
for (let b of this._serverSearchButtons) {
|
||||
b.addEventListener("click", () => {
|
||||
this.onServerSearch();
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "../modules/common.js";
|
||||
import { Marked } from "@ts-stack/markdown";
|
||||
import { stripMarkdown } from "../modules/stripmd.js";
|
||||
import { PDT } from "src/data/timezoneNames";
|
||||
|
||||
declare var window: GlobalWindow;
|
||||
|
||||
|
||||
12
usercache.go
12
usercache.go
@@ -599,18 +599,6 @@ func (q *QueryDTO) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerSearchReqDTO is a usual PaginatedReqDTO with added fields for searching and filtering.
|
||||
type ServerSearchReqDTO struct {
|
||||
PaginatedReqDTO
|
||||
ServerFilterReqDTO
|
||||
}
|
||||
|
||||
// ServerFilterReqDTO provides search terms and queries to a search or count route.
|
||||
type ServerFilterReqDTO struct {
|
||||
SearchTerms []string `json:"searchTerms"`
|
||||
Queries []QueryDTO `json:"queries"`
|
||||
}
|
||||
|
||||
// Filter reduces the passed slice of *respUsers
|
||||
// by searching for each term of terms[] with respUser.MatchesSearch,
|
||||
// and by evaluating Queries with Query.AsFilter().
|
||||
|
||||
Reference in New Issue
Block a user