@yuneta/gobj-ui 7.25.15 → 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.
@@ -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", () => {
@@ -618,6 +620,7 @@ describe("a load that fails IN session", () => {
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", () => {
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, 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
 
@@ -1353,3 +1368,136 @@ describe("a move sent by the host while a dialog is up", () => {
1353
1368
  expect(shown).toEqual([]);
1354
1369
  });
1355
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
+ });
@@ -613,9 +613,30 @@ function build_dialog(shell, message, buttons, opts)
613
613
  }
614
614
 
615
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
+
616
637
  export function yui_shell_confirm_ok(shell, message, opts)
617
638
  {
618
- let label = (opts && opts.ok_label) || "OK";
639
+ let label = (opts && opts.ok_label) || DEFAULT_LABEL.ok.i18n;
619
640
  if(!opts || !opts.type) {
620
641
  opts = Object.assign({}, opts, {type: "success"});
621
642
  }
@@ -626,8 +647,8 @@ export function yui_shell_confirm_ok(shell, message, opts)
626
647
 
627
648
  export function yui_shell_confirm_yesno(shell, message, opts)
628
649
  {
629
- let yes_label = (opts && opts.yes_label) || "Yes";
630
- 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;
631
652
  return build_dialog(shell, message, [
632
653
  {label: yes_label, value: "yes", kind: "primary"},
633
654
  {label: no_label, value: "no"}
@@ -650,8 +671,8 @@ export function yui_shell_confirm_yesno(shell, message, opts)
650
671
  ***************************************************************/
651
672
  export function yui_shell_confirm_danger(shell, message, opts)
652
673
  {
653
- let confirm_label = (opts && opts.confirm_label) || "Delete";
654
- 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;
655
676
  if(!opts || !opts.type) {
656
677
  opts = Object.assign({}, opts, {type: "danger"});
657
678
  }
@@ -663,9 +684,9 @@ export function yui_shell_confirm_danger(shell, message, opts)
663
684
 
664
685
  export function yui_shell_confirm_yesnocancel(shell, message, opts)
665
686
  {
666
- let yes_label = (opts && opts.yes_label) || "Yes";
667
- let no_label = (opts && opts.no_label) || "No";
668
- 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;
669
690
  return build_dialog(shell, message, [
670
691
  {label: yes_label, value: "yes", kind: "primary"},
671
692
  {label: no_label, value: "no"},
@@ -30,6 +30,9 @@ const {
30
30
  yui_shell_show_info,
31
31
  yui_shell_show_modal,
32
32
  yui_shell_confirm_danger,
33
+ yui_shell_confirm_ok,
34
+ yui_shell_confirm_yesno,
35
+ yui_shell_confirm_yesnocancel,
33
36
  } = await import("./shell_modals.js");
34
37
 
35
38
  let shell = null;
@@ -200,3 +203,63 @@ describe("every other button of a dialog is named", () => {
200
203
  ]);
201
204
  });
202
205
  });
206
+
207
+ /*
208
+ * A confirmation that names no label shows the DEFAULT ones, and those
209
+ * are i18n keys like every other label: lower-case, translated by the
210
+ * app's t(), and carried as the key of the text, the title and the
211
+ * aria-label. They were "OK", "Yes", "No", "Delete" and "Cancel" --
212
+ * keys no locale has, so the button read in English in every language.
213
+ */
214
+ describe("the default labels of a confirmation are i18n keys", () => {
215
+
216
+ function keys_of($layer)
217
+ {
218
+ return [...$layer.querySelectorAll(".CONFIRM_BTN")].map(($b) => [
219
+ $b.getAttribute("data-i18n"),
220
+ $b.getAttribute("data-i18n-title"),
221
+ $b.getAttribute("data-i18n-aria-label"),
222
+ ]);
223
+ }
224
+
225
+ /* What the reader hears: the aria-label, translated by t() when
226
+ * the button is built. */
227
+ function texts_of($layer)
228
+ {
229
+ return [...$layer.querySelectorAll(".CONFIRM_BTN")].map(($b) => $b.getAttribute("aria-label"));
230
+ }
231
+
232
+ const k = (key) => [key, key, key];
233
+
234
+ /* A translator that tells a key it knows from one it does not. */
235
+ const ES = {ok: "Aceptar", yes: "Sí", no: "No", delete: "Borrar", cancel: "Cancelar"};
236
+ const t = (key) => ES[key] || `?${key}?`;
237
+
238
+ test("ok", () => {
239
+ shell.priv.layers.modal = document.createElement("div");
240
+ yui_shell_confirm_ok(shell, "saved", {t: t});
241
+ expect(keys_of(shell.priv.layers.modal)).toEqual([k("ok")]);
242
+ expect(texts_of(shell.priv.layers.modal)).toEqual(["Aceptar"]);
243
+ });
244
+
245
+ test("yes / no", () => {
246
+ shell.priv.layers.modal = document.createElement("div");
247
+ yui_shell_confirm_yesno(shell, "go on?", {t: t});
248
+ expect(keys_of(shell.priv.layers.modal)).toEqual([k("yes"), k("no")]);
249
+ expect(texts_of(shell.priv.layers.modal)).toEqual(["Sí", "No"]);
250
+ });
251
+
252
+ test("yes / no / cancel", () => {
253
+ shell.priv.layers.modal = document.createElement("div");
254
+ yui_shell_confirm_yesnocancel(shell, "keep it?", {t: t});
255
+ expect(keys_of(shell.priv.layers.modal)).toEqual([k("yes"), k("no"), k("cancel")]);
256
+ expect(texts_of(shell.priv.layers.modal)).toEqual(["Sí", "No", "Cancelar"]);
257
+ });
258
+
259
+ test("delete / cancel", () => {
260
+ shell.priv.layers.modal = document.createElement("div");
261
+ yui_shell_confirm_danger(shell, "delete this column?", {t: t});
262
+ expect(keys_of(shell.priv.layers.modal)).toEqual([k("delete"), k("cancel")]);
263
+ expect(texts_of(shell.priv.layers.modal)).toEqual(["Borrar", "Cancelar"]);
264
+ });
265
+ });
@@ -9,7 +9,7 @@ const doc = {
9
9
  describe("table_cell_text", () => {
10
10
 
11
11
  it("record text is a text node, markup included", () => {
12
- /* M31: Tabulator puts a string through innerHTML */
12
+ /* Tabulator puts a string through innerHTML */
13
13
  const node = cell_text("limit a<b and c>d", doc);
14
14
  expect(node.nodeType).toBe(3);
15
15
  expect(node.textContent).toBe("limit a<b and c>d");
@@ -169,7 +169,7 @@ function errors()
169
169
  return logged.filter((l) => l.level === "error").map((l) => l.msg);
170
170
  }
171
171
 
172
- describe("a Save in flight when the backend drops (M8)", () => {
172
+ describe("a Save in flight when the backend drops", () => {
173
173
 
174
174
  test("the shell's connection edge answers every form, with no host forwarding", () => {
175
175
  const {shell, topics, forms} = build("t1");
@@ -194,6 +194,17 @@ function yui_install_start_watch()
194
194
  * app can call yui_install_prompt() from a button of its
195
195
  * own whenever it likes.
196
196
  ***************************************************************/
197
+ /* The question and its answers when the app names none. They are i18n
198
+ * KEYS, lower-case, and the ones every consumer already defines; the
199
+ * question was "Install this app?" until 7.25.16, a key no locale
200
+ * carries. Written as `i18n:` entries so the validate-locales script of
201
+ * a consumer finds them when it scans this module. */
202
+ const DEFAULT_ASK = Object.freeze({
203
+ message: Object.freeze({i18n: "install this app"}),
204
+ yes: Object.freeze({i18n: "install"}),
205
+ no: Object.freeze({i18n: "not now"})
206
+ });
207
+
197
208
  function yui_install_ask_once(shell, opts)
198
209
  {
199
210
  opts = opts || {};
@@ -213,10 +224,10 @@ function yui_install_ask_once(shell, opts)
213
224
  S.asking = true;
214
225
  kw_set_local_storage_value(ASKED_KEY, true);
215
226
 
216
- yui_shell_confirm_yesno(shell, opts.message || "Install this app?", {
227
+ yui_shell_confirm_yesno(shell, opts.message || DEFAULT_ASK.message.i18n, {
217
228
  t: opts.t,
218
- yes_label: opts.yes_label || "install",
219
- no_label: opts.no_label || "not now"
229
+ yes_label: opts.yes_label || DEFAULT_ASK.yes.i18n,
230
+ no_label: opts.no_label || DEFAULT_ASK.no.i18n
220
231
  }).then(function(yes) {
221
232
  S.asking = false;
222
233
  if(yes) {
@@ -140,3 +140,26 @@ test("subscribe(): a throwing listener does not break the others", async () => {
140
140
  off1();
141
141
  off2();
142
142
  });
143
+
144
+ /* The question asked when the app names none is an i18n KEY, the one
145
+ * every consumer already defines ("install this app"). It was
146
+ * "Install this app?", a key no locale carries, so an app that took
147
+ * the default asked in English in every language. */
148
+ test("ask_once(): the default question and answers are i18n keys", async () => {
149
+ const asked = [];
150
+ vi.resetModules();
151
+ vi.doMock("./shell_modals.js", () => ({
152
+ yui_shell_confirm_yesno: (shell, message, opts) => {
153
+ asked.push({message, yes: opts.yes_label, no: opts.no_label});
154
+ return new Promise(() => {});
155
+ }
156
+ }));
157
+ try {
158
+ const {yui_install_ask_once} = await import("./yui_install.js");
159
+ window[STASH] = fake_event("accepted");
160
+ yui_install_ask_once({fake_shell: true}, {});
161
+ expect(asked).toEqual([{message: "install this app", yes: "install", no: "not now"}]);
162
+ } finally {
163
+ vi.doUnmock("./shell_modals.js");
164
+ }
165
+ });