diff --git a/.gitignore b/.gitignore index a549da2..d65961d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ build/ version.go notes docs/* +config-payload.json !docs/go.mod diff --git a/css/base.css b/css/base.css index 368f486..f76e126 100644 --- a/css/base.css +++ b/css/base.css @@ -196,6 +196,11 @@ sup.\~critical, .text-critical { align-items: center; } +.inv-empty .inv-codearea { + justify-content: start; +} + + .invite-link { text-overflow: ellipsis; overflow: hidden; diff --git a/html/admin.html b/html/admin.html index 49962ba..5c397b6 100644 --- a/html/admin.html +++ b/html/admin.html @@ -236,40 +236,11 @@ Save
-
+
About - User Profiles -
-
-
-

Settings section description.

-
- -
- -
-
-
- - -
-
- -
-
+
diff --git a/ts/admin.ts b/ts/admin.ts index 58e8887..56c8cf5 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -3,6 +3,7 @@ import { Modal } from "./modules/modal.js"; import { Tabs } from "./modules/tabs.js"; import { inviteList, createInvite } from "./modules/invites.js"; import { accountsList } from "./modules/accounts.js"; +import { settingsList } from "./modules/settings.js"; import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; loadTheme(); @@ -42,6 +43,8 @@ var accounts = new accountsList(); window.invites = new inviteList(); +var settings = new settingsList(); + window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); /*const modifySettingsSource = function () { @@ -59,10 +62,9 @@ window.notifications = new notificationBox(document.getElementById('notification // load tabs window.tabs = new Tabs(); -for (let tabID of ["invitesTab", "settingsTab"]) { - window.tabs.addTab(tabID); -} +window.tabs.addTab("invitesTab"); window.tabs.addTab("accountsTab", null, accounts.reload); +window.tabs.addTab("settingsTab", null, settings.reload); function login(username: string, password: string, run?: (state?: number) => void) { const req = new XMLHttpRequest(); diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts new file mode 100644 index 0000000..9329906 --- /dev/null +++ b/ts/modules/settings.ts @@ -0,0 +1,478 @@ +import { _get } from "../modules/common.js"; + +interface settingsBoolEvent extends Event { + detail: boolean; +} + +interface Profile { + Admin: boolean; + LibraryAccess: string; + FromUser: string; +} + +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"); + } + } + + 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", "~critical"); + this._restart.textContent = "R"; + } else { + this._restart.classList.remove("badge", "~critical"); + this._restart.textContent = ""; + } + } + + constructor(inputType: string, setting: Setting, section: string) { + this._container = document.createElement("div"); + this._container.classList.add("setting"); + this._container.innerHTML = ` + + `; + 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); + }); + } + this.update(setting); + } + + 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) { super("text", setting, section); } + 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) { super("password", setting, section); } + 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) { super("email", setting, section); } + 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) { super("number", setting, section); } + 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", "~critical"); + this._restart.textContent = "R"; + } else { + this._restart.classList.remove("badge", "~critical"); + 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 = ` + + `; + 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._input.checked }) + document.dispatchEvent(event); + }; + this._input.onchange = onValueChange; + 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", "~critical"); + this._restart.textContent = "R"; + } else { + this._restart.classList.remove("badge", "~critical"); + 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 += ``; + } + this._select.innerHTML = innerHTML; + } + + constructor(setting: SSelect, section: string) { + this._options = []; + this._container = document.createElement("div"); + this._container.classList.add("setting"); + this._container.innerHTML = ` + + `; + 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); + }); + } + 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; + + 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 = `

${s.meta.description}

`; + 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); + break; + case "password": + setting = new DOMPassword(setting, this._sectionName); + break; + case "email": + setting = new DOMEmail(setting, this._sectionName); + break; + case "number": + setting = new DOMNumber(setting, this._sectionName); + break; + case "bool": + setting = new DOMBool(setting as SBool, this._sectionName, name); + break; + case "select": + setting = new DOMSelect(setting as SSelect, this._sectionName); + break; + } + this._section.appendChild(setting.asElement()); + this._settings[name] = setting; + } + } + } + + 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"); + } + } + + asElement = (): HTMLDivElement => { return this._section; } +} + + + +interface Settings { + order: string[]; + sections: { [sectionName: string]: Section }; +} + +export class settingsList { + 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 } + + 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) { + console.log("found", n); + this._sections[name].visible = true; + this._buttons[name].classList.add("selected"); + } else { + this._sections[n].visible = false; + this._buttons[n].classList.remove("selected"); + } + } + } + + constructor() { + this._sections = {}; + this._buttons = {}; + } + + 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]); + } + } + document.dispatchEvent(new CustomEvent("settings-loaded")); + } + }) + +}