@yuneta/gobj-ui 7.23.185 → 7.23.187
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 +201 -43
- package/dist/gobj-ui.es.js +202 -44
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +105 -39
- package/src/c_yui_schema_editor.js +3 -1
- package/src/c_yui_treedb_topics.js +81 -11
- package/src/graph_save_plan.js +108 -0
- package/src/graph_save_plan.test.js +146 -0
- package/src/yui_icons.css +9 -0
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
kw_get_dict_value,
|
|
78
78
|
kw_set_dict_value,
|
|
79
79
|
kw_clone_by_keys,
|
|
80
|
+
json_deep_copy,
|
|
80
81
|
json_object_update,
|
|
81
82
|
json_object_update_missing,
|
|
82
83
|
json_object_size,
|
|
@@ -91,6 +92,11 @@ import {
|
|
|
91
92
|
safeSrc,
|
|
92
93
|
} from "@yuneta/gobj-js";
|
|
93
94
|
|
|
95
|
+
import {
|
|
96
|
+
plan_graph_saves,
|
|
97
|
+
topic_arrangement_changed,
|
|
98
|
+
} from "./graph_save_plan.js";
|
|
99
|
+
|
|
94
100
|
import {
|
|
95
101
|
addClasses,
|
|
96
102
|
removeClasses,
|
|
@@ -483,6 +489,7 @@ let PRIVATE_DATA = {
|
|
|
483
489
|
graph: null, // Instance of G6
|
|
484
490
|
__graphs__: [], // Rows of __graphs__
|
|
485
491
|
_graph_properties: {}, // topic_name → {nodes: {node_id: {x,y,size,...}}}
|
|
492
|
+
_saved_graph_properties: {}, // the same, as the BACKEND has it: what a Save compares against
|
|
486
493
|
yet_showed: false,
|
|
487
494
|
edit_mode: false,
|
|
488
495
|
operation_mode: null,
|
|
@@ -960,7 +967,12 @@ function build_graph(gobj)
|
|
|
960
967
|
container: priv.$container,
|
|
961
968
|
animation: false,
|
|
962
969
|
autoResize: false,
|
|
963
|
-
|
|
970
|
+
/* The floor was 0.2, and a treedb of a few hundred records does
|
|
971
|
+
* not fit in a screen at 20%: the reader could see the shape of
|
|
972
|
+
* the whole thing in the minimap and never on the canvas. Fit
|
|
973
|
+
* still stops at a legible zoom on purpose (graph_fit_readable);
|
|
974
|
+
* zooming out by hand is the reader's business. */
|
|
975
|
+
zoomRange: [0.02, 4],
|
|
964
976
|
node: { // WARNING this affect to all nodes with prevalence over individual defines!
|
|
965
977
|
palette: {
|
|
966
978
|
type: 'group',
|
|
@@ -2931,6 +2943,7 @@ function draw_link(
|
|
|
2931
2943
|
parent_topic: parent_topic,
|
|
2932
2944
|
parent_id: parent_id,
|
|
2933
2945
|
hook_name: hook_name,
|
|
2946
|
+
child_topic: child_topic,
|
|
2934
2947
|
child_id: child_id,
|
|
2935
2948
|
is_tree: is_tree,
|
|
2936
2949
|
});
|
|
@@ -3051,6 +3064,7 @@ function build_graph_properties(gobj)
|
|
|
3051
3064
|
{
|
|
3052
3065
|
let priv = gobj.priv;
|
|
3053
3066
|
priv._graph_properties = {};
|
|
3067
|
+
priv._saved_graph_properties = {};
|
|
3054
3068
|
|
|
3055
3069
|
for(let i = 0; i < priv.__graphs__.length; i++) {
|
|
3056
3070
|
let rec = priv.__graphs__[i];
|
|
@@ -3060,6 +3074,11 @@ function build_graph_properties(gobj)
|
|
|
3060
3074
|
let props = rec.properties;
|
|
3061
3075
|
if(is_object(props)) {
|
|
3062
3076
|
priv._graph_properties[rec.topic] = props;
|
|
3077
|
+
/* A COPY: the line above hands the record's own object to
|
|
3078
|
+
* the view, which then arranges it in place, so anything
|
|
3079
|
+
* kept by reference would change with it and a Save would
|
|
3080
|
+
* never find a difference. */
|
|
3081
|
+
priv._saved_graph_properties[rec.topic] = json_deep_copy(props);
|
|
3063
3082
|
}
|
|
3064
3083
|
}
|
|
3065
3084
|
}
|
|
@@ -3205,34 +3224,30 @@ function save_geometry(gobj)
|
|
|
3205
3224
|
update_edge_geometry(gobj, edges[i].id);
|
|
3206
3225
|
}
|
|
3207
3226
|
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
options: {
|
|
3225
|
-
list_dict: true,
|
|
3226
|
-
autolink: false,
|
|
3227
|
-
create: true // Create if doesn't exist, update if it does
|
|
3228
|
-
}
|
|
3229
|
-
};
|
|
3230
|
-
gobj_publish_event(gobj, "EV_UPDATE_NODE", kw_update);
|
|
3227
|
+
/* One __graphs__ record per topic that CHANGED, and nothing for
|
|
3228
|
+
* the rest. The Save button says that something happened, never
|
|
3229
|
+
* what: it lights from `history.canUndo()` for the moves G6
|
|
3230
|
+
* records and from mark_graph_dirty() for everything it does not
|
|
3231
|
+
* (a colour, a pill, a per-topic default). So the topics are
|
|
3232
|
+
* chosen by comparing what the view holds against what the
|
|
3233
|
+
* backend has -- which sees both kinds of change, and goes on
|
|
3234
|
+
* working after an undo, a redo, or the history.clear() that
|
|
3235
|
+
* ac_save_graph() does one line before calling this.
|
|
3236
|
+
*
|
|
3237
|
+
* The store is append-only: a topic written with the content it
|
|
3238
|
+
* already had is a record that says nothing, for ever. Moving one
|
|
3239
|
+
* card of a five-topic treedb used to append five. */
|
|
3240
|
+
let plan = plan_graph_saves(priv._graph_properties, priv._saved_graph_properties);
|
|
3241
|
+
for(let i = 0; i < plan.length; i++) {
|
|
3242
|
+
save_topic_graph_properties(gobj, plan[i]);
|
|
3231
3243
|
}
|
|
3232
3244
|
}
|
|
3233
3245
|
|
|
3234
3246
|
/************************************************************
|
|
3235
3247
|
* Save __graphs__ properties for a single topic to backend.
|
|
3248
|
+
*
|
|
3249
|
+
* The ONE door to `__graphs__`: a caller that knows a topic moved
|
|
3250
|
+
* says so, and this decides whether the backend needs telling.
|
|
3236
3251
|
************************************************************/
|
|
3237
3252
|
function save_topic_graph_properties(gobj, topic_name)
|
|
3238
3253
|
{
|
|
@@ -3242,9 +3257,30 @@ function save_topic_graph_properties(gobj, topic_name)
|
|
|
3242
3257
|
return;
|
|
3243
3258
|
}
|
|
3244
3259
|
|
|
3260
|
+
/* A record that says what the record under it says is a record
|
|
3261
|
+
* that will be read for ever and mean nothing -- the store is
|
|
3262
|
+
* append-only. It reached here by two doors: the Save, which
|
|
3263
|
+
* wrote every topic the view had loaded, and the delete of a
|
|
3264
|
+
* node that carried no geometry of its own. */
|
|
3265
|
+
if(!topic_arrangement_changed(properties, priv._saved_graph_properties[topic_name])) {
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3245
3269
|
let origin = gobj_read_str_attr(gobj_yuno(), "node_uuid");
|
|
3246
3270
|
properties.__origin__ = origin;
|
|
3247
3271
|
|
|
3272
|
+
/* What the backend will hold once this is written. Taken BEFORE
|
|
3273
|
+
* the publish and as a copy, because the object below travels by
|
|
3274
|
+
* reference and goes on being arranged after the write.
|
|
3275
|
+
*
|
|
3276
|
+
* Nothing answers an `EV_UPDATE_NODE` of `__graphs__` -- the view
|
|
3277
|
+
* writes that topic itself and is deliberately not told about it
|
|
3278
|
+
* (see the EV_RECORD_WRITTEN of C_YUI_TREEDB_GRAPH) -- so this is
|
|
3279
|
+
* the write taken for granted. A refused one leaves the view
|
|
3280
|
+
* believing the backend holds an arrangement it does not, until
|
|
3281
|
+
* the next load reads the records again. */
|
|
3282
|
+
priv._saved_graph_properties[topic_name] = json_deep_copy(properties);
|
|
3283
|
+
|
|
3248
3284
|
let kw_update = {
|
|
3249
3285
|
treedb_name: priv.treedb_name,
|
|
3250
3286
|
topic_name: "__graphs__",
|
|
@@ -3688,7 +3724,7 @@ async function graph_fit_selection(gobj, ids)
|
|
|
3688
3724
|
let bh = Math.max(1, maxy - miny);
|
|
3689
3725
|
|
|
3690
3726
|
let zoom = Math.min(vw / bw, vh / bh);
|
|
3691
|
-
let range = graph.getZoomRange() || [0.
|
|
3727
|
+
let range = graph.getZoomRange() || [0.02, 4];
|
|
3692
3728
|
zoom = Math.max(range[0], Math.min(range[1], zoom));
|
|
3693
3729
|
|
|
3694
3730
|
await graph.zoomTo(zoom);
|
|
@@ -7765,7 +7801,6 @@ function refresh_default_edges_theme(gobj, theme)
|
|
|
7765
7801
|
if(!graph) {
|
|
7766
7802
|
return;
|
|
7767
7803
|
}
|
|
7768
|
-
let dark = (theme === "dark");
|
|
7769
7804
|
let edges = graph.getData().edges || [];
|
|
7770
7805
|
let updates = [];
|
|
7771
7806
|
for(let i = 0; i < edges.length; i++) {
|
|
@@ -7773,10 +7808,7 @@ function refresh_default_edges_theme(gobj, theme)
|
|
|
7773
7808
|
if(!d.themed_default) {
|
|
7774
7809
|
continue;
|
|
7775
7810
|
}
|
|
7776
|
-
|
|
7777
|
-
? (dark ? '#22a7c2' : '#0e7490')
|
|
7778
|
-
: (dark ? '#8b94a3' : '#6b7280');
|
|
7779
|
-
updates.push({ id: edges[i].id, style: { stroke: stroke } });
|
|
7811
|
+
updates.push({id: edges[i].id, style: {stroke: default_edge_stroke(gobj, d, theme)}});
|
|
7780
7812
|
}
|
|
7781
7813
|
if(updates.length > 0) {
|
|
7782
7814
|
graph.updateEdgeData(updates);
|
|
@@ -8658,26 +8690,60 @@ function apply_edge_properties(gobj, edge_id, lineWidth, stroke, scope)
|
|
|
8658
8690
|
deselect_edge(gobj);
|
|
8659
8691
|
}
|
|
8660
8692
|
|
|
8693
|
+
/************************************************************
|
|
8694
|
+
* The colour a link is DRAWN in: the colour of the two knobs it
|
|
8695
|
+
* joins.
|
|
8696
|
+
*
|
|
8697
|
+
* Both ends of a link wear the CHILD topic's colour -- the
|
|
8698
|
+
* parent's hook port is painted with the colour of whatever may
|
|
8699
|
+
* hang there, and the child's fkey port with its own (see
|
|
8700
|
+
* build_ports) -- so the line between them has one obvious
|
|
8701
|
+
* colour, and a reader follows a link by its hue instead of
|
|
8702
|
+
* tracing a grey thread across the canvas.
|
|
8703
|
+
*
|
|
8704
|
+
* It goes through port_rim(): the FILL of a port says which
|
|
8705
|
+
* topic, and a line on the canvas is a graphical object that
|
|
8706
|
+
* needs 3:1 against it, which that mix clears in both themes
|
|
8707
|
+
* (9.11 dark, 3.80 light) while keeping the hue. So a link is
|
|
8708
|
+
* drawn in exactly the colour of the rim of the two knobs it
|
|
8709
|
+
* ends on.
|
|
8710
|
+
*
|
|
8711
|
+
* The containment/tree relation (parent and child of the same
|
|
8712
|
+
* topic) is no longer marked by a teal of its own -- it is that
|
|
8713
|
+
* topic's colour like any other link, and what still tells it
|
|
8714
|
+
* apart is its WIDTH.
|
|
8715
|
+
*
|
|
8716
|
+
* A topic with no colour, or an edge of a graph with no schema
|
|
8717
|
+
* yet, keeps the neutral grey this used to give everything.
|
|
8718
|
+
************************************************************/
|
|
8719
|
+
function default_edge_stroke(gobj, d, theme)
|
|
8720
|
+
{
|
|
8721
|
+
let priv = gobj.priv;
|
|
8722
|
+
let desc = is_object(priv.descs)? priv.descs[d.child_topic] : null;
|
|
8723
|
+
if(desc && desc.color) {
|
|
8724
|
+
return port_rim(desc.color, theme);
|
|
8725
|
+
}
|
|
8726
|
+
let dark = (theme === "dark");
|
|
8727
|
+
return d.is_tree? (dark? '#22a7c2' : '#0e7490') : (dark? '#8b94a3' : '#6b7280');
|
|
8728
|
+
}
|
|
8729
|
+
|
|
8661
8730
|
/************************************************************
|
|
8662
8731
|
* The style an edge INHERITS, before anything saved for the
|
|
8663
8732
|
* edge itself: its hook's default in the parent topic, else the
|
|
8664
8733
|
* parent topic's default for every edge, else the library's.
|
|
8665
8734
|
*
|
|
8666
|
-
* The library's:
|
|
8667
|
-
*
|
|
8668
|
-
*
|
|
8669
|
-
* library's, so a theme toggle re-themes it and leaves
|
|
8670
|
-
* one alone.
|
|
8735
|
+
* The library's: the colour of the ports it joins (see
|
|
8736
|
+
* default_edge_stroke), and a wider line for the
|
|
8737
|
+
* containment/tree relation. `themed_default` says the colour is
|
|
8738
|
+
* still the library's, so a theme toggle re-themes it and leaves
|
|
8739
|
+
* a chosen one alone.
|
|
8671
8740
|
************************************************************/
|
|
8672
8741
|
function edge_inherited_style_of(gobj, d)
|
|
8673
8742
|
{
|
|
8674
8743
|
let priv = gobj.priv;
|
|
8675
|
-
let dark = (priv.theme === "dark");
|
|
8676
8744
|
let style = {
|
|
8677
8745
|
lineWidth: d.is_tree ? 2 : 1.6,
|
|
8678
|
-
stroke: d.
|
|
8679
|
-
? (dark ? '#22a7c2' : '#0e7490')
|
|
8680
|
-
: (dark ? '#8b94a3' : '#6b7280'),
|
|
8746
|
+
stroke: default_edge_stroke(gobj, d, priv.theme),
|
|
8681
8747
|
themed_default: true,
|
|
8682
8748
|
};
|
|
8683
8749
|
|
|
@@ -810,7 +810,9 @@ function render_toolbar(gobj)
|
|
|
810
810
|
*----------------------------------------------*/
|
|
811
811
|
let $right = [];
|
|
812
812
|
if(priv.treedb_id && has_model) {
|
|
813
|
-
|
|
813
|
+
/* The compass, like every other door to a SCHEMA: the
|
|
814
|
+
* `hexagon-nodes` it wore is the graph of the DATA. */
|
|
815
|
+
$right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-compass-drafting",
|
|
814
816
|
priv.diagram ? "topics" : "diagram", "EV_TOGGLE_DIAGRAM", false));
|
|
815
817
|
$right.push(toolbar_button("SCHEMA_VALIDATE_BTN", "yi-square-check",
|
|
816
818
|
"check", "EV_VALIDATE", false));
|
|
@@ -335,12 +335,19 @@ function build_ui(gobj)
|
|
|
335
335
|
}
|
|
336
336
|
}],
|
|
337
337
|
/* Landing view toggle: cards grid <-> schema graph
|
|
338
|
-
* (topics as nodes). Shown only on the landing.
|
|
338
|
+
* (topics as nodes). Shown only on the landing.
|
|
339
|
+
*
|
|
340
|
+
* The glyph is the draughtsman's compass, not the
|
|
341
|
+
* `hexagon-nodes` of the graph: the button one place
|
|
342
|
+
* to its left opens the graph of the DATA, and the
|
|
343
|
+
* two wore the same shape with only their labels
|
|
344
|
+
* telling them apart -- which on a narrow toolbar is
|
|
345
|
+
* nothing. A schema is the PLAN of the treedb. */
|
|
339
346
|
['button', {class: 'button TREEDB_LANDING_TOGGLE is-hidden',
|
|
340
347
|
title: t('schema graph'), 'aria-label': t('schema graph'),
|
|
341
348
|
'data-i18n-title': 'schema graph',
|
|
342
349
|
'data-i18n-aria-label': 'schema graph'}, [
|
|
343
|
-
['span', {class: 'icon'}, [yui_toolbar_icon('yi-
|
|
350
|
+
['span', {class: 'icon'}, [yui_toolbar_icon('yi-compass-drafting')]],
|
|
344
351
|
['span', {i18n: 'schema'}, 'schema']
|
|
345
352
|
], {
|
|
346
353
|
click: (evt) => {
|
|
@@ -348,6 +355,24 @@ function build_ui(gobj)
|
|
|
348
355
|
gobj_send_event(gobj, "EV_TOGGLE_LANDING_VIEW", {}, gobj);
|
|
349
356
|
}
|
|
350
357
|
}],
|
|
358
|
+
/* The schema as JSON -- the `descs` the view is drawing,
|
|
359
|
+
* which is what a person writing a schema literal, or
|
|
360
|
+
* chasing a flag that did not take, actually needs to
|
|
361
|
+
* read. Landing-only, like the toggle it stands next
|
|
362
|
+
* to; the `raw json` button further right is another
|
|
363
|
+
* thing entirely (the tranger's content). */
|
|
364
|
+
['button', {class: 'button TREEDB_SCHEMA_JSON_BTN is-hidden',
|
|
365
|
+
title: t('schema json'), 'aria-label': t('schema json'),
|
|
366
|
+
'data-i18n-title': 'schema json',
|
|
367
|
+
'data-i18n-aria-label': 'schema json'}, [
|
|
368
|
+
['span', {class: 'icon'}, [yui_toolbar_icon('yi-code')]],
|
|
369
|
+
['span', {i18n: 'schema json'}, 'schema json']
|
|
370
|
+
], {
|
|
371
|
+
click: (evt) => {
|
|
372
|
+
evt.stopPropagation();
|
|
373
|
+
gobj_send_event(gobj, "EV_OPEN_SCHEMA_JSON", {}, gobj);
|
|
374
|
+
}
|
|
375
|
+
}],
|
|
351
376
|
/* The backend this view browses. The tab that hosts this
|
|
352
377
|
* view is labelled with the TREEDB name, and the same
|
|
353
378
|
* treedb name lives on more than one node — two tabs
|
|
@@ -817,6 +842,7 @@ function show_topics_landing(gobj)
|
|
|
817
842
|
let $info = $container.querySelector(".TREEDB_TOPIC_INFO");
|
|
818
843
|
let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
|
|
819
844
|
let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
|
|
845
|
+
let $schema_json = $container.querySelector(".TREEDB_SCHEMA_JSON_BTN");
|
|
820
846
|
if($tabs) {
|
|
821
847
|
$tabs.classList.add("is-hidden");
|
|
822
848
|
}
|
|
@@ -835,6 +861,11 @@ function show_topics_landing(gobj)
|
|
|
835
861
|
$toggle.classList.remove("is-hidden"); /* the toggle lives on the landing */
|
|
836
862
|
$toggle.classList.toggle("is-primary", schema_mode);
|
|
837
863
|
}
|
|
864
|
+
if($schema_json) {
|
|
865
|
+
/* The schema's json is worth reading when the schema is what is
|
|
866
|
+
* on screen; on the cards grid it is one more button in the way. */
|
|
867
|
+
$schema_json.classList.toggle("is-hidden", !schema_mode);
|
|
868
|
+
}
|
|
838
869
|
if(schema_mode) {
|
|
839
870
|
if($landing) {
|
|
840
871
|
$landing.classList.add("is-hidden");
|
|
@@ -946,6 +977,7 @@ function show_topic_detail(gobj)
|
|
|
946
977
|
let $info = $container.querySelector(".TREEDB_TOPIC_INFO");
|
|
947
978
|
let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
|
|
948
979
|
let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
|
|
980
|
+
let $schema_json = $container.querySelector(".TREEDB_SCHEMA_JSON_BTN");
|
|
949
981
|
if($landing) {
|
|
950
982
|
$landing.classList.add("is-hidden");
|
|
951
983
|
}
|
|
@@ -955,6 +987,9 @@ function show_topic_detail(gobj)
|
|
|
955
987
|
if($info) {
|
|
956
988
|
$info.classList.add("is-hidden");
|
|
957
989
|
}
|
|
990
|
+
if($schema_json) {
|
|
991
|
+
$schema_json.classList.add("is-hidden");
|
|
992
|
+
}
|
|
958
993
|
if($toggle) {
|
|
959
994
|
$toggle.classList.add("is-hidden"); /* landing-only */
|
|
960
995
|
}
|
|
@@ -1010,12 +1045,16 @@ function show_topic_info(gobj, topic)
|
|
|
1010
1045
|
let $schema = $container.querySelector(".TREEDB_TOPICS_SCHEMA");
|
|
1011
1046
|
let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
|
|
1012
1047
|
let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
|
|
1048
|
+
let $schema_json = $container.querySelector(".TREEDB_SCHEMA_JSON_BTN");
|
|
1013
1049
|
if($landing) {
|
|
1014
1050
|
$landing.classList.add("is-hidden");
|
|
1015
1051
|
}
|
|
1016
1052
|
if($schema) {
|
|
1017
1053
|
$schema.classList.add("is-hidden");
|
|
1018
1054
|
}
|
|
1055
|
+
if($schema_json) {
|
|
1056
|
+
$schema_json.classList.add("is-hidden");
|
|
1057
|
+
}
|
|
1019
1058
|
if($toggle) {
|
|
1020
1059
|
$toggle.classList.add("is-hidden");
|
|
1021
1060
|
}
|
|
@@ -1307,7 +1346,7 @@ function register_sub_routes(gobj)
|
|
|
1307
1346
|
let nodes = [];
|
|
1308
1347
|
if(is_object(gobj_read_attr(gobj, "landing_routes"))) {
|
|
1309
1348
|
nodes.push({route: base + "/schema", label: "schema",
|
|
1310
|
-
icon: "yi-
|
|
1349
|
+
icon: "yi-compass-drafting", gclass: "C_YUI_TREEDB_SCHEMA"});
|
|
1311
1350
|
}
|
|
1312
1351
|
if(is_object(descs)) {
|
|
1313
1352
|
for(let topic of Object.keys(descs)) {
|
|
@@ -1655,13 +1694,18 @@ function is_mobile()
|
|
|
1655
1694
|
* whole service (lazy drill). CHILD model: the viewer publishes
|
|
1656
1695
|
* EV_EXPAND_PATH to us.
|
|
1657
1696
|
************************************************************/
|
|
1658
|
-
function open_json_viewer(gobj)
|
|
1697
|
+
function open_json_viewer(gobj, opts)
|
|
1659
1698
|
{
|
|
1660
1699
|
let priv = gobj.priv;
|
|
1700
|
+
let schema = !!(opts && opts.schema);
|
|
1661
1701
|
|
|
1662
|
-
/* Already open: just re-fetch.
|
|
1702
|
+
/* Already open: just re-fetch. The schema needs no fetch -- it is
|
|
1703
|
+
* the `descs` this view already holds -- so a second click on a
|
|
1704
|
+
* viewer that is already showing it does nothing. */
|
|
1663
1705
|
if(priv.json_win || priv.json_modal) {
|
|
1664
|
-
|
|
1706
|
+
if(!schema) {
|
|
1707
|
+
request_print_tranger(gobj, "");
|
|
1708
|
+
}
|
|
1665
1709
|
return;
|
|
1666
1710
|
}
|
|
1667
1711
|
|
|
@@ -1681,7 +1725,11 @@ function open_json_viewer(gobj)
|
|
|
1681
1725
|
/* No `title`: the host titles it — the window's title bar on
|
|
1682
1726
|
* desktop, the dialog's header on mobile. The viewer's own
|
|
1683
1727
|
* title would land INSIDE that host, doubling it. */
|
|
1684
|
-
subscriber: gobj
|
|
1728
|
+
subscriber: gobj, /* publishes EV_EXPAND_PATH to us */
|
|
1729
|
+
/* The schema is here already and it is small: it goes in
|
|
1730
|
+
* whole, and no drill-down happens because nothing is
|
|
1731
|
+
* collapsed. */
|
|
1732
|
+
json_data: schema? gobj_read_attr(gobj, "descs") : null
|
|
1685
1733
|
},
|
|
1686
1734
|
gobj
|
|
1687
1735
|
);
|
|
@@ -1703,7 +1751,7 @@ function open_json_viewer(gobj)
|
|
|
1703
1751
|
dialog: true,
|
|
1704
1752
|
logical_class: "TREEDB_JSON_SHEET",
|
|
1705
1753
|
title_prefix: priv.treedb_name,
|
|
1706
|
-
title: "raw json",
|
|
1754
|
+
title: schema? "schema json" : "raw json",
|
|
1707
1755
|
t: t,
|
|
1708
1756
|
on_close: () => {
|
|
1709
1757
|
if(gobj_is_destroying(gobj)) {
|
|
@@ -1733,8 +1781,8 @@ function open_json_viewer(gobj)
|
|
|
1733
1781
|
height: 620,
|
|
1734
1782
|
logical_class: "TREEDB_JSON_WINDOW",
|
|
1735
1783
|
title_prefix: priv.treedb_name,
|
|
1736
|
-
title: "raw json",
|
|
1737
|
-
icon: "yi-eye",
|
|
1784
|
+
title: schema? "schema json" : "raw json",
|
|
1785
|
+
icon: schema? "yi-code" : "yi-eye",
|
|
1738
1786
|
body: $box,
|
|
1739
1787
|
manager: null,
|
|
1740
1788
|
on_close: () => {
|
|
@@ -1754,7 +1802,9 @@ function open_json_viewer(gobj)
|
|
|
1754
1802
|
gobj_start(priv.json_win);
|
|
1755
1803
|
}
|
|
1756
1804
|
|
|
1757
|
-
|
|
1805
|
+
if(!schema) {
|
|
1806
|
+
request_print_tranger(gobj, "");
|
|
1807
|
+
}
|
|
1758
1808
|
}
|
|
1759
1809
|
|
|
1760
1810
|
/************************************************************
|
|
@@ -2718,6 +2768,24 @@ function ac_open_json(gobj, event, kw, src)
|
|
|
2718
2768
|
return 0;
|
|
2719
2769
|
}
|
|
2720
2770
|
|
|
2771
|
+
/********************************************
|
|
2772
|
+
* The schema, as the json it is: the `descs` the schema graph is
|
|
2773
|
+
* drawing. Same viewer and same host as the tranger's raw json --
|
|
2774
|
+
* one at a time, so it closes whatever was open first.
|
|
2775
|
+
********************************************/
|
|
2776
|
+
function ac_open_schema_json(gobj, event, kw, src)
|
|
2777
|
+
{
|
|
2778
|
+
if(!is_object(gobj_read_attr(gobj, "descs"))) {
|
|
2779
|
+
/* Not reachable by hand -- the button only exists while the
|
|
2780
|
+
* schema is on screen, which needs the descs -- so this is a
|
|
2781
|
+
* broken caller, not something to tell the reader about. */
|
|
2782
|
+
log_error(`${gobj_short_name(gobj)}: schema json with no descs`);
|
|
2783
|
+
return -1;
|
|
2784
|
+
}
|
|
2785
|
+
open_json_viewer(gobj, {schema: true});
|
|
2786
|
+
return 0;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2721
2789
|
/********************************************
|
|
2722
2790
|
* Open the graph where the reader left it: its last visited
|
|
2723
2791
|
* route under the graph's base -- `<graph>/<topic>` when a topic
|
|
@@ -2823,6 +2891,7 @@ function create_gclass(gclass_name)
|
|
|
2823
2891
|
["EV_DELETE_RECORD", ac_delete_record, null],
|
|
2824
2892
|
["EV_REFRESH_TOPIC", ac_refresh_topic, null],
|
|
2825
2893
|
["EV_OPEN_JSON", ac_open_json, null],
|
|
2894
|
+
["EV_OPEN_SCHEMA_JSON", ac_open_schema_json, null],
|
|
2826
2895
|
["EV_OPEN_GRAPH", ac_open_graph, null],
|
|
2827
2896
|
["EV_EXPAND_PATH", ac_json_expand_path, null],
|
|
2828
2897
|
["EV_JSON_CLOSED", ac_json_closed, null],
|
|
@@ -2857,6 +2926,7 @@ function create_gclass(gclass_name)
|
|
|
2857
2926
|
event_flag_t.EVF_NO_WARN_SUBS],
|
|
2858
2927
|
["EV_REFRESH_TOPIC", 0],
|
|
2859
2928
|
["EV_OPEN_JSON", 0],
|
|
2929
|
+
["EV_OPEN_SCHEMA_JSON", 0],
|
|
2860
2930
|
["EV_OPEN_GRAPH", 0],
|
|
2861
2931
|
["EV_EXPAND_PATH", 0],
|
|
2862
2932
|
["EV_JSON_CLOSED", 0],
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* graph_save_plan.js
|
|
3
|
+
*
|
|
4
|
+
* Which topics a Save of the graph has to write, and which it
|
|
5
|
+
* must leave alone.
|
|
6
|
+
*
|
|
7
|
+
* The arrangement of a treedb graph lives in `__graphs__`, ONE
|
|
8
|
+
* record per topic. The Save collected every topic the view had
|
|
9
|
+
* loaded and wrote all of them, whatever had moved: on a treedb
|
|
10
|
+
* of five topics, dragging one card appended five records, four
|
|
11
|
+
* of them saying exactly what the record under them said. The
|
|
12
|
+
* store is append-only, so those four are there for ever.
|
|
13
|
+
*
|
|
14
|
+
* This module is the decision, and nothing else: no G6, no DOM,
|
|
15
|
+
* no gobj. Give it what the view is holding and what the backend
|
|
16
|
+
* already has, and it answers which topics differ.
|
|
17
|
+
*
|
|
18
|
+
* Two things it deliberately does NOT do:
|
|
19
|
+
*
|
|
20
|
+
* - It does not ask the history plugin. The Save button lights
|
|
21
|
+
* from two places -- `history.canUndo()` for what G6 records
|
|
22
|
+
* (a move, a resize) and `mark_graph_dirty()` for what it does
|
|
23
|
+
* not (a colour, a pill, a per-topic default) -- so the undo
|
|
24
|
+
* stack is not the list of what changed, and reading it as one
|
|
25
|
+
* would silently drop half the changes. A value comparison
|
|
26
|
+
* sees both, and keeps working through undo, redo and a
|
|
27
|
+
* history that was just cleared.
|
|
28
|
+
*
|
|
29
|
+
* - It does not compare `__origin__`. That key is the node_uuid
|
|
30
|
+
* of whoever wrote the record, bookkeeping about the write and
|
|
31
|
+
* not about the arrangement; comparing it would make every
|
|
32
|
+
* topic differ the first time a second browser saved, and the
|
|
33
|
+
* record it forces would say nothing new.
|
|
34
|
+
*
|
|
35
|
+
* Copyright (c) 2026, ArtGins.
|
|
36
|
+
* All Rights Reserved.
|
|
37
|
+
***********************************************************************/
|
|
38
|
+
import {
|
|
39
|
+
is_object,
|
|
40
|
+
json_is_identical,
|
|
41
|
+
} from "@yuneta/gobj-js";
|
|
42
|
+
|
|
43
|
+
/***************************************************************
|
|
44
|
+
* The properties as they are worth comparing: everything
|
|
45
|
+
* except who wrote them.
|
|
46
|
+
***************************************************************/
|
|
47
|
+
function arrangement_of(properties)
|
|
48
|
+
{
|
|
49
|
+
if(!is_object(properties)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
let out = {};
|
|
53
|
+
for(const [key, value] of Object.entries(properties)) {
|
|
54
|
+
if(key === "__origin__") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
out[key] = value;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/***************************************************************
|
|
63
|
+
* TRUE when the arrangement the view holds for `topic_name`
|
|
64
|
+
* is not the one the backend already has.
|
|
65
|
+
*
|
|
66
|
+
* A topic with no saved record has never been written, so
|
|
67
|
+
* anything it holds is a change -- and an EMPTY arrangement
|
|
68
|
+
* is not: a topic the view loaded and nobody ever arranged
|
|
69
|
+
* has nothing to say, and saying it would create a record
|
|
70
|
+
* that means "no layout".
|
|
71
|
+
***************************************************************/
|
|
72
|
+
function topic_arrangement_changed(live, saved)
|
|
73
|
+
{
|
|
74
|
+
let a = arrangement_of(live);
|
|
75
|
+
if(!saved) {
|
|
76
|
+
return Object.keys(a).length > 0;
|
|
77
|
+
}
|
|
78
|
+
return !json_is_identical(a, arrangement_of(saved));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/***************************************************************
|
|
82
|
+
* The topics a Save has to write.
|
|
83
|
+
*
|
|
84
|
+
* live {topic_name: properties} what the view holds
|
|
85
|
+
* saved {topic_name: properties} what the backend has
|
|
86
|
+
*
|
|
87
|
+
* Returns their names, in the order the view holds them.
|
|
88
|
+
***************************************************************/
|
|
89
|
+
function plan_graph_saves(live, saved)
|
|
90
|
+
{
|
|
91
|
+
let plan = [];
|
|
92
|
+
if(!is_object(live)) {
|
|
93
|
+
return plan;
|
|
94
|
+
}
|
|
95
|
+
let have = is_object(saved)? saved: {};
|
|
96
|
+
for(const [topic_name, properties] of Object.entries(live)) {
|
|
97
|
+
if(topic_arrangement_changed(properties, have[topic_name])) {
|
|
98
|
+
plan.push(topic_name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return plan;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
plan_graph_saves,
|
|
106
|
+
topic_arrangement_changed,
|
|
107
|
+
arrangement_of,
|
|
108
|
+
};
|