Skip to content

Commit

Permalink
feature (connect): frontend rewrite - connection page
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-kerjean committed Nov 30, 2023
1 parent 2d91ae8 commit fae9391
Show file tree
Hide file tree
Showing 18 changed files with 220 additions and 49 deletions.
2 changes: 1 addition & 1 deletion public/assets/components/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class NotificationComponent extends window.HTMLElement {
this.buffer.push({ message, type });
if (this.buffer.length !== 1) {
const $close = this.querySelector(".close");
if (!($close instanceof window.HTMLElement) || !$close.onclick) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: notification close button missing");
if (!($close instanceof window.HTMLElement) || !$close.onclick) return;
$close.onclick(new window.MouseEvent("mousedown"));
return;
}
Expand Down
3 changes: 3 additions & 0 deletions public/assets/css/designsystem_button.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ button.light {
button.large {
width: 100%;
}
button[disabled] {
opacity: 0.9;
}

.touch-no button.dark:hover, .touch-no button.emphasis:hover, .touch-no button.primary:hover {
filter: brightness(95%);
Expand Down
1 change: 1 addition & 0 deletions public/assets/lib/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function animate($node, opts = {}) {

if (!$node) return Promise.resolve();
else if (typeof $node.animate !== "function") return Promise.resolve();
else if (window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true) return Promise.resolve();

return new Promise((done) => {
$node.animate(keyframes, {
Expand Down
2 changes: 1 addition & 1 deletion public/assets/lib/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async function createFormNodes(node, { renderNode, renderLeaf, renderInput, path
// initialise the dom structure
const $container = window.document.createElement("div");
$container.classList.add("advanced_form");
$container.style.setProperty("overflow", "hidden");
$container.style.setProperty("overflow-x", "hidden");
for (const k of Object.keys(node)) {
if (typeof node[k] !== "object") continue;
else if (!node[k].id) continue;
Expand Down
15 changes: 15 additions & 0 deletions public/assets/lib/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const settings = JSON.parse(window.localStorage.getItem("settings") || "null") || {};

export function settings_get(key) {
if (settings[key] === undefined) {
return null;
}
return settings[key];
}

export function settings_put(key, value) {
settings[key] = value;
setTimeout(() => {
window.localStorage.setItem("settings", JSON.stringify(settings));
}, 0);
}
7 changes: 2 additions & 5 deletions public/assets/model/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ import rxjs from "../lib/rx.js";
import ajax from "../lib/ajax.js";

export function createSession(authenticationRequest) {
// TODO: how to handle null values?
// rxjs.tap((a) => console.log(JSON.stringify(a, (key, value) => {
// if (value !== null) return value;
// }, 4))),
return ajax({
method: "POST",
url: "/api/session",
body: authenticationRequest
body: authenticationRequest,
responseType: "json",
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createElement } from "../../lib/skeleton/index.js";
import { createElement, createRender } from "../../lib/skeleton/index.js";
import rxjs, { effect, applyMutations, onClick } from "../../lib/rx.js";
import { createForm } from "../../lib/form.js";
import { qs, qsa } from "../../lib/dom.js";
import { formTmpl } from "../../components/form.js";
import { generateSkeleton } from "../../components/skeleton.js";
import ctrlError from "../ctrl_error.js";

import { initStorage, getState, getBackendAvailable, getBackendEnabled, addBackendEnabled, removeBackendEnabled } from "./ctrl_backend_state.js";
import { save as saveConfig } from "./model_config.js";
Expand Down Expand Up @@ -123,4 +124,5 @@ const saveConnections = () => rxjs.pipe(
return config;
}))),
saveConfig(),
rxjs.catchError(ctrlError()),
);
2 changes: 1 addition & 1 deletion public/assets/pages/adminpage/ctrl_settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default AdminHOC(async function(render) {
)),
reshapeConfigBeforeSave,
saveConfig(),
rxjs.catchError(ctrlError(createRender(qs(document.body, "[role=\"main\"]")))),
rxjs.catchError(ctrlError()),
));
});

Expand Down
7 changes: 6 additions & 1 deletion public/assets/pages/adminpage/model_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export function save() {
method: "POST",
responseType: "json",
body: formData,
}).pipe(rxjs.tap(() => isSaving$.next(false)))),
})),
rxjs.tap(() => isSaving$.next(false)),
rxjs.catchError((err) => {
isSaving$.next(false);
return rxjs.throwError(err);
}),
);
}
33 changes: 33 additions & 0 deletions public/assets/pages/connectpage/ctrl_forkme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createElement } from "../../lib/skeleton/index.js";
import rxjs from "../../lib/rx.js";

import config$ from "./model_config.js";

