From 563888c5e796bc27eb7d93a183fee07e735b10c3 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 4 Jan 2021 01:02:47 +0000 Subject: [PATCH] add user profiles menu also fixed a bug where invites wouldn't load after deleting a profile. --- api.go | 3 + css/base.css | 6 ++ css/modal.css | 12 ++- html/admin.html | 46 ++++++++++ ts/admin.ts | 7 ++ ts/modules/profiles.ts | 204 +++++++++++++++++++++++++++++++++++++++++ ts/modules/settings.ts | 6 -- ts/typings/d.ts | 2 + 8 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 ts/modules/profiles.ts diff --git a/api.go b/api.go index b1581d9..c70d2f7 100644 --- a/api.go +++ b/api.go @@ -667,6 +667,9 @@ func (app *appContext) DeleteProfile(gc *gin.Context) { gc.BindJSON(&req) name := req.Name if _, ok := app.storage.profiles[name]; ok { + if app.storage.defaultProfile == name { + app.storage.defaultProfile = "" + } delete(app.storage.profiles, name) } app.storage.storeProfiles() diff --git a/css/base.css b/css/base.css index d997dd4..0c4eaf7 100644 --- a/css/base.css +++ b/css/base.css @@ -216,6 +216,12 @@ sup.\~critical, .text-critical { width: auto; } +.ellipsis { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + .no-pad { padding: 0px 0px 0px 0px; } diff --git a/css/modal.css b/css/modal.css index 64e69ef..3f27a99 100644 --- a/css/modal.css +++ b/css/modal.css @@ -40,12 +40,22 @@ width: 30%; } +.modal-content.wide { + width: 60%; +} + .modal-shown .modal-content { animation: modal-content-show 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); } +@media screen and (max-width: 1000px) { + .modal-content.wide { + width: 75%; + } +} + @media screen and (max-width: 400px) { - .modal-content { + .modal-content, .modal-content.wide { width: 90%; } } diff --git a/html/admin.html b/html/admin.html index 5b01907..bd34a09 100644 --- a/html/admin.html +++ b/html/admin.html @@ -132,6 +132,51 @@ + +
@@ -239,6 +284,7 @@
About + User profiles
diff --git a/ts/admin.ts b/ts/admin.ts index 56c8cf5..ca2317b 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -4,6 +4,7 @@ import { Tabs } from "./modules/tabs.js"; import { inviteList, createInvite } from "./modules/invites.js"; import { accountsList } from "./modules/accounts.js"; import { settingsList } from "./modules/settings.js"; +import { ProfileEditor } from "./modules/profiles.js"; import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; loadTheme(); @@ -36,6 +37,10 @@ window.availableProfiles = window.availableProfiles || []; window.modals.ombiDefaults = new Modal(document.getElementById('modal-ombi-defaults')); document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiDefaults.close); + + window.modals.profiles = new Modal(document.getElementById("modal-user-profiles")); + + window.modals.addProfile = new Modal(document.getElementById("modal-add-profile")); })(); var inviteCreator = new createInvite(); @@ -45,6 +50,8 @@ window.invites = new inviteList(); var settings = new settingsList(); +var profiles = new ProfileEditor(); + window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); /*const modifySettingsSource = function () { diff --git a/ts/modules/profiles.ts b/ts/modules/profiles.ts new file mode 100644 index 0000000..c2f5296 --- /dev/null +++ b/ts/modules/profiles.ts @@ -0,0 +1,204 @@ +import { _get, _post, _delete, toggleLoader } from "../modules/common.js"; + +interface Profile { + admin: boolean; + libraries: string; + fromUser: string; +} + +class profile implements Profile { + private _row: HTMLTableRowElement; + private _name: HTMLElement; + private _adminChip: HTMLSpanElement; + private _libraries: HTMLTableDataCellElement; + private _fromUser: HTMLTableDataCellElement; + private _defaultRadio: HTMLInputElement; + + get name(): string { return this._name.textContent; } + set name(v: string) { this._name.textContent = v; } + + get admin(): boolean { return this._adminChip.classList.contains("chip"); } + set admin(state: boolean) { + if (state) { + this._adminChip.classList.add("chip", "~info", "ml-half"); + this._adminChip.textContent = "Admin"; + } else { + this._adminChip.classList.remove("chip", "~info", "ml-half"); + this._adminChip.textContent = ""; + } + } + + get libraries(): string { return this._libraries.textContent; } + set libraries(v: string) { this._libraries.textContent = v; } + + get fromUser(): string { return this._fromUser.textContent; } + set fromUser(v: string) { this._fromUser.textContent = v; } + + get default(): boolean { return this._defaultRadio.checked; } + set default(v: boolean) { this._defaultRadio.checked = v; } + + constructor(name: string, p: Profile) { + this._row = document.createElement("tr") as HTMLTableRowElement; + this._row.innerHTML = ` + + + + + Delete + `; + this._name = this._row.querySelector("b.profile-name"); + this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement; + this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement; + this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement; + this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement; + this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name })); + (this._row.querySelector("span.button") as HTMLSpanElement).onclick = this.delete; + + this.update(name, p); + } + + update = (name: string, p: Profile) => { + this.name = name; + this.admin = p.admin; + this.fromUser = p.fromUser; + this.libraries = p.libraries; + } + + remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); } + + delete = () => _delete("/profiles", { "name": this.name }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + this.remove(); + } else { + window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`); + } + } + }) + + asElement = (): HTMLTableRowElement => { return this._row; } +} + +interface profileResp { + default_profile: string; + profiles: { [name: string]: Profile }; +} + +export class ProfileEditor { + private _table = document.getElementById("table-profiles") as HTMLTableElement; + private _createButton = document.getElementById("button-profile-create") as HTMLSpanElement; + private _profiles: { [name: string]: profile } = {}; + private _default: string; + + private _createForm = document.getElementById("form-add-profile") as HTMLFormElement; + private _profileName = document.getElementById("add-profile-name") as HTMLInputElement; + private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement; + private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement; + + get empty(): boolean { return (Object.keys(this._table.children).length == 0) } + set empty(state: boolean) { + if (state) { + this._table.innerHTML = `None` + } else if (this._table.querySelector("td.empty")) { + this._table.textContent = ``; + } + } + + get default(): string { return this._default; } + set default(v: string) { + this._default = v; + if (v != "") { this._profiles[v].default = true; } + for (let name in this._profiles) { + if (name != v) { this._profiles[name].default = false; } + } + } + + load = () => _get("/profiles", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + let resp = req.response as profileResp; + if (Object.keys(resp.profiles).length == 0) { + this.empty = true; + } else { + this.empty = false; + for (let name in resp.profiles) { + if (name in this._profiles) { + this._profiles[name].update(name, resp.profiles[name]); + } else { + this._profiles[name] = new profile(name, resp.profiles[name]); + this._table.appendChild(this._profiles[name].asElement()); + } + } + } + this.default = resp.default_profile; + window.modals.profiles.show(); + } else { + window.notifications.customError("profileEditor", "Failed to load profiles."); + } + } + }) + + constructor() { + (document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load; + document.addEventListener("profiles-default", (event: CustomEvent) => { + const prevDefault = this.default; + const newDefault = event.detail; + _post("/profiles/default", { "name": newDefault }, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + this.default = newDefault; + } else { + this.default = prevDefault; + window.notifications.customError("profileDefault", "Failed to set default profile."); + } + } + }); + }); + document.addEventListener("profiles-delete", (event: CustomEvent) => { + delete this._profiles[event.detail]; + this.load(); + }); + + this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + let innerHTML = ``; + for (let user of req.response["users"]) { + innerHTML += ``; + } + this._userSelect.innerHTML = innerHTML; + this._storeHomescreen.checked = true; + window.modals.profiles.close(); + window.modals.addProfile.show(); + } else { + window.notifications.customError("loadUsers", "Failed to load users."); + } + } + }); + + this._createForm.onsubmit = (event: SubmitEvent) => { + event.preventDefault(); + const button = this._createForm.querySelector("span.submit") as HTMLSpanElement; + toggleLoader(button); + let send = { + "homescreen": this._storeHomescreen.checked, + "id": this._userSelect.value, + "name": this._profileName.value + } + _post("/profiles", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + window.modals.addProfile.close(); + if (req.status == 200 || req.status == 204) { + this.load(); + window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`); + } else { + window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`); + } + window.modals.profiles.show(); + } + }) + }; + + } +} diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index eb4b1c2..0467dcd 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -4,12 +4,6 @@ interface settingsBoolEvent extends Event { detail: boolean; } -interface Profile { - Admin: boolean; - LibraryAccess: string; - FromUser: string; -} - interface Meta { name: string; description: string; diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 1d975da..f09054c 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -60,6 +60,8 @@ declare interface Modals { settingsRefresh: Modal; ombiDefaults?: Modal; newAccountSuccess?: Modal; + profiles: Modal; + addProfile: Modal; } interface Invite {