@yuneta/gobj-ui 7.25.7 → 7.25.8

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.
@@ -206,6 +206,7 @@ let PRIVATE_DATA = {
206
206
  pending: 0, /* `nodes` requests still in flight */
207
207
  load_error: "",
208
208
  load_round: 0, /* which load an answer belongs to: echoed as `round` */
209
+ load_keeps_written: false, /* the load in flight is not the host's: `written` survives it */
209
210
 
210
211
  /* Where the operator is. The url carries it; these mirror it. */
211
212
  treedb_id: "",
@@ -458,8 +459,25 @@ function refuse_if_readonly(gobj, event)
458
459
  * schema set of the yuno is here: everything below — the
459
460
  * drawing, the validation, the export — is computed from these
460
461
  * and asks the backend nothing more.
462
+ *
463
+ * THE LOADING SCREEN IS DRAWN, not assumed. ST_LOADING declares
464
+ * none of the actions of a screen, so the screen it replaces goes
465
+ * and the toolbar stops answering: left up, a click on a card, on
466
+ * Back or on the drawing answered "Event NOT DEFINED in state
467
+ * ST_LOADING" and was lost (third independent review). A dialog
468
+ * left open is answered in ST_LOADING instead (ac_wait_for_the_load).
469
+ *
470
+ * `keep_written`: a load the host did not ask for -- the reconnect,
471
+ * a write that turned out done -- keeps what this session wrote.
472
+ * Only the host's EV_REFRESH (a Save sends it) forgets it.
473
+ *
474
+ * A load that cannot leave WHOLE did not happen (out of session it
475
+ * is not even asked): the records the model was built on are put
476
+ * back (emptied, the next write patched them into a model with no
477
+ * treedb), the round is spent so a request that did leave is
478
+ * stale, and the session back asks again.
461
479
  ***************************************************************/
462
- function request_model(gobj)
480
+ function request_model(gobj, keep_written)
463
481
  {
464
482
  let priv = gobj.priv;
465
483
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
@@ -468,27 +486,41 @@ function request_model(gobj)
468
486
  return -1; /* not mounted on a transport yet: mt_start retries via EV_TRANSPORT_STATE */
469
487
  }
470
488
 
489
+ let previous = priv.records;
471
490
  priv.records = {treedbs: [], topics: [], cols: []};
472
491
  priv.load_error = "";
473
492
  priv.pending = 0;
474
493
  priv.load_round++;
494
+ priv.load_keeps_written = !!keep_written;
475
495
  gobj_change_state(gobj, "ST_LOADING");
476
- show_notice(gobj, "loading the schemas");
477
-
478
- for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
479
- if(remote_command(gobj, "nodes", {
480
- topic_name: topic_name,
481
- options: {list_dict: true}
482
- }, {round: priv.load_round}) === 0) {
483
- priv.pending++;
496
+ set_busy(gobj, false);
497
+ render(gobj);
498
+
499
+ /* Out of session nothing is asked: each request would come back
500
+ * refused and logged as an ERROR, for a drop that is not one. */
501
+ let left = 0;
502
+ if(transport_in_session(gobj)) {
503
+ for(let topic_name of [T_TREEDBS, T_TOPICS, T_COLS]) {
504
+ if(remote_command(gobj, "nodes", {
505
+ topic_name: topic_name,
506
+ options: {list_dict: true}
507
+ }, {round: priv.load_round}) === 0) {
508
+ left++;
509
+ }
484
510
  }
485
511
  }
486
- if(priv.pending === 0) {
512
+ if(left < 3) {
513
+ priv.records = previous;
514
+ priv.load_round++;
515
+ priv.reload_on_open = true;
487
516
  priv.load_error = t("cannot reach the treedb");
488
- gobj_change_state(gobj, "ST_IDLE");
489
- show_notice(gobj, "cannot reach the treedb");
517
+ if(priv.model) {
518
+ yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", {t: t});
519
+ }
520
+ end_load(gobj);
490
521
  return -1;
491
522
  }
523
+ priv.pending = left;
492
524
  return 0;
493
525
  }
494
526
 
@@ -571,17 +603,23 @@ function build_model(gobj)
571
603
  * From here on, "did this session change it?" has an answer.
572
604
  *
573
605
  * Called ONLY when records arrive from the store: that is what a
574
- * measurement of "changed since" can be measured against. Both
575
- * halves reset together -- a baseline without its `written` is
576
- * the same false positive from the other side, since every
577
- * version equals a baseline just taken.
606
+ * measurement of "changed since" can be measured against.
607
+ *
608
+ * `written` is forgotten only by a load the HOST asked for (its
609
+ * EV_REFRESH, which a Save sends). Since M36 a write moves no
610
+ * version, so `written` is the draft chip of what this session
611
+ * wrote and no longer half of a version measurement: a reload of
612
+ * the reconnect, or of a write that turned out done, forgot the
613
+ * chips of writes the host had not been told about.
578
614
  ***************************************************************/
579
615
  function start_measuring(gobj)
580
616
  {
581
617
  let priv = gobj.priv;
582
618
 
583
619
  priv.baseline = {};
584
- priv.written = {};
620
+ if(!priv.load_keeps_written) {
621
+ priv.written = {};
622
+ }
585
623
  priv.host_ids = {};
586
624
 
587
625
  if(!priv.model) {
@@ -805,12 +843,19 @@ function render_toolbar(gobj)
805
843
  let state = gobj_current_state(gobj);
806
844
  let readonly = is_readonly(gobj);
807
845
  let has_model = !!priv.model;
846
+ /* The position can outlive its screen (a load that failed keeps the
847
+ * url's; ST_EMPTY has a treedb in the url and none in the model),
848
+ * and a button there sends what only a treedb screen declares. */
849
+ let on_a_treedb = state !== "ST_IDLE" && state !== "ST_EMPTY";
850
+ /* Nothing is legal while the model is in the air: every action is
851
+ * computed against it. */
852
+ let in_flight = state === "ST_SAVING" || state === "ST_LOADING";
808
853
 
809
854
  /*----------------------------------------------*
810
855
  * Left: back, then the position as a trail.
811
856
  *----------------------------------------------*/
812
857
  let $left = [];
813
- if(priv.treedb_id) {
858
+ if(priv.treedb_id && on_a_treedb) {
814
859
  $left.push(["button", {class: "SCHEMA_BACK button",
815
860
  type: "button",
816
861
  title: t("back"), "aria-label": t("back"),
@@ -847,7 +892,7 @@ function render_toolbar(gobj)
847
892
  * refused says why when it is pressed.
848
893
  *----------------------------------------------*/
849
894
  let $right = [];
850
- if(priv.treedb_id && has_model) {
895
+ if(priv.treedb_id && has_model && on_a_treedb) {
851
896
  /* The compass, like every other door to a SCHEMA: the
852
897
  * `hexagon-nodes` it wore is the graph of the DATA. */
853
898
  $right.push(toolbar_button("SCHEMA_DIAGRAM_BTN", "yi-compass-drafting",
@@ -861,7 +906,7 @@ function render_toolbar(gobj)
861
906
  "import", "EV_IMPORT", false));
862
907
  }
863
908
  }
864
- if(priv.topic_name && !readonly && !priv.diagram) {
909
+ if(priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
865
910
  /* Only while there IS somewhere to go back to: a drag is a write,
866
911
  * and this is the way back from one. */
867
912
  if(undo_order_writes(gobj, current_topic(gobj)).length > 0) {
@@ -871,7 +916,7 @@ function render_toolbar(gobj)
871
916
  $right.push(toolbar_button("SCHEMA_ADD_COL_BTN", "yi-plus",
872
917
  "new column", "EV_ADD_COLUMN", false));
873
918
  }
874
- if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram) {
919
+ if(priv.treedb_id && !priv.topic_name && !readonly && !priv.diagram && on_a_treedb) {
875
920
  $right.push(toolbar_button("SCHEMA_ADD_TOPIC_BTN", "yi-plus",
876
921
  "new topic", "EV_ADD_TOPIC", false));
877
922
  }
@@ -894,7 +939,7 @@ function render_toolbar(gobj)
894
939
  * destroyed. */
895
940
  for(let $button of $bar.querySelectorAll("button")) {
896
941
  $button.__gobj__ = gobj;
897
- if(state === "ST_SAVING") {
942
+ if(in_flight) {
898
943
  $button.disabled = true;
899
944
  }
900
945
  }
@@ -918,19 +963,25 @@ function render(gobj)
918
963
  }
919
964
  render_toolbar(gobj);
920
965
 
966
+ /* A screen with nothing to draw takes the drawing down too: left
967
+ * alive, it was only re-shown on the way back -- into a body that
968
+ * no longer held it. */
921
969
  if(state === "ST_LOADING") {
922
970
  show_notice(gobj, "loading the schemas");
971
+ destroy_diagram(gobj);
923
972
  clear($body);
924
973
  return;
925
974
  }
926
975
  if(state === "ST_IDLE") {
927
976
  show_notice(gobj, priv.load_error ? "cannot load the schemas" : "not connected",
928
977
  priv.load_error);
978
+ destroy_diagram(gobj);
929
979
  clear($body);
930
980
  return;
931
981
  }
932
982
  if(state === "ST_EMPTY") {
933
983
  show_notice(gobj, "this yuno stores no schema");
984
+ destroy_diagram(gobj);
934
985
  clear($body);
935
986
  return;
936
987
  }
@@ -1471,12 +1522,21 @@ function render_diagram(gobj)
1471
1522
  let $body = $of(gobj, ".SCHEMA_BODY");
1472
1523
  let treedb = current_treedb(gobj);
1473
1524
 
1474
- if(!$body || !treedb) {
1525
+ if(!$body) {
1526
+ return;
1527
+ }
1528
+ /* Said on an EMPTY body, as the topics and the columns say it:
1529
+ * render() clears the body for every screen but this one, and the
1530
+ * screen before stayed up under the notice, clickable. */
1531
+ if(!treedb) {
1532
+ destroy_diagram(gobj);
1533
+ clear($body);
1475
1534
  show_notice(gobj, "that treedb is not here any more", priv.treedb_id);
1476
1535
  return;
1477
1536
  }
1478
1537
  if(!gclass_find_by_name("C_YUI_TREEDB_SCHEMA")) {
1479
1538
  log_error(`${gobj_short_name(gobj)}: C_YUI_TREEDB_SCHEMA not registered by the app`);
1539
+ clear($body);
1480
1540
  show_notice(gobj, "the schema drawing is not available");
1481
1541
  return;
1482
1542
  }
@@ -2501,29 +2561,15 @@ function end_writes(gobj, error)
2501
2561
  {
2502
2562
  let priv = gobj.priv;
2503
2563
 
2564
+ let seg = priv.pending_seg;
2565
+
2504
2566
  priv.save_queue = [];
2567
+ priv.pending_seg = null;
2505
2568
  gobj_change_state(gobj, priv.save_return || "ST_TREEDBS");
2506
2569
  priv.save_return = "";
2507
2570
  show_notice(gobj, "");
2508
2571
  set_busy(gobj, false);
2509
2572
 
2510
- /* A position that arrived while the write was in flight: applied
2511
- * now, so a Back pressed mid-save is not simply lost. */
2512
- if(priv.pending_seg !== null) {
2513
- let seg = priv.pending_seg;
2514
- priv.pending_seg = null;
2515
- if(!error) {
2516
- apply_seg(gobj, seg);
2517
- if(priv.save_wrote) {
2518
- gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2519
- treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2520
- });
2521
- }
2522
- priv.save_wrote = false;
2523
- return reload_if_owed(gobj);
2524
- }
2525
- }
2526
-
2527
2573
  if(error) {
2528
2574
  yui_shell_show_error(yui_shell_of(gobj), error, {t: t});
2529
2575
  }
@@ -2533,8 +2579,18 @@ function end_writes(gobj, error)
2533
2579
  });
2534
2580
  }
2535
2581
  priv.save_wrote = false;
2536
- render(gobj);
2537
- publish_check(gobj);
2582
+
2583
+ /* A position that arrived while the write was in flight: applied
2584
+ * now, so a Back pressed mid-save is not simply lost -- with an
2585
+ * error too. The host's url already says it, and a load cut by a
2586
+ * drop applies it (end_load()): the write kept the old screen, and
2587
+ * the url and the view disagreed. */
2588
+ if(seg !== null) {
2589
+ apply_seg(gobj, seg);
2590
+ } else {
2591
+ render(gobj);
2592
+ publish_check(gobj);
2593
+ }
2538
2594
  reload_if_owed(gobj);
2539
2595
  return error ? -1 : 0;
2540
2596
  }
@@ -2556,7 +2612,7 @@ function reload_if_owed(gobj)
2556
2612
  priv.reload_on_open = true;
2557
2613
  return 0;
2558
2614
  }
2559
- return request_model(gobj);
2615
+ return request_model(gobj, true);
2560
2616
  }
2561
2617
 
2562
2618
  /***************************************************************
@@ -2660,6 +2716,9 @@ function go(gobj, treedb_id, topic_name, diagram, mirror)
2660
2716
  {
2661
2717
  let priv = gobj.priv;
2662
2718
 
2719
+ /* Moving IS the answer to a position that waited: one left behind
2720
+ * sent a later load back to it. */
2721
+ priv.pending_seg = null;
2663
2722
  priv.treedb_id = treedb_id || "";
2664
2723
  priv.topic_name = topic_name || "";
2665
2724
  priv.diagram = !!diagram;
@@ -2857,7 +2916,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
2857
2916
  `for a write that is over (result ${result}): ignored`);
2858
2917
  return 0;
2859
2918
  }
2860
- return late_write_done(gobj, command, topic_name);
2919
+ return late_write_done(gobj, command, topic_name, data);
2861
2920
  }
2862
2921
  if(result < 0) {
2863
2922
  return end_writes(gobj, comment || t("the treedb refused the write"));
@@ -2874,8 +2933,17 @@ function ac_mt_command_answer(gobj, event, kw, src)
2874
2933
  /* The treedb wrote it and did not describe it back: ask
2875
2934
  * again rather than draw a model that has drifted. */
2876
2935
  log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record`);
2936
+ if(topic_name === T_TOPICS || topic_name === T_COLS) {
2937
+ let write = priv.save_queue[0];
2938
+ mark_written(gobj, topic_name, write ? write.record : null);
2939
+ }
2877
2940
  priv.save_queue = [];
2878
- request_model(gobj);
2941
+ priv.save_return = "";
2942
+ priv.save_wrote = false;
2943
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2944
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2945
+ });
2946
+ request_model(gobj, true);
2879
2947
  return 0;
2880
2948
  }
2881
2949
  }
@@ -2907,14 +2975,24 @@ function ac_mt_command_answer(gobj, event, kw, src)
2907
2975
  * the view was answered once, as failed -- and echoes the node
2908
2976
  * event instead, which this view does not hear: there, Refresh is
2909
2977
  * what reads the store.
2978
+ *
2979
+ * It is a WRITE like any other for the host (EV_RECORD_WRITTEN):
2980
+ * the host was told nothing when it was given up, and it owns the
2981
+ * Save that publishes it. The reload keeps what this session wrote
2982
+ * (request_model()), and the record it answered marks its topic.
2910
2983
  ***************************************************************/
2911
- function late_write_done(gobj, command, topic_name)
2984
+ function late_write_done(gobj, command, topic_name, data)
2912
2985
  {
2913
2986
  let priv = gobj.priv;
2914
2987
  let state = gobj_current_state(gobj);
2915
2988
 
2916
2989
  log_warning(`${gobj_short_name(gobj)}: '${command}' of '${topic_name}' was given up, ` +
2917
2990
  `and it was DONE: the model is read again`);
2991
+ mark_written_record(gobj, topic_name, is_object(data) ? data
2992
+ : (Array.isArray(data) && is_object(data[0]) ? data[0] : null));
2993
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2994
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name")
2995
+ });
2918
2996
  if(state === "ST_LOADING") {
2919
2997
  return 0;
2920
2998
  }
@@ -2926,7 +3004,37 @@ function late_write_done(gobj, command, topic_name)
2926
3004
  priv.reload_on_open = true;
2927
3005
  return 0;
2928
3006
  }
2929
- return request_model(gobj);
3007
+ return request_model(gobj, true);
3008
+ }
3009
+
3010
+ /***************************************************************
3011
+ * The topic a record written belongs to, read off the RECORD and
3012
+ * not off the screen: a write answered late may be about a topic
3013
+ * the operator has left. A column names its topic in its fkey
3014
+ * (`topics^<topic id>^cols`); a topic is its own id.
3015
+ ***************************************************************/
3016
+ function mark_written_record(gobj, topic_name, record)
3017
+ {
3018
+ let priv = gobj.priv;
3019
+
3020
+ if(!record) {
3021
+ return;
3022
+ }
3023
+ if(topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
3024
+ priv.written[record.id] = true;
3025
+ return;
3026
+ }
3027
+ if(topic_name !== T_COLS) {
3028
+ return;
3029
+ }
3030
+ let refs = Array.isArray(record.topics) ? record.topics
3031
+ : (typeof record.topics === "string" ? [record.topics] : []);
3032
+ for(let ref of refs) {
3033
+ let parts = String(ref).split("^");
3034
+ if(parts.length >= 2 && parts[1]) {
3035
+ priv.written[parts[1]] = true;
3036
+ }
3037
+ }
2930
3038
  }
2931
3039
 
2932
3040
  /***************************************************************
@@ -3004,7 +3112,7 @@ function ac_transport_state(gobj, event, kw, src)
3004
3112
  if((!priv.model || priv.reload_on_open) &&
3005
3113
  state !== "ST_LOADING" && state !== "ST_SAVING") {
3006
3114
  priv.reload_on_open = false;
3007
- return request_model(gobj);
3115
+ return request_model(gobj, true);
3008
3116
  }
3009
3117
  render_toolbar(gobj);
3010
3118
  return 0;
@@ -3023,7 +3131,28 @@ function ac_language_changed(gobj, event, kw, src)
3023
3131
 
3024
3132
  function ac_refresh(gobj, event, kw, src)
3025
3133
  {
3026
- return request_model(gobj);
3134
+ return request_model(gobj, false);
3135
+ }
3136
+
3137
+ /***************************************************************
3138
+ * What a DIALOG sends while the model is being read again: the
3139
+ * Save of a form left open when the reload started, a
3140
+ * confirmation, an import. The loading screen took the rest of
3141
+ * the view away (request_model()); a dialog is the one thing the
3142
+ * operator can still reach, and what it asks is computed against
3143
+ * a model the answers in the air are about to replace.
3144
+ *
3145
+ * Refused, and SAID -- and the dialog stays open with what was
3146
+ * typed, so the same Save works once the load is in. Undeclared,
3147
+ * it answered "Event NOT DEFINED in state ST_LOADING" and the
3148
+ * edit was gone with no word (third independent review).
3149
+ ***************************************************************/
3150
+ function ac_wait_for_the_load(gobj, event, kw, src)
3151
+ {
3152
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas are loading`);
3153
+ yui_shell_show_error(yui_shell_of(gobj),
3154
+ "the schemas are loading: try again when they are in", {t: t});
3155
+ return -1;
3027
3156
  }
3028
3157
 
3029
3158
  /***************************************************************
@@ -3655,7 +3784,14 @@ function create_gclass(gclass_name)
3655
3784
  const states = [
3656
3785
  ["ST_IDLE", COMMON.slice()],
3657
3786
 
3658
- ["ST_LOADING", COMMON.slice()],
3787
+ ["ST_LOADING", COMMON.concat([
3788
+ ["EV_SAVE_COLUMN", ac_wait_for_the_load, null],
3789
+ ["EV_SAVE_TOPIC", ac_wait_for_the_load, null],
3790
+ ["EV_DELETE_ORPHAN", ac_wait_for_the_load, null],
3791
+ ["EV_PREVIEW_IMPORT", ac_wait_for_the_load, null],
3792
+ ["EV_APPLY_IMPORT", ac_wait_for_the_load, null],
3793
+ ["EV_CONFIRMED", ac_wait_for_the_load, null]
3794
+ ])],
3659
3795
 
3660
3796
  ["ST_EMPTY", COMMON.slice()],
3661
3797