@yuneta/gobj-ui 7.23.168 → 7.23.170
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 +2 -2
- package/dist/gobj-ui.cjs.js +784 -478
- package/dist/gobj-ui.es.js +784 -478
- package/package.json +2 -2
- package/src/broadcast_attrs.test.js +115 -0
- package/src/c_g6_nodes_tree.js +208 -19
- package/src/c_yui_treedb_topic_with_form.js +206 -107
- package/src/treedb_write_plan.js +47 -1
- package/src/treedb_write_plan.test.js +53 -1
- package/src/yui_row_search.js +56 -27
- package/src/yui_row_search.test.js +51 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
json_deep_copy,
|
|
24
24
|
createElement2,
|
|
25
25
|
gobj_send_event,
|
|
26
|
+
gobj_post_event,
|
|
26
27
|
clean_name,
|
|
27
28
|
is_array,
|
|
28
29
|
is_string,
|
|
@@ -95,7 +96,9 @@ import {t} from "i18next";
|
|
|
95
96
|
|
|
96
97
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
97
98
|
|
|
98
|
-
import {
|
|
99
|
+
import {
|
|
100
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
101
|
+
} from "./treedb_write_plan.js";
|
|
99
102
|
import {
|
|
100
103
|
yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
|
|
101
104
|
} from "./yui_file_field.js";
|
|
@@ -209,8 +212,8 @@ SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"
|
|
|
209
212
|
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
|
|
210
213
|
SDATA(data_type_t.DTP_BOOLEAN, "with_radio", 0, false, "Auxiliary first column to select one row"),
|
|
211
214
|
SDATA(data_type_t.DTP_BOOLEAN, "with_selection_bar", 0, false, "Show the shared selection bar (\"N selected\" + a way out) above the table while in edition mode. OFF by default: the bar takes its words from the HOST's i18n, and a host that has not defined \"{{n}} selected\" and \"clear selection\" would render the keys"),
|
|
212
|
-
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "
|
|
213
|
-
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "
|
|
215
|
+
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_select_rows_event", 0, false, "Publish EV_SELECT_ROWS to the subscriber. OFF by default: the event is an OUTPUT event, so a host that turns it on must declare EV_SELECT_ROWS in its own FSM or it answers \"Event NOT DEFINED in state\" on every selected row"),
|
|
216
|
+
SDATA(data_type_t.DTP_BOOLEAN, "broadcast_unselect_rows_event", 0, false, "Publish EV_UNSELECT_ROWS to the subscriber. Same contract as broadcast_select_rows_event, and INDEPENDENT of it: a host that wants both sets both"),
|
|
214
217
|
|
|
215
218
|
/*---------------- Tabulator Defaults ----------------*/
|
|
216
219
|
SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
|
|
@@ -605,7 +608,9 @@ function build_ui(gobj)
|
|
|
605
608
|
* instead of being pushed away from the harmless ones. Do not
|
|
606
609
|
* reorder this group as if they were peer actions. */
|
|
607
610
|
let $edit_button = createElement2(
|
|
608
|
-
['button', {id: ``, class: 'button button-edit-record mr-1'
|
|
611
|
+
['button', {id: ``, class: 'button button-edit-record mr-1',
|
|
612
|
+
title: t('edit'), 'data-i18n-title': 'edit',
|
|
613
|
+
'aria-label': t('edit'), 'data-i18n-aria-label': 'edit'}, [
|
|
609
614
|
yui_toolbar_icon('yi-pen'),
|
|
610
615
|
['span',
|
|
611
616
|
{
|
|
@@ -631,7 +636,9 @@ function build_ui(gobj)
|
|
|
631
636
|
|
|
632
637
|
if(with_new_button) {
|
|
633
638
|
let $new_button = createElement2(
|
|
634
|
-
['button', {id: ``, class: 'button button-new-record mr-1',
|
|
639
|
+
['button', {id: ``, class: 'button button-new-record mr-1',
|
|
640
|
+
title: t('new'), 'data-i18n-title': 'new',
|
|
641
|
+
'aria-label': t('new'), 'data-i18n-aria-label': 'new', disabled: true}, [
|
|
635
642
|
yui_toolbar_icon('yi-plus'),
|
|
636
643
|
['span',
|
|
637
644
|
{
|
|
@@ -651,7 +658,9 @@ function build_ui(gobj)
|
|
|
651
658
|
|
|
652
659
|
if(with_delete_button) {
|
|
653
660
|
let $delete_button = createElement2(
|
|
654
|
-
['button', {id: ``, class: 'button button-delete-record mr-1',
|
|
661
|
+
['button', {id: ``, class: 'button button-delete-record mr-1',
|
|
662
|
+
title: t('delete'), 'data-i18n-title': 'delete',
|
|
663
|
+
'aria-label': t('delete'), 'data-i18n-aria-label': 'delete', disabled: true}, [
|
|
655
664
|
yui_toolbar_icon('yi-trash'),
|
|
656
665
|
['span',
|
|
657
666
|
{
|
|
@@ -672,7 +681,9 @@ function build_ui(gobj)
|
|
|
672
681
|
|
|
673
682
|
if(with_copy_button) {
|
|
674
683
|
let $copy_button = createElement2(
|
|
675
|
-
['button', {id: ``, class: 'button button-copy-record mr-1',
|
|
684
|
+
['button', {id: ``, class: 'button button-copy-record mr-1',
|
|
685
|
+
title: t('copy'), 'data-i18n-title': 'copy',
|
|
686
|
+
'aria-label': t('copy'), 'data-i18n-aria-label': 'copy', disabled: true}, [
|
|
676
687
|
yui_toolbar_icon('yi-copy'),
|
|
677
688
|
['span',
|
|
678
689
|
{
|
|
@@ -693,7 +704,9 @@ function build_ui(gobj)
|
|
|
693
704
|
|
|
694
705
|
if(with_paste_button) {
|
|
695
706
|
let $paste_button = createElement2(
|
|
696
|
-
['button', {id: ``, class: 'button button-paste-record mr-1',
|
|
707
|
+
['button', {id: ``, class: 'button button-paste-record mr-1',
|
|
708
|
+
title: t('paste'), 'data-i18n-title': 'paste',
|
|
709
|
+
'aria-label': t('paste'), 'data-i18n-aria-label': 'paste', disabled: true}, [
|
|
697
710
|
yui_toolbar_icon('yi-paste'),
|
|
698
711
|
['span',
|
|
699
712
|
{
|
|
@@ -745,9 +758,17 @@ function build_ui(gobj)
|
|
|
745
758
|
type: 'text',
|
|
746
759
|
/* The placeholder is not a text node, so the
|
|
747
760
|
* data-i18n walk cannot reach it — it needs its own
|
|
748
|
-
* key or it stays English for good.
|
|
761
|
+
* key or it stays English for good.
|
|
762
|
+
*
|
|
763
|
+
* And a placeholder is not a NAME: it goes away the
|
|
764
|
+
* moment something is typed, and a reader is not
|
|
765
|
+
* obliged to announce it. The box carries no <label>
|
|
766
|
+
* either, so without the two below it is an unnamed
|
|
767
|
+
* text field. */
|
|
749
768
|
placeholder: t('search'),
|
|
750
|
-
'data-i18n-placeholder': 'search'
|
|
769
|
+
'data-i18n-placeholder': 'search',
|
|
770
|
+
title: t('search'), 'data-i18n-title': 'search',
|
|
771
|
+
'aria-label': t('search'), 'data-i18n-aria-label': 'search'
|
|
751
772
|
}],
|
|
752
773
|
['span', {class: 'icon is-left'}, [
|
|
753
774
|
yui_toolbar_icon('yi-magnifying-glass')
|
|
@@ -776,7 +797,9 @@ function build_ui(gobj)
|
|
|
776
797
|
|
|
777
798
|
if(with_refresh_button) {
|
|
778
799
|
let $refresh = createElement2(
|
|
779
|
-
['button', {class: 'button mr-1',
|
|
800
|
+
['button', {class: 'button mr-1',
|
|
801
|
+
title: t('refresh'), 'data-i18n-title': 'refresh',
|
|
802
|
+
'aria-label': t('refresh'), 'data-i18n-aria-label': 'refresh'}, [
|
|
780
803
|
yui_toolbar_icon('yi-arrows-rotate'),
|
|
781
804
|
['span', {class: 'is-hidden-mobile', i18n: 'refresh', style: 'padding-left:5px;'}, 'refresh']
|
|
782
805
|
], {
|
|
@@ -1485,7 +1508,7 @@ function create_tabulator(gobj)
|
|
|
1485
1508
|
let index = cell.getRow().getPosition();
|
|
1486
1509
|
if(e.target.closest('.edit')) {
|
|
1487
1510
|
e.stopPropagation();
|
|
1488
|
-
|
|
1511
|
+
gobj_send_event(gobj, "EV_EDIT_RECORD", {index: index}, gobj);
|
|
1489
1512
|
} else if(e.target.closest('.remove')) {
|
|
1490
1513
|
e.stopPropagation();
|
|
1491
1514
|
gobj_send_event(gobj, "EV_DELETE_ROWS", {index: index, row: row}, gobj);
|
|
@@ -2263,15 +2286,31 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2263
2286
|
return;
|
|
2264
2287
|
}
|
|
2265
2288
|
|
|
2266
|
-
/*
|
|
2267
|
-
*
|
|
2289
|
+
/* The title says WHAT YOU ARE DOING, and it has to survive a
|
|
2290
|
+
* language change.
|
|
2291
|
+
*
|
|
2292
|
+
* It used to be composed here -- `t("new") + " " + t(topic_name)`
|
|
2293
|
+
* -- and handed to the dialog as its `title`, which the header
|
|
2294
|
+
* renders as an i18n KEY. So the composed sentence was looked up
|
|
2295
|
+
* as a key, found nothing, and rendered as itself: right the first
|
|
2296
|
+
* time, because both halves had been translated already, and
|
|
2297
|
+
* FROZEN in that language for the life of the dialog. Switching
|
|
2298
|
+
* language left "Nuevo usuarios" on an English page.
|
|
2299
|
+
*
|
|
2300
|
+
* The header carries two halves and only one of them can hold a
|
|
2301
|
+
* key: `title_prefix` is DATA and is never translated, `title` is
|
|
2302
|
+
* the KIND and re-translates. So the record id goes in the prefix
|
|
2303
|
+
* -- it is the one part no language touches -- and the kind takes
|
|
2304
|
+
* the key. The topic name leaves the title: it is what the TAB
|
|
2305
|
+
* behind the dialog says, and the form's own fields say it again,
|
|
2306
|
+
* while "new"/"edit"/"view" is what only this header can tell you.
|
|
2307
|
+
* All three are keys this library already uses. */
|
|
2268
2308
|
let pkey = desc.pkey || "id";
|
|
2269
|
-
let title;
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
} else {
|
|
2309
|
+
let title = (mode === "create")? "new" : ((mode === "view")? "view" : "edit");
|
|
2310
|
+
let title_prefix = "";
|
|
2311
|
+
if(mode !== "create") {
|
|
2273
2312
|
let rid = record ? String(record[pkey] ?? "") : "";
|
|
2274
|
-
|
|
2313
|
+
title_prefix = empty_string(rid)? "" : rid;
|
|
2275
2314
|
}
|
|
2276
2315
|
|
|
2277
2316
|
/* "view" is an "update" that writes nothing: same title, same pkey
|
|
@@ -2334,6 +2373,7 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2334
2373
|
priv.form_modal = yui_shell_show_modal(shell, $body, {
|
|
2335
2374
|
dialog: true,
|
|
2336
2375
|
logical_class: "TREEDB_FORM_SHEET",
|
|
2376
|
+
title_prefix: title_prefix,
|
|
2337
2377
|
title: title,
|
|
2338
2378
|
t: t,
|
|
2339
2379
|
before_close: function() {
|
|
@@ -2382,15 +2422,8 @@ function form_may_close(gobj)
|
|
|
2382
2422
|
* a second key to define — and the one that was here had never
|
|
2383
2423
|
* been defined in any locale, which renders as the English
|
|
2384
2424
|
* sentence itself, in every language, for ever. */
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
kw.warning || "all changes will be lost",
|
|
2388
|
-
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2389
|
-
).then(function(answer) {
|
|
2390
|
-
if(answer === "yes" && priv.form_modal) {
|
|
2391
|
-
priv.form_modal.close();
|
|
2392
|
-
}
|
|
2393
|
-
});
|
|
2425
|
+
confirm_then(gobj, kw.warning || "all changes will be lost",
|
|
2426
|
+
{what: "close_form"});
|
|
2394
2427
|
return false;
|
|
2395
2428
|
}
|
|
2396
2429
|
}
|
|
@@ -2416,6 +2449,33 @@ function teardown_form_child(gobj)
|
|
|
2416
2449
|
priv.form_modal = null;
|
|
2417
2450
|
}
|
|
2418
2451
|
|
|
2452
|
+
/************************************************************
|
|
2453
|
+
* Ask, and turn the answer into an EVENT.
|
|
2454
|
+
*
|
|
2455
|
+
* A dialog's promise settles outside the machine: acting from its
|
|
2456
|
+
* `.then` puts the decision -- delete these records, throw these
|
|
2457
|
+
* edits away -- in a callback the `machine` trace never sees, and
|
|
2458
|
+
* in a view that may be gone by the time the person answers.
|
|
2459
|
+
*
|
|
2460
|
+
* `kw` carries an IDENTITY and never an object: the trace dumps
|
|
2461
|
+
* it, and a gobj, a Tabulator row or a DOM node in there is a
|
|
2462
|
+
* circular structure that breaks the very trace the FSM feeds.
|
|
2463
|
+
*
|
|
2464
|
+
* Same shape as C_YUI_SCHEMA_EDITOR's confirm_then().
|
|
2465
|
+
************************************************************/
|
|
2466
|
+
function confirm_then(gobj, message, kw)
|
|
2467
|
+
{
|
|
2468
|
+
yui_shell_confirm_yesnocancel(
|
|
2469
|
+
yui_shell_of(gobj), message,
|
|
2470
|
+
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
2471
|
+
).then(function(answer) {
|
|
2472
|
+
if(answer !== "yes" || gobj_is_destroying(gobj)) {
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
gobj_send_event(gobj, "EV_CONFIRMED", kw, gobj);
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2419
2479
|
/************************************************************
|
|
2420
2480
|
* Force the dialog down (bypasses the unsaved guard): used after a
|
|
2421
2481
|
* successful save and on teardown. Closing the modal runs its
|
|
@@ -3130,7 +3190,7 @@ function pkey_is_rowid(gobj)
|
|
|
3130
3190
|
* Show the edit "update" form of a row (record)
|
|
3131
3191
|
* internally called from the icon edit at Op column
|
|
3132
3192
|
************************************************************/
|
|
3133
|
-
function show_edit_form(gobj, row
|
|
3193
|
+
function show_edit_form(gobj, row)
|
|
3134
3194
|
{
|
|
3135
3195
|
/* a rowid pkey has no "update": every save appends a new
|
|
3136
3196
|
* instance (timeranger semantics) — open in create mode */
|
|
@@ -3204,42 +3264,6 @@ function get_schema_col(gobj, id)
|
|
|
3204
3264
|
return null;
|
|
3205
3265
|
}
|
|
3206
3266
|
|
|
3207
|
-
/************************************************************
|
|
3208
|
-
* Does column `col` travel back to treedb on a write?
|
|
3209
|
-
*
|
|
3210
|
-
* The form SHOWS every field and sends back only what the topic
|
|
3211
|
-
* accepts: the writable cols, the fkeys (a link is edited by
|
|
3212
|
-
* linking) and the pkey, which is not written but is what
|
|
3213
|
-
* ADDRESSES the record.
|
|
3214
|
-
*
|
|
3215
|
-
* It matters because `treedb_update_node()` does not check
|
|
3216
|
-
* `writable` -- it writes any col it is handed -- so a read-only
|
|
3217
|
-
* field travelling back is written with whatever the form made of
|
|
3218
|
-
* it, and the fields that describe a record are exactly the ones
|
|
3219
|
-
* that do not survive a round trip through a widget: a `time` is an
|
|
3220
|
-
* integer rendered as a `datetime-local`, with no seconds, so every
|
|
3221
|
-
* save moved it back up to 59 s. Nothing looked wrong until the
|
|
3222
|
-
* stored timestamp had moved.
|
|
3223
|
-
*
|
|
3224
|
-
* `is_file` and not the type: a `file` column IS an fkey (it is
|
|
3225
|
-
* flagged ['fkey','file']) but answers `type: "file"`, and the write
|
|
3226
|
-
* goes out with `autolink`, which rebuilds the links from what the
|
|
3227
|
-
* record carries. Dropped, a read-only `file` column -- the one only
|
|
3228
|
-
* a load fills -- was UNLINKED by every save of any other field.
|
|
3229
|
-
************************************************************/
|
|
3230
|
-
function col_goes_back_to_treedb(gobj, col)
|
|
3231
|
-
{
|
|
3232
|
-
let desc = gobj_read_attr(gobj, "desc");
|
|
3233
|
-
let pkey = desc.pkey || "id";
|
|
3234
|
-
const field_desc = treedb_get_field_desc(col);
|
|
3235
|
-
|
|
3236
|
-
if(field_desc.is_writable || field_desc.type === "fkey" ||
|
|
3237
|
-
field_desc.is_file || col.id === pkey) {
|
|
3238
|
-
return true;
|
|
3239
|
-
}
|
|
3240
|
-
return false;
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
3267
|
/************************************************************
|
|
3244
3268
|
* The record the FORM hands over, without the columns that do
|
|
3245
3269
|
* not go back to treedb (see col_goes_back_to_treedb()). The
|
|
@@ -3251,7 +3275,7 @@ function strip_read_only_cols(gobj, record)
|
|
|
3251
3275
|
let row = {};
|
|
3252
3276
|
for(let field_name of Object.keys(record || {})) {
|
|
3253
3277
|
let col = get_schema_col(gobj, field_name);
|
|
3254
|
-
if(col && !col_goes_back_to_treedb(gobj, col)) {
|
|
3278
|
+
if(col && !col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) {
|
|
3255
3279
|
continue;
|
|
3256
3280
|
}
|
|
3257
3281
|
row[field_name] = record[field_name];
|
|
@@ -3278,7 +3302,7 @@ function transform__form_record_2_treedb_record(gobj, kw, operation)
|
|
|
3278
3302
|
continue;
|
|
3279
3303
|
}
|
|
3280
3304
|
|
|
3281
|
-
if(!col_goes_back_to_treedb(gobj, col)) {
|
|
3305
|
+
if(!col_goes_back_to_treedb(gobj_read_attr(gobj, "desc"), col)) {
|
|
3282
3306
|
continue;
|
|
3283
3307
|
}
|
|
3284
3308
|
|
|
@@ -3940,50 +3964,81 @@ function ac_delete_rows(gobj, event, kw, src)
|
|
|
3940
3964
|
return 0;
|
|
3941
3965
|
}
|
|
3942
3966
|
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
3946
|
-
).then(function(answer) {
|
|
3947
|
-
if(answer === "yes") {
|
|
3948
|
-
for(let row of rows) {
|
|
3949
|
-
// TODO why don't send once EV_DELETE_RECORD(S)
|
|
3950
|
-
gobj_publish_event(
|
|
3951
|
-
gobj,
|
|
3952
|
-
"EV_DELETE_RECORD",
|
|
3953
|
-
{
|
|
3954
|
-
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
3955
|
-
record: row
|
|
3956
|
-
}
|
|
3957
|
-
);
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
});
|
|
3967
|
+
confirm_then(gobj, build_delete_question(gobj, rows),
|
|
3968
|
+
{what: "delete_selection"});
|
|
3961
3969
|
|
|
3962
3970
|
} else {
|
|
3963
3971
|
/*----------------------------*
|
|
3964
3972
|
* Delete one row
|
|
3965
3973
|
* {index: , row: }
|
|
3966
3974
|
*----------------------------*/
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
{t: t, yes_label: "yes", no_label: "no", cancel_label: "cancel"}
|
|
3970
|
-
).then(function(answer) {
|
|
3971
|
-
if(answer === "yes") {
|
|
3972
|
-
gobj_publish_event(
|
|
3973
|
-
gobj,
|
|
3974
|
-
"EV_DELETE_RECORD",
|
|
3975
|
-
{
|
|
3976
|
-
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
3977
|
-
record: kw.row
|
|
3978
|
-
}
|
|
3979
|
-
);
|
|
3980
|
-
}
|
|
3981
|
-
});
|
|
3975
|
+
confirm_then(gobj, build_delete_question(gobj, kw.row),
|
|
3976
|
+
{what: "delete_row", index: kw.index});
|
|
3982
3977
|
}
|
|
3983
3978
|
|
|
3984
3979
|
return 0;
|
|
3985
3980
|
}
|
|
3986
3981
|
|
|
3982
|
+
/************************************************************
|
|
3983
|
+
* {what, ...} -- the person said yes to what confirm_then()
|
|
3984
|
+
* asked. The rows are read again HERE and not carried in the
|
|
3985
|
+
* kw: the kw is plain json, and a Tabulator row is not.
|
|
3986
|
+
************************************************************/
|
|
3987
|
+
function ac_confirmed(gobj, event, kw, src)
|
|
3988
|
+
{
|
|
3989
|
+
let priv = gobj.priv;
|
|
3990
|
+
let what = kw? kw.what : "";
|
|
3991
|
+
|
|
3992
|
+
if(what === "close_form") {
|
|
3993
|
+
if(priv.form_modal) {
|
|
3994
|
+
priv.form_modal.close();
|
|
3995
|
+
}
|
|
3996
|
+
return 0;
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
4000
|
+
return -1; /* Error already logged */
|
|
4001
|
+
}
|
|
4002
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
4003
|
+
let topic_name = gobj_read_str_attr(gobj, "topic_name");
|
|
4004
|
+
|
|
4005
|
+
if(what === "delete_selection") {
|
|
4006
|
+
let rows = yui_selected_rows(tabulator);
|
|
4007
|
+
if(!rows.length) {
|
|
4008
|
+
log_error(`${gobj_short_name(gobj)}: ${event} with the selection gone`);
|
|
4009
|
+
return -1;
|
|
4010
|
+
}
|
|
4011
|
+
for(let row of rows) {
|
|
4012
|
+
// TODO why don't send once EV_DELETE_RECORD(S)
|
|
4013
|
+
gobj_publish_event(
|
|
4014
|
+
gobj,
|
|
4015
|
+
"EV_DELETE_RECORD",
|
|
4016
|
+
{topic_name: topic_name, record: row}
|
|
4017
|
+
);
|
|
4018
|
+
}
|
|
4019
|
+
return 0;
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
if(what === "delete_row") {
|
|
4023
|
+
let index = kw.index;
|
|
4024
|
+
let row = (tabulator && typeof index === "number")?
|
|
4025
|
+
tabulator.getRowFromPosition(index) : null;
|
|
4026
|
+
if(!row) {
|
|
4027
|
+
log_error(`${gobj_short_name(gobj)}: ${event} names no row: ${index}`);
|
|
4028
|
+
return -1;
|
|
4029
|
+
}
|
|
4030
|
+
gobj_publish_event(
|
|
4031
|
+
gobj,
|
|
4032
|
+
"EV_DELETE_RECORD",
|
|
4033
|
+
{topic_name: topic_name, record: row.getData()}
|
|
4034
|
+
);
|
|
4035
|
+
return 0;
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${what}`);
|
|
4039
|
+
return -1;
|
|
4040
|
+
}
|
|
4041
|
+
|
|
3987
4042
|
/************************************************************
|
|
3988
4043
|
* From internal
|
|
3989
4044
|
************************************************************/
|
|
@@ -4085,12 +4140,15 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
4085
4140
|
if(cols.length === 0) {
|
|
4086
4141
|
publish_treedb_write(gobj, mode, kw);
|
|
4087
4142
|
|
|
4088
|
-
/*
|
|
4089
|
-
*
|
|
4090
|
-
* callback
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4143
|
+
/* We are INSIDE the form child's gobj_publish_event stack, and
|
|
4144
|
+
* closing the dialog tears that child down -- never destroy the
|
|
4145
|
+
* publisher synchronously from a subscriber's callback. So the
|
|
4146
|
+
* close is DEFERRED, and a deferral is not a time:
|
|
4147
|
+
* gobj_post_event() delivers EV_CLOSE_FORM to us on the next
|
|
4148
|
+
* turn of the loop, it drops it if we are being destroyed
|
|
4149
|
+
* meanwhile, and it says so in the `machine` trace. A bare
|
|
4150
|
+
* setTimeout(…, 0) did none of the three. */
|
|
4151
|
+
gobj_post_event(gobj, "EV_CLOSE_FORM", {}, gobj);
|
|
4094
4152
|
return 0;
|
|
4095
4153
|
}
|
|
4096
4154
|
|
|
@@ -4197,6 +4255,16 @@ function publish_treedb_write(gobj, mode, record, jn_files)
|
|
|
4197
4255
|
);
|
|
4198
4256
|
}
|
|
4199
4257
|
|
|
4258
|
+
/************************************************************
|
|
4259
|
+
* The deferred close of the form dialog, posted by the save
|
|
4260
|
+
* from inside the form child's own publish stack.
|
|
4261
|
+
************************************************************/
|
|
4262
|
+
function ac_close_form(gobj, event, kw, src)
|
|
4263
|
+
{
|
|
4264
|
+
close_form_dialog(gobj);
|
|
4265
|
+
return 0;
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4200
4268
|
/************************************************************
|
|
4201
4269
|
* {mode, record, picks} -- the bytes are here now
|
|
4202
4270
|
************************************************************/
|
|
@@ -4260,7 +4328,7 @@ function ac_unselect_rows(gobj, event, kw, src)
|
|
|
4260
4328
|
|
|
4261
4329
|
// WARNING with radio, there is no unselect event.
|
|
4262
4330
|
|
|
4263
|
-
if(gobj_read_bool_attr(gobj, "
|
|
4331
|
+
if(gobj_read_bool_attr(gobj, "broadcast_unselect_rows_event")) {
|
|
4264
4332
|
gobj_publish_event(gobj, event, kw);
|
|
4265
4333
|
}
|
|
4266
4334
|
|
|
@@ -4809,6 +4877,31 @@ function ac_show_record(gobj, event, kw, src)
|
|
|
4809
4877
|
return 0;
|
|
4810
4878
|
}
|
|
4811
4879
|
|
|
4880
|
+
/************************************************************
|
|
4881
|
+
* {index} -- the pencil of the Op column. Same shape as
|
|
4882
|
+
* EV_SHOW_RECORD: the kw carries WHICH row, and the row itself
|
|
4883
|
+
* is resolved here, where the table is.
|
|
4884
|
+
************************************************************/
|
|
4885
|
+
function ac_edit_record(gobj, event, kw, src)
|
|
4886
|
+
{
|
|
4887
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
4888
|
+
let index = kw? kw.index : undefined;
|
|
4889
|
+
if(!tabulator || typeof index !== "number") {
|
|
4890
|
+
log_error(`${gobj_short_name(gobj)}: no row ${index} to edit`);
|
|
4891
|
+
return -1;
|
|
4892
|
+
}
|
|
4893
|
+
|
|
4894
|
+
let row = tabulator.getRowFromPosition(index);
|
|
4895
|
+
if(!row) {
|
|
4896
|
+
log_error(`${gobj_short_name(gobj)}: row ${index} is gone`);
|
|
4897
|
+
return -1;
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
show_edit_form(gobj, row.getData());
|
|
4901
|
+
|
|
4902
|
+
return 0;
|
|
4903
|
+
}
|
|
4904
|
+
|
|
4812
4905
|
/************************************************************
|
|
4813
4906
|
* The reader picked another page size: remember it, so coming back to
|
|
4814
4907
|
* this topic opens where they left it instead of at 200.
|
|
@@ -4948,11 +5041,13 @@ function create_gclass(gclass_name)
|
|
|
4948
5041
|
|
|
4949
5042
|
["EV_EDITION_MODE", ac_edition_mode, null],
|
|
4950
5043
|
["EV_SAVE_RECORD", ac_form_save_record, null],
|
|
5044
|
+
["EV_CLOSE_FORM", ac_close_form, null],
|
|
4951
5045
|
["EV_FILES_READ", ac_files_read, null],
|
|
4952
5046
|
["EV_FILES_FAILED", ac_files_failed, null],
|
|
4953
5047
|
|
|
4954
5048
|
["EV_NEW_ROW", ac_new_row, null],
|
|
4955
5049
|
["EV_DELETE_ROWS", ac_delete_rows, null],
|
|
5050
|
+
["EV_CONFIRMED", ac_confirmed, null],
|
|
4956
5051
|
["EV_SELECT_ROWS", ac_select_rows, null],
|
|
4957
5052
|
["EV_CLEAR_SELECTION", ac_clear_selection, null],
|
|
4958
5053
|
["EV_UNSELECT_ROWS", ac_unselect_rows, null],
|
|
@@ -4978,6 +5073,7 @@ function create_gclass(gclass_name)
|
|
|
4978
5073
|
["EV_OPEN_COLUMNS", ac_open_columns, null],
|
|
4979
5074
|
["EV_PAGE_SIZE_CHANGED", ac_page_size_changed, null],
|
|
4980
5075
|
["EV_SHOW_RECORD", ac_show_record, null],
|
|
5076
|
+
["EV_EDIT_RECORD", ac_edit_record, null],
|
|
4981
5077
|
["EV_TOGGLE_COLUMN", ac_toggle_column, null],
|
|
4982
5078
|
["EV_EXPORT_TABLE", ac_export_table, null],
|
|
4983
5079
|
["EV_SHOW", ac_show, null],
|
|
@@ -4997,10 +5093,12 @@ function create_gclass(gclass_name)
|
|
|
4997
5093
|
|
|
4998
5094
|
["EV_EDITION_MODE", 0],
|
|
4999
5095
|
["EV_SAVE_RECORD", 0],
|
|
5096
|
+
["EV_CLOSE_FORM", 0],
|
|
5000
5097
|
["EV_FILES_READ", 0],
|
|
5001
5098
|
["EV_FILES_FAILED", 0],
|
|
5002
5099
|
["EV_NEW_ROW", 0],
|
|
5003
5100
|
["EV_DELETE_ROWS", 0],
|
|
5101
|
+
["EV_CONFIRMED", 0],
|
|
5004
5102
|
["EV_COPY_ROWS", 0],
|
|
5005
5103
|
["EV_PASTE_ROWS", 0],
|
|
5006
5104
|
["EV_CLEAR_SELECTION", 0],
|
|
@@ -5030,6 +5128,7 @@ function create_gclass(gclass_name)
|
|
|
5030
5128
|
["EV_OPEN_COLUMNS", 0],
|
|
5031
5129
|
["EV_PAGE_SIZE_CHANGED", 0],
|
|
5032
5130
|
["EV_SHOW_RECORD", 0],
|
|
5131
|
+
["EV_EDIT_RECORD", 0],
|
|
5033
5132
|
["EV_TOGGLE_COLUMN", 0],
|
|
5034
5133
|
["EV_EXPORT_TABLE", 0],
|
|
5035
5134
|
|
package/src/treedb_write_plan.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* All Rights Reserved.
|
|
20
20
|
***********************************************************************/
|
|
21
21
|
|
|
22
|
+
import {treedb_get_field_desc} from "@yuneta/gobj-js";
|
|
23
|
+
import {topic_pkey2s} from "./schema_model.js";
|
|
24
|
+
|
|
22
25
|
/* What a read-only form keeps: reading a record includes taking it with
|
|
23
26
|
* you, so `copy` stays and the write half goes. */
|
|
24
27
|
const READONLY_FORM_TOOLBAR = ["copy"];
|
|
@@ -70,4 +73,47 @@ function plan_treedb_writes(flags)
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
/************************************************************
|
|
77
|
+
* Does column `col` travel back to treedb on a write?
|
|
78
|
+
*
|
|
79
|
+
* The form SHOWS every field and sends back only what the topic
|
|
80
|
+
* accepts: the writable cols, the fkeys (a link is edited by
|
|
81
|
+
* linking) and the pkey, which is not written but is what
|
|
82
|
+
* ADDRESSES the record.
|
|
83
|
+
*
|
|
84
|
+
* It matters because `treedb_update_node()` does not check
|
|
85
|
+
* `writable` -- it writes any col it is handed -- so a read-only
|
|
86
|
+
* field travelling back is written with whatever the form made of
|
|
87
|
+
* it, and the fields that describe a record are exactly the ones
|
|
88
|
+
* that do not survive a round trip through a widget: a `time` is an
|
|
89
|
+
* integer rendered as a `datetime-local`, with no seconds, so every
|
|
90
|
+
* save moved it back up to 59 s. Nothing looked wrong until the
|
|
91
|
+
* stored timestamp had moved.
|
|
92
|
+
*
|
|
93
|
+
* `is_file` and not the type: a `file` column IS an fkey (it is
|
|
94
|
+
* flagged ['fkey','file']) but answers `type: "file"`, and the write
|
|
95
|
+
* goes out with `autolink`, which rebuilds the links from what the
|
|
96
|
+
* record carries. Dropped, a read-only `file` column -- the one only
|
|
97
|
+
* a load fills -- was UNLINKED by every save of any other field.
|
|
98
|
+
*
|
|
99
|
+
* The pkey2s go back for the same reason as the pkey: a secondary key
|
|
100
|
+
* names the INSTANCE the update is for (`yunos.yuno_release` in the
|
|
101
|
+
* agent), and it is `persistent, required`, not `writable`. Dropped,
|
|
102
|
+
* the update reached the backend with no pkey2 and C_NODE resolved it
|
|
103
|
+
* to the PRIMARY instance -- right by chance while the table lists
|
|
104
|
+
* primaries, wrong from a form opened on a row of `instances`.
|
|
105
|
+
************************************************************/
|
|
106
|
+
function col_goes_back_to_treedb(desc, col)
|
|
107
|
+
{
|
|
108
|
+
let d = desc || {};
|
|
109
|
+
let pkey = d.pkey || "id";
|
|
110
|
+
const field_desc = treedb_get_field_desc(col);
|
|
111
|
+
|
|
112
|
+
if(field_desc.is_writable || field_desc.type === "fkey" ||
|
|
113
|
+
field_desc.is_file || col.id === pkey) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return topic_pkey2s(d).includes(col.id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb};
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* affordance back by accident.
|
|
9
9
|
***********************************************************************/
|
|
10
10
|
import { describe, test, expect } from "vitest";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
13
|
+
} from "./treedb_write_plan.js";
|
|
12
14
|
|
|
13
15
|
const WRITES = ["edition_mode", "new_button", "delete_button", "paste_button", "in_row_icons"];
|
|
14
16
|
|
|
@@ -81,3 +83,53 @@ describe("readonly beats every with_*", () => {
|
|
|
81
83
|
expect(plan_treedb_writes({readonly: "yes"}).new_button).toBe(false);
|
|
82
84
|
});
|
|
83
85
|
});
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/***********************************************************************
|
|
89
|
+
* Which columns of a form go back in the write. The shape is the
|
|
90
|
+
* agent's `yunos`: a pkey, a secondary key that is required but not
|
|
91
|
+
* writable, and the read-only columns that never survive a widget.
|
|
92
|
+
***********************************************************************/
|
|
93
|
+
const YUNOS_DESC = {
|
|
94
|
+
pkey: "id",
|
|
95
|
+
pkey2s: ["yuno_release"],
|
|
96
|
+
};
|
|
97
|
+
const COL = {
|
|
98
|
+
id: {id: "id", type: "string", flag: ["persistent", "required", "rowid"]},
|
|
99
|
+
yuno_release: {id: "yuno_release", type: "string", flag: ["persistent", "required"]},
|
|
100
|
+
yuno_name: {id: "yuno_name", type: "string", flag: ["persistent", "writable"]},
|
|
101
|
+
date: {id: "date", type: "integer", flag: ["persistent", "time"]},
|
|
102
|
+
realm_id: {id: "realm_id", type: "string", flag: ["persistent", "fkey"]},
|
|
103
|
+
photo: {id: "photo", type: "string", flag: ["persistent", "fkey", "file"]},
|
|
104
|
+
yunos: {id: "yunos", type: "object", flag: ["hook"], hook: {yunos: "realm_id"}},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
describe("what goes back to treedb from a form", () => {
|
|
108
|
+
test("a writable column, an fkey and a file column go back", () => {
|
|
109
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yuno_name)).toBe(true);
|
|
110
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.realm_id)).toBe(true);
|
|
111
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.photo)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a read-only time and a hook stay: they do not survive the widget", () => {
|
|
115
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.date)).toBe(false);
|
|
116
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yunos)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the pkey goes back although it is not writable: it addresses the record", () => {
|
|
120
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.id)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a pkey2 goes back although it is not writable: it names the INSTANCE", () => {
|
|
124
|
+
expect(col_goes_back_to_treedb(YUNOS_DESC, COL.yuno_release)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a pkey2s declared as a bare string (the C literal) counts the same", () => {
|
|
128
|
+
expect(col_goes_back_to_treedb({pkey: "id", pkey2s: "yuno_release"}, COL.yuno_release)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("without pkey2s in the desc the same column stays read-only", () => {
|
|
132
|
+
expect(col_goes_back_to_treedb({pkey: "id"}, COL.yuno_release)).toBe(false);
|
|
133
|
+
expect(col_goes_back_to_treedb(null, COL.yuno_release)).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|