diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 34595ad..cb1c757 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -3,6 +3,7 @@ import { templateEmail } from "../modules/settings.js"; import { Marked } from "@ts-stack/markdown"; import { stripMarkdown } from "../modules/stripmd.js"; import { DiscordUser, newDiscordSearch } from "../modules/discord.js"; +import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js"; const dateParser = require("any-date-parser"); interface User { @@ -39,7 +40,7 @@ interface announcementTemplate { var addDiscord: (passData: string) => void; -class user implements User { +class user implements User, SearchableItem { private _id = ""; private _row: HTMLTableRowElement; private _check: HTMLInputElement; @@ -269,7 +270,7 @@ class user implements User { - `; + `; (this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix; } else { this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused"); @@ -780,13 +781,13 @@ export class accountsList { private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement; private _referralsProfileSelect = document.getElementById("enable-referrals-user-profiles") as HTMLSelectElement; private _referralsInviteSelect = document.getElementById("enable-referrals-user-invites") as HTMLSelectElement; - private _search = document.getElementById("accounts-search") as HTMLInputElement; + private _searchBox = document.getElementById("accounts-search") as HTMLInputElement; + private _search: Search; private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; private _users: { [id: string]: user }; private _ordering: string[] = []; private _checkCount: number = 0; - private _inSearch = false; // Whether the enable/disable button should enable or not. private _shouldEnable = false; @@ -836,7 +837,7 @@ export class accountsList { } } - private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = { + private _queries: { [field: string]: QueryType } = { "id": { // We don't use a translation here to circumvent the name substitution feature. name: "Jellyfin/Emby ID", @@ -887,7 +888,7 @@ export class accountsList { bool: true, string: false, date: false, - dependsOnTableHeader: "accounts-header-access-jfa" + dependsOnElement: ".accounts-header-access-jfa" }, "email": { name: window.lang.strings("emailAddress"), @@ -895,7 +896,7 @@ export class accountsList { bool: true, string: true, date: false, - dependsOnTableHeader: "accounts-header-email" + dependsOnElement: ".accounts-header-email" }, "telegram": { name: "Telegram", @@ -903,7 +904,7 @@ export class accountsList { bool: true, string: true, date: false, - dependsOnTableHeader: "accounts-header-telegram" + dependsOnElement: ".accounts-header-telegram" }, "matrix": { name: "Matrix", @@ -911,7 +912,7 @@ export class accountsList { bool: true, string: true, date: false, - dependsOnTableHeader: "accounts-header-matrix" + dependsOnElement: ".accounts-header-matrix" }, "discord": { name: "Discord", @@ -919,7 +920,7 @@ export class accountsList { bool: true, string: true, date: false, - dependsOnTableHeader: "accounts-header-discord" + dependsOnElement: ".accounts-header-discord" }, "expiry": { name: window.lang.strings("expiry"), @@ -927,7 +928,7 @@ export class accountsList { bool: true, string: false, date: true, - dependsOnTableHeader: "accounts-header-expiry" + dependsOnElement: ".accounts-header-expiry" }, "last-active": { name: window.lang.strings("lastActiveTime"), @@ -942,229 +943,12 @@ export class accountsList { bool: true, string: false, date: false, - dependsOnTableHeader: "accounts-header-referrals" + dependsOnElement: ".accounts-header-referrals" } } private _notFoundPanel: HTMLElement = document.getElementById("accounts-not-found"); - search = (query: String): string[] => { - console.log(this._queries); - this._filterArea.textContent = ""; - - query = query.toLowerCase(); - let result: string[] = [...this._ordering]; - // console.log("initial:", result); - - // const words = query.split(" "); - let words: string[] = []; - - let quoteSymbol = ``; - let queryStart = -1; - let lastQuote = -1; - for (let i = 0; i < query.length; i++) { - if (queryStart == -1 && query[i] != " " && query[i] != `"` && query[i] != `'`) { - queryStart = i; - } - if ((query[i] == `"` || query[i] == `'`) && (quoteSymbol == `` || query[i] == quoteSymbol)) { - if (lastQuote != -1) { - lastQuote = -1; - quoteSymbol = ``; - } else { - lastQuote = i; - quoteSymbol = query[i]; - } - } - - if (query[i] == " " || i == query.length-1) { - if (lastQuote != -1) { - continue; - } else { - let end = i+1; - if (query[i] == " ") { - end = i; - while (i+1 < query.length && query[i+1] == " ") { - i += 1; - } - } - words.push(query.substring(queryStart, end).replace(/['"]/g, "")); - console.log("pushed", words); - queryStart = -1; - } - } - } - - query = ""; - for (let word of words) { - if (!word.includes(":")) { - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this._users[id]; - if (!u.matchesSearch(word)) { - result.splice(result.indexOf(id), 1); - } - } - continue; - } - const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; - - if (!(split[0] in this._queries)) continue; - - const queryFormat = this._queries[split[0]]; - - if (queryFormat.bool) { - let isBool = false; - let boolState = false; - if (split[1] == "true" || split[1] == "yes" || split[1] == "t" || split[1] == "y") { - isBool = true; - boolState = true; - } else if (split[1] == "false" || split[1] == "no" || split[1] == "f" || split[1] == "n") { - isBool = true; - boolState = false; - } - if (isBool) { - const filterCard = document.createElement("span"); - filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "mx-2", "h-full"); - filterCard.innerHTML = ` - ${queryFormat.name} - - `; - - filterCard.addEventListener("click", () => { - for (let quote of [`"`, `'`, ``]) { - this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); - } - this._search.oninput((null as Event)); - }) - - this._filterArea.appendChild(filterCard); - - // console.log("is bool, state", boolState); - // So removing elements doesn't affect us - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this._users[id]; - const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u); - // console.log("got", queryFormat.getter + ":", value); - // Remove from result if not matching query - if (!((value && boolState) || (!value && !boolState))) { - // console.log("not matching, result is", result); - result.splice(result.indexOf(id), 1); - } - } - continue - } - } - if (queryFormat.string) { - const filterCard = document.createElement("span"); - filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~neutral", "@low", "center", "mx-2", "h-full"); - filterCard.innerHTML = ` - ${queryFormat.name}: "${split[1]}" - `; - - filterCard.addEventListener("click", () => { - for (let quote of [`"`, `'`, ``]) { - let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig"); - this._search.value = this._search.value.replace(regex, ""); - } - this._search.oninput((null as Event)); - }) - - this._filterArea.appendChild(filterCard); - - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this._users[id]; - const value = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u); - if (!(value.includes(split[1]))) { - result.splice(result.indexOf(id), 1); - } - } - continue; - } - if (queryFormat.date) { - // -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0 - let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2)); - let unmodifiedValue = split[1]; - if (compareType != 2) { - split[1] = split[1].substring(1); - } - if (compareType == 2) compareType = 0; - - let attempt: { year?: number, month?: number, day?: number, hour?: number, minute?: number } = dateParser.attempt(split[1]); - // Month in Date objects is 0-based, so make our parsed date that way too - if ("month" in attempt) attempt.month -= 1; - - let date: Date = (Date as any).fromString(split[1]) as Date; - console.log("Read", attempt, "and", date); - if ("invalid" in (date as any)) continue; - - const filterCard = document.createElement("span"); - filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full"); - filterCard.innerHTML = ` - ${queryFormat.name}: ${(compareType == 1) ? window.lang.strings("after")+" " : ((compareType == -1) ? window.lang.strings("before")+" " : "")}${split[1]} - `; - - filterCard.addEventListener("click", () => { - for (let quote of [`"`, `'`, ``]) { - let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig"); - this._search.value = this._search.value.replace(regex, ""); - } - - this._search.oninput((null as Event)); - }) - - this._filterArea.appendChild(filterCard); - - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this._users[id]; - const unixValue = Object.getOwnPropertyDescriptor(user.prototype, queryFormat.getter).get.call(u); - if (unixValue == 0) { - result.splice(result.indexOf(id), 1); - continue; - } - let value = new Date(unixValue*1000); - - const getterPairs: [string, () => number][] = [["year", Date.prototype.getFullYear], ["month", Date.prototype.getMonth], ["day", Date.prototype.getDate], ["hour", Date.prototype.getHours], ["minute", Date.prototype.getMinutes]]; - - // When doing > or <