@yuneta/gobj-ui 7.25.15 → 7.25.17

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.
@@ -493,6 +493,7 @@ let PRIVATE_DATA = {
493
493
  __graphs__: [], // Rows of __graphs__
494
494
  _graph_properties: {}, // topic_name → {nodes: {node_id: {x,y,size,...}}}
495
495
  _saved_graph_properties: {}, // the same, as the BACKEND has it: what a Save compares against
496
+ _graphs_writes_owed: {}, // topic_name → true: a __graphs__ write the host said did not land
496
497
  yet_showed: false,
497
498
  edit_mode: false,
498
499
  operation_mode: null,
@@ -3283,6 +3284,9 @@ function save_topic_graph_properties(gobj, topic_name)
3283
3284
  * The host says when one does NOT land (EV_GRAPHS_WRITE_REFUSED),
3284
3285
  * and the next Save writes the topic again. */
3285
3286
  priv._saved_graph_properties[topic_name] = json_deep_copy(properties);
3287
+ /* Paid: this is the write that was owed, if one was. Refused
3288
+ * again, EV_GRAPHS_WRITE_REFUSED owes it again. */
3289
+ delete priv._graphs_writes_owed[topic_name];
3286
3290
 
3287
3291
  let kw_update = {
3288
3292
  treedb_name: priv.treedb_name,
@@ -3388,9 +3392,16 @@ function update_history_buttons(gobj)
3388
3392
  disableElements($container, ".EV_HISTORY_UNDO");
3389
3393
  set_active_state($container, ".EV_HISTORY_UNDO", false);
3390
3394
 
3391
- // No more undos: graph is back to original state, disable save
3392
- disableElements($container, ".EV_SAVE_GRAPH");
3393
- set_submit_state($container, ".EV_SAVE_GRAPH", false);
3395
+ /* No more undos: the graph is back to what was loaded
3396
+ * or saved -- unless a write of it did not land. That
3397
+ * one is still owed, and only a Save pays it. */
3398
+ if(graphs_write_is_owed(gobj)) {
3399
+ enableElements($container, ".EV_SAVE_GRAPH");
3400
+ set_submit_state($container, ".EV_SAVE_GRAPH", true);
3401
+ } else {
3402
+ disableElements($container, ".EV_SAVE_GRAPH");
3403
+ set_submit_state($container, ".EV_SAVE_GRAPH", false);
3404
+ }
3394
3405
  }
3395
3406
  } else {
3396
3407
  disableElements($container, ".EV_HISTORY_REDO");
@@ -4763,6 +4774,17 @@ function run_preview_undo(gobj, key)
4763
4774
  }
4764
4775
  }
4765
4776
 
4777
+ /************************************************************
4778
+ * Is a `__graphs__` write owed? The host said one did not land
4779
+ * (EV_GRAPHS_WRITE_REFUSED), and no Save has written that topic
4780
+ * since. The Save button stays lit while it is: the history
4781
+ * knows nothing of it, so `canUndo()` alone would put it out.
4782
+ ************************************************************/
4783
+ function graphs_write_is_owed(gobj)
4784
+ {
4785
+ return Object.keys(gobj.priv._graphs_writes_owed).length > 0;
4786
+ }
4787
+
4766
4788
  function mark_graph_dirty(gobj)
