diff --git a/public/assets/pages/adminpage/ctrl_login.js b/public/assets/pages/adminpage/ctrl_login.js
index f443fe71..6af58eb0 100644
--- a/public/assets/pages/adminpage/ctrl_login.js
+++ b/public/assets/pages/adminpage/ctrl_login.js
@@ -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
diff --git a/public/assets/pages/adminpage/decorator_admin_only.js b/public/assets/pages/adminpage/decorator_admin_only.js
index 080a765f..1c2f8b37 100644
--- a/public/assets/pages/adminpage/decorator_admin_only.js
+++ b/public/assets/pages/adminpage/decorator_admin_only.js
@@ -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)),
));
};
}
diff --git a/public/assets/pages/connectpage/component_forkme.js b/public/assets/pages/connectpage/component_forkme.js
deleted file mode 100644
index 512c8352..00000000
--- a/public/assets/pages/connectpage/component_forkme.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { createElement } from "../../lib/skeleton/index.js";
-
-export default createElement(`
-
-
-
-
-`);
diff --git a/public/assets/pages/connectpage/component_poweredby.js b/public/assets/pages/connectpage/component_poweredby.js
deleted file mode 100644
index 0aa4fd30..00000000
--- a/public/assets/pages/connectpage/component_poweredby.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { createElement } from "../../lib/skeleton/index.js";
-import t from "../../lib/locales.js";
-
-export default createElement(`
-
-`);
diff --git a/public/assets/pages/connectpage/ctrl_form.css b/public/assets/pages/connectpage/ctrl_form.css
index dfb0f8ca..17dca515 100644
--- a/public/assets/pages/connectpage/ctrl_form.css
+++ b/public/assets/pages/connectpage/ctrl_form.css
@@ -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;
diff --git a/public/assets/pages/connectpage/ctrl_form.js b/public/assets/pages/connectpage/ctrl_form.js
index 867d57b9..969ab952 100644
--- a/public/assets/pages/connectpage/ctrl_form.js
+++ b/public/assets/pages/connectpage/ctrl_form.js
@@ -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(`
`);
+ 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(``)),
- 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(``))),
+ 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(""),
+ renderLeaf: ({ label, type }) => {
+ if (type === "enable") return createElement(`
+
+ `);
+ return createElement("");
+ }
+ }))),
+ applyMutation(qs($page, "form"), "replaceChildren"),
+ rxjs.tap(() => animate($page.querySelector("form > div"), { time: 200, keyframes: slideYIn(2) })),
+ rxjs.tap(() => qs($page, "form").appendChild(createElement(``))),
+ ));
+
+ // 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(""),
- renderLeaf: ({ label, type }) => {
- if (type === "enable") {
- return createElement(`
-
- `);
- }
- return createElement("");
- }
- });
- 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("")))
- ));
-
- // 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
}
diff --git a/public/assets/pages/connectpage/model_backend.js b/public/assets/pages/connectpage/model_backend.js
index ef997d03..cb97ad04 100644
--- a/public/assets/pages/connectpage/model_backend.js
+++ b/public/assets/pages/connectpage/model_backend.js
@@ -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),
+);
diff --git a/public/assets/pages/connectpage/model_config.js b/public/assets/pages/connectpage/model_config.js
index 5b44617b..cdf7c668 100644
--- a/public/assets/pages/connectpage/model_config.js
+++ b/public/assets/pages/connectpage/model_config.js
@@ -6,5 +6,5 @@ export default ajax({
responseType: "json"
}).pipe(
rxjs.map(({ responseJSON }) => responseJSON.result),
- rxjs.share()
+ rxjs.shareReplay(1),
);
diff --git a/public/assets/pages/connectpage/state.js b/public/assets/pages/connectpage/state.js
deleted file mode 100644
index acd3580a..00000000
--- a/public/assets/pages/connectpage/state.js
+++ /dev/null
@@ -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();
-}
diff --git a/public/assets/pages/ctrl_connectpage.js b/public/assets/pages/ctrl_connectpage.js
index 8b21bc41..64aab24c 100644
--- a/public/assets/pages/ctrl_connectpage.js
+++ b/public/assets/pages/ctrl_connectpage.js
@@ -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(`
`);
+
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;
diff --git a/public/assets/pages/ctrl_error.css b/public/assets/pages/ctrl_error.css
deleted file mode 100644
index 4158170d..00000000
--- a/public/assets/pages/ctrl_error.css
+++ /dev/null
@@ -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);
-}
diff --git a/public/assets/pages/ctrl_error.js b/public/assets/pages/ctrl_error.js
index d22167ae..8ce86692 100644
--- a/public/assets/pages/ctrl_error.js
+++ b/public/assets/pages/ctrl_error.js
@@ -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);
+}
+`
diff --git a/public/assets/pages/ctrl_filespage.js b/public/assets/pages/ctrl_filespage.js
index 492b14bb..1bd9777e 100644
--- a/public/assets/pages/ctrl_filespage.js
+++ b/public/assets/pages/ctrl_filespage.js
@@ -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";
diff --git a/public/assets/pages/ctrl_homepage.js b/public/assets/pages/ctrl_homepage.js
index 6ca362c9..bf57efa5 100644
--- a/public/assets/pages/ctrl_homepage.js
+++ b/public/assets/pages/ctrl_homepage.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(""));
+
+ // 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(""));
-
+ // 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)),
));
};
diff --git a/public/tsconfig.json b/public/tsconfig.json
index 0573d3ba..47075a8b 100644
--- a/public/tsconfig.json
+++ b/public/tsconfig.json
@@ -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"
]
}