From 76bb95098c81de0c57f11ab740ac6b501bc7a3bc Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 13:58:36 +0100 Subject: [PATCH] accounts: add list of available filters, fix deletion of existing date filters The "Filters" button gives a list of filterable fields, and buttons to select the type, including true/false, text match, and on/before/after a date. When clicked, the appropriate values are put in the search box and the cursor is placed if any input is needed. Dates and strings are also now matched correctly, and case-insensitively when deleting a filter. --- html/admin.html | 12 +- lang/admin/en-us.json | 1 + ts/modules/accounts.ts | 289 +++++++++++++++++++++++++++-------------- 3 files changed, 200 insertions(+), 102 deletions(-) diff --git a/html/admin.html b/html/admin.html index 77f02de..4c297c0 100644 --- a/html/admin.html +++ b/html/admin.html @@ -579,11 +579,19 @@
-
+
-
+
{{ .strings.accounts }} +
{{ .quantityStrings.addUser.Singular }} diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 9afde19..4dd03b3 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -118,6 +118,7 @@ "accessJFA": "Access jfa-go", "accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.", "sortingBy": "Sorting By", + "filters": "Filters", "clickToRemoveFilter": "Click to remove this filter." }, "notifications": { diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 9354afe..74f27e9 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -789,102 +789,109 @@ export class accountsList { } } } - - search = (query: String): string[] => { - const queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean }} = { - "id": { - name: "Jellyfin ID", - getter: "id", - bool: false, - string: true, - date: false - }, - "label": { - name: "Label", - getter: "label", - bool: true, - string: true, - date: false - }, - "username": { - name: "Username", - getter: "name", - bool: false, - string: true, - date: false - }, - "name": { - name: "Username", - getter: "name", - bool: false, - string: true, - date: false - }, - "admin": { - name: "Admin", - getter: "admin", - bool: true, - string: false, - date: false - }, - "disabled": { - name: "Disabled", - getter: "disabled", - bool: true, - string: false, - date: false - }, - "access-jfa": { - name: "Access jfa-go", - getter: "accounts_admin", - bool: true, - string: false, - date: false - }, - "email": { - name: "Email", - getter: "email", - bool: true, - string: true, - date: false - }, - "telegram": { - name: "Telegram", - getter: "telegram", - bool: true, - string: true, - date: false - }, - "matrix": { - name: "Matrix", - getter: "matrix", - bool: true, - string: true, - date: false - }, - "discord": { - name: "Discord", - getter: "discord", - bool: true, - string: true, - date: false - }, - "expiry": { - name: "Expiry", - getter: "expiry", - bool: true, - string: false, - date: true - }, - "last-active": { - name: "Last Active", - getter: "last_active", - bool: true, - string: false, - date: true - } - } + private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = { + "id": { + name: "Jellyfin ID", + getter: "id", + bool: false, + string: true, + date: false + }, + "label": { + name: "Label", + getter: "label", + bool: true, + string: true, + date: false + }, + "username": { + name: "Username", + getter: "name", + bool: false, + string: true, + date: false + }, + "name": { + name: "Username", + getter: "name", + bool: false, + string: true, + date: false, + show: false + }, + "admin": { + name: "Admin", + getter: "admin", + bool: true, + string: false, + date: false + }, + "disabled": { + name: "Disabled", + getter: "disabled", + bool: true, + string: false, + date: false + }, + "access-jfa": { + name: "Access jfa-go", + getter: "accounts_admin", + bool: true, + string: false, + date: false, + dependsOnTableHeader: "accounts-header-access-jfa" + }, + "email": { + name: "Email", + getter: "email", + bool: true, + string: true, + date: false, + dependsOnTableHeader: "accounts-header-email" + }, + "telegram": { + name: "Telegram", + getter: "telegram", + bool: true, + string: true, + date: false, + dependsOnTableHeader: "accounts-header-telegram" + }, + "matrix": { + name: "Matrix", + getter: "matrix", + bool: true, + string: true, + date: false, + dependsOnTableHeader: "accounts-header-matrix" + }, + "discord": { + name: "Discord", + getter: "discord", + bool: true, + string: true, + date: false, + dependsOnTableHeader: "accounts-header-discord" + }, + "expiry": { + name: "Expiry", + getter: "expiry", + bool: true, + string: false, + date: true, + dependsOnTableHeader: "accounts-header-expiry" + }, + "last-active": { + name: "Last Active", + getter: "last_active", + bool: true, + string: false, + date: true + } + } + + search = (query: String): string[] => { const filterArea = document.getElementById("accounts-filter-area"); filterArea.textContent = ""; @@ -945,9 +952,9 @@ export class accountsList { } const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; - if (!(split[0] in queries)) continue; + if (!(split[0] in this._queries)) continue; - const queryFormat = queries[split[0]]; + const queryFormat = this._queries[split[0]]; if (queryFormat.bool) { let isBool = false; @@ -1004,7 +1011,8 @@ export class accountsList { filterCard.addEventListener("click", () => { for (let quote of [`"`, `'`, ``]) { - this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); + 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)); }) @@ -1024,6 +1032,7 @@ export class accountsList { 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); } @@ -1046,7 +1055,8 @@ export class accountsList { filterCard.addEventListener("click", () => { for (let quote of [`"`, `'`, ``]) { - this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); + let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig"); + this._search.value = this._search.value.replace(regex, ""); } this._search.oninput((null as Event)); @@ -1823,7 +1833,7 @@ export class accountsList { const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"]; const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active"]; for (let i = 0; i < headerNames.length; i++) { - const header: HTMLTableHeaderCellElement = document.getElementsByClassName("accounts-header-" + headerNames[i])[0] as HTMLTableHeaderCellElement; + const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement; if (header !== null) { this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get); } @@ -1854,6 +1864,85 @@ export class accountsList { }); defaultSort(); + + const filterList = document.getElementById("accounts-filter-list"); + + const fillInFilter = (name: string, value: string, offset?: number) => { + this._search.value = name + ":" + value + " " + this._search.value; + this._search.focus(); + let newPos = name.length + 1 + value.length; + if (typeof offset !== 'undefined') + newPos += offset; + this._search.setSelectionRange(newPos, newPos); + this._search.oninput(null as any); + }; + + // Generate filter buttons + for (let queryName of Object.keys(this._queries)) { + const query = this._queries[queryName]; + if ("show" in query && !query.show) continue; + if ("dependsOnTableHeader" in query && query.dependsOnTableHeader) { + const el = document.querySelector("."+query.dependsOnTableHeader); + if (el === null) continue; + } + + const container = document.createElement("span") as HTMLSpanElement; + container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2"); + container.innerHTML = `${query.name}`; + if (query.bool) { + const pos = document.createElement("button") as HTMLButtonElement; + pos.type = "button"; + pos.ariaLabel = `Filter by "${query.name}": True`; + pos.classList.add("button", "~positive", "ml-2"); + pos.innerHTML = ``; + pos.addEventListener("click", () => fillInFilter(queryName, "true")); + const neg = document.createElement("button") as HTMLButtonElement; + neg.type = "button"; + neg.ariaLabel = `Filter by "${query.name}": False`; + neg.classList.add("button", "~critical", "ml-2"); + neg.innerHTML = ``; + neg.addEventListener("click", () => fillInFilter(queryName, "false")); + + container.appendChild(pos); + container.appendChild(neg); + } + if (query.string) { + const button = document.createElement("button") as HTMLButtonElement; + button.type = "button"; + button.classList.add("button", "~urge", "ml-2"); + button.innerHTML = `Match Text`; + + // Position cursor between quotes + button.addEventListener("click", () => fillInFilter(queryName, `""`, -1)); + + container.appendChild(button); + } + if (query.date) { + const onDate = document.createElement("button") as HTMLButtonElement; + onDate.type = "button"; + onDate.classList.add("button", "~urge", "ml-2"); + onDate.innerHTML = `On Date`; + onDate.addEventListener("click", () => fillInFilter(queryName, `"="`, -1)); + + const beforeDate = document.createElement("button") as HTMLButtonElement; + beforeDate.type = "button"; + beforeDate.classList.add("button", "~urge", "ml-2"); + beforeDate.innerHTML = `Before Date`; + beforeDate.addEventListener("click", () => fillInFilter(queryName, `"<"`, -1)); + + const afterDate = document.createElement("button") as HTMLButtonElement; + afterDate.type = "button"; + afterDate.classList.add("button", "~urge", "ml-2"); + afterDate.innerHTML = `After Date`; + afterDate.addEventListener("click", () => fillInFilter(queryName, `">"`, -1)); + + container.appendChild(onDate); + container.appendChild(beforeDate); + container.appendChild(afterDate); + } + + filterList.appendChild(container); + } } reload = () => {