mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-28 19:22:31 +01:00
chore (refactoring): error and connection page
This commit is contained in:
parent
792ef4ddfb
commit
bc117af48d
15 changed files with 226 additions and 260 deletions
|
|
@ -45,8 +45,7 @@ export default async function(render) {
|
|||
if (err instanceof AjaxError) {
|
||||
switch (err.code()) {
|
||||
case "INTERNAL_SERVER_ERROR":
|
||||
ctrlError(render)(err);
|
||||
return rxjs.EMPTY;
|
||||
return rxjs.throwError(err);
|
||||
case "FORBIDDEN":
|
||||
return rxjs.of(false);
|
||||
}
|
||||
|
|
@ -61,7 +60,8 @@ export default async function(render) {
|
|||
rxjs.mapTo(["name", "arrow_right"]), applyMutation(qs($form, "component-icon"), "setAttribute"),
|
||||
rxjs.mapTo(""), stateMutation(qs($form, "[name=\"password\"]"), "value"),
|
||||
rxjs.mapTo(["error"]), applyMutation(qs($form, ".input_group"), "classList", "add"),
|
||||
rxjs.delay(300), applyMutation(qs($form, ".input_group"), "classList", "remove")
|
||||
rxjs.delay(300), applyMutation(qs($form, ".input_group"), "classList", "remove"),
|
||||
rxjs.catchError(ctrlError(render)),
|
||||
));
|
||||
|
||||
// feature: autofocus
|
||||
|
|
|
|||
|
|
@ -8,14 +8,8 @@ export default function AdminOnly(ctrlWrapped) {
|
|||
return (render) => {
|
||||
effect(isAdmin$().pipe(
|
||||
rxjs.map((isAdmin) => isAdmin ? ctrlWrapped : ctrlLogin),
|
||||
rxjs.catchError((err) => {
|
||||
if (err instanceof AjaxError && err.code() === "INTERNAL_SERVER_ERROR") {
|
||||
ctrlError(err)(render);
|
||||
return rxjs.EMPTY;
|
||||
}
|
||||
return rxjs.of(ctrlError(err));
|
||||
}),
|
||||
rxjs.tap((ctrl) => ctrl(render)),
|
||||
rxjs.catchError(ctrlError(render)),
|
||||
));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
|
||||
export default createElement(`
|
||||
<a href="https://github.com/mickael-kerjean/skeleton" class="component_forkme" aria-label="View source on GitHub">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--color); color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
|
||||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
|
||||
</svg>
|
||||
<style>
|
||||
.component_forkme:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}
|
||||
.dark-mode .component_forkme .octo-arm,
|
||||
.dark-mode .component_forkme .octo-body { fill: var(--bg-color); }
|
||||
@media (max-width:500px){
|
||||
.component_forkme:hover .octo-arm{animation:none}
|
||||
.component_forkme .octo-arm{animation:octocat-wave 560ms ease-in-out}
|
||||
}
|
||||
@keyframes octocat-wave{
|
||||
0%,100%{transform:rotate(0)}
|
||||
20%,60%{transform:rotate(-25deg)}
|
||||
40%,80%{transform:rotate(10deg)}
|
||||
}
|
||||
</style>
|
||||
</a>
|
||||
`);
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import t from "../../lib/locales.js";
|
||||
|
||||
export default createElement(`
|
||||
<div class="component_poweredbyfilestash">
|
||||
${t("Powered by")} <strong><a href="https://www.filestash.app">Filestash</a></strong>
|
||||
<style>
|
||||
.component_poweredbyfilestash{
|
||||
display: inline-block;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
font-size: 0.9em;
|
||||
line-height: 20px;
|
||||
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 20px;
|
||||
}
|
||||
.component_poweredbyfilestash strong{
|
||||
font-weight: normal;
|
||||
}
|
||||
.component_poweredbyfilestash strong a{
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dark-mode .component_poweredbyfilestash {
|
||||
color: var(--light);
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`);
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
color: white;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.component_page_connection_form form .third-party {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -71,17 +72,6 @@
|
|||
margin: 45px 0;
|
||||
}
|
||||
|
||||
.component_page_connection_form.form-appear {
|
||||
opacity: 0;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.component_page_connection_form.form-appear.form-appear-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.25s ease-out, opacity 0.5s ease-out;
|
||||
}
|
||||
|
||||
.scroll-x {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
|
|
|
|||
|
|
@ -1,110 +1,162 @@
|
|||
import { createElement, navigate } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { effect, applyMutation, preventDefault } from "../../lib/rx.js";
|
||||
import rxjs, { effect, applyMutation, applyMutations, preventDefault, onClick } from "../../lib/rx.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
import { qs, qsa, safe } from "../../lib/dom.js";
|
||||
import { animate, slideYIn } from "../../lib/animate.js";
|
||||
import { animate, slideYIn, transition } from "../../lib/animate.js";
|
||||
import { createForm } from "../../lib/form.js";
|
||||
import { settings_get, settings_put } from "../../lib/settings.js";
|
||||
import t from "../../lib/locales.js";
|
||||
import { formTmpl } from "../../components/form.js";
|
||||
|
||||
import { CSS } from "../../helpers/loader.js";
|
||||
import { createSession } from "../../model/session.js";
|
||||
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
import config$ from "./model_config.js";
|
||||
import backend$ from "./model_backend.js";
|
||||
import { setCurrentBackend, getCurrentBackend } from "./state.js";
|
||||
import { setCurrentBackend, getCurrentBackend } from "./ctrl_form_state.js";
|
||||
|
||||
const connections$ = config$.pipe(
|
||||
rxjs.map(({ connections }) => connections || []),
|
||||
rxjs.shareReplay(1),
|
||||
);
|
||||
|
||||
export default async function(render) {
|
||||
const $page = createElement(`
|
||||
<div class="no-select component_page_connection_form">
|
||||
<div role="navigation" class="buttons scroll-x box"></div>
|
||||
<style>${await CSS(import.meta.url, "ctrl_form.css")}</style>
|
||||
<div role="navigation" class="buttons scroll-x box"></div>
|
||||
<div class="box">
|
||||
<form></form>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
render(transition($page, {
|
||||
enter: [
|
||||
{ transform: "scale(0.97)", opacity: 0 },
|
||||
{ transform: "scale(1)", opacity: 1 },
|
||||
],
|
||||
timeEnter: 100,
|
||||
}));
|
||||
|
||||
// feature1: backend selector
|
||||
effect(config$.pipe(
|
||||
// dom creation
|
||||
rxjs.map(({ connections }) => connections),
|
||||
rxjs.mergeMap((conns) => conns.map((conn, i) => ({ ...conn, n: i }))),
|
||||
rxjs.map(({ label, n }) => createElement(`<button class="" data-current="${n}">${safe(label)}</button>`)),
|
||||
rxjs.map(($button) => [$button]), applyMutation(qs($page, "[role=\"navigation\"]"), "appendChild"),
|
||||
// initialise selection
|
||||
rxjs.toArray(),
|
||||
rxjs.map((conns) => Math.max(0, conns.length / 2 - 1)),
|
||||
rxjs.tap((current) => setCurrentBackend(current))
|
||||
// feature1: create navigation buttons to select storage
|
||||
effect(connections$.pipe(
|
||||
rxjs.map((conns) => conns.map((conn, i) => ({ ...conn, n: i }))),
|
||||
rxjs.map((conns) => conns.map(({ label, n }) => createElement(`<button data-current="${n}">${safe(label)}</button>`))),
|
||||
applyMutations(qs($page, "[role=\"navigation\"]"), "appendChild"),
|
||||
));
|
||||
|
||||
// feature2: interaction with the buttons
|
||||
// feature2: select a default storage among all the available ones
|
||||
effect(connections$.pipe(
|
||||
rxjs.map((conns) => {
|
||||
let n = parseInt(settings_get("login_tab"));
|
||||
if (Number.isNaN(n)) n = (conns.length || 0) / 2 - 1;
|
||||
if (n < 0 || n >= conns.length) n = 0;
|
||||
return n
|
||||
}),
|
||||
rxjs.tap((current) => setCurrentBackend(Math.round(current))),
|
||||
));
|
||||
|
||||
// feature3: create the storage forms
|
||||
const formSpecs$ = connections$.pipe(rxjs.mergeMap((conns) => backend$.pipe(
|
||||
rxjs.map((backendSpecs) => conns.map(({ type }) => backendSpecs[type])),
|
||||
)));
|
||||
effect(getCurrentBackend().pipe(
|
||||
rxjs.mergeMap((n) => formSpecs$.pipe(
|
||||
rxjs.map((specs) => specs[n]),
|
||||
)),
|
||||
rxjs.mergeMap((formSpec) => createForm(formSpec, formTmpl({
|
||||
renderNode: () => createElement("<div></div>"),
|
||||
renderLeaf: ({ label, type }) => {
|
||||
if (type === "enable") return createElement(`
|
||||
<label class="advanced">
|
||||
<span data-bind="children"></span>
|
||||
${label}
|
||||
</label>
|
||||
`);
|
||||
return createElement("<label></label>");
|
||||
}
|
||||
}))),
|
||||
applyMutation(qs($page, "form"), "replaceChildren"),
|
||||
rxjs.tap(() => animate($page.querySelector("form > div"), { time: 200, keyframes: slideYIn(2) })),
|
||||
rxjs.tap(() => qs($page, "form").appendChild(createElement(`<button class="emphasis full-width">${t("CONNECT")}</button>`))),
|
||||
));
|
||||
|
||||
// feature4: interaction with the nav buttons
|
||||
effect(getCurrentBackend().pipe(
|
||||
rxjs.first(),
|
||||
rxjs.map(() => qsa($page, "[role=\"navigation\"] button")),
|
||||
rxjs.mergeMap((els) => els),
|
||||
rxjs.mergeMap(($button) => rxjs.fromEvent($button, "click")),
|
||||
rxjs.map((e) => parseInt(e.target.getAttribute("data-current"))),
|
||||
rxjs.tap((current) => setCurrentBackend(current))
|
||||
));
|
||||
|
||||
// feature3: highlight the selected button
|
||||
effect(getCurrentBackend().pipe(
|
||||
rxjs.map((n) => ({ $buttons: qsa($page, "[role=\"navigation\"] button"), n })),
|
||||
rxjs.tap(({ $buttons }) => $buttons.forEach(($node) => $node.classList.remove("active", "primary"))),
|
||||
rxjs.map(({ $buttons, n }) => $buttons[n]),
|
||||
rxjs.filter(($button) => !!$button),
|
||||
rxjs.tap(($button) => $button.classList.add("active", "primary"))
|
||||
));
|
||||
|
||||
// feature4: insert all the connection form
|
||||
const tmpl = formTmpl({
|
||||
renderNode: () => createElement("<div></div>"),
|
||||
renderLeaf: ({ label, type }) => {
|
||||
if (type === "enable") {
|
||||
return createElement(`
|
||||
<label class="advanced">
|
||||
<span data-bind="children"></span>
|
||||
${label}
|
||||
</label>
|
||||
`);
|
||||
}
|
||||
return createElement("<label></label>");
|
||||
}
|
||||
});
|
||||
effect(rxjs.combineLatest(
|
||||
config$.pipe(
|
||||
rxjs.first(),
|
||||
rxjs.mergeMap(({ connections }) => connections),
|
||||
rxjs.mergeMap(({ type }) => backend$.pipe(rxjs.map((spec) => spec[type]))),
|
||||
rxjs.mergeMap((formSpec) => createForm(formSpec, tmpl)),
|
||||
rxjs.toArray(),
|
||||
rxjs.share()
|
||||
),
|
||||
getCurrentBackend()
|
||||
).pipe(
|
||||
rxjs.map(([$forms, n]) => [$forms[n]]),
|
||||
applyMutation(qs($page, "form"), "replaceChildren"),
|
||||
rxjs.tap(() => animate($page.querySelector("form > div"), { time: 200, keyframes: slideYIn(-2) })),
|
||||
rxjs.tap(() => qs($page, "form").appendChild(createElement("<button class=\"emphasis full-width\">CONNECT</button>")))
|
||||
));
|
||||
|
||||
// feature5: form submission
|
||||
effect(rxjs.fromEvent(qs($page, "form"), "submit").pipe(
|
||||
preventDefault(),
|
||||
rxjs.map((e) => new FormData(e.target)),
|
||||
rxjs.map((formData) => {
|
||||
const json = {};
|
||||
for (const pair of formData.entries()) {
|
||||
json[pair[0]] = pair[1] === "" ? null : pair[1];
|
||||
}
|
||||
return json;
|
||||
rxjs.mergeMap(() => qsa($page, "[role=\"navigation\"] button")),
|
||||
rxjs.mergeMap(($button) => onClick($button)),
|
||||
rxjs.map(($button) => parseInt($button.getAttribute("data-current"))),
|
||||
rxjs.distinctUntilChanged(),
|
||||
rxjs.tap((current) => {
|
||||
settings_put("login_tab", current);
|
||||
setCurrentBackend(current);
|
||||
}),
|
||||
// rxjs.map((formData) => JSON.parse(JSON.stringify(a, (key, value) => {
|
||||
// if (value !== null) return value;
|
||||
// })),
|
||||
rxjs.mergeMap((creds) => createSession(creds)),
|
||||
rxjs.tap(() => navigate("/")),
|
||||
// TODO: error with notification
|
||||
));
|
||||
|
||||
render($page);
|
||||
// feature5: highlight the currently selected storage
|
||||
effect(getCurrentBackend().pipe(
|
||||
rxjs.map((n) => [qsa($page, "[role=\"navigation\"] button"), n]),
|
||||
rxjs.tap(([$buttons, n]) => $buttons.forEach(($button, i) => {
|
||||
if (i !== n) $button.classList.remove("active", "primary");
|
||||
else $button.classList.add("active", "primary");
|
||||
})),
|
||||
));
|
||||
|
||||
// feature6: form submission
|
||||
effect(rxjs.merge(
|
||||
// 6.a submit when url has a type key
|
||||
rxjs.of([...new URLSearchParams(location.search)]).pipe(
|
||||
rxjs.filter((arr) => arr.find(([key, _]) => key === "type")),
|
||||
rxjs.map((arr) => arr.reduce((acc, el) => {
|
||||
acc[el[0]] = el[1]
|
||||
return acc;
|
||||
}, {})),
|
||||
),
|
||||
// 6.b submit on pressing the submit button in the form
|
||||
rxjs.fromEvent(qs($page, "form"), "submit").pipe(
|
||||
preventDefault(),
|
||||
rxjs.map((e) => new FormData(e.target)),
|
||||
rxjs.map((formData) => {
|
||||
const json = {};
|
||||
for (const pair of formData.entries()) {
|
||||
json[pair[0]] = pair[1] === "" ? null : pair[1];
|
||||
}
|
||||
return json;
|
||||
}),
|
||||
),
|
||||
).pipe(
|
||||
rxjs.mergeMap((formData) => { // CASE 1: authentication middleware flow
|
||||
// TODO
|
||||
return rxjs.of(formData);
|
||||
}),
|
||||
rxjs.mergeMap((formData) => { // CASE 2: oauth2 related backends like dropbox and gdrive
|
||||
if (!("oauth2" in formData)) return rxjs.of(formData);
|
||||
return new rxjs.Observable((subscriber) => {
|
||||
const u = new URL(location.toString());
|
||||
u.pathname = formData["oauth2"];
|
||||
const _next = new URLSearchParams(location.search).get("next");
|
||||
if (_next) u.searchParams.set("next", _next);
|
||||
subscriber.next(u.toString());
|
||||
}).pipe(
|
||||
rxjs.tap((a) => console.log(a)),
|
||||
rxjs.mergeMap((url) => ajax({ url, responseType: "json" })),
|
||||
// TODO: loading
|
||||
rxjs.tap(({ responseJSON }) => location.href = responseJSON.result),
|
||||
rxjs.catchError(ctrlError(render)),
|
||||
rxjs.mergeMap(() => rxjs.EMPTY),
|
||||
);
|
||||
}),
|
||||
rxjs.mergeMap((formData) => { // CASE 3: regular login
|
||||
console.log(formData);
|
||||
return createSession(formData).pipe(
|
||||
rxjs.tap(() => navigate("/")), // TODO: home and next redirect
|
||||
);
|
||||
// return rxjs.EMPTY;
|
||||
}),
|
||||
));
|
||||
|
||||
|
||||
// TODO submit when there's only 1 backend defined through auth. middleware
|
||||
// feature: clear the cache
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,7 @@ import ajax from "../../lib/ajax.js";
|
|||
export default ajax({
|
||||
url: "/api/backend",
|
||||
responseType: "json"
|
||||
}).pipe(rxjs.map(({ responseJSON }) => responseJSON.result));
|
||||
}).pipe(
|
||||
rxjs.map(({ responseJSON }) => responseJSON.result),
|
||||
rxjs.shareReplay(1),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ export default ajax({
|
|||
responseType: "json"
|
||||
}).pipe(
|
||||
rxjs.map(({ responseJSON }) => responseJSON.result),
|
||||
rxjs.share()
|
||||
rxjs.shareReplay(1),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import rxjs from "../../lib/rx.js";
|
||||
|
||||
const currentBackend$ = new rxjs.Subject();
|
||||
|
||||
export function setCurrentBackend(n) {
|
||||
currentBackend$.next(n);
|
||||
}
|
||||
|
||||
export function getCurrentBackend() {
|
||||
return currentBackend$.asObservable();
|
||||
}
|
||||
|
|
@ -4,50 +4,37 @@ import { qs } from "../lib/dom.js";
|
|||
import { CSS } from "../helpers/loader.js";
|
||||
|
||||
import ctrlForm from "./connectpage/ctrl_form.js";
|
||||
import config$ from "./connectpage/model_config.js";
|
||||
|
||||
import $fork from "./connectpage/component_forkme.js";
|
||||
import $poweredby from "./connectpage/component_poweredby.js";
|
||||
import ctrlForkme from "./connectpage/ctrl_forkme.js";
|
||||
import ctrlPoweredby from "./connectpage/ctrl_poweredby.js";
|
||||
|
||||
export default async function(render) {
|
||||
const $page = createElement(`
|
||||
<div class="component_page_connect">
|
||||
<style>${await CSS(import.meta.url, "ctrl_connectpage.css")}</style>
|
||||
<div data-bind="component_forkme"></div>
|
||||
<div data-bind="centerthis" class="component_page_connection_form component_container" style="max-width:565px;">
|
||||
<div data-bind="component_form"></div>
|
||||
</div>
|
||||
<div data-bind="component_poweredby"></div>
|
||||
<style>${await CSS(import.meta.url, "ctrl_connectpage.css")}</style>
|
||||
</div>
|
||||
`);
|
||||
|
||||
render($page);
|
||||
|
||||
// feature1: connection form
|
||||
// feature1: forkme & poweredby button
|
||||
ctrlForkme(createRender(qs($page, "[data-bind=\"component_forkme\"]")));
|
||||
ctrlPoweredby(createRender(qs($page, "[data-bind=\"component_poweredby\"]")));
|
||||
await new Promise((done) => setTimeout(done, 300));
|
||||
|
||||
// feature2: connection form
|
||||
ctrlForm(createRender(qs($page, "[data-bind=\"component_form\"]")));
|
||||
|
||||
// feature2: forkme button
|
||||
effect(config$.pipe(
|
||||
rxjs.filter(({ fork_button }) => fork_button !== false),
|
||||
rxjs.mapTo([$fork]),
|
||||
applyMutation(qs($page, "[data-bind=\"component_forkme\"]"), "appendChild")
|
||||
));
|
||||
|
||||
// feature3: poweredby button
|
||||
effect(config$.pipe(
|
||||
rxjs.filter(({ fork_button }) => fork_button !== false),
|
||||
rxjs.mapTo([$poweredby]),
|
||||
applyMutation(qs($page, "[data-bind=\"component_poweredby\"]"), "appendChild")
|
||||
));
|
||||
|
||||
// feature4: center the form
|
||||
// feature3: center the form
|
||||
effect(rxjs.fromEvent(window, "resize").pipe(
|
||||
rxjs.startWith(null),
|
||||
rxjs.map(() => {
|
||||
let size = 300;
|
||||
const $screen = document.querySelector(".login-form");
|
||||
if ($screen instanceof window.HTMLElement) size = $screen.offsetHeight;
|
||||
|
||||
size = Math.round((document.body.offsetHeight - size) / 2);
|
||||
const h = 400;
|
||||
const size = Math.round((document.body.offsetHeight - h) / 2);
|
||||
if (size < 0) return 0;
|
||||
if (size > 150) return 150;
|
||||
return size;
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
.error-page {
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
margin: 50px auto 0 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error-page h1 {
|
||||
margin: 5px 0;
|
||||
font-size: 3.1em;
|
||||
}
|
||||
.error-page h2 {
|
||||
margin: 10px 0;
|
||||
font-weight: normal;
|
||||
font-weight: 100;
|
||||
}
|
||||
.error-page code {
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
background: #e2e2e2;
|
||||
color: var(--dark);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.error-page pre {
|
||||
margin: 0;
|
||||
}
|
||||
.error-page p {
|
||||
font-style: italic;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.error-page a {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
.backnav {
|
||||
font-weight: 100;
|
||||
display: inline-block;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
.backnav .component_icon {
|
||||
height: 23px;
|
||||
margin-right: -3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark-mode .error-page, .dark-mode .backnav {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
|
@ -2,14 +2,11 @@ import { createElement } from "../lib/skeleton/index.js";
|
|||
import rxjs, { effect, applyMutation } from "../lib/rx.js";
|
||||
import { qs } from "../lib/dom.js";
|
||||
import t from "../lib/locales.js";
|
||||
import { CSS } from "../helpers/loader.js";
|
||||
|
||||
import { AjaxError, ApplicationError } from "../lib/error.js";
|
||||
|
||||
import "../components/icon.js";
|
||||
|
||||
const css = await CSS(import.meta.url, "ctrl_error.css")
|
||||
|
||||
export default function(render) {
|
||||
return async function(err) {
|
||||
const [msg, trace] = processError(err);
|
||||
|
|
@ -74,3 +71,56 @@ trace: ${err.stack || "N/A"}`;
|
|||
}
|
||||
return [msg, trace.trim()];
|
||||
}
|
||||
|
||||
|
||||
const css = `
|
||||
.error-page {
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
margin: 50px auto 0 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error-page h1 {
|
||||
margin: 5px 0;
|
||||
font-size: 3.1em;
|
||||
}
|
||||
.error-page h2 {
|
||||
margin: 10px 0;
|
||||
font-weight: normal;
|
||||
font-weight: 100;
|
||||
}
|
||||
.error-page code {
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
background: #e2e2e2;
|
||||
color: var(--dark);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.error-page pre {
|
||||
margin: 0;
|
||||
}
|
||||
.error-page p {
|
||||
font-style: italic;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.error-page a {
|
||||
border-bottom: 1px dashed;
|
||||
}
|
||||
|
||||
.backnav {
|
||||
font-weight: 100;
|
||||
display: inline-block;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
.backnav .component_icon {
|
||||
height: 23px;
|
||||
margin-right: -3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark-mode .error-page, .dark-mode .backnav {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { createElement, createRender } from "../lib/skeleton/index.js";
|
||||
import rxjs, { effect } from "../lib/rx.js";
|
||||
import { CSS } from "../helpers/loader.js";
|
||||
import ctrlError from "./ctrl_error.js";
|
||||
|
||||
import { getState$ } from "./filespage/state.js";
|
||||
import componentFilesystem from "./filespage/filesystem.js";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createElement, navigate } from "../lib/skeleton/index.js";
|
||||
import rxjs, { effect } from "../lib/rx.js";
|
||||
import { ApplicationError } from "../lib/error.js";
|
||||
import { ApplicationError, AjaxError } from "../lib/error.js";
|
||||
import ctrlError from "./ctrl_error.js";
|
||||
|
||||
import { getSession } from "../model/session.js";
|
||||
|
|
@ -8,26 +8,31 @@ import { getSession } from "../model/session.js";
|
|||
import "../components/loader.js";
|
||||
|
||||
export default function(render) {
|
||||
render(createElement("<component-loader></component-loader>"));
|
||||
|
||||
// feature1: trigger error page via url params
|
||||
const GET = new URLSearchParams(location.search);
|
||||
const err = GET.get("error");
|
||||
if (err) {
|
||||
ctrlError(new ApplicationError(
|
||||
ctrlError(render)(new ApplicationError(
|
||||
err,
|
||||
GET.get("trace") || "server error from URL"
|
||||
))(render);
|
||||
GET.get("trace") || "server error from URL",
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
render(createElement("<component-loader></component-loader>"));
|
||||
|
||||
// feature2: redirect user where it makes most sense
|
||||
effect(getSession().pipe(
|
||||
rxjs.catchError((err) => {
|
||||
if (err instanceof AjaxError && err.err().status === 401) {
|
||||
return rxjs.of({ is_authenticated: false });
|
||||
}
|
||||
return rxjs.throwError(err);
|
||||
}),
|
||||
rxjs.tap(({ is_authenticated, home = "/" }) => {
|
||||
if (is_authenticated !== true) return navigate("/login");
|
||||
return navigate(`/files${home}`);
|
||||
}),
|
||||
rxjs.catchError(() => {
|
||||
navigate("/login");
|
||||
return rxjs.EMPTY;
|
||||
}),
|
||||
rxjs.catchError(ctrlError(render)),
|
||||
));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
},
|
||||
"input": ["pages/ctrl_boot.d.ts", "pages/*.js"],
|
||||
"exclude": [
|
||||
"**/*.test.js", "worker/sw_cache.js",
|
||||
"**/*.test.js", "assets/worker/sw_cache.js",
|
||||
"coverage", "jest.setup.js"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue