@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.
@@ -6040,6 +6040,293 @@ function yui_watch_theme(gobj, event = "EV_THEME") {
6040
6040
  }
6041
6041
  } };
6042
6042
  }
6043
+ /************************************************************
6044
+ * JSON type discriminator (viewer vocabulary)
6045
+ ************************************************************/
6046
+ function json_type(value) {
6047
+ if (value === null) return "null";
6048
+ if (Array.isArray(value)) return "array";
6049
+ switch (typeof value) {
6050
+ case "string": return "string";
6051
+ case "number": return "number";
6052
+ case "boolean": return "boolean";
6053
+ case "object": return "object";
6054
+ default: return "unknown";
6055
+ }
6056
+ }
6057
+ /************************************************************
6058
+ * Detect a kw_collapse() sentinel.
6059
+ *
6060
+ * The kernel replaces an over-limit dict with:
6061
+ * { "__collapsed__": { "path": ..., "size": N } }
6062
+ * and an over-limit array with:
6063
+ * [ { "__collapsed__": { "path": ..., "size": N } } ]
6064
+ *
6065
+ * Returns { size, path, is_array } or null.
6066
+ ************************************************************/
6067
+ function is_collapsed(value) {
6068
+ if (value && typeof value === "object" && !Array.isArray(value)) {
6069
+ let keys = Object.keys(value);
6070
+ if (keys.length === 1 && keys[0] === "__collapsed__") {
6071
+ let c = value.__collapsed__ || {};
6072
+ return {
6073
+ size: c.size,
6074
+ path: c.path,
6075
+ is_array: false
6076
+ };
6077
+ }
6078
+ return null;
6079
+ }
6080
+ if (Array.isArray(value) && value.length === 1) {
6081
+ let e = value[0];
6082
+ if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
6083
+ let c = e.__collapsed__ || {};
6084
+ return {
6085
+ size: c.size,
6086
+ path: c.path,
6087
+ is_array: true
6088
+ };
6089
+ }
6090
+ }
6091
+ return null;
6092
+ }
6093
+ /************************************************************
6094
+ * Join / split absolute path segments (kernel convention)
6095
+ ************************************************************/
6096
+ function seg_join(segments) {
6097
+ return segments.map(function(s) {
6098
+ return String(s);
6099
+ }).join("`");
6100
+ }
6101
+ function seg_split(path) {
6102
+ if (path === "" || path === null || path === void 0) return [];
6103
+ return path.split("`");
6104
+ }
6105
+ /************************************************************
6106
+ * Walk a JSON tree by absolute segments; undefined if the
6107
+ * path does not resolve.
6108
+ ************************************************************/
6109
+ function get_by_segments(root, segments) {
6110
+ let v = root;
6111
+ for (let seg of segments) {
6112
+ if (v === null || v === void 0) return;
6113
+ if (Array.isArray(v)) v = v[Number(seg)];
6114
+ else if (typeof v === "object") v = v[seg];
6115
+ else return;
6116
+ }
6117
+ return v;
6118
+ }
6119
+ /************************************************************
6120
+ * Set a value at absolute segments (mutates root).
6121
+ *
6122
+ * segments == [] means "replace the whole root" — the caller
6123
+ * must use the RETURNED value in that case (a primitive/array
6124
+ * root cannot be mutated in place).
6125
+ ************************************************************/
6126
+ function set_by_segments(root, segments, new_value) {
6127
+ if (segments.length === 0) return new_value;
6128
+ let parent = get_by_segments(root, segments.slice(0, -1));
6129
+ if (parent === null || parent === void 0 || typeof parent !== "object") return root;
6130
+ let last = segments[segments.length - 1];
6131
+ if (Array.isArray(parent)) parent[Number(last)] = new_value;
6132
+ else parent[last] = new_value;
6133
+ return root;
6134
+ }
6135
+ /************************************************************
6136
+ * Does `value` (or any loaded descendant / its own key)
6137
+ * contain the lower-cased search term?
6138
+ *
6139
+ * Collapsed (not-yet-loaded) subtrees can't be searched, so
6140
+ * they never match on content — only their key can match,
6141
+ * which the renderer checks separately.
6142
+ ************************************************************/
6143
+ function subtree_matches(value, term) {
6144
+ if (!term) return true;
6145
+ if (is_collapsed(value)) return false;
6146
+ let type = json_type(value);
6147
+ if (type === "object") {
6148
+ for (let [k, v] of Object.entries(value)) {
6149
+ if (String(k).toLowerCase().includes(term)) return true;
6150
+ if (subtree_matches(v, term)) return true;
6151
+ }
6152
+ return false;
6153
+ }
6154
+ if (type === "array") {
6155
+ for (let v of value) if (subtree_matches(v, term)) return true;
6156
+ return false;
6157
+ }
6158
+ return String(value).toLowerCase().includes(term);
6159
+ }
6160
+ /************************************************************
6161
+ * Fields whose numeric value is an epoch timestamp.
6162
+ *
6163
+ * THE ONE LIST. C_YUI_FORM's jsoneditor takes its
6164
+ * `timestampTag` from here: it used to carry its own copy,
6165
+ * and a copy is how the two stopped agreeing.
6166
+ *
6167
+ * TWO SPELLINGS, AND LAS DOS SON DEL BACKEND. timeranger2
6168
+ * writes `from_t`/`from_tm` in the match_cond of a query and
6169
+ * `fr_t`/`fr_tm` in the metadata of a topic and of a file --
6170
+ * which is what a Raw JSON of a treedb actually shows. With
6171
+ * only the long pair listed, `to_t` got its date and the
6172
+ * `fr_t` right above it did not, which reads as a broken
6173
+ * annotator rather than a missing name.
6174
+ ************************************************************/
6175
+ var TIME_FIELDS = [
6176
+ "__t__",
6177
+ "__tm__",
6178
+ "t",
6179
+ "tm",
6180
+ "time",
6181
+ "from_t",
6182
+ "to_t",
6183
+ "from_tm",
6184
+ "to_tm",
6185
+ "fr_t",
6186
+ "fr_tm",
6187
+ "t_input",
6188
+ "t_output"
6189
+ ];
6190
+ function is_time_field(field) {
6191
+ return TIME_FIELDS.indexOf(field) >= 0;
6192
+ }
6193
+ /************************************************************
6194
+ * Format an epoch value as a local wall-clock string.
6195
+ * Accepts seconds or milliseconds; returns null when the
6196
+ * value is not a usable positive timestamp (0 == "unset").
6197
+ ************************************************************/
6198
+ function format_epoch(value) {
6199
+ if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
6200
+ let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
6201
+ let d = new Date(ms);
6202
+ if (isNaN(d.getTime())) return null;
6203
+ return d.toLocaleString();
6204
+ }
6205
+ /************************************************************
6206
+ * The text view's payload: the document as text, indented
6207
+ * four characters, cut at `max_chars`.
6208
+ *
6209
+ * Returns {text, capped}. `capped` is what the caller
6210
+ * announces under the dump — a cut that is not announced
6211
+ * reads as a document that ends there.
6212
+ ************************************************************/
6213
+ function json_text_dump(root, max_chars) {
6214
+ let text;
6215
+ try {
6216
+ text = JSON.stringify(root, null, 4);
6217
+ } catch (e) {
6218
+ return {
6219
+ text: "",
6220
+ capped: false,
6221
+ error: String(e)
6222
+ };
6223
+ }
6224
+ if (text === void 0) return {
6225
+ text: "",
6226
+ capped: false,
6227
+ error: "not representable as JSON"
6228
+ };
6229
+ if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
6230
+ text: text.slice(0, max_chars),
6231
+ capped: true
6232
+ };
6233
+ return {
6234
+ text,
6235
+ capped: false
6236
+ };
6237
+ }
6238
+ /************************************************************
6239
+ * Which view a freshly mounted viewer opens on.
6240
+ *
6241
+ * Three answers, in order of authority: what the HOST asked
6242
+ * for, what the READER chose last time, and `fallback`.
6243
+ *
6244
+ * The host wins because a viewer mounted to show a graph has
6245
+ * to show one. The memory comes next, because reopening a
6246
+ * document in the view you just left is the whole point of
6247
+ * remembering it. `fallback` is the floor.
6248
+ *
6249
+ * Anything not in `modes` is treated as unsaid, which covers
6250
+ * an absent value, an empty one, and a stored string from an
6251
+ * older release that named a view no longer offered.
6252
+ ************************************************************/
6253
+ function pick_view_mode(asked, remembered, modes, fallback) {
6254
+ let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
6255
+ if (known(asked)) return asked;
6256
+ if (known(remembered)) return remembered;
6257
+ return fallback;
6258
+ }
6259
+ /************************************************************
6260
+ * The short label a container row carries beside its size.
6261
+ *
6262
+ * An array of dicts that carry an `id` is a list of RECORDS,
6263
+ * and a row that says only `2: {15}` makes you open all
6264
+ * fifteen fields of every one of them to find out WHICH
6265
+ * record it is. The id is the one field that answers that,
6266
+ * so the row says it without being opened.
6267
+ *
6268
+ * Only a SCALAR id, and only when it is really there: an `id`
6269
+ * that is itself a dict or a list is not a name, it is more
6270
+ * document, and printing `[object Object]` beside the size
6271
+ * would be worse than printing nothing.
6272
+ *
6273
+ * Cut at `max_chars`, because this rides on a row whose job
6274
+ * is to stay one line.
6275
+ ************************************************************/
6276
+ function container_label(value, max_chars) {
6277
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
6278
+ if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
6279
+ let id = value.id;
6280
+ let t = typeof id;
6281
+ if (t !== "string" && t !== "number" && t !== "boolean") return "";
6282
+ let text = String(id).trim();
6283
+ if (text === "") return "";
6284
+ let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
6285
+ if (cap && text.length > cap) return text.slice(0, cap) + "…";
6286
+ return text;
6287
+ }
6288
+ /************************************************************
6289
+ * A dict, in the JSON sense: not an array and not null.
6290
+ ************************************************************/
6291
+ function is_plain_object(v) {
6292
+ return v !== null && typeof v === "object" && !Array.isArray(v);
6293
+ }
6294
+ /************************************************************
6295
+ * Does this container hold ONLY other containers?
6296
+ *
6297
+ * Such a value is not a thing, it is a LIST of things -- and a
6298
+ * list is fully described by the row that names it in its
6299
+ * parent, plus the edges to what it holds. Drawing it as a
6300
+ * card of its own put a node in the middle of the graph with no
6301
+ * data in it, saying nothing the parent's row did not already
6302
+ * say, and pushed everything below it one level down.
6303
+ *
6304
+ * An EMPTY container inside counts as content: `{0}` is
6305
+ * something the card has to show, and there is no child card
6306
+ * to show it instead.
6307
+ ************************************************************/
6308
+ function is_pure_collection(value) {
6309
+ let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
6310
+ if (entries.length === 0) return false;
6311
+ for (let [, v] of entries) if (!has_branch(v)) return false;
6312
+ return true;
6313
+ }
6314
+ /************************************************************
6315
+ * A value that gets a card, or children, of its own.
6316
+ ************************************************************/
6317
+ function has_branch(value) {
6318
+ return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
6319
+ }
6320
+ /************************************************************
6321
+ * How many branches a container has, which is what the fold
6322
+ * chip counts.
6323
+ ************************************************************/
6324
+ function count_branches(value) {
6325
+ let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
6326
+ let n = 0;
6327
+ for (let v of entries) if (has_branch(v)) n++;
6328
+ return n;
6329
+ }
6043
6330
  //#endregion
