@yuneta/gobj-ui 7.25.14 → 7.25.16

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.
Files changed (35) hide show
  1. package/README.md +100 -4
  2. package/dist/gobj-ui.cjs.js +406 -94
  3. package/dist/gobj-ui.es.js +406 -94
  4. package/package.json +2 -2
  5. package/src/c_g6_nodes_tree.js +79 -15
  6. package/src/c_yui_schema_editor.js +343 -66
  7. package/src/c_yui_treedb_graph.js +53 -6
  8. package/src/c_yui_treedb_topic_with_form.js +10 -10
  9. package/src/c_yui_treedb_topics.js +7 -7
  10. package/src/form_busy.js +2 -3
  11. package/src/form_busy.test.js +1 -1
  12. package/src/form_time_value.js +1 -1
  13. package/src/form_time_value.test.js +1 -1
  14. package/src/form_writes_in_flight.js +1 -2
  15. package/src/form_writes_in_flight.test.js +1 -1
  16. package/src/g6_graphs_write_owed.wiring.test.js +200 -0
  17. package/src/g6_nodes_tree_fsm.test.js +33 -0
  18. package/src/graph_save_plan.js +28 -0
  19. package/src/graph_save_plan.test.js +36 -2
  20. package/src/host_drafts.js +2 -3
  21. package/src/host_drafts.test.js +2 -2
  22. package/src/schema_editor_drafts.wiring.test.js +2 -2
  23. package/src/schema_editor_reload.wiring.test.js +329 -49
  24. package/src/schema_editor_transport.wiring.test.js +2 -2
  25. package/src/schema_version_bump.test.js +1 -1
  26. package/src/shell_modals.js +31 -11
  27. package/src/shell_toast_once.test.js +73 -10
  28. package/src/table_cell_text.js +1 -1
  29. package/src/table_cell_text.test.js +1 -1
  30. package/src/treedb_graph_writes.wiring.test.js +208 -0
  31. package/src/treedb_topics_transport.wiring.test.js +2 -2
  32. package/src/yui_install.js +14 -3
  33. package/src/yui_install.test.js +23 -0
  34. package/src/yui_row_search.test.js +1 -1
  35. package/src/yui_table_select.test.js +1 -1
@@ -1,37 +1,35 @@
1
1
  /***********************************************************************
2
2
  * schema_editor_reload.wiring.test.js
3
3
  *
4
- * C_YUI_SCHEMA_EDITOR while it RELOADS (the third independent
5
- * review, 2026-09-23). A reload entered ST_LOADING and drew
6
- * nothing: the screen it replaced stayed up and clickable, and
7
- * ST_LOADING declares none of its actions, so a click on a card,
8
- * on Back, on the drawing -- or the Save of a column form left
9
- * open -- answered "Event NOT DEFINED in state ST_LOADING" and
4
+ * C_YUI_SCHEMA_EDITOR while it RELOADS. A reload entered
5
+ * ST_LOADING and drew nothing: the screen it replaced stayed up and
6
+ * clickable, and ST_LOADING declares none of its actions, so a click
7
+ * on a card, on Back, on the drawing -- or the Save of a column form
8
+ * left open -- answered "Event NOT DEFINED in state ST_LOADING" and
10
9
  * the click, or the edit, was lost.
11
10
  *
12
- * And the lows of the same review: a position left over from a
13
- * load that could not leave, a drop during a WRITE that dropped
14
- * the host's position (a drop during a load applies it), the
15
- * drawing of a treedb that is gone drawn over the old screen, and
16
- * the reload of a late write that forgot this session's marks and
17
- * never told the host a write had landed.
11
+ * And what a reload left behind: a position left over from a load
12
+ * that could not leave, a drop during a WRITE that dropped the
13
+ * host's position (a drop during a load applies it), the drawing of
14
+ * a treedb that is gone drawn over the old screen, the reload of a
15
+ * late write that forgot this session's marks and never told the
16
+ * host a write had landed, a dialog opened on the model it replaced
17
+ * (its Save wrote the old record over the newer one), a load refused
18
+ * IN session that blanked the model, a late write whose list_dict
19
+ * fkey marked nothing, a write answered with no record that owed a
20
+ * second reload and dropped the rest of its queue in silence, and a
21
+ * toolbar gated on the state instead of on the treedb being there.
18
22
  *
19
- * The fourth independent review (same day) found what a reload
20
- * still left behind: a dialog opened on the model it replaced
21
- * (its Save wrote the old record over the newer one), a load
22
- * refused IN session that blanked the model, a late write whose
23
- * list_dict fkey marked nothing, a write answered with no record
24
- * that owed a second reload and dropped the rest of its queue in
25
- * silence, and a toolbar gated on the state instead of on the
26
- * treedb being there.
23
+ * And where the owed reload lingered: a load that LANDED by another
24
+ * road (the operator's Refresh, a late write) left it owed, so the
25
+ * next edit was refused and read the store again; a move sent by the
26
+ * host did not run it; a load that failed forgot the import plan of
27
+ * a dialog it left up; and the export's two views switched in a DOM
28
+ * handler, out of the machine.
27
29
  *
28
- * The sixth independent review (same day) found where the owed
29
- * reload still lingered: a load that LANDED by another road (the
30
- * operator's Refresh, a late write) left it owed, so the next
31
- * edit was refused and read the store again; a move sent by the
32
- * host did not run it; a load that failed forgot the import plan
33
- * of a dialog it left up; and the export's two views switched in
34
- * a DOM handler, out of the machine.
30
+ * And a dialog that outlives its screen through a move sent by the
31
+ * host: its Save answered "Event NOT DEFINED", and a confirmation
32
+ * answered after a move to another treedb deleted there.
35
33
  *
36
34
  * Driven through the FSM on a document double, with a fake
37
35
  * transport whose state is the one the library reads.
@@ -63,8 +61,10 @@ vi.mock("./shell_modals.js", () => ({
63
61
  }};
64
62
  },
65
63
  yui_shell_confirm_danger: (shell, message, opts) => {
66
- confirms.push({message, opts});
67
- return new Promise(() => {});
64
+ /* Answered by the test that needs it (`resolve`), or never. */
65
+ return new Promise((resolve) => {
66
+ confirms.push({message, opts, resolve});
67
+ });
68
68
  },
