@yuneta/gobj-ui 7.23.42 → 7.23.44

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.
@@ -519,17 +519,50 @@ function remote_command(gobj, command, kw)
519
519
  }
520
520
 
521
521
  /***************************************************************
522
- * The three answers are in: build the schema and remember the
523
- * versions it arrived with. The baseline is what "this topic
524
- * still needs its version raised" is measured against, so it is
525
- * taken here and never touched again until the next load.
522
+ * The three answers are in: build the schema.
523
+ *
524
+ * IT DOES NOT TOUCH THE BASELINE, and that is the whole point.
525
+ * This runs again on the patch after EVERY write, so a baseline
526
+ * taken here was re-taken as the version the write had just
527
+ * raised -- and "written, and its version has not moved" then
528
+ * read TRUE for a topic whose version had just moved.
529
+ *
530
+ * The banner that says so offers a Raise button, which wrote
531
+ * another version, which was measured against itself again: the
532
+ * banner came back, every time, and could not be dismissed. One
533
+ * operator pressed it thirty-three times on one column delete.
534
+ * It is why schema versions climbed by tens with the schema
535
+ * unchanged.
536
+ *
537
+ * See start_measuring(): the baseline belongs to the LOAD, the
538
+ * same reason `order_undo` was moved out of here before it.
526
539
  ***************************************************************/
527
540
  function build_model(gobj)
528
541
  {
529
542
  let priv = gobj.priv;
530
543
 
531
544
  priv.model = build_schema_model(priv.records);
545
+ }
546
+
547
+ /***************************************************************
548
+ * From here on, "did this session change it?" has an answer.
549
+ *
550
+ * Called ONLY when records arrive from the store: that is what a
551
+ * measurement of "changed since" can be measured against. Both
552
+ * halves reset together -- a baseline without its `written` is
553
+ * the same false positive from the other side, since every
554
+ * version equals a baseline just taken.
555
+ ***************************************************************/
556
+ function start_measuring(gobj)
557
+ {
558
+ let priv = gobj.priv;
559
+
532
560
  priv.baseline = {};
561
+ priv.written = {};
562
+
563
+ if(!priv.model) {
564
+ return;
565
+ }
533
566
  for(let treedb of priv.model.treedbs) {
534
567
  for(let topic of treedb.topics) {
535
568
  priv.baseline[topic.id] = topic.topic_version;
@@ -537,6 +570,24 @@ function build_model(gobj)
537
570
  }
538
571
  }
539
572
 
573
+ /***************************************************************
574
+ * "This topic was written and its version did not move."
575
+ *
576
+ * ONE predicate, because it is asked in two places -- the topic
577
+ * list draws a chip with it and the column view draws a banner --
578
+ * and two copies of a rule is how they drift.
579
+ ***************************************************************/
580
+ function needs_version_bump(written, baseline, topic)
581
+ {
582
+ if(!topic || !written || !baseline) {
583
+ return false;
584
+ }
585
+ if(!written[topic.id]) {
586
+ return false;
587
+ }
588
+ return String(baseline[topic.id]) === String(topic.topic_version);
589
+ }
590
+
540
591
  /***************************************************************
541
592
  * Say what the whole store would be refused for.
542
593
  *
@@ -987,8 +1038,7 @@ function render_topics(gobj, $body)
987
1038
  let twice = repeated_names(treedb.topics);
988
1039
  let $rows = [];
989
1040
  for(let topic of treedb.topics) {
990
- let pending = priv.written[topic.id] &&
991
- String(priv.baseline[topic.id]) === String(topic.topic_version);
1041
+ let pending = needs_version_bump(priv.written, priv.baseline, topic);
992
1042
  /* Two topics of one name: the id is what tells them apart, and
993
1043
  * it is what the url has to carry — addressing by name would
994
1044
  * open the first one whichever row was clicked. */
@@ -1187,10 +1237,7 @@ function version_banner(gobj, topic)
1187
1237
  {
1188
1238
  let priv = gobj.priv;
1189
1239
 
1190
- if(!priv.written[topic.id]) {
1191
- return null;
1192
- }
1193
- if(String(priv.baseline[topic.id]) !== String(topic.topic_version)) {
1240
+ if(!needs_version_bump(priv.written, priv.baseline, topic)) {
1194
1241
  return null;
1195
1242
  }
1196
1243
  if(is_readonly(gobj)) {
@@ -2558,6 +2605,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
2558
2605
  * write, which is exactly when the undo has to survive.) */
2559
2606
  priv.order_undo = {};
2560
2607
  build_model(gobj);
2608
+ /* Same reason as order_undo above: this is the LOAD, and the
2609
+ * baseline is what a later "the version did not move" is measured
2610
+ * against. Taken on the write patches instead, it measured every
2611
+ * version against itself. */
2612
+ start_measuring(gobj);
2561
2613
  /* Back to where the operator was: the load may be a refresh, or
2562
2614
  * the answer to a deep link that arrived before the model. */
2563
2615
  return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
@@ -3424,4 +3476,9 @@ function register_c_yui_schema_editor()
3424
3476
  return create_gclass(GCLASS_NAME);
3425
3477
  }
3426
3478
 
3427
- export {register_c_yui_schema_editor};
3479
+ export {
3480
+ register_c_yui_schema_editor,
3481
+ /* Exported for its test: the rule that decides whether a topic still
3482
+ * owes a version bump, which is asked in two places. */
3483
+ needs_version_bump
3484
+ };
@@ -183,18 +183,34 @@ export function subtree_matches(value, term)
183
183
 
184
184
  /************************************************************
185
185
  * Fields whose numeric value is an epoch timestamp.
186
- * Mirrors the timestampTag list wired into C_YUI_FORM's
187
- * jsoneditor control, so the two viewers agree.
186
+ *
187
+ * THE ONE LIST. C_YUI_FORM's jsoneditor takes its
188
+ * `timestampTag` from here: it used to carry its own copy,
189
+ * and a copy is how the two stopped agreeing.
190
+ *
191
+ * TWO SPELLINGS, AND LAS DOS SON DEL BACKEND. timeranger2
192
+ * writes `from_t`/`from_tm` in the match_cond of a query and
193
+ * `fr_t`/`fr_tm` in the metadata of a topic and of a file --
194
+ * which is what a Raw JSON of a treedb actually shows. With
195
+ * only the long pair listed, `to_t` got its date and the
196
+ * `fr_t` right above it did not, which reads as a broken
197
+ * annotator rather than a missing name.
188
198
  ************************************************************/
199
+ const TIME_FIELDS = [
200
+ "__t__", "__tm__",
201
+ "t", "tm", "time",
202
+ "from_t", "to_t", "from_tm", "to_tm",
203
+ "fr_t", "fr_tm",
204
+ "t_input", "t_output"
205
+ ];
206
+
189
207
  export function is_time_field(field)
190
208
  {
191
- return field === "__t__" || field === "__tm__" ||
192
- field === "tm" || field === "t" || field === "time" ||
193
- field === "from_t" || field === "to_t" ||
194
- field === "t_input" || field === "t_output" ||
195
- field === "from_tm" || field === "to_tm";
209
+ return TIME_FIELDS.indexOf(field) >= 0;
196
210
  }
197
211
 
212
+ export {TIME_FIELDS};
213
+
198
214
  /************************************************************
199
215
  * Format an epoch value as a local wall-clock string.
200
216
  * Accepts seconds or milliseconds; returns null when the
@@ -310,3 +310,36 @@ test("is_pure_collection: the schema this came from", () => {
310
310
  /* A column has content, so it keeps its card. */
311
311
  expect(is_pure_collection(col)).toBe(false);
312
312
  });
