Rework typescript to use modules
web UI now uses modules, and relies less on bodge to make things work. Also fixes an issue where invites where "failed to send to xx" appeared in invite form.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { _get, _post, _delete } from "../modules/common.js";
|
||||
import { Focus, Unfocus } from "../modules/admin.js";
|
||||
|
||||
interface aWindow extends Window {
|
||||
checkCheckboxes: () => void;
|
||||
}
|
||||
|
||||
declare var window: aWindow;
|
||||
|
||||
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';
|
||||
} else {
|
||||
deleteButton.textContent = 'Delete Users';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.checkCheckboxes = checkCheckboxes;
|
||||
|
||||
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 isAdmin = "No";
|
||||
if (admin) {
|
||||
isAdmin = "Yes";
|
||||
}
|
||||
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}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
|
||||
<td nowrap="nowrap" class="align-middle">${isAdmin}</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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { rmAttr, addAttr, _post, _get, _delete } from "../modules/common.js";
|
||||
|
||||
export const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
|
||||
export const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
|
||||
|
||||
export function createEl(html: string): HTMLElement {
|
||||
let div = document.createElement('div') as HTMLDivElement;
|
||||
div.innerHTML = html;
|
||||
return div.firstElementChild as HTMLElement;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
declare var window: Window;
|
||||
|
||||
export function serializeForm(id: string): Object {
|
||||
const form = document.getElementById(id) as HTMLFormElement;
|
||||
let formData = {};
|
||||
for (let i = 0; i < form.elements.length; i++) {
|
||||
const el = form.elements[i];
|
||||
if ((el as HTMLInputElement).type == "submit") {
|
||||
continue;
|
||||
}
|
||||
let name = (el as HTMLInputElement).name;
|
||||
if (!name) {
|
||||
name = el.id;
|
||||
}
|
||||
switch ((el as HTMLInputElement).type) {
|
||||
case "checkbox":
|
||||
formData[name] = (el as HTMLInputElement).checked;
|
||||
break;
|
||||
case "text":
|
||||
case "password":
|
||||
case "email":
|
||||
case "number":
|
||||
formData[name] = (el as HTMLInputElement).value;
|
||||
break;
|
||||
case "select-one":
|
||||
case "select":
|
||||
let val: string = (el as HTMLSelectElement).value.toString();
|
||||
if (!isNaN(val as any)) {
|
||||
formData[name] = +val;
|
||||
} else {
|
||||
formData[name] = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
export const rmAttr = (el: HTMLElement, attr: string): void => {
|
||||
if (el.classList.contains(attr)) {
|
||||
el.classList.remove(attr);
|
||||
}
|
||||
};
|
||||
|
||||
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
|
||||
|
||||
export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("GET", url, true);
|
||||
req.responseType = 'json';
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("POST", url, true);
|
||||
if (response) {
|
||||
req.responseType = 'json';
|
||||
}
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.send(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("DELETE", url, true);
|
||||
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
|
||||
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
req.onreadystatechange = onreadystatechange;
|
||||
req.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
import { _get, _post, _delete } 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>
|
||||
`;
|
||||
}
|
||||
uB += `</ul>`
|
||||
}
|
||||
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;
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
innerHTML += `
|
||||
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
|
||||
`;
|
||||
}
|
||||
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);
|
||||
|
||||
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";
|
||||
}
|
||||
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]} `;
|
||||
}
|
||||
}
|
||||
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>
|
||||
`;
|
||||
}
|
||||
innerHTML += `
|
||||
<option value="NoProfile" ${(window.availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
|
||||
`;
|
||||
Profiles.innerHTML = innerHTML;
|
||||
if (data['invites'] == null || data['invites'].length == 0) {
|
||||
document.getElementById('invites').textContent = '';
|
||||
addItem(emptyInvite());
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js";
|
||||
import { Focus, Unfocus } from "../modules/admin.js";
|
||||
|
||||
interface Profile {
|
||||
Admin: boolean;
|
||||
LibraryAccess: string;
|
||||
FromUser: string;
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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>
|
||||
</div>
|
||||
`;
|
||||
|
||||
for (const x in config[section]["order"]) {
|
||||
const entry: string = config[section]["order"][x];
|
||||
if (entry == "meta") {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
addAttr(document.getElementById(`${id}_button`), "active");
|
||||
const section = document.getElementById(id);
|
||||
if (runBefore) {
|
||||
runBefore();
|
||||
}
|
||||
Focus(section);
|
||||
if (screen.width <= 1100) {
|
||||
// ugly
|
||||
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user