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