4767
4789
  {
4768
4790
  let $container = gobj_read_attr(gobj, "$container");
@@ -10346,6 +10368,11 @@ function ac_clear_data(gobj, event, kw, src)
10346
10368
 
10347
10369
  gobj_write_attr(gobj, "records", {});
10348
10370
 
10371
+ /* A write owed is owed on the arrangement being thrown away: the
10372
+ * reload brings what the backend holds, and that is what the view
10373
+ * will hold. */
10374
+ priv._graphs_writes_owed = {};
10375
+
10349
10376
  /* The node counters belong to the data that is going away: a refresh
10350
10377
  * that kept them would decide the layout on the previous treedb. */
10351
10378
  priv._nodes_total = 0;
@@ -10495,10 +10522,13 @@ function ac_save_graph(gobj, event, kw, src)
10495
10522
  let history = graph_get_plugin(gobj, "history");
10496
10523
  if(history) {
10497
10524
  history.clear();
10498
- update_history_buttons(gobj);
10499
10525
  }
10500
10526
 
10501
10527
  save_geometry(gobj); // Publish an EV_UPDATE_NODE of each node
10528
+
10529
+ /* After the writes: they pay a write owed, and the buttons
10530
+ * are repainted from what is still owed. */
10531
+ update_history_buttons(gobj);
10502
10532
  }
10503
10533
 
10504
10534
  return 0;
@@ -10526,6 +10556,9 @@ function ac_graphs_write_refused(gobj, event, kw, src)
10526
10556
  log_warning(`${gobj_short_name(gobj)}: the arrangement of '${topic_name}' ` +
10527
10557
  `was not saved: the next Save writes it again`);
10528
10558
  forget_refused_graphs_write(priv._saved_graph_properties, topic_name);
10559
+ /* Owed until a Save writes it: update_history_buttons() keeps
10560
+ * the Save lit while it is, in edition and on entering it. */
10561
+ priv._graphs_writes_owed[topic_name] = true;
10529
10562
  if(priv.edit_mode) {
10530
10563
  mark_graph_dirty(gobj);
10531
10564
  }
@@ -117,6 +117,7 @@ import {
117
117
  kw_get_int,
118
118
  kw_get_dict,
119
119
  is_object,
120
+ json_deep_copy,
120
121
  is_array,
121
122
  empty_string,
122
123
  } from "@yuneta/gobj-js";
@@ -246,6 +247,8 @@ let PRIVATE_DATA = {
246
247
  dropped_round: 0, /* the load a drop ended: its failures are the drop, said once */
247
248
  dropped_write: 0, /* the same for the write a drop ended */
248
249
  reload_after_write: false, /* a write given up turned out DONE while another was in flight */
250
+ refresh_after_write: false, /* EV_REFRESH heard while writes were in flight: run at their end */
251
+ reopen_form: null, /* the form whose Save ran the owed reload: opened again once it lands */
249
252
 
250
253
  /* A position that arrived while a load or a write was in flight */
251
254
  pending_seg: null,
@@ -400,7 +403,11 @@ function el_attrs(attrs)
400
403
  }
401
404
 
402
405
  /***************************************************************
403
- * The body stops answering while a write is in flight.
406
+ * The body stops answering while a write is in flight. For the
407
+ * keyboard too: `pointer-events` stops the mouse alone, and the
408
+ * shell's focus trap puts the focus back on the row control that
409
+ * had it, where Enter or Delete sent an action ST_SAVING does
410
+ * not declare. `inert` takes the whole body out of reach.
404
411
  ***************************************************************/
405
412
  function set_busy(gobj, busy)
406
413
  {
@@ -408,6 +415,7 @@ function set_busy(gobj, busy)
408
415
 
409
416
  if($body) {
410
417
  $body.classList.toggle("SCHEMA_BUSY", !!busy);
418
+ $body.toggleAttribute("inert", !!busy);
411
419
  }
412
420
  }
413
421
 
@@ -522,12 +530,37 @@ function owes_reload(gobj)
522
530
  state !== "ST_LOADING" && state !== "ST_SAVING";
523
531
  }
524
532
 