313
+
314
+
315
+ /*
316
+ * DOS ORTOGRAFIAS, Y LAS DOS SON DEL BACKEND.
317
+ *
318
+ * timeranger2 escribe `from_t`/`from_tm` en el match_cond de una consulta y
319
+ * `fr_t`/`fr_tm` en los metadatos de un topic y de un fichero -- que es lo
320
+ * que enseña el Raw JSON de un treedb. Con solo la larga en la lista,
321
+ * `to_t` salia fechado y el `fr_t` de encima no: se lee como un anotador
322
+ * roto y es un nombre que falta.
323
+ */
324
+ test("is_time_field reconoce la abreviada, la que sale en un Raw JSON", () => {
325
+ expect(is_time_field("fr_t")).toBe(true);
326
+ expect(is_time_field("fr_tm")).toBe(true);
327
+ });
328
+
329
+ test("is_time_field sigue reconociendo la larga, la de las consultas", () => {
330
+ expect(is_time_field("from_t")).toBe(true);
331
+ expect(is_time_field("from_tm")).toBe(true);
332
+ });
333
+
334
+ test("is_time_field: las dos mitades de cada par, o la fila queda a medias", () => {
335
+ [["fr_t", "to_t"], ["fr_tm", "to_tm"],
336
+ ["from_t", "to_t"], ["from_tm", "to_tm"]].forEach((pair) => {
337
+ expect(is_time_field(pair[0])).toBe(is_time_field(pair[1]));
338
+ });
339
+ });
340
+
341
+ test("is_time_field no fecha lo que no es una fecha", () => {
342
+ ["rows", "id", "", undefined, null].forEach((f) => {
343
+ expect(is_time_field(f)).toBe(false);
344
+ });
345
+ });
@@ -0,0 +1,92 @@
1
+ /***********************************************************************
2
+ * schema_version_bump.test.js
3
+ *
4
+ * "This topic was written and its version did not move."
5
+ *
6
+ * THE RULE MEASURES AGAINST A BASELINE, AND THE BASELINE MUST NOT
7
+ * MOVE WITH IT. It used to be re-taken inside build_model(), which
8
+ * runs again on the patch after EVERY write -- so it came back as
9
+ * the version the write had just raised, the rule read TRUE for a
10
+ * topic whose version HAD moved, and the banner that says so
11
+ * offered a Raise button that wrote another version and was
12
+ * measured against itself again.
13
+ *
14
+ * The banner could not be dismissed. One operator pressed it
15
+ * thirty-three times on a single column delete, and that is why
16
+ * schema versions climbed by tens with the schema unchanged.
17
+ *
18
+ * Copyright (c) 2026, ArtGins.
19
+ * All Rights Reserved.
20
+ ***********************************************************************/
21
+ import {describe, it, expect} from "vitest";
22
+ import {needs_version_bump} from "./c_yui_schema_editor.js";
23
+
24
+ const TOPIC = {id: "device_groups", topic_version: 5};
25
+
26
+ describe("needs_version_bump", () => {
27
+ it("says nothing about a topic this session never wrote", () => {
28
+ expect(needs_version_bump({}, {device_groups: 5}, TOPIC)).toBe(false);
29
+ });
30
+
31
+ it("asks for the bump when the version sits where it was found", () => {
32
+ expect(needs_version_bump({device_groups: true}, {device_groups: 5}, TOPIC))
33
+ .toBe(true);
34
+ });
35
+
36
+ it("STOPS asking once the version has moved past the baseline", () => {
37
+ /* The case the loop got wrong: the write raised it to 6 and the
38
+ * baseline still says 5, so there is nothing left to raise. */
39
+ expect(needs_version_bump(
40
+ {device_groups: true}, {device_groups: 5}, {id: "device_groups", topic_version: 6}
41
+ )).toBe(false);
42
+ });
43
+
44
+ it("compares as text, so 5 and '5' are the same version", () => {
45
+ expect(needs_version_bump(
46
+ {device_groups: true}, {device_groups: "5"}, TOPIC
47
+ )).toBe(true);
48
+ expect(needs_version_bump(
49
+ {device_groups: true}, {device_groups: 5}, {id: "device_groups", topic_version: "5"}
50
+ )).toBe(true);
51
+ });
52
+
53
+ it("answers false rather than throwing on a missing half", () => {
54
+ expect(needs_version_bump(null, {device_groups: 5}, TOPIC)).toBe(false);
55
+ expect(needs_version_bump({device_groups: true}, null, TOPIC)).toBe(false);
56
+ expect(needs_version_bump({device_groups: true}, {device_groups: 5}, null)).toBe(false);
57
+ });
58
+
59
+ it("a topic with no baseline entry is not owed a bump", () => {
60
+ expect(needs_version_bump({device_groups: true}, {}, TOPIC)).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe("the loop it came from", () => {
65
+ /*
66
+ * What the editor does per bump: raise the version by one. With the
67
+ * baseline left alone, ONE press settles it; with the baseline
68
+ * re-taken each time -- the old bug -- it never settles.
69
+ */
70
+ function press_raise(state) {
71
+ return {written: state.written, baseline: state.baseline,
72
+ topic: {id: "t", topic_version: Number(state.topic.topic_version) + 1}};
73
+ }
74
+
75
+ it("settles after a single press", () => {
76
+ let st = {written: {t: true}, baseline: {t: 5}, topic: {id: "t", topic_version: 5}};
77
+ expect(needs_version_bump(st.written, st.baseline, st.topic)).toBe(true);
78
+ st = press_raise(st);
79
+ expect(needs_version_bump(st.written, st.baseline, st.topic)).toBe(false);
80
+ });
81
+
82
+ it("never settles if the baseline is re-taken with the version", () => {
83
+ let st = {written: {t: true}, baseline: {t: 5}, topic: {id: "t", topic_version: 5}};
84
+ let presses = 0;
85
+ while(needs_version_bump(st.written, st.baseline, st.topic) && presses < 40) {
86
+ st = press_raise(st);
87
+ st.baseline = {t: st.topic.topic_version}; /* the bug */
88
+ presses++;
89
+ }
90
+ expect(presses).toBe(40); /* it ran to the guard: no way out */
91
+ });
92
+ });