mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
add user profiles menu
also fixed a bug where invites wouldn't load after deleting a profile.
This commit is contained in:
parent
29fafba035
commit
563888c5e7
3
api.go
3
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()
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +132,51 @@
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-user-profiles" class="modal">
|
||||
<div class="modal-content wide card">
|
||||
<span class="heading">User profiles <span class="modal-close">×</span></span>
|
||||
<p class="support lg">Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Default</th>
|
||||
<th>From</th>
|
||||
<th>Libraries</th>
|
||||
<th><span class="button ~neutral !high" id="button-profile-create">Create</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-profiles">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-add-profile" class="modal">
|
||||
<form class="modal-content card" id="form-add-profile" href="">
|
||||
<span class="heading">Add profile <span class="modal-close">×</span></span>
|
||||
<p class="content">Create a Jellyfin user and configure it. Select it here, and when this profile is applied to an invite, new users will be created with its settings.</p>
|
||||
<label>
|
||||
<span class="supra">Profile Name </span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="Name" id="add-profile-name">
|
||||
<label>
|
||||
<span class="supra">User</span>
|
||||
<div class="select ~neutral !normal mt-half mb-1">
|
||||
<select id="add-profile-user">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="switch mb-1">
|
||||
<input type="checkbox" id="add-profile-homescreen" checked>
|
||||
<span>Store homescreen layout</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="page-container max-w-screen-lg px-6 py-4 mx-auto lg:mx-auto md:py-8">
|
||||
<div class="mb-1">
|
||||
@ -239,6 +284,7 @@
|
||||
<div class="card ~neutral !normal col" id="settings-sidebar">
|
||||
<aside class="aside sm ~info mb-half">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-about"><span class="flex">About <i class="ri-information-line ml-half"></i></span></span>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-profiles"><span class="flex">User profiles <i class="ri-user-line ml-half"></i></span></span>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col overflow" id="settings-panel"></div>
|
||||
</div>
|
||||
|
@ -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 () {
|
||||
|
204
ts/modules/profiles.ts
Normal file
204
ts/modules/profiles.ts
Normal file
@ -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 = `
|
||||
<td><b class="profile-name"></b> <span class="profile-admin"></span></td>
|
||||
<td><input type="radio" name="profile-default"></td>
|
||||
<td class="profile-from ellipsis"></td>
|
||||
<td class="profile-libraries"></td>
|
||||
<td><span class="button ~critical !normal">Delete</span></td>
|
||||
`;
|
||||
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 = `<tr><td class="empty">None</td></tr>`
|
||||
} 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 += `<option value="${user['id']}">${user['name']}</option>`;
|
||||
}
|
||||
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();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
}
|
||||
}
|
@ -4,12 +4,6 @@ interface settingsBoolEvent extends Event {
|
||||
detail: boolean;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
Admin: boolean;
|
||||
LibraryAccess: string;
|
||||
FromUser: string;
|
||||
}
|
||||
|
||||
interface Meta {
|
||||
name: string;
|
||||
description: string;
|
||||
|
@ -60,6 +60,8 @@ declare interface Modals {
|
||||
settingsRefresh: Modal;
|
||||
ombiDefaults?: Modal;
|
||||
newAccountSuccess?: Modal;
|
||||
profiles: Modal;
|
||||
addProfile: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
Loading…
Reference in New Issue
Block a user