525
- function reload_first(gobj, event)
533
+ function reload_first(gobj, event, reopen)
526
534
  {
535
+ let priv = gobj.priv;
536
+
527
537
  if(!owes_reload(gobj)) {
528
538
  return false;
529
539
  }
530
- gobj.priv.reload_on_open = false;
540
+ priv.reload_on_open = false;
541
+
542
+ /* The Save of a FORM: what the operator typed is not thrown away
543
+ * with the model it was typed on. The load that lands closes the
544
+ * form (end_load()) and opens it again on the schemas it read,
545
+ * with the fields the operator CHANGED put back on top -- only
546
+ * those: a field left as it was shows what the store holds now,
547
+ * so the next Save does not write the old value over a newer
548
+ * one. Closed alone, the form took what was typed with it. */
549
+ if(reopen && priv.dialog) {
550
+ priv.reopen_form = Object.assign(json_deep_copy(reopen), {
551
+ dialog: priv.dialog,
552
+ treedb_id: priv.treedb_id
553
+ });
554
+ log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
555
+ `may be out of date: they are read again first, and the form opens again ` +
556
+ `on them with its changes`);
557
+ yui_shell_show_error(yui_shell_of(gobj),
558
+ "the schemas shown may be out of date: they are read again, and the form opens again on them with your changes",
559
+ {t: t});
560
+ request_model(gobj, true);
561
+ return true;
562
+ }
563
+
531
564
  log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
532
565
  `may be out of date: they are read again first`);
533
566
  yui_shell_show_error(yui_shell_of(gobj),
@@ -537,6 +570,53 @@ function reload_first(gobj, event)
537
570
  return true;
538
571
  }
539
572
 
573
+ /***************************************************************
574
+ * The form reload_first() took down, opened again on the model
575
+ * the load just made, with the operator's changes on top.
576
+ *
577
+ * Only where it can mean the same thing: on the treedb it was
578
+ * opened on, on the screen whose Save it has, and on a record
579
+ * that is still there. Otherwise it is not opened, and that is
580
+ * said -- a form opened on another treedb, or on nothing, would
581
+ * save somewhere the operator never meant.
582
+ ***************************************************************/
583
+ function reopen_form(gobj, r)
584
+ {
585
+ let priv = gobj.priv;
586
+ let state = gobj_current_state(gobj);
587
+ let treedb = current_treedb(gobj);
588
+ let topic = null;
589
+ let col = null;
590
+ let fits = false;
591
+
592
+ if(treedb && r.treedb_id === priv.treedb_id) {
593
+ if(r.form === "column" && state === "ST_COLUMNS") {
594
+ topic = find_topic(priv.model, priv.treedb_id, r.topic);
595
+ col = (topic && !r.creating) ?
596
+ find_col(priv.model, priv.treedb_id, r.topic, r.col) : null;
597
+ fits = !!topic && (r.creating || !!col);
598
+ } else if(r.form === "topic" && state === "ST_TOPICS") {
599
+ topic = r.creating ? null : find_topic(priv.model, priv.treedb_id, r.topic);
600
+ fits = r.creating || !!topic;
601
+ }
602
+ }
603
+ if(!fits) {
604
+ log_warning(`${gobj_short_name(gobj)}: the ${r.form} form was not opened again: ` +
605
+ `what it edited is not on the schemas the reload read ` +
606
+ `(${r.treedb_id} ${r.topic}${r.col ? "." + r.col : ""}, now ${state})`);
607
+ yui_shell_show_error(yui_shell_of(gobj),
608
+ "the schemas were read again and what the form was editing is not there any more",
609
+ {t: t});
610
+ return -1;
611
+ }
612
+ if(r.form === "column") {
613
+ open_column_form(gobj, topic, col, null, r.changed);
614
+ } else {
615
+ open_topic_form(gobj, treedb, topic, r.changed);
616
+ }
617
+ return 0;
618
+ }
619
+
540
620
  function reload_after_move(gobj)