6044
6331
  //#region src/c_yui_form.js
6045
6332
  /***********************************************************************
@@ -7168,7 +7455,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
7168
7455
  (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_RECORD_CHANGED", {}, gobj);
7169
7456
  },
7170
7457
  timestampTag: function({ field, value, path }) {
7171
- return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
7458
+ return is_time_field(field);
7172
7459
  }
7173
7460
  }
7174
7461
  });
@@ -10330,268 +10617,6 @@ function ensure_drag_canvas_patch() {
10330
10617
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
10331
10618
  __drag_canvas_patched__ = true;
10332
10619
  }
10333
- /************************************************************
10334
- * JSON type discriminator (viewer vocabulary)
10335
- ************************************************************/
10336
- function json_type(value) {
10337
- if (value === null) return "null";
10338
- if (Array.isArray(value)) return "array";
10339
- switch (typeof value) {
10340
- case "string": return "string";
10341
- case "number": return "number";
10342
- case "boolean": return "boolean";
10343
- case "object": return "object";
10344
- default: return "unknown";
10345
- }
10346
- }
10347
- /************************************************************
10348
- * Detect a kw_collapse() sentinel.
10349
- *
10350
- * The kernel replaces an over-limit dict with:
10351
- * { "__collapsed__": { "path": ..., "size": N } }
10352
- * and an over-limit array with:
10353
- * [ { "__collapsed__": { "path": ..., "size": N } } ]
10354
- *
10355
- * Returns { size, path, is_array } or null.
10356
- ************************************************************/
10357
- function is_collapsed(value) {
10358
- if (value && typeof value === "object" && !Array.isArray(value)) {
10359
- let keys = Object.keys(value);
10360
- if (keys.length === 1 && keys[0] === "__collapsed__") {
10361
- let c = value.__collapsed__ || {};
10362
- return {
10363
- size: c.size,
10364
- path: c.path,
10365
- is_array: false
10366
- };
10367
- }
10368
- return null;
10369
- }
10370
- if (Array.isArray(value) && value.length === 1) {
10371
- let e = value[0];
10372
- if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
10373
- let c = e.__collapsed__ || {};
10374
- return {
10375
- size: c.size,
10376
- path: c.path,
10377
- is_array: true
10378
- };
10379
- }
10380
- }
10381
- return null;
10382
- }
10383
- /************************************************************
10384
- * Join / split absolute path segments (kernel convention)
10385
- ************************************************************/
10386
- function seg_join(segments) {
10387
- return segments.map(function(s) {
10388
- return String(s);
10389
- }).join("`");
10390
- }
10391
- function seg_split(path) {
10392
- if (path === "" || path === null || path === void 0) return [];
10393
- return path.split("`");
10394
- }
10395
- /************************************************************
10396
- * Walk a JSON tree by absolute segments; undefined if the
10397
- * path does not resolve.
10398
- ************************************************************/
10399
- function get_by_segments(root, segments) {
10400
- let v = root;
10401
- for (let seg of segments) {
10402
- if (v === null || v === void 0) return;
10403
- if (Array.isArray(v)) v = v[Number(seg)];
10404
- else if (typeof v === "object") v = v[seg];
10405
- else return;
10406
- }
10407
- return v;
10408
- }
10409
- /************************************************************
10410
- * Set a value at absolute segments (mutates root).
10411
- *
10412
- * segments == [] means "replace the whole root" — the caller
10413
- * must use the RETURNED value in that case (a primitive/array
10414
- * root cannot be mutated in place).
10415
- ************************************************************/
10416
- function set_by_segments(root, segments, new_value) {
10417
- if (segments.length === 0) return new_value;
10418
- let parent = get_by_segments(root, segments.slice(0, -1));
10419
- if (parent === null || parent === void 0 || typeof parent !== "object") return root;
10420
- let last = segments[segments.length - 1];
10421
- if (Array.isArray(parent)) parent[Number(last)] = new_value;
10422
- else parent[last] = new_value;
10423
- return root;
10424
- }
10425
- /************************************************************
10426
- * Does `value` (or any loaded descendant / its own key)
10427
- * contain the lower-cased search term?
10428
- *
10429
- * Collapsed (not-yet-loaded) subtrees can't be searched, so
10430
- * they never match on content — only their key can match,
10431
- * which the renderer checks separately.
10432
- ************************************************************/
10433
- function subtree_matches(value, term) {
10434
- if (!term) return true;
10435
- if (is_collapsed(value)) return false;
10436
- let type = json_type(value);
10437
- if (type === "object") {
10438
- for (let [k, v] of Object.entries(value)) {
10439
- if (String(k).toLowerCase().includes(term)) return true;
10440
- if (subtree_matches(v, term)) return true;
10441
- }
10442
- return false;
10443
- }
10444
- if (type === "array") {
10445
- for (let v of value) if (subtree_matches(v, term)) return true;
10446
- return false;
10447
- }
10448
- return String(value).toLowerCase().includes(term);
10449
- }
10450
- /************************************************************
10451
- * Fields whose numeric value is an epoch timestamp.
10452
- * Mirrors the timestampTag list wired into C_YUI_FORM's
10453
- * jsoneditor control, so the two viewers agree.
10454
- ************************************************************/
10455
- function is_time_field(field) {
10456
- return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
10457
- }
10458
- /************************************************************
10459
- * Format an epoch value as a local wall-clock string.
10460
- * Accepts seconds or milliseconds; returns null when the
10461
- * value is not a usable positive timestamp (0 == "unset").
10462
- ************************************************************/
10463
- function format_epoch(value) {
10464
- if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
10465
- let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
10466
- let d = new Date(ms);
10467
- if (isNaN(d.getTime())) return null;
10468
- return d.toLocaleString();
10469
- }
10470
- /************************************************************
10471
- * The text view's payload: the document as text, indented
10472
- * four characters, cut at `max_chars`.
10473
- *
10474
- * Returns {text, capped}. `capped` is what the caller
10475
- * announces under the dump — a cut that is not announced
10476
- * reads as a document that ends there.
10477
- ************************************************************/
10478
- function json_text_dump(root, max_chars) {
10479
- let text;
10480
- try {
10481
- text = JSON.stringify(root, null, 4);
10482
- } catch (e) {
10483
- return {
10484
- text: "",
10485
- capped: false,
10486
- error: String(e)
10487
- };
10488
- }
10489
- if (text === void 0) return {
10490
- text: "",
10491
- capped: false,
10492
- error: "not representable as JSON"
10493
- };
10494
- if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
10495
- text: text.slice(0, max_chars),
10496
- capped: true
10497
- };
10498
- return {
10499
- text,
10500
- capped: false
10501
- };
10502
- }
10503
- /************************************************************
10504
- * Which view a freshly mounted viewer opens on.
10505
- *
10506
- * Three answers, in order of authority: what the HOST asked
10507
- * for, what the READER chose last time, and `fallback`.
10508
- *
10509
- * The host wins because a viewer mounted to show a graph has
10510
- * to show one. The memory comes next, because reopening a
10511
- * document in the view you just left is the whole point of
10512
- * remembering it. `fallback` is the floor.
10513
- *
10514
- * Anything not in `modes` is treated as unsaid, which covers
10515
- * an absent value, an empty one, and a stored string from an
10516
- * older release that named a view no longer offered.
10517
- ************************************************************/
10518
- function pick_view_mode(asked, remembered, modes, fallback) {
10519
- let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
10520
- if (known(asked)) return asked;
10521
- if (known(remembered)) return remembered;
10522
- return fallback;
10523
- }
10524
- /************************************************************
10525
- * The short label a container row carries beside its size.
10526
- *
10527
- * An array of dicts that carry an `id` is a list of RECORDS,
10528
- * and a row that says only `2: {15}` makes you open all
10529
- * fifteen fields of every one of them to find out WHICH
10530
- * record it is. The id is the one field that answers that,
10531
- * so the row says it without being opened.
10532
- *
10533
- * Only a SCALAR id, and only when it is really there: an `id`
10534
- * that is itself a dict or a list is not a name, it is more
10535
- * document, and printing `[object Object]` beside the size
10536
- * would be worse than printing nothing.
10537
- *
10538
- * Cut at `max_chars`, because this rides on a row whose job
10539
- * is to stay one line.
10540
- ************************************************************/
10541
- function container_label(value, max_chars) {
10542
- if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
10543
- if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
10544
- let id = value.id;
10545
- let t = typeof id;
10546
- if (t !== "string" && t !== "number" && t !== "boolean") return "";
10547
- let text = String(id).trim();
10548
- if (text === "") return "";
10549
- let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
10550
- if (cap && text.length > cap) return text.slice(0, cap) + "…";
10551
- return text;
10552
- }
10553
- /************************************************************
10554
- * A dict, in the JSON sense: not an array and not null.
10555
- ************************************************************/
10556
- function is_plain_object(v) {
10557
- return v !== null && typeof v === "object" && !Array.isArray(v);
10558
- }
10559
- /************************************************************
10560
- * Does this container hold ONLY other containers?
10561
- *
10562
- * Such a value is not a thing, it is a LIST of things -- and a
10563
- * list is fully described by the row that names it in its
10564
- * parent, plus the edges to what it holds. Drawing it as a
10565
- * card of its own put a node in the middle of the graph with no
10566
- * data in it, saying nothing the parent's row did not already
10567
- * say, and pushed everything below it one level down.
10568
- *
10569
- * An EMPTY container inside counts as content: `{0}` is
10570
- * something the card has to show, and there is no child card
10571
- * to show it instead.
10572
- ************************************************************/
10573
- function is_pure_collection(value) {
10574
- let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
10575
- if (entries.length === 0) return false;
10576
- for (let [, v] of entries) if (!has_branch(v)) return false;
10577
- return true;
10578
- }
10579
- /************************************************************
10580
- * A value that gets a card, or children, of its own.
10581
- ************************************************************/
10582
- function has_branch(value) {
10583
- return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
10584
- }
10585
- /************************************************************
10586
- * How many branches a container has, which is what the fold
10587
- * chip counts.
10588
- ************************************************************/
10589
- function count_branches(value) {
10590
- let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
10591
- let n = 0;
10592
- for (let v of entries) if (has_branch(v)) n++;
10593
- return n;
10594
- }
10595
10620
  //#endregion
