@yuneta/gobj-ui 7.25.1 → 7.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ import {describe, it, expect} from "vitest";
2
+ import {set_toolbar_busy} from "./form_busy.js";
3
+
4
+ /* A toolbar double: the two things the helper touches on a button are
5
+ * `disabled` and `dataset`, and on the save button its class list. */
6
+ function button(cls, disabled)
7
+ {
8
+ const classes = new Set(cls.split(" "));
9
+ return {
10
+ disabled: disabled,
11
+ dataset: {},
12
+ classList: {
13
+ toggle: (c, on) => {
14
+ if(on) {
15
+ classes.add(c);
16
+ } else {
17
+ classes.delete(c);
18
+ }
19
+ },
20
+ contains: (c) => classes.has(c),
21
+ },
22
+ };
23
+ }
24
+
25
+ function toolbar()
26
+ {
27
+ const save = button("button-save", false);
28
+ const cancel = button("button-cancel", false);
29
+ const undo = button("button-undo", true); /* disabled on its own */
30
+ const buttons = [save, cancel, undo];
31
+ return {
32
+ save, cancel, undo,
33
+ $container: {
34
+ querySelectorAll: () => buttons,
35
+ querySelector: () => save,
36
+ },
37
+ };
38
+ }
39
+
40
+ describe("set_toolbar_busy", () => {
41
+
42
+ it("busy then free: every button is as it was", () => {
43
+ const tb = toolbar();
44
+ const state = {busy: false};
45
+ expect(set_toolbar_busy(tb.$container, state, true)).toBe(true);
46
+ expect(tb.save.disabled).toBe(true);
47
+ expect(tb.cancel.disabled).toBe(true);
48
+ expect(tb.save.classList.contains("is-loading")).toBe(true);
49
+ expect(set_toolbar_busy(tb.$container, state, false)).toBe(true);
50
+ expect(tb.save.disabled).toBe(false);
51
+ expect(tb.cancel.disabled).toBe(false);
52
+ expect(tb.undo.disabled).toBe(true);
53
+ expect(tb.save.classList.contains("is-loading")).toBe(false);
54
+ });
55
+
56
+ it("busy twice then free: Save and Cancel come back (N9)", () => {
57
+ /* A record with a picked file went busy for the read and busy
58
+ * again for the write; the second pass recorded the buttons the
59
+ * first had disabled as disabled on their own, and the free of a
60
+ * refused write left Save and Cancel dead. */
61
+ const tb = toolbar();
62
+ const state = {busy: false};
63
+ set_toolbar_busy(tb.$container, state, true);
64
+ expect(set_toolbar_busy(tb.$container, state, true)).toBe(false);
65
+ set_toolbar_busy(tb.$container, state, false);
66
+ expect(tb.save.disabled).toBe(false);
67
+ expect(tb.cancel.disabled).toBe(false);
68
+ expect(tb.undo.disabled).toBe(true);
69
+ expect(state.busy).toBe(false);
70
+ });
71
+
72
+ it("free while free touches nothing", () => {
73
+ const tb = toolbar();
74
+ const state = {busy: false};
75
+ expect(set_toolbar_busy(tb.$container, state, false)).toBe(false);
76
+ expect(tb.save.disabled).toBe(false);
77
+ expect(tb.undo.disabled).toBe(true);
78
+ });
79
+
80
+ it("no container, no state: nothing, and says so", () => {
81
+ expect(set_toolbar_busy(null, {busy: false}, true)).toBe(false);
82
+ expect(set_toolbar_busy(toolbar().$container, null, true)).toBe(false);
83
+ });
84
+ });
@@ -0,0 +1,60 @@
1
+ /***********************************************************************
2
+ * form_writes_in_flight.js
3
+ *
4
+ * The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
5
+ * and not yet answered, by their `form_write` serial. A form that
6
+ * waits for its answer (form_waits_for_answer) stays open and
7
+ * busy until the host answers, so the host has to answer EVERY
8
+ * way a write ends -- and the transport closing under a write in
9
+ * flight, or a command that never left because there was no
10
+ * session, are two of them that nobody answered: the form stayed
11
+ * busy for ever (N8 of the 2026-09-22 review).
12
+ *
13
+ * Copyright (c) 2026, ArtGins.
14
+ * All Rights Reserved.
15
+ ***********************************************************************/
16
+
17
+ /************************************************************
18
+ * A write left: remember whose it is
19
+ ************************************************************/
20
+ function track_form_write(in_flight, form_write, topic_name)
21
+ {
22
+ if(!in_flight || !form_write) {
23
+ return;
24
+ }
25
+ in_flight[form_write] = topic_name || "";
26
+ }
27
+
28
+ /************************************************************
29
+ * A write was answered, one way or the other
30
+ ************************************************************/
31
+ function settle_form_write(in_flight, form_write)
32
+ {
33
+ if(!in_flight || !form_write) {
34
+ return;
35
+ }
36
+ delete in_flight[form_write];
37
+ }
38
+
39
+ /************************************************************
40
+ * Nothing in flight can be answered any more: take them all,
41
+ * as [{form_write, topic_name}], and leave the map empty.
42
+ ************************************************************/
43
+ function abandon_form_writes(in_flight)
44
+ {
45
+ const abandoned = [];
46
+ if(!in_flight) {
47
+ return abandoned;
48
+ }
49
+ Object.keys(in_flight).forEach((serial) => {
50
+ abandoned.push({form_write: Number(serial), topic_name: in_flight[serial]});
51
+ delete in_flight[serial];
52
+ });
53
+ return abandoned;
54
+ }
55
+
56
+ export {
57
+ track_form_write,
58
+ settle_form_write,
59
+ abandon_form_writes,
60
+ };
@@ -0,0 +1,45 @@
1
+ import {describe, it, expect} from "vitest";
2
+ import {
3
+ track_form_write,
4
+ settle_form_write,
5
+ abandon_form_writes,
6
+ } from "./form_writes_in_flight.js";
7
+
8
+ describe("form writes in flight", () => {
9
+
10
+ it("a write answered is settled; the others stay", () => {
11
+ const in_flight = {};
12
+ track_form_write(in_flight, 1, "users");
13
+ track_form_write(in_flight, 2, "roles");
14
+ settle_form_write(in_flight, 1);
15
+ expect(Object.keys(in_flight)).toEqual(["2"]);
16
+ });
17
+
18
+ it("the transport closes: every write in flight is abandoned, once (N8)", () => {
19
+ /* The form of each one is answered EV_WRITE_REFUSED by the host
20
+ * and stays open on what was typed; before, it stayed busy for
21
+ * ever, because nobody answered a write whose answer was lost. */
22
+ const in_flight = {};
23
+ track_form_write(in_flight, 3, "users");
24
+ track_form_write(in_flight, 4, "roles");
25
+ const abandoned = abandon_form_writes(in_flight);
26
+ expect(abandoned).toEqual([
27
+ {form_write: 3, topic_name: "users"},
28
+ {form_write: 4, topic_name: "roles"},
29
+ ]);
30
+ expect(abandon_form_writes(in_flight)).toEqual([]);
31
+ });
32
+
33
+ it("a cell edited in place (no serial) is not a form write", () => {
34
+ const in_flight = {};
35
+ track_form_write(in_flight, 0, "users");
36
+ track_form_write(in_flight, undefined, "users");
37
+ expect(abandon_form_writes(in_flight)).toEqual([]);
38
+ });
39
+
40
+ it("no map: nothing to do", () => {
41
+ expect(() => track_form_write(null, 1, "x")).not.toThrow();
42
+ expect(() => settle_form_write(null, 1)).not.toThrow();
43
+ expect(abandon_form_writes(null)).toEqual([]);
44
+ });
45
+ });
@@ -0,0 +1,68 @@
1
+ /***********************************************************************
2
+ * host_drafts.js
3
+ *
4
+ * The topics a schema editor shows as DRAFTS (edited in
5
+ * __system__, not yet saved), told by the host from data. The
6
+ * editor marks a topic when a write of this session lands on
7
+ * it, and that mark lived in the memory of the session only:
8
+ * a reload of the page, a reconnect or a refresh of the model
9
+ * lost the chip, the banner and the export warning while
10
+ * __system__ still differed from the file in use (N13 of the
11
+ * 2026-09-22 review). The host reads that difference from
12
+ * C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
13
+ * hands it here; the editor applies it to its model.
14
+ *
15
+ * Copyright (c) 2026, ArtGins.
16
+ * All Rights Reserved.
17
+ ***********************************************************************/
18
+
19
+ /************************************************************
20
+ * Mark in `written` the topics of `model` the host names as
21
+ * drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
22
+ * topic the model does not hold is ignored. Returns how many were
23
+ * marked.
24
+ ************************************************************/
25
+ function mark_host_drafts(written, model, drafts)
26
+ {
27
+ let marked = 0;
28
+ if(!written || !model || !Array.isArray(model.treedbs) || !drafts) {
29
+ return marked;
30
+ }
31
+ for(let treedb of model.treedbs) {
32
+ const names = drafts[treedb.id];
33
+ if(!Array.isArray(names) || names.length === 0) {
34
+ continue;
35
+ }
36
+ for(let topic of treedb.topics || []) {
37
+ if(names.includes(topic.name)) {
38
+ written[topic.id] = true;
39
+ marked++;
40
+ }
41
+ }
42
+ }
43
+ return marked;
44
+ }
45
+
46
+ /************************************************************
47
+ * What a `saved-schema` answer says, as {treedb_name: [topic names]}:
48
+ * its `data` is one row per treedb (the unnamed form of the command),
49
+ * each carrying `draft_changed` {topic: true}.
50
+ ************************************************************/
51
+ function drafts_of_saved_answer(rows, into)
52
+ {
53
+ const drafts = into || {};
54
+ for(let row of Array.isArray(rows)? rows: []) {
55
+ const treedb_name = row && row.treedb_name;
56
+ const changed = row && row.data && row.data.draft_changed;
57
+ if(!treedb_name || !changed || typeof changed !== "object") {
58
+ continue;
59
+ }
60
+ drafts[treedb_name] = Object.keys(changed).filter((k) => !!changed[k]);
61
+ }
62
+ return drafts;
63
+ }
64
+
65
+ export {
66
+ mark_host_drafts,
67
+ drafts_of_saved_answer,
68
+ };
@@ -0,0 +1,62 @@
1
+ import {describe, it, expect} from "vitest";
2
+ import {mark_host_drafts, drafts_of_saved_answer} from "./host_drafts.js";
3
+
4
+ const model = {
5
+ treedbs: [
6
+ {id: "treedb_a", name: "treedb_a", topics: [
7
+ {id: "treedb_a^users", name: "users"},
8
+ {id: "treedb_a^roles", name: "roles"},
9
+ ]},
10
+ {id: "treedb_b", name: "treedb_b", topics: [
11
+ {id: "treedb_b^users", name: "users"},
12
+ ]},
13
+ ],
14
+ };
15
+
16
+ describe("mark_host_drafts", () => {
17
+
18
+ it("marks the topics the host names, by treedb (N13)", () => {
19
+ /* The mark lived in the memory of one session: after a reload the
20
+ * editor showed no draft while __system__ still differed from the
21
+ * file in use. Rebuilt from the host's data. */
22
+ const written = {};
23
+ const n = mark_host_drafts(written, model, {treedb_a: ["users"]});
24
+ expect(n).toBe(1);
25
+ expect(written).toEqual({"treedb_a^users": true});
26
+ });
27
+
28
+ it("a topic of the same name in another treedb is another topic", () => {
29
+ const written = {};
30
+ mark_host_drafts(written, model, {treedb_b: ["users"]});
31
+ expect(written).toEqual({"treedb_b^users": true});
32
+ });
33
+
34
+ it("keeps what the session wrote, ignores what the model does not hold", () => {
35
+ const written = {"treedb_a^roles": true};
36
+ const n = mark_host_drafts(written, model, {treedb_a: ["nobody"], treedb_z: ["users"]});
37
+ expect(n).toBe(0);
38
+ expect(written).toEqual({"treedb_a^roles": true});
39
+ });
40
+
41
+ it("no model, no drafts: nothing", () => {
42
+ expect(mark_host_drafts({}, null, {treedb_a: ["users"]})).toBe(0);
43
+ expect(mark_host_drafts({}, model, null)).toBe(0);
44
+ });
45
+ });
46
+
47
+ describe("drafts_of_saved_answer", () => {
48
+
49
+ it("reads draft_changed of each treedb row", () => {
50
+ const rows = [
51
+ {treedb_name: "treedb_a", result: 0, data: {draft_changed: {users: true, roles: false}}},
52
+ {treedb_name: "treedb_b", result: 0, data: {draft_changed: {}}},
53
+ {treedb_name: "treedb_c", result: -1, comment: "not open here"},
54
+ ];
55
+ expect(drafts_of_saved_answer(rows)).toEqual({treedb_a: ["users"], treedb_b: []});
56
+ });
57
+
58
+ it("a node older than draft_changed says nothing", () => {
59
+ expect(drafts_of_saved_answer([{treedb_name: "t", data: {can_apply: false}}])).toEqual({});
60
+ expect(drafts_of_saved_answer(null)).toEqual({});
61
+ });
62
+ });