541
621
  {
542
622
  if(!owes_reload(gobj)) {
@@ -632,7 +712,7 @@ function request_model(gobj, keep_written)
632
712
  priv.records_before = null;
633
713
  priv.load_round++;
634
714
  priv.reload_on_open = true;
635
- priv.load_error = t("cannot reach the treedb");
715
+ priv.load_error = "cannot reach the treedb"; /* a key: the notice translates it */
636
716
  if(priv.model) {
637
717
  yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", {t: t});
638
718
  }
@@ -725,7 +805,7 @@ function build_model(gobj)
725
805
  * measurement of "changed since" can be measured against.
726
806
  *
727
807
  * `written` is forgotten only by an EV_REFRESH (the view's own
728
- * Refresh button, or a host that sends one). Since M36 a write moves no
808
+ * Refresh button, or a host that sends one). Since 7.23.196 a write moves no
729
809
  * version, so `written` is the draft chip of what this session
730
810
  * wrote and no longer half of a version measurement: a reload of
731
811
  * the reconnect, or of a write that turned out done, forgot the
@@ -918,9 +998,14 @@ function show_notice(gobj, key, detail)
918
998
  $notice.appendChild(createElement2(
919
999
  ["span", {class: "SCHEMA_NOTICE_TEXT", i18n: key}, t(key)]
920
1000
  ));
921
- if(detail) {
1001
+ /* The detail is a KEY too (ours, or the backend's words, which
1002
+ * translate to themselves when no locale has them), so a change
1003
+ * of language changes it with the rest. One that repeats the
1004
+ * title says nothing. */
1005
+ if(detail && detail !== key) {
922
1006
  $notice.appendChild(createElement2(
923
- ["span", {class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2"}, `${detail}`]
1007
+ ["span", {class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2", i18n: `${detail}`},
1008
+ t(`${detail}`)]
924
1009
  ));
925
1010
  }
926
1011
  }
@@ -1915,6 +2000,75 @@ function read_form($root)
1915
2000
  return out;
1916
2001
  }
1917
2002
 
2003
+ /***************************************************************
2004
+ * A column form as its Save sends it: the fields and the flags.
2005
+ ***************************************************************/
2006
+ function column_form_state($form)
2007
+ {
2008
+ let values = read_form($form);
2009
+ values.flag = read_flags($form);
2010
+ return values;
2011
+ }
2012
+
2013
+ /***************************************************************
2014
+ * The fields the operator CHANGED: those whose value is not the
2015
+ * one the form was opened with.
2016
+ ***************************************************************/
2017
+ function form_changes(initial, values)
2018
+ {
2019
+ let changed = {};
2020
+
2021
+ for(let name of Object.keys(values)) {
2022
+ if(JSON.stringify(values[name]) !== JSON.stringify(initial[name])) {
2023
+ changed[name] = values[name];
2024
+ }
2025
+ }
2026
+ return changed;
2027
+ }
2028
+
2029
+ /***************************************************************
2030
+ * Put changes back into a form just built (reopen_form()). The
2031
+ * flags are drawn again for the type the changes leave, holding
2032
+ * the flags they name, so none of them is hidden by the type.
2033
+ ***************************************************************/
2034
+ function apply_form_changes(gobj, $form, changes)
2035
+ {
2036
+ for(let name of Object.keys(changes)) {
2037
+ if(name === "flag") {
2038
+ continue;
2039
+ }
2040
+ let $control = $form.querySelector(`[data-name="${name}"]`);
2041
+ if(!$control) {
2042
+ log_warning(`${gobj_short_name(gobj)}: a change of '${name}' has no field ` +
2043
+ `in the form opened again: not put back`);
2044
+ continue;
2045
+ }
2046
+ if($control.type === "checkbox") {
2047
+ $control.checked = !!changes[name];
2048
+ } else {
2049
+ $control.value = changes[name];
2050
+ }
2051
+ }
2052
+
2053
+ let $type = $form.querySelector('[data-name="type"]');
2054
+ if($type && $form.querySelector(".SCHEMA_FLAGS")) {
2055
+ let flags = Array.isArray(changes.flag) ? changes.flag : read_flags($form);
2056
+ redraw_flags(gobj, $form, $type.value, flags);
2057
+ }
2058
+ }
2059
+
2060
+ /***************************************************************
2061
+ * Draw the flags of a column form again, for `type`, with
2062
+ * `current` checked.
2063
+ ***************************************************************/
2064
+ function redraw_flags(gobj, $form, type, current)
2065
+ {
2066
+ let $holder = $form.querySelector(".SCHEMA_FLAGS");
2067
+ let $new = createElement2(flags_field(gobj, type, current));
2068
+ $holder.parentNode.replaceChild($new, $holder);
2069
+ refresh_language($new, t);
2070
+ }
2071
+
1918
2072
  /***************************************************************
1919
2073
  * The flags of a column, as checkboxes that say what they do.
1920
2074
  *
@@ -1977,7 +2131,7 @@ function read_flags($root)
1977
2131
  * Offering a field that silently does something else is worse
1978
2132
  * than not offering it.
1979
2133
  ***************************************************************/
1980
- function open_column_form(gobj, topic, col, prefill)
2134
+ function open_column_form(gobj, topic, col, prefill, changes)
1981
2135
  {
1982
2136
  let priv = gobj.priv;
1983
2137
  /* A DUPLICATE is a creation whose fields start filled: the name must
@@ -2051,11 +2205,7 @@ function open_column_form(gobj, topic, col, prefill)
2051
2205
  * new type does not use. */
2052
2206
  let $type = $form.querySelector('[data-name="type"]');
2053
2207
  $type.addEventListener("change", () => {
2054
- let $holder = $form.querySelector(".SCHEMA_FLAGS");
2055
- let current = read_flags($form);
2056
- let $new = createElement2(flags_field(gobj, $type.value, current));
2057
- $holder.parentNode.replaceChild($new, $holder);
2058
- refresh_language($new, t);
2208
+ redraw_flags(gobj, $form, $type.value, read_flags($form));
2059
2209
  });
2060
2210
 
2061
2211
  /* A flag that excludes another is answered here and not on save,
@@ -2075,15 +2225,22 @@ function open_column_form(gobj, topic, col, prefill)
2075
2225
  evt.stopPropagation();
2076
2226
  close_dialog(gobj);
2077
2227
  });
2228
+ /* What the form showed before the operator touched it: what
2229
+ * was CHANGED is measured against it (form_changes()). */
2230
+ let initial = column_form_state($form);
2231
+ if(changes) {
2232
+ apply_form_changes(gobj, $form, changes);
2233
+ }
2234
+
2078
2235
  $form.querySelector(".SCHEMA_COL_FORM_SAVE").addEventListener("click", (evt) => {
2079
2236
  evt.stopPropagation();
2080
- let values = read_form($form);
2081
- values.flag = read_flags($form);
2237
+ let values = column_form_state($form);
2082
2238
  gobj_send_event(gobj, "EV_SAVE_COLUMN", {
2083
2239
  topic: topic.name,
2084
2240
  col: creating ? "" : col.name,
2085
2241
  creating: creating,
2086
2242
  values: values,
2243
+ changed: creating ? values : form_changes(initial, values),
2087
2244
  model_gen: model_gen
2088
2245
  }, gobj);
2089
2246
  });
@@ -2112,7 +2269,7 @@ function stringify_field(value)
2112
2269
  * actually has: a pkey naming no column is a topic that does
2113
2270
  * not open, and it is the first thing the validator looks for.
2114
2271
  ***************************************************************/
2115
- function open_topic_form(gobj, treedb, topic)
2272
+ function open_topic_form(gobj, treedb, topic, changes)
2116
2273
  {
2117
2274
  let record = (topic && topic.record) || {};
2118
2275
  let creating = !topic;
@@ -2171,12 +2328,19 @@ function open_topic_form(gobj, treedb, topic)
2171
2328
  evt.stopPropagation();
2172
2329
  close_dialog(gobj);
2173
2330
  });
2331
+ let initial = read_form($form);
2332
+ if(changes) {
2333
+ apply_form_changes(gobj, $form, changes);
2334
+ }
2335
+
2174
2336
  $form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
2175
2337
  evt.stopPropagation();
2338
+ let values = read_form($form);
2176
2339
  gobj_send_event(gobj, "EV_SAVE_TOPIC", {
2177
2340
  topic: creating ? "" : topic.name,
2178
2341
  creating: creating,
2179
- values: read_form($form),
2342
+ values: values,
2343
+ changed: creating ? values : form_changes(initial, values),
2180
2344
  model_gen: model_gen
2181
2345
  }, gobj);
2182
2346
  });
@@ -2756,27 +2920,46 @@ function end_load(gobj)
2756
2920
  {
2757
2921
  let priv = gobj.priv;
2758
2922
  let seg = priv.pending_seg;
2923
+ let reopen = priv.reopen_form;
2759
2924
 
2760
2925
  priv.pending_seg = null;
2926
+ priv.reopen_form = null;
2761
2927
  if(priv.dialog && priv.dialog_gen !== priv.model_gen) {
2762
- log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
2763
- `the load replaced: closed`);
2928
+ /* The form whose Save asked for this load is opened again,
2929
+ * below, once the screen is there (reload_first()). Any other
2930
+ * dialog is closed, and that is said. */
2931
+ if(!reopen || reopen.dialog !== priv.dialog) {
2932
+ reopen = null;
2933
+ log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
2934
+ `the load replaced: closed`);
2935
+ yui_shell_show_error(yui_shell_of(gobj),
2936
+ "the schemas were read again: open the dialog again", {t: t});
2937
+ }
2764
2938
  close_dialog(gobj);
2765
- yui_shell_show_error(yui_shell_of(gobj),
2766
- "the schemas were read again: open the dialog again", {t: t});
2939
+ } else {
2940
+ /* Closed meanwhile by the operator, or a load that replaced
2941
+ * nothing: the form is where it was, or where it was left. */
2942
+ reopen = null;
2767
2943
  }