10596
10621
  //#region src/c_yui_json_graph.js
10597
10622
  /***********************************************************************
@@ -62151,18 +62176,57 @@ function remote_command(gobj, command, kw) {
62151
62176
  return 0;
62152
62177
  }
62153
62178
  /***************************************************************
62154
- * The three answers are in: build the schema and remember the
62155
- * versions it arrived with. The baseline is what "this topic
62156
- * still needs its version raised" is measured against, so it is
62157
- * taken here and never touched again until the next load.
62179
+ * The three answers are in: build the schema.
62180
+ *
62181
+ * IT DOES NOT TOUCH THE BASELINE, and that is the whole point.
62182
+ * This runs again on the patch after EVERY write, so a baseline
62183
+ * taken here was re-taken as the version the write had just
62184
+ * raised -- and "written, and its version has not moved" then
62185
+ * read TRUE for a topic whose version had just moved.
62186
+ *
62187
+ * The banner that says so offers a Raise button, which wrote
62188
+ * another version, which was measured against itself again: the
62189
+ * banner came back, every time, and could not be dismissed. One
62190
+ * operator pressed it thirty-three times on one column delete.
62191
+ * It is why schema versions climbed by tens with the schema
62192
+ * unchanged.
62193
+ *
62194
+ * See start_measuring(): the baseline belongs to the LOAD, the
62195
+ * same reason `order_undo` was moved out of here before it.
62158
62196
  ***************************************************************/
