@yuneta/gobj-ui 7.25.9 → 7.25.10
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.
- package/README.md +44 -12
- package/dist/gobj-ui.cjs.js +165 -60
- package/dist/gobj-ui.es.js +165 -60
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +210 -39
- package/src/schema_editor_reload.wiring.test.js +233 -3
- package/src/shell_modals.js +9 -1
- package/src/shell_toast_once.test.js +36 -1
|
@@ -300,7 +300,8 @@ function records_with(change)
|
|
|
300
300
|
* under an open dialog. */
|
|
301
301
|
const WAIT = "the schemas are loading: wait for them";
|
|
302
302
|
const STALE = "the schemas were read again: open the dialog again";
|
|
303
|
-
const KEPT = "cannot read the schemas again: the
|
|
303
|
+
const KEPT = "cannot read the schemas again: the ones shown may be out of date, your next change reads them first";
|
|
304
|
+
const OWED = "the schemas shown may be out of date: they are read again, try again when they are in";
|
|
304
305
|
const NOT_SENT = "the treedb did not describe a write back: the writes after it were not sent";
|
|
305
306
|
|
|
306
307
|
describe("the screen of a reload", () => {
|
|
@@ -616,10 +617,18 @@ describe("a load that fails IN session (fourth review)", () => {
|
|
|
616
617
|
expect(form.closed).toBe(false);
|
|
617
618
|
expect($in(editor, ".SCHEMA_BODY .SCHEMA_COLUMNS")).toBeTruthy();
|
|
618
619
|
|
|
619
|
-
/* The form was built on
|
|
620
|
+
/* The form was built on a model that may be older than the
|
|
621
|
+
* store: its Save would write every field of it. It runs the
|
|
622
|
+
* reload owed instead (fifth review), and the load that lands
|
|
623
|
+
* closes the form. */
|
|
620
624
|
form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
|
|
621
625
|
expect(not_defined()).toEqual([]);
|
|
622
|
-
expect(take("update-node")
|
|
626
|
+
expect(take("update-node")).toEqual([]);
|
|
627
|
+
expect(shown).toEqual([KEPT, OWED]);
|
|
628
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
629
|
+
expect(form.closed).toBe(true);
|
|
630
|
+
expect(shown).toEqual([KEPT, OWED, STALE]);
|
|
631
|
+
expect(editor.priv.reload_on_open).toBe(false);
|
|
623
632
|
expect(errors()).toEqual([]);
|
|
624
633
|
});
|
|
625
634
|
|
|
@@ -785,3 +794,224 @@ describe("every control of the editor's dialogs is named (fourth review)", () =>
|
|
|
785
794
|
expect(errors()).toEqual([]);
|
|
786
795
|
});
|
|
787
796
|
});
|
|
797
|
+
|
|
798
|
+
describe("the export's two views are controls (fifth review)", () => {
|
|
799
|
+
|
|
800
|
+
test("each is a named button that says which one is shown, and switches the text", () => {
|
|
801
|
+
const {editor, remote, host} = build("x1", "db");
|
|
802
|
+
gobj_send_event(editor, "EV_EXPORT", {}, host);
|
|
803
|
+
const $content = modals[modals.length - 1].$content;
|
|
804
|
+
const $tabs = [...$content.querySelectorAll(".SCHEMA_EXPORT_TAB")];
|
|
805
|
+
expect($tabs.map(($b) => [
|
|
806
|
+
$b.tagName.toLowerCase(),
|
|
807
|
+
$b.getAttribute("type"),
|
|
808
|
+
$b.getAttribute("title") !== null,
|
|
809
|
+
$b.getAttribute("data-i18n-title"),
|
|
810
|
+
$b.getAttribute("aria-label") !== null,
|
|
811
|
+
$b.getAttribute("data-i18n-aria-label"),
|
|
812
|
+
$b.getAttribute("aria-pressed"),
|
|
813
|
+
])).toEqual([
|
|
814
|
+
["button", "button", true, "schema as c source", true, "schema as c source", "true"],
|
|
815
|
+
["button", "button", true, "schema as json", true, "schema as json", "false"],
|
|
816
|
+
]);
|
|
817
|
+
|
|
818
|
+
const $text = $content.querySelector(".SCHEMA_EXPORT_TEXT");
|
|
819
|
+
$tabs[1].click();
|
|
820
|
+
expect($text.value.trim().startsWith("{")).toBe(true);
|
|
821
|
+
expect($tabs.map(($b) => $b.getAttribute("aria-pressed"))).toEqual(["false", "true"]);
|
|
822
|
+
$tabs[0].click();
|
|
823
|
+
expect($text.value.trim().startsWith("{")).toBe(false);
|
|
824
|
+
expect(errors()).toEqual([]);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
describe("a Refresh asked while a load is in flight (fifth review)", () => {
|
|
829
|
+
|
|
830
|
+
test("keeps the records of the model shown, not the half the first load got", () => {
|
|
831
|
+
const {editor, remote, host} = build("h1", "db/users");
|
|
832
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
833
|
+
const first = take("nodes");
|
|
834
|
+
answer(editor, remote, first[0], 0, []); /* half a load */
|
|
835
|
+
|
|
836
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
837
|
+
const second = take("nodes");
|
|
838
|
+
expect(second.length).toBe(3);
|
|
839
|
+
drop(editor, remote, host);
|
|
840
|
+
expect(editor.priv.records.treedbs.length).toBe(1);
|
|
841
|
+
expect(editor.priv.records.topics.length).toBe(1);
|
|
842
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
843
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
844
|
+
expect(errors()).toEqual([]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("...and so does one that fails in session", () => {
|
|
848
|
+
const {editor, remote, host} = build("h2", "db/users");
|
|
849
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
850
|
+
const first = take("nodes");
|
|
851
|
+
answer(editor, remote, first[0], 0, []);
|
|
852
|
+
|
|
853
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
854
|
+
const second = take("nodes");
|
|
855
|
+
answer(editor, remote, second[0], -1, null, "deadline");
|
|
856
|
+
answer(editor, remote, second[1], 0, []);
|
|
857
|
+
answer(editor, remote, second[2], 0, []);
|
|
858
|
+
expect(editor.priv.records.cols.length).toBe(1);
|
|
859
|
+
expect(editor.priv.model.treedbs.length).toBe(1);
|
|
860
|
+
expect(errors()).toEqual([]);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
describe("the reload a load refused in session owes (fifth review)", () => {
|
|
865
|
+
|
|
866
|
+
function refused_load(name, subpath)
|
|
867
|
+
{
|
|
868
|
+
const built = build(name, subpath);
|
|
869
|
+
gobj_send_event(built.editor, "EV_REFRESH", {}, built.host);
|
|
870
|
+
const asked = take("nodes");
|
|
871
|
+
answer(built.editor, built.remote, asked[0], -1, null, "deadline");
|
|
872
|
+
answer(built.editor, built.remote, asked[1], 0, RECORDS[asked[1].kw.topic_name]);
|
|
873
|
+
answer(built.editor, built.remote, asked[2], 0, RECORDS[asked[2].kw.topic_name]);
|
|
874
|
+
expect(built.editor.priv.reload_on_open).toBe(true);
|
|
875
|
+
expect(shown).toEqual([KEPT]);
|
|
876
|
+
return built;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
test("RUNS on the next edit, which is refused and said: nothing is opened on the old model", () => {
|
|
880
|
+
const {editor, remote, host} = refused_load("o1", "db/users");
|
|
881
|
+
const before = modals.length;
|
|
882
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
883
|
+
expect(modals.length).toBe(before);
|
|
884
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
885
|
+
expect(shown).toEqual([KEPT, OWED]);
|
|
886
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
887
|
+
expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
|
|
888
|
+
expect(editor.priv.reload_on_open).toBe(false);
|
|
889
|
+
|
|
890
|
+
/* Once: the next edit opens its form. */
|
|
891
|
+
gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
|
|
892
|
+
expect(modals.length).toBe(before + 1);
|
|
893
|
+
expect(take("nodes")).toEqual([]);
|
|
894
|
+
expect(errors()).toEqual([]);
|
|
895
|
+
expect(not_defined()).toEqual([]);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test.each([
|
|
899
|
+
["EV_ADD_COLUMN", {}],
|
|
900
|
+
["EV_DUPLICATE_COLUMN", {col: "id"}],
|
|
901
|
+
["EV_DELETE_COLUMN", {col: "id"}],
|
|
902
|
+
["EV_MOVE_COLUMN", {from: 0, to: 1}],
|
|
903
|
+
["EV_EXPORT", {}],
|
|
904
|
+
["EV_IMPORT", {}],
|
|
905
|
+
["EV_VALIDATE", {}],
|
|
906
|
+
["EV_CONFIRMED", {what: "column", topic: "users", col: "id"}],
|
|
907
|
+
])("%s runs it too, and writes nothing", (event, kw) => {
|
|
908
|
+
const {editor, remote, host} = refused_load(`o2_${event}`, "db/users");
|
|
909
|
+
kw = Object.assign({model_gen: editor.priv.model_gen}, kw);
|
|
910
|
+
const before = modals.length;
|
|
911
|
+
gobj_send_event(editor, event, kw, host);
|
|
912
|
+
expect(modals.length).toBe(before);
|
|
913
|
+
expect(take("update-node")).toEqual([]);
|
|
914
|
+
expect(take("delete-node")).toEqual([]);
|
|
915
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
916
|
+
expect(errors()).toEqual([]);
|
|
917
|
+
expect(not_defined()).toEqual([]);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("a move goes where it was going, and reads the schemas there", () => {
|
|
921
|
+
const {editor, remote, host} = refused_load("o3", "db/users");
|
|
922
|
+
gobj_send_event(editor, "EV_BACK", {}, host);
|
|
923
|
+
expect(gobj_current_state(editor)).toBe("ST_LOADING");
|
|
924
|
+
expect(shown).toEqual([KEPT]);
|
|
925
|
+
expect(answer_the_load(editor, remote)).toBe(3);
|
|
926
|
+
expect(gobj_current_state(editor)).toBe("ST_TOPICS");
|
|
927
|
+
expect(editor.priv.reload_on_open).toBe(false);
|
|
928
|
+
expect(errors()).toEqual([]);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("keeps what this session wrote", () => {
|
|
932
|
+
const {editor, remote, host} = build("o4", "db/users");
|
|
933
|
+
const w = start_a_write(editor, host, "name");
|
|
934
|
+
answer(editor, remote, w, 0, {id: "db.users.name", value: "name", order: 2,
|
|
935
|
+
type: "string", topics: ["topics^db.users^cols"]});
|
|
936
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
937
|
+
editor.priv.reload_on_open = true; /* as a refused load leaves it */
|
|
938
|
+
gobj_send_event(editor, "EV_ADD_COLUMN", {}, host);
|
|
939
|
+
answer_the_load(editor, remote);
|
|
940
|
+
expect(editor.priv.written).toEqual({"db.users": true});
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
describe("the import plan (fifth review)", () => {
|
|
945
|
+
|
|
946
|
+
const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
|
|
947
|
+
{id: "id", type: "string", flag: ["persistent", "required"]},
|
|
948
|
+
{id: "email", type: "string"}
|
|
949
|
+
]}]});
|
|
950
|
+
|
|
951
|
+
test("a load that could not be sent keeps it: the model it was planned on stays", () => {
|
|
952
|
+
const {editor, remote, host} = build("i1", "db");
|
|
953
|
+
gobj_send_event(editor, "EV_IMPORT", {}, host);
|
|
954
|
+
const dialog = modals[modals.length - 1];
|
|
955
|
+
gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
|
|
956
|
+
model_gen: editor.priv.model_gen}, editor);
|
|
957
|
+
const plan = editor.priv.import_plan;
|
|
958
|
+
expect(plan.writes.length).toBeGreaterThan(0);
|
|
959
|
+
|
|
960
|
+
gobj_change_state(remote, "ST_DISCONNECTED");
|
|
961
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
962
|
+
expect(editor.priv.import_plan).toBe(plan);
|
|
963
|
+
expect(dialog.$content.querySelector(".SCHEMA_IMPORT_RUN").disabled).toBe(false);
|
|
964
|
+
expect(errors()).toEqual([]);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
test("a Yes that finds it gone is a warning, and said -- not an ERROR", () => {
|
|
968
|
+
const {editor, remote, host} = build("i2", "db");
|
|
969
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "import",
|
|
970
|
+
model_gen: editor.priv.model_gen}, editor);
|
|
971
|
+
expect(errors()).toEqual([]);
|
|
972
|
+
expect(logged.some((l) => l.level === "warning" && /import plan is gone/.test(l.msg))).toBe(true);
|
|
973
|
+
expect(shown).toEqual(["the import plan is gone: preview it again"]);
|
|
974
|
+
expect(take("update-node")).toEqual([]);
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
describe("a confirmation answered where no treedb is open (fifth review)", () => {
|
|
979
|
+
|
|
980
|
+
test("after a reload that landed on zero treedbs: refused as stale, not NOT DEFINED", () => {
|
|
981
|
+
const {editor, remote, host} = build("e1", "db/users");
|
|
982
|
+
const kw = {what: "column", topic: "users", col: "id", model_gen: editor.priv.model_gen};
|
|
983
|
+
gobj_send_event(editor, "EV_REFRESH", {}, host);
|
|
984
|
+
answer_the_load_with(editor, remote, records_with((r) => {
|
|
985
|
+
r.treedbs = [];
|
|
986
|
+
r.topics = [];
|
|
987
|
+
r.cols = [];
|
|
988
|
+
}));
|
|
989
|
+
expect(gobj_current_state(editor)).toBe("ST_EMPTY");
|
|
990
|
+
gobj_send_event(editor, "EV_CONFIRMED", kw, editor);
|
|
991
|
+
expect(not_defined()).toEqual([]);
|
|
992
|
+
expect(errors()).toEqual([]);
|
|
993
|
+
expect(shown).toEqual([STALE]);
|
|
994
|
+
expect(take("delete-node")).toEqual([]);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
test("with no model at all (ST_IDLE): refused and said", () => {
|
|
998
|
+
const remote = gobj_create_service("e2_remote", "C_TEST_REMOTE", {}, yuno);
|
|
999
|
+
gobj_change_state(remote, "ST_SESSION");
|
|
1000
|
+
const host = gobj_create("e2_host", "C_TEST_EDITOR_HOST", {}, yuno);
|
|
1001
|
+
const editor = gobj_create("e2_editor", "C_YUI_SCHEMA_EDITOR", {
|
|
1002
|
+
gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
|
|
1003
|
+
}, host);
|
|
1004
|
+
gobj_start(editor);
|
|
1005
|
+
for(const c of take("nodes")) {
|
|
1006
|
+
answer(editor, remote, c, -1, null, "deadline");
|
|
1007
|
+
}
|
|
1008
|
+
expect(gobj_current_state(editor)).toBe("ST_IDLE");
|
|
1009
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id",
|
|
1010
|
+
model_gen: editor.priv.model_gen - 1}, editor);
|
|
1011
|
+
gobj_send_event(editor, "EV_CONFIRMED", {what: "column", topic: "users", col: "id",
|
|
1012
|
+
model_gen: editor.priv.model_gen}, editor);
|
|
1013
|
+
expect(not_defined()).toEqual([]);
|
|
1014
|
+
expect(errors()).toEqual([]);
|
|
1015
|
+
expect(shown).toEqual([STALE, STALE]);
|
|
1016
|
+
});
|
|
1017
|
+
});
|
package/src/shell_modals.js
CHANGED
|
@@ -278,7 +278,8 @@ export function yui_shell_show_modal(shell, content, opts)
|
|
|
278
278
|
let modal_children;
|
|
279
279
|
if(dialog) {
|
|
280
280
|
let header = ["div", {class: "MODAL_HEADER yui-dialog-header"}, [
|
|
281
|
-
["button", {class: "MODAL_BACK yui-dialog-back", type: "button",
|
|
281
|
+
["button", {class: "MODAL_BACK yui-dialog-back", type: "button", title: t("back"), "data-i18n-title": "back",
|
|
282
|
+
"aria-label": t("back"), "data-i18n-aria-label": "back"},
|
|
282
283
|
[["i", {class: "yi-arrow-left"}]]],
|
|
283
284
|
["span", {class: "MODAL_TITLE yui-dialog-title"},
|
|
284
285
|
(title_prefix ? [["span", {class: "MODAL_TITLE_PREFIX"}, title_prefix]] : [])
|
|
@@ -504,8 +505,15 @@ function build_dialog(shell, message, buttons, opts)
|
|
|
504
505
|
}
|
|
505
506
|
let btn_attrs = {class: cls, type: "button",
|
|
506
507
|
"data-modal-button-value": b.value};
|
|
508
|
+
/* Named by its own label key: its text is its name for the eye,
|
|
509
|
+
* and the title and aria-label say the same to the pointer and
|
|
510
|
+
* to a reader, re-translated with it. */
|
|
507
511
|
if(typeof b.label === "string") {
|
|
508
512
|
btn_attrs.i18n = b.label;
|
|
513
|
+
btn_attrs.title = t(b.label);
|
|
514
|
+
btn_attrs["data-i18n-title"] = b.label;
|
|
515
|
+
btn_attrs["aria-label"] = t(b.label);
|
|
516
|
+
btn_attrs["data-i18n-aria-label"] = b.label;
|
|
509
517
|
}
|
|
510
518
|
return ["button", btn_attrs, b.label];
|
|
511
519
|
});
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* And every ✕ this module draws is a control with a NAME: a
|
|
15
15
|
* `title` and an `aria-label`, both translatable (fourth
|
|
16
|
-
* independent review: TOAST_CLOSE had the label only).
|
|
16
|
+
* independent review: TOAST_CLOSE had the label only). So is
|
|
17
|
+
* every other button of a dialog (fifth: the dialog's back arrow
|
|
18
|
+
* had no title, and the Yes/No of every confirmation had neither).
|
|
17
19
|
*
|
|
18
20
|
* Copyright (c) 2026, ArtGins.
|
|
19
21
|
* All Rights Reserved.
|
|
@@ -165,3 +167,36 @@ describe("every close control is named", () => {
|
|
|
165
167
|
}
|
|
166
168
|
});
|
|
167
169
|
});
|
|
170
|
+
|
|
171
|
+
describe("every other button of a dialog is named (fifth review)", () => {
|
|
172
|
+
|
|
173
|
+
function named($b)
|
|
174
|
+
{
|
|
175
|
+
return {
|
|
176
|
+
title: $b.getAttribute("title") !== null,
|
|
177
|
+
title_key: $b.getAttribute("data-i18n-title"),
|
|
178
|
+
label: $b.getAttribute("aria-label") !== null,
|
|
179
|
+
label_key: $b.getAttribute("data-i18n-aria-label"),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test("the back arrow of an adaptive dialog", () => {
|
|
184
|
+
shell.priv.layers.modal = document.createElement("div");
|
|
185
|
+
yui_shell_show_modal(shell, document.createElement("div"), {dialog: true, title: "x"});
|
|
186
|
+
const $back = shell.priv.layers.modal.querySelector(".MODAL_BACK");
|
|
187
|
+
expect(named($back)).toEqual(
|
|
188
|
+
{title: true, title_key: "back", label: true, label_key: "back"});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("the answers of a confirmation, named by their own label key", () => {
|
|
192
|
+
shell.priv.layers.modal = document.createElement("div");
|
|
193
|
+
yui_shell_confirm_danger(shell, "delete this column?",
|
|
194
|
+
{confirm_label: "delete", cancel_label: "cancel"});
|
|
195
|
+
const $answers = shell.priv.layers.modal.querySelectorAll(".CONFIRM_BTN");
|
|
196
|
+
expect($answers.length).toBe(2);
|
|
197
|
+
expect([...$answers].map(named)).toEqual([
|
|
198
|
+
{title: true, title_key: "delete", label: true, label_key: "delete"},
|
|
199
|
+
{title: true, title_key: "cancel", label: true, label_key: "cancel"},
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
});
|