69
69
  }));
70
70
 
@@ -316,6 +316,8 @@ const WAIT = "the schemas are loading: wait for them";
316
316
  const STALE = "the schemas were read again: open the dialog again";
317
317
  const KEPT = "cannot read the schemas again: the ones shown may be out of date, your next change reads them first";
318
318
  const OWED = "the schemas shown may be out of date: they are read again, try again when they are in";
319
+ const OWED_FORM = "the schemas shown may be out of date: they are read again, and the form opens again on them with your changes";
320
+ const GONE = "the schemas were read again and what the form was editing is not there any more";
319
321
  const NOT_SENT = "the treedb did not describe a write back: the writes after it were not sent";
320
322
 
321
323
  describe("the screen of a reload", () => {
@@ -530,7 +532,7 @@ describe("a write given up that was DONE", () => {
530
532
  });
531
533
  });
532
534
 
533
- describe("a dialog built on the model a reload replaced (fourth review)", () => {
535
+ describe("a dialog built on the model a reload replaced", () => {
534
536
 
535
537
  test("an edit form: the load that lands closes it, and its Save writes nothing", () => {
536
538
  const {editor, remote, host} = build("s1", "db/users");
@@ -588,7 +590,7 @@ describe("a dialog built on the model a reload replaced (fourth review)", () =>
588
590
  r.cols.push({id: "db.users.email", value: "email", order: 2, type: "integer",
589
591
  topics: ["topics^db.users^cols"]});
590
592
  }));
591
- /* Forgotten when the load LANDS, not when it leaves (sixth review). */
593
+ /* Forgotten when the load LANDS, not when it leaves. */
592
594
  expect(editor.priv.import_plan).toBe(null);
593
595
  expect(dialog.closed).toBe(true);
594
596
 
@@ -612,12 +614,13 @@ describe("a dialog built on the model a reload replaced (fourth review)", () =>
612
614
  });
613
615
  });
614
616
 
615
- describe("a load that fails IN session (fourth review)", () => {
617
+ describe("a load that fails IN session", () => {
616
618
 
617
619
  test("keeps the model, the screen and the open form, says so, and owes the reload", () => {
618
620
  const {editor, remote, host} = build("b1", "db/users");
619
621
  gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
620
622
  const form = modals[modals.length - 1];
623
+ form.$content.querySelector('[data-name="header"]').value = "Identifier";
621
624
  gobj_send_event(editor, "EV_REFRESH", {}, host);
622
625
  const asked = take("nodes");
623
626
  answer(editor, remote, asked[0], -1, null, "deadline");
@@ -634,16 +637,28 @@ describe("a load that fails IN session (fourth review)", () => {
634
637
 
635
638
  /* The form was built on a model that may be older than the
636
639
  * store: its Save would write every field of it. It runs the
637
- * reload owed instead (fifth review), and the load that lands
638
- * closes the form. */
640
+ * reload owed instead. The load that lands closes the form,
641
+ * and opens it again ON THE SCHEMAS JUST READ with what the
642
+ * operator changed: closed alone, what was typed was lost. */
639
643
  form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
640
644
  expect(not_defined()).toEqual([]);
641
645
  expect(take("update-node")).toEqual([]);
642
- expect(shown).toEqual([KEPT, OWED]);
646
+ expect(shown).toEqual([KEPT, OWED_FORM]);
647
+ const before = modals.length;
643
648
  expect(answer_the_load(editor, remote)).toBe(3);
644
649
  expect(form.closed).toBe(true);
645
- expect(shown).toEqual([KEPT, OWED, STALE]);
650
+ expect(modals.length).toBe(before + 1);
651
+ const again = modals[modals.length - 1];
652
+ expect(again.closed).toBe(false);
653
+ expect(again.$content.querySelector('[data-name="header"]').value).toBe("Identifier");
654
+ expect(shown).toEqual([KEPT, OWED_FORM]);
646
655
  expect(editor.priv.reload_on_open).toBe(false);
656
+
657
+ /* Its Save writes, on the record just read. */
658
+ again.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
659
+ const [write] = take("update-node");
660
+ expect(write.kw.record.id).toBe("db.users.id");
661
+ expect(write.kw.record.header).toBe("Identifier");
647
662
  expect(errors()).toEqual([]);
648
663
  });
649
664
 
@@ -675,7 +690,7 @@ describe("a load that fails IN session (fourth review)", () => {
675
690
  });
676
691
  });
677
692
 
678
- describe("a late write, in the shape the store answers (fourth review)", () => {
693
+ describe("a late write, in the shape the store answers", () => {
679
694
 
680
695
  test("a list_dict fkey marks the topic the column belongs to", () => {
681
696
  const {editor, remote, host} = build("c1", "db/users");
@@ -692,7 +707,7 @@ describe("a late write, in the shape the store answers (fourth review)", () => {
692
707
  });
693
708
  });
694
709
 
695
- describe("a write answered with no record (fourth review)", () => {
710
+ describe("a write answered with no record", () => {
696
711
 
697
712
  test("the reload it asks is the one owed: the next write does not load again", () => {
698
713
  const {editor, remote, host} = build("n1", "db/users");
@@ -736,7 +751,7 @@ describe("a write answered with no record (fourth review)", () => {
736
751
  });
737
752
  });
738
753
 
739
- describe("the toolbar of a treedb that is not there (fourth review)", () => {
754
+ describe("the toolbar of a treedb that is not there", () => {
740
755
 
741
756
  test("offers Back and Refresh, and nothing that needs the treedb", () => {
742
757
  const {editor, remote, host} = build("t1", "db");
@@ -773,7 +788,7 @@ describe("the toolbar of a treedb that is not there (fourth review)", () => {
773
788
  });
774
789
  });
775
790
 
776
- describe("every control of the editor's dialogs is named (fourth review)", () => {
791
+ describe("every control of the editor's dialogs is named", () => {
777
792
 
778
793
  /* A control a wrapping <label> names is the one shape that needs no
779
794
  * attribute (CLAUDE.md, "title + aria-label"). */
@@ -810,7 +825,7 @@ describe("every control of the editor's dialogs is named (fourth review)", () =>
810
825
  });
811
826
  });
812
827
 
813
- describe("the export's two views are controls (fifth review)", () => {
828
+ describe("the export's two views are controls", () => {
814
829
 
815
830
  test("each is a named button that says which one is shown, and switches the text", () => {
816
831
  const {editor, remote, host} = build("x1", "db");
@@ -840,7 +855,7 @@ describe("the export's two views are controls (fifth review)", () => {
840
855
  });
841
856
  });
842
857
 
843
- describe("a Refresh asked while a load is in flight (fifth review)", () => {
858
+ describe("a Refresh asked while a load is in flight", () => {
844
859
 
845
860
  test("keeps the records of the model shown, not the half the first load got", () => {
846
861
  const {editor, remote, host} = build("h1", "db/users");
@@ -876,7 +891,7 @@ describe("a Refresh asked while a load is in flight (fifth review)", () => {
876
891
  });
877
892
  });
878
893
 
879
- describe("the reload a load refused in session owes (fifth review)", () => {
894
+ describe("the reload a load refused in session owes", () => {
880
895
 
881
896
  function refused_load(name, subpath)
882
897
  {
@@ -956,7 +971,7 @@ describe("the reload a load refused in session owes (fifth review)", () => {
956
971
  });
957
972
  });
958
973
 
959
- describe("the import plan (fifth review)", () => {
974
+ describe("the import plan", () => {
960
975
 
961
976
  const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
962
977
  {id: "id", type: "string", flag: ["persistent", "required"]},
@@ -990,7 +1005,7 @@ describe("the import plan (fifth review)", () => {
990
1005
  });
991
1006
  });
992
1007
 
993
- describe("a confirmation answered where no treedb is open (fifth review)", () => {
1008
+ describe("a confirmation answered where no treedb is open", () => {
994
1009
 
995
1010
  test("after a reload that landed on zero treedbs: refused as stale, not NOT DEFINED", () => {
996
1011
  const {editor, remote, host} = build("e1", "db/users");
@@ -1031,7 +1046,7 @@ describe("a confirmation answered where no treedb is open (fifth review)", () =>
1031
1046
  });
1032
1047
  });
1033
1048
 
1034
- describe("the answers of the editor's confirmations are i18n keys (fifth review, live)", () => {
1049
+ describe("the answers of the editor's confirmations are i18n keys", () => {
1035
1050
 
1036
1051
  /* shell_modals names each answer by its label; its defaults ("Delete",
1037
1052
  * "Cancel") are not lower-case, so no validated locale holds them and
@@ -1046,7 +1061,7 @@ describe("the answers of the editor's confirmations are i18n keys (fifth review,
1046
1061
  });
1047
1062
  });
1048
1063
 
1049
- describe("the owed reload, settled wherever a load lands (sixth review)", () => {
1064
+ describe("the owed reload, settled wherever a load lands", () => {
1050
1065
 
1051
1066
  function refused_load(name, subpath)
1052
1067
  {
@@ -1125,7 +1140,7 @@ describe("the owed reload, settled wherever a load lands (sixth review)", () =>
1125
1140
  });
1126
1141
  });
1127
1142
 
1128
- describe("the import plan outlives a load that fails (sixth review)", () => {
1143
+ describe("the import plan outlives a load that fails", () => {
1129
1144
 
1130
1145
  const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
1131
1146
  {id: "id", type: "string", flag: ["persistent", "required"]},
@@ -1173,7 +1188,7 @@ describe("the import plan outlives a load that fails (sixth review)", () => {
1173
1188
  });
1174
1189
  });
1175
1190
 
1176
- describe("the export's two views go through the machine (sixth review)", () => {
1191
+ describe("the export's two views go through the machine", () => {
1177
1192
 
1178
1193
  test("a click on a view is an event, and the action switches the text", () => {
1179
1194
  const {editor, remote, host} = build("y1", "db");
@@ -1221,3 +1236,268 @@ describe("the export's two views go through the machine (sixth review)", () => {
1221
1236
  expect(errors().length).toBe(2);
1222
1237
  });
1223
1238
  });
1239
+
1240
+ describe("a move sent by the host while a dialog is up", () => {
1241
+
1242
+ /* C_YUI_SHELL keeps the overlays open on a move that changes only
1243
+ * the subpath (a url typed in, a link). The dialog of the screen the
1244
+ * editor left sends what only that screen declares. */
1245
+ const MOVED = "the view moved: open the dialog again";
1246
+
1247
+ const IMPORT = JSON.stringify({id: "db", topics: [{id: "users", pkey: "id", cols: [
1248
+ {id: "id", type: "string", flag: ["persistent", "required"]},
1249
+ {id: "email", type: "string"}
1250
+ ]}]});
1251
+
1252
+ function settle()
1253
+ {
1254
+ return new Promise((resolve) => {
1255
+ setTimeout(resolve, 0);
1256
+ });
1257
+ }
1258
+
1259
+ test("a column form: closed and said, not left to answer NOT DEFINED", () => {
1260
+ const {editor, remote, host} = build("mv1", "db/users");
1261
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1262
+ const form = modals[modals.length - 1];
1263
+ expect(form.closed).toBe(false);
1264
+
1265
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1266
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1267
+ expect(form.closed).toBe(true);
1268
+ expect(editor.priv.dialog).toBe(null);
1269
+ expect(shown).toEqual([MOVED]);
1270
+ expect(not_defined()).toEqual([]);
1271
+ expect(errors()).toEqual([]);
1272
+ expect(commands).toEqual([]);
1273
+ });
1274
+
1275
+ test("the import dialog: closed, and its plan forgotten", () => {
1276
+ const {editor, remote, host} = build("mv2", "db");
1277
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1278
+ const dialog = modals[modals.length - 1];
1279
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1280
+ model_gen: editor.priv.model_gen}, editor);
1281
+ expect(editor.priv.import_plan).not.toBe(null);
1282
+
1283
+ gobj_send_event(editor, "EV_SHOW", {subpath: ""}, host);
1284
+ expect(gobj_current_state(editor)).toBe("ST_TREEDBS");
1285
+ expect(dialog.closed).toBe(true);
1286
+ expect(editor.priv.import_plan).toBe(null);
1287
+ expect(shown).toEqual([MOVED]);
1288
+ expect(not_defined()).toEqual([]);
1289
+ expect(errors()).toEqual([]);
1290
+ });
1291
+
1292
+ test("a move that arrives during a load that fails: the same, when it is applied", () => {
1293
+ const {editor, remote, host} = build("mv3", "db/users");
1294
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1295
+ const form = modals[modals.length - 1];
1296
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1297
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1298
+ expect(form.closed).toBe(false);
1299
+ for(const c of take("nodes")) {
1300
+ answer(editor, remote, c, -1, null, "deadline");
1301
+ }
1302
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1303
+ expect(form.closed).toBe(true);
1304
+ expect(shown).toEqual([KEPT, MOVED]);
1305
+ expect(not_defined()).toEqual([]);
1306
+ });
1307
+
1308
+ test("the same position sent again leaves the dialog alone", () => {
1309
+ const {editor, remote, host} = build("mv4", "db/users");
1310
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1311
+ const form = modals[modals.length - 1];
1312
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
1313
+ expect(form.closed).toBe(false);
1314
+ expect(shown).toEqual([]);
1315
+ });
1316
+
1317
+ test("the export and the check read nothing of the screen: they stay up", () => {
1318
+ const {editor, remote, host} = build("mv5", "db");
1319
+ gobj_send_event(editor, "EV_EXPORT", {}, host);
1320
+ const exported = modals[modals.length - 1];
1321
+ gobj_send_event(editor, "EV_SHOW", {subpath: ""}, host);
1322
+ expect(exported.closed).toBe(false);
1323
+ exported.$content.querySelectorAll(".SCHEMA_EXPORT_TAB")[1].click();
1324
+ expect(exported.$content.querySelector(".SCHEMA_EXPORT_TEXT").value.trim()
1325
+ .startsWith("{")).toBe(true);
1326
+
1327
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db"}, host);
1328
+ gobj_send_event(editor, "EV_VALIDATE", {}, host);
1329
+ const report = modals[modals.length - 1];
1330
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db/users"}, host);
1331
+ expect(report.closed).toBe(false);
1332
+ expect(shown).toEqual([]);
1333
+ expect(not_defined()).toEqual([]);
1334
+ expect(errors()).toEqual([]);
1335
+ });
1336
+
1337
+ test("a confirmation answered after a move to another treedb deletes nothing", async () => {
1338
+ const {editor, remote, host} = build("mv6", "db/users");
1339
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1340
+ answer_the_load_with(editor, remote, records_with((r) => {
1341
+ r.treedbs.push({id: "db2", schema_version: 1});
1342
+ r.topics.push({id: "db2.users", value: "users", order: 1, pkey: "id",
1343
+ topic_version: 1, treedbs: ["treedbs^db2^topics"]});
1344
+ r.cols.push({id: "db2.users.id", value: "id", order: 1, type: "string",
1345
+ flag: ["persistent", "required"], topics: ["topics^db2.users^cols"]});
1346
+ }));
1347
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1348
+
1349
+ gobj_send_event(editor, "EV_DELETE_COLUMN", {col: "id"}, host);
1350
+ expect(confirms.length).toBe(1);
1351
+ gobj_send_event(editor, "EV_SHOW", {subpath: "db2/users"}, host);
1352
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1353
+ confirms[0].resolve(true);
1354
+ await settle();
1355
+
1356
+ expect(take("delete-node")).toEqual([]);
1357
+ expect(commands).toEqual([]);
1358
+ expect(shown).toEqual([MOVED]);
1359
+ expect(errors()).toEqual([]);
1360
+ });
1361
+
1362
+ test("a confirmation answered on the screen it was asked from still runs", async () => {
1363
+ const {editor, remote, host} = build("mv7", "db/users");
1364
+ gobj_send_event(editor, "EV_DELETE_COLUMN", {col: "id"}, host);
1365
+ confirms[0].resolve(true);
1366
+ await settle();
1367
+ expect(take("delete-node").length).toBe(1);
1368
+ expect(shown).toEqual([]);
1369
+ });
1370
+ });
1371
+
1372
+ describe("the form opened again after the owed reload", () => {
1373
+
1374
+ function owing(name, subpath)
1375
+ {
1376
+ const built = build(name, subpath);
1377
+ built.editor.priv.reload_on_open = true; /* as a load refused in session leaves it */
1378
+ return built;
1379
+ }
1380
+
1381
+ test("only what was CHANGED is carried: a field the store changed meanwhile shows the store's", () => {
1382
+ const {editor, remote, host} = build("ro6", "db/users");
1383
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1384
+ editor.priv.reload_on_open = true;
1385
+ const form = modals[modals.length - 1];
1386
+ form.$content.querySelector('[data-name="header"]').value = "Identifier";
1387
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1388
+ answer_the_load_with(editor, remote, records_with((r) => {
1389
+ r.cols[0].description = "written by somebody else";
1390
+ }));
1391
+ const again = modals[modals.length - 1];
1392
+ expect(again.$content.querySelector('[data-name="header"]').value).toBe("Identifier");
1393
+ expect(again.$content.querySelector('[data-name="description"]').value)
1394
+ .toBe("written by somebody else");
1395
+ again.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1396
+ const [write] = take("update-node");
1397
+ expect(write.kw.record.description).toBe("written by somebody else");
1398
+ expect(write.kw.record.header).toBe("Identifier");
1399
+ expect(errors()).toEqual([]);
1400
+ });
1401
+
1402
+ test("a NEW column comes back whole: its name, its type and its flags", () => {
1403
+ const {editor, remote, host} = owing("ro2", "db/users");
1404
+ /* The owed reload runs on the form's opening too: open it on a
1405
+ * model that is current, then owe the reload. */
1406
+ editor.priv.reload_on_open = false;
1407
+ gobj_send_event(editor, "EV_ADD_COLUMN", {}, host);
1408
+ editor.priv.reload_on_open = true;
1409
+ const form = modals[modals.length - 1];
1410
+ form.$content.querySelector('[data-name="value"]').value = "email";
1411
+ form.$content.querySelector('[data-name="header"]').value = "Email";
1412
+ for(const $box of form.$content.querySelectorAll(".SCHEMA_FLAG_BOX")) {
1413
+ if($box.dataset.flag === "required") {
1414
+ $box.checked = true;
1415
+ }
1416
+ }
1417
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1418
+ expect(shown).toEqual([OWED_FORM]);
1419
+ answer_the_load(editor, remote);
1420
+ const again = modals[modals.length - 1];
1421
+ expect(again).not.toBe(form);
1422
+ expect(again.$content.querySelector('[data-name="value"]').value).toBe("email");
1423
+ expect(again.$content.querySelector('[data-name="header"]').value).toBe("Email");
1424
+ const on = [...again.$content.querySelectorAll(".SCHEMA_FLAG_BOX")]
1425
+ .filter(($b) => $b.checked).map(($b) => $b.dataset.flag);
1426
+ expect(on).toContain("required");
1427
+ expect(errors()).toEqual([]);
1428
+ });
1429
+
1430
+ test("a topic form comes back the same way", () => {
1431
+ const {editor, remote, host} = build("ro3", "db");
1432
+ gobj_send_event(editor, "EV_EDIT_TOPIC", {topic: "users"}, host);
1433
+ editor.priv.reload_on_open = true;
1434
+ const form = modals[modals.length - 1];
1435
+ form.$content.querySelector('[data-name="tkey"]').value = "tm";
1436
+ form.$content.querySelector(".SCHEMA_TOPIC_FORM_SAVE").click();
1437
+ expect(shown).toEqual([OWED_FORM]);
1438
+ answer_the_load(editor, remote);
1439
+ const again = modals[modals.length - 1];
1440
+ expect(again).not.toBe(form);
1441
+ expect(again.$content.querySelector('[data-name="tkey"]').value).toBe("tm");
1442
+ expect(errors()).toEqual([]);
1443
+ });
1444
+
1445
+ test("what the form edited is gone after the reload: not opened again, and SAID", () => {
1446
+ const {editor, remote, host} = build("ro4", "db/users");
1447
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1448
+ editor.priv.reload_on_open = true;
1449
+ const form = modals[modals.length - 1];
1450
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1451
+ const before = modals.length;
1452
+ answer_the_load_with(editor, remote, records_with((r) => {
1453
+ r.cols = [];
1454
+ }));
1455
+ expect(form.closed).toBe(true);
1456
+ expect(modals.length).toBe(before);
1457
+ expect(shown).toEqual([OWED_FORM, GONE]);
1458
+ expect(errors()).toEqual([]);
1459
+ });
1460
+
1461
+ test("a form closed by the operator while the reload ran is not opened again", () => {
1462
+ const {editor, remote, host} = build("ro5", "db/users");
1463
+ gobj_send_event(editor, "EV_EDIT_COLUMN", {col: "id"}, host);
1464
+ editor.priv.reload_on_open = true;
1465
+ const form = modals[modals.length - 1];
1466
+ form.$content.querySelector(".SCHEMA_COL_FORM_SAVE").click();
1467
+ form.$content.querySelector(".SCHEMA_COL_FORM_CANCEL").click();
1468
+ const before = modals.length;
1469
+ answer_the_load(editor, remote);
1470
+ expect(modals.length).toBe(before);
1471
+ expect(shown).toEqual([OWED_FORM]);
1472
+ expect(errors()).toEqual([]);
1473
+ });
1474
+ });
1475
+
1476
+ describe("EV_REFRESH while a write is in flight", () => {
1477
+
1478
+ test("waits for the end of the writes: the queue is written whole, then the schemas are read", () => {
1479
+ const {editor, remote, host} = build("rq1", "db");
1480
+ gobj_send_event(editor, "EV_CONFIRMED", {what: "topic", topic: "users",
1481
+ model_gen: editor.priv.model_gen}, editor);
1482
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1483
+ const [first] = take("delete-node");
1484
+ expect(first.kw.topic_name).toBe("cols");
1485
+
1486
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1487
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1488
+ expect(take("nodes")).toEqual([]);
1489
+ expect(logged.some((l) => l.level === "warning" &&
1490
+ /EV_REFRESH.*when the writes end/.test(l.msg))).toBe(true);
1491
+
1492
+ answer(editor, remote, first, 0, {});
1493
+ const [second] = take("delete-node");
1494
+ expect(second.kw.topic_name).toBe("topics");
1495
+ answer(editor, remote, second, 0, {});
1496
+
1497
+ expect(gobj_current_state(editor)).toBe("ST_LOADING");
1498
+ expect(answer_the_load(editor, remote)).toBe(3);
1499
+ expect(editor.priv.written).toEqual({});
1500
+ expect(errors()).toEqual([]);
1501
+ expect(not_defined()).toEqual([]);
1502
+ });
1503
+ });
@@ -1,8 +1,8 @@
1
1
  /***********************************************************************
2
2
  * schema_editor_transport.wiring.test.js
3
3
  *
4
- * C_YUI_SCHEMA_EDITOR when the transport drops (M-1 of the
5
- * independent review of 7.25.4). A drop used to leave the editor
4
+ * C_YUI_SCHEMA_EDITOR when the transport drops. A drop used to
5
+ * leave the editor
6
6
  * where it was: in ST_SAVING with its body busy, or in ST_LOADING
7
7
  * -- and the reconnect skipped the reload BECAUSE the state was
8
8
  * ST_LOADING. Only a reload of the page got it out.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * "This topic was written in this session and is not saved."
5
5
  *
6
- * An edit is a DRAFT (M36 of yunetas' 2026-09-21 review): it moves
6
+ * An edit is a DRAFT: it moves
7
7
  * no version, and the host's save-schema raises the versions of what
8
8
  * changed. So "written" alone is the whole question -- the version is
9
9
  * no longer measured against a baseline, which is also why the old
@@ -124,9 +124,8 @@ function maybe_apply_translator($node, opts)
124
124
  * it -- until every handle is closed or has timed out. Handed the
125
125
  * first caller's handle and timer, a repeat asking `timeout: 0`
126
126
  * was dismissed on the first one's timer, and closing one
127
- * caller's handle closed a toast another was still showing
128
- * (third independent review). The operator's ✕ closes it for
129
- * everybody.
127
+ * caller's handle closed a toast another was still showing.
128
+ * The operator's ✕ closes it for everybody.
130
129
  ***************************************************************/
131
130
  function show_notification(shell, kind, message, opts)
132
131
  {
@@ -614,9 +613,30 @@ function build_dialog(shell, message, buttons, opts)
614
613
  }
615
614
 
616
615
 
616
+ /***************************************************************
617
+ * The labels a confirmation shows when the caller names none.
618
+ *
619
+ * They are i18n KEYS, lower-case like every key: a button
620
+ * translates its label with the app's t() -- its text, its title
621
+ * and its aria-label. Until 7.25.16 they were "OK", "Yes", "No",
622
+ * "Delete" and "Cancel", keys that no locale carries, so a dialog
623
+ * that took the defaults read in English in every language.
624
+ *
625
+ * Written as `i18n:` entries so the validate-locales script of a
626
+ * consumer finds them when it scans this module: an app that
627
+ * mounts these dialogs must define the five keys.
628
+ ***************************************************************/
629
+ const DEFAULT_LABEL = Object.freeze({
630
+ ok: Object.freeze({i18n: "ok"}),
631
+ yes: Object.freeze({i18n: "yes"}),
632
+ no: Object.freeze({i18n: "no"}),
633
+ delete: Object.freeze({i18n: "delete"}),
634
+ cancel: Object.freeze({i18n: "cancel"})
635
+ });
636
+
617
637
  export function yui_shell_confirm_ok(shell, message, opts)
618
638
  {
619
- let label = (opts && opts.ok_label) || "OK";
639
+ let label = (opts && opts.ok_label) || DEFAULT_LABEL.ok.i18n;
620
640
  if(!opts || !opts.type) {
621
641
  opts = Object.assign({}, opts, {type: "success"});
622
642
  }
@@ -627,8 +647,8 @@ export function yui_shell_confirm_ok(shell, message, opts)
627
647
 
628
648
  export function yui_shell_confirm_yesno(shell, message, opts)
629
649
  {
630
- let yes_label = (opts && opts.yes_label) || "Yes";
631
- let no_label = (opts && opts.no_label) || "No";
650
+ let yes_label = (opts && opts.yes_label) || DEFAULT_LABEL.yes.i18n;
651
+ let no_label = (opts && opts.no_label) || DEFAULT_LABEL.no.i18n;
632
652
  return build_dialog(shell, message, [
633
653
  {label: yes_label, value: "yes", kind: "primary"},
634
654
  {label: no_label, value: "no"}
@@ -651,8 +671,8 @@ export function yui_shell_confirm_yesno(shell, message, opts)
651
671
  ***************************************************************/
652
672
  export function yui_shell_confirm_danger(shell, message, opts)
653
673
  {
654
- let confirm_label = (opts && opts.confirm_label) || "Delete";
655
- let cancel_label = (opts && opts.cancel_label) || "Cancel";
674
+ let confirm_label = (opts && opts.confirm_label) || DEFAULT_LABEL.delete.i18n;
675
+ let cancel_label = (opts && opts.cancel_label) || DEFAULT_LABEL.cancel.i18n;
656
676
  if(!opts || !opts.type) {
657
677
  opts = Object.assign({}, opts, {type: "danger"});
658
678
  }
@@ -664,9 +684,9 @@ export function yui_shell_confirm_danger(shell, message, opts)
664
684
 
665
685
  export function yui_shell_confirm_yesnocancel(shell, message, opts)
666
686
  {
667
- let yes_label = (opts && opts.yes_label) || "Yes";
668
- let no_label = (opts && opts.no_label) || "No";
669
- let cancel_label = (opts && opts.cancel_label) || "Cancel";
687
+ let yes_label = (opts && opts.yes_label) || DEFAULT_LABEL.yes.i18n;
688
+ let no_label = (opts && opts.no_label) || DEFAULT_LABEL.no.i18n;
689
+ let cancel_label = (opts && opts.cancel_label) || DEFAULT_LABEL.cancel.i18n;
670
690
  return build_dialog(shell, message, [
671
691
  {label: yes_label, value: "yes", kind: "primary"},
672
692
  {label: no_label, value: "no"},