export default async function(render) {
const hasFork = await config$.pipe(rxjs.filter(({ fork_button }) => fork_button !== false)).toPromise();
if (!hasFork) return;

render(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(--emphasis); color:var(--primary); 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 change: 0 additions & 1 deletion public/assets/pages/connectpage/ctrl_form.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
}
.component_page_connection_form form .advanced_form {
max-height: 156px;
overflow-y: auto;
margin-top: 5px;
padding-right: 10px;
}
Expand Down
118 changes: 85 additions & 33 deletions public/assets/pages/connectpage/ctrl_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ 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 notification from "../../components/notification.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 "./ctrl_form_state.js";
import { setCurrentBackend, getCurrentBackend, getURLParams } from "./ctrl_form_state.js";

const connections$ = config$.pipe(
rxjs.map(({ connections }) => connections || []),
rxjs.map(({ connections = [], auth = [] }) => connections.map((conn) => {
conn.middleware = auth.indexOf(conn.label) >= 0;
return conn;
})),
rxjs.shareReplay(1),
);

Expand All @@ -25,9 +29,7 @@ export default async function(render) {
<div class="no-select component_page_connection_form">
<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 data-bind="form" class="box hidden"><form></form></div>
</div>
`);
render(transition($page, {
Expand All @@ -39,10 +41,12 @@ export default async function(render) {
}));

// feature1: create navigation buttons to select storage
const $nav = qs($page, "[role=\"navigation\"]");
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"),
applyMutations($nav, "appendChild"),
rxjs.tap(() => animate($nav)),
));

// feature2: select a default storage among all the available ones
Expand All @@ -58,7 +62,13 @@ export default async function(render) {

// feature3: create the storage forms
const formSpecs$ = connections$.pipe(rxjs.mergeMap((conns) => backend$.pipe(
rxjs.map((backendSpecs) => conns.map(({ type }) => backendSpecs[type])),
rxjs.map((backendSpecs) => conns.map(({ type, middleware, label }) => {
if (middleware) return { // admin has set this storage as auth middleware
middleware: { type: "hidden" },
label: { type: "hidden", value: label },
};
return backendSpecs[type];
})),
)));
effect(getCurrentBackend().pipe(
rxjs.mergeMap((n) => formSpecs$.pipe(
Expand All @@ -76,9 +86,17 @@ export default async function(render) {
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>`))),
applyMutation(qs($page, "[data-bind=\"form\"] form"), "replaceChildren"),
rxjs.tap(($innerForm) => $innerForm.parentElement.appendChild(createElement(`<button class="emphasis full-width">${t("CONNECT")}</button>`))),
rxjs.tap(($innerForm) => {
const $box = $innerForm.parentElement.parentElement;
let $animationTarget = $innerForm;
if ($box.classList.contains("hidden")) { // first load
$box.classList.remove("hidden");
$animationTarget = $box;
}
animate($animationTarget, { time: 200, keyframes: slideYIn(2) });
}),
));

// feature4: interaction with the nav buttons
Expand All @@ -104,16 +122,18 @@ export default async function(render) {
));

// feature6: form submission
const $loader = createElement(`<component-loader></component-loader>`);
const toggleLoader = (hide) => {
if (hide) {
$page.classList.add("hidden");
$page.parentElement.appendChild($loader);
} else {
$loader.remove();
$page.classList.remove("hidden");
}
};
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
// 6.a form submission event handler
rxjs.fromEvent(qs($page, "form"), "submit").pipe(
preventDefault(),
rxjs.map((e) => new FormData(e.target)),
Expand All @@ -125,38 +145,70 @@ export default async function(render) {
return json;
}),
),
// 6.b formatted URL in the like of type=xxx&etc=etc
rxjs.of(getURLParams()).pipe(
rxjs.filter(({ type }) => !!type),
rxjs.mergeMap((urlParams) => connections$.pipe(
rxjs.map((conns) => conns.filter(({ middleware, type }) => middleware !== true && type === urlParams["type"])),
rxjs.mergeMap((conns) => {
if (conns.length === 0) return rxjs.EMPTY;
return rxjs.of(urlParams);
}),
)),
),
// 6.c auto submit when it's the only choice available
connections$.pipe(
rxjs.filter((conns) => conns.length === 1),
rxjs.map((conns) => conns[0]),
rxjs.filter(({ middleware }) => middleware),
),
).pipe(
rxjs.mergeMap((formData) => { // CASE 1: authentication middleware flow
// TODO
return rxjs.of(formData);
if (!("middleware" in formData)) return rxjs.of(formData);
let url = "/api/session/auth/?action=redirect";
url += "&label=" + formData["label"];
const p = getURLParams();
if (Object.keys(p).length > 0) {
url += "&state=" + btoa(JSON.stringify(p));
}
location.href = url
return rxjs.EMPTY;
}),
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");
const _next = getURLParams()["next"];
if (_next) u.searchParams.set("next", _next);
subscriber.next(u.toString());
}).pipe(
rxjs.tap((a) => console.log(a)),
rxjs.tap(() => toggleLoader(true)),
rxjs.mergeMap((url) => ajax({ url, responseType: "json" })),
// TODO: loading
rxjs.tap(({ responseJSON }) => location.href = responseJSON.result),
rxjs.catchError(ctrlError(render)),
rxjs.catchError(ctrlError()),
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
delete formData["label"];
delete formData["middleware"];
return rxjs.of(null).pipe(
rxjs.tap(() => toggleLoader(true)),
rxjs.mergeMap(() => createSession(formData)),
rxjs.tap(({ responseJSON }) => {
let redirectURL = "/files/";
const GET = getURLParams();
if (GET["next"]) redirectURL = GET["next"];
else if (responseJSON.result) redirectURL = "/files" + responseJSON.result;
navigate(redirectURL);
}),
rxjs.catchError((err) => {
toggleLoader(false);
notification.error(t(err && err.message));
return rxjs.EMPTY;
})
);
// return rxjs.EMPTY;
}),
));


// TODO submit when there's only 1 backend defined through auth. middleware
// feature: clear the cache
}
19 changes: 19 additions & 0 deletions public/assets/pages/connectpage/ctrl_form_state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import rxjs from "../../lib/rx.js";

const currentBackend$ = new rxjs.ReplaySubject(1);

export function setCurrentBackend(n) {
console.log("SET: ", n);
currentBackend$.next(n);
}

export function getCurrentBackend() {
return currentBackend$.asObservable();
}

export function getURLParams() {
return [...new URLSearchParams(location.search)].reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
}
Loading

0 comments on commit fae9391

Please sign in to comment.