2944
+
2945
+ let ret = 0;
2768
2946
  if(priv.model) {
2769
2947
  if(seg !== null) {
2770
- return apply_seg(gobj, seg);
2948
+ ret = apply_seg(gobj, seg);
2949
+ } else {
2950
+ ret = go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2771
2951
  }
2772
- return go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
2952
+ } else {
2953
+ if(seg !== null) {
2954
+ adopt_seg(gobj, seg);
2955
+ }
2956
+ gobj_change_state(gobj, "ST_IDLE");
2957
+ render(gobj);
2773
2958
  }
2774
- if(seg !== null) {
2775
- adopt_seg(gobj, seg);
2959
+ if(reopen) {
2960
+ reopen_form(gobj, reopen);
2776
2961
  }
2777
- gobj_change_state(gobj, "ST_IDLE");
2778
- render(gobj);
2779
- return 0;
2962
+ return ret;
2780
2963
  }
2781
2964
 
2782
2965
  /***************************************************************
@@ -2819,7 +3002,17 @@ function end_writes(gobj, error)
2819
3002
  render(gobj);
2820
3003
  publish_check(gobj);
2821
3004
  }
2822
- reload_if_owed(gobj);
3005
+ if(priv.refresh_after_write) {
3006
+ /* The EV_REFRESH heard while the writes were in flight
3007
+ * (ac_refresh_after_the_writes()), run now that none is. It
3008
+ * reads the store whole, so it also pays a reload a late
3009
+ * write owed. */
3010
+ priv.refresh_after_write = false;
3011
+ priv.reload_after_write = false;
3012
+ request_model(gobj, false);
3013
+ } else {
3014
+ reload_if_owed(gobj);
3015
+ }
2823
3016
  return error ? -1 : 0;