62159
62197
  function build_model(gobj) {
62160
62198
  let priv = gobj.priv;
62161
62199
  priv.model = build_schema_model(priv.records);
62200
+ }
62201
+ /***************************************************************
62202
+ * From here on, "did this session change it?" has an answer.
62203
+ *
62204
+ * Called ONLY when records arrive from the store: that is what a
62205
+ * measurement of "changed since" can be measured against. Both
62206
+ * halves reset together -- a baseline without its `written` is
62207
+ * the same false positive from the other side, since every
62208
+ * version equals a baseline just taken.
62209
+ ***************************************************************/
62210
+ function start_measuring(gobj) {
62211
+ let priv = gobj.priv;
62162
62212
  priv.baseline = {};
62213
+ priv.written = {};
62214
+ if (!priv.model) return;
62163
62215
  for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
62164
62216
  }
62165
62217
  /***************************************************************
62218
+ * "This topic was written and its version did not move."
62219
+ *
62220
+ * ONE predicate, because it is asked in two places -- the topic
62221
+ * list draws a chip with it and the column view draws a banner --
62222
+ * and two copies of a rule is how they drift.
62223
+ ***************************************************************/
62224
+ function needs_version_bump(written, baseline, topic) {
62225
+ if (!topic || !written || !baseline) return false;
62226
+ if (!written[topic.id]) return false;
62227
+ return String(baseline[topic.id]) === String(topic.topic_version);
62228
+ }
62229
+ /***************************************************************
62166
62230
  * Say what the whole store would be refused for.
62167
62231
  *
62168
62232
  * Not for this view — it has a button for that — but for
@@ -62602,7 +62666,7 @@ function render_topics(gobj, $body) {
62602
62666
  let twice = repeated_names(treedb.topics);
62603
62667
  let $rows = [];
62604
62668
  for (let topic of treedb.topics) {
62605
- let pending = priv.written[topic.id] && String(priv.baseline[topic.id]) === String(topic.topic_version);
62669
+ let pending = needs_version_bump(priv.written, priv.baseline, topic);
62606
62670
  let ambiguous = !!twice[topic.name];
62607
62671
  let address = ambiguous ? topic.id : topic.name;
62608
62672
  let $actions = [];
@@ -62911,8 +62975,7 @@ function render_columns(gobj, $body) {
62911
62975
  ***************************************************************/
62912
62976
  function version_banner(gobj, topic) {
62913
62977
  let priv = gobj.priv;
62914
- if (!priv.written[topic.id]) return null;
62915
- if (String(priv.baseline[topic.id]) !== String(topic.topic_version)) return null;
62978
+ if (!needs_version_bump(priv.written, priv.baseline, topic)) return null;
62916
62979
  if (is_readonly(gobj)) return null;
62917
62980
  let $banner = (0, _yuneta_gobj_js.createElement2)([
62918
62981
  "div",
@@ -64478,6 +64541,7 @@ function ac_mt_command_answer(gobj, event, kw, src) {
64478
64541
  }
64479
64542
  priv.order_undo = {};
64480
64543
  build_model(gobj);
64544
+ start_measuring(gobj);
64481
64545
  return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
64482
64546
  }
64483
64547
  if (command === "update-node" || command === "delete-node") {