@yuneta/gobj-ui 7.23.191 → 7.23.193
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 +24 -0
- package/dist/gobj-ui.cjs.js +238 -62
- package/dist/gobj-ui.es.js +238 -62
- package/package.json +3 -3
- package/src/c_g6_nodes_tree.js +15 -2
- package/src/c_yui_treedb_topic_with_form.js +143 -49
- package/src/c_yui_treedb_topics.js +62 -15
- package/src/graph_save_plan.js +40 -0
- package/src/graph_save_plan.test.js +57 -1
- package/src/yui_row_search.js +7 -2
- package/src/yui_row_search.test.js +22 -0
- package/src/yui_table_select.js +50 -0
- package/src/yui_table_select.test.js +54 -0
|
@@ -85,6 +85,8 @@ import {
|
|
|
85
85
|
yui_selection_settings,
|
|
86
86
|
yui_selection_bar,
|
|
87
87
|
yui_selected_rows,
|
|
88
|
+
yui_row_ids,
|
|
89
|
+
yui_rows_by_ids,
|
|
88
90
|
yui_clear_selection,
|
|
89
91
|
} from "./yui_table_select.js";
|
|
90
92
|
|
|
@@ -195,6 +197,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_new_button", 0, true, "Button tool
|
|
|
195
197
|
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
|
|
196
198
|
SDATA(data_type_t.DTP_BOOLEAN, "with_copy_button", 0, true, "Button toolbar COPY"),
|
|
197
199
|
SDATA(data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button toolbar PASTE"),
|
|
200
|
+
SDATA(data_type_t.DTP_BOOLEAN, "form_waits_for_answer", 0, false, "The form stays open (busy) after a Save until the host answers EV_WRITE_DONE (close) or EV_WRITE_REFUSED (stay open on what was typed). Off: the form closes when the write is published, whatever the backend says. The host must echo the `form_write` serial of EV_CREATE_RECORD / EV_UPDATE_RECORD; C_YUI_TREEDB_TOPICS does"),
|
|
198
201
|
SDATA(data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
|
|
199
202
|
SDATA(data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
|
|
200
203
|
SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
|
|
@@ -424,6 +427,10 @@ function mt_create(gobj)
|
|
|
424
427
|
* reopened cannot land on the new form. */
|
|
425
428
|
gobj.priv.reading_files = 0;
|
|
426
429
|
gobj.priv.read_serial = 0;
|
|
430
|
+
/* 0 = no write of the form waiting for its answer; otherwise the
|
|
431
|
+
* serial the host echoes in EV_WRITE_DONE / EV_WRITE_REFUSED. */
|
|
432
|
+
gobj.priv.awaiting_write = 0;
|
|
433
|
+
gobj.priv.write_serial = 0;
|
|
427
434
|
|
|
428
435
|
let name = clean_name(gobj_name(gobj));
|
|
429
436
|
gobj_write_attr(gobj, "table_id", "table" + name);
|
|
@@ -1013,14 +1020,18 @@ function render_selection_state(gobj)
|
|
|
1013
1020
|
let $delete = $container ? $container.querySelector(`.button-delete-record`) : null;
|
|
1014
1021
|
let $copy = $container ? $container.querySelector(`.button-copy-record`) : null;
|
|
1015
1022
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1023
|
+
/* Each on its own: with_delete_button / with_copy_button = false build
|
|
1024
|
+
* no button, and asking for BOTH left the other one never enabled. */
|
|
1025
|
+
let some = yui_selected_rows(gobj_read_attr(gobj, "tabulator")).length > 0;
|
|
1026
|
+
let enable = some && gobj_read_bool_attr(gobj, "editable");
|
|
1027
|
+
for(let $button of [$delete, $copy]) {
|
|
1028
|
+
if(!$button) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if(enable) {
|
|
1032
|
+
$button.removeAttribute("disabled");
|
|
1021
1033
|
} else {
|
|
1022
|
-
$
|
|
1023
|
-
$copy.setAttribute("disabled", true);
|
|
1034
|
+
$button.setAttribute("disabled", true);
|
|
1024
1035
|
}
|
|
1025
1036
|
}
|
|
1026
1037
|
|
|
@@ -1481,9 +1492,13 @@ function create_tabulator(gobj)
|
|
|
1481
1492
|
* came out wider than the viewport and pushed every other column,
|
|
1482
1493
|
* the id included, off the screen. The cell keeps its whole text;
|
|
1483
1494
|
* what the ceiling costs is having to widen the column, or open
|
|
1484
|
-
* the record, to read the end of it.
|
|
1495
|
+
* the record, to read the end of it.
|
|
1496
|
+
* `maxInitialWidth`, not `maxWidth`: the latter is a HARD ceiling
|
|
1497
|
+
* that neither the drag handle nor the double click could pass
|
|
1498
|
+
* (M29 of the 2026-09-21 review); this one caps only the width the
|
|
1499
|
+
* layout gives, and leaves the column to the reader. */
|
|
1485
1500
|
if(max_col_width > 0) {
|
|
1486
|
-
colDef.
|
|
1501
|
+
colDef.maxInitialWidth = max_col_width;
|
|
1487
1502
|
}
|
|
1488
1503
|
if(with_header_filters) {
|
|
1489
1504
|
apply_header_filter(colDef, field_desc);
|
|
@@ -1557,7 +1572,7 @@ function create_tabulator(gobj)
|
|
|
1557
1572
|
gobj_send_event(gobj, "EV_EDIT_RECORD", {index: index}, gobj);
|
|
1558
1573
|
} else if(e.target.closest('.remove')) {
|
|
1559
1574
|
e.stopPropagation();
|
|
1560
|
-
gobj_send_event(gobj, "EV_DELETE_ROWS", {
|
|
1575
|
+
gobj_send_event(gobj, "EV_DELETE_ROWS", {row: row}, gobj);
|
|
1561
1576
|
}
|
|
1562
1577
|
}
|
|
1563
1578
|
});
|
|
@@ -2485,6 +2500,7 @@ function teardown_form_child(gobj)
|
|
|
2485
2500
|
{
|
|
2486
2501
|
let priv = gobj.priv;
|
|
2487
2502
|
priv.reading_files = 0; // a read in flight lands on nobody
|
|
2503
|
+
priv.awaiting_write = 0; // and so does the answer of a write
|
|
2488
2504
|
if(priv.form) {
|
|
2489
2505
|
if(gobj_is_running(priv.form)) {
|
|
2490
2506
|
gobj_stop(priv.form);
|
|
@@ -3882,14 +3898,21 @@ function ac_edition_mode(gobj, event, kw, src)
|
|
|
3882
3898
|
/*
|
|
3883
3899
|
* Set edition mode
|
|
3884
3900
|
*/
|
|
3885
|
-
|
|
3886
|
-
$
|
|
3887
|
-
|
|
3901
|
+
/* with_new_button / with_delete_button = false build no button */
|
|
3902
|
+
if($button_edit_record) {
|
|
3903
|
+
$button_edit_record.classList.add('is-primary');
|
|
3904
|
+
}
|
|
3905
|
+
if($button_new_record) {
|
|
3906
|
+
$button_new_record.classList.add('is-info');
|
|
3907
|
+
$button_new_record.removeAttribute("disabled");
|
|
3908
|
+
}
|
|
3909
|
+
if($button_delete_record) {
|
|
3910
|
+
$button_delete_record.classList.add('is-danger');
|
|
3911
|
+
}
|
|
3888
3912
|
|
|
3889
3913
|
tabulator.showColumn('_operation');
|
|
3890
3914
|
tabulator.showColumn('_check_box_state_');
|
|
3891
3915
|
|
|
3892
|
-
$button_new_record.removeAttribute("disabled");
|
|
3893
3916
|
/* with_copy_button / with_paste_button = false build no button */
|
|
3894
3917
|
if($button_paste_record) {
|
|
3895
3918
|
$button_paste_record.removeAttribute("disabled");
|
|
@@ -3897,7 +3920,9 @@ function ac_edition_mode(gobj, event, kw, src)
|
|
|
3897
3920
|
|
|
3898
3921
|
let rows = yui_selected_rows(tabulator);
|
|
3899
3922
|
if (rows.length) {
|
|
3900
|
-
$button_delete_record
|
|
3923
|
+
if($button_delete_record) {
|
|
3924
|
+
$button_delete_record.removeAttribute("disabled");
|
|
3925
|
+
}
|
|
3901
3926
|
if($button_copy_record) {
|
|
3902
3927
|
$button_copy_record.removeAttribute("disabled");
|
|
3903
3928
|
}
|
|
@@ -3907,15 +3932,21 @@ function ac_edition_mode(gobj, event, kw, src)
|
|
|
3907
3932
|
/*
|
|
3908
3933
|
* Remove edition mode
|
|
3909
3934
|
*/
|
|
3910
|
-
$button_edit_record
|
|
3911
|
-
|
|
3912
|
-
|
|
3935
|
+
if($button_edit_record) {
|
|
3936
|
+
$button_edit_record.classList.remove('is-primary');
|
|
3937
|
+
}
|
|
3938
|
+
if($button_new_record) {
|
|
3939
|
+
$button_new_record.classList.remove('is-info');
|
|
3940
|
+
$button_new_record.setAttribute("disabled", true);
|
|
3941
|
+
}
|
|
3942
|
+
if($button_delete_record) {
|
|
3943
|
+
$button_delete_record.classList.remove('is-danger');
|
|
3944
|
+
$button_delete_record.setAttribute("disabled", true);
|
|
3945
|
+
}
|
|
3913
3946
|
|
|
3914
3947
|
tabulator.hideColumn('_operation');
|
|
3915
3948
|
tabulator.hideColumn('_check_box_state_');
|
|
3916
3949
|
|
|
3917
|
-
$button_new_record.setAttribute("disabled", true);
|
|
3918
|
-
$button_delete_record.setAttribute("disabled", true);
|
|
3919
3950
|
if($button_copy_record) {
|
|
3920
3951
|
$button_copy_record.setAttribute("disabled", true);
|
|
3921
3952
|
}
|
|
@@ -3982,7 +4013,12 @@ function ac_new_row(gobj, event, kw, src)
|
|
|
3982
4013
|
* kw {} empty
|
|
3983
4014
|
* - From the column _operation delete icon inside a row
|
|
3984
4015
|
* It will delete this one row
|
|
3985
|
-
* kw {
|
|
4016
|
+
* kw {row:}
|
|
4017
|
+
*
|
|
4018
|
+
* What crosses the confirmation is the rows' IDENTITY, never their
|
|
4019
|
+
* position: the view applies EV_TREEDB_NODE_* of every writer while the
|
|
4020
|
+
* dialog is open, and a position names whatever row sits there when the
|
|
4021
|
+
* person answers -- with force:true (A6 of the 2026-09-21 review).
|
|
3986
4022
|
************************************************************/
|
|
3987
4023
|
function ac_delete_rows(gobj, event, kw, src)
|
|
3988
4024
|
{
|
|
@@ -4005,15 +4041,15 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
4005
4041
|
}
|
|
4006
4042
|
|
|
4007
4043
|
confirm_then(gobj, build_delete_question(gobj, rows),
|
|
4008
|
-
{what: "delete_selection"});
|
|
4044
|
+
{what: "delete_selection", ids: yui_row_ids(tabulator, rows)});
|
|
4009
4045
|
|
|
4010
4046
|
} else {
|
|
4011
4047
|
/*----------------------------*
|
|
4012
4048
|
* Delete one row
|
|
4013
|
-
* {
|
|
4049
|
+
* {row: }
|
|
4014
4050
|
*----------------------------*/
|
|
4015
4051
|
confirm_then(gobj, build_delete_question(gobj, kw.row),
|
|
4016
|
-
{what: "delete_row",
|
|
4052
|
+
{what: "delete_row", ids: yui_row_ids(tabulator, [kw.row])});
|
|
4017
4053
|
}
|
|
4018
4054
|
|
|
4019
4055
|
return 0;
|
|
@@ -4021,8 +4057,8 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
4021
4057
|
|
|
4022
4058
|
/************************************************************
|
|
4023
4059
|
* {what, ...} -- the person said yes to what confirm_then()
|
|
4024
|
-
* asked. The rows are
|
|
4025
|
-
*
|
|
4060
|
+
* asked. The rows are found again HERE by the ids the question
|
|
4061
|
+
* named: the kw is plain json, and a Tabulator row is not.
|
|
4026
4062
|
************************************************************/
|
|
4027
4063
|
function ac_confirmed(gobj, event, kw, src)
|
|
4028
4064
|
{
|
|
@@ -4042,13 +4078,19 @@ function ac_confirmed(gobj, event, kw, src)
|
|
|
4042
4078
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
4043
4079
|
let topic_name = gobj_read_str_attr(gobj, "topic_name");
|
|
4044
4080
|
|
|
4045
|
-
if(what === "delete_selection") {
|
|
4046
|
-
let
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4081
|
+
if(what === "delete_selection" || what === "delete_row") {
|
|
4082
|
+
let ids = Array.isArray(kw.ids) ? kw.ids : [];
|
|
4083
|
+
let found = yui_rows_by_ids(tabulator, ids);
|
|
4084
|
+
if(found.missing.length) {
|
|
4085
|
+
/* Gone while the question was open (another writer deleted
|
|
4086
|
+
* them): never replaced by the row that took their place. */
|
|
4087
|
+
log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while ` +
|
|
4088
|
+
`confirming the delete, not deleted: ${found.missing.join(", ")}`);
|
|
4050
4089
|
}
|
|
4051
|
-
|
|
4090
|
+
if(!found.rows.length) {
|
|
4091
|
+
return -1; /* Error already logged, or nothing was named */
|
|
4092
|
+
}
|
|
4093
|
+
for(let row of found.rows) {
|
|
4052
4094
|
// TODO why don't send once EV_DELETE_RECORD(S)
|
|
4053
4095
|
gobj_publish_event(
|
|
4054
4096
|
gobj,
|
|
@@ -4059,22 +4101,6 @@ function ac_confirmed(gobj, event, kw, src)
|
|
|
4059
4101
|
return 0;
|
|
4060
4102
|
}
|
|
4061
4103
|
|
|
4062
|
-
if(what === "delete_row") {
|
|
4063
|
-
let index = kw.index;
|
|
4064
|
-
let row = (tabulator && typeof index === "number")?
|
|
4065
|
-
tabulator.getRowFromPosition(index) : null;
|
|
4066
|
-
if(!row) {
|
|
4067
|
-
log_error(`${gobj_short_name(gobj)}: ${event} names no row: ${index}`);
|
|
4068
|
-
return -1;
|
|
4069
|
-
}
|
|
4070
|
-
gobj_publish_event(
|
|
4071
|
-
gobj,
|
|
4072
|
-
"EV_DELETE_RECORD",
|
|
4073
|
-
{topic_name: topic_name, record: row.getData()}
|
|
4074
|
-
);
|
|
4075
|
-
return 0;
|
|
4076
|
-
}
|
|
4077
|
-
|
|
4078
4104
|
log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${what}`);
|
|
4079
4105
|
return -1;
|
|
4080
4106
|
}
|
|
@@ -4178,6 +4204,10 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
4178
4204
|
const cols = Object.keys(picked);
|
|
4179
4205
|
|
|
4180
4206
|
if(cols.length === 0) {
|
|
4207
|
+
if(wait_for_write_answer(gobj)) {
|
|
4208
|
+
publish_treedb_write(gobj, mode, kw, null, priv.awaiting_write);
|
|
4209
|
+
return 0;
|
|
4210
|
+
}
|
|
4181
4211
|
publish_treedb_write(gobj, mode, kw);
|
|
4182
4212
|
|
|
4183
4213
|
/* We are INSIDE the form child's gobj_publish_event stack, and
|
|
@@ -4275,7 +4305,7 @@ function read_is_current(gobj, kw)
|
|
|
4275
4305
|
/************************************************************
|
|
4276
4306
|
* The one place a write of this view leaves for the app.
|
|
4277
4307
|
************************************************************/
|
|
4278
|
-
function publish_treedb_write(gobj, mode, record, jn_files)
|
|
4308
|
+
function publish_treedb_write(gobj, mode, record, jn_files, form_write)
|
|
4279
4309
|
{
|
|
4280
4310
|
/* Both writes of the form pass here, with and without files: the
|
|
4281
4311
|
* read-only columns it shows stay on screen and off the wire. */
|
|
@@ -4283,6 +4313,10 @@ function publish_treedb_write(gobj, mode, record, jn_files)
|
|
|
4283
4313
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
4284
4314
|
record: strip_read_only_cols(gobj, record)
|
|
4285
4315
|
};
|
|
4316
|
+
if(form_write) {
|
|
4317
|
+
/* The serial the host echoes back with the answer */
|
|
4318
|
+
kw_write.form_write = form_write;
|
|
4319
|
+
}
|
|
4286
4320
|
if(jn_files && Object.keys(jn_files).length > 0) {
|
|
4287
4321
|
/* Not a column: an instruction to the treedb write path,
|
|
4288
4322
|
* consumed and dropped at the door. */
|
|
@@ -4315,11 +4349,67 @@ function ac_files_read(gobj, event, kw, src)
|
|
|
4315
4349
|
}
|
|
4316
4350
|
gobj.priv.reading_files = 0;
|
|
4317
4351
|
const {record, __files__} = yui_files_manifest(kw.picks, kw.record);
|
|
4352
|
+
if(wait_for_write_answer(gobj)) {
|
|
4353
|
+
publish_treedb_write(gobj, kw.mode, record, __files__, gobj.priv.awaiting_write);
|
|
4354
|
+
return 0;
|
|
4355
|
+
}
|
|
4318
4356
|
publish_treedb_write(gobj, kw.mode, record, __files__);
|
|
4319
4357
|
close_form_dialog(gobj);
|
|
4320
4358
|
return 0;
|
|
4321
4359
|
}
|
|
4322
4360
|
|
|
4361
|
+
/************************************************************
|
|
4362
|
+
* Does the form wait for the answer of its write? Then it stays
|
|
4363
|
+
* open and BUSY, and the serial it waits for is taken here.
|
|
4364
|
+
*
|
|
4365
|
+
* A refused write used to close the form all the same and throw
|
|
4366
|
+
* away what was typed, although the README said it stayed open
|
|
4367
|
+
* (M25 of the 2026-09-21 review): the close was posted with the
|
|
4368
|
+
* publish, before the backend had answered anything.
|
|
4369
|
+
************************************************************/
|
|
4370
|
+
function wait_for_write_answer(gobj)
|
|
4371
|
+
{
|
|
4372
|
+
let priv = gobj.priv;
|
|
4373
|
+
if(!gobj_read_bool_attr(gobj, "form_waits_for_answer") || !priv.form) {
|
|
4374
|
+
return false;
|
|
4375
|
+
}
|
|
4376
|
+
priv.awaiting_write = ++priv.write_serial;
|
|
4377
|
+
set_form_busy(gobj, true);
|
|
4378
|
+
return true;
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
/************************************************************
|
|
4382
|
+
* {form_write} -- the host says the form's write was done
|
|
4383
|
+
************************************************************/
|
|
4384
|
+
function ac_write_done(gobj, event, kw, src)
|
|
4385
|
+
{
|
|
4386
|
+
let priv = gobj.priv;
|
|
4387
|
+
if(!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) {
|
|
4388
|
+
/* Not the write this form waits for: its form was closed, or
|
|
4389
|
+
* replaced by another one, meanwhile. Nothing to act on. */
|
|
4390
|
+
return 0;
|
|
4391
|
+
}
|
|
4392
|
+
priv.awaiting_write = 0;
|
|
4393
|
+
set_form_busy(gobj, false);
|
|
4394
|
+
close_form_dialog(gobj);
|
|
4395
|
+
return 0;
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
/************************************************************
|
|
4399
|
+
* {form_write} -- the host says the form's write was refused.
|
|
4400
|
+
* The form stays OPEN on what was typed; the host has shown why.
|
|
4401
|
+
************************************************************/
|
|
4402
|
+
function ac_write_refused(gobj, event, kw, src)
|
|
4403
|
+
{
|
|
4404
|
+
let priv = gobj.priv;
|
|
4405
|
+
if(!priv.awaiting_write || !kw || kw.form_write !== priv.awaiting_write) {
|
|
4406
|
+
return 0; /* Not the write this form waits for (see ac_write_done) */
|
|
4407
|
+
}
|
|
4408
|
+
priv.awaiting_write = 0;
|
|
4409
|
+
set_form_busy(gobj, false);
|
|
4410
|
+
return 0;
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4323
4413
|
/************************************************************
|
|
4324
4414
|
* {reason} -- the browser could not read what was picked
|
|
4325
4415
|
*
|
|
@@ -5218,6 +5308,8 @@ function create_gclass(gclass_name)
|
|
|
5218
5308
|
["EV_EDITION_MODE", ac_edition_mode, null],
|
|
5219
5309
|
["EV_SAVE_RECORD", ac_form_save_record, null],
|
|
5220
5310
|
["EV_CLOSE_FORM", ac_close_form, null],
|
|
5311
|
+
["EV_WRITE_DONE", ac_write_done, null],
|
|
5312
|
+
["EV_WRITE_REFUSED", ac_write_refused, null],
|
|
5221
5313
|
["EV_FILES_READ", ac_files_read, null],
|
|
5222
5314
|
["EV_FILES_FAILED", ac_files_failed, null],
|
|
5223
5315
|
|
|
@@ -5272,6 +5364,8 @@ function create_gclass(gclass_name)
|
|
|
5272
5364
|
["EV_EDITION_MODE", 0],
|
|
5273
5365
|
["EV_SAVE_RECORD", 0],
|
|
5274
5366
|
["EV_CLOSE_FORM", 0],
|
|
5367
|
+
["EV_WRITE_DONE", 0],
|
|
5368
|
+
["EV_WRITE_REFUSED", 0],
|
|
5275
5369
|
["EV_FILES_READ", 0],
|
|
5276
5370
|
["EV_FILES_FAILED", 0],
|
|
5277
5371
|
["EV_NEW_ROW", 0],
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
createElement2,
|
|
27
27
|
kw_get_dict_value,
|
|
28
28
|
kw_get_str,
|
|
29
|
+
kw_get_int,
|
|
29
30
|
gobj_send_event,
|
|
30
31
|
gobj_publish_event,
|
|
31
32
|
gobj_write_attr,
|
|
@@ -1245,7 +1246,10 @@ function process_treedb_descs(gobj)
|
|
|
1245
1246
|
desc: desc,
|
|
1246
1247
|
with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging"),
|
|
1247
1248
|
page_size: gobj_read_integer_attr(gobj, "page_size"),
|
|
1248
|
-
with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar")
|
|
1249
|
+
with_selection_bar: gobj_read_bool_attr(gobj, "with_selection_bar"),
|
|
1250
|
+
/* This host answers every write of the form (see
|
|
1251
|
+
* answer_form_write): the form closes on success only. */
|
|
1252
|
+
form_waits_for_answer: true
|
|
1249
1253
|
};
|
|
1250
1254
|
|
|
1251
1255
|
let id = `${gobj_name(gobj)}?${desc.topic_name}`;
|
|
@@ -1620,7 +1624,7 @@ function treedb_create_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1620
1624
|
/************************************************************
|
|
1621
1625
|
* Command to remote service
|
|
1622
1626
|
************************************************************/
|
|
1623
|
-
function treedb_update_node(gobj, treedb_name, topic_name, record, options)
|
|
1627
|
+
function treedb_update_node(gobj, treedb_name, topic_name, record, options, form_write)
|
|
1624
1628
|
{
|
|
1625
1629
|
let command = "update-node";
|
|
1626
1630
|
|
|
@@ -1636,6 +1640,9 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1636
1640
|
kw.__md_command__ = { // Data to be returned
|
|
1637
1641
|
topic_name: topic_name,
|
|
1638
1642
|
};
|
|
1643
|
+
if(form_write) {
|
|
1644
|
+
kw.__md_command__.form_write = form_write;
|
|
1645
|
+
}
|
|
1639
1646
|
// TODO review msg_iev_write_key(kw, "__topic_name__", topic_name);
|
|
1640
1647
|
|
|
1641
1648
|
let ret = gobj_command(
|
|
@@ -1646,7 +1653,9 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1646
1653
|
);
|
|
1647
1654
|
if(ret) {
|
|
1648
1655
|
log_error(ret);
|
|
1656
|
+
return -1;
|
|
1649
1657
|
}
|
|
1658
|
+
return 0;
|
|
1650
1659
|
}
|
|
1651
1660
|
|
|
1652
1661
|
/************************************************************
|
|
@@ -2056,6 +2065,12 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2056
2065
|
gobj_send_event(gobj, "EV_REFRESH_TOPIC",
|
|
2057
2066
|
{topic_name: failed_topic}, gobj);
|
|
2058
2067
|
}
|
|
2068
|
+
answer_form_write(
|
|
2069
|
+
gobj,
|
|
2070
|
+
get_gobj_formtable(gobj, failed_topic),
|
|
2071
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0),
|
|
2072
|
+
false
|
|
2073
|
+
);
|
|
2059
2074
|
}
|
|
2060
2075
|
}
|
|
2061
2076
|
return 0;
|
|
@@ -2148,6 +2163,12 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2148
2163
|
created: (command === "create-node"),
|
|
2149
2164
|
command: command
|
|
2150
2165
|
});
|
|
2166
|
+
answer_form_write(
|
|
2167
|
+
gobj,
|
|
2168
|
+
get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)),
|
|
2169
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0),
|
|
2170
|
+
true
|
|
2171
|
+
);
|
|
2151
2172
|
break;
|
|
2152
2173
|
|
|
2153
2174
|
case "delete-node":
|
|
@@ -2493,7 +2514,9 @@ function ac_treedb_node_deleted(gobj, event, kw, src)
|
|
|
2493
2514
|
********************************************/
|
|
2494
2515
|
function ac_create_record(gobj, event, kw, src)
|
|
2495
2516
|
{
|
|
2517
|
+
let form_write = (kw && kw.form_write) || 0;
|
|
2496
2518
|
if(refuse_if_readonly(gobj, event)) {
|
|
2519
|
+
answer_form_write(gobj, src, form_write, false);
|
|
2497
2520
|
return -1; /* Error already logged */
|
|
2498
2521
|
}
|
|
2499
2522
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
@@ -2506,13 +2529,12 @@ function ac_create_record(gobj, event, kw, src)
|
|
|
2506
2529
|
autolink: true
|
|
2507
2530
|
};
|
|
2508
2531
|
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
);
|
|
2532
|
+
if(treedb_update_node( // HACK use the powerful update_node
|
|
2533
|
+
gobj, treedb_name, topic_name, record, options, form_write) < 0) {
|
|
2534
|
+
answer_form_write(gobj, src, form_write, false);
|
|
2535
|
+
return -1; /* Error already logged */
|
|
2536
|
+
}
|
|
2537
|
+
return 0;
|
|
2516
2538
|
}
|
|
2517
2539
|
|
|
2518
2540
|
/********************************************
|
|
@@ -2715,7 +2737,9 @@ function ac_update_field(gobj, event, kw, src)
|
|
|
2715
2737
|
********************************************/
|
|
2716
2738
|
function ac_update_record(gobj, event, kw, src)
|
|
2717
2739
|
{
|
|
2740
|
+
let form_write = (kw && kw.form_write) || 0;
|
|
2718
2741
|
if(refuse_if_readonly(gobj, event)) {
|
|
2742
|
+
answer_form_write(gobj, src, form_write, false);
|
|
2719
2743
|
return -1; /* Error already logged */
|
|
2720
2744
|
}
|
|
2721
2745
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
@@ -2727,12 +2751,35 @@ function ac_update_record(gobj, event, kw, src)
|
|
|
2727
2751
|
autolink: true
|
|
2728
2752
|
};
|
|
2729
2753
|
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2754
|
+
if(treedb_update_node(
|
|
2755
|
+
gobj, treedb_name, topic_name, record, options, form_write) < 0) {
|
|
2756
|
+
answer_form_write(gobj, src, form_write, false);
|
|
2757
|
+
return -1; /* Error already logged */
|
|
2758
|
+
}
|
|
2759
|
+
return 0;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
/************************************************************
|
|
2763
|
+
* Tell a topic form how its write ended: EV_WRITE_DONE closes
|
|
2764
|
+
* it, EV_WRITE_REFUSED leaves it open on what was typed. Every
|
|
2765
|
+
* way a form's write can end passes here -- refused before it
|
|
2766
|
+
* left (read-only, no session) or answered by the backend -- so
|
|
2767
|
+
* a form never waits for an answer that will not come.
|
|
2768
|
+
************************************************************/
|
|
2769
|
+
function answer_form_write(gobj, gobj_topic_form, form_write, ok)
|
|
2770
|
+
{
|
|
2771
|
+
if(!form_write) {
|
|
2772
|
+
return; /* Not a form's write (a cell edited in place) */
|
|
2773
|
+
}
|
|
2774
|
+
if(!gobj_topic_form) {
|
|
2775
|
+
log_error(`${gobj_short_name(gobj)}: no topic form to answer write ${form_write}`);
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
gobj_send_event(
|
|
2779
|
+
gobj_topic_form,
|
|
2780
|
+
ok ? "EV_WRITE_DONE" : "EV_WRITE_REFUSED",
|
|
2781
|
+
{form_write: form_write},
|
|
2782
|
+
gobj
|
|
2736
2783
|
);
|
|
2737
2784
|
}
|
|
2738
2785
|
|
package/src/graph_save_plan.js
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import {
|
|
39
39
|
is_object,
|
|
40
40
|
json_is_identical,
|
|
41
|
+
json_deep_copy,
|
|
41
42
|
} from "@yuneta/gobj-js";
|
|
42
43
|
|
|
43
44
|
/***************************************************************
|
|
@@ -101,7 +102,46 @@ function plan_graph_saves(live, saved)
|
|
|
101
102
|
return plan;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/***************************************************************
|
|
106
|
+
* An ECHO of one __graphs__ record arrived: the node event a save
|
|
107
|
+
* of it sends back, from this browser or from another one.
|
|
108
|
+
*
|
|
109
|
+
* live {topic_name: properties} what the view shows
|
|
110
|
+
* saved {topic_name: properties} what the backend has
|
|
111
|
+
* records every __graphs__ record the view holds, `rec`
|
|
112
|
+
* already in its place among them
|
|
113
|
+
* rec the echoed record
|
|
114
|
+
*
|
|
115
|
+
* Only rec's TOPIC moves: it becomes the record, as shown and as
|
|
116
|
+
* saved. The other topics are left alone -- rebuilding their saved
|
|
117
|
+
* copies here took them from the live objects the view had already
|
|
118
|
+
* rearranged in place, so their unsaved changes counted as saved.
|
|
119
|
+
*
|
|
120
|
+
* `live[topic]` IS the record's own object, as when the view loads:
|
|
121
|
+
* the view arranges it in place. `saved[topic]` is a copy.
|
|
122
|
+
***************************************************************/
|
|
123
|
+
function apply_graphs_echo(live, saved, records, rec)
|
|
124
|
+
{
|
|
125
|
+
if(!is_object(rec) || !rec.topic) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if(rec.active && is_object(rec.properties)) {
|
|
129
|
+
live[rec.topic] = rec.properties;
|
|
130
|
+
saved[rec.topic] = json_deep_copy(rec.properties);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let other_active = (records || []).some((r) => {
|
|
134
|
+
return r && r !== rec && r.topic === rec.topic && r.active &&
|
|
135
|
+
is_object(r.properties);
|
|
136
|
+
});
|
|
137
|
+
if(!other_active) {
|
|
138
|
+
delete live[rec.topic];
|
|
139
|
+
delete saved[rec.topic];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
104
143
|
export {
|
|
144
|
+
apply_graphs_echo,
|
|
105
145
|
plan_graph_saves,
|
|
106
146
|
topic_arrangement_changed,
|
|
107
147
|
arrangement_of,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
***********************************************************************/
|
|
19
19
|
import {describe, test, expect} from "vitest";
|
|
20
20
|
import {
|
|
21
|
-
plan_graph_saves, topic_arrangement_changed, arrangement_of
|
|
21
|
+
plan_graph_saves, topic_arrangement_changed, arrangement_of, apply_graphs_echo
|
|
22
22
|
} from "./graph_save_plan.js";
|
|
23
23
|
|
|
24
24
|
const ORIGIN = "f3163a9f-1f37-412a-b8c9-87a4341f14de";
|
|
@@ -144,3 +144,59 @@ describe("arrangement_of", () => {
|
|
|
144
144
|
expect(arrangement_of("nodes")).toEqual({});
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* An ECHO of one __graphs__ record -- the node event a save of it, or a
|
|
150
|
+
* save by another browser, sends back. It used to rebuild the saved
|
|
151
|
+
* snapshot of EVERY topic, taking the copies from the live objects the
|
|
152
|
+
* view had already rearranged: what was unsaved in OTHER topics counted
|
|
153
|
+
* as saved, and the next Save skipped it (M32 of the 2026-09-21 review).
|
|
154
|
+
*/
|
|
155
|
+
describe("an echo of one __graphs__ record", () => {
|
|
156
|
+
function state()
|
|
157
|
+
{
|
|
158
|
+
let live = {
|
|
159
|
+
realms: layout({a: {x: 1, y: 1}}),
|
|
160
|
+
yunos: layout({b: {x: 2, y: 2}}),
|
|
161
|
+
};
|
|
162
|
+
let saved = {
|
|
163
|
+
realms: layout({a: {x: 1, y: 1}}),
|
|
164
|
+
yunos: layout({b: {x: 2, y: 2}}),
|
|
165
|
+
};
|
|
166
|
+
return {live, saved};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
test("touches only the topic of its record", () => {
|
|
170
|
+
let {live, saved} = state();
|
|
171
|
+
live.yunos.nodes.b.x = 99; /* moved, not saved yet */
|
|
172
|
+
let echo = {id: "1", topic: "realms", active: true,
|
|
173
|
+
properties: layout({a: {x: 5, y: 5}})};
|
|
174
|
+
apply_graphs_echo(live, saved, [echo], echo);
|
|
175
|
+
expect(plan_graph_saves(live, saved)).toEqual(["yunos"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("its topic becomes the echoed record, saved and live", () => {
|
|
179
|
+
let {live, saved} = state();
|
|
180
|
+
let echo = {id: "1", topic: "realms", active: true,
|
|
181
|
+
properties: layout({a: {x: 5, y: 5}})};
|
|
182
|
+
apply_graphs_echo(live, saved, [echo], echo);
|
|
183
|
+
expect(live.realms.nodes.a).toEqual({x: 5, y: 5});
|
|
184
|
+
expect(saved.realms.nodes.a).toEqual({x: 5, y: 5});
|
|
185
|
+
live.realms.nodes.a.x = 6; /* the view arranges it in place */
|
|
186
|
+
expect(saved.realms.nodes.a.x).toBe(5);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an inactive record drops its topic only when no active one is left", () => {
|
|
190
|
+
let {live, saved} = state();
|
|
191
|
+
let old = {id: "2", topic: "realms", active: false, properties: layout({})};
|
|
192
|
+
let current = {id: "3", topic: "realms", active: true,
|
|
193
|
+
properties: layout({a: {x: 1, y: 1}})};
|
|
194
|
+
apply_graphs_echo(live, saved, [old, current], old);
|
|
195
|
+
expect(live.realms).toBeDefined();
|
|
196
|
+
|
|
197
|
+
apply_graphs_echo(live, saved, [old], old);
|
|
198
|
+
expect(live.realms).toBeUndefined();
|
|
199
|
+
expect(saved.realms).toBeUndefined();
|
|
200
|
+
expect(live.yunos).toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
});
|
package/src/yui_row_search.js
CHANGED
|
@@ -40,8 +40,13 @@ const MAX_DEPTH = 4;
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
/***************************************************************
|
|
43
|
-
*
|
|
43
|
+
* A REFERENCE to a node: an fkey as `nodes` answers it with
|
|
44
|
+
* `list_dict` -- `{id, topic_name, hook_name}` -- or a hook's child
|
|
45
|
+
* as the node events deliver it, `{id, topic_name}`. It carries
|
|
46
|
+
* nothing but those keys; an object with more is data.
|
|
44
47
|
***************************************************************/
|
|
48
|
+
const REF_KEYS = new Set(["id", "topic_name", "hook_name"]);
|
|
49
|
+
|
|
45
50
|
function is_fkey_ref(value)
|
|
46
51
|
{
|
|
47
52
|
return !!value
|
|
@@ -49,7 +54,7 @@ function is_fkey_ref(value)
|
|
|
49
54
|
&& !Array.isArray(value)
|
|
50
55
|
&& ("id" in value)
|
|
51
56
|
&& ("topic_name" in value)
|
|
52
|
-
&& (
|
|
57
|
+
&& Object.keys(value).every((k) => REF_KEYS.has(k));
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
|