2824
3017
  }
2825
3018
 
@@ -3167,7 +3360,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3167
3360
  }
3168
3361
  priv.pending--;
3169
3362
  if(result < 0) {
3170
- priv.load_error = comment || t("cannot load the schemas");
3363
+ priv.load_error = comment || "cannot load the schemas"; /* a key */
3171
3364
  } else if(priv.records && Array.isArray(priv.records[topic_name])) {
3172
3365
  priv.records[topic_name] = Array.isArray(data) ? data : [];
3173
3366
  }
@@ -3244,10 +3437,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
3244
3437
  return end_writes(gobj, comment || t("the treedb refused the write"));
3245
3438
  }
3246
3439
  priv.save_wrote = true;
3440
+ let record = null;
3247
3441
  if(command === "delete-node") {
3248
3442
  forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
3249
3443
  } else {
3250
- let record = is_object(data) ? data
3444
+ record = is_object(data) ? data
3251
3445
  : (Array.isArray(data) && is_object(data[0]) ? data[0] : null);
3252
3446
  if(record) {
3253
3447
  patch_record(gobj, topic_name, record);
@@ -3270,7 +3464,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3270
3464
  }
3271
3465
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
3272
3466
  let write = priv.save_queue[0];
3273
- mark_written(gobj, topic_name, write ? write.record : null);
3467
+ mark_written(gobj, topic_name, null, write ? write.record : null);
3274
3468
  }
