Merge a17t-redesign, kinda ts-ify setup.js
the web ui has been redesigned with the a17t toolkit, which imo looks a lot better than bootstrap. This also brought a complete rework of the web code, which now makes a lot more sense hopefully. the setup page is still stuck with bootstrap, its not much of a priority but i'll rewrite it eventually.
This commit is contained in:
+391
-145
@@ -1,163 +1,409 @@
|
||||
import { _get, _post, _delete, createEl } from "../modules/common.js";
|
||||
import { Focus, Unfocus } from "../modules/admin.js";
|
||||
import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
|
||||
|
||||
interface aWindow extends Window {
|
||||
checkCheckboxes: () => void;
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string | undefined;
|
||||
last_active: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
class user implements User {
|
||||
private _row: HTMLTableRowElement;
|
||||
private _check: HTMLInputElement;
|
||||
private _username: HTMLSpanElement;
|
||||
private _admin: HTMLSpanElement;
|
||||
private _email: HTMLInputElement;
|
||||
private _emailAddress: string;
|
||||
private _emailEditButton: HTMLElement;
|
||||
private _lastActive: HTMLTableDataCellElement;
|
||||
id: string;
|
||||
private _selected: boolean;
|
||||
|
||||
export const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email);
|
||||
get selected(): boolean { return this._selected; }
|
||||
set selected(state: boolean) {
|
||||
this._selected = state;
|
||||
this._check.checked = state;
|
||||
state ? document.dispatchEvent(this._checkEvent) : document.dispatchEvent(this._uncheckEvent);
|
||||
}
|
||||
|
||||
export const checkCheckboxes = (): void => {
|
||||
const defaultsButton = document.getElementById('accountsTabSetDefaults');
|
||||
const deleteButton = document.getElementById('accountsTabDelete');
|
||||
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
|
||||
let checked = checkboxes.length;
|
||||
if (checked == 0) {
|
||||
Unfocus(defaultsButton);
|
||||
Unfocus(deleteButton);
|
||||
} else {
|
||||
Focus(defaultsButton);
|
||||
Focus(deleteButton);
|
||||
if (checked == 1) {
|
||||
deleteButton.textContent = 'Delete User';
|
||||
get name(): string { return this._username.textContent; }
|
||||
set name(value: string) { this._username.textContent = value; }
|
||||
|
||||
get admin(): boolean { return this._admin.classList.contains("chip"); }
|
||||
set admin(state: boolean) {
|
||||
if (state) {
|
||||
this._admin.classList.add("chip", "~info", "ml-1");
|
||||
this._admin.textContent = "Admin";
|
||||
} else {
|
||||
deleteButton.textContent = 'Delete Users';
|
||||
this._admin.classList.remove("chip", "~info", "ml-1");
|
||||
this._admin.textContent = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.checkCheckboxes = checkCheckboxes;
|
||||
get email(): string { return this._emailAddress; }
|
||||
set email(value: string) { this._email.value = value; this._emailAddress = value; }
|
||||
|
||||
get last_active(): string { return this._lastActive.textContent; }
|
||||
set last_active(value: string) { this._lastActive.textContent = value; }
|
||||
|
||||
export function changeEmail(icon: HTMLElement, id: string): void {
|
||||
const iconContent = icon.outerHTML;
|
||||
icon.setAttribute('class', '');
|
||||
const entry = icon.nextElementSibling as HTMLInputElement;
|
||||
const ogEmail = entry.value;
|
||||
entry.readOnly = false;
|
||||
entry.classList.remove('form-control-plaintext');
|
||||
entry.classList.add('form-control');
|
||||
if (ogEmail == "") {
|
||||
entry.placeholder = 'Address';
|
||||
private _checkEvent = new CustomEvent("accountCheckEvent");
|
||||
private _uncheckEvent = new CustomEvent("accountUncheckEvent");
|
||||
|
||||
constructor(user: User) {
|
||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||
this._row.innerHTML = `
|
||||
<td><input type="checkbox" value=""></td>
|
||||
<td><span class="accounts-username"></span> <span class="accounts-admin"></span></td>
|
||||
<td><i class="icon ri-edit-line accounts-email-edit"></i><input type="email" class="input ~neutral !normal stealth-input stealth-input-hidden accounts-email" readonly></td>
|
||||
<td class="accounts-last-active"></td>
|
||||
`;
|
||||
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
||||
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
|
||||
this._email = this._row.querySelector(".accounts-email") as HTMLInputElement;
|
||||
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||
|
||||
const toggleStealthInput = () => {
|
||||
this._email.classList.toggle("stealth-input-hidden");
|
||||
this._email.readOnly = !this._email.readOnly;
|
||||
this._emailEditButton.classList.toggle("ri-check-line");
|
||||
this._emailEditButton.classList.toggle("ri-edit-line");
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) {
|
||||
toggleStealthInput();
|
||||
this.email = this.email;
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
}
|
||||
};
|
||||
this._emailEditButton.onclick = () => {
|
||||
if (this._email.classList.contains("stealth-input-hidden")) {
|
||||
document.addEventListener('click', outerClickListener);
|
||||
} else {
|
||||
this._updateEmail();
|
||||
document.removeEventListener('click', outerClickListener);
|
||||
}
|
||||
toggleStealthInput();
|
||||
};
|
||||
|
||||
this.update(user);
|
||||
}
|
||||
const tick = createEl(`
|
||||
<i class="fa fa-check d-inline-block icon-button text-success" style="margin-left: 0.5rem; margin-right: 0.5rem;"></i>
|
||||
`);
|
||||
tick.onclick = (): void => {
|
||||
const newEmail = entry.value;
|
||||
if (!validateEmail(newEmail) || newEmail == ogEmail) {
|
||||
return;
|
||||
}
|
||||
cross.remove();
|
||||
const spinner = createEl(`
|
||||
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
|
||||
<span class="sr-only">Saving...</span>
|
||||
</div>
|
||||
`);
|
||||
tick.replaceWith(spinner);
|
||||
|
||||
private _updateEmail = () => {
|
||||
let oldEmail = this.email;
|
||||
this.email = this._email.value;
|
||||
let send = {};
|
||||
send[id] = newEmail;
|
||||
_post("/users/emails", send, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
entry.nextElementSibling.remove();
|
||||
send[this.id] = this.email;
|
||||
_post("/users/emails", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200) {
|
||||
window.notifications.customPositive("emailChanged", "Success:", `Changed email address of "${this.name}".`);
|
||||
} else {
|
||||
entry.value = ogEmail;
|
||||
this.email = oldEmail;
|
||||
window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`);
|
||||
}
|
||||
}
|
||||
});
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
};
|
||||
const cross = createEl(`
|
||||
<i class="fa fa-close d-inline-block icon-button text-danger"></i>
|
||||
`);
|
||||
cross.onclick = (): void => {
|
||||
tick.remove();
|
||||
cross.remove();
|
||||
icon.outerHTML = iconContent;
|
||||
entry.readOnly = true;
|
||||
entry.classList.remove('form-control');
|
||||
entry.classList.add('form-control-plaintext');
|
||||
entry.placeholder = '';
|
||||
entry.value = ogEmail;
|
||||
};
|
||||
icon.parentNode.appendChild(tick);
|
||||
icon.parentNode.appendChild(cross);
|
||||
};
|
||||
|
||||
export function populateUsers(): void {
|
||||
const acList = document.getElementById('accountsList');
|
||||
acList.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<strong>Getting Users...</strong>
|
||||
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
|
||||
</div>
|
||||
`;
|
||||
Unfocus(acList.parentNode.querySelector('thead'));
|
||||
const accountsList = document.createElement('tbody');
|
||||
accountsList.id = 'accountsList';
|
||||
const generateEmail = (id: string, name: string, email: string): string => {
|
||||
let entry: HTMLDivElement = document.createElement('div');
|
||||
entry.id = 'email_' + id;
|
||||
let emailValue: string = email;
|
||||
if (emailValue == undefined) {
|
||||
emailValue = "";
|
||||
}
|
||||
entry.innerHTML = `
|
||||
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
|
||||
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
|
||||
`;
|
||||
return entry.outerHTML;
|
||||
};
|
||||
const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
|
||||
let fci = "form-check-input";
|
||||
if (window.bsVersion != 5) {
|
||||
fci = "";
|
||||
}
|
||||
return `
|
||||
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
|
||||
<td nowrap="nowrap" class="align-middle">${username}${admin ? '<span style="margin-left: 1rem;" class="badge rounded-pill bg-info text-dark">Admin</span>' : ''}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
|
||||
`;
|
||||
};
|
||||
|
||||
_get("/users", null, function (): void {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
window.jfUsers = this.response['users'];
|
||||
for (const user of window.jfUsers) {
|
||||
let tr = document.createElement('tr');
|
||||
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
|
||||
accountsList.appendChild(tr);
|
||||
}
|
||||
Focus(acList.parentNode.querySelector('thead'));
|
||||
acList.replaceWith(accountsList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function populateRadios(): void {
|
||||
const radioList = document.getElementById('defaultUserRadios');
|
||||
radioList.textContent = '';
|
||||
let first = true;
|
||||
for (const i in window.jfUsers) {
|
||||
const user = window.jfUsers[i];
|
||||
const radio = document.createElement('div');
|
||||
radio.classList.add('form-check');
|
||||
let checked = '';
|
||||
if (first) {
|
||||
checked = 'checked';
|
||||
first = false;
|
||||
}
|
||||
radio.innerHTML = `
|
||||
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
|
||||
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
|
||||
radioList.appendChild(radio);
|
||||
}
|
||||
}
|
||||
|
||||
update = (user: User) => {
|
||||
this.id = user.id;
|
||||
this.name = user.name;
|
||||
this.email = user.email || "";
|
||||
this.last_active = user.last_active;
|
||||
this.admin = user.admin;
|
||||
}
|
||||
|
||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||
remove = () => {
|
||||
if (this.selected) {
|
||||
document.dispatchEvent(this._uncheckEvent);
|
||||
}
|
||||
this._row.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export class accountsList {
|
||||
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
|
||||
|
||||
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
|
||||
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
|
||||
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
|
||||
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
|
||||
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
|
||||
private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
|
||||
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;
|
||||
private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement;
|
||||
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
|
||||
|
||||
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
|
||||
private _users: { [id: string]: user };
|
||||
private _checkCount: number = 0;
|
||||
|
||||
private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement;
|
||||
private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement;
|
||||
private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement;
|
||||
private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement;
|
||||
|
||||
get selectAll(): boolean { return this._selectAll.checked; }
|
||||
set selectAll(state: boolean) {
|
||||
for (let id in this._users) {
|
||||
this._users[id].selected = state;
|
||||
}
|
||||
this._selectAll.checked = state;
|
||||
this._selectAll.indeterminate = false;
|
||||
state ? this._checkCount = Object.keys(this._users).length : 0;
|
||||
|
||||
}
|
||||
|
||||
add = (u: User) => {
|
||||
let domAccount = new user(u);
|
||||
this._users[u.id] = domAccount;
|
||||
this._table.appendChild(domAccount.asElement());
|
||||
}
|
||||
|
||||
private _checkCheckCount = () => {
|
||||
if (this._checkCount == 0) {
|
||||
this._selectAll.indeterminate = false;
|
||||
this._selectAll.checked = false;
|
||||
this._modifySettings.classList.add("unfocused");
|
||||
this._deleteUser.classList.add("unfocused");
|
||||
} else {
|
||||
if (this._checkCount == Object.keys(this._users).length) {
|
||||
this._selectAll.checked = true;
|
||||
this._selectAll.indeterminate = false;
|
||||
} else {
|
||||
this._selectAll.checked = false;
|
||||
this._selectAll.indeterminate = true;
|
||||
}
|
||||
this._modifySettings.classList.remove("unfocused");
|
||||
this._deleteUser.classList.remove("unfocused");
|
||||
(this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users";
|
||||
}
|
||||
}
|
||||
|
||||
private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; }
|
||||
|
||||
private _collectUsers = (): string[] => {
|
||||
let list: string[] = [];
|
||||
for (let id in this._users) {
|
||||
if (this._users[id].selected) { list.push(id); }
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private _addUser = (event: Event) => {
|
||||
event.preventDefault();
|
||||
const button = this._addUserForm.querySelector("span.submit") as HTMLSpanElement;
|
||||
const send = {
|
||||
"username": this._addUserName.value,
|
||||
"email": this._addUserEmail.value,
|
||||
"password": this._addUserPassword.value
|
||||
};
|
||||
for (let field in send) {
|
||||
if (!send[field]) {
|
||||
window.notifications.customError("addUserBlankField", "Fields were left blank.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
toggleLoader(button);
|
||||
_post("/users", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200) {
|
||||
window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`);
|
||||
}
|
||||
this.reload();
|
||||
window.modals.addUser.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteUsers = () => {
|
||||
const modalHeader = document.getElementById("header-delete-user");
|
||||
modalHeader.textContent = this._genCountString();
|
||||
let list = this._collectUsers();
|
||||
const form = document.getElementById("form-delete-user") as HTMLFormElement;
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
this._deleteNotify.checked = false;
|
||||
this._deleteReason.value = "";
|
||||
this._deleteReason.classList.add("unfocused");
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"users": list,
|
||||
"notify": this._deleteNotify.checked,
|
||||
"reason": this._deleteNotify ? this._deleteReason.value : ""
|
||||
};
|
||||
_delete("/users", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
window.modals.deleteUser.close();
|
||||
if (req.status != 200 && req.status != 204) {
|
||||
let errorMsg = "Failed (check console/logs).";
|
||||
if (!("error" in req.response)) {
|
||||
errorMsg = "Partial failure (check console/logs).";
|
||||
}
|
||||
window.notifications.customError("deleteUserError", errorMsg);
|
||||
} else {
|
||||
window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`);
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
window.modals.deleteUser.show();
|
||||
}
|
||||
|
||||
modifyUsers = () => {
|
||||
const modalHeader = document.getElementById("header-modify-user");
|
||||
modalHeader.textContent = this._genCountString();
|
||||
let list = this._collectUsers();
|
||||
(() => {
|
||||
let innerHTML = "";
|
||||
for (const profile of window.availableProfiles) {
|
||||
innerHTML += `<option value="${profile}">${profile}</option>`;
|
||||
}
|
||||
this._profileSelect.innerHTML = innerHTML;
|
||||
})();
|
||||
|
||||
(() => {
|
||||
let innerHTML = "";
|
||||
for (let id in this._users) {
|
||||
innerHTML += `<option value="${id}">${this._users[id].name}</option>`;
|
||||
}
|
||||
this._userSelect.innerHTML = innerHTML;
|
||||
})();
|
||||
|
||||
const form = document.getElementById("form-modify-user") as HTMLFormElement;
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
this._modifySettingsProfile.checked = true;
|
||||
this._modifySettingsUser.checked = false;
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"apply_to": list,
|
||||
"homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked
|
||||
};
|
||||
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
|
||||
send["from"] = "profile";
|
||||
send["profile"] = this._profileSelect.value;
|
||||
} else if (this._modifySettingsUser.checked && !this._modifySettingsProfile.checked) {
|
||||
send["from"] = "user";
|
||||
send["id"] = this._userSelect.value;
|
||||
}
|
||||
_post("/users/settings", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 500) {
|
||||
let response = JSON.parse(req.response);
|
||||
let errorMsg = "";
|
||||
if ("homescreen" in response && "policy" in response) {
|
||||
const homescreen = Object.keys(response["homescreen"]).length;
|
||||
const policy = Object.keys(response["policy"]).length;
|
||||
if (homescreen != 0 && policy == 0) {
|
||||
errorMsg = "Settings were applied, but applying homescreen layout may have failed.";
|
||||
} else if (policy != 0 && homescreen == 0) {
|
||||
errorMsg = "Homescreen layout was applied, but applying settings may have failed.";
|
||||
} else if (policy != 0 && homescreen != 0) {
|
||||
errorMsg = "Application failed.";
|
||||
}
|
||||
} else if ("error" in response) {
|
||||
errorMsg = response["error"];
|
||||
}
|
||||
window.notifications.customError("modifySettingsError", errorMsg);
|
||||
} else if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customPositive("modifySettingsSuccess", "Success:", `applied settings to ${this._genCountString()}.`);
|
||||
}
|
||||
this.reload();
|
||||
window.modals.modifyUser.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
window.modals.modifyUser.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor() {
|
||||
this._users = {};
|
||||
this._selectAll.checked = false;
|
||||
this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked };
|
||||
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
|
||||
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
|
||||
this._addUserButton.onclick = window.modals.addUser.toggle;
|
||||
this._addUserForm.addEventListener("submit", this._addUser);
|
||||
|
||||
this._deleteNotify.onchange = () => {
|
||||
if (this._deleteNotify.checked) {
|
||||
this._deleteReason.classList.remove("unfocused");
|
||||
} else {
|
||||
this._deleteReason.classList.add("unfocused");
|
||||
}
|
||||
};
|
||||
this._modifySettings.onclick = this.modifyUsers;
|
||||
this._modifySettings.classList.add("unfocused");
|
||||
const checkSource = () => {
|
||||
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
|
||||
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
|
||||
if (this._modifySettingsProfile.checked) {
|
||||
this._userSelect.parentElement.classList.add("unfocused");
|
||||
this._profileSelect.parentElement.classList.remove("unfocused")
|
||||
profileSpan.classList.add("!high");
|
||||
profileSpan.classList.remove("!normal");
|
||||
userSpan.classList.remove("!high");
|
||||
userSpan.classList.add("!normal");
|
||||
} else {
|
||||
this._userSelect.parentElement.classList.remove("unfocused");
|
||||
this._profileSelect.parentElement.classList.add("unfocused");
|
||||
userSpan.classList.add("!high");
|
||||
userSpan.classList.remove("!normal");
|
||||
profileSpan.classList.remove("!high");
|
||||
profileSpan.classList.add("!normal");
|
||||
}
|
||||
};
|
||||
this._modifySettingsProfile.onchange = checkSource;
|
||||
this._modifySettingsUser.onchange = checkSource;
|
||||
|
||||
this._deleteUser.onclick = this.deleteUsers;
|
||||
this._deleteUser.classList.add("unfocused");
|
||||
|
||||
if (!window.usernameEnabled) {
|
||||
this._addUserName.classList.add("unfocused");
|
||||
this._addUserName = this._addUserEmail;
|
||||
}
|
||||
/*if (!window.emailEnabled) {
|
||||
this._deleteNotify.parentElement.classList.add("unfocused");
|
||||
this._deleteNotify.checked = false;
|
||||
}*/
|
||||
}
|
||||
|
||||
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4 && req.status == 200) {
|
||||
// same method as inviteList.reload()
|
||||
let accountsOnDOM: { [id: string]: boolean } = {};
|
||||
for (let id in this._users) { accountsOnDOM[id] = true; }
|
||||
for (let u of (req.response["users"] as User[])) {
|
||||
if (u.id in this._users) {
|
||||
this._users[u.id].update(u);
|
||||
delete accountsOnDOM[u.id];
|
||||
} else {
|
||||
this.add(u);
|
||||
}
|
||||
}
|
||||
for (let id in accountsOnDOM) {
|
||||
this._users[id].remove();
|
||||
delete this._users[id];
|
||||
}
|
||||
this._checkCheckCount;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { rmAttr, addAttr, _post, _get, _delete, createEl } from "../modules/common.js";
|
||||
|
||||
export const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
|
||||
export const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
|
||||
|
||||
export function storeDefaults(users: string | Array<string>): void {
|
||||
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
|
||||
button.disabled = true;
|
||||
button.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
|
||||
'Loading...';
|
||||
let data = { "homescreen": false };
|
||||
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
|
||||
data["from"] = "profile";
|
||||
data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
|
||||
} else {
|
||||
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
|
||||
let id = radio.id.replace("default_", "");
|
||||
data["from"] = "user";
|
||||
data["id"] = id;
|
||||
}
|
||||
if (users != "all") {
|
||||
data["apply_to"] = users;
|
||||
}
|
||||
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
|
||||
data["homescreen"] = true;
|
||||
}
|
||||
_post("/users/settings", data, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 200 || this.status == 204) {
|
||||
button.textContent = "Success";
|
||||
addAttr(button, "btn-success");
|
||||
rmAttr(button, "btn-danger");
|
||||
rmAttr(button, "btn-primary");
|
||||
button.disabled = false;
|
||||
setTimeout((): void => {
|
||||
button.textContent = "Submit";
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-success");
|
||||
button.disabled = false;
|
||||
window.Modals.userDefaults.hide();
|
||||
}, 1000);
|
||||
} else {
|
||||
if ("error" in this.response) {
|
||||
button.textContent = this.response["error"];
|
||||
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
|
||||
button.textContent = "Failed (check console)";
|
||||
} else {
|
||||
button.textContent = "Failed";
|
||||
}
|
||||
addAttr(button, "btn-danger");
|
||||
rmAttr(button, "btn-primary");
|
||||
setTimeout((): void => {
|
||||
button.textContent = "Submit";
|
||||
addAttr(button, "btn-primary");
|
||||
rmAttr(button, "btn-danger");
|
||||
button.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { rmAttr, addAttr } from "../modules/common.js";
|
||||
|
||||
interface aWindow extends Window {
|
||||
rotateButton(el: HTMLElement): void;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
|
||||
// Used for animation on theme change
|
||||
const whichTransitionEvent = (): string => {
|
||||
const el = document.createElement('fakeElement');
|
||||
const transitions = {
|
||||
'transition': 'transitionend',
|
||||
'OTransition': 'oTransitionEnd',
|
||||
'MozTransition': 'transitionend',
|
||||
'WebkitTransition': 'webkitTransitionEnd'
|
||||
};
|
||||
for (const t in transitions) {
|
||||
if (el.style[t] !== undefined) {
|
||||
return transitions[t];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
var transitionEndEvent = whichTransitionEvent();
|
||||
|
||||
// Toggles between light and dark themes
|
||||
const _toggleCSS = (): void => {
|
||||
const els: NodeListOf<HTMLLinkElement> = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]');
|
||||
let cssEl = 0;
|
||||
let remove = false;
|
||||
if (els.length != 1) {
|
||||
cssEl = 1;
|
||||
remove = true
|
||||
}
|
||||
let href: string = "bs" + window.bsVersion;
|
||||
if (!els[cssEl].href.includes(href + "-jf")) {
|
||||
href += "-jf";
|
||||
}
|
||||
href += ".css";
|
||||
let newEl = els[cssEl].cloneNode(true) as HTMLLinkElement;
|
||||
newEl.href = href;
|
||||
els[cssEl].parentNode.insertBefore(newEl, els[cssEl].nextSibling);
|
||||
if (remove) {
|
||||
els[0].remove();
|
||||
}
|
||||
document.cookie = "css=" + href;
|
||||
}
|
||||
|
||||
// Toggles between light and dark themes, but runs animation if window small enough.
|
||||
window.buttonWidth = 0;
|
||||
export const toggleCSS = (el: HTMLElement): void => {
|
||||
const switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
|
||||
// Max page width for animation to take place
|
||||
let maxWidth = 1500;
|
||||
if (window.innerWidth < maxWidth) {
|
||||
// Calculate minimum radius to cover screen
|
||||
const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
|
||||
const currentRadius = el.getBoundingClientRect().width / 2;
|
||||
const scale = radius / currentRadius;
|
||||
window.buttonWidth = +window.getComputedStyle(el, null).width;
|
||||
document.body.classList.remove('smooth-transition');
|
||||
el.style.transform = `scale(${scale})`;
|
||||
el.style.color = switchToColor;
|
||||
el.addEventListener(transitionEndEvent, function (): void {
|
||||
if (this.style.transform.length != 0) {
|
||||
_toggleCSS();
|
||||
this.style.removeProperty('transform');
|
||||
document.body.classList.add('smooth-transition');
|
||||
}
|
||||
}, false);
|
||||
} else {
|
||||
_toggleCSS();
|
||||
el.style.color = switchToColor;
|
||||
}
|
||||
};
|
||||
|
||||
window.rotateButton = (el: HTMLElement): void => {
|
||||
if (el.classList.contains("rotated")) {
|
||||
rmAttr(el, "rotated")
|
||||
addAttr(el, "not-rotated");
|
||||
} else {
|
||||
rmAttr(el, "not-rotated");
|
||||
addAttr(el, "rotated");
|
||||
}
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
declare var $: any;
|
||||
|
||||
class Modal implements BSModal {
|
||||
el: HTMLDivElement;
|
||||
modal: any;
|
||||
|
||||
constructor(id: string, find?: boolean) {
|
||||
this.el = document.getElementById(id) as HTMLDivElement;
|
||||
this.modal = $(this.el) as any;
|
||||
this.modal.on("shown.b.modal", (): void => document.body.classList.add('modal-open'));
|
||||
};
|
||||
|
||||
show(): void { this.modal.modal("show"); };
|
||||
hide(): void { this.modal.modal("hide"); };
|
||||
}
|
||||
|
||||
export class BS4 implements Bootstrap {
|
||||
triggerTooltips: tooltipTrigger = function (): void {
|
||||
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
|
||||
for (const i in checkboxes) {
|
||||
checkboxes[i].click();
|
||||
checkboxes[i].click();
|
||||
}
|
||||
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||
tooltips.map((el: HTMLAnchorElement): any => {
|
||||
return ($(el) as any).tooltip();
|
||||
});
|
||||
};
|
||||
|
||||
Compat(): void {
|
||||
console.log('Fixing BS4 Compatability');
|
||||
const send_to_address_enabled = document.getElementById('send_to_address_enabled');
|
||||
if (send_to_address_enabled) {
|
||||
send_to_address_enabled.classList.remove("form-check-input");
|
||||
}
|
||||
const multiUseEnabled = document.getElementById('multiUseEnabled');
|
||||
if (multiUseEnabled) {
|
||||
multiUseEnabled.classList.remove("form-check-input");
|
||||
}
|
||||
}
|
||||
|
||||
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
|
||||
return new Modal(id, find);
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
declare var bootstrap: any;
|
||||
|
||||
class Modal implements BSModal {
|
||||
el: HTMLDivElement;
|
||||
modal: any;
|
||||
|
||||
constructor(id: string, find?: boolean) {
|
||||
this.el = document.getElementById(id) as HTMLDivElement;
|
||||
if (find) {
|
||||
this.modal = bootstrap.Modal.getInstance(this.el);
|
||||
} else {
|
||||
this.modal = new bootstrap.Modal(this.el);
|
||||
}
|
||||
this.el.addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
|
||||
};
|
||||
|
||||
show(): void { this.modal.show(); };
|
||||
hide(): void { this.modal.hide(); };
|
||||
}
|
||||
|
||||
export class BS5 implements Bootstrap {
|
||||
triggerTooltips: tooltipTrigger = function (): void {
|
||||
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
|
||||
for (const i in checkboxes) {
|
||||
checkboxes[i].click();
|
||||
checkboxes[i].click();
|
||||
}
|
||||
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
|
||||
tooltips.map((el: HTMLAnchorElement): any => {
|
||||
return new bootstrap.Tooltip(el);
|
||||
});
|
||||
};
|
||||
|
||||
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
|
||||
return new Modal(id, find);
|
||||
};
|
||||
};
|
||||
+129
-7
@@ -49,18 +49,25 @@ export const rmAttr = (el: HTMLElement, attr: string): void => {
|
||||
};
|
||||
|
||||
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
|
||||
|
||||
export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
|
||||
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", window.URLBase + url, true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.onreadystatechange = () => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.connectionError();
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
|
||||
}
|
||||
onreadystatechange(req);
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
|
||||
export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", window.URLBase + url, true);
|
||||
if (response) {
|
||||
@@ -68,16 +75,131 @@ export const _post = (url: string, data: Object, onreadystatechange: () => void,
|
||||
}
|
||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.onreadystatechange = () => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.connectionError();
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
|
||||
}
|
||||
onreadystatechange(req);
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
|
||||
export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("DELETE", window.URLBase + url, true);
|
||||
req.setRequestHeader("Authorization", "Bearer " + window.token);
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.onreadystatechange = () => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.connectionError();
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
|
||||
}
|
||||
onreadystatechange(req);
|
||||
};
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function toClipboard (str: string) {
|
||||
const el = document.createElement('textarea') as HTMLTextAreaElement;
|
||||
el.value = str;
|
||||
el.readOnly = true;
|
||||
el.style.position = "absolute";
|
||||
el.style.left = "-9999px";
|
||||
document.body.appendChild(el);
|
||||
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
}
|
||||
|
||||
export class notificationBox implements NotificationBox {
|
||||
private _box: HTMLDivElement;
|
||||
private _errorTypes: { [type: string]: boolean } = {};
|
||||
private _positiveTypes: { [type: string]: boolean } = {};
|
||||
timeout: number;
|
||||
constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; }
|
||||
|
||||
private _error = (message: string): HTMLElement => {
|
||||
const noti = document.createElement('aside');
|
||||
noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error");
|
||||
noti.innerHTML = `<strong>Error:</strong> ${message}`;
|
||||
const closeButton = document.createElement('span') as HTMLSpanElement;
|
||||
closeButton.classList.add("button", "~critical", "!low", "ml-1");
|
||||
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
|
||||
closeButton.onclick = () => { this._box.removeChild(noti); };
|
||||
noti.appendChild(closeButton);
|
||||
return noti;
|
||||
}
|
||||
|
||||
private _positive = (bold: string, message: string): HTMLElement => {
|
||||
const noti = document.createElement('aside');
|
||||
noti.classList.add("aside", "~positive", "!normal", "mt-half", "notification-positive");
|
||||
noti.innerHTML = `<strong>${bold}</strong> ${message}`;
|
||||
const closeButton = document.createElement('span') as HTMLSpanElement;
|
||||
closeButton.classList.add("button", "~positive", "!low", "ml-1");
|
||||
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
|
||||
closeButton.onclick = () => { this._box.removeChild(noti); };
|
||||
noti.appendChild(closeButton);
|
||||
return noti;
|
||||
}
|
||||
|
||||
connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); }
|
||||
|
||||
customError = (type: string, message: string) => {
|
||||
this._errorTypes[type] = this._errorTypes[type] || false;
|
||||
const noti = this._error(message);
|
||||
noti.classList.add("error-" + type);
|
||||
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type);
|
||||
if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) {
|
||||
previousNoti.remove();
|
||||
}
|
||||
this._box.appendChild(noti);
|
||||
this._errorTypes[type] = true;
|
||||
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000);
|
||||
}
|
||||
|
||||
customPositive = (type: string, bold: string, message: string) => {
|
||||
this._positiveTypes[type] = this._positiveTypes[type] || false;
|
||||
const noti = this._positive(bold, message);
|
||||
noti.classList.add("positive-" + type);
|
||||
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type);
|
||||
if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) {
|
||||
previousNoti.remove();
|
||||
}
|
||||
this._box.appendChild(noti);
|
||||
this._positiveTypes[type] = true;
|
||||
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000);
|
||||
}
|
||||
}
|
||||
|
||||
export const whichAnimationEvent = () => {
|
||||
const el = document.createElement("fakeElement");
|
||||
if (el.style["animation"] !== void 0) {
|
||||
return "animationend";
|
||||
}
|
||||
return "webkitAnimationEnd";
|
||||
}
|
||||
|
||||
export function toggleLoader(el: HTMLElement, small: boolean = true) {
|
||||
if (el.classList.contains("loader")) {
|
||||
el.classList.remove("loader");
|
||||
el.classList.remove("loader-sm");
|
||||
const dot = el.querySelector("span.dot");
|
||||
if (dot) { dot.remove(); }
|
||||
} else {
|
||||
el.classList.add("loader");
|
||||
if (small) { el.classList.add("loader-sm"); }
|
||||
const dot = document.createElement("span") as HTMLSpanElement;
|
||||
dot.classList.add("dot")
|
||||
el.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
+603
-270
@@ -1,297 +1,630 @@
|
||||
import { _get, _post, _delete } from "../modules/common.js";
|
||||
import { _get, _post, _delete, toClipboard, toggleLoader } from "../modules/common.js";
|
||||
|
||||
interface aWindow extends Window {
|
||||
setNotify(el: HTMLElement): void;
|
||||
deleteInvite(code: string): void;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
|
||||
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
|
||||
|
||||
function genUsedBy(usedBy: Array<Array<string>>): string {
|
||||
let uB = "";
|
||||
if (usedBy && usedBy.length != 0) {
|
||||
uB = `
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item py-1">Users created:</li>
|
||||
`;
|
||||
for (const i in usedBy) {
|
||||
uB += `
|
||||
<li class="list-group-item py-1 disabled">
|
||||
<div class="d-flex float-left">${usedBy[i][0]}</div>
|
||||
<div class="d-flex float-right">${usedBy[i][1]}</div>
|
||||
</li>
|
||||
`;
|
||||
export class DOMInvite implements Invite {
|
||||
updateNotify = (checkbox: HTMLInputElement) => {
|
||||
let state: { [code: string]: { [type: string]: boolean } } = {};
|
||||
let revertChanges: () => void;
|
||||
if (checkbox.classList.contains("inv-notify-expiry")) {
|
||||
revertChanges = () => { this.notifyExpiry = !this.notifyExpiry };
|
||||
state[this.code] = { "notify-expiry": this.notifyExpiry };
|
||||
} else {
|
||||
revertChanges = () => { this.notifyCreation = !this.notifyCreation };
|
||||
state[this.code] = { "notify-creation": this.notifyCreation };
|
||||
}
|
||||
uB += `</ul>`
|
||||
_post("/invites/notify", state, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) {
|
||||
revertChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
return uB;
|
||||
}
|
||||
|
||||
function addItem(invite: Invite): void {
|
||||
const links = document.getElementById('invites');
|
||||
const container = document.createElement('div') as HTMLDivElement;
|
||||
container.id = invite.code;
|
||||
const item = document.createElement('div') as HTMLDivElement;
|
||||
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
|
||||
let link = "";
|
||||
let innerHTML = `<a>None</a>`;
|
||||
if (invite.empty) {
|
||||
item.innerHTML = `
|
||||
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
|
||||
${innerHTML}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(item);
|
||||
links.appendChild(container);
|
||||
return;
|
||||
delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
|
||||
this.remove();
|
||||
const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code });
|
||||
document.dispatchEvent(inviteDeletedEvent);
|
||||
}
|
||||
})
|
||||
|
||||
private _code: string = "None";
|
||||
get code(): string { return this._code; }
|
||||
set code(code: string) {
|
||||
this._code = code;
|
||||
this._codeLink = window.location.href.split("#")[0] + "invite/" + code;
|
||||
const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement;
|
||||
linkEl.textContent = code.replace(/-/g, '-');
|
||||
linkEl.href = this._codeLink;
|
||||
}
|
||||
link = window.location.href.split('#')[0] + "invite/" + invite.code;
|
||||
innerHTML = `
|
||||
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
|
||||
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
|
||||
<i class="fa fa-clipboard icon-button" onclick="window.toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
|
||||
`;
|
||||
if (invite.email) {
|
||||
let email = invite.email;
|
||||
if (!invite.email.includes("Failed to send to")) {
|
||||
email = `Sent to ${email}`;
|
||||
private _codeLink: string;
|
||||
|
||||
private _expiresIn: string;
|
||||
get expiresIn(): string { return this._expiresIn }
|
||||
set expiresIn(expiry: string) {
|
||||
this._expiresIn = expiry;
|
||||
this._infoArea.querySelector("span.inv-expiry").textContent = expiry;
|
||||
}
|
||||
|
||||
private _remainingUses: string = "1";
|
||||
get remainingUses(): string { return this._remainingUses; }
|
||||
set remainingUses(remaining: string) {
|
||||
this._remainingUses = remaining;
|
||||
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
|
||||
}
|
||||
|
||||
private _email: string = "";
|
||||
get email(): string { return this._email };
|
||||
set email(address: string) {
|
||||
this._email = address;
|
||||
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
|
||||
const icon = container.querySelector("i");
|
||||
const chip = container.querySelector("span.inv-email-chip");
|
||||
const tooltip = container.querySelector("span.content") as HTMLSpanElement;
|
||||
if (address == "") {
|
||||
container.classList.remove("mr-1");
|
||||
icon.classList.remove("ri-mail-line");
|
||||
icon.classList.remove("ri-mail-close-line");
|
||||
chip.classList.remove("~neutral");
|
||||
chip.classList.remove("~critical");
|
||||
chip.classList.remove("chip");
|
||||
} else {
|
||||
container.classList.add("mr-1");
|
||||
chip.classList.add("chip");
|
||||
if (address.includes("Failed to send to")) {
|
||||
icon.classList.remove("ri-mail-line");
|
||||
icon.classList.add("ri-mail-close-line");
|
||||
chip.classList.remove("~neutral");
|
||||
chip.classList.add("~critical");
|
||||
} else {
|
||||
address = "Sent to " + address;
|
||||
icon.classList.remove("ri-mail-close-line");
|
||||
icon.classList.add("ri-mail-line");
|
||||
chip.classList.remove("~critical");
|
||||
chip.classList.add("~neutral");
|
||||
}
|
||||
}
|
||||
tooltip.textContent = address;
|
||||
}
|
||||
|
||||
private _usedBy: string[][];
|
||||
get usedBy(): string[][] { return this._usedBy; }
|
||||
set usedBy(uB: string[][]) {
|
||||
// ub[i][0]: username, ub[i][1]: date
|
||||
this._usedBy = uB;
|
||||
if (uB.length == 0) {
|
||||
this._right.classList.add("empty");
|
||||
this._userTable.innerHTML = `<p class="content">None yet!</p>`;
|
||||
return;
|
||||
}
|
||||
this._right.classList.remove("empty");
|
||||
let innerHTML = `
|
||||
<table class="table inv-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
for (let user of uB) {
|
||||
innerHTML += `
|
||||
<tr>
|
||||
<td>${user[0]}</td>
|
||||
<td>${user[1]}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
innerHTML += `
|
||||
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
this._userTable.innerHTML = innerHTML;
|
||||
}
|
||||
innerHTML += `
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
|
||||
<div style="display: inline-block;">
|
||||
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
|
||||
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="window.rotateButton(this)"></i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.innerHTML = innerHTML;
|
||||
container.appendChild(item);
|
||||
private _created: string;
|
||||
get created(): string { return this._created; }
|
||||
set created(created: string) {
|
||||
this._created = created;
|
||||
this._middle.querySelector("strong.inv-created").textContent = created;
|
||||
}
|
||||
|
||||
let profiles = `
|
||||
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
|
||||
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="window.setProfile(this)">
|
||||
<option value="NoProfile" selected>No Profile</option>
|
||||
`;
|
||||
for (const i in window.availableProfiles) {
|
||||
let selected = "";
|
||||
if (window.availableProfiles[i] == invite.profile) {
|
||||
selected = "selected";
|
||||
private _notifyExpiry: boolean = false;
|
||||
get notifyExpiry(): boolean { return this._notifyExpiry }
|
||||
set notifyExpiry(state: boolean) {
|
||||
this._notifyExpiry = state;
|
||||
(this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement).checked = state;
|
||||
}
|
||||
|
||||
private _notifyCreation: boolean = false;
|
||||
get notifyCreation(): boolean { return this._notifyCreation }
|
||||
set notifyCreation(state: boolean) {
|
||||
this._notifyCreation = state;
|
||||
(this._left.querySelector("input.inv-notify-creation") as HTMLInputElement).checked = state;
|
||||
}
|
||||
|
||||
private _profile: string;
|
||||
get profile(): string { return this._profile; }
|
||||
set profile(profile: string) { this.loadProfiles(profile); }
|
||||
loadProfiles = (selected?: string) => {
|
||||
const select = this._left.querySelector("select") as HTMLSelectElement;
|
||||
let noProfile = false;
|
||||
if (selected === "") {
|
||||
noProfile = true;
|
||||
} else {
|
||||
selected = selected || select.value;
|
||||
}
|
||||
profiles += `<option value="${window.availableProfiles[i]}" ${selected}>${window.availableProfiles[i]}</option>`;
|
||||
}
|
||||
profiles += `</select>`;
|
||||
|
||||
let dateCreated: string;
|
||||
if (invite.created) {
|
||||
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
|
||||
}
|
||||
|
||||
let middle: string;
|
||||
if (window.notifications_enabled) {
|
||||
middle = `
|
||||
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
|
||||
<ul class="list-group list-group-flush">
|
||||
Notify on:
|
||||
<li class="list-group-item py-1 form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
|
||||
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
|
||||
</li>
|
||||
<li class="list-group-item py-1 form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
|
||||
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let right: string = genUsedBy(invite.usedBy)
|
||||
|
||||
const dropdown = document.createElement('div') as HTMLDivElement;
|
||||
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
|
||||
dropdown.classList.add("collapse");
|
||||
dropdown.innerHTML = `
|
||||
<div class="container row align-items-start card-body">
|
||||
<div class="col">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="input-group py-1">
|
||||
${profiles}
|
||||
</li>
|
||||
${dateCreated}
|
||||
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
|
||||
</ul>
|
||||
</div>
|
||||
${middle}
|
||||
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
|
||||
${right}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(dropdown);
|
||||
links.appendChild(container);
|
||||
}
|
||||
|
||||
function parseInvite(invite: Object): Invite {
|
||||
let inv: Invite = { code: invite["code"], empty: false, };
|
||||
if (invite["email"]) {
|
||||
inv.email = invite["email"];
|
||||
}
|
||||
let time = ""
|
||||
const f = ["days", "hours", "minutes"];
|
||||
for (const i in f) {
|
||||
if (invite[f[i]] != 0) {
|
||||
time += `${invite[f[i]]}${f[i][0]} `;
|
||||
let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>No Profile</option>`;
|
||||
for (let profile of window.availableProfiles) {
|
||||
innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`;
|
||||
}
|
||||
}
|
||||
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
|
||||
if (invite["no-limit"]) {
|
||||
inv.remainingUses = "∞";
|
||||
} else if ("remaining-uses" in invite) {
|
||||
inv.remainingUses = invite["remaining-uses"];
|
||||
}
|
||||
if ("used-by" in invite) {
|
||||
inv.usedBy = invite["used-by"];
|
||||
}
|
||||
if ("created" in invite) {
|
||||
inv.created = invite["created"];
|
||||
}
|
||||
if ("notify-expiry" in invite) {
|
||||
inv.notifyExpiry = invite["notify-expiry"];
|
||||
}
|
||||
if ("notify-creation" in invite) {
|
||||
inv.notifyCreation = invite["notify-creation"];
|
||||
}
|
||||
if ("profile" in invite) {
|
||||
inv.profile = invite["profile"];
|
||||
}
|
||||
return inv;
|
||||
}
|
||||
|
||||
window.setNotify = (el: HTMLElement): void => {
|
||||
let send = {};
|
||||
let code: string;
|
||||
let notifyType: string;
|
||||
if (el.id.includes("Expiry")) {
|
||||
code = el.id.replace("_notifyExpiry", "");
|
||||
notifyType = "notify-expiry";
|
||||
} else if (el.id.includes("Creation")) {
|
||||
code = el.id.replace("_notifyCreation", "");
|
||||
notifyType = "notify-creation";
|
||||
}
|
||||
send[code] = {};
|
||||
send[code][notifyType] = (el as HTMLInputElement).checked;
|
||||
_post("/invites/notify", send, function (): void {
|
||||
if (this.readyState == 4 && this.status != 200) {
|
||||
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateInvite(invite: Invite): void {
|
||||
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
|
||||
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
|
||||
if (remainingUses) {
|
||||
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
|
||||
}
|
||||
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
|
||||
}
|
||||
|
||||
// delete invite from DOM
|
||||
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
|
||||
|
||||
// delete invite from jfa-go
|
||||
window.deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
generateInvites();
|
||||
}
|
||||
});
|
||||
|
||||
export function generateInvites(empty?: boolean): void {
|
||||
if (empty) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(emptyInvite());
|
||||
return;
|
||||
}
|
||||
_get("/invites", null, function (): void {
|
||||
if (this.readyState == 4) {
|
||||
let data = this.response;
|
||||
window.availableProfiles = data['profiles'];
|
||||
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
|
||||
let innerHTML = "";
|
||||
for (let i = 0; i < window.availableProfiles.length; i++) {
|
||||
const profile = window.availableProfiles[i];
|
||||
innerHTML += `
|
||||
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
|
||||
`;
|
||||
select.innerHTML = innerHTML;
|
||||
this._profile = selected;
|
||||
};
|
||||
updateProfile = () => {
|
||||
const select = this._left.querySelector("select") as HTMLSelectElement;
|
||||
const previous = this.profile;
|
||||
let profile = select.value;
|
||||
if (profile == "noProfile") { profile = ""; }
|
||||
_post("/invites/profile", { "invite": this.code, "profile": profile }, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (!(req.status == 200 || req.status == 204)) {
|
||||
select.value = previous || "noProfile";
|
||||
} else {
|
||||
this._profile = profile;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _container: HTMLDivElement;
|
||||
|
||||
private _header: HTMLDivElement;
|
||||
private _codeArea: HTMLDivElement;
|
||||
private _infoArea: HTMLDivElement;
|
||||
|
||||
private _details: HTMLDivElement;
|
||||
private _left: HTMLDivElement;
|
||||
private _middle: HTMLDivElement;
|
||||
private _right: HTMLDivElement;
|
||||
private _userTable: HTMLDivElement;
|
||||
|
||||
// whether the details card is expanded.
|
||||
get expanded(): boolean {
|
||||
return this._details.classList.contains("focused");
|
||||
}
|
||||
set expanded(state: boolean) {
|
||||
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
|
||||
if (state) {
|
||||
this._details.classList.remove("unfocused");
|
||||
this._details.classList.add("focused");
|
||||
toggle.previousElementSibling.classList.add("rotated");
|
||||
toggle.previousElementSibling.classList.remove("not-rotated");
|
||||
} else {
|
||||
this._details.classList.add("unfocused");
|
||||
this._details.classList.remove("focused");
|
||||
toggle.previousElementSibling.classList.remove("rotated");
|
||||
toggle.previousElementSibling.classList.add("not-rotated");
|
||||
}
|
||||
}
|
||||
|
||||
constructor(invite: Invite) {
|
||||
// first create the invite structure, then use our setter methods to fill in the data.
|
||||
this._container = document.createElement('div') as HTMLDivElement;
|
||||
this._container.classList.add("inv");
|
||||
|
||||
this._header = document.createElement('div') as HTMLDivElement;
|
||||
this._container.appendChild(this._header);
|
||||
this._header.classList.add("card", "~neutral", "!normal", "inv-header", "elem-pad", "no-pad", "flex-expand", "row", "mt-half", "overflow-y");
|
||||
|
||||
this._codeArea = document.createElement('div') as HTMLDivElement;
|
||||
this._header.appendChild(this._codeArea);
|
||||
this._codeArea.classList.add("inv-codearea");
|
||||
this._codeArea.innerHTML = `
|
||||
<a class="invite-link code monospace mr-1" href=""></a>
|
||||
<span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
|
||||
`;
|
||||
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
|
||||
copyButton.onclick = () => {
|
||||
toClipboard(this._codeLink);
|
||||
const icon = copyButton.children[0];
|
||||
icon.classList.remove("ri-file-copy-line");
|
||||
icon.classList.add("ri-check-line");
|
||||
copyButton.classList.remove("~info");
|
||||
copyButton.classList.add("~positive");
|
||||
setTimeout(() => {
|
||||
icon.classList.remove("ri-check-line");
|
||||
icon.classList.add("ri-file-copy-line");
|
||||
copyButton.classList.remove("~positive");
|
||||
copyButton.classList.add("~info");
|
||||
}, 800);
|
||||
};
|
||||
|
||||
this._infoArea = document.createElement('div') as HTMLDivElement;
|
||||
this._header.appendChild(this._infoArea);
|
||||
this._infoArea.classList.add("inv-infoarea");
|
||||
this._infoArea.innerHTML = `
|
||||
<div class="tooltip left">
|
||||
<span class="inv-email-chip"><i></i></span>
|
||||
<span class="content sm"></span>
|
||||
</div>
|
||||
<span class="inv-expiry mr-1"></span>
|
||||
<span class="button ~critical !normal inv-delete">Delete</span>
|
||||
<label>
|
||||
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
|
||||
<input class="inv-toggle-details unfocused" type="checkbox">
|
||||
</label>
|
||||
`;
|
||||
|
||||
(this._infoArea.querySelector(".inv-delete") as HTMLSpanElement).onclick = this.delete;
|
||||
|
||||
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
|
||||
toggle.onchange = () => { this.expanded = !this.expanded; };
|
||||
|
||||
this._details = document.createElement('div') as HTMLDivElement;
|
||||
this._container.appendChild(this._details);
|
||||
this._details.classList.add("card", "~neutral", "!normal", "mt-half", "no-pad", "inv-details");
|
||||
const detailsInner = document.createElement('div') as HTMLDivElement;
|
||||
this._details.appendChild(detailsInner);
|
||||
detailsInner.classList.add("inv-row", "flex-expand", "row", "elem-pad", "align-top");
|
||||
|
||||
this._left = document.createElement('div') as HTMLDivElement;
|
||||
detailsInner.appendChild(this._left);
|
||||
this._left.classList.add("inv-profilearea");
|
||||
let innerHTML = `
|
||||
<p class="supra mb-1 top">Profile</p>
|
||||
<div class="select ~neutral !normal inv-profileselect inline-block">
|
||||
<select>
|
||||
<option value="noProfile" selected>No Profile</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
if (window.notificationsEnabled) {
|
||||
innerHTML += `
|
||||
<option value="NoProfile" ${(window.availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
|
||||
<p class="label supra">Notify on:</p>
|
||||
<label class="switch block">
|
||||
<input class="inv-notify-expiry" type="checkbox">
|
||||
<span>On expiry</span>
|
||||
</label>
|
||||
<label class="switch block">
|
||||
<input class="inv-notify-creation" type="checkbox">
|
||||
<span>On user creation</span>
|
||||
</label>
|
||||
`;
|
||||
Profiles.innerHTML = innerHTML;
|
||||
if (data['invites'] == null || data['invites'].length == 0) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(emptyInvite());
|
||||
}
|
||||
this._left.innerHTML = innerHTML;
|
||||
(this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile;
|
||||
|
||||
if (window.notificationsEnabled) {
|
||||
const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement;
|
||||
notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); };
|
||||
|
||||
const notifyCreation = this._left.querySelector("input.inv-notify-creation") as HTMLInputElement;
|
||||
notifyCreation.onchange = () => { this._notifyCreation = notifyCreation.checked; this.updateNotify(notifyCreation); };
|
||||
}
|
||||
|
||||
this._middle = document.createElement('div') as HTMLDivElement;
|
||||
detailsInner.appendChild(this._middle);
|
||||
this._middle.classList.add("block");
|
||||
this._middle.innerHTML = `
|
||||
<p class="supra mb-1 top">Created <strong class="inv-created"></strong></p>
|
||||
<p class="supra mb-1">Remaining uses <strong class="inv-remaining"></strong></p>
|
||||
`;
|
||||
|
||||
this._right = document.createElement('div') as HTMLDivElement;
|
||||
detailsInner.appendChild(this._right);
|
||||
this._right.classList.add("card", "~neutral", "!low", "inv-created-users");
|
||||
this._right.innerHTML = `<strong class="supra table-header">Created users</strong>`;
|
||||
this._userTable = document.createElement('div') as HTMLDivElement;
|
||||
this._right.appendChild(this._userTable);
|
||||
|
||||
|
||||
this.expanded = false;
|
||||
this.update(invite);
|
||||
|
||||
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
||||
}
|
||||
|
||||
update = (invite: Invite) => {
|
||||
this.code = invite.code;
|
||||
this.created = invite.created;
|
||||
this.email = invite.email;
|
||||
this.expiresIn = invite.expiresIn;
|
||||
if (window.notificationsEnabled) {
|
||||
this.notifyCreation = invite.notifyCreation;
|
||||
this.notifyExpiry = invite.notifyExpiry;
|
||||
}
|
||||
this.profile = invite.profile;
|
||||
this.remainingUses = invite.remainingUses;
|
||||
this.usedBy = invite.usedBy;
|
||||
}
|
||||
|
||||
asElement = (): HTMLDivElement => { return this._container; }
|
||||
|
||||
remove = () => { this._container.remove(); }
|
||||
}
|
||||
|
||||
export class inviteList implements inviteList {
|
||||
private _list: HTMLDivElement;
|
||||
private _empty: boolean;
|
||||
// since invite reload sends profiles, this event it broadcast so the createInvite object can load them.
|
||||
private _profileLoadEvent = new CustomEvent("profileLoadEvent");
|
||||
|
||||
invites: { [code: string]: DOMInvite };
|
||||
|
||||
constructor() {
|
||||
this._list = document.getElementById('invites') as HTMLDivElement;
|
||||
this.empty = true;
|
||||
this.invites = {};
|
||||
document.addEventListener("newInviteEvent", () => { this.reload(); }, false);
|
||||
document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => {
|
||||
const code = event.detail;
|
||||
const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys
|
||||
delete this.invites[code];
|
||||
if (length == 0) {
|
||||
this.empty = true;
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
get empty(): boolean { return this._empty; }
|
||||
set empty(state: boolean) {
|
||||
this._empty = state;
|
||||
if (state) {
|
||||
this.invites = {};
|
||||
this._list.classList.add("empty");
|
||||
this._list.innerHTML = `
|
||||
<div class="inv inv-empty">
|
||||
<div class="card ~neutral !normal inv-header flex-expand mt-half">
|
||||
<div class="inv-codearea">
|
||||
<span class="code monospace">None</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
this._list.classList.remove("empty");
|
||||
if (this._list.querySelector(".inv-empty")) {
|
||||
this._list.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add = (invite: Invite) => {
|
||||
let domInv = new DOMInvite(invite);
|
||||
this.invites[invite.code] = domInv;
|
||||
if (this.empty) { this.empty = false; }
|
||||
this._list.appendChild(domInv.asElement());
|
||||
}
|
||||
|
||||
reload = () => _get("/invites", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
let data = req.response;
|
||||
if (req.status == 200) {
|
||||
window.availableProfiles = data["profiles"];
|
||||
document.dispatchEvent(this._profileLoadEvent);
|
||||
}
|
||||
if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) {
|
||||
this.empty = true;
|
||||
return;
|
||||
}
|
||||
let items = document.getElementById('invites').children;
|
||||
for (const i in data['invites']) {
|
||||
let match = false;
|
||||
const inv = parseInvite(data['invites'][i]);
|
||||
for (const x in items) {
|
||||
if (items[x].id == inv.code) {
|
||||
match = true;
|
||||
updateInvite(inv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
addItem(inv);
|
||||
// get a list of all current inv codes on dom
|
||||
// every time we find a match in resp, delete from list
|
||||
// at end delete all remaining in list from dom
|
||||
let invitesOnDOM: { [code: string]: boolean } = {};
|
||||
for (let code in this.invites) { invitesOnDOM[code] = true; }
|
||||
for (let inv of (data["invites"] as Array<any>)) {
|
||||
const invite = parseInvite(inv);
|
||||
if (invite.code in this.invites) {
|
||||
this.invites[invite.code].update(invite);
|
||||
delete invitesOnDOM[invite.code];
|
||||
} else {
|
||||
this.add(invite);
|
||||
}
|
||||
}
|
||||
// second pass to check for expired invites
|
||||
items = document.getElementById('invites').children;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let exists = false;
|
||||
for (const x in data['invites']) {
|
||||
if (items[i].id == data['invites'][x]['code']) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
hideInvite(items[i].id);
|
||||
}
|
||||
for (let code in invitesOnDOM) {
|
||||
this.invites[code].remove();
|
||||
delete this.invites[code];
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function parseInvite(invite: { [f: string]: string | number | string[][] | boolean }): Invite {
|
||||
let parsed: Invite = {};
|
||||
parsed.code = invite["code"] as string;
|
||||
parsed.email = invite["email"] as string || "";
|
||||
let time = "";
|
||||
const fields = ["days", "hours", "minutes"];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
if (invite[fields[i]] != 0) {
|
||||
time += `${invite[fields[i]]}${fields[i][0]} `;
|
||||
}
|
||||
}
|
||||
parsed.expiresIn = `Expires in ${time.slice(0, -1)}`;
|
||||
parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
|
||||
parsed.usedBy = invite["used-by"] as string[][] || [];
|
||||
parsed.created = invite["created"] as string || "Unknown";
|
||||
parsed.profile = invite["profile"] as string || "";
|
||||
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
|
||||
parsed.notifyCreation = invite["notify-creation"] as boolean || false;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export const addOptions = (length: number, el: HTMLSelectElement): void => {
|
||||
for (let v = 0; v <= length; v++) {
|
||||
const opt = document.createElement('option');
|
||||
opt.textContent = ""+v;
|
||||
opt.value = ""+v;
|
||||
el.appendChild(opt);
|
||||
}
|
||||
el.value = "0";
|
||||
};
|
||||
export class createInvite {
|
||||
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
|
||||
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
|
||||
private _uses = document.getElementById('create-uses') as HTMLInputElement;
|
||||
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
|
||||
private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement;
|
||||
private _createButton = document.getElementById("create-submit") as HTMLSpanElement;
|
||||
private _profile = document.getElementById("create-profile") as HTMLSelectElement;
|
||||
|
||||
export function checkDuration(): void {
|
||||
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
|
||||
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
|
||||
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
|
||||
submit.disabled = true;
|
||||
} else {
|
||||
submit.disabled = false;
|
||||
private _days = document.getElementById("create-days") as HTMLSelectElement;
|
||||
private _hours = document.getElementById("create-hours") as HTMLSelectElement;
|
||||
private _minutes = document.getElementById("create-minutes") as HTMLSelectElement;
|
||||
|
||||
// Broadcast when new invite created
|
||||
private _newInviteEvent = new CustomEvent("newInviteEvent");
|
||||
private _firstLoad = true;
|
||||
|
||||
private _count: Number = 30;
|
||||
private _populateNumbers = () => {
|
||||
const fieldIDs = ["create-days", "create-hours", "create-minutes"];
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
const field = document.getElementById(fieldIDs[i]);
|
||||
field.textContent = '';
|
||||
for (let n = 0; n <= this._count; n++) {
|
||||
const opt = document.createElement("option") as HTMLOptionElement;
|
||||
opt.textContent = ""+n;
|
||||
opt.value = ""+n;
|
||||
field.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get sendToEnabled(): boolean {
|
||||
return this._sendToEnabled.checked;
|
||||
}
|
||||
set sendToEnabled(state: boolean) {
|
||||
this._sendToEnabled.checked = state;
|
||||
this._sendTo.disabled = !state;
|
||||
if (state) {
|
||||
this._sendToEnabled.parentElement.classList.remove("~neutral");
|
||||
this._sendToEnabled.parentElement.classList.add("~urge");
|
||||
} else {
|
||||
this._sendToEnabled.parentElement.classList.remove("~urge");
|
||||
this._sendToEnabled.parentElement.classList.add("~neutral");
|
||||
}
|
||||
}
|
||||
|
||||
get infiniteUses(): boolean {
|
||||
return this._infUses.checked;
|
||||
}
|
||||
set infiniteUses(state: boolean) {
|
||||
this._infUses.checked = state;
|
||||
this._uses.disabled = state;
|
||||
if (state) {
|
||||
this._infUses.parentElement.classList.remove("~neutral");
|
||||
this._infUses.parentElement.classList.add("~urge");
|
||||
this._infUsesWarning.classList.remove("unfocused");
|
||||
} else {
|
||||
this._infUses.parentElement.classList.remove("~urge");
|
||||
this._infUses.parentElement.classList.add("~neutral");
|
||||
this._infUsesWarning.classList.add("unfocused");
|
||||
}
|
||||
}
|
||||
|
||||
get uses(): number { return this._uses.valueAsNumber; }
|
||||
set uses(n: number) { this._uses.valueAsNumber = n; }
|
||||
|
||||
private _checkDurationValidity = () => {
|
||||
if (this.days + this.hours + this.minutes == 0) {
|
||||
this._createButton.setAttribute("disabled", "");
|
||||
this._createButton.onclick = null;
|
||||
} else {
|
||||
this._createButton.removeAttribute("disabled");
|
||||
this._createButton.onclick = this.create;
|
||||
}
|
||||
}
|
||||
|
||||
get days(): number {
|
||||
return +this._days.value;
|
||||
}
|
||||
set days(n: number) {
|
||||
this._days.value = ""+n;
|
||||
this._checkDurationValidity();
|
||||
}
|
||||
get hours(): number {
|
||||
return +this._hours.value;
|
||||
}
|
||||
set hours(n: number) {
|
||||
this._hours.value = ""+n;
|
||||
this._checkDurationValidity();
|
||||
}
|
||||
get minutes(): number {
|
||||
return +this._minutes.value;
|
||||
}
|
||||
set minutes(n: number) {
|
||||
this._minutes.value = ""+n;
|
||||
this._checkDurationValidity();
|
||||
}
|
||||
|
||||
get sendTo(): string { return this._sendTo.value; }
|
||||
set sendTo(address: string) { this._sendTo.value = address; }
|
||||
|
||||
get profile(): string {
|
||||
const val = this._profile.value;
|
||||
if (val == "noProfile") {
|
||||
return "";
|
||||
}
|
||||
return val;
|
||||
}
|
||||
set profile(p: string) {
|
||||
if (p == "") { p = "noProfile"; }
|
||||
this._profile.value = p;
|
||||
}
|
||||
|
||||
loadProfiles = () => {
|
||||
let innerHTML = `<option value="noProfile">No Profile</option>`;
|
||||
for (let profile of window.availableProfiles) {
|
||||
innerHTML += `<option value="${profile}">${profile}</option>`;
|
||||
}
|
||||
let selected = this.profile;
|
||||
this._profile.innerHTML = innerHTML;
|
||||
if (this._firstLoad) {
|
||||
this.profile = window.availableProfiles[0] || "";
|
||||
this._firstLoad = false;
|
||||
} else {
|
||||
this.profile = selected;
|
||||
}
|
||||
}
|
||||
|
||||
create = () => {
|
||||
toggleLoader(this._createButton);
|
||||
let send = {
|
||||
"days": this.days,
|
||||
"hours": this.hours,
|
||||
"minutes": this.minutes,
|
||||
"multiple-uses": (this.uses > 1 || this.infiniteUses),
|
||||
"no-limit": this.infiniteUses,
|
||||
"remaining-uses": this.uses,
|
||||
"email": this.sendToEnabled ? this.sendTo : "",
|
||||
"profile": this.profile
|
||||
};
|
||||
_post("/invites", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
document.dispatchEvent(this._newInviteEvent);
|
||||
}
|
||||
toggleLoader(this._createButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._populateNumbers();
|
||||
this.days = 0;
|
||||
this.hours = 0;
|
||||
this.minutes = 30;
|
||||
this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; };
|
||||
this.infiniteUses = false;
|
||||
this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; };
|
||||
this.sendToEnabled = false;
|
||||
this._createButton.onclick = this.create;
|
||||
this.sendTo = "";
|
||||
this.uses = 1;
|
||||
|
||||
this._days.onchange = this._checkDurationValidity;
|
||||
this._hours.onchange = this._checkDurationValidity;
|
||||
this._minutes.onchange = this._checkDurationValidity;
|
||||
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
||||
|
||||
if (!window.emailEnabled) {
|
||||
document.getElementById("create-send-to-container").classList.add("unfocused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
declare var window: Window;
|
||||
|
||||
export class Modal implements Modal {
|
||||
modal: HTMLElement;
|
||||
closeButton: HTMLSpanElement;
|
||||
constructor(modal: HTMLElement, important: boolean = false) {
|
||||
this.modal = modal;
|
||||
const closeButton = this.modal.querySelector('span.modal-close')
|
||||
if (closeButton !== null) {
|
||||
this.closeButton = closeButton as HTMLSpanElement;
|
||||
this.closeButton.onclick = this.close;
|
||||
}
|
||||
if (!important) {
|
||||
window.addEventListener('click', (event: Event) => {
|
||||
if (event.target == this.modal) { this.close(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
close = (event?: Event) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.modal.classList.add('modal-hiding');
|
||||
const modal = this.modal;
|
||||
const listenerFunc = function () {
|
||||
modal.classList.remove('modal-shown');
|
||||
modal.classList.remove('modal-hiding');
|
||||
modal.removeEventListener(window.animationEvent, listenerFunc);
|
||||
};
|
||||
this.modal.addEventListener(window.animationEvent, listenerFunc, false);
|
||||
}
|
||||
show = () => {
|
||||
this.modal.classList.add('modal-shown');
|
||||
}
|
||||
toggle = () => {
|
||||
if (this.modal.classList.contains('modal-shown')) {
|
||||
this.close();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
+594
-144
@@ -1,164 +1,614 @@
|
||||
import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js";
|
||||
import { Focus, Unfocus } from "../modules/admin.js";
|
||||
import { _get, _post, toggleLoader } from "../modules/common.js";
|
||||
|
||||
interface Profile {
|
||||
Admin: boolean;
|
||||
LibraryAccess: string;
|
||||
FromUser: string;
|
||||
interface settingsBoolEvent extends Event {
|
||||
detail: boolean;
|
||||
}
|
||||
|
||||
export const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
const profileList = document.getElementById('profileList');
|
||||
profileList.textContent = '';
|
||||
window.availableProfiles = [this.response["default_profile"]];
|
||||
for (let name in this.response["profiles"]) {
|
||||
if (name != window.availableProfiles[0]) {
|
||||
window.availableProfiles.push(name);
|
||||
}
|
||||
const reqProfile = this.response["profiles"][name];
|
||||
if (!noTable && name != "default_profile") {
|
||||
const profile: Profile = {
|
||||
Admin: reqProfile["admin"],
|
||||
LibraryAccess: reqProfile["libraries"],
|
||||
FromUser: reqProfile["fromUser"]
|
||||
};
|
||||
profileList.innerHTML += `
|
||||
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
|
||||
<td nowrap="nowrap" class="align-middle"><input class="${window.bs5 ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == window.availableProfiles[0]) ? "checked" : ""}></td>
|
||||
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
|
||||
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
|
||||
`;
|
||||
}
|
||||
interface Meta {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
requires_restart: boolean;
|
||||
type: string;
|
||||
value: string | boolean | number;
|
||||
depends_true?: Setting;
|
||||
depends_false?: Setting;
|
||||
|
||||
asElement: () => HTMLElement;
|
||||
update: (s: Setting) => void;
|
||||
}
|
||||
|
||||
class DOMInput {
|
||||
protected _input: HTMLInputElement;
|
||||
private _container: HTMLDivElement;
|
||||
private _tooltip: HTMLDivElement;
|
||||
private _required: HTMLSpanElement;
|
||||
private _restart: HTMLSpanElement;
|
||||
|
||||
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
|
||||
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
|
||||
|
||||
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
|
||||
set description(d: string) {
|
||||
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
|
||||
content.textContent = d;
|
||||
if (d == "") {
|
||||
this._tooltip.classList.add("unfocused");
|
||||
} else {
|
||||
this._tooltip.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
settingsList.textContent = '';
|
||||
window.config = this.response;
|
||||
for (const i in window.config["order"]) {
|
||||
const section: string = window.config["order"][i]
|
||||
const sectionCollapse = document.createElement('div') as HTMLDivElement;
|
||||
Unfocus(sectionCollapse);
|
||||
sectionCollapse.id = section;
|
||||
get required(): boolean { return this._required.classList.contains("badge"); }
|
||||
set required(state: boolean) {
|
||||
if (state) {
|
||||
this._required.classList.add("badge", "~critical");
|
||||
this._required.textContent = "*";
|
||||
} else {
|
||||
this._required.classList.remove("badge", "~critical");
|
||||
this._required.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
|
||||
set requires_restart(state: boolean) {
|
||||
if (state) {
|
||||
this._restart.classList.add("badge", "~info");
|
||||
this._restart.textContent = "R";
|
||||
} else {
|
||||
this._restart.classList.remove("badge", "~info");
|
||||
this._restart.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
const title: string = window.config[section]["meta"]["name"];
|
||||
const description: string = window.config[section]["meta"]["description"];
|
||||
const entryListID: string = `${section}_entryList`;
|
||||
// const footerID: string = `${section}_footer`;
|
||||
|
||||
sectionCollapse.innerHTML = `
|
||||
<div class="card card-body">
|
||||
<small class="text-muted">${description}</small>
|
||||
<div class="${entryListID}">
|
||||
</div>
|
||||
constructor(inputType: string, setting: Setting, section: string, name: string) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.classList.add("setting");
|
||||
this._container.innerHTML = `
|
||||
<label class="label">
|
||||
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||
<div class="setting-tooltip tooltip right unfocused">
|
||||
<i class="icon ri-information-line"></i>
|
||||
<span class="content sm"></span>
|
||||
</div>
|
||||
`;
|
||||
<input type="${inputType}" class="input ~neutral !normal mt-half">
|
||||
</label>
|
||||
`;
|
||||
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
|
||||
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
|
||||
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
||||
this._input = this._container.querySelector("input[type=" + inputType + "]") as HTMLInputElement;
|
||||
if (setting.depends_false || setting.depends_true) {
|
||||
let dependant = setting.depends_true || setting.depends_false;
|
||||
let state = true;
|
||||
if (setting.depends_false) { state = false; }
|
||||
document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => {
|
||||
this._input.disabled = (event.detail !== state);
|
||||
});
|
||||
}
|
||||
const onValueChange = () => {
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
||||
document.dispatchEvent(event);
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
this._input.onchange = onValueChange;
|
||||
this.update(setting);
|
||||
}
|
||||
|
||||
for (const x in config[section]["order"]) {
|
||||
const entry: string = config[section]["order"][x];
|
||||
if (entry == "meta") {
|
||||
continue;
|
||||
get value(): any { return this._input.value; }
|
||||
set value(v: any) { this._input.value = v; }
|
||||
|
||||
update = (s: Setting) => {
|
||||
this.name = s.name;
|
||||
this.description = s.description;
|
||||
this.required = s.required;
|
||||
this.requires_restart = s.requires_restart;
|
||||
this.value = s.value;
|
||||
}
|
||||
|
||||
asElement = (): HTMLDivElement => { return this._container; }
|
||||
}
|
||||
|
||||
interface SText extends Setting {
|
||||
value: string;
|
||||
}
|
||||
class DOMText extends DOMInput implements SText {
|
||||
constructor(setting: Setting, section: string, name: string) { super("text", setting, section, name); }
|
||||
type: string = "text";
|
||||
get value(): string { return this._input.value }
|
||||
set value(v: string) { this._input.value = v; }
|
||||
}
|
||||
|
||||
interface SPassword extends Setting {
|
||||
value: string;
|
||||
}
|
||||
class DOMPassword extends DOMInput implements SPassword {
|
||||
constructor(setting: Setting, section: string, name: string) { super("password", setting, section, name); }
|
||||
type: string = "password";
|
||||
get value(): string { return this._input.value }
|
||||
set value(v: string) { this._input.value = v; }
|
||||
}
|
||||
|
||||
interface SEmail extends Setting {
|
||||
value: string;
|
||||
}
|
||||
class DOMEmail extends DOMInput implements SEmail {
|
||||
constructor(setting: Setting, section: string, name: string) { super("email", setting, section, name); }
|
||||
type: string = "email";
|
||||
get value(): string { return this._input.value }
|
||||
set value(v: string) { this._input.value = v; }
|
||||
}
|
||||
|
||||
interface SNumber extends Setting {
|
||||
value: number;
|
||||
}
|
||||
class DOMNumber extends DOMInput implements SNumber {
|
||||
constructor(setting: Setting, section: string, name: string) { super("number", setting, section, name); }
|
||||
type: string = "number";
|
||||
get value(): number { return +this._input.value; }
|
||||
set value(v: number) { this._input.value = ""+v; }
|
||||
}
|
||||
|
||||
interface SBool extends Setting {
|
||||
value: boolean;
|
||||
}
|
||||
class DOMBool implements SBool {
|
||||
protected _input: HTMLInputElement;
|
||||
private _container: HTMLDivElement;
|
||||
private _tooltip: HTMLDivElement;
|
||||
private _required: HTMLSpanElement;
|
||||
private _restart: HTMLSpanElement;
|
||||
type: string = "bool";
|
||||
|
||||
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
|
||||
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
|
||||
|
||||
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
|
||||
set description(d: string) {
|
||||
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
|
||||
content.textContent = d;
|
||||
if (d == "") {
|
||||
this._tooltip.classList.add("unfocused");
|
||||
} else {
|
||||
this._tooltip.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
|
||||
get required(): boolean { return this._required.classList.contains("badge"); }
|
||||
set required(state: boolean) {
|
||||
if (state) {
|
||||
this._required.classList.add("badge", "~critical");
|
||||
this._required.textContent = "*";
|
||||
} else {
|
||||
this._required.classList.remove("badge", "~critical");
|
||||
this._required.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
|
||||
set requires_restart(state: boolean) {
|
||||
if (state) {
|
||||
this._restart.classList.add("badge", "~info");
|
||||
this._restart.textContent = "R";
|
||||
} else {
|
||||
this._restart.classList.remove("badge", "~info");
|
||||
this._restart.textContent = "";
|
||||
}
|
||||
}
|
||||
get value(): boolean { return this._input.checked; }
|
||||
set value(state: boolean) { this._input.checked = state; }
|
||||
constructor(setting: SBool, section: string, name: string) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.classList.add("setting");
|
||||
this._container.innerHTML = `
|
||||
<label class="switch">
|
||||
<input type="checkbox">
|
||||
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||
<div class="setting-tooltip tooltip right unfocused">
|
||||
<i class="icon ri-information-line"></i>
|
||||
<span class="content sm"></span>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
|
||||
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
|
||||
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
||||
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
const onValueChange = () => {
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
this._input.onchange = () => {
|
||||
onValueChange();
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
document.addEventListener(`settings-loaded`, onValueChange);
|
||||
|
||||
if (setting.depends_false || setting.depends_true) {
|
||||
let dependant = setting.depends_true || setting.depends_false;
|
||||
let state = true;
|
||||
if (setting.depends_false) { state = false; }
|
||||
document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => {
|
||||
this._input.disabled = (event.detail !== state);
|
||||
});
|
||||
}
|
||||
this.update(setting);
|
||||
}
|
||||
update = (s: SBool) => {
|
||||
this.name = s.name;
|
||||
this.description = s.description;
|
||||
this.required = s.required;
|
||||
this.requires_restart = s.requires_restart;
|
||||
this.value = s.value;
|
||||
}
|
||||
|
||||
asElement = (): HTMLDivElement => { return this._container; }
|
||||
}
|
||||
|
||||
interface SSelect extends Setting {
|
||||
options: string[];
|
||||
value: string;
|
||||
}
|
||||
class DOMSelect implements SSelect {
|
||||
protected _select: HTMLSelectElement;
|
||||
private _container: HTMLDivElement;
|
||||
private _tooltip: HTMLDivElement;
|
||||
private _required: HTMLSpanElement;
|
||||
private _restart: HTMLSpanElement;
|
||||
private _options: string[];
|
||||
type: string = "bool";
|
||||
|
||||
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
|
||||
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
|
||||
|
||||
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
|
||||
set description(d: string) {
|
||||
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
|
||||
content.textContent = d;
|
||||
if (d == "") {
|
||||
this._tooltip.classList.add("unfocused");
|
||||
} else {
|
||||
this._tooltip.classList.remove("unfocused");
|
||||
}
|
||||
}
|
||||
|
||||
get required(): boolean { return this._required.classList.contains("badge"); }
|
||||
set required(state: boolean) {
|
||||
if (state) {
|
||||
this._required.classList.add("badge", "~critical");
|
||||
this._required.textContent = "*";
|
||||
} else {
|
||||
this._required.classList.remove("badge", "~critical");
|
||||
this._required.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
|
||||
set requires_restart(state: boolean) {
|
||||
if (state) {
|
||||
this._restart.classList.add("badge", "~info");
|
||||
this._restart.textContent = "R";
|
||||
} else {
|
||||
this._restart.classList.remove("badge", "~info");
|
||||
this._restart.textContent = "";
|
||||
}
|
||||
}
|
||||
get value(): string { return this._select.value; }
|
||||
set value(v: string) { this._select.value = v; }
|
||||
|
||||
get options(): string[] { return this._options; }
|
||||
set options(opt: string[]) {
|
||||
this._options = opt;
|
||||
let innerHTML = "";
|
||||
for (let option of this._options) {
|
||||
innerHTML += `<option value="${option}">${option}</option>`;
|
||||
}
|
||||
this._select.innerHTML = innerHTML;
|
||||
}
|
||||
|
||||
constructor(setting: SSelect, section: string, name: string) {
|
||||
this._options = [];
|
||||
this._container = document.createElement("div");
|
||||
this._container.classList.add("setting");
|
||||
this._container.innerHTML = `
|
||||
<label class="label">
|
||||
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
|
||||
<div class="setting-tooltip tooltip right unfocused">
|
||||
<i class="icon ri-information-line"></i>
|
||||
<span class="content sm"></span>
|
||||
</div>
|
||||
<div class="select ~neutral !normal mt-half">
|
||||
<select class="settings-select"></select>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
|
||||
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
|
||||
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
|
||||
this._select = this._container.querySelector("select.settings-select") as HTMLSelectElement;
|
||||
if (setting.depends_false || setting.depends_true) {
|
||||
let dependant = setting.depends_true || setting.depends_false;
|
||||
let state = true;
|
||||
if (setting.depends_false) { state = false; }
|
||||
document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => {
|
||||
this._input.disabled = (event.detail !== state);
|
||||
});
|
||||
}
|
||||
const onValueChange = () => {
|
||||
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
|
||||
document.dispatchEvent(event);
|
||||
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
|
||||
};
|
||||
this._select.onchange = onValueChange;
|
||||
this.update(setting);
|
||||
}
|
||||
update = (s: SSelect) => {
|
||||
this.name = s.name;
|
||||
this.description = s.description;
|
||||
this.required = s.required;
|
||||
this.requires_restart = s.requires_restart;
|
||||
this.options = s.options;
|
||||
this.value = s.value;
|
||||
}
|
||||
|
||||
asElement = (): HTMLDivElement => { return this._container; }
|
||||
}
|
||||
|
||||
interface Section {
|
||||
meta: Meta;
|
||||
order: string[];
|
||||
settings: { [settingName: string]: Setting };
|
||||
}
|
||||
|
||||
class sectionPanel {
|
||||
private _section: HTMLDivElement;
|
||||
private _settings: { [name: string]: Setting };
|
||||
private _sectionName: string;
|
||||
values: { [field: string]: string } = {};
|
||||
|
||||
constructor(s: Section, sectionName: string) {
|
||||
this._sectionName = sectionName;
|
||||
this._settings = {};
|
||||
this._section = document.createElement("div") as HTMLDivElement;
|
||||
this._section.classList.add("settings-section", "unfocused");
|
||||
this._section.innerHTML = `
|
||||
<span class="heading">${s.meta.name}</span>
|
||||
<p class="support lg">${s.meta.description}</p>
|
||||
`;
|
||||
this.update(s);
|
||||
}
|
||||
update = (s: Section) => {
|
||||
for (let name of s.order) {
|
||||
let setting: Setting = s.settings[name];
|
||||
if (name in this._settings) {
|
||||
this._settings[name].update(setting);
|
||||
} else {
|
||||
switch (setting.type) {
|
||||
case "text":
|
||||
setting = new DOMText(setting, this._sectionName, name);
|
||||
break;
|
||||
case "password":
|
||||
setting = new DOMPassword(setting, this._sectionName, name);
|
||||
break;
|
||||
case "email":
|
||||
setting = new DOMEmail(setting, this._sectionName, name);
|
||||
break;
|
||||
case "number":
|
||||
setting = new DOMNumber(setting, this._sectionName, name);
|
||||
break;
|
||||
case "bool":
|
||||
setting = new DOMBool(setting as SBool, this._sectionName, name);
|
||||
break;
|
||||
case "select":
|
||||
setting = new DOMSelect(setting as SSelect, this._sectionName, name);
|
||||
break;
|
||||
}
|
||||
let entryName: string = window.config[section][entry]["name"];
|
||||
let required = false;
|
||||
if (window.config[section][entry]["required"]) {
|
||||
entryName += ` <sup class="text-danger">*</sup>`;
|
||||
required = true;
|
||||
}
|
||||
if (window.config[section][entry]["requires_restart"]) {
|
||||
entryName += ` <sup class="text-danger">R</sup>`;
|
||||
}
|
||||
if ("description" in window.config[section][entry]) {
|
||||
entryName +=`
|
||||
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${window.config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
|
||||
`;
|
||||
}
|
||||
const entryValue: boolean | string = window.config[section][entry]["value"];
|
||||
const entryType: string = window.config[section][entry]["type"];
|
||||
const entryGroup = document.createElement('div');
|
||||
if (entryType == "bool") {
|
||||
entryGroup.classList.add("form-check");
|
||||
entryGroup.innerHTML = `
|
||||
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
|
||||
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
|
||||
`;
|
||||
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
|
||||
const me = this as HTMLInputElement;
|
||||
for (const y in window.config["order"]) {
|
||||
const sect: string = window.config["order"][y];
|
||||
for (const z in window.config[sect]["order"]) {
|
||||
const ent: string = window.config[sect]["order"][z];
|
||||
if (`${sect}_${window.config[sect][ent]['depends_true']}` == me.id) {
|
||||
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
|
||||
} else if (`${sect}_${window.config[sect][ent]['depends_false']}` == me.id) {
|
||||
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
|
||||
entryGroup.classList.add("form-group");
|
||||
entryGroup.innerHTML = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
|
||||
`;
|
||||
} else if (entryType == 'select') {
|
||||
entryGroup.classList.add("form-group");
|
||||
const entryOptions: Array<string> = window.config[section][entry]["options"];
|
||||
let innerGroup = `
|
||||
<label for="${section}_${entry}">${entryName}</label>
|
||||
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
|
||||
`;
|
||||
for (const z in entryOptions) {
|
||||
const entryOption = entryOptions[z];
|
||||
let selected: boolean = (entryOption == entryValue);
|
||||
innerGroup += `
|
||||
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
|
||||
`;
|
||||
}
|
||||
innerGroup += `</select>`;
|
||||
entryGroup.innerHTML = innerGroup;
|
||||
}
|
||||
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
|
||||
this.values[name] = ""+setting.value;
|
||||
document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => {
|
||||
const oldValue = this.values[name];
|
||||
this.values[name] = ""+event.detail;
|
||||
document.dispatchEvent(new CustomEvent("settings-section-changed"));
|
||||
});
|
||||
this._section.appendChild(setting.asElement());
|
||||
this._settings[name] = setting;
|
||||
}
|
||||
|
||||
settingsList.innerHTML += `
|
||||
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
|
||||
`;
|
||||
settingsContent.appendChild(sectionCollapse);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
get visible(): boolean { return !this._section.classList.contains("unfocused"); }
|
||||
set visible(s: boolean) {
|
||||
if (s) {
|
||||
this._section.classList.remove("unfocused");
|
||||
} else {
|
||||
this._section.classList.add("unfocused");
|
||||
}
|
||||
}
|
||||
|
||||
export function showSetting(id: string, runBefore?: () => void): void {
|
||||
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
const el = els[i];
|
||||
if (el.id != `${id}_button`) {
|
||||
rmAttr(el, "active");
|
||||
}
|
||||
const sectEl = document.getElementById(el.id.replace("_button", ""));
|
||||
if (sectEl.id != id) {
|
||||
Unfocus(sectEl);
|
||||
asElement = (): HTMLDivElement => { return this._section; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface Settings {
|
||||
order: string[];
|
||||
sections: { [sectionName: string]: Section };
|
||||
}
|
||||
|
||||
export class settingsList {
|
||||
private _saveButton = document.getElementById("settings-save") as HTMLSpanElement;
|
||||
private _saveNoRestart = document.getElementById("settings-apply-no-restart") as HTMLSpanElement;
|
||||
private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement;
|
||||
|
||||
private _panel = document.getElementById("settings-panel") as HTMLDivElement;
|
||||
private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
|
||||
private _sections: { [name: string]: sectionPanel }
|
||||
private _buttons: { [name: string]: HTMLSpanElement }
|
||||
private _needsRestart: boolean = false;
|
||||
|
||||
addSection = (name: string, s: Section) => {
|
||||
const section = new sectionPanel(s, name);
|
||||
this._sections[name] = section;
|
||||
this._panel.appendChild(this._sections[name].asElement());
|
||||
const button = document.createElement("span") as HTMLSpanElement;
|
||||
button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
|
||||
button.textContent = s.meta.name;
|
||||
button.onclick = () => { this._showPanel(name); };
|
||||
this._buttons[name] = button;
|
||||
this._sidebar.appendChild(this._buttons[name]);
|
||||
}
|
||||
|
||||
private _showPanel = (name: string) => {
|
||||
for (let n in this._sections) {
|
||||
if (n == name) {
|
||||
this._sections[name].visible = true;
|
||||
this._buttons[name].classList.add("selected");
|
||||
} else {
|
||||
this._sections[n].visible = false;
|
||||
this._buttons[n].classList.remove("selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
addAttr(document.getElementById(`${id}_button`), "active");
|
||||
const section = document.getElementById(id);
|
||||
if (runBefore) {
|
||||
runBefore();
|
||||
|
||||
private _save = () => {
|
||||
let config = {};
|
||||
for (let name in this._sections) {
|
||||
config[name] = this._sections[name].values;
|
||||
}
|
||||
if (this._needsRestart) {
|
||||
this._saveRestart.onclick = () => {
|
||||
config["restart-program"] = true;
|
||||
this._send(config, () => {
|
||||
window.modals.settingsRestart.close();
|
||||
window.modals.settingsRefresh.show();
|
||||
});
|
||||
};
|
||||
this._saveNoRestart.onclick = () => {
|
||||
config["restart-program"] = false;
|
||||
this._send(config, window.modals.settingsRestart.close);
|
||||
}
|
||||
window.modals.settingsRestart.show();
|
||||
} else {
|
||||
this._send(config);
|
||||
}
|
||||
// console.log(config);
|
||||
}
|
||||
Focus(section);
|
||||
if (screen.width <= 1100) {
|
||||
// ugly
|
||||
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
|
||||
|
||||
private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customPositive("settingsSaved", "Success:", "settings were saved.");
|
||||
} else {
|
||||
window.notifications.customError("settingsSaved", "Couldn't save settings.");
|
||||
}
|
||||
this.reload();
|
||||
if (run) { run(); }
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this._sections = {};
|
||||
this._buttons = {};
|
||||
document.addEventListener("settings-section-changed", () => this._saveButton.classList.remove("unfocused"));
|
||||
this._saveButton.onclick = this._save;
|
||||
document.addEventListener("settings-requires-restart", () => { this._needsRestart = true; });
|
||||
|
||||
if (window.ombiEnabled) {
|
||||
let ombi = new ombiDefaults();
|
||||
this._sidebar.appendChild(ombi.button());
|
||||
}
|
||||
}
|
||||
|
||||
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("settingsLoadError", "Failed to load settings.");
|
||||
return;
|
||||
}
|
||||
let settings = req.response as Settings;
|
||||
for (let name of settings.order) {
|
||||
if (name in this._sections) {
|
||||
this._sections[name].update(settings.sections[name]);
|
||||
} else {
|
||||
this.addSection(name, settings.sections[name]);
|
||||
}
|
||||
}
|
||||
this._showPanel(settings.order[0]);
|
||||
this._needsRestart = false;
|
||||
document.dispatchEvent(new CustomEvent("settings-loaded"));
|
||||
this._saveButton.classList.add("unfocused");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface ombiUser {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
class ombiDefaults {
|
||||
private _form: HTMLFormElement;
|
||||
private _button: HTMLSpanElement;
|
||||
private _select: HTMLSelectElement;
|
||||
private _users: { [id: string]: string } = {};
|
||||
constructor() {
|
||||
this._button = document.createElement("span") as HTMLSpanElement;
|
||||
this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
|
||||
this._button.innerHTML = `<span class="flex">Ombi user defaults <i class="ri-link-unlink-m ml-half"></i></span>`;
|
||||
this._button.onclick = this.load;
|
||||
this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement;
|
||||
this._form.onsubmit = this.send;
|
||||
this._select = this._form.querySelector("select") as HTMLSelectElement;
|
||||
}
|
||||
button = (): HTMLSpanElement => { return this._button; }
|
||||
send = () => {
|
||||
const button = this._form.querySelector("span.submit") as HTMLSpanElement;
|
||||
toggleLoader(button);
|
||||
let resp = {} as ombiUser;
|
||||
resp.id = this._select.value;
|
||||
resp.name = this._users[resp.id];
|
||||
_post("/ombi/defaults", resp, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults.");
|
||||
} else {
|
||||
window.notifications.customError("ombiDefaults", "Failed to store ombi defaults.");
|
||||
}
|
||||
window.modals.ombiDefaults.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
load = () => {
|
||||
toggleLoader(this._button);
|
||||
_get("/ombi/users", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 && "users" in req.response) {
|
||||
const users = req.response["users"] as ombiUser[];
|
||||
let innerHTML = "";
|
||||
for (let user of users) {
|
||||
this._users[user.id] = user.name;
|
||||
innerHTML += `<option value="${user.id}">${user.name}</option>`;
|
||||
}
|
||||
this._select.innerHTML = innerHTML;
|
||||
toggleLoader(this._button);
|
||||
window.modals.ombiDefaults.show();
|
||||
} else {
|
||||
toggleLoader(this._button);
|
||||
window.notifications.customError("ombiLoadError", "Failed to load ombi users.")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export class Tabs implements Tabs {
|
||||
private _current: string = "";
|
||||
tabs: Array<Tab>;
|
||||
|
||||
constructor() {
|
||||
this.tabs = [];
|
||||
}
|
||||
|
||||
addTab = (tabID: string, preFunc = () => void {}, postFunc = () => void {}) => {
|
||||
let tab = {} as Tab;
|
||||
tab.tabID = tabID;
|
||||
tab.tabEl = document.getElementById("tab-" + tabID) as HTMLDivElement;
|
||||
tab.buttonEl = document.getElementById("button-tab-" + tabID) as HTMLSpanElement;
|
||||
tab.buttonEl.onclick = () => { this.switch(tabID); };
|
||||
tab.preFunc = preFunc;
|
||||
tab.postFunc = postFunc;
|
||||
this.tabs.push(tab);
|
||||
}
|
||||
|
||||
get current(): string { return this._current; }
|
||||
set current(tabID: string) { this.switch(tabID); }
|
||||
|
||||
switch = (tabID: string, noRun: boolean = false) => {
|
||||
this._current = tabID;
|
||||
for (let t of this.tabs) {
|
||||
if (t.tabID == tabID) {
|
||||
t.buttonEl.classList.add("active", "~urge");
|
||||
if (t.preFunc && !noRun) { t.preFunc(); }
|
||||
t.tabEl.classList.remove("unfocused");
|
||||
if (t.postFunc && !noRun) { t.postFunc(); }
|
||||
document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID }));
|
||||
} else {
|
||||
t.buttonEl.classList.remove("active");
|
||||
t.buttonEl.classList.remove("~urge");
|
||||
t.tabEl.classList.add("unfocused");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export function toggleTheme() {
|
||||
document.documentElement.classList.toggle('dark-theme');
|
||||
document.documentElement.classList.toggle('light-theme');
|
||||
localStorage.setItem('theme', document.documentElement.classList.contains('dark-theme') ? "dark" : "light");
|
||||
}
|
||||
|
||||
export function loadTheme() {
|
||||
const theme = localStorage.getItem("theme");
|
||||
if (theme == "dark") {
|
||||
document.documentElement.classList.add('dark-theme');
|
||||
document.documentElement.classList.remove('light-theme');
|
||||
} else if (theme == "light") {
|
||||
document.documentElement.classList.add('light-theme');
|
||||
document.documentElement.classList.remove('dark-theme');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user