@yuneta/gobj-ui 7.25.4 → 7.25.5
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/README.md +72 -1
- package/dist/gobj-ui.cjs.js +194 -47
- package/dist/gobj-ui.es.js +195 -48
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +42 -23
- package/src/c_yui_shell.js +61 -8
- package/src/c_yui_treedb_topic_with_form.js +12 -1
- package/src/c_yui_treedb_topics.js +41 -10
- package/src/form_time_value.js +23 -0
- package/src/form_writes_in_flight.js +32 -13
- package/src/form_writes_in_flight.test.js +18 -3
- package/src/host_drafts.js +28 -2
- package/src/host_drafts.test.js +24 -1
- package/src/schema_editor_drafts.wiring.test.js +176 -0
- package/src/schema_version_bump.test.js +10 -5
- package/src/treedb_topics_transport.wiring.test.js +228 -0
- package/src/treedb_write_plan.js +32 -1
- package/src/treedb_write_plan.test.js +22 -1
package/README.md
CHANGED
|
@@ -790,6 +790,49 @@ writes in flight when the transport closes are answered refused on the
|
|
|
790
790
|
transport edge (`form_writes_in_flight.js`), so the form comes back on what
|
|
791
791
|
was typed instead of staying busy for ever.
|
|
792
792
|
|
|
793
|
+
**Where the transport edge comes from** (7.25.5). Until 7.25.4 only from the
|
|
794
|
+
host, as `EV_TRANSPORT_STATE` — and two hosts (wattyzer, yunovatios) never
|
|
795
|
+
forwarded it, so a websocket drop during a Save left Save and Cancel busy for
|
|
796
|
+
ever. The view now also hears it from its SHELL: `yui_shell_set_connection_state()`
|
|
797
|
+
— which every single-backend app already calls for the toolbar dot — publishes
|
|
798
|
+
`EV_CONNECTION_STATE {connected}` on each edge, and `C_YUI_TREEDB_TOPICS`
|
|
799
|
+
subscribes to it in `mt_start`. Nothing to add in such a host. A host with
|
|
800
|
+
one transport per view (gui_treedb's `C_TREEDB_VIEW`, gui_agent's
|
|
801
|
+
`C_AGENT_TREEDB_VIEW`) keeps forwarding `EV_TRANSPORT_STATE`; both paths can
|
|
802
|
+
fire for one edge, and the view answers each write once. The view never
|
|
803
|
+
subscribes to the transport itself: a subscription to a `C_IEVENT_CLI` is
|
|
804
|
+
forwarded to the backend.
|
|
805
|
+
|
|
806
|
+
`EV_CONNECTION_STATE` reaches only the subscribers that DECLARE it: an app
|
|
807
|
+
that subscribed to every event of its shell with the `subscriber` attr does
|
|
808
|
+
not get it (the shell's `mt_publication_pre_filter`), so adding it broke no
|
|
809
|
+
app.
|
|
810
|
+
|
|
811
|
+
```js
|
|
812
|
+
// The app, from its transport handlers -- as it already did for the dot:
|
|
813
|
+
yui_shell_set_connection_state(shell, false); // EV_ON_CLOSE
|
|
814
|
+
// -> every C_YUI_TREEDB_TOPICS under that shell answers its forms'
|
|
815
|
+
// writes in flight EV_WRITE_REFUSED, and they come back on what was typed.
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
A write is known by its topic AND its serial (`<topic>^<form_write>`): each
|
|
819
|
+
topic's form counts its serials from 1, and keyed by the serial alone two
|
|
820
|
+
forms saving at once shared one entry.
|
|
821
|
+
|
|
822
|
+
**The pkey2 of an update goes back as the record had it** (7.25.5). It names
|
|
823
|
+
the instance the write is for, and a `time` pkey2 that crossed the form's
|
|
824
|
+
`datetime-local` came back without its seconds — another instance, or none.
|
|
825
|
+
`instance_keys_of()` (`treedb_write_plan.js`) keeps it from the record the
|
|
826
|
+
form was opened on.
|
|
827
|
+
|
|
828
|
+
**Known limit — the change of hour** (`form_time_value.js`). A
|
|
829
|
+
`datetime-local` holds a local wall time and no offset. The night the clocks
|
|
830
|
+
go back, one hour happens twice: an epoch in its second pass is shown with the
|
|
831
|
+
wall time of the first and read back one hour earlier, so saving that record —
|
|
832
|
+
any field of it — moves a WRITABLE time column holding it by -3600 s. The
|
|
833
|
+
hour skipped in spring holds no epoch, so nothing read there comes back wrong.
|
|
834
|
+
Read-only time columns never go back, and a time pkey2 goes back as it was.
|
|
835
|
+
|
|
793
836
|
```js
|
|
794
837
|
// A host of C_YUI_TREEDB_TOPIC_WITH_FORM that answers the form's writes
|
|
795
838
|
gobj_create_pure_child(name, "C_YUI_TREEDB_TOPIC_WITH_FORM",
|
|
@@ -802,7 +845,24 @@ gobj_send_event(topic_form, ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED",
|
|
|
802
845
|
**A delete is resolved by IDENTITY, never by position** (7.23.193). The row's
|
|
803
846
|
`id` crosses the confirmation dialog; a Tabulator position is regenerated by
|
|
804
847
|
every row the node events add or remove while the dialog is open, and it used
|
|
805
|
-
to delete the row that sat there when the person answered.
|
|
848
|
+
to delete the row that sat there when the person answered. Rows (table) or
|
|
849
|
+
cards (`C_G6_NODES_TREE`) that another writer deleted while the question was
|
|
850
|
+
open are not deleted and the person is told (7.25.4); the rest go on.
|
|
851
|
+
|
|
852
|
+
Consumer i18n keys of the form's writes and deletes — define them in the app,
|
|
853
|
+
or they render as the key itself in every language:
|
|
854
|
+
|
|
855
|
+
| key | shown when |
|
|
856
|
+
|---|---|
|
|
857
|
+
| `no session` | a Save asked with no backend session: the write is not sent (7.25.2) |
|
|
858
|
+
| `some records were gone before the delete` | a confirmed delete named rows or cards that no longer exist (7.25.4). The text must not promise a refresh: nothing is reloaded, and in the graph there is no table |
|
|
859
|
+
|
|
860
|
+
```js
|
|
861
|
+
// en.js / es.js of the app
|
|
862
|
+
"no session": "No session",
|
|
863
|
+
"some records were gone before the delete":
|
|
864
|
+
"Some records no longer existed when the delete was confirmed; they were not deleted",
|
|
865
|
+
```
|
|
806
866
|
|
|
807
867
|
### Reading a topic table: filters, columns, CSV
|
|
808
868
|
|
|
@@ -2030,6 +2090,17 @@ host sends it as `EV_DRAFTS`; `drafts_of_saved_answer()` reads the unnamed
|
|
|
2030
2090
|
answer into the shape the event takes. What the session wrote stays marked;
|
|
2031
2091
|
what the host said is applied again after every reload of the model.
|
|
2032
2092
|
|
|
2093
|
+
**`EV_DRAFTS` REPLACES what the host said before** (7.25.5). It only added,
|
|
2094
|
+
and a topic the host had named before a Save stayed a draft after the Save
|
|
2095
|
+
that published it ("unsaved schema changes" again). The editor keeps the
|
|
2096
|
+
host's marks apart from its session's writes (`host_draft_ids()`) and swaps
|
|
2097
|
+
them whole on each `EV_DRAFTS`, so a host sends the COMPLETE set every time —
|
|
2098
|
+
an empty `{drafts: {}}` included, which is how "nothing is a draft any more"
|
|
2099
|
+
is said. What `draft_changed` measures is the node's: the draft against the
|
|
2100
|
+
SAVED schema on a node newer than yunetas 7.25.3, against the file in use on
|
|
2101
|
+
a 7.25.3 node (where a topic saved and not yet applied is still a draft until
|
|
2102
|
+
the Apply).
|
|
2103
|
+
|
|
2033
2104
|
```js
|
|
2034
2105
|
// The host, when its saved-schema answers land (rows = the unnamed answer's data)
|
|
2035
2106
|
import {drafts_of_saved_answer} from "@yuneta/gobj-ui/src/host_drafts.js";
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -924,6 +924,7 @@ var PRIVATE_DATA$22 = {
|
|
|
924
924
|
avatar_nodes: [],
|
|
925
925
|
translator: null,
|
|
926
926
|
conn_nodes: [],
|
|
927
|
+
connected: null,
|
|
927
928
|
active_dropdown: null
|
|
928
929
|
};
|
|
929
930
|
var __gclass__$22 = null;
|
|
@@ -2555,11 +2556,28 @@ function ac_nav_item_close(gobj, event, kw, src) {
|
|
|
2555
2556
|
/***************************************************************
|
|
2556
2557
|
* FSM
|
|
2557
2558
|
***************************************************************/
|
|
2559
|
+
/***************************************************************
|
|
2560
|
+
* EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
|
|
2561
|
+
* backend answer subscribes to it by name. An app that subscribed
|
|
2562
|
+
* to EVERY event of its shell with one attr (`subscriber`) did not
|
|
2563
|
+
* ask for it and does not declare it -- published to it anyway, the
|
|
2564
|
+
* event would be an FSM error in the app on every connection edge,
|
|
2565
|
+
* and adding it to the shell a breaking change for every such app.
|
|
2566
|
+
* So it reaches only the subscribers that declare it. Every other
|
|
2567
|
+
* event of the shell goes out as before.
|
|
2568
|
+
***************************************************************/
|
|
2569
|
+
function mt_publication_pre_filter(gobj, subs, event, kw) {
|
|
2570
|
+
if (event !== "EV_CONNECTION_STATE") return 1;
|
|
2571
|
+
let subscriber = subs && subs.subscriber;
|
|
2572
|
+
if (!subscriber || !(0, _yuneta_gobj_js.gobj_has_event)(subscriber, event, 0)) return 0;
|
|
2573
|
+
return 1;
|
|
2574
|
+
}
|
|
2558
2575
|
var gmt$22 = {
|
|
2559
2576
|
mt_create: mt_create$22,
|
|
2560
2577
|
mt_start: mt_start$20,
|
|
2561
2578
|
mt_stop: mt_stop$20,
|
|
2562
|
-
mt_destroy: mt_destroy$22
|
|
2579
|
+
mt_destroy: mt_destroy$22,
|
|
2580
|
+
mt_publication_pre_filter
|
|
2563
2581
|
};
|
|
2564
2582
|
function create_gclass$22(gclass_name) {
|
|
2565
2583
|
if (__gclass__$22) {
|
|
@@ -2589,7 +2607,8 @@ function create_gclass$22(gclass_name) {
|
|
|
2589
2607
|
["EV_NAV_ITEM_CLOSE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
|
|
2590
2608
|
["EV_ROUTE_REQUESTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
|
|
2591
2609
|
["EV_ROUTE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
|
|
2592
|
-
["EV_LANGUAGE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
|
|
2610
|
+
["EV_LANGUAGE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
|
|
2611
|
+
["EV_CONNECTION_STATE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
|
|
2593
2612
|
];
|
|
2594
2613
|
__gclass__$22 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, event_types, states, gmt$22, 0, attrs_table$22, PRIVATE_DATA$22, 0, 0, 0, _yuneta_gobj_js.gclass_flag_t.gcflag_no_check_output_events);
|
|
2595
2614
|
if (!__gclass__$22) return -1;
|
|
@@ -2872,13 +2891,17 @@ function yui_shell_translate(shell_gobj, $el) {
|
|
|
2872
2891
|
* is-connected / is-disconnected classes; CSS owns the look.
|
|
2873
2892
|
************************************************************/
|
|
2874
2893
|
function yui_shell_set_connection_state(shell_gobj, connected) {
|
|
2894
|
+
if (!shell_gobj || !(0, _yuneta_gobj_js.is_gobj)(shell_gobj) || (0, _yuneta_gobj_js.gobj_is_destroying)(shell_gobj)) return;
|
|
2875
2895
|
let priv = shell_gobj.priv;
|
|
2876
|
-
if (!priv
|
|
2896
|
+
if (!priv) return;
|
|
2877
2897
|
let on = !!connected;
|
|
2878
|
-
|
|
2898
|
+
let changed = priv.connected !== on;
|
|
2899
|
+
priv.connected = on;
|
|
2900
|
+
if ((0, _yuneta_gobj_js.is_array)(priv.conn_nodes)) for (let $n of priv.conn_nodes) {
|
|
2879
2901
|
$n.classList.toggle("is-connected", on);
|
|
2880
2902
|
$n.classList.toggle("is-disconnected", !on);
|
|
2881
2903
|
}
|
|
2904
|
+
if (changed) (0, _yuneta_gobj_js.gobj_publish_event)(shell_gobj, "EV_CONNECTION_STATE", { connected: on });
|
|
2882
2905
|
}
|
|
2883
2906
|
/************************************************************
|
|
2884
2907
|
* Swap the icon of a toolbar item at runtime (host-driven,
|
|
@@ -7319,6 +7342,29 @@ function json_diff_rows(doc_a, doc_b) {
|
|
|
7319
7342
|
* them, and they must be each other's inverse: a writable time
|
|
7320
7343
|
* column travels back on EVERY save of the record.
|
|
7321
7344
|
*
|
|
7345
|
+
* KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
|
|
7346
|
+
* LOCAL WALL time and no offset, and a wall time is not always
|
|
7347
|
+
* one instant:
|
|
7348
|
+
*
|
|
7349
|
+
* - the night the clocks go back (autumn), one hour happens
|
|
7350
|
+
* twice. "02:30" names two instants an hour apart, and
|
|
7351
|
+
* `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
|
|
7352
|
+
* (ECMAScript's "compatible" disambiguation). An epoch in the
|
|
7353
|
+
* SECOND pass of that hour is shown as 02:30 and comes back
|
|
7354
|
+
* one hour earlier: saving the record -- any field of it --
|
|
7355
|
+
* moves a writable time column that holds it by -3600 s.
|
|
7356
|
+
* - the night the clocks go forward (spring), one hour does not
|
|
7357
|
+
* exist. No epoch is shown in it, so nothing read comes back
|
|
7358
|
+
* wrong; a wall time TYPED in it is moved forward by the
|
|
7359
|
+
* browser.
|
|
7360
|
+
*
|
|
7361
|
+
* Not fixable here without the offset, which the input cannot
|
|
7362
|
+
* carry. What is safe: a read-only time column never goes back
|
|
7363
|
+
* (treedb_write_plan.js, col_goes_back_to_treedb), and a time
|
|
7364
|
+
* pkey2 goes back as the record had it, never through this
|
|
7365
|
+
* widget (instance_keys_of). A writable time column edited or
|
|
7366
|
+
* merely saved inside the repeated hour is the exposed case.
|
|
7367
|
+
*
|
|
7322
7368
|
* Copyright (c) 2026, ArtGins.
|
|
7323
7369
|
* All Rights Reserved.
|
|
7324
7370
|
****************************************************************************/
|
|
@@ -23026,30 +23072,46 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
23026
23072
|
* form_writes_in_flight.js
|
|
23027
23073
|
*
|
|
23028
23074
|
* The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
|
|
23029
|
-
* and not yet answered
|
|
23030
|
-
*
|
|
23031
|
-
*
|
|
23032
|
-
*
|
|
23033
|
-
*
|
|
23034
|
-
*
|
|
23035
|
-
*
|
|
23075
|
+
* and not yet answered. A form that waits for its answer
|
|
23076
|
+
* (form_waits_for_answer) stays open and busy until the host
|
|
23077
|
+
* answers, so the host has to answer EVERY way a write ends --
|
|
23078
|
+
* and the transport closing under a write in flight, or a
|
|
23079
|
+
* command that never left because there was no session, are two
|
|
23080
|
+
* of them that nobody answered: the form stayed busy for ever
|
|
23081
|
+
* (N8 of the 2026-09-22 review).
|
|
23082
|
+
*
|
|
23083
|
+
* A write is known by its TOPIC and its `form_write` serial,
|
|
23084
|
+
* never by the serial alone: each topic has its own form, and
|
|
23085
|
+
* each form counts its serials from 1. Keyed by the serial, two
|
|
23086
|
+
* forms with a write in flight at once shared the key "1", the
|
|
23087
|
+
* first answer settled both, and the transport closing then
|
|
23088
|
+
* answered only one of them.
|
|
23036
23089
|
*
|
|
23037
23090
|
* Copyright (c) 2026, ArtGins.
|
|
23038
23091
|
* All Rights Reserved.
|
|
23039
23092
|
***********************************************************************/
|
|
23040
23093
|
/************************************************************
|
|
23094
|
+
* The key of one write: `<topic>^<serial>`.
|
|
23095
|
+
************************************************************/
|
|
23096
|
+
function form_write_key(topic_name, form_write) {
|
|
23097
|
+
return `${topic_name || ""}^${form_write}`;
|
|
23098
|
+
}
|
|
23099
|
+
/************************************************************
|
|
23041
23100
|
* A write left: remember whose it is
|
|
23042
23101
|
************************************************************/
|
|
23043
23102
|
function track_form_write(in_flight, form_write, topic_name) {
|
|
23044
23103
|
if (!in_flight || !form_write) return;
|
|
23045
|
-
in_flight[form_write] =
|
|
23104
|
+
in_flight[form_write_key(topic_name, form_write)] = {
|
|
23105
|
+
form_write,
|
|
23106
|
+
topic_name: topic_name || ""
|
|
23107
|
+
};
|
|
23046
23108
|
}
|
|
23047
23109
|
/************************************************************
|
|
23048
23110
|
* A write was answered, one way or the other
|
|
23049
23111
|
************************************************************/
|
|
23050
|
-
function settle_form_write(in_flight, form_write) {
|
|
23112
|
+
function settle_form_write(in_flight, form_write, topic_name) {
|
|
23051
23113
|
if (!in_flight || !form_write) return;
|
|
23052
|
-
delete in_flight[form_write];
|
|
23114
|
+
delete in_flight[form_write_key(topic_name, form_write)];
|
|
23053
23115
|
}
|
|
23054
23116
|
/************************************************************
|
|
23055
23117
|
* Nothing in flight can be answered any more: take them all,
|
|
@@ -23058,12 +23120,13 @@ function settle_form_write(in_flight, form_write) {
|
|
|
23058
23120
|
function abandon_form_writes(in_flight) {
|
|
23059
23121
|
const abandoned = [];
|
|
23060
23122
|
if (!in_flight) return abandoned;
|
|
23061
|
-
Object.keys(in_flight).forEach((
|
|
23123
|
+
Object.keys(in_flight).forEach((key) => {
|
|
23124
|
+
const w = in_flight[key];
|
|
23062
23125
|
abandoned.push({
|
|
23063
|
-
form_write: Number(
|
|
23064
|
-
topic_name:
|
|
23126
|
+
form_write: Number(w.form_write),
|
|
23127
|
+
topic_name: w.topic_name
|
|
23065
23128
|
});
|
|
23066
|
-
delete in_flight[
|
|
23129
|
+
delete in_flight[key];
|
|
23067
23130
|
});
|
|
23068
23131
|
return abandoned;
|
|
23069
23132
|
}
|
|
@@ -23635,6 +23698,7 @@ var __gclass__$6 = null;
|
|
|
23635
23698
|
***************************************************************/
|
|
23636
23699
|
function mt_create$6(gobj) {
|
|
23637
23700
|
gobj.priv.form_writes = {};
|
|
23701
|
+
gobj.priv.conn_shell = null;
|
|
23638
23702
|
build_ui$6(gobj);
|
|
23639
23703
|
let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
|
|
23640
23704
|
if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
|
|
@@ -23644,12 +23708,22 @@ function mt_create$6(gobj) {
|
|
|
23644
23708
|
* Framework Method: Start
|
|
23645
23709
|
***************************************************************/
|
|
23646
23710
|
function mt_start$6(gobj) {
|
|
23711
|
+
let shell = yui_shell_of(gobj);
|
|
23712
|
+
if (shell) {
|
|
23713
|
+
(0, _yuneta_gobj_js.gobj_subscribe_event)(shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
23714
|
+
gobj.priv.conn_shell = shell;
|
|
23715
|
+
}
|
|
23647
23716
|
request_treedb_descs$1(gobj);
|
|
23648
23717
|
}
|
|
23649
23718
|
/***************************************************************
|
|
23650
23719
|
* Framework Method: Stop
|
|
23651
23720
|
***************************************************************/
|
|
23652
23721
|
function mt_stop$6(gobj) {
|
|
23722
|
+
let priv = gobj.priv;
|
|
23723
|
+
if (priv.conn_shell) {
|
|
23724
|
+
if (!(0, _yuneta_gobj_js.gobj_is_destroying)(priv.conn_shell)) (0, _yuneta_gobj_js.gobj_unsubscribe_event)(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
23725
|
+
priv.conn_shell = null;
|
|
23726
|
+
}
|
|
23653
23727
|
let shell = yui_shell_of(gobj);
|
|
23654
23728
|
if (shell) yui_shell_set_sub_routes(shell, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "base_route"), null);
|
|
23655
23729
|
close_json_viewer$1(gobj);
|
|
@@ -25055,7 +25129,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25055
25129
|
if (command === "update-node" || command === "create-node") {
|
|
25056
25130
|
let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
|
|
25057
25131
|
if (failed_topic) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
|
|
25058
|
-
settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
|
|
25132
|
+
settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), failed_topic);
|
|
25059
25133
|
answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), false);
|
|
25060
25134
|
}
|
|
25061
25135
|
}
|
|
@@ -25093,7 +25167,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25093
25167
|
created: command === "create-node",
|
|
25094
25168
|
command
|
|
25095
25169
|
});
|
|
25096
|
-
settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0));
|
|
25170
|
+
settle_form_write(gobj.priv.form_writes, (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0));
|
|
25097
25171
|
answer_form_write(gobj, get_gobj_formtable(gobj, (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0)), (0, _yuneta_gobj_js.kw_get_int)(gobj, kw_command, "form_write", 0, 0), true);
|
|
25098
25172
|
break;
|
|
25099
25173
|
case "delete-node": break;
|
|
@@ -25237,11 +25311,19 @@ function ac_back_to_topics(gobj, event, kw, src) {
|
|
|
25237
25311
|
return 0;
|
|
25238
25312
|
}
|
|
25239
25313
|
/************************************************************
|
|
25240
|
-
*
|
|
25241
|
-
*
|
|
25242
|
-
*
|
|
25243
|
-
*
|
|
25244
|
-
*
|
|
25314
|
+
* A backend transport edge, from either of two places:
|
|
25315
|
+
* - EV_TRANSPORT_STATE, forwarded by a host that keeps one
|
|
25316
|
+
* transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
|
|
25317
|
+
* C_AGENT_TREEDB_VIEW);
|
|
25318
|
+
* - EV_CONNECTION_STATE, published by the SHELL when the app tells
|
|
25319
|
+
* it its backend went up or down (yui_shell_set_connection_state),
|
|
25320
|
+
* which is what every single-backend app already does for the
|
|
25321
|
+
* toolbar dot. No host has to remember anything for this one.
|
|
25322
|
+
* Both can arrive for one edge; what is done here is idempotent.
|
|
25323
|
+
* The toolbar disables the JSON viewers the moment the session drops
|
|
25324
|
+
* (and re-enables them on reconnect). The library view must not
|
|
25325
|
+
* subscribe to the C_IEVENT_CLI itself: that forwards the subscription
|
|
25326
|
+
* upstream and breaks the session.
|
|
25245
25327
|
************************************************************/
|
|
25246
25328
|
function ac_transport_state$2(gobj, event, kw, src) {
|
|
25247
25329
|
let connected = !!(kw && kw.connected);
|
|
@@ -25773,6 +25855,11 @@ function create_gclass$6(gclass_name) {
|
|
|
25773
25855
|
"EV_TRANSPORT_STATE",
|
|
25774
25856
|
ac_transport_state$2,
|
|
25775
25857
|
null
|
|
25858
|
+
],
|
|
25859
|
+
[
|
|
25860
|
+
"EV_CONNECTION_STATE",
|
|
25861
|
+
ac_transport_state$2,
|
|
25862
|
+
null
|
|
25776
25863
|
]
|
|
25777
25864
|
]]];
|
|
25778
25865
|
const event_types = [
|
|
@@ -25803,6 +25890,7 @@ function create_gclass$6(gclass_name) {
|
|
|
25803
25890
|
["EV_HIDE", 0],
|
|
25804
25891
|
["EV_BACK_TO_TOPICS", 0],
|
|
25805
25892
|
["EV_TRANSPORT_STATE", 0],
|
|
25893
|
+
["EV_CONNECTION_STATE", 0],
|
|
25806
25894
|
["EV_TOPIC_SELECTED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS]
|
|
25807
25895
|
];
|
|
25808
25896
|
__gclass__$6 = (0, _yuneta_gobj_js.gclass_create)(gclass_name, event_types, states, gmt$6, 0, attrs_table$6, PRIVATE_DATA$6, 0, 0, 0, 0);
|
|
@@ -26520,6 +26608,23 @@ function col_goes_back_to_treedb(desc, col) {
|
|
|
26520
26608
|
if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
|
|
26521
26609
|
return topic_pkey2s(d).includes(col.id);
|
|
26522
26610
|
}
|
|
26611
|
+
/************************************************************
|
|
26612
|
+
* The INSTANCE an update of `record` is for: its pkey2 values, as the
|
|
26613
|
+
* record held them when the form was opened, {pkey2: value}.
|
|
26614
|
+
*
|
|
26615
|
+
* They go back as they WERE, never as the form's widget hands them: a
|
|
26616
|
+
* `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
|
|
26617
|
+
* that crossed the widget (seconds dropped, a local wall time read
|
|
26618
|
+
* back, an hour the autumn change names twice) names ANOTHER instance
|
|
26619
|
+
* -- or none, and C_NODE falls back to the primary. The pkey2 is an
|
|
26620
|
+
* address, not a field of the edit.
|
|
26621
|
+
************************************************************/
|
|
26622
|
+
function instance_keys_of(record, desc) {
|
|
26623
|
+
const keys = {};
|
|
26624
|
+
if (!record || typeof record !== "object") return keys;
|
|
26625
|
+
for (const k of topic_pkey2s(desc || {})) if (Object.prototype.hasOwnProperty.call(record, k) && record[k] !== void 0 && record[k] !== null) keys[k] = record[k];
|
|
26626
|
+
return keys;
|
|
26627
|
+
}
|
|
26523
26628
|
//#endregion
|
|
26524
26629
|
//#region src/delete_impact.js
|
|
26525
26630
|
/***********************************************************************
|
|
@@ -28251,6 +28356,7 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
28251
28356
|
},
|
|
28252
28357
|
[]
|
|
28253
28358
|
]);
|
|
28359
|
+
priv.form_instance = form_mode === "update" ? instance_keys_of(record, desc) : null;
|
|
28254
28360
|
let form_plan = write_plan(gobj);
|
|
28255
28361
|
let form = (0, _yuneta_gobj_js.gobj_create_pure_child)("form_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)), "C_YUI_FORM", {
|
|
28256
28362
|
template: build_form_template(gobj),
|
|
@@ -28319,6 +28425,7 @@ function teardown_form_child(gobj) {
|
|
|
28319
28425
|
priv.reading_files = 0;
|
|
28320
28426
|
priv.awaiting_write = 0;
|
|
28321
28427
|
priv.form_busy = { busy: false };
|
|
28428
|
+
priv.form_instance = null;
|
|
28322
28429
|
if (priv.form) {
|
|
28323
28430
|
if ((0, _yuneta_gobj_js.gobj_is_running)(priv.form)) (0, _yuneta_gobj_js.gobj_stop)(priv.form);
|
|
28324
28431
|
(0, _yuneta_gobj_js.gobj_destroy)(priv.form);
|
|
@@ -29578,6 +29685,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
|
|
|
29578
29685
|
topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
|
|
29579
29686
|
record: strip_read_only_cols(gobj, record)
|
|
29580
29687
|
};
|
|
29688
|
+
if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
|
|
29581
29689
|
if (form_write) kw_write.form_write = form_write;
|
|
29582
29690
|
if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
|
|
29583
29691
|
(0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
|
|
@@ -70278,16 +70386,39 @@ function register_c_yui_treedb_schema() {
|
|
|
70278
70386
|
* __system__ still differed from the file in use (N13 of the
|
|
70279
70387
|
* 2026-09-22 review). The host reads that difference from
|
|
70280
70388
|
* C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
|
|
70281
|
-
* hands it here; the editor applies it to its model
|
|
70389
|
+
* hands it here; the editor applies it to its model, in place
|
|
70390
|
+
* of what the host said before.
|
|
70391
|
+
*
|
|
70392
|
+
* What `draft_changed` measures depends on the node: the draft
|
|
70393
|
+
* against the SAVED schema on a node newer than yunetas 7.25.3,
|
|
70394
|
+
* and against the file IN USE on a 7.25.3 node -- where a topic saved and
|
|
70395
|
+
* not yet applied is still named, and the editor, which cannot
|
|
70396
|
+
* tell the two apart, shows it as a draft until the Apply.
|
|
70282
70397
|
*
|
|
70283
70398
|
* Copyright (c) 2026, ArtGins.
|
|
70284
70399
|
* All Rights Reserved.
|
|
70285
70400
|
***********************************************************************/
|
|
70286
70401
|
/************************************************************
|
|
70402
|
+
* The topics of `model` the host names as drafts, as
|
|
70403
|
+
* {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
|
|
70404
|
+
* treedb or a topic the model does not hold is ignored.
|
|
70405
|
+
*
|
|
70406
|
+
* What the editor keeps is THIS, apart from what its own session
|
|
70407
|
+
* wrote, and replaced whole on every EV_DRAFTS: the host is the
|
|
70408
|
+
* truth after a reload. Folded into the session's marks, a topic the
|
|
70409
|
+
* host once named stayed a draft after the save that published it.
|
|
70410
|
+
************************************************************/
|
|
70411
|
+
function host_draft_ids(model, drafts) {
|
|
70412
|
+
const ids = {};
|
|
70413
|
+
mark_host_drafts(ids, model, drafts);
|
|
70414
|
+
return ids;
|
|
70415
|
+
}
|
|
70416
|
+
/************************************************************
|
|
70287
70417
|
* Mark in `written` the topics of `model` the host names as
|
|
70288
70418
|
* drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
|
|
70289
70419
|
* topic the model does not hold is ignored. Returns how many were
|
|
70290
|
-
* marked.
|
|
70420
|
+
* marked. It only ADDS: a caller that must forget what the host
|
|
70421
|
+
* no longer names starts from an empty map (host_draft_ids()).
|
|
70291
70422
|
************************************************************/
|
|
70292
70423
|
function mark_host_drafts(written, model, drafts) {
|
|
70293
70424
|
let marked = 0;
|
|
@@ -71926,8 +72057,9 @@ function write_options(op) {
|
|
|
71926
72057
|
* the owner's design of M36 (2026-09-21 review) every write here
|
|
71927
72058
|
* raised both versions, so an edit half made was already the
|
|
71928
72059
|
* schema of the next start. The topic list marks what this session
|
|
71929
|
-
* wrote and has not saved
|
|
71930
|
-
* what forgets it
|
|
72060
|
+
* wrote and has not saved (the host's EV_REFRESH after a save is
|
|
72061
|
+
* what forgets it) and what the host says is not saved (EV_DRAFTS,
|
|
72062
|
+
* replaced whole each time it is sent).
|
|
71931
72063
|
*
|
|
71932
72064
|
* WHAT ELSE IS HERE, and why each is here and not somewhere else:
|
|
71933
72065
|
*
|
|
@@ -72006,6 +72138,8 @@ var PRIVATE_DATA$1 = {
|
|
|
72006
72138
|
diagram: false,
|
|
72007
72139
|
baseline: null,
|
|
72008
72140
|
written: null,
|
|
72141
|
+
host_drafts: null,
|
|
72142
|
+
host_ids: null,
|
|
72009
72143
|
order_undo: null,
|
|
72010
72144
|
diagram_gobj: null,
|
|
72011
72145
|
dialog: null,
|
|
@@ -72030,6 +72164,7 @@ function mt_create$1(gobj) {
|
|
|
72030
72164
|
priv.baseline = {};
|
|
72031
72165
|
priv.written = {};
|
|
72032
72166
|
priv.host_drafts = {};
|
|
72167
|
+
priv.host_ids = {};
|
|
72033
72168
|
priv.save_queue = [];
|
|
72034
72169
|
build_ui$1(gobj);
|
|
72035
72170
|
let subscriber = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "subscriber");
|
|
@@ -72269,25 +72404,33 @@ function start_measuring(gobj) {
|
|
|
72269
72404
|
let priv = gobj.priv;
|
|
72270
72405
|
priv.baseline = {};
|
|
72271
72406
|
priv.written = {};
|
|
72407
|
+
priv.host_ids = {};
|
|
72272
72408
|
if (!priv.model) return;
|
|
72273
72409
|
for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
|
|
72274
|
-
|
|
72410
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
72275
72411
|
}
|
|
72276
72412
|
/***************************************************************
|
|
72277
|
-
* "This topic
|
|
72413
|
+
* "This topic holds an edit that is not saved."
|
|
72278
72414
|
*
|
|
72279
72415
|
* An edit is a DRAFT: it moves no version and reaches no treedb
|
|
72280
72416
|
* until the host saves it (C_TREEDB's save-schema, which raises the
|
|
72281
|
-
* versions of what changed)
|
|
72282
|
-
*
|
|
72417
|
+
* versions of what changed). Two sources say a topic is one, and
|
|
72418
|
+
* they are kept APART because they are forgotten differently:
|
|
72419
|
+
* - `written`: what THIS session wrote since the model was loaded
|
|
72420
|
+
* (a reload, the host's EV_REFRESH, forgets it -- the store says
|
|
72421
|
+
* what is left);
|
|
72422
|
+
* - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
|
|
72423
|
+
* by the next EV_DRAFTS. Folded into `written`, a topic the host
|
|
72424
|
+
* named once stayed a draft after the Save that published it.
|
|
72283
72425
|
*
|
|
72284
72426
|
* ONE predicate, because it is asked in three places -- the topic
|
|
72285
72427
|
* list draws a chip with it, the column view a banner, the export
|
|
72286
72428
|
* a warning -- and copies of a rule are how they drift.
|
|
72287
72429
|
***************************************************************/
|
|
72288
|
-
function topic_is_draft(
|
|
72289
|
-
if (!topic || !
|
|
72290
|
-
|
|
72430
|
+
function topic_is_draft(priv, topic) {
|
|
72431
|
+
if (!topic || !priv) return false;
|
|
72432
|
+
if (priv.written && priv.written[topic.id]) return true;
|
|
72433
|
+
return priv.host_ids && priv.host_ids[topic.id] ? true : false;
|
|
72291
72434
|
}
|
|
72292
72435
|
/***************************************************************
|
|
72293
72436
|
* Say what the whole store would be refused for.
|
|
@@ -72729,7 +72872,7 @@ function render_topics(gobj, $body) {
|
|
|
72729
72872
|
let twice = repeated_names(treedb.topics);
|
|
72730
72873
|
let $rows = [];
|
|
72731
72874
|
for (let topic of treedb.topics) {
|
|
72732
|
-
let pending = topic_is_draft(priv
|
|
72875
|
+
let pending = topic_is_draft(priv, topic);
|
|
72733
72876
|
let ambiguous = !!twice[topic.name];
|
|
72734
72877
|
let address = ambiguous ? topic.id : topic.name;
|
|
72735
72878
|
let $actions = [];
|
|
@@ -73052,7 +73195,7 @@ function render_columns(gobj, $body) {
|
|
|
73052
73195
|
***************************************************************/
|
|
73053
73196
|
function draft_banner(gobj, topic) {
|
|
73054
73197
|
let priv = gobj.priv;
|
|
73055
|
-
if (!topic_is_draft(priv
|
|
73198
|
+
if (!topic_is_draft(priv, topic)) return null;
|
|
73056
73199
|
if (is_readonly(gobj)) return null;
|
|
73057
73200
|
return (0, _yuneta_gobj_js.createElement2)([
|
|
73058
73201
|
"div",
|
|
@@ -73942,7 +74085,7 @@ function open_report(gobj, findings, treedb_id) {
|
|
|
73942
74085
|
***************************************************************/
|
|
73943
74086
|
function open_export(gobj, treedb) {
|
|
73944
74087
|
let priv = gobj.priv;
|
|
73945
|
-
let drafts = treedb.topics.some((topic) => topic_is_draft(priv
|
|
74088
|
+
let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
|
|
73946
74089
|
let c_text = schema_to_c(treedb);
|
|
73947
74090
|
let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
|
|
73948
74091
|
let $content = (0, _yuneta_gobj_js.createElement2)([
|
|
@@ -74710,18 +74853,22 @@ function ac_refresh$1(gobj, event, kw, src) {
|
|
|
74710
74853
|
}
|
|
74711
74854
|
/***************************************************************
|
|
74712
74855
|
* {drafts: {treedb_name: [topic names]}} -- the host says which
|
|
74713
|
-
* topics hold a draft in __system__ that
|
|
74714
|
-
*
|
|
74715
|
-
*
|
|
74716
|
-
*
|
|
74717
|
-
*
|
|
74718
|
-
*
|
|
74719
|
-
*
|
|
74856
|
+
* topics hold a draft in __system__ that is not saved (C_TREEDB's
|
|
74857
|
+
* saved-schema, `draft_changed`). The mark of a write lived in this
|
|
74858
|
+
* session's memory only, so a reload of the page, a reconnect or a
|
|
74859
|
+
* refresh showed no draft while __system__ still differed (N13 of
|
|
74860
|
+
* the 2026-09-22 review).
|
|
74861
|
+
*
|
|
74862
|
+
* REPLACES what the host said before -- the host is the truth after
|
|
74863
|
+
* a reload, and a Save is exactly the host saying "none any more".
|
|
74864
|
+
* It only ever added, and a topic stayed a draft after the Save that
|
|
74865
|
+
* published it (M1 of the 2026-09-23 review). What this session wrote
|
|
74866
|
+
* stays marked: it is kept apart (see topic_is_draft()).
|
|
74720
74867
|
***************************************************************/
|
|
74721
74868
|
function ac_drafts(gobj, event, kw, src) {
|
|
74722
74869
|
let priv = gobj.priv;
|
|
74723
74870
|
priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
|
|
74724
|
-
|
|
74871
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
74725
74872
|
if (priv.model) render(gobj);
|
|
74726
74873
|
return 0;
|
|
74727
74874
|
}
|