3275
3469
  priv.reload_after_write = false;
3276
3470
  priv.save_queue = [];
@@ -3279,16 +3473,21 @@ function ac_mt_command_answer(gobj, event, kw, src)
3279
3473
  gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
3280
3474
  treedb_name: gobj_read_str_attr(gobj, "treedb_name")
3281
3475
  });
3282
- request_model(gobj, true);
3476
+ /* An EV_REFRESH heard during the writes is paid by this
3477
+ * load: it forgets the drafts, as the refresh would. */
3478
+ let refresh = priv.refresh_after_write;
3479
+ priv.refresh_after_write = false;
3480
+ request_model(gobj, !refresh);
3283
3481
  return 0;
3284
3482
  }
3285
3483
  }
3286
3484
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
3287
- /* Which record it was is in the QUEUE, not in the answer: an
3288
- * adapter echoes only what it was asked to echo, and the
3289
- * record is not small enough to be worth echoing. */
3485
+ /* Which record it was: the one the store answered, else the
3486
+ * one in the QUEUE -- an adapter echoes only what it was
3487
+ * asked to echo, and the record is not small enough to be
3488
+ * worth echoing. */
3290
3489
  let write = priv.save_queue[0];
3291
- mark_written(gobj, topic_name, write ? write.record : null);
3490
+ mark_written(gobj, topic_name, record, write ? write.record : null);
3292
3491
  }
3293
3492
  priv.save_queue.shift();
3294
3493
  return run_next_write(gobj);
@@ -3357,35 +3556,46 @@ function late_write_done(gobj, command, topic_name, data)
3357
3556
  function mark_written_record(gobj, topic_name, record)
3358
3557
  {
3359
3558
  let priv = gobj.priv;
3559
+ let marked = 0;
3360
3560
 
3361
3561
  if(!record) {
3362
- return;
3562
+ return 0;
3363
3563
  }
3364
3564
  if(topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
3365
3565
  priv.written[record.id] = true;
3366
- return;
3566
+ return 1;
3367
3567
  }
3368
3568
  if(topic_name !== T_COLS) {
3369
- return;
3569
+ return 0;
3370
3570
  }
3371
3571
  for(let ref of parse_fkey_ref(record.topics)) {
3372
3572
  priv.written[ref.id] = true;
3573
+ marked++;
3373
3574
  }
3575
+ return marked;
3374
3576
  }
3375
3577
 
3376
3578
  /***************************************************************
3377
3579
  * Remember which topic this write belongs to, which is what
3378
3580
  * "its version still has to move" is asked about.
3581
+ *
3582
+ * Asked of the RECORD first: the one the store answered (a new
3583
+ * topic has its id only there), then the one queued (a column
3584
+ * names its topic in its fkey). The screen is the last resort,
3585
+ * and a poor one: an import writes topics the operator is not
3586
+ * on, and from the topics screen there is no topic on it at all.
3379
3587
  ***************************************************************/
