@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/dist/gobj-ui.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_is_identical, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
|
|
1
|
+
import { SDATA, SDATA_END, clean_name, createElement2, createOneHtml, create_template_record, data_type_t, debounce, delete_from_list, duplicate_objects, empty_json, empty_string, escapeHtml, event_flag_t, flat_diff, gclass_create, gclass_find_by_name, gclass_flag_t, getPositionRelativeToBody, gobj_bottom_gobj, gobj_change_state, gobj_command, gobj_create, gobj_create_pure_child, gobj_create_service, gobj_current_state, gobj_destroy, gobj_find_child, gobj_find_service, gobj_full_name, gobj_gclass_name, gobj_get_gclass_trace_level2, gobj_global_trace_level, gobj_global_trace_no_level, gobj_has_event, gobj_is_destroying, gobj_is_playing, gobj_is_pure_child, gobj_is_running, gobj_is_service, gobj_is_volatil, gobj_match_children, gobj_name, gobj_parent, gobj_post_event, gobj_publish_event, gobj_read_attr, gobj_read_bool_attr, gobj_read_integer_attr, gobj_read_pointer_attr, gobj_read_str_attr, gobj_save_persistent_attrs, gobj_send_event, gobj_set_trace_machine_format, gobj_short_name, gobj_start, gobj_start_tree, gobj_stop, gobj_stop_children, gobj_subscribe_event, gobj_unsubscribe_event, gobj_write_attr, gobj_write_bool_attr, gobj_write_integer_attr, gobj_write_str_attr, gobj_yuno, gobj_yuno_name, is_array, is_boolean, is_date, is_gobj, is_null, is_number, is_object, is_string, json2flat, json_deep_copy, json_is_identical, json_object_size, json_object_update, json_object_update_missing, json_size, kw_clone_by_keys, kw_flag_t, kw_get_dict, kw_get_dict_value, kw_get_int, kw_get_local_storage_value, kw_get_str, kw_has_key, kw_set_dict_value, kw_set_local_storage_value, kwid_find_one_record, kwid_new_dict, log_error, log_warning, msg_iev_get_stack, parseBoolean, parseSVG, refresh_language, safeSrc, sdata_flag_t, set_console_log_enabled, set_log_callback, sprintf, str_in_list, template_get_field_desc, trace_level_t, trace_msg, treedb_decoder_fkey, treedb_encoder_fkey, treedb_get_field_desc, treedb_hook_data_size } from "@yuneta/gobj-js";
|
|
2
2
|
import i18next, { t } from "i18next";
|
|
3
3
|
import TomSelect from "tom-select";
|
|
4
4
|
import { createJSONEditor } from "vanilla-jsoneditor";
|
|
@@ -919,6 +919,7 @@ var PRIVATE_DATA$22 = {
|
|
|
919
919
|
avatar_nodes: [],
|
|
920
920
|
translator: null,
|
|
921
921
|
conn_nodes: [],
|
|
922
|
+
connected: null,
|
|
922
923
|
active_dropdown: null
|
|
923
924
|
};
|
|
924
925
|
var __gclass__$22 = null;
|
|
@@ -2550,11 +2551,28 @@ function ac_nav_item_close(gobj, event, kw, src) {
|
|
|
2550
2551
|
/***************************************************************
|
|
2551
2552
|
* FSM
|
|
2552
2553
|
***************************************************************/
|
|
2554
|
+
/***************************************************************
|
|
2555
|
+
* EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
|
|
2556
|
+
* backend answer subscribes to it by name. An app that subscribed
|
|
2557
|
+
* to EVERY event of its shell with one attr (`subscriber`) did not
|
|
2558
|
+
* ask for it and does not declare it -- published to it anyway, the
|
|
2559
|
+
* event would be an FSM error in the app on every connection edge,
|
|
2560
|
+
* and adding it to the shell a breaking change for every such app.
|
|
2561
|
+
* So it reaches only the subscribers that declare it. Every other
|
|
2562
|
+
* event of the shell goes out as before.
|
|
2563
|
+
***************************************************************/
|
|
2564
|
+
function mt_publication_pre_filter(gobj, subs, event, kw) {
|
|
2565
|
+
if (event !== "EV_CONNECTION_STATE") return 1;
|
|
2566
|
+
let subscriber = subs && subs.subscriber;
|
|
2567
|
+
if (!subscriber || !gobj_has_event(subscriber, event, 0)) return 0;
|
|
2568
|
+
return 1;
|
|
2569
|
+
}
|
|
2553
2570
|
var gmt$22 = {
|
|
2554
2571
|
mt_create: mt_create$22,
|
|
2555
2572
|
mt_start: mt_start$20,
|
|
2556
2573
|
mt_stop: mt_stop$20,
|
|
2557
|
-
mt_destroy: mt_destroy$22
|
|
2574
|
+
mt_destroy: mt_destroy$22,
|
|
2575
|
+
mt_publication_pre_filter
|
|
2558
2576
|
};
|
|
2559
2577
|
function create_gclass$22(gclass_name) {
|
|
2560
2578
|
if (__gclass__$22) {
|
|
@@ -2584,7 +2602,8 @@ function create_gclass$22(gclass_name) {
|
|
|
2584
2602
|
["EV_NAV_ITEM_CLOSE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2585
2603
|
["EV_ROUTE_REQUESTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2586
2604
|
["EV_ROUTE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2587
|
-
["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
|
|
2605
|
+
["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_PUBLIC_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2606
|
+
["EV_CONNECTION_STATE", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
|
|
2588
2607
|
];
|
|
2589
2608
|
__gclass__$22 = gclass_create(gclass_name, event_types, states, gmt$22, 0, attrs_table$22, PRIVATE_DATA$22, 0, 0, 0, gclass_flag_t.gcflag_no_check_output_events);
|
|
2590
2609
|
if (!__gclass__$22) return -1;
|
|
@@ -2867,13 +2886,17 @@ function yui_shell_translate(shell_gobj, $el) {
|
|
|
2867
2886
|
* is-connected / is-disconnected classes; CSS owns the look.
|
|
2868
2887
|
************************************************************/
|
|
2869
2888
|
function yui_shell_set_connection_state(shell_gobj, connected) {
|
|
2889
|
+
if (!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj)) return;
|
|
2870
2890
|
let priv = shell_gobj.priv;
|
|
2871
|
-
if (!priv
|
|
2891
|
+
if (!priv) return;
|
|
2872
2892
|
let on = !!connected;
|
|
2873
|
-
|
|
2893
|
+
let changed = priv.connected !== on;
|
|
2894
|
+
priv.connected = on;
|
|
2895
|
+
if (is_array(priv.conn_nodes)) for (let $n of priv.conn_nodes) {
|
|
2874
2896
|
$n.classList.toggle("is-connected", on);
|
|
2875
2897
|
$n.classList.toggle("is-disconnected", !on);
|
|
2876
2898
|
}
|
|
2899
|
+
if (changed) gobj_publish_event(shell_gobj, "EV_CONNECTION_STATE", { connected: on });
|
|
2877
2900
|
}
|
|
2878
2901
|
/************************************************************
|
|
2879
2902
|
* Swap the icon of a toolbar item at runtime (host-driven,
|
|
@@ -7314,6 +7337,29 @@ function json_diff_rows(doc_a, doc_b) {
|
|
|
7314
7337
|
* them, and they must be each other's inverse: a writable time
|
|
7315
7338
|
* column travels back on EVERY save of the record.
|
|
7316
7339
|
*
|
|
7340
|
+
* KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
|
|
7341
|
+
* LOCAL WALL time and no offset, and a wall time is not always
|
|
7342
|
+
* one instant:
|
|
7343
|
+
*
|
|
7344
|
+
* - the night the clocks go back (autumn), one hour happens
|
|
7345
|
+
* twice. "02:30" names two instants an hour apart, and
|
|
7346
|
+
* `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
|
|
7347
|
+
* (ECMAScript's "compatible" disambiguation). An epoch in the
|
|
7348
|
+
* SECOND pass of that hour is shown as 02:30 and comes back
|
|
7349
|
+
* one hour earlier: saving the record -- any field of it --
|
|
7350
|
+
* moves a writable time column that holds it by -3600 s.
|
|
7351
|
+
* - the night the clocks go forward (spring), one hour does not
|
|
7352
|
+
* exist. No epoch is shown in it, so nothing read comes back
|
|
7353
|
+
* wrong; a wall time TYPED in it is moved forward by the
|
|
7354
|
+
* browser.
|
|
7355
|
+
*
|
|
7356
|
+
* Not fixable here without the offset, which the input cannot
|
|
7357
|
+
* carry. What is safe: a read-only time column never goes back
|
|
7358
|
+
* (treedb_write_plan.js, col_goes_back_to_treedb), and a time
|
|
7359
|
+
* pkey2 goes back as the record had it, never through this
|
|
7360
|
+
* widget (instance_keys_of). A writable time column edited or
|
|
7361
|
+
* merely saved inside the repeated hour is the exposed case.
|
|
7362
|
+
*
|
|
7317
7363
|
* Copyright (c) 2026, ArtGins.
|
|
7318
7364
|
* All Rights Reserved.
|
|
7319
7365
|
****************************************************************************/
|
|
@@ -23021,30 +23067,46 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
23021
23067
|
* form_writes_in_flight.js
|
|
23022
23068
|
*
|
|
23023
23069
|
* The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
|
|
23024
|
-
* and not yet answered
|
|
23025
|
-
*
|
|
23026
|
-
*
|
|
23027
|
-
*
|
|
23028
|
-
*
|
|
23029
|
-
*
|
|
23030
|
-
*
|
|
23070
|
+
* and not yet answered. A form that waits for its answer
|
|
23071
|
+
* (form_waits_for_answer) stays open and busy until the host
|
|
23072
|
+
* answers, so the host has to answer EVERY way a write ends --
|
|
23073
|
+
* and the transport closing under a write in flight, or a
|
|
23074
|
+
* command that never left because there was no session, are two
|
|
23075
|
+
* of them that nobody answered: the form stayed busy for ever
|
|
23076
|
+
* (N8 of the 2026-09-22 review).
|
|
23077
|
+
*
|
|
23078
|
+
* A write is known by its TOPIC and its `form_write` serial,
|
|
23079
|
+
* never by the serial alone: each topic has its own form, and
|
|
23080
|
+
* each form counts its serials from 1. Keyed by the serial, two
|
|
23081
|
+
* forms with a write in flight at once shared the key "1", the
|
|
23082
|
+
* first answer settled both, and the transport closing then
|
|
23083
|
+
* answered only one of them.
|
|
23031
23084
|
*
|
|
23032
23085
|
* Copyright (c) 2026, ArtGins.
|
|
23033
23086
|
* All Rights Reserved.
|
|
23034
23087
|
***********************************************************************/
|
|
23035
23088
|
/************************************************************
|
|
23089
|
+
* The key of one write: `<topic>^<serial>`.
|
|
23090
|
+
************************************************************/
|
|
23091
|
+
function form_write_key(topic_name, form_write) {
|
|
23092
|
+
return `${topic_name || ""}^${form_write}`;
|
|
23093
|
+
}
|
|
23094
|
+
/************************************************************
|
|
23036
23095
|
* A write left: remember whose it is
|
|
23037
23096
|
************************************************************/
|
|
23038
23097
|
function track_form_write(in_flight, form_write, topic_name) {
|
|
23039
23098
|
if (!in_flight || !form_write) return;
|
|
23040
|
-
in_flight[form_write] =
|
|
23099
|
+
in_flight[form_write_key(topic_name, form_write)] = {
|
|
23100
|
+
form_write,
|
|
23101
|
+
topic_name: topic_name || ""
|
|
23102
|
+
};
|
|
23041
23103
|
}
|
|
23042
23104
|
/************************************************************
|
|
23043
23105
|
* A write was answered, one way or the other
|
|
23044
23106
|
************************************************************/
|
|
23045
|
-
function settle_form_write(in_flight, form_write) {
|
|
23107
|
+
function settle_form_write(in_flight, form_write, topic_name) {
|
|
23046
23108
|
if (!in_flight || !form_write) return;
|
|
23047
|
-
delete in_flight[form_write];
|
|
23109
|
+
delete in_flight[form_write_key(topic_name, form_write)];
|
|
23048
23110
|
}
|
|
23049
23111
|
/************************************************************
|
|
23050
23112
|
* Nothing in flight can be answered any more: take them all,
|
|
@@ -23053,12 +23115,13 @@ function settle_form_write(in_flight, form_write) {
|
|
|
23053
23115
|
function abandon_form_writes(in_flight) {
|
|
23054
23116
|
const abandoned = [];
|
|
23055
23117
|
if (!in_flight) return abandoned;
|
|
23056
|
-
Object.keys(in_flight).forEach((
|
|
23118
|
+
Object.keys(in_flight).forEach((key) => {
|
|
23119
|
+
const w = in_flight[key];
|
|
23057
23120
|
abandoned.push({
|
|
23058
|
-
form_write: Number(
|
|
23059
|
-
topic_name:
|
|
23121
|
+
form_write: Number(w.form_write),
|
|
23122
|
+
topic_name: w.topic_name
|
|
23060
23123
|
});
|
|
23061
|
-
delete in_flight[
|
|
23124
|
+
delete in_flight[key];
|
|
23062
23125
|
});
|
|
23063
23126
|
return abandoned;
|
|
23064
23127
|
}
|
|
@@ -23630,6 +23693,7 @@ var __gclass__$6 = null;
|
|
|
23630
23693
|
***************************************************************/
|
|
23631
23694
|
function mt_create$6(gobj) {
|
|
23632
23695
|
gobj.priv.form_writes = {};
|
|
23696
|
+
gobj.priv.conn_shell = null;
|
|
23633
23697
|
build_ui$6(gobj);
|
|
23634
23698
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
23635
23699
|
if (!subscriber) subscriber = gobj_parent(gobj);
|
|
@@ -23639,12 +23703,22 @@ function mt_create$6(gobj) {
|
|
|
23639
23703
|
* Framework Method: Start
|
|
23640
23704
|
***************************************************************/
|
|
23641
23705
|
function mt_start$6(gobj) {
|
|
23706
|
+
let shell = yui_shell_of(gobj);
|
|
23707
|
+
if (shell) {
|
|
23708
|
+
gobj_subscribe_event(shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
23709
|
+
gobj.priv.conn_shell = shell;
|
|
23710
|
+
}
|
|
23642
23711
|
request_treedb_descs$1(gobj);
|
|
23643
23712
|
}
|
|
23644
23713
|
/***************************************************************
|
|
23645
23714
|
* Framework Method: Stop
|
|
23646
23715
|
***************************************************************/
|
|
23647
23716
|
function mt_stop$6(gobj) {
|
|
23717
|
+
let priv = gobj.priv;
|
|
23718
|
+
if (priv.conn_shell) {
|
|
23719
|
+
if (!gobj_is_destroying(priv.conn_shell)) gobj_unsubscribe_event(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
23720
|
+
priv.conn_shell = null;
|
|
23721
|
+
}
|
|
23648
23722
|
let shell = yui_shell_of(gobj);
|
|
23649
23723
|
if (shell) yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
|
|
23650
23724
|
close_json_viewer$1(gobj);
|
|
@@ -25050,7 +25124,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25050
25124
|
if (command === "update-node" || command === "create-node") {
|
|
25051
25125
|
let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
25052
25126
|
if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
|
|
25053
|
-
settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
25127
|
+
settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
|
|
25054
25128
|
answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), kw_get_int(gobj, kw_command, "form_write", 0, 0), false);
|
|
25055
25129
|
}
|
|
25056
25130
|
}
|
|
@@ -25088,7 +25162,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25088
25162
|
created: command === "create-node",
|
|
25089
25163
|
command
|
|
25090
25164
|
});
|
|
25091
|
-
settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
25165
|
+
settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0), kw_get_str(gobj, kw_command, "topic_name", "", 0));
|
|
25092
25166
|
answer_form_write(gobj, get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)), kw_get_int(gobj, kw_command, "form_write", 0, 0), true);
|
|
25093
25167
|
break;
|
|
25094
25168
|
case "delete-node": break;
|
|
@@ -25232,11 +25306,19 @@ function ac_back_to_topics(gobj, event, kw, src) {
|
|
|
25232
25306
|
return 0;
|
|
25233
25307
|
}
|
|
25234
25308
|
/************************************************************
|
|
25235
|
-
*
|
|
25236
|
-
*
|
|
25237
|
-
*
|
|
25238
|
-
*
|
|
25239
|
-
*
|
|
25309
|
+
* A backend transport edge, from either of two places:
|
|
25310
|
+
* - EV_TRANSPORT_STATE, forwarded by a host that keeps one
|
|
25311
|
+
* transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
|
|
25312
|
+
* C_AGENT_TREEDB_VIEW);
|
|
25313
|
+
* - EV_CONNECTION_STATE, published by the SHELL when the app tells
|
|
25314
|
+
* it its backend went up or down (yui_shell_set_connection_state),
|
|
25315
|
+
* which is what every single-backend app already does for the
|
|
25316
|
+
* toolbar dot. No host has to remember anything for this one.
|
|
25317
|
+
* Both can arrive for one edge; what is done here is idempotent.
|
|
25318
|
+
* The toolbar disables the JSON viewers the moment the session drops
|
|
25319
|
+
* (and re-enables them on reconnect). The library view must not
|
|
25320
|
+
* subscribe to the C_IEVENT_CLI itself: that forwards the subscription
|
|
25321
|
+
* upstream and breaks the session.
|
|
25240
25322
|
************************************************************/
|
|
25241
25323
|
function ac_transport_state$2(gobj, event, kw, src) {
|
|
25242
25324
|
let connected = !!(kw && kw.connected);
|
|
@@ -25768,6 +25850,11 @@ function create_gclass$6(gclass_name) {
|
|
|
25768
25850
|
"EV_TRANSPORT_STATE",
|
|
25769
25851
|
ac_transport_state$2,
|
|
25770
25852
|
null
|
|
25853
|
+
],
|
|
25854
|
+
[
|
|
25855
|
+
"EV_CONNECTION_STATE",
|
|
25856
|
+
ac_transport_state$2,
|
|
25857
|
+
null
|
|
25771
25858
|
]
|
|
25772
25859
|
]]];
|
|
25773
25860
|
const event_types = [
|
|
@@ -25798,6 +25885,7 @@ function create_gclass$6(gclass_name) {
|
|
|
25798
25885
|
["EV_HIDE", 0],
|
|
25799
25886
|
["EV_BACK_TO_TOPICS", 0],
|
|
25800
25887
|
["EV_TRANSPORT_STATE", 0],
|
|
25888
|
+
["EV_CONNECTION_STATE", 0],
|
|
25801
25889
|
["EV_TOPIC_SELECTED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
|
|
25802
25890
|
];
|
|
25803
25891
|
__gclass__$6 = gclass_create(gclass_name, event_types, states, gmt$6, 0, attrs_table$6, PRIVATE_DATA$6, 0, 0, 0, 0);
|
|
@@ -26515,6 +26603,23 @@ function col_goes_back_to_treedb(desc, col) {
|
|
|
26515
26603
|
if (field_desc.is_writable || field_desc.type === "fkey" || field_desc.is_file || col.id === pkey) return true;
|
|
26516
26604
|
return topic_pkey2s(d).includes(col.id);
|
|
26517
26605
|
}
|
|
26606
|
+
/************************************************************
|
|
26607
|
+
* The INSTANCE an update of `record` is for: its pkey2 values, as the
|
|
26608
|
+
* record held them when the form was opened, {pkey2: value}.
|
|
26609
|
+
*
|
|
26610
|
+
* They go back as they WERE, never as the form's widget hands them: a
|
|
26611
|
+
* `time` pkey2 is an epoch rendered in a `datetime-local`, and a value
|
|
26612
|
+
* that crossed the widget (seconds dropped, a local wall time read
|
|
26613
|
+
* back, an hour the autumn change names twice) names ANOTHER instance
|
|
26614
|
+
* -- or none, and C_NODE falls back to the primary. The pkey2 is an
|
|
26615
|
+
* address, not a field of the edit.
|
|
26616
|
+
************************************************************/
|
|
26617
|
+
function instance_keys_of(record, desc) {
|
|
26618
|
+
const keys = {};
|
|
26619
|
+
if (!record || typeof record !== "object") return keys;
|
|
26620
|
+
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];
|
|
26621
|
+
return keys;
|
|
26622
|
+
}
|
|
26518
26623
|
//#endregion
|
|
26519
26624
|
//#region src/delete_impact.js
|
|
26520
26625
|
/***********************************************************************
|
|
@@ -28246,6 +28351,7 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
28246
28351
|
},
|
|
28247
28352
|
[]
|
|
28248
28353
|
]);
|
|
28354
|
+
priv.form_instance = form_mode === "update" ? instance_keys_of(record, desc) : null;
|
|
28249
28355
|
let form_plan = write_plan(gobj);
|
|
28250
28356
|
let form = gobj_create_pure_child("form_" + clean_name(gobj_name(gobj)), "C_YUI_FORM", {
|
|
28251
28357
|
template: build_form_template(gobj),
|
|
@@ -28314,6 +28420,7 @@ function teardown_form_child(gobj) {
|
|
|
28314
28420
|
priv.reading_files = 0;
|
|
28315
28421
|
priv.awaiting_write = 0;
|
|
28316
28422
|
priv.form_busy = { busy: false };
|
|
28423
|
+
priv.form_instance = null;
|
|
28317
28424
|
if (priv.form) {
|
|
28318
28425
|
if (gobj_is_running(priv.form)) gobj_stop(priv.form);
|
|
28319
28426
|
gobj_destroy(priv.form);
|
|
@@ -29573,6 +29680,7 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write) {
|
|
|
29573
29680
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
29574
29681
|
record: strip_read_only_cols(gobj, record)
|
|
29575
29682
|
};
|
|
29683
|
+
if (mode !== "create" && gobj.priv.form_instance) Object.assign(kw_write.record, gobj.priv.form_instance);
|
|
29576
29684
|
if (form_write) kw_write.form_write = form_write;
|
|
29577
29685
|
if (jn_files && Object.keys(jn_files).length > 0) kw_write.record.__files__ = jn_files;
|
|
29578
29686
|
gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", kw_write);
|
|
@@ -70273,16 +70381,39 @@ function register_c_yui_treedb_schema() {
|
|
|
70273
70381
|
* __system__ still differed from the file in use (N13 of the
|
|
70274
70382
|
* 2026-09-22 review). The host reads that difference from
|
|
70275
70383
|
* C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
|
|
70276
|
-
* hands it here; the editor applies it to its model
|
|
70384
|
+
* hands it here; the editor applies it to its model, in place
|
|
70385
|
+
* of what the host said before.
|
|
70386
|
+
*
|
|
70387
|
+
* What `draft_changed` measures depends on the node: the draft
|
|
70388
|
+
* against the SAVED schema on a node newer than yunetas 7.25.3,
|
|
70389
|
+
* and against the file IN USE on a 7.25.3 node -- where a topic saved and
|
|
70390
|
+
* not yet applied is still named, and the editor, which cannot
|
|
70391
|
+
* tell the two apart, shows it as a draft until the Apply.
|
|
70277
70392
|
*
|
|
70278
70393
|
* Copyright (c) 2026, ArtGins.
|
|
70279
70394
|
* All Rights Reserved.
|
|
70280
70395
|
***********************************************************************/
|
|
70281
70396
|
/************************************************************
|
|
70397
|
+
* The topics of `model` the host names as drafts, as
|
|
70398
|
+
* {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
|
|
70399
|
+
* treedb or a topic the model does not hold is ignored.
|
|
70400
|
+
*
|
|
70401
|
+
* What the editor keeps is THIS, apart from what its own session
|
|
70402
|
+
* wrote, and replaced whole on every EV_DRAFTS: the host is the
|
|
70403
|
+
* truth after a reload. Folded into the session's marks, a topic the
|
|
70404
|
+
* host once named stayed a draft after the save that published it.
|
|
70405
|
+
************************************************************/
|
|
70406
|
+
function host_draft_ids(model, drafts) {
|
|
70407
|
+
const ids = {};
|
|
70408
|
+
mark_host_drafts(ids, model, drafts);
|
|
70409
|
+
return ids;
|
|
70410
|
+
}
|
|
70411
|
+
/************************************************************
|
|
70282
70412
|
* Mark in `written` the topics of `model` the host names as
|
|
70283
70413
|
* drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
|
|
70284
70414
|
* topic the model does not hold is ignored. Returns how many were
|
|
70285
|
-
* marked.
|
|
70415
|
+
* marked. It only ADDS: a caller that must forget what the host
|
|
70416
|
+
* no longer names starts from an empty map (host_draft_ids()).
|
|
70286
70417
|
************************************************************/
|
|
70287
70418
|
function mark_host_drafts(written, model, drafts) {
|
|
70288
70419
|
let marked = 0;
|
|
@@ -71921,8 +72052,9 @@ function write_options(op) {
|
|
|
71921
72052
|
* the owner's design of M36 (2026-09-21 review) every write here
|
|
71922
72053
|
* raised both versions, so an edit half made was already the
|
|
71923
72054
|
* schema of the next start. The topic list marks what this session
|
|
71924
|
-
* wrote and has not saved
|
|
71925
|
-
* what forgets it
|
|
72055
|
+
* wrote and has not saved (the host's EV_REFRESH after a save is
|
|
72056
|
+
* what forgets it) and what the host says is not saved (EV_DRAFTS,
|
|
72057
|
+
* replaced whole each time it is sent).
|
|
71926
72058
|
*
|
|
71927
72059
|
* WHAT ELSE IS HERE, and why each is here and not somewhere else:
|
|
71928
72060
|
*
|
|
@@ -72001,6 +72133,8 @@ var PRIVATE_DATA$1 = {
|
|
|
72001
72133
|
diagram: false,
|
|
72002
72134
|
baseline: null,
|
|
72003
72135
|
written: null,
|
|
72136
|
+
host_drafts: null,
|
|
72137
|
+
host_ids: null,
|
|
72004
72138
|
order_undo: null,
|
|
72005
72139
|
diagram_gobj: null,
|
|
72006
72140
|
dialog: null,
|
|
@@ -72025,6 +72159,7 @@ function mt_create$1(gobj) {
|
|
|
72025
72159
|
priv.baseline = {};
|
|
72026
72160
|
priv.written = {};
|
|
72027
72161
|
priv.host_drafts = {};
|
|
72162
|
+
priv.host_ids = {};
|
|
72028
72163
|
priv.save_queue = [];
|
|
72029
72164
|
build_ui$1(gobj);
|
|
72030
72165
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
@@ -72264,25 +72399,33 @@ function start_measuring(gobj) {
|
|
|
72264
72399
|
let priv = gobj.priv;
|
|
72265
72400
|
priv.baseline = {};
|
|
72266
72401
|
priv.written = {};
|
|
72402
|
+
priv.host_ids = {};
|
|
72267
72403
|
if (!priv.model) return;
|
|
72268
72404
|
for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
|
|
72269
|
-
|
|
72405
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
72270
72406
|
}
|
|
72271
72407
|
/***************************************************************
|
|
72272
|
-
* "This topic
|
|
72408
|
+
* "This topic holds an edit that is not saved."
|
|
72273
72409
|
*
|
|
72274
72410
|
* An edit is a DRAFT: it moves no version and reaches no treedb
|
|
72275
72411
|
* until the host saves it (C_TREEDB's save-schema, which raises the
|
|
72276
|
-
* versions of what changed)
|
|
72277
|
-
*
|
|
72412
|
+
* versions of what changed). Two sources say a topic is one, and
|
|
72413
|
+
* they are kept APART because they are forgotten differently:
|
|
72414
|
+
* - `written`: what THIS session wrote since the model was loaded
|
|
72415
|
+
* (a reload, the host's EV_REFRESH, forgets it -- the store says
|
|
72416
|
+
* what is left);
|
|
72417
|
+
* - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
|
|
72418
|
+
* by the next EV_DRAFTS. Folded into `written`, a topic the host
|
|
72419
|
+
* named once stayed a draft after the Save that published it.
|
|
72278
72420
|
*
|
|
72279
72421
|
* ONE predicate, because it is asked in three places -- the topic
|
|
72280
72422
|
* list draws a chip with it, the column view a banner, the export
|
|
72281
72423
|
* a warning -- and copies of a rule are how they drift.
|
|
72282
72424
|
***************************************************************/
|
|
72283
|
-
function topic_is_draft(
|
|
72284
|
-
if (!topic || !
|
|
72285
|
-
|
|
72425
|
+
function topic_is_draft(priv, topic) {
|
|
72426
|
+
if (!topic || !priv) return false;
|
|
72427
|
+
if (priv.written && priv.written[topic.id]) return true;
|
|
72428
|
+
return priv.host_ids && priv.host_ids[topic.id] ? true : false;
|
|
72286
72429
|
}
|
|
72287
72430
|
/***************************************************************
|
|
72288
72431
|
* Say what the whole store would be refused for.
|
|
@@ -72724,7 +72867,7 @@ function render_topics(gobj, $body) {
|
|
|
72724
72867
|
let twice = repeated_names(treedb.topics);
|
|
72725
72868
|
let $rows = [];
|
|
72726
72869
|
for (let topic of treedb.topics) {
|
|
72727
|
-
let pending = topic_is_draft(priv
|
|
72870
|
+
let pending = topic_is_draft(priv, topic);
|
|
72728
72871
|
let ambiguous = !!twice[topic.name];
|
|
72729
72872
|
let address = ambiguous ? topic.id : topic.name;
|
|
72730
72873
|
let $actions = [];
|
|
@@ -73047,7 +73190,7 @@ function render_columns(gobj, $body) {
|
|
|
73047
73190
|
***************************************************************/
|
|
73048
73191
|
function draft_banner(gobj, topic) {
|
|
73049
73192
|
let priv = gobj.priv;
|
|
73050
|
-
if (!topic_is_draft(priv
|
|
73193
|
+
if (!topic_is_draft(priv, topic)) return null;
|
|
73051
73194
|
if (is_readonly(gobj)) return null;
|
|
73052
73195
|
return createElement2([
|
|
73053
73196
|
"div",
|
|
@@ -73937,7 +74080,7 @@ function open_report(gobj, findings, treedb_id) {
|
|
|
73937
74080
|
***************************************************************/
|
|
73938
74081
|
function open_export(gobj, treedb) {
|
|
73939
74082
|
let priv = gobj.priv;
|
|
73940
|
-
let drafts = treedb.topics.some((topic) => topic_is_draft(priv
|
|
74083
|
+
let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
|
|
73941
74084
|
let c_text = schema_to_c(treedb);
|
|
73942
74085
|
let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
|
|
73943
74086
|
let $content = createElement2([
|
|
@@ -74705,18 +74848,22 @@ function ac_refresh$1(gobj, event, kw, src) {
|
|
|
74705
74848
|
}
|
|
74706
74849
|
/***************************************************************
|
|
74707
74850
|
* {drafts: {treedb_name: [topic names]}} -- the host says which
|
|
74708
|
-
* topics hold a draft in __system__ that
|
|
74709
|
-
*
|
|
74710
|
-
*
|
|
74711
|
-
*
|
|
74712
|
-
*
|
|
74713
|
-
*
|
|
74714
|
-
*
|
|
74851
|
+
* topics hold a draft in __system__ that is not saved (C_TREEDB's
|
|
74852
|
+
* saved-schema, `draft_changed`). The mark of a write lived in this
|
|
74853
|
+
* session's memory only, so a reload of the page, a reconnect or a
|
|
74854
|
+
* refresh showed no draft while __system__ still differed (N13 of
|
|
74855
|
+
* the 2026-09-22 review).
|
|
74856
|
+
*
|
|
74857
|
+
* REPLACES what the host said before -- the host is the truth after
|
|
74858
|
+
* a reload, and a Save is exactly the host saying "none any more".
|
|
74859
|
+
* It only ever added, and a topic stayed a draft after the Save that
|
|
74860
|
+
* published it (M1 of the 2026-09-23 review). What this session wrote
|
|
74861
|
+
* stays marked: it is kept apart (see topic_is_draft()).
|
|
74715
74862
|
***************************************************************/
|
|
74716
74863
|
function ac_drafts(gobj, event, kw, src) {
|
|
74717
74864
|
let priv = gobj.priv;
|
|
74718
74865
|
priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
|
|
74719
|
-
|
|
74866
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
74720
74867
|
if (priv.model) render(gobj);
|
|
74721
74868
|
return 0;
|
|
74722
74869
|
}
|
package/package.json
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* the owner's design of M36 (2026-09-21 review) every write here
|
|
27
27
|
* raised both versions, so an edit half made was already the
|
|
28
28
|
* schema of the next start. The topic list marks what this session
|
|
29
|
-
* wrote and has not saved
|
|
30
|
-
* what forgets it
|
|
29
|
+
* wrote and has not saved (the host's EV_REFRESH after a save is
|
|
30
|
+
* what forgets it) and what the host says is not saved (EV_DRAFTS,
|
|
31
|
+
* replaced whole each time it is sent).
|
|
31
32
|
*
|
|
32
33
|
* WHAT ELSE IS HERE, and why each is here and not somewhere else:
|
|
33
34
|
*
|
|
@@ -113,7 +114,7 @@ import {
|
|
|
113
114
|
empty_string,
|
|
114
115
|
} from "@yuneta/gobj-js";
|
|
115
116
|
import {yui_tint} from "./bulma_tint.js";
|
|
116
|
-
import {
|
|
117
|
+
import {host_draft_ids} from "./host_drafts.js";
|
|
117
118
|
|
|
118
119
|
import "./c_yui_schema_editor.css";
|
|
119
120
|
|
|
@@ -207,6 +208,8 @@ let PRIVATE_DATA = {
|
|
|
207
208
|
* version still has to move. */
|
|
208
209
|
baseline: null, /* {topic id: topic_version} at load time */
|
|
209
210
|
written: null, /* {topic id: true} */
|
|
211
|
+
host_drafts: null, /* {treedb: [topic names]}, the host's last EV_DRAFTS */
|
|
212
|
+
host_ids: null, /* host_drafts resolved on the model, {topic id: true} */
|
|
210
213
|
order_undo: null, /* {topic id: [{id, order}]} before the first drag */
|
|
211
214
|
|
|
212
215
|
/* Children and overlays */
|
|
@@ -250,6 +253,7 @@ function mt_create(gobj)
|
|
|
250
253
|
priv.baseline = {};
|
|
251
254
|
priv.written = {};
|
|
252
255
|
priv.host_drafts = {}; /* what the host says is a draft, {treedb: [topics]} (EV_DRAFTS) */
|
|
256
|
+
priv.host_ids = {}; /* the same, as {topic id: true} of the model */
|
|
253
257
|
priv.save_queue = [];
|
|
254
258
|
|
|
255
259
|
build_ui(gobj);
|
|
@@ -564,6 +568,7 @@ function start_measuring(gobj)
|
|
|
564
568
|
|
|
565
569
|
priv.baseline = {};
|
|
566
570
|
priv.written = {};
|
|
571
|
+
priv.host_ids = {};
|
|
567
572
|
|
|
568
573
|
if(!priv.model) {
|
|
569
574
|
return;
|
|
@@ -573,29 +578,39 @@ function start_measuring(gobj)
|
|
|
573
578
|
priv.baseline[topic.id] = topic.topic_version;
|
|
574
579
|
}
|
|
575
580
|
}
|
|
576
|
-
/* A draft is a draft after a reload too: what the host said
|
|
577
|
-
*
|
|
578
|
-
|
|
581
|
+
/* A draft is a draft after a reload too: what the host said last
|
|
582
|
+
* is resolved against the new model (host_drafts.js), apart from
|
|
583
|
+
* what this session writes, until the host says otherwise. */
|
|
584
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
579
585
|
}
|
|
580
586
|
|
|
581
587
|
/***************************************************************
|
|
582
|
-
* "This topic
|
|
588
|
+
* "This topic holds an edit that is not saved."
|
|
583
589
|
*
|
|
584
590
|
* An edit is a DRAFT: it moves no version and reaches no treedb
|
|
585
591
|
* until the host saves it (C_TREEDB's save-schema, which raises the
|
|
586
|
-
* versions of what changed)
|
|
587
|
-
*
|
|
592
|
+
* versions of what changed). Two sources say a topic is one, and
|
|
593
|
+
* they are kept APART because they are forgotten differently:
|
|
594
|
+
* - `written`: what THIS session wrote since the model was loaded
|
|
595
|
+
* (a reload, the host's EV_REFRESH, forgets it -- the store says
|
|
596
|
+
* what is left);
|
|
597
|
+
* - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
|
|
598
|
+
* by the next EV_DRAFTS. Folded into `written`, a topic the host
|
|
599
|
+
* named once stayed a draft after the Save that published it.
|
|
588
600
|
*
|
|
589
601
|
* ONE predicate, because it is asked in three places -- the topic
|
|
590
602
|
* list draws a chip with it, the column view a banner, the export
|
|
591
603
|
* a warning -- and copies of a rule are how they drift.
|
|
592
604
|
***************************************************************/
|
|
593
|
-
function topic_is_draft(
|
|
605
|
+
function topic_is_draft(priv, topic)
|
|
594
606
|
{
|
|
595
|
-
if(!topic || !
|
|
607
|
+
if(!topic || !priv) {
|
|
596
608
|
return false;
|
|
597
609
|
}
|
|
598
|
-
|
|
610
|
+
if(priv.written && priv.written[topic.id]) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return (priv.host_ids && priv.host_ids[topic.id])? true: false;
|
|
599
614
|
}
|
|
600
615
|
|
|
601
616
|
/***************************************************************
|
|
@@ -1050,7 +1065,7 @@ function render_topics(gobj, $body)
|
|
|
1050
1065
|
let twice = repeated_names(treedb.topics);
|
|
1051
1066
|
let $rows = [];
|
|
1052
1067
|
for(let topic of treedb.topics) {
|
|
1053
|
-
let pending = topic_is_draft(priv
|
|
1068
|
+
let pending = topic_is_draft(priv, topic);
|
|
1054
1069
|
/* Two topics of one name: the id is what tells them apart, and
|
|
1055
1070
|
* it is what the url has to carry — addressing by name would
|
|
1056
1071
|
* open the first one whichever row was clicked. */
|
|
@@ -1256,7 +1271,7 @@ function draft_banner(gobj, topic)
|
|
|
1256
1271
|
{
|
|
1257
1272
|
let priv = gobj.priv;
|
|
1258
1273
|
|
|
1259
|
-
if(!topic_is_draft(priv
|
|
1274
|
+
if(!topic_is_draft(priv, topic)) {
|
|
1260
1275
|
return null;
|
|
1261
1276
|
}
|
|
1262
1277
|
if(is_readonly(gobj)) {
|
|
@@ -1997,7 +2012,7 @@ function open_report(gobj, findings, treedb_id)
|
|
|
1997
2012
|
function open_export(gobj, treedb)
|
|
1998
2013
|
{
|
|
1999
2014
|
let priv = gobj.priv;
|
|
2000
|
-
let drafts = treedb.topics.some((topic) => topic_is_draft(priv
|
|
2015
|
+
let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
|
|
2001
2016
|
let c_text = schema_to_c(treedb);
|
|
2002
2017
|
let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
|
|
2003
2018
|
|
|
@@ -2762,20 +2777,24 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
2762
2777
|
|
|
2763
2778
|
/***************************************************************
|
|
2764
2779
|
* {drafts: {treedb_name: [topic names]}} -- the host says which
|
|
2765
|
-
* topics hold a draft in __system__ that
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2780
|
+
* topics hold a draft in __system__ that is not saved (C_TREEDB's
|
|
2781
|
+
* saved-schema, `draft_changed`). The mark of a write lived in this
|
|
2782
|
+
* session's memory only, so a reload of the page, a reconnect or a
|
|
2783
|
+
* refresh showed no draft while __system__ still differed (N13 of
|
|
2784
|
+
* the 2026-09-22 review).
|
|
2785
|
+
*
|
|
2786
|
+
* REPLACES what the host said before -- the host is the truth after
|
|
2787
|
+
* a reload, and a Save is exactly the host saying "none any more".
|
|
2788
|
+
* It only ever added, and a topic stayed a draft after the Save that
|
|
2789
|
+
* published it (M1 of the 2026-09-23 review). What this session wrote
|
|
2790
|
+
* stays marked: it is kept apart (see topic_is_draft()).
|
|
2772
2791
|
***************************************************************/
|
|
2773
2792
|
function ac_drafts(gobj, event, kw, src)
|
|
2774
2793
|
{
|
|
2775
2794
|
let priv = gobj.priv;
|
|
2776
2795
|
|
|
2777
2796
|
priv.host_drafts = (kw && kw.drafts && typeof kw.drafts === "object")? kw.drafts: {};
|
|
2778
|
-
|
|
2797
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
2779
2798
|
if(priv.model) {
|
|
2780
2799
|
render(gobj);
|
|
2781
2800
|
}
|