Skip to content

Commit

Permalink
Merge pull request #8530 from ehuelsmann/feature/serverui-fsm
Browse files Browse the repository at this point in the history
Update UI only from last click
  • Loading branch information
ehuelsmann authored Dec 1, 2024
2 parents 7deea97 + 24be9b7 commit 0418711
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 78 deletions.
102 changes: 24 additions & 78 deletions UI/src/components/ServerUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import { h, inject, ref } from "vue";
import { useI18n } from "vue-i18n";

import { createServerUIMachine } from "./ServerUI.machines.js";

const registry = require("dijit/registry");
const parser = require("dojo/parser");
const query = require("dojo/query");
const topic = require("dojo/topic");

Expand Down Expand Up @@ -33,82 +34,8 @@ export default {
}
},
methods: {
async updateContent(tgt, options = {}) {
let dismiss;
try {
this.notify({
title: options.doing || this.$t("Loading..."),
type: "info",
dismissReceiver: (cb) => {
dismiss = cb;
}
});
let headers = new Headers(options.headers);
headers.set("X-Requested-With", "XMLHttpRequest");

document
.getElementById("maindiv")
.removeAttribute("data-lsmb-done");
// chop off the leading '/' to use relative paths
let base = window.location.pathname.replace(/[^/]*$/, "");
let relTgt =
tgt.substring(0, 1) === "/" ? tgt.substring(1) : tgt;
let r = await fetch(base + relTgt, {
method: options.method,
body: options.data,
headers: headers
// additional parameters to consider:
// mode(cors?), credentials, referrerPolicy?
});

if (r.ok && !domReject(r)) {
let newContent = await r.text();
this.notify({
title: options.done || this.$t("Loaded")
});
if (newContent === this.content) {
// when there is no difference in returned content,
// Vue won't re-render... so don't rerun the parser!
return;
}
this._cleanWidgets();
this.content = newContent;
this.$nextTick(() => {
let maindiv = document.getElementById("maindiv");
parser.parse(maindiv).then(
() => {
registry
.findWidgets(maindiv)
.forEach((child) => {
this.recursivelyResize(child);
});
maindiv
.querySelectorAll("a")
.forEach((node) =>
this._interceptClick(node)
);
if (dismiss) {
dismiss();
}
topic.publish("lsmb/page-fresh-content");
maindiv.setAttribute("data-lsmb-done", "true");
this._setFormFocus();
},
(e) => {
this.reportError(e);
}
);
});
} else {
this.reportError(r);
}
} catch (e) {
this.reportError(e);
} finally {
if (dismiss) {
dismiss();
}
}
updateContent(tgt, options = {}) {
this.machine.send({ type: "loadContent", value: { tgt, options } });
},
_setFormFocus() {
[...document.forms].forEach((form) => {
Expand Down Expand Up @@ -188,7 +115,26 @@ export default {
}
},
beforeRouteLeave() {
this._cleanWidgets();
this.machine.send("unloadContent");
return (
this.machine.current !== "parsing" &&
this.machine.current !== "updating"
);
},
created() {
let maindiv = document.getElementById("maindiv");
this.machine = createServerUIMachine(
{
notify: this.notify,
view: this
},
({ machine }) => {
if (machine.current === "idle") {
topic.publish("lsmb/page-fresh-content");
maindiv.setAttribute("data-lsmb-done", "true");
}
}
);
},
mounted() {
document
Expand Down
221 changes: 221 additions & 0 deletions UI/src/components/ServerUI.machines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/** @format */

import {
action,
createMachine,
immediate,
interpret,
invoke,
guard,
reduce,
state,
transition
} from "@/robot-vue";

const registry = require("dijit/registry");
const parser = require("dojo/parser");

function armWidgets(ctx) {
let maindiv = document.getElementById("maindiv");
registry.findWidgets(maindiv).forEach((child) => {
ctx.view.recursivelyResize(child);
});
maindiv
.querySelectorAll("a")
.forEach((node) => ctx.view._interceptClick(node));
ctx.view._setFormFocus();
}

function disarmWidgets(ctx) {
ctx.view._cleanWidgets();
}

function dismissNotify(ctx) {
if (ctx.dismiss) {
ctx.dismiss();
delete ctx.dismiss;
}
}

function domAcceptable(ctx, { data }) {
let response = data.response;
let rv = !(
response.headers.get("X-LedgerSMB-App-Content") !== "yes" ||
(response.headers.get("Content-Disposition") || "").startsWith(
"attachment"
)
);
return rv;
}

async function requestContent(ctx) {
let headers = new Headers(ctx.options.headers);
headers.set("X-Requested-With", "XMLHttpRequest");

document.getElementById("maindiv").removeAttribute("data-lsmb-done");
// chop off the leading '/' to use relative paths
let base = window.location.pathname.replace(/[^/]*$/, "");
let tgt = ctx.tgt;
let relTgt = tgt.substring(0, 1) === "/" ? tgt.substring(1) : tgt;
return {
response: await fetch(base + relTgt, {
method: ctx.options.method,
body: ctx.options.data,
headers: headers
// additional parameters to consider:
// mode(cors?), credentials, referrerPolicy?
})
};
}

async function retrieveContent(ctx) {
return await ctx.response.text();
}

async function updateContent(ctx) {
ctx.view.content = ctx.content;
let p = new Promise((resolve) => {
ctx.view.$nextTick(() => {
resolve();
});
});
await p;
}

async function parseContent(ctx) {
let maindiv = document.getElementById("maindiv");
let p = new Promise((resolve) => {
parser.parse(maindiv).then(() => {
resolve();
});
});
await p;
ctx.view.notify({
title: ctx.options.done || ctx.view.$t("Loaded")
});
}

function reportError(ctx) {
ctx.view.reportError(ctx.error);
}

function setContentSrc(ctx, { value }) {
let dismiss;
let dismissed = false;
ctx.notify({
title: value.options.doing || ctx.view.$t("Loading..."),
type: "info",
dismissReceiver: (cb) => {
if (dismissed) {
// receiving the callback *after* someone tried to dismiss...
// do it right now.
cb();
} else {
dismiss = cb;
}
}
});
return {
...ctx,
tgt: value.tgt,
options: value.options,
// 'dismiss' is received delayed; pass a forwarder
dismiss: () => {
dismissed = true;
if (dismiss) {
dismiss();
}
}
};
}

function setContent(ctx, { data }) {
return { ...ctx, content: data };
}

function setError(ctx, { data }) {
return { ...ctx, error: data };
}

function setErrorResponse(ctx, { data }) {
return { ...ctx, error: data.response };
}

function setResponse(ctx, { data }) {
return { ...ctx, response: data.response };
}

const machine = createMachine(
{
idle: state(
transition("loadContent", "requesting", reduce(setContentSrc)),
transition("unloadContent", "unloaded", action(disarmWidgets))
),
requesting: invoke(
requestContent,
transition(
"loadContent",
"requesting",
action(dismissNotify),
reduce(setContentSrc)
),
transition(
"unloadContent",
"unloaded",
action(dismissNotify),
action(disarmWidgets)
),
transition(
"done",
"retrieving",
guard(domAcceptable),
reduce(setResponse)
),
transition("done", "error", reduce(setErrorResponse)),
transition("error", "error", reduce(setError))
),
retrieving: invoke(
retrieveContent,
transition(
"loadContent",
"requesting",
action(dismissNotify),
reduce(setContentSrc)
),
transition(
"unloadContent",
"unloaded",
action(dismissNotify),
action(disarmWidgets)
),
transition("done", "disarming", reduce(setContent)),
transition("error", "error", reduce(setError))
),
disarming: state(immediate("updating", action(disarmWidgets))),
updating: invoke(
updateContent,
transition("done", "parsing"),
transition("error", "error", reduce(setError))
),
parsing: invoke(
parseContent,
transition("done", "arming"),
transition("error", "error", reduce(setError))
),
arming: state(
immediate("idle", action(armWidgets), action(dismissNotify))
),
error: invoke(
reportError,
transition("done", "idle", action(dismissNotify))
),
unloaded: state()
},
(ctx) => ({ ...ctx })
);

function createServerUIMachine(initialContext, callback) {
return interpret(machine, callback, initialContext);
}

export { createServerUIMachine };

0 comments on commit 0418711

Please sign in to comment.