3380
- function mark_written(gobj, topic_name, record)
3588
+ function mark_written(gobj, topic_name, answered, queued)
3381
3589
  {
3382
3590
  let priv = gobj.priv;
3383
- let topic = current_topic(gobj);
3384
3591
 
3385
- if(topic_name === T_TOPICS && record && record.id) {
3386
- priv.written[record.id] = true;
3592
+ if(mark_written_record(gobj, topic_name, answered) > 0) {
3593
+ return;
3594
+ }
3595
+ if(mark_written_record(gobj, topic_name, queued) > 0) {
3387
3596
  return;
3388
3597
  }
3598
+ let topic = current_topic(gobj);
3389
3599
  if(topic) {
3390
3600
  priv.written[topic.id] = true;
3391
3601
  }
@@ -3475,6 +3685,25 @@ function ac_refresh(gobj, event, kw, src)
3475
3685
  return request_model(gobj, false);
3476
3686
  }
3477
3687
 
3688
+ /***************************************************************
3689
+ * EV_REFRESH while writes are in flight: the view's own Refresh
3690
+ * is busy then, so it is a host's (after a Save, say).
3691
+ *
3692
+ * Run when the writes END (end_writes()), not now. Run now, the
3693
+ * load replaced ST_SAVING and what was still queued was never
3694
+ * sent, with no word: nothing but the end of what is in flight
3695
+ * moves this view out of ST_LOADING or ST_SAVING.
3696
+ ***************************************************************/
3697
+ function ac_refresh_after_the_writes(gobj, event, kw, src)
3698
+ {
3699
+ let priv = gobj.priv;
3700
+
3701
+ log_warning(`${gobj_short_name(gobj)}: ${event} while writes are in flight: ` +
3702
+ `the schemas are read again when the writes end`);
3703
+ priv.refresh_after_write = true;
3704
+ return 0;
3705
+ }
3706
+
3478
3707
  /***************************************************************
3479
3708
  * What a DIALOG sends while the model is being read again: the
3480
3709
  * Save of a form left open when the reload started, a
@@ -3717,7 +3946,8 @@ function ac_save_column(gobj, event, kw, src)
3717
3946
  if(refuse_if_readonly(gobj, event)) {
3718
3947
  return -1;
3719
3948
  }
3720
- if(reload_first(gobj, event)) {
3949
+ if(reload_first(gobj, event, {form: "column", topic: kw.topic, col: kw.col,
3950
+ creating: !!kw.creating, changed: kw.changed || {}})) {
3721
3951
  return -1;
3722
3952
  }
3723
3953
  if(built_on_a_replaced_model(gobj, event, kw)) {
@@ -3892,7 +4122,8 @@ function ac_save_topic(gobj, event, kw, src)
3892
4122
  if(refuse_if_readonly(gobj, event)) {
3893
4123
  return -1;
3894
4124
  }
3895
- if(reload_first(gobj, event)) {
4125
+ if(reload_first(gobj, event, {form: "topic", topic: kw.topic, col: "",
4126
+ creating: !!kw.creating, changed: kw.changed || {}})) {
3896
4127
  return -1;
3897
4128
  }
3898
4129
  if(built_on_a_replaced_model(gobj, event, kw)) {
@@ -4306,7 +4537,9 @@ function create_gclass(gclass_name)
4306
4537
  * the host are heard: every other action would be computed
4307
4538
  * against a model the answer still in the air is about to
4308
4539
  * change. */
4309
- ["ST_SAVING", COMMON.slice()]
4540
+ ["ST_SAVING", COMMON.filter((a) => a[0] !== "EV_REFRESH").concat([
4541
+ ["EV_REFRESH", ac_refresh_after_the_writes, null]
4542
+ ])]
4310
4543
  ];
4311
4544
 
4312
4545
  /*---------------------------------------------*