From f13c0d78a858ce7c44169d5b2f160590e317c8a6 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 13 Jun 2023 11:07:56 +0100 Subject: [PATCH 01/16] accounts: ability to sort by columns click on column header in account table to sort by it, click again to change sort direction. Works for all of them. --- html/admin.html | 16 +- package-lock.json | 366 ++++++++++++++++++++--------------------- package.json | 2 +- ts/modules/accounts.ts | 173 +++++++++++++------ 4 files changed, 312 insertions(+), 245 deletions(-) diff --git a/html/admin.html b/html/admin.html index fff725b..da08aea 100644 --- a/html/admin.html +++ b/html/admin.html @@ -615,22 +615,22 @@ - {{ .strings.username }} + {{ .strings.username }} {{ if .jellyfinLogin }} - {{ .strings.accessJFA }} + {{ .strings.accessJFA }} {{ end }} - {{ .strings.emailAddress }} + {{ .strings.emailAddress }} {{ if .telegramEnabled }} - Telegram + Telegram {{ end }} {{ if .matrixEnabled }} - Matrix + Matrix {{ end }} {{ if .discordEnabled }} - Discord + Discord {{ end }} - {{ .strings.expiry }} - {{ .strings.lastActiveTime }} + {{ .strings.expiry }} + {{ .strings.lastActiveTime }} diff --git a/package-lock.json b/package-lock.json index a3bd7f9..b84905b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "a17t": "^0.10.1", "browserslist": "^4.21.7", "cheerio": "^1.0.0-rc.12", - "esbuild": "^0.18.1", + "esbuild": "^0.18.2", "fs-cheerio": "^3.0.0", "inline-source": "^8.0.2", "jsdom": "^22.1.0", @@ -56,9 +56,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.1.tgz", - "integrity": "sha512-8+QS98jqdreHLvCojIke8NjcuelB+Osysazr15EhkUIuG0Ov5WK26XgPYWViCTjHnKQxbpS86/JryBOkEpyrBA==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.2.tgz", + "integrity": "sha512-YAnQBHlY0IvYtvY0avnXjI8ywW23emEjk5XExqbFmypath+Snq9MgY1IS47rnqBKVSqnl0ElDt221ZgaeRrkXg==", "cpu": [ "arm" ], @@ -71,9 +71,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.1.tgz", - "integrity": "sha512-l5V0IWGTYfQMzl4ulgIHKMZPwabIS4a39ZvtkPwL6LYiX3UtL76sylA6eFKufJCB43mwEYqbXoBSMn++NpxILw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.2.tgz", + "integrity": "sha512-1Y2pb0hLdmji8I0zBwNsYSDN7zJSQqufgLOuOsrrod00WEAgKywQR5MB/E046Is/YTP4bgcPS4BioaSDBaLaTg==", "cpu": [ "arm64" ], @@ -86,9 +86,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.1.tgz", - "integrity": "sha512-1y8/bRek6EYxQeGTUfwL2mmj6NAeXZ3h5YSc4W2Y/kduI1B8VhT4x5X0VxrcGkIKef4N5qCdziRxvei/YUfESg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.2.tgz", + "integrity": "sha512-P047Mh3pj8uYVE3A/B3QDX6nG8dKbHLJ+48R6Y0CRXCJ5PkXJxdHOTaS8SYs6eSR3FFU6/YQ5TishQXVHX7F5A==", "cpu": [ "x64" ], @@ -101,9 +101,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.1.tgz", - "integrity": "sha512-FFT/on9qQTOntdloQvgfwFkRhNI5l/TCNXZS1CpH6JQd0boR637aThi9g9FYs4o31Ao72/jPZFDiRln5Cu6R2A==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.2.tgz", + "integrity": "sha512-a3Rkqd0tGVYMEKNy9SstWEdeBmM60l8FVD5o4rmwHr3xO1LbLqtCJSrWGbnf37hevo6m437mURVmpEHOmkXeTA==", "cpu": [ "arm64" ], @@ -116,9 +116,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.1.tgz", - "integrity": "sha512-p/ZIUt+NlW8qRNVTXoKJgRuc49teazvmXBquoGOm5D6IAimTfWJVJrEivqpoMKyDS/0/PxDMRM2lrkxlSa7XeQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.2.tgz", + "integrity": "sha512-cvH58adz9L10JNsIcgtkWNS/1eutjRTi3rtWz1s3ZhR64BpdmkxJBAXE/UjqybyNAWLhaN8mPJdlYI2f+tQA7g==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.1.tgz", - "integrity": "sha512-eYUDR3thO96ULRf4rJcG9TJ/sQc6Z/YNe16mC/KvVeAOtzmeTXiPMETEv/iMqTCxZhYkHyQG/mYbAxPBWC2mcg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.2.tgz", + "integrity": "sha512-68rGMGUdgmq+c5IvseCMqY4yaa2CAY/DIILMBA6bEU1caISF7fXnV69B1uU4s3ERuVDcasVVwiAFyNxCtkS6Zg==", "cpu": [ "arm64" ], @@ -146,9 +146,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.1.tgz", - "integrity": "sha512-w03zjxyg51qktv0JKsV+AbY3uSb1Awifs8IkKQSUXdP3sdPxxmPzZLrlJ1+LjKZRiSa4yP/Haayi/hriNVoIdQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.2.tgz", + "integrity": "sha512-ZSR9On/rXoYuAtrXo5hYKy7OuZwKZyFh2rr6L3TX4UeR1tWLf84aLyAFt7e0tlRbh4zNgqFx+ePWmsSHw7L9Bw==", "cpu": [ "x64" ], @@ -161,9 +161,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.1.tgz", - "integrity": "sha512-d6FXeb8F/cuXtSZuVHQN0Rz3gs3g2Xy/M4KJJRzbKsBx3pwCQuRdSrYxcr7g0PFN8geIOspiLQnUzwONyMA3Bw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.2.tgz", + "integrity": "sha512-jAbA75qJ70T5AOdmw9X8675ppeRfj7j57sOypoZ4mQlfQ/LKF8eoeLzTYVo8+aqLKqeIIl0vQ4hKOB0FyG98Zg==", "cpu": [ "arm" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.1.tgz", - "integrity": "sha512-dHlvkKAVlYNt5LPg1GUha99QiaEGKEC21zpHVAxs7hhW6EkR8nN3iWmyndGXxVJm4K7e4lKAzl8ekPqSr5gAXQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.2.tgz", + "integrity": "sha512-DFKavAzbu/n9HXWuetxmYN10XnfzW7FgOgpcrGD8eXaiu77KdgB+OVWA83x9FtDYtsoFpfdlDuVFAQFfrhu77A==", "cpu": [ "arm64" ], @@ -191,9 +191,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.1.tgz", - "integrity": "sha512-QHS4duBPuAsLZP82sNeoqTXAJ1mNU4QcfmYtBN/jNvQJXb6n0im8F4ljFSAQbivt1jl1OnKL8HhlLUeKY75nLg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.2.tgz", + "integrity": "sha512-VEaK3Z+vJyDwwPsP0sovaEw1foDzrMs7XQNYEIFkOwMjSe2BipKRKUUyrznil0p8qqsK7U8W+T7oNqZpgdnD2Q==", "cpu": [ "ia32" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.1.tgz", - "integrity": "sha512-g4YSiF/qBvXvJhSowxaR7Ei/79otL48Qfjviuo+FpXREykA9nSe407T5ZvezFXryFgdf44Fe8lWpjvtQ+n42cQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.2.tgz", + "integrity": "sha512-Af1uZdB0oeJo4PW67l9aw94oakSamFxhC6ltC2eDkndozd9QygVNMTF7s7uxTLjo+BJqyVqG9wjmLCYF1o4NmA==", "cpu": [ "loong64" ], @@ -221,9 +221,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.1.tgz", - "integrity": "sha512-/G1fzmaR5u2S9wgQhiQEhWRct0+GMpuNjhll59uv5Tjojlma9MUPinVnvpw9Re+Idb6gxe6kmzUxFP2YkC/svg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.2.tgz", + "integrity": "sha512-WcTbt61+9dREuOFKXac4Qg+3OuRhLxPL9lmkI2P7fGuq/fWS2qq+AvGGVLMyk+OtXGDjyQolcEDeYlRoOmjRYQ==", "cpu": [ "mips64el" ], @@ -236,9 +236,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.1.tgz", - "integrity": "sha512-NkDjIvleUc3lSV1VI3QE9Oh5mz3nY11H5TCbi4DJ8X09FGwHN5pDVXdAsQYPGjlt/frXZzq6x7vMmTOb5VyBog==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.2.tgz", + "integrity": "sha512-Ov+VHayvCPb52axma6+xm8QDawRjwHscPXedHg4U92DxlhKQ0H+6onRiC3J9kKI50p8pKKypprpCWrRrXjZN7Q==", "cpu": [ "ppc64" ], @@ -251,9 +251,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.1.tgz", - "integrity": "sha512-IhN7Nz+HyDRnMQOLcCl6m5BgQMITMhS9O1hOqgAUIy6FI0m/0zTSkZHtvMmSIpOy1uleaGqfNDA9SM3nBeTATQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.2.tgz", + "integrity": "sha512-qW37zzKKN9C5l5LnVDriOK0eZRzQeixhtrfd5C78PAsTE15GeHU9G0oyT/u/IkNjEBjXWpTZOOHKNbjhrvuL9g==", "cpu": [ "riscv64" ], @@ -266,9 +266,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.1.tgz", - "integrity": "sha512-u9iRg0eUUIyBbg5hANvRBYRaAnhVemAA2+pi3IgrzQTMeR/uPHQtJI3XInNZkNR6ACA4Fdl8N941p81XygeqWQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.2.tgz", + "integrity": "sha512-izzEFMRO8LaQIlX22+fTgP5I7Os3T51mtAWsRNpZ5pMfQIa9PqtgFAoRcb10DV+/YkH/TMMxQIlevUvDS6E4vw==", "cpu": [ "s390x" ], @@ -281,9 +281,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.1.tgz", - "integrity": "sha512-0QeWU0a0+RmxPCDt+plXS7/hVMJtfde/LaSzs6X3UTr4FYA0hYpnwDzGXxumcPLzt5c8ctugPuKat0tmRb7noQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.2.tgz", + "integrity": "sha512-y5yqQ1ww4FfI9bQ1ZP/0k1rcgA6Ql2/AgzvqpowN0Q5tXDZkCavPdJbFXKrqA43vd1UTXt+AutTHYJ7km6e2Eg==", "cpu": [ "x64" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.1.tgz", - "integrity": "sha512-eFL7sxibN8wKuwrRf3HRkcjELALlfl/TavJ8P4J+BJfnkXOITF7zx4tTmGkzK8OwAR9snT2kEfp1Ictc80atGw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.2.tgz", + "integrity": "sha512-usNjpKFf83X4o60gdMD47NCblaSZ6DARf31/FyCzxOgnF80mJ+RhDs9RTqgyfH8KyduO5mjgInw9+ct286ayYA==", "cpu": [ "x64" ], @@ -311,9 +311,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.1.tgz", - "integrity": "sha512-riCQUnngF2xYUzr0XDdrGEEz0nqnocch0No7SBIQM22wTRi5gt5WqTQexGd/2u2Z19d0rVYQbKBelaJ1dwe9bg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.2.tgz", + "integrity": "sha512-6urzy1+VwcPuhG+5jwHA8lD9E87E5+ey3qKw2EhRS+qUmMxLvfwP8szWC2JHVGZDPEDge6fgn0pBj+y9rxDLwQ==", "cpu": [ "x64" ], @@ -326,9 +326,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.1.tgz", - "integrity": "sha512-6lTop2k+GMkWlrwMy2+55xIBXKfXOi6uzWYypXZZP8HxXG3Mb5N4O71z2KzisVNJtYK2VlQRNbmGtUzIQIhHAw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.2.tgz", + "integrity": "sha512-SMZPTACsvpKYAIl9o8nhnmMn6/lp62iMeV/2EBMtj+sW6dXwW9b0cLjihkBv4PG1CCRlwWKPZo43imqZxC95ZA==", "cpu": [ "x64" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.1.tgz", - "integrity": "sha512-d6wt4g9GluZp7xCmgpm7gY6wy0mjcBHbKeeK9MYrlWNFJd8KBcD2uCil8kFuaH3Dt6AUz62D0wIoDETFsZ01Tg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.2.tgz", + "integrity": "sha512-H2zzjPdzSDNwUnZdZf9/xfm0CYqHFXuenCMAx+tRzIRqWUT6MmZ9/q7722KnAZ6uPpq0RLs7EjCIIfmt6CaRGg==", "cpu": [ "arm64" ], @@ -356,9 +356,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.1.tgz", - "integrity": "sha512-z51DOtcwECu4WlqJUhu39AVnnpaVmTvXei0EQxc99QK7ZJyn4tj0EelYkMBZckpqzqB/GyGSLwEclKtRJ0l2uw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.2.tgz", + "integrity": "sha512-lfyjTN+FrKgvNvrH7nOLtaz58J/8coZOo4LQwgBMP4D7ZOurhvluXS3GjePLzq9GbWnJDZdKCKbMKhZPPcdJJA==", "cpu": [ "ia32" ], @@ -371,9 +371,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.1.tgz", - "integrity": "sha512-6tdeuCLT+l9QuCFaYsNtULO6xH2fgJObvICMCsOZvkqIey6FUXVVju5aO+OZjxswy7WgKadhI1k/nq2wQSmB+Q==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.2.tgz", + "integrity": "sha512-Q4nIjqWXjxkELwd7kVepsJxbQ/6ERNsHpjz1j+IKjwSYw+g06U0RQOy5xh848AHvgr9itnGLa3cT2G5t0dBFsw==", "cpu": [ "x64" ], @@ -1641,9 +1641,9 @@ } }, "node_modules/esbuild": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.1.tgz", - "integrity": "sha512-ZUvsIx2wPjpj86b805UwbGJu47Kxgr2UTio9rGhWpBr1oOVk88exzoieOgTweX0UcVCwSAk3z2WvNALpTcpQZw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.2.tgz", + "integrity": "sha512-1P4sK9gXVcjvrrUjE94Hbo9goU+T6U1sdzLf+JJ+3uI6GEb4e4n3Wrqto9hZHUWabblpT2ifmC61LhZnLyTNFw==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -1652,28 +1652,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.1", - "@esbuild/android-arm64": "0.18.1", - "@esbuild/android-x64": "0.18.1", - "@esbuild/darwin-arm64": "0.18.1", - "@esbuild/darwin-x64": "0.18.1", - "@esbuild/freebsd-arm64": "0.18.1", - "@esbuild/freebsd-x64": "0.18.1", - "@esbuild/linux-arm": "0.18.1", - "@esbuild/linux-arm64": "0.18.1", - "@esbuild/linux-ia32": "0.18.1", - "@esbuild/linux-loong64": "0.18.1", - "@esbuild/linux-mips64el": "0.18.1", - "@esbuild/linux-ppc64": "0.18.1", - "@esbuild/linux-riscv64": "0.18.1", - "@esbuild/linux-s390x": "0.18.1", - "@esbuild/linux-x64": "0.18.1", - "@esbuild/netbsd-x64": "0.18.1", - "@esbuild/openbsd-x64": "0.18.1", - "@esbuild/sunos-x64": "0.18.1", - "@esbuild/win32-arm64": "0.18.1", - "@esbuild/win32-ia32": "0.18.1", - "@esbuild/win32-x64": "0.18.1" + "@esbuild/android-arm": "0.18.2", + "@esbuild/android-arm64": "0.18.2", + "@esbuild/android-x64": "0.18.2", + "@esbuild/darwin-arm64": "0.18.2", + "@esbuild/darwin-x64": "0.18.2", + "@esbuild/freebsd-arm64": "0.18.2", + "@esbuild/freebsd-x64": "0.18.2", + "@esbuild/linux-arm": "0.18.2", + "@esbuild/linux-arm64": "0.18.2", + "@esbuild/linux-ia32": "0.18.2", + "@esbuild/linux-loong64": "0.18.2", + "@esbuild/linux-mips64el": "0.18.2", + "@esbuild/linux-ppc64": "0.18.2", + "@esbuild/linux-riscv64": "0.18.2", + "@esbuild/linux-s390x": "0.18.2", + "@esbuild/linux-x64": "0.18.2", + "@esbuild/netbsd-x64": "0.18.2", + "@esbuild/openbsd-x64": "0.18.2", + "@esbuild/sunos-x64": "0.18.2", + "@esbuild/win32-arm64": "0.18.2", + "@esbuild/win32-ia32": "0.18.2", + "@esbuild/win32-x64": "0.18.2" } }, "node_modules/escalade": { @@ -6791,135 +6791,135 @@ } }, "@esbuild/android-arm": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.1.tgz", - "integrity": "sha512-8+QS98jqdreHLvCojIke8NjcuelB+Osysazr15EhkUIuG0Ov5WK26XgPYWViCTjHnKQxbpS86/JryBOkEpyrBA==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.2.tgz", + "integrity": "sha512-YAnQBHlY0IvYtvY0avnXjI8ywW23emEjk5XExqbFmypath+Snq9MgY1IS47rnqBKVSqnl0ElDt221ZgaeRrkXg==", "optional": true }, "@esbuild/android-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.1.tgz", - "integrity": "sha512-l5V0IWGTYfQMzl4ulgIHKMZPwabIS4a39ZvtkPwL6LYiX3UtL76sylA6eFKufJCB43mwEYqbXoBSMn++NpxILw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.2.tgz", + "integrity": "sha512-1Y2pb0hLdmji8I0zBwNsYSDN7zJSQqufgLOuOsrrod00WEAgKywQR5MB/E046Is/YTP4bgcPS4BioaSDBaLaTg==", "optional": true }, "@esbuild/android-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.1.tgz", - "integrity": "sha512-1y8/bRek6EYxQeGTUfwL2mmj6NAeXZ3h5YSc4W2Y/kduI1B8VhT4x5X0VxrcGkIKef4N5qCdziRxvei/YUfESg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.2.tgz", + "integrity": "sha512-P047Mh3pj8uYVE3A/B3QDX6nG8dKbHLJ+48R6Y0CRXCJ5PkXJxdHOTaS8SYs6eSR3FFU6/YQ5TishQXVHX7F5A==", "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.1.tgz", - "integrity": "sha512-FFT/on9qQTOntdloQvgfwFkRhNI5l/TCNXZS1CpH6JQd0boR637aThi9g9FYs4o31Ao72/jPZFDiRln5Cu6R2A==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.2.tgz", + "integrity": "sha512-a3Rkqd0tGVYMEKNy9SstWEdeBmM60l8FVD5o4rmwHr3xO1LbLqtCJSrWGbnf37hevo6m437mURVmpEHOmkXeTA==", "optional": true }, "@esbuild/darwin-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.1.tgz", - "integrity": "sha512-p/ZIUt+NlW8qRNVTXoKJgRuc49teazvmXBquoGOm5D6IAimTfWJVJrEivqpoMKyDS/0/PxDMRM2lrkxlSa7XeQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.2.tgz", + "integrity": "sha512-cvH58adz9L10JNsIcgtkWNS/1eutjRTi3rtWz1s3ZhR64BpdmkxJBAXE/UjqybyNAWLhaN8mPJdlYI2f+tQA7g==", "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.1.tgz", - "integrity": "sha512-eYUDR3thO96ULRf4rJcG9TJ/sQc6Z/YNe16mC/KvVeAOtzmeTXiPMETEv/iMqTCxZhYkHyQG/mYbAxPBWC2mcg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.2.tgz", + "integrity": "sha512-68rGMGUdgmq+c5IvseCMqY4yaa2CAY/DIILMBA6bEU1caISF7fXnV69B1uU4s3ERuVDcasVVwiAFyNxCtkS6Zg==", "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.1.tgz", - "integrity": "sha512-w03zjxyg51qktv0JKsV+AbY3uSb1Awifs8IkKQSUXdP3sdPxxmPzZLrlJ1+LjKZRiSa4yP/Haayi/hriNVoIdQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.2.tgz", + "integrity": "sha512-ZSR9On/rXoYuAtrXo5hYKy7OuZwKZyFh2rr6L3TX4UeR1tWLf84aLyAFt7e0tlRbh4zNgqFx+ePWmsSHw7L9Bw==", "optional": true }, "@esbuild/linux-arm": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.1.tgz", - "integrity": "sha512-d6FXeb8F/cuXtSZuVHQN0Rz3gs3g2Xy/M4KJJRzbKsBx3pwCQuRdSrYxcr7g0PFN8geIOspiLQnUzwONyMA3Bw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.2.tgz", + "integrity": "sha512-jAbA75qJ70T5AOdmw9X8675ppeRfj7j57sOypoZ4mQlfQ/LKF8eoeLzTYVo8+aqLKqeIIl0vQ4hKOB0FyG98Zg==", "optional": true }, "@esbuild/linux-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.1.tgz", - "integrity": "sha512-dHlvkKAVlYNt5LPg1GUha99QiaEGKEC21zpHVAxs7hhW6EkR8nN3iWmyndGXxVJm4K7e4lKAzl8ekPqSr5gAXQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.2.tgz", + "integrity": "sha512-DFKavAzbu/n9HXWuetxmYN10XnfzW7FgOgpcrGD8eXaiu77KdgB+OVWA83x9FtDYtsoFpfdlDuVFAQFfrhu77A==", "optional": true }, "@esbuild/linux-ia32": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.1.tgz", - "integrity": "sha512-QHS4duBPuAsLZP82sNeoqTXAJ1mNU4QcfmYtBN/jNvQJXb6n0im8F4ljFSAQbivt1jl1OnKL8HhlLUeKY75nLg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.2.tgz", + "integrity": "sha512-VEaK3Z+vJyDwwPsP0sovaEw1foDzrMs7XQNYEIFkOwMjSe2BipKRKUUyrznil0p8qqsK7U8W+T7oNqZpgdnD2Q==", "optional": true }, "@esbuild/linux-loong64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.1.tgz", - "integrity": "sha512-g4YSiF/qBvXvJhSowxaR7Ei/79otL48Qfjviuo+FpXREykA9nSe407T5ZvezFXryFgdf44Fe8lWpjvtQ+n42cQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.2.tgz", + "integrity": "sha512-Af1uZdB0oeJo4PW67l9aw94oakSamFxhC6ltC2eDkndozd9QygVNMTF7s7uxTLjo+BJqyVqG9wjmLCYF1o4NmA==", "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.1.tgz", - "integrity": "sha512-/G1fzmaR5u2S9wgQhiQEhWRct0+GMpuNjhll59uv5Tjojlma9MUPinVnvpw9Re+Idb6gxe6kmzUxFP2YkC/svg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.2.tgz", + "integrity": "sha512-WcTbt61+9dREuOFKXac4Qg+3OuRhLxPL9lmkI2P7fGuq/fWS2qq+AvGGVLMyk+OtXGDjyQolcEDeYlRoOmjRYQ==", "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.1.tgz", - "integrity": "sha512-NkDjIvleUc3lSV1VI3QE9Oh5mz3nY11H5TCbi4DJ8X09FGwHN5pDVXdAsQYPGjlt/frXZzq6x7vMmTOb5VyBog==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.2.tgz", + "integrity": "sha512-Ov+VHayvCPb52axma6+xm8QDawRjwHscPXedHg4U92DxlhKQ0H+6onRiC3J9kKI50p8pKKypprpCWrRrXjZN7Q==", "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.1.tgz", - "integrity": "sha512-IhN7Nz+HyDRnMQOLcCl6m5BgQMITMhS9O1hOqgAUIy6FI0m/0zTSkZHtvMmSIpOy1uleaGqfNDA9SM3nBeTATQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.2.tgz", + "integrity": "sha512-qW37zzKKN9C5l5LnVDriOK0eZRzQeixhtrfd5C78PAsTE15GeHU9G0oyT/u/IkNjEBjXWpTZOOHKNbjhrvuL9g==", "optional": true }, "@esbuild/linux-s390x": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.1.tgz", - "integrity": "sha512-u9iRg0eUUIyBbg5hANvRBYRaAnhVemAA2+pi3IgrzQTMeR/uPHQtJI3XInNZkNR6ACA4Fdl8N941p81XygeqWQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.2.tgz", + "integrity": "sha512-izzEFMRO8LaQIlX22+fTgP5I7Os3T51mtAWsRNpZ5pMfQIa9PqtgFAoRcb10DV+/YkH/TMMxQIlevUvDS6E4vw==", "optional": true }, "@esbuild/linux-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.1.tgz", - "integrity": "sha512-0QeWU0a0+RmxPCDt+plXS7/hVMJtfde/LaSzs6X3UTr4FYA0hYpnwDzGXxumcPLzt5c8ctugPuKat0tmRb7noQ==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.2.tgz", + "integrity": "sha512-y5yqQ1ww4FfI9bQ1ZP/0k1rcgA6Ql2/AgzvqpowN0Q5tXDZkCavPdJbFXKrqA43vd1UTXt+AutTHYJ7km6e2Eg==", "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.1.tgz", - "integrity": "sha512-eFL7sxibN8wKuwrRf3HRkcjELALlfl/TavJ8P4J+BJfnkXOITF7zx4tTmGkzK8OwAR9snT2kEfp1Ictc80atGw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.2.tgz", + "integrity": "sha512-usNjpKFf83X4o60gdMD47NCblaSZ6DARf31/FyCzxOgnF80mJ+RhDs9RTqgyfH8KyduO5mjgInw9+ct286ayYA==", "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.1.tgz", - "integrity": "sha512-riCQUnngF2xYUzr0XDdrGEEz0nqnocch0No7SBIQM22wTRi5gt5WqTQexGd/2u2Z19d0rVYQbKBelaJ1dwe9bg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.2.tgz", + "integrity": "sha512-6urzy1+VwcPuhG+5jwHA8lD9E87E5+ey3qKw2EhRS+qUmMxLvfwP8szWC2JHVGZDPEDge6fgn0pBj+y9rxDLwQ==", "optional": true }, "@esbuild/sunos-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.1.tgz", - "integrity": "sha512-6lTop2k+GMkWlrwMy2+55xIBXKfXOi6uzWYypXZZP8HxXG3Mb5N4O71z2KzisVNJtYK2VlQRNbmGtUzIQIhHAw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.2.tgz", + "integrity": "sha512-SMZPTACsvpKYAIl9o8nhnmMn6/lp62iMeV/2EBMtj+sW6dXwW9b0cLjihkBv4PG1CCRlwWKPZo43imqZxC95ZA==", "optional": true }, "@esbuild/win32-arm64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.1.tgz", - "integrity": "sha512-d6wt4g9GluZp7xCmgpm7gY6wy0mjcBHbKeeK9MYrlWNFJd8KBcD2uCil8kFuaH3Dt6AUz62D0wIoDETFsZ01Tg==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.2.tgz", + "integrity": "sha512-H2zzjPdzSDNwUnZdZf9/xfm0CYqHFXuenCMAx+tRzIRqWUT6MmZ9/q7722KnAZ6uPpq0RLs7EjCIIfmt6CaRGg==", "optional": true }, "@esbuild/win32-ia32": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.1.tgz", - "integrity": "sha512-z51DOtcwECu4WlqJUhu39AVnnpaVmTvXei0EQxc99QK7ZJyn4tj0EelYkMBZckpqzqB/GyGSLwEclKtRJ0l2uw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.2.tgz", + "integrity": "sha512-lfyjTN+FrKgvNvrH7nOLtaz58J/8coZOo4LQwgBMP4D7ZOurhvluXS3GjePLzq9GbWnJDZdKCKbMKhZPPcdJJA==", "optional": true }, "@esbuild/win32-x64": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.1.tgz", - "integrity": "sha512-6tdeuCLT+l9QuCFaYsNtULO6xH2fgJObvICMCsOZvkqIey6FUXVVju5aO+OZjxswy7WgKadhI1k/nq2wQSmB+Q==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.2.tgz", + "integrity": "sha512-Q4nIjqWXjxkELwd7kVepsJxbQ/6ERNsHpjz1j+IKjwSYw+g06U0RQOy5xh848AHvgr9itnGLa3cT2G5t0dBFsw==", "optional": true }, "@jridgewell/gen-mapping": { @@ -7885,32 +7885,32 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, "esbuild": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.1.tgz", - "integrity": "sha512-ZUvsIx2wPjpj86b805UwbGJu47Kxgr2UTio9rGhWpBr1oOVk88exzoieOgTweX0UcVCwSAk3z2WvNALpTcpQZw==", + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.2.tgz", + "integrity": "sha512-1P4sK9gXVcjvrrUjE94Hbo9goU+T6U1sdzLf+JJ+3uI6GEb4e4n3Wrqto9hZHUWabblpT2ifmC61LhZnLyTNFw==", "requires": { - "@esbuild/android-arm": "0.18.1", - "@esbuild/android-arm64": "0.18.1", - "@esbuild/android-x64": "0.18.1", - "@esbuild/darwin-arm64": "0.18.1", - "@esbuild/darwin-x64": "0.18.1", - "@esbuild/freebsd-arm64": "0.18.1", - "@esbuild/freebsd-x64": "0.18.1", - "@esbuild/linux-arm": "0.18.1", - "@esbuild/linux-arm64": "0.18.1", - "@esbuild/linux-ia32": "0.18.1", - "@esbuild/linux-loong64": "0.18.1", - "@esbuild/linux-mips64el": "0.18.1", - "@esbuild/linux-ppc64": "0.18.1", - "@esbuild/linux-riscv64": "0.18.1", - "@esbuild/linux-s390x": "0.18.1", - "@esbuild/linux-x64": "0.18.1", - "@esbuild/netbsd-x64": "0.18.1", - "@esbuild/openbsd-x64": "0.18.1", - "@esbuild/sunos-x64": "0.18.1", - "@esbuild/win32-arm64": "0.18.1", - "@esbuild/win32-ia32": "0.18.1", - "@esbuild/win32-x64": "0.18.1" + "@esbuild/android-arm": "0.18.2", + "@esbuild/android-arm64": "0.18.2", + "@esbuild/android-x64": "0.18.2", + "@esbuild/darwin-arm64": "0.18.2", + "@esbuild/darwin-x64": "0.18.2", + "@esbuild/freebsd-arm64": "0.18.2", + "@esbuild/freebsd-x64": "0.18.2", + "@esbuild/linux-arm": "0.18.2", + "@esbuild/linux-arm64": "0.18.2", + "@esbuild/linux-ia32": "0.18.2", + "@esbuild/linux-loong64": "0.18.2", + "@esbuild/linux-mips64el": "0.18.2", + "@esbuild/linux-ppc64": "0.18.2", + "@esbuild/linux-riscv64": "0.18.2", + "@esbuild/linux-s390x": "0.18.2", + "@esbuild/linux-x64": "0.18.2", + "@esbuild/netbsd-x64": "0.18.2", + "@esbuild/openbsd-x64": "0.18.2", + "@esbuild/sunos-x64": "0.18.2", + "@esbuild/win32-arm64": "0.18.2", + "@esbuild/win32-ia32": "0.18.2", + "@esbuild/win32-x64": "0.18.2" } }, "escalade": { diff --git a/package.json b/package.json index fc09087..fbf21d2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "a17t": "^0.10.1", "browserslist": "^4.21.7", "cheerio": "^1.0.0-rc.12", - "esbuild": "^0.18.1", + "esbuild": "^0.18.2", "fs-cheerio": "^3.0.0", "inline-source": "^8.0.2", "jsdom": "^22.1.0", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index abe6913..bab0dd8 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -737,7 +737,7 @@ export class accountsList { private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; private _users: { [id: string]: user }; - private _sortedByName: string[] = []; + private _ordering: string[] = []; private _checkCount: number = 0; private _inSearch = false; // Whether the enable/disable button should enable or not. @@ -748,6 +748,11 @@ export class accountsList { private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement; private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement; + // Columns for sorting. + private _columns: { [className: string]: Column } = {}; + private _activeSortColumn: string; + + // Whether the "Extend expiry" is extending or setting an expiry. private _settingExpiry = false; @@ -782,7 +787,7 @@ export class accountsList { if (querySplit[1] == "true" || querySplit[1] == "yes") { state = true; } - for (let id in this._users) { + for (let id of this._ordering) { const user = this._users[id]; let attrib: boolean; if (querySplit[0] == "admin") { attrib = user.admin; } @@ -793,7 +798,7 @@ export class accountsList { } } if (query == "") { return result; } - for (let id in this._users) { + for (let id of this._ordering) { const user = this._users[id]; if (user.name.toLowerCase().includes(query)) { result.push(id); @@ -821,36 +826,6 @@ export class accountsList { add = (u: User) => { let domAccount = new user(u); this._users[u.id] = domAccount; - this.unhide(u.id); - } - - unhide = (id: string) => { - const keys = Object.keys(this._users); - if (keys.length == 0) { - this._table.appendChild(this._users[id].asElement()); - return; - } - this._sortedByName = keys.sort((a, b) => this._users[a].name.localeCompare(this._users[b].name)); - let index = this._sortedByName.indexOf(id)+1; - if (index == this._sortedByName.length-1) { - this._table.appendChild(this._users[id].asElement()); - return; - } - while (index < this._sortedByName.length) { - if (this._table.contains(this._users[this._sortedByName[index]].asElement())) { - this._table.insertBefore(this._users[id].asElement(), this._users[this._sortedByName[index]].asElement()); - return; - } - index++; - } - this._table.appendChild(this._users[id].asElement()); - } - - hide = (id: string) => { - const el = this._users[id].asElement(); - if (this._table.contains(el)) { - this._table.removeChild(this._users[id].asElement()); - } } private _checkCheckCount = () => { @@ -1427,6 +1402,18 @@ export class accountsList { } window.modals.extendExpiry.show(); } + + + setVisibility = (users: string[], visible: boolean) => { + this._table.textContent = ""; + for (let id of this._ordering) { + if (visible && users.indexOf(id) != -1) { + this._table.appendChild(this._users[id].asElement()); + } else if (!visible && users.indexOf(id) == -1) { + this._table.appendChild(this._users[id].asElement()); + } + } + } constructor() { this._populateNumbers(); @@ -1508,32 +1495,14 @@ export class accountsList { this._deleteNotify.checked = false; }*/ - const setVisibility = (users: string[], visible: boolean) => { - for (let id in this._users) { - if (users.indexOf(id) != -1) { - if (visible) { - this.unhide(id); - } else { - this.hide(id); - } - } else { - if (visible) { - this.hide(id); - } else { - this.unhide(id); - } - } - } - } - this._search.oninput = () => { const query = this._search.value; if (!query) { - setVisibility(Object.keys(this._users), true); + this.setVisibility(this._ordering, true); this._inSearch = false; } else { this._inSearch = true; - setVisibility(this.search(query), true); + this.setVisibility(this.search(query), true); } this._checkCheckCount(); }; @@ -1559,6 +1528,30 @@ export class accountsList { insertText(this._announceTextarea, announceVarUsername.children[0].textContent); this.loadPreview(); }; + + 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; + if (header !== null) { + this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get); + } + } + + document.addEventListener("header-click", (event: CustomEvent) => { + this._ordering = this._columns[event.detail].sort(this._users); + this._activeSortColumn = event.detail; + // console.log("ordering by", event.detail, ": ", this._ordering); + if (!(this._inSearch)) { + this.setVisibility(this._ordering, true); + } else { + this.setVisibility(this.search(this._search.value), true); + } + }); + + // Start off sorting by Name + this._activeSortColumn = document.getElementsByClassName("accounts-header-" + headerNames[0])[0].className; + document.dispatchEvent(new CustomEvent("header-click", { detail: this._activeSortColumn })); } reload = () => { @@ -1579,9 +1572,83 @@ export class accountsList { this._users[id].remove(); delete this._users[id]; } + // console.log("reload, so sorting by", this._activeSortColumn); + this._ordering = this._columns[this._activeSortColumn].sort(this._users); + if (!(this._inSearch)) { + this.setVisibility(this._ordering, true); + } else { + this.setVisibility(this.search(this._search.value), true); + } this._checkCheckCount(); } }); this.loadTemplates(); } } + +type GetterReturnType = Boolean | boolean | String | Number | number; +type Getter = () => GetterReturnType; + +// When a column is clicked, it broadcasts it's name and ordering to be picked up and stored by accountsList +// When list is refreshed, accountList calls method of the specific Column and re-orders accordingly. +// Listen for broadcast event from others, check its not us by comparing the header className in the message, then hide the arrow icon +class Column { + private _header: HTMLTableHeaderCellElement; + private _headerContent: string; + private _getter: Getter; + private _ascending: boolean; + private _active: boolean; + + constructor(header: HTMLTableHeaderCellElement, getter: Getter) { + this._header = header; + this._headerContent = this._header.textContent; + this._getter = getter; + this._ascending = true; + this._active = false; + + this._header.addEventListener("click", () => { + // If we are the active sort column, a click means to switch between ascending/descending. + if (this._active) { + this._ascending = !this._ascending; + console.log("was already active, switching direction to", this._ascending ? "ascending" : "descending"); + } else { + console.log("wasn't active keeping direction as", this._ascending ? "ascending" : "descending"); + } + this._active = true; + this.updateHeader(); + document.dispatchEvent(new CustomEvent("header-click", { detail: this._header.className })); + }); + document.addEventListener("header-click", (event: CustomEvent) => { + if (event.detail != this._header.className) { + this._active = false; + this.hideIcon(); + } + }); + } + + hideIcon = () => { + this._header.textContent = this._headerContent; + } + + updateHeader = () => { + this._header.innerHTML = ` + ${this._headerContent} + + `; + } + + + // 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); + userIDs.sort((a: string, b: string): number => { + const av: GetterReturnType = this._getter.call(users[a]); + const bv: GetterReturnType = this._getter.call(users[b]); + if (av < bv) return this._ascending ? -1 : 1; + if (av > bv) return this._ascending ? 1 : -1; + return 0; + }); + + return userIDs; + } +} From bc4015ac506ebc63b390fa51e933ea4ca893d642 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 13 Jun 2023 12:02:43 +0100 Subject: [PATCH 02/16] accounts: add indicator of sorted column at top of list When clicking on a column to sort by it, a button with "Sorting By: " appears. Clicking it will reset the sort, which defaults for ascending username. --- html/admin.html | 17 +++++++++-------- lang/admin/en-us.json | 3 ++- ts/modules/accounts.ts | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/html/admin.html b/html/admin.html index da08aea..6bac4e2 100644 --- a/html/admin.html +++ b/html/admin.html @@ -584,6 +584,7 @@
{{ .strings.accounts }} +
{{ .quantityStrings.addUser.Singular }} @@ -615,22 +616,22 @@ - {{ .strings.username }} + {{ .strings.username }} {{ if .jellyfinLogin }} - {{ .strings.accessJFA }} + {{ .strings.accessJFA }} {{ end }} - {{ .strings.emailAddress }} + {{ .strings.emailAddress }} {{ if .telegramEnabled }} - Telegram + Telegram {{ end }} {{ if .matrixEnabled }} - Matrix + Matrix {{ end }} {{ if .discordEnabled }} - Discord + Discord {{ end }} - {{ .strings.expiry }} - {{ .strings.lastActiveTime }} + {{ .strings.expiry }} + {{ .strings.lastActiveTime }} diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index b2056b3..8a5fe27 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -114,7 +114,8 @@ "deleteTemplate": "Delete template", "templateEnterName": "Enter a name to save this template.", "accessJFA": "Access jfa-go", - "accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General." + "accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.", + "sortingBy": "Sorting By" }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index bab0dd8..20f34cb 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -752,6 +752,7 @@ export class accountsList { private _columns: { [className: string]: Column } = {}; private _activeSortColumn: string; + private _sortingByButton = document.getElementById("accounts-sort-by-field") as HTMLButtonElement; // Whether the "Extend expiry" is extending or setting an expiry. private _settingExpiry = false; @@ -1538,9 +1539,22 @@ export class accountsList { } } + // Start off sorting by Name + const defaultSort = () => { + this._activeSortColumn = document.getElementsByClassName("accounts-header-" + headerNames[0])[0].className; + document.dispatchEvent(new CustomEvent("header-click", { detail: this._activeSortColumn })); + this._columns[this._activeSortColumn].ascending = true; + this._columns[this._activeSortColumn].hideIcon(); + this._sortingByButton.parentElement.classList.add("hidden"); + }; + + this._sortingByButton.parentElement.addEventListener("click", defaultSort); + document.addEventListener("header-click", (event: CustomEvent) => { this._ordering = this._columns[event.detail].sort(this._users); this._activeSortColumn = event.detail; + this._sortingByButton.innerHTML = this._columns[event.detail].buttonContent; + this._sortingByButton.parentElement.classList.remove("hidden"); // console.log("ordering by", event.detail, ": ", this._ordering); if (!(this._inSearch)) { this.setVisibility(this._ordering, true); @@ -1549,9 +1563,7 @@ export class accountsList { } }); - // Start off sorting by Name - this._activeSortColumn = document.getElementsByClassName("accounts-header-" + headerNames[0])[0].className; - document.dispatchEvent(new CustomEvent("header-click", { detail: this._activeSortColumn })); + defaultSort(); } reload = () => { @@ -1615,12 +1627,14 @@ class Column { console.log("wasn't active keeping direction as", this._ascending ? "ascending" : "descending"); } this._active = true; + this._header.setAttribute("aria-sort", this._headerContent); this.updateHeader(); document.dispatchEvent(new CustomEvent("header-click", { detail: this._header.className })); }); document.addEventListener("header-click", (event: CustomEvent) => { if (event.detail != this._header.className) { this._active = false; + this._header.removeAttribute("aria-sort"); this.hideIcon(); } }); @@ -1632,11 +1646,24 @@ class Column { updateHeader = () => { this._header.innerHTML = ` - ${this._headerContent} - + ${this._headerContent} + `; } + // Returns the inner HTML to show in the "Sorting By" button. + get buttonContent() { + return `` + window.lang.strings("sortingBy") + ": " + `` + this._headerContent; + } + + get ascending() { return this._ascending; } + set ascending(v: boolean) { + this._ascending = v; + if (!this._active) return; + this.updateHeader(); + this._header.setAttribute("aria-sort", this._headerContent); + document.dispatchEvent(new CustomEvent("header-click", { detail: this._header.className })); + } // 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[] => { From 8a37663c892b435498fb6c26e0b8fc2724729645 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 13 Jun 2023 13:39:13 +0100 Subject: [PATCH 03/16] accounts: start advanced search filter support uses the same format ":", but supports quoted (allows for spaces in them), and lays groundwork to support string and date-type field filtering. Truthiness is supported, meaning you can check if an email is set with "email:yes" for example. --- ts/modules/accounts.ts | 180 ++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 31 deletions(-) diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 20f34cb..a79ca62 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -774,41 +774,159 @@ export class accountsList { } } } - - search = (query: string): string[] => { - query = query.toLowerCase() - let result: string[] = []; - if (query.includes(":")) { // Support admin: and disabled: - const words = query.split(" "); - query = ""; - for (let word of words) { - if (word.includes(":")) { - const querySplit = word.split(":") - let state = false; - if (querySplit[1] == "true" || querySplit[1] == "yes") { - state = true; - } - for (let id of this._ordering) { - const user = this._users[id]; - let attrib: boolean; - if (querySplit[0] == "admin") { attrib = user.admin; } - else if (querySplit[0] == "disabled") { attrib = user.disabled; } - if (attrib == state) { result.push(id); } - } - } else { query += word + " "; } + + search = (query: String): string[] => { + console.log("called!"); + const queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean }} = { + "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 } } - if (query == "") { return result; } - for (let id of this._ordering) { - const user = this._users[id]; - if (user.name.toLowerCase().includes(query)) { - result.push(id); - } else if (user.email.toLowerCase().includes(query)) { - result.push(id); + + query = query.toLowerCase(); + let result: string[] = [...this._ordering]; + console.log("initial:", result); + if (!(query.includes(":"))) return result; + + // const words = query.split(" "); + let words: string[] = []; + // FIXME: SPLIT BY SPACE, UNLESS IN QUOTES + + 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 { + words.push(query.substring(queryStart, i+1).replace(/['"]/g, "")); + queryStart = -1; + } } } - return result; - } + + query = ""; + for (let word of words) { + const split = word.split(":"); + + if (!(split[0] in queries)) continue; + + const queryFormat = 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) { + // 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) { + + // FIXME: STRING SEARCH FOR FIELD + // FIXME: SUBTRACT FROM RESULT + continue; + } + if (queryFormat.date) { + + } + } + return result + }; + get selectAll(): boolean { return this._selectAll.checked; } set selectAll(state: boolean) { From 90c433443face506c1af1ab90a36ce95a1f620a4 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 13 Jun 2023 13:55:40 +0100 Subject: [PATCH 04/16] accounts: filter by string field, general search string fields can now be searched by with the ":" syntax, also added back a better general search, that supports essentially all string fields, including Jellyfin ID. --- ts/modules/accounts.ts | 50 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index a79ca62..6724a86 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -459,6 +459,20 @@ class user implements User { this._label.classList.add("chip", "~gray", "mr-2"); } } + + matchesSearch = (query: string): boolean => { + return ( + this.name.includes(query) || + this.label.includes(query) || + this.discord.includes(query) || + this.email.includes(query) || + this.id.includes(query) || + this.label.includes(query) || + this.matrix.includes(query) || + this.telegram.includes(query) + ); + } + private _checkEvent = new CustomEvent("accountCheckEvent"); private _uncheckEvent = new CustomEvent("accountUncheckEvent"); @@ -778,6 +792,20 @@ export class accountsList { search = (query: String): string[] => { console.log("called!"); 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 + }, "admin": { name: "Admin", getter: "admin", @@ -846,7 +874,16 @@ export class accountsList { query = query.toLowerCase(); let result: string[] = [...this._ordering]; console.log("initial:", result); - if (!(query.includes(":"))) return result; + if (!(query.includes(":"))) { + let cachedResult = [...result]; + for (let id of cachedResult) { + const u = this._users[id]; + if (!u.matchesSearch(query as string)) { + result.splice(result.indexOf(id), 1); + } + } + return result; + } // const words = query.split(" "); let words: string[] = []; @@ -915,9 +952,14 @@ export class accountsList { } } if (queryFormat.string) { - - // FIXME: STRING SEARCH FOR FIELD - // FIXME: SUBTRACT FROM RESULT + 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) { From ff62f8821a57e4fd8ef244efc7da492e4980a5e7 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 13 Jun 2023 17:19:24 +0100 Subject: [PATCH 05/16] accounts: filter by date, with =, <, > Uses "any-date-parser" library to understand more date/time types. Format is: ":", where the part after the colon uses =, <, >. Omitting a symbol is the same as using "=". --- package-lock.json | 11 ++++++++ package.json | 1 + ts/modules/accounts.ts | 61 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b84905b..bca893c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ts-stack/markdown": "^1.4.0", "@types/node": "^20.3.0", "a17t": "^0.10.1", + "any-date-parser": "^1.5.4", "browserslist": "^4.21.7", "cheerio": "^1.0.0-rc.12", "esbuild": "^0.18.2", @@ -630,6 +631,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-date-parser": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/any-date-parser/-/any-date-parser-1.5.4.tgz", + "integrity": "sha512-S4gl9UmXNk9XXSQxp5w5harUD6aM0fepyL3dZM/B3znX57sWf792hS2UvvCFIHECfpsqfKbQ+cqWBky4AKyRIg==" + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -7110,6 +7116,11 @@ "color-convert": "^2.0.1" } }, + "any-date-parser": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/any-date-parser/-/any-date-parser-1.5.4.tgz", + "integrity": "sha512-S4gl9UmXNk9XXSQxp5w5harUD6aM0fepyL3dZM/B3znX57sWf792hS2UvvCFIHECfpsqfKbQ+cqWBky4AKyRIg==" + }, "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", diff --git a/package.json b/package.json index fbf21d2..1a630ad 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@ts-stack/markdown": "^1.4.0", "@types/node": "^20.3.0", "a17t": "^0.10.1", + "any-date-parser": "^1.5.4", "browserslist": "^4.21.7", "cheerio": "^1.0.0-rc.12", "esbuild": "^0.18.2", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 6724a86..b0577d3 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"; +const dateParser = require("any-date-parser"); interface User { id: string; @@ -790,7 +791,6 @@ export class accountsList { } search = (query: String): string[] => { - console.log("called!"); const queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean }} = { "id": { name: "Jellyfin ID", @@ -873,7 +873,7 @@ export class accountsList { query = query.toLowerCase(); let result: string[] = [...this._ordering]; - console.log("initial:", result); + // console.log("initial:", result); if (!(query.includes(":"))) { let cachedResult = [...result]; for (let id of cachedResult) { @@ -918,7 +918,7 @@ export class accountsList { query = ""; for (let word of words) { - const split = word.split(":"); + const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; if (!(split[0] in queries)) continue; @@ -963,7 +963,60 @@ export class accountsList { 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)); + 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; + 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 <
+
+ + +
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 8a5fe27..9afde19 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -37,6 +37,8 @@ "advancedSettings": "Advanced Settings", "lastActiveTime": "Last Active", "from": "From", + "after": "After", + "before": "Before", "user": "User", "expiry": "Expiry", "userExpiry": "User Expiry", @@ -115,7 +117,8 @@ "templateEnterName": "Enter a name to save this template.", "accessJFA": "Access jfa-go", "accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.", - "sortingBy": "Sorting By" + "sortingBy": "Sorting By", + "clickToRemoveFilter": "Click to remove this filter." }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index b0577d3..9354afe 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -806,6 +806,20 @@ export class accountsList { 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", @@ -871,19 +885,12 @@ export class accountsList { } } + const filterArea = document.getElementById("accounts-filter-area"); + filterArea.textContent = ""; + query = query.toLowerCase(); let result: string[] = [...this._ordering]; // console.log("initial:", result); - if (!(query.includes(":"))) { - let cachedResult = [...result]; - for (let id of cachedResult) { - const u = this._users[id]; - if (!u.matchesSearch(query as string)) { - result.splice(result.indexOf(id), 1); - } - } - return result; - } // const words = query.split(" "); let words: string[] = []; @@ -910,7 +917,15 @@ export class accountsList { if (lastQuote != -1) { continue; } else { - words.push(query.substring(queryStart, i+1).replace(/['"]/g, "")); + 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; } } @@ -918,6 +933,16 @@ export class accountsList { 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 queries)) continue; @@ -935,6 +960,24 @@ export class accountsList { boolState = false; } if (isBool) { + // FIXME: Generate filter card for each filter class + const filterCard = document.createElement("span"); + filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); + filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "ml-2", "mr-2"); + 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)); + }) + + filterArea.appendChild(filterCard); + // console.log("is bool, state", boolState); // So removing elements doesn't affect us let cachedResult = [...result]; @@ -952,6 +995,22 @@ export class accountsList { } } if (queryFormat.string) { + const filterCard = document.createElement("span"); + filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); + filterCard.classList.add("button", "~neutral", "@low", "center", "ml-2", "mr-2", "h-full"); + filterCard.innerHTML = ` + ${queryFormat.name}: "${split[1]}" + `; + + 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)); + }) + + filterArea.appendChild(filterCard); + let cachedResult = [...result]; for (let id of cachedResult) { const u = this._users[id]; @@ -977,6 +1036,24 @@ export class accountsList { 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", "ml-2", "mr-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 [`"`, `'`, ``]) { + this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); + } + + this._search.oninput((null as Event)); + }) + + filterArea.appendChild(filterCard); + let cachedResult = [...result]; for (let id of cachedResult) { const u = this._users[id]; From 0e241f56fb60fe6232c1376d6c42d7ca4cd327bc Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 11:50:22 +0100 Subject: [PATCH 07/16] scripts: add script to generate fake accounts might be useful for screenshots too, currently just using for testing the sorting/filtering. --- scripts/account-gen/go.mod | 5 ++ scripts/account-gen/go.sum | 2 + scripts/account-gen/main.go | 122 ++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 scripts/account-gen/go.mod create mode 100644 scripts/account-gen/go.sum create mode 100644 scripts/account-gen/main.go diff --git a/scripts/account-gen/go.mod b/scripts/account-gen/go.mod new file mode 100644 index 0000000..bdcfa36 --- /dev/null +++ b/scripts/account-gen/go.mod @@ -0,0 +1,5 @@ +module github.com/hrfee/jfa-go/scripts/account-gen + +go 1.20 + +require github.com/hrfee/mediabrowser v0.3.8 // indirect diff --git a/scripts/account-gen/go.sum b/scripts/account-gen/go.sum new file mode 100644 index 0000000..2b733b9 --- /dev/null +++ b/scripts/account-gen/go.sum @@ -0,0 +1,2 @@ +github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk= +github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U= diff --git a/scripts/account-gen/main.go b/scripts/account-gen/main.go new file mode 100644 index 0000000..bc1c528 --- /dev/null +++ b/scripts/account-gen/main.go @@ -0,0 +1,122 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "math/rand" + "os" + "strconv" + "strings" + "time" + + "github.com/hrfee/mediabrowser" +) + +var ( + names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab"} +) + +const ( + PASSWORD = "test" + COUNT = 10 +) + +func main() { + fmt.Println("Usage: account-gen ") + var server, username, password string + reader := bufio.NewReader(os.Stdin) + if len(os.Args) > 1 { + server = os.Args[1] + } else { + fmt.Print("Server Address: ") + server, _ = reader.ReadString('\n') + server = strings.TrimSuffix(server, "\n") + } + + if len(os.Args) > 2 { + username = os.Args[2] + } else { + fmt.Print("Username: ") + username, _ = reader.ReadString('\n') + username = strings.TrimSuffix(username, "\n") + } + + if len(os.Args) > 3 { + password = os.Args[3] + if strings.HasPrefix(password, "file://") { + p, err := os.ReadFile(strings.TrimPrefix(password, "file://")) + if err != nil { + log.Fatalf("Failed to read password file \"%s\": %+v\n", password, err) + } + password = strings.TrimSuffix(string(p), "\n") + } + } else { + fmt.Print("Password: ") + password, _ = reader.ReadString('\n') + password = strings.TrimSuffix(password, "\n") + } + + jf, err := mediabrowser.NewServer( + mediabrowser.JellyfinServer, + server, + "jfa-go-account-gen-script", + "0.0.1", + "testing", + "my_left_foot", + mediabrowser.NewNamedTimeoutHandler("Jellyfin Account Gen", "\""+server+"\"", true), + 30, + ) + + if err != nil { + log.Fatalf("Failed to connect to Jellyin @ \"%s\": %+v\n", server, err) + } + + _, status, err := jf.Authenticate(username, password) + if status != 200 || err != nil { + log.Fatalf("Failed to authenticate: %+v\n", err) + } + + jfTemp, err := mediabrowser.NewServer( + mediabrowser.JellyfinServer, + server, + "jfa-go-account-gen-script", + "0.0.1", + "fake-activity", + "my_left_foot", + mediabrowser.NewNamedTimeoutHandler("Jellyfin Account Gen", "\""+server+"\"", true), + 30, + ) + + if err != nil { + log.Fatalf("Failed to connect to Jellyin @ \"%s\": %+v\n", server, err) + } + + rand.Seed(time.Now().Unix()) + + for i := 0; i < COUNT; i++ { + name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(100)) + + user, status, err := jf.NewUser(name, PASSWORD) + if (status != 200 && status != 201 && status != 204) || err != nil { + log.Fatalf("Failed to create user \"%s\" (%d): %+v\n", name, status, err) + } + + if rand.Intn(100) > 65 { + user.Policy.IsAdministrator = true + } + + if rand.Intn(100) > 80 { + user.Policy.IsDisabled = true + } + + status, err = jf.SetPolicy(user.ID, user.Policy) + if (status != 200 && status != 201 && status != 204) || err != nil { + log.Fatalf("Failed to set policy for user \"%s\" (%d): %+v\n", name, status, err) + } + + if rand.Intn(100) > 20 { + jfTemp.Authenticate(name, PASSWORD) + } + } +} From 76bb95098c81de0c57f11ab740ac6b501bc7a3bc Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 13:58:36 +0100 Subject: [PATCH 08/16] 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 = () => { From 6f452c62de3f93b38ae0bdbac8a14d1352dd4f4d Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 17:15:24 +0100 Subject: [PATCH 09/16] accounts: fix search bugs, adjust top bar layout search bar is now massive with a small filter button next to it. Action buttons are on their own row. Also fixed dealing with going from a search with filters in to an empty one, search() is now called for any change at all to the input. --- css/base.css | 9 ++++-- html/admin.html | 68 ++++++++++++++++++++---------------------- ts/modules/accounts.ts | 28 +++++++++-------- 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/css/base.css b/css/base.css index fbb1e60..f912d3e 100644 --- a/css/base.css +++ b/css/base.css @@ -490,10 +490,10 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) { text-align: center; } -.search { +/* .search { max-width: 15rem; min-width: 10rem; -} +} */ td.img-circle { width: 32px; @@ -548,6 +548,11 @@ div.card:contains(section.banner.footer) { padding-bottom: 0px !important } +.mx-0i { + margin-left: 0px !important; + margin-right: 0px !important +} + .text-center-i { text-align: center !important; } diff --git a/html/admin.html b/html/admin.html index 4c297c0..6efbe1f 100644 --- a/html/admin.html +++ b/html/admin.html @@ -580,45 +580,43 @@
-
-
- {{ .strings.accounts }} - -
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index e8fe211..f3d76ee 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -120,7 +120,9 @@ "sortingBy": "Sorting By", "filters": "Filters", "clickToRemoveFilter": "Click to remove this filter.", - "clearSearch": "Clear search" + "clearSearch": "Clear search", + "actions": "Actions", + "searchOptions": "Search Options" }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index af352f2..942c829 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -768,6 +768,8 @@ export class accountsList { private _activeSortColumn: string; private _sortingByButton = document.getElementById("accounts-sort-by-field") as HTMLButtonElement; + private _filterArea = document.getElementById("accounts-filter-area"); + private _searchOptionsHeader = document.getElementById("accounts-search-options-header"); // Whether the "Extend expiry" is extending or setting an expiry. private _settingExpiry = false; @@ -790,6 +792,17 @@ export class accountsList { } } + showHideSearchOptionsHeader = () => { + const sortingBy = !(this._sortingByButton.parentElement.classList.contains("hidden")); + const hasFilters = this._filterArea.textContent != ""; + console.log("sortingBy", sortingBy, "hasFilters", hasFilters); + if (sortingBy || hasFilters) { + this._searchOptionsHeader.classList.remove("hidden"); + } else { + this._searchOptionsHeader.classList.add("hidden"); + } + } + private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = { "id": { name: "Jellyfin ID", @@ -892,8 +905,7 @@ export class accountsList { } search = (query: String): string[] => { - const filterArea = document.getElementById("accounts-filter-area"); - filterArea.textContent = ""; + this._filterArea.textContent = ""; query = query.toLowerCase(); let result: string[] = [...this._ordering]; @@ -970,7 +982,7 @@ export class accountsList { // FIXME: Generate filter card for each filter class const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "ml-2", "mr-2", "mt-2"); + filterCard.classList.add("button", "~" + (boolState ? "positive" : "critical"), "@high", "center", "m-2"); filterCard.innerHTML = ` ${queryFormat.name} @@ -983,7 +995,7 @@ export class accountsList { this._search.oninput((null as Event)); }) - filterArea.appendChild(filterCard); + this._filterArea.appendChild(filterCard); // console.log("is bool, state", boolState); // So removing elements doesn't affect us @@ -1004,7 +1016,7 @@ export class accountsList { if (queryFormat.string) { const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~neutral", "@low", "center", "ml-2", "mr-2", "mt-2", "h-full"); + filterCard.classList.add("button", "~neutral", "@low", "center", "m-2", "h-full"); filterCard.innerHTML = ` ${queryFormat.name}: "${split[1]}" `; @@ -1017,7 +1029,7 @@ export class accountsList { this._search.oninput((null as Event)); }) - filterArea.appendChild(filterCard); + this._filterArea.appendChild(filterCard); let cachedResult = [...result]; for (let id of cachedResult) { @@ -1048,7 +1060,7 @@ export class accountsList { const filterCard = document.createElement("span"); filterCard.ariaLabel = window.lang.strings("clickToRemoveFilter"); - filterCard.classList.add("button", "~neutral", "@low", "center", "ml-2", "mr-2", "mt-2", "h-full"); + 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]} `; @@ -1062,7 +1074,7 @@ export class accountsList { this._search.oninput((null as Event)); }) - filterArea.appendChild(filterCard); + this._filterArea.appendChild(filterCard); let cachedResult = [...result]; for (let id of cachedResult) { @@ -1807,6 +1819,7 @@ export class accountsList { } this.setVisibility(this.search(query), true); this._checkCheckCount(); + this.showHideSearchOptionsHeader(); }; this._search.oninput = onchange; @@ -1869,9 +1882,11 @@ export class accountsList { } else { this.setVisibility(this.search(this._search.value), true); } + this.showHideSearchOptionsHeader(); }); defaultSort(); + this.showHideSearchOptionsHeader(); const filterList = document.getElementById("accounts-filter-list"); From f24ea4a5f8f77032e1463a18ef2864ffe1ce07d2 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 18:41:47 +0100 Subject: [PATCH 12/16] accounts: fix sizing of sorting by button --- html/admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/admin.html b/html/admin.html index dc76e35..2404a4c 100644 --- a/html/admin.html +++ b/html/admin.html @@ -620,7 +620,7 @@
- +
From 708d382a3fc4e19bf2f663e52cacebdae50288e5 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 18:43:46 +0100 Subject: [PATCH 13/16] accounts: fix hiding of search options header for default sort --- ts/modules/accounts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 942c829..1705cf0 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -1867,6 +1867,7 @@ export class accountsList { this._columns[this._activeSortColumn].ascending = true; this._columns[this._activeSortColumn].hideIcon(); this._sortingByButton.parentElement.classList.add("hidden"); + this.showHideSearchOptionsHeader(); }; this._sortingByButton.parentElement.addEventListener("click", defaultSort); From 9cef626b286739ac5e193f4c60309173538af54f Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 18:52:33 +0100 Subject: [PATCH 14/16] accounts: filter dropdown appears over announce one --- html/admin.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/html/admin.html b/html/admin.html index 2404a4c..a02b44f 100644 --- a/html/admin.html +++ b/html/admin.html @@ -582,7 +582,7 @@
{{ .strings.accounts }} -
From b544931ee51d84724ce91335a154fd3ed7f5cb84 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 18:57:30 +0100 Subject: [PATCH 15/16] accounts: fix id filtering, make string translatable --- lang/admin/en-us.json | 3 ++- ts/modules/accounts.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index f3d76ee..770e3b3 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -122,7 +122,8 @@ "clickToRemoveFilter": "Click to remove this filter.", "clearSearch": "Clear search", "actions": "Actions", - "searchOptions": "Search Options" + "searchOptions": "Search Options", + "matchText": "Match Text" }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 1705cf0..0c2cce5 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -39,6 +39,7 @@ interface announcementTemplate { var addDiscord: (passData: string) => void; class user implements User { + private _id = ""; private _row: HTMLTableRowElement; private _check: HTMLInputElement; private _username: HTMLSpanElement; @@ -67,7 +68,6 @@ class user implements User { private _userLabel: string; private _labelEditButton: HTMLElement; private _accounts_admin: HTMLInputElement - id = ""; private _selected: boolean; lastNotifyMethod = (): string => { @@ -690,6 +690,9 @@ class user implements User { } }); + get id() { return this._id; } + set id(v: string) { this._id = v; } + update = (user: User) => { this.id = user.id; @@ -1934,7 +1937,7 @@ export class accountsList { const button = document.createElement("button") as HTMLButtonElement; button.type = "button"; button.classList.add("button", "~urge", "ml-2"); - button.innerHTML = `Match Text`; + button.innerHTML = `${window.lang.strings("matchText")}`; // Position cursor between quotes button.addEventListener("click", () => fillInFilter(queryName, `""`, -1)); From bf12016315988c27a5f954c1a3cb11328e84f58c Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 14 Jun 2023 19:59:38 +0100 Subject: [PATCH 16/16] accounts: make filter names translatable --- lang/admin/en-us.json | 3 ++- ts/modules/accounts.ts | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 770e3b3..d5aa662 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -123,7 +123,8 @@ "clearSearch": "Clear search", "actions": "Actions", "searchOptions": "Search Options", - "matchText": "Match Text" + "matchText": "Match Text", + "jellyfinID": "Jellyfin ID" }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index 0c2cce5..957e86c 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -808,28 +808,29 @@ export class accountsList { private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = { "id": { - name: "Jellyfin ID", + // We don't use a translation here to circumvent the name substitution feature. + name: "Jellyfin/Emby ID", getter: "id", bool: false, string: true, date: false }, "label": { - name: "Label", + name: window.lang.strings("label"), getter: "label", bool: true, string: true, date: false }, "username": { - name: "Username", + name: window.lang.strings("username"), getter: "name", bool: false, string: true, date: false }, "name": { - name: "Username", + name: window.lang.strings("username"), getter: "name", bool: false, string: true, @@ -837,21 +838,21 @@ export class accountsList { show: false }, "admin": { - name: "Admin", + name: window.lang.strings("admin"), getter: "admin", bool: true, string: false, date: false }, "disabled": { - name: "Disabled", + name: window.lang.strings("disabled"), getter: "disabled", bool: true, string: false, date: false }, "access-jfa": { - name: "Access jfa-go", + name: window.lang.strings("accessJFA"), getter: "accounts_admin", bool: true, string: false, @@ -859,7 +860,7 @@ export class accountsList { dependsOnTableHeader: "accounts-header-access-jfa" }, "email": { - name: "Email", + name: window.lang.strings("emailAddress"), getter: "email", bool: true, string: true, @@ -891,7 +892,7 @@ export class accountsList { dependsOnTableHeader: "accounts-header-discord" }, "expiry": { - name: "Expiry", + name: window.lang.strings("expiry"), getter: "expiry", bool: true, string: false, @@ -899,7 +900,7 @@ export class accountsList { dependsOnTableHeader: "accounts-header-expiry" }, "last-active": { - name: "Last Active", + name: window.lang.strings("lastActiveTime"), getter: "last_active", bool: true, string: false, @@ -908,6 +909,7 @@ export class accountsList { } search = (query: String): string[] => { + console.log(this._queries); this._filterArea.textContent = ""; query = query.toLowerCase();