diff --git a/public/boot/ctrl_boot_frontoffice.js b/public/boot/ctrl_boot_frontoffice.js index a00605f61..a7e1d70ca 100644 --- a/public/boot/ctrl_boot_frontoffice.js +++ b/public/boot/ctrl_boot_frontoffice.js @@ -57,13 +57,13 @@ function setup_translation() { selectedLanguage = "zh_tw"; break; default: - const userLanguage = window.navigator.language.split("-")[0]; + const userLanguage = window.navigator.language.split("-")[0] || "en"; const idx = [ "az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "et", "eu", "fi", "fr", "gl", "hr", "hu", "id", "is", "it", "ja", "ka", "ko", "lt", "lv", "mn", "nb", "nl", "pl", "pt", "ro", "ru", "sk", "sl", "sr", "sv", "th", "tr", "uk", "vi", "zh" - ].indexOf(window.navigator.language.split("-")[0]); + ].indexOf(window.navigator.language.split("-")[0] || ""); if (idx !== -1) { selectedLanguage = userLanguage; } diff --git a/public/components/breadcrumb.js b/public/components/breadcrumb.js index 5540a0269..f1ffb0e2e 100644 --- a/public/components/breadcrumb.js +++ b/public/components/breadcrumb.js @@ -6,7 +6,7 @@ const css = await CSS(import.meta.url, "breadcrumb.css"); class ComponentBreadcrumb extends HTMLDivElement { constructor() { super(); - if (new window.URL(location.href).searchParams.get("nav") === "false") return null; + if (new window.URL(location.href).searchParams.get("nav") === "false") return; const htmlLogout = isRunningFromAnIframe ? "" diff --git a/public/components/form.js b/public/components/form.js index bd996f430..135e012aa 100644 --- a/public/components/form.js +++ b/public/components/form.js @@ -1,5 +1,6 @@ import { createElement } from "../lib/skeleton/index.js"; import { gid } from "../lib/random.js"; +import { ApplicationError } from "../lib/error.js"; import "./icon.js"; @@ -76,7 +77,8 @@ export function $renderInput(options = {}) { class="component_input" /> `); - $input.setAttribute("value", value || ""); + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; attrs.map((setAttribute) => setAttribute($input)); if (!datalist) return $input; @@ -94,17 +96,19 @@ export function $renderInput(options = {}) { $datalist.appendChild(new Option(value)); }); if (!props.multi) return $wrapper; + // @ts-ignore $input.refresh = () => { - const _datalist = $input.getAttribute("datalist").split(","); + const _datalist = $input?.getAttribute("datalist")?.split(","); $datalist.innerHTML = ""; multicomplete($input.getAttribute("value"), _datalist).forEach((value) => { $datalist.appendChild(new Option(value)); }); }; - $input.oninput = (e) => { + $input.oninput = () => { for (const $option of $datalist.children) { $option.remove(); } + // @ts-ignore $input.refresh(); }; return $wrapper; @@ -130,7 +134,8 @@ export function $renderInput(options = {}) { class="component_input" /> `); - $input.setAttribute("value", value); + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; attrs.map((setAttribute) => setAttribute($input)); return $input; } @@ -145,14 +150,16 @@ export function $renderInput(options = {}) { `); const $input = $div.querySelector("input"); - $input.value = value; + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; attrs.map((setAttribute) => setAttribute($input)); const $icon = $div.querySelector("component-icon"); if ($icon instanceof window.HTMLElement) { $icon.onclick = function(e) { if (!(e.target instanceof window.HTMLElement)) return; - const $input = e.target.parentElement.previousElementSibling; + const $input = e.target?.parentElement?.previousElementSibling; + if (!$input) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if ($input.getAttribute("type") === "password") $input.setAttribute("type", "text"); else $input.setAttribute("type", "password"); }; @@ -166,7 +173,8 @@ export function $renderInput(options = {}) { rows="8" > `); - $textarea.setAttribute("value", value); + if (!($textarea instanceof window.HTMLTextAreaElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $textarea.value = value; attrs.map((setAttribute) => setAttribute($textarea)); return $textarea; } @@ -178,7 +186,8 @@ export function $renderInput(options = {}) { readonly /> `); - $input.setAttribute("value", value); + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; attrs.map((setAttribute) => setAttribute($input)); return $input; } @@ -186,7 +195,8 @@ export function $renderInput(options = {}) { const $input = createElement(` `); - $input.setAttribute("value", value); + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; $input.setAttribute("name", path.join(".")); return $input; } @@ -208,7 +218,8 @@ export function $renderInput(options = {}) { const $select = createElement(` `); - $select.setAttribute("value", value || props.default); + if (!($select instanceof window.HTMLSelectElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $select.value = value || props.default; attrs.map((setAttribute) => setAttribute($select)); (options || []).forEach((name) => { const $option = createElement(` @@ -230,7 +241,8 @@ export function $renderInput(options = {}) { class="component_input" /> `); - $input.setAttribute("value", value); + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; attrs.map((setAttribute) => setAttribute($input)); return $input; } @@ -241,7 +253,8 @@ export function $renderInput(options = {}) { class="component_input" /> `); - $input.setAttribute("value", value); + if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); + else if (value) $input.value = value; attrs.map((setAttribute) => setAttribute($input)); return $input; } @@ -285,7 +298,7 @@ export function format(name) { if (word.length < 1) { return word; } - return word[0].toUpperCase() + word.substring(1); + return (word[0] || "").toUpperCase() + word.substring(1); }) .join(" "); }; diff --git a/public/components/loader.js b/public/components/loader.js index 2a09618a5..ca3dc49b6 100644 --- a/public/components/loader.js +++ b/public/components/loader.js @@ -8,7 +8,7 @@ class Loader extends window.HTMLElement { this.innerHTML = this.render({ inline: this.hasAttribute("inlined"), }); - }, parseInt(this.getAttribute("delay")) || 0); + }, parseInt(this.getAttribute("delay") || "0")); } disconnectedCallback() { diff --git a/public/components/notification.js b/public/components/notification.js index af32beff8..91b84264c 100644 --- a/public/components/notification.js +++ b/public/components/notification.js @@ -25,7 +25,8 @@ class NotificationComponent extends window.HTMLElement { this.buffer.push({ message, type }); if (this.buffer.length !== 1) { const $close = this.querySelector(".close"); - if ($close && typeof $close.onclick === "function") $close.onclick(); + if (!($close instanceof window.HTMLElement) || !$close.onclick) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: notification close button missing"); + $close.onclick(new window.MouseEvent("mousedown")); return; } await this.run(); @@ -42,8 +43,14 @@ class NotificationComponent extends window.HTMLElement { }); const ids = []; await Promise.race([ - new Promise((done) => ids.push(window.setTimeout(done, this.buffer.length === 1 ? 8000 : 800))), - new Promise((done) => ids.push(window.setTimeout(() => $notification.querySelector(".close").onclick = done, 1000))), + new Promise((done) => ids.push(window.setTimeout(() => { + done(new window.MouseEvent("mousedown")); + }, this.buffer.length === 1 ? 8000 : 800))), + new Promise((done) => ids.push(window.setTimeout(() => { + const $close = $notification.querySelector(".close"); + if (!($close instanceof window.HTMLElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: notification close button missing"); + $close.onclick = done; + }, 1000))), ]); ids.forEach((id) => window.clearTimeout(id)); await animate($notification, { diff --git a/public/helpers/loader.js b/public/helpers/loader.js index d9615d7fa..a19dcbadf 100644 --- a/public/helpers/loader.js +++ b/public/helpers/loader.js @@ -22,7 +22,7 @@ async function loadSingleCSS(baseURL, filename) { cache: "force-cache", }); if (res.status !== 200) return `/* ERROR: ${res.status} */`; - else if (!res.headers.get("Content-Type").startsWith("text/css")) return `/* ERROR: wrong type, got "${res.headers.get("Content-Type")}"*/`; + else if (!res.headers.get("Content-Type")?.startsWith("text/css")) `/* ERROR: wrong type, got "${res.headers?.get("Content-Type")}"*/`; return await res.text(); } diff --git a/public/lib/form.js b/public/lib/form.js index 211cf9053..2fbf1fa31 100644 --- a/public/lib/form.js +++ b/public/lib/form.js @@ -1,4 +1,5 @@ import { createElement } from "./skeleton/index.js"; +import { ApplicationError } from "./error.js"; import { animate } from "./animate.js"; export function mutateForm(formSpec, formState) { @@ -7,8 +8,12 @@ export function mutateForm(formSpec, formState) { const keys = inputName.split("."); let ptr = formSpec; - while (keys.length > 1) ptr = ptr[keys.shift()]; - const key = keys.shift(); + while (keys.length > 1) { + let k = keys.shift(); + if (!k) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing key"); + ptr = ptr[k]; + } + const key = keys.shift() || ""; if (ptr && ptr[key]) ptr[key].value = (value === "" ? null : value); }); return formSpec; diff --git a/public/lib/locales.js b/public/lib/locales.js index 6dfab72d8..7c367a149 100644 --- a/public/lib/locales.js +++ b/public/lib/locales.js @@ -1,21 +1,20 @@ export default function t(str = "", replacementString, requestedKey) { - return str; - // const calculatedKey = str.toUpperCase() - // .replace(/ /g, "_") - // .replace(/[^a-zA-Z0-9\-\_\*\{\}\?]/g, "") - // .replace(/\_+$/, ""); - // const value = requestedKey === undefined ? - // window.LNG && window.LNG[calculatedKey] : - // window.LNG && window.LNG[requestedKey]; - // return reformat( - // value || str || "", - // str, - // ).replace("{{VALUE}}", replacementString); + const calculatedKey = str.toUpperCase() + .replace(/ /g, "_") + .replace(/[^a-zA-Z0-9\-\_\*\{\}\?]/g, "") + .replace(/\_+$/, ""); + const value = requestedKey === undefined ? + window.LNG && window.LNG[calculatedKey] : + window.LNG && window.LNG[requestedKey]; + return reformat( + value || str || "", + str, + ).replace("{{VALUE}}", replacementString); } -// function reformat(translated, initial) { -// if (initial[0] && initial[0].toLowerCase() === initial[0]) { -// return translated || ""; -// } -// return (translated[0] && translated[0].toUpperCase() + translated.substring(1)) || ""; -// } +function reformat(translated, initial) { + if (initial[0] && initial[0].toLowerCase() === initial[0]) { + return translated || ""; + } + return (translated[0] && translated[0].toUpperCase() + translated.substring(1)) || ""; +} diff --git a/public/lib/skeleton/router.test.js b/public/lib/skeleton/router.test.js index 2016f5090..7e182960f 100644 --- a/public/lib/skeleton/router.test.js +++ b/public/lib/skeleton/router.test.js @@ -43,7 +43,7 @@ describe("router", () => { // then expect(fn).toBeCalled(); }); - xit("trigger a page change when clicking on a link with [data-link] attribute", () => { + it("trigger a page change when clicking on a link with [data-link] attribute", () => { // given const fn = jest.fn(); const $link = createElement(""); diff --git a/public/lib/vendor/bcrypt.js b/public/lib/vendor/bcrypt.js index cfd12ca73..150725da1 100644 --- a/public/lib/vendor/bcrypt.js +++ b/public/lib/vendor/bcrypt.js @@ -1,3 +1,4 @@ +// @ts-nocheck // code was adapted from https://github.com/dcodeIO/bcrypt.js, meaning: // - we took the code from a CDN https://cdnjs.cloudflare.com/ajax/libs/bcryptjs/2.2.0/bcrypt.js // - remove the amd,commonJS stuff on the top of the file diff --git a/public/pages/adminpage/component_box-item.js b/public/pages/adminpage/component_box-item.js index 073f4fee5..97cd9686d 100644 --- a/public/pages/adminpage/component_box-item.js +++ b/public/pages/adminpage/component_box-item.js @@ -1,3 +1,5 @@ +import { ApplicationError } from "../../lib/error.js"; + class BoxItem extends window.HTMLDivElement { constructor() { super(); @@ -11,12 +13,11 @@ class BoxItem extends window.HTMLDivElement { attributeChangedCallback() { this.innerHTML = this.render({ label: this.getAttribute("data-label"), - selected: false, }); this.classList.add("box-item", "pointer", "no-select"); } - render({ label, selected }) { + render({ label }) { return `
${label} @@ -30,6 +31,7 @@ class BoxItem extends window.HTMLDivElement { toggleSelection(opt = {}) { const { tmpl, isSelected = !this.classList.contains("active") } = opt; const $icon = this.querySelector(".icon"); + if (!$icon) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: no icon"); if (isSelected) { this.classList.add("active"); if (tmpl) $icon.innerHTML = tmpl; diff --git a/public/pages/adminpage/ctrl_backend_component_authentication.js b/public/pages/adminpage/ctrl_backend_component_authentication.js index e3f57bba4..29f8ed47e 100644 --- a/public/pages/adminpage/ctrl_backend_component_authentication.js +++ b/public/pages/adminpage/ctrl_backend_component_authentication.js @@ -2,6 +2,7 @@ import { createElement } from "../../lib/skeleton/index.js"; import rxjs, { effect, applyMutation, applyMutations, onClick } from "../../lib/rx.js"; import { createForm, mutateForm } from "../../lib/form.js"; import { qs, qsa } from "../../lib/dom.js"; +import { ApplicationError } from "../../lib/error.js"; import { formTmpl } from "../../components/form.js"; import { generateSkeleton } from "../../components/skeleton.js"; @@ -77,7 +78,10 @@ export default async function(render) { })), rxjs.map((idpState) => [availableSpecs, idpState]), )), - rxjs.concatMap(async([availableSpecs, idpState = {}]) => { + rxjs.concatMap(async([ + availableSpecs, + idpState = { type: null, params: null }, + ]) => { const { type, params } = idpState; const idps = []; for (const key in availableSpecs) { @@ -167,7 +171,9 @@ export default async function(render) { rxjs.map((connections) => connections.map(({ label }) => label)), rxjs.tap((datalist) => { const $input = $page.querySelector("[name=\"attribute_mapping.related_backend\"]"); + if (!$input) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing related backend"); $input.setAttribute("datalist", datalist.join(",")); + // @ts-ignore $input.refresh(); }), )); @@ -238,6 +244,7 @@ export default async function(render) { renderLeaf: () => createElement(""), }))), rxjs.tap(($node) => { + /** @type { Element | undefined} */ let $relatedBackendField; $page.querySelectorAll("[data-bind=\"attribute-mapping\"] fieldset").forEach(($el, i) => { if (i === 0) $relatedBackendField = $el; diff --git a/public/pages/adminpage/ctrl_backend_state.js b/public/pages/adminpage/ctrl_backend_state.js index de5625ba6..ecb85b3e4 100644 --- a/public/pages/adminpage/ctrl_backend_state.js +++ b/public/pages/adminpage/ctrl_backend_state.js @@ -1,5 +1,6 @@ import rxjs from "../../lib/rx.js"; import { qs } from "../../lib/dom.js"; +import { ApplicationError } from "../../lib/error.js"; import { get as getConfig } from "../../model/config.js"; import { get as getAdminConfig } from "./model_config.js"; import { formObjToJSON$ } from "./helper_form.js"; @@ -92,15 +93,18 @@ export function getState() { }; if (!authType) return config; - let formValues = [...new FormData(document.querySelector("[data-bind=\"idp\"]"))]; + const $formIDP = document.querySelector("[data-bind=\"idp\"]") + if (!($formIDP instanceof window.HTMLFormElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: idp isn't a form"); + let formValues = [...new FormData($formIDP)]; config.middleware.identity_provider = { type: authType, params: JSON.stringify( formValues - .filter(([key, value]) => key.startsWith(`${authType}.`)) // remove elements that aren't in scope + .filter(([key]) => key.startsWith(`${authType}.`)) // remove elements that aren't in scope .map(([key, value]) => [key.replace(new RegExp(`^${authType}\.`), ""), value]) // format the relevant keys .reduce((acc, [key, value]) => { // transform onto something ready to be saved if (key === "type") return acc; + else if (typeof key !== "string") return acc; return { ...acc, [key]: value, @@ -109,14 +113,16 @@ export function getState() { ), }; - formValues = [...new FormData(document.querySelector("[data-bind=\"attribute-mapping\"]"))]; + const $formAM = document.querySelector("[data-bind=\"attribute-mapping\"]"); + if (!($formAM instanceof window.HTMLFormElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: attribute mapping isn't a form"); + formValues = [...new FormData($formAM)]; config.middleware.attribute_mapping = { - related_backend: formValues.shift()[1], + related_backend: (formValues.shift() || [])[1], params: JSON.stringify(formValues.reduce((acc, [key, value]) => { const k = key.split("."); if (k.length !== 2) return acc; - if (!acc[k[0]]) acc[k[0]] = {}; - if (value !== "") acc[k[0]][k[1]] = value; + if (!acc[`${k[0]}`]) acc[`${k[0]}`] = {}; + if (value !== "") acc[`${k[0]}`][`${k[1]}`] = value; return acc; }, {})), }; diff --git a/public/pages/adminpage/ctrl_log_audit.js b/public/pages/adminpage/ctrl_log_audit.js index c810edc5f..8ce6897fe 100644 --- a/public/pages/adminpage/ctrl_log_audit.js +++ b/public/pages/adminpage/ctrl_log_audit.js @@ -57,7 +57,7 @@ function updateLoop($page, audit$) { const p = new URLSearchParams(); for (const [key, value] of formData.entries()) { if (!value) continue; - p.set(key.replace(new RegExp("^search\."), ""), value); + p.set(key.replace(new RegExp("^search\."), ""), `${value}`); } return p; }), diff --git a/public/pages/adminpage/helper_form.js b/public/pages/adminpage/helper_form.js index cfe0ab432..5a69ff887 100644 --- a/public/pages/adminpage/helper_form.js +++ b/public/pages/adminpage/helper_form.js @@ -1,7 +1,7 @@ import { createElement } from "../../lib/skeleton/index.js"; import rxjs from "../../lib/rx.js"; -export function renderLeaf({ format, type, label, description }) { +export function renderLeaf({ format, label, description }) { return createElement(`