@yuneta/gobj-ui 7.23.158 → 7.23.160
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 +78 -1
- package/dist/gobj-ui.cjs.js +987 -168
- package/dist/gobj-ui.es.js +987 -168
- package/package.json +1 -1
- package/src/c_yui_form.js +52 -1
- package/src/c_yui_treedb_schema.js +134 -10
- package/src/c_yui_treedb_topic_with_form.css +67 -0
- package/src/c_yui_treedb_topic_with_form.js +401 -9
- package/src/c_yui_treedb_topics.js +80 -0
- package/src/yui_asset.css +51 -0
- package/src/yui_asset.js +223 -3
- package/src/yui_asset.test.js +18 -0
- package/src/yui_file_field.css +28 -0
- package/src/yui_file_field.js +45 -1
- package/src/yui_graph_camera.js +64 -0
|
@@ -96,8 +96,12 @@ import {t} from "i18next";
|
|
|
96
96
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
97
97
|
|
|
98
98
|
import {plan_treedb_writes, READONLY_FORM_TOOLBAR} from "./treedb_write_plan.js";
|
|
99
|
-
import {
|
|
100
|
-
|
|
99
|
+
import {
|
|
100
|
+
yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
|
|
101
|
+
} from "./yui_file_field.js";
|
|
102
|
+
import {
|
|
103
|
+
yui_asset_ids, yui_asset_element, yui_asset_open_link, yui_asset_release
|
|
104
|
+
} from "./yui_asset.js";
|
|
101
105
|
import {delete_impact} from "./delete_impact.js";
|
|
102
106
|
|
|
103
107
|
import "./c_yui_treedb_topic_with_form.css";
|
|
@@ -249,6 +253,11 @@ let PRIVATE_DATA = {
|
|
|
249
253
|
cell_json_gobj: null, // hosted C_YUI_JSON child (while a cell is open)
|
|
250
254
|
cell_json_modal: null, // { close } handle of the cell dialog (phone)
|
|
251
255
|
cell_json_win: null, // C_YUI_WINDOW presenting it (laptop)
|
|
256
|
+
cell_file_box: null, // $box of the file popup (while a file cell is open)
|
|
257
|
+
cell_file_modal: null, // { close } handle of the file dialog (phone)
|
|
258
|
+
cell_file_win: null, // C_YUI_WINDOW presenting it (laptop)
|
|
259
|
+
_pending_assets: null, // req_id -> {target, col, id, slot, timer}
|
|
260
|
+
_asset_seq: 0, // correlation id of an asset request
|
|
252
261
|
};
|
|
253
262
|
|
|
254
263
|
let __gclass__ = null;
|
|
@@ -449,6 +458,8 @@ function mt_stop(gobj)
|
|
|
449
458
|
close_form_dialog(gobj);
|
|
450
459
|
close_schema_dialog(gobj);
|
|
451
460
|
close_cell_json_dialog(gobj);
|
|
461
|
+
close_cell_file_dialog(gobj);
|
|
462
|
+
drop_pending_assets(gobj);
|
|
452
463
|
table__destroy(gobj);
|
|
453
464
|
}
|
|
454
465
|
|
|
@@ -464,6 +475,8 @@ function mt_destroy(gobj)
|
|
|
464
475
|
close_form_dialog(gobj);
|
|
465
476
|
close_schema_dialog(gobj);
|
|
466
477
|
close_cell_json_dialog(gobj);
|
|
478
|
+
close_cell_file_dialog(gobj);
|
|
479
|
+
drop_pending_assets(gobj);
|
|
467
480
|
destroy_ui(gobj);
|
|
468
481
|
}
|
|
469
482
|
|
|
@@ -1309,9 +1322,31 @@ function create_tabulator(gobj)
|
|
|
1309
1322
|
* picture, because a picture is a `get-asset` per cell -- an
|
|
1310
1323
|
* action, with its own event and its own answer -- and a
|
|
1311
1324
|
* table of 50 rows would fire 50 of them at render time. The
|
|
1312
|
-
* id is what the cell can say honestly on its own
|
|
1325
|
+
* id is what the cell can say honestly on its own; a CLICK
|
|
1326
|
+
* asks for the bytes and shows them (EV_SHOW_CELL_FILE). */
|
|
1313
1327
|
case "file":
|
|
1314
1328
|
colFormatter = file_cell_formatter;
|
|
1329
|
+
cellClick = function(e, cell) {
|
|
1330
|
+
if(!e.target.closest('.FILE_CELL_LINK')) {
|
|
1331
|
+
return; /* an empty cell: nothing to show */
|
|
1332
|
+
}
|
|
1333
|
+
e.stopPropagation();
|
|
1334
|
+
let pkey = desc.pkey || "id";
|
|
1335
|
+
let row_id = cell.getData()[pkey];
|
|
1336
|
+
if(row_id === undefined || row_id === null) {
|
|
1337
|
+
log_error(
|
|
1338
|
+
`${gobj_short_name(gobj)}: row without pkey '${pkey}',` +
|
|
1339
|
+
` cannot open the file of '${cell.getField()}'`
|
|
1340
|
+
);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
gobj_send_event(
|
|
1344
|
+
gobj,
|
|
1345
|
+
"EV_SHOW_CELL_FILE",
|
|
1346
|
+
{row_id: row_id, col_id: cell.getField()},
|
|
1347
|
+
gobj
|
|
1348
|
+
);
|
|
1349
|
+
};
|
|
1315
1350
|
break;
|
|
1316
1351
|
case "object":
|
|
1317
1352
|
case "dict":
|
|
@@ -1835,21 +1870,51 @@ function table__destroy(gobj)
|
|
|
1835
1870
|
* What a `file` column shows in a table cell: the asset it names.
|
|
1836
1871
|
*
|
|
1837
1872
|
* A sha256 in full is 64 characters of noise in a column, so it is
|
|
1838
|
-
* shortened and the whole of it
|
|
1839
|
-
* blank -- a blank cell and a cell whose asset failed
|
|
1840
|
-
* same, and only one of them is normal.
|
|
1873
|
+
* shortened and the whole of it goes to the popup the click opens. Empty
|
|
1874
|
+
* is SAID, not left blank -- a blank cell and a cell whose asset failed
|
|
1875
|
+
* to load look the same, and only one of them is normal.
|
|
1876
|
+
*
|
|
1877
|
+
* The value arrives already turned into ids joined by ", "
|
|
1878
|
+
* (transform__treedb_value_2_table_value), and an array column names
|
|
1879
|
+
* several: the first is shown and the rest counted.
|
|
1841
1880
|
************************************************************/
|
|
1881
|
+
function file_cell_ids(value)
|
|
1882
|
+
{
|
|
1883
|
+
if(is_array(value)) {
|
|
1884
|
+
return yui_asset_ids(value);
|
|
1885
|
+
}
|
|
1886
|
+
return String(value || "").split(",")
|
|
1887
|
+
.map((s) => s.trim())
|
|
1888
|
+
.filter((s) => !!s)
|
|
1889
|
+
.flatMap((s) => yui_asset_ids(s));
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1842
1892
|
function file_cell_formatter(cell, formatterParams, onRendered)
|
|
1843
1893
|
{
|
|
1844
|
-
const
|
|
1845
|
-
if(!
|
|
1894
|
+
const ids = file_cell_ids(cell.getValue());
|
|
1895
|
+
if(!ids.length) {
|
|
1846
1896
|
const $empty = createElement2(
|
|
1847
1897
|
["span", {class: "FILE_CELL is-empty", "data-i18n": "no file"}, "no file"]
|
|
1848
1898
|
);
|
|
1849
1899
|
return $empty;
|
|
1850
1900
|
}
|
|
1901
|
+
const children = [
|
|
1902
|
+
['span', {class: 'FILE_CELL_ICON icon yi-eye'}],
|
|
1903
|
+
['span', {class: 'FILE_CELL_ID'}, yui_file_id_label(ids[0])]
|
|
1904
|
+
];
|
|
1905
|
+
if(ids.length > 1) {
|
|
1906
|
+
children.push(['span', {class: 'FILE_CELL_MORE'}, `+${ids.length - 1}`]);
|
|
1907
|
+
}
|
|
1908
|
+
/* A link and not a button, like the JSON cell beside it; named for
|
|
1909
|
+
* the eye and for a reader, through its key. */
|
|
1851
1910
|
return createElement2(
|
|
1852
|
-
[
|
|
1911
|
+
['a', {
|
|
1912
|
+
class: 'FILE_CELL FILE_CELL_LINK',
|
|
1913
|
+
title: t('show file'),
|
|
1914
|
+
'data-i18n-title': 'show file',
|
|
1915
|
+
'aria-label': t('show file'),
|
|
1916
|
+
'data-i18n-aria-label': 'show file'
|
|
1917
|
+
}, children]
|
|
1853
1918
|
);
|
|
1854
1919
|
}
|
|
1855
1920
|
|
|
@@ -2243,6 +2308,10 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2243
2308
|
gobj_start(form); // mt_start loads `record` into the form
|
|
2244
2309
|
refresh_language($body, t); // translate anything the form emitted on start
|
|
2245
2310
|
|
|
2311
|
+
/* The form shows the asset a `file` column names only once the host
|
|
2312
|
+
* has fetched it -- asking the backend is this gclass's action. */
|
|
2313
|
+
request_form_previews(gobj, record);
|
|
2314
|
+
|
|
2246
2315
|
/* Focus the pkey unless the mode made it readonly (update) —
|
|
2247
2316
|
* then the first editable field. */
|
|
2248
2317
|
let $with_focus = $body.querySelector('.with-focus:not([readonly])');
|
|
@@ -2573,6 +2642,256 @@ function close_cell_json_dialog(gobj)
|
|
|
2573
2642
|
teardown_cell_json_child(gobj);
|
|
2574
2643
|
}
|
|
2575
2644
|
|
|
2645
|
+
/************************************************************
|
|
2646
|
+
* Ask the host for the bytes of ONE asset.
|
|
2647
|
+
*
|
|
2648
|
+
* Same contract as the page requests: the transport belongs to the
|
|
2649
|
+
* HOST, so the request goes up as an event and the answer comes back
|
|
2650
|
+
* down as one, correlated by a `req_id` the host echoes. What is parked
|
|
2651
|
+
* here is WHERE the answer goes -- the form's preview of a column, or a
|
|
2652
|
+
* slot of the file popup -- and the watchdog, because a request whose
|
|
2653
|
+
* answer never lands would leave a "loading" on screen for ever.
|
|
2654
|
+
*
|
|
2655
|
+
* `target` is kept here and never travels: it holds a gobj and a DOM
|
|
2656
|
+
* node, and a kw is plain json.
|
|
2657
|
+
************************************************************/
|
|
2658
|
+
const ASSET_TIMEOUT_MS = 30000;
|
|
2659
|
+
|
|
2660
|
+
function request_asset(gobj, asset_id, target)
|
|
2661
|
+
{
|
|
2662
|
+
let priv = gobj.priv;
|
|
2663
|
+
if(!priv._pending_assets) {
|
|
2664
|
+
priv._pending_assets = {};
|
|
2665
|
+
}
|
|
2666
|
+
let req_id = `a${++priv._asset_seq}`;
|
|
2667
|
+
|
|
2668
|
+
let timer = window.setTimeout(function() {
|
|
2669
|
+
gobj_send_event(gobj, "EV_ASSET_FAILED",
|
|
2670
|
+
{req_id: req_id, error: "asset request timed out"}, gobj);
|
|
2671
|
+
}, ASSET_TIMEOUT_MS);
|
|
2672
|
+
|
|
2673
|
+
priv._pending_assets[req_id] = Object.assign({id: asset_id, timer: timer}, target);
|
|
2674
|
+
|
|
2675
|
+
gobj_publish_event(
|
|
2676
|
+
gobj,
|
|
2677
|
+
"EV_REQUEST_ASSET",
|
|
2678
|
+
{
|
|
2679
|
+
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
2680
|
+
req_id: req_id,
|
|
2681
|
+
asset_id: asset_id
|
|
2682
|
+
}
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/************************************************************
|
|
2687
|
+
* Take a parked asset request out, however it ended. Null when
|
|
2688
|
+
* it was already settled (a late answer after a timeout).
|
|
2689
|
+
************************************************************/
|
|
2690
|
+
function take_pending_asset(gobj, req_id)
|
|
2691
|
+
{
|
|
2692
|
+
let priv = gobj.priv;
|
|
2693
|
+
let pend = priv._pending_assets? priv._pending_assets[req_id] : null;
|
|
2694
|
+
if(!pend) {
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
delete priv._pending_assets[req_id];
|
|
2698
|
+
if(pend.timer) {
|
|
2699
|
+
window.clearTimeout(pend.timer);
|
|
2700
|
+
}
|
|
2701
|
+
return pend;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
/************************************************************
|
|
2705
|
+
* Forget every parked asset request: the view is going away,
|
|
2706
|
+
* and a watchdog firing into a stopped gobj is an error for
|
|
2707
|
+
* nothing.
|
|
2708
|
+
************************************************************/
|
|
2709
|
+
function drop_pending_assets(gobj)
|
|
2710
|
+
{
|
|
2711
|
+
let priv = gobj.priv;
|
|
2712
|
+
if(priv._pending_assets) {
|
|
2713
|
+
for(const pend of Object.values(priv._pending_assets)) {
|
|
2714
|
+
if(pend.timer) {
|
|
2715
|
+
window.clearTimeout(pend.timer);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
priv._pending_assets = null;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/************************************************************
|
|
2723
|
+
* Ask for the asset of every `file` column the record names,
|
|
2724
|
+
* for the form's preview. One per column: the form shows the
|
|
2725
|
+
* asset the column keeps, which is the first it names.
|
|
2726
|
+
************************************************************/
|
|
2727
|
+
function request_form_previews(gobj, record)
|
|
2728
|
+
{
|
|
2729
|
+
let priv = gobj.priv;
|
|
2730
|
+
if(!record || !priv.form) {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
let desc = gobj_read_attr(gobj, "desc");
|
|
2734
|
+
for(let col of desc.cols) {
|
|
2735
|
+
if(!col.id || col.id.charAt(0) === '_') {
|
|
2736
|
+
continue;
|
|
2737
|
+
}
|
|
2738
|
+
const field_desc = treedb_get_field_desc(col);
|
|
2739
|
+
if(field_desc.type !== "file" || field_desc.is_hidden) {
|
|
2740
|
+
continue;
|
|
2741
|
+
}
|
|
2742
|
+
let ids = file_cell_ids(record[col.id]);
|
|
2743
|
+
if(!ids.length) {
|
|
2744
|
+
continue; /* a column that names nothing: nothing to fetch */
|
|
2745
|
+
}
|
|
2746
|
+
request_asset(gobj, ids[0], {target: "form", form: priv.form, col: col.id});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
/************************************************************
|
|
2751
|
+
* Show what a `file` cell names -- every asset, when the
|
|
2752
|
+
* column is an array -- in the standardized adaptive dialog.
|
|
2753
|
+
*
|
|
2754
|
+
* Unlike the JSON cell this is NOT offline: the row holds the
|
|
2755
|
+
* ids and the bytes are the backend's, so each slot opens as
|
|
2756
|
+
* "loading" and one `get-asset` per id fills it.
|
|
2757
|
+
************************************************************/
|
|
2758
|
+
function open_cell_file_dialog(gobj, row_id, col_id)
|
|
2759
|
+
{
|
|
2760
|
+
close_cell_file_dialog(gobj); // only one at a time
|
|
2761
|
+
|
|
2762
|
+
let priv = gobj.priv;
|
|
2763
|
+
|
|
2764
|
+
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2765
|
+
if(!tabulator) {
|
|
2766
|
+
log_error(`${gobj_short_name(gobj)}: no table, no file cell to show`);
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
let row = tabulator.getRow(row_id);
|
|
2770
|
+
if(!row) {
|
|
2771
|
+
log_error(`${gobj_short_name(gobj)}: row '${row_id}' not found in the table`);
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
let ids = file_cell_ids(row.getData()[col_id]);
|
|
2775
|
+
if(!ids.length) {
|
|
2776
|
+
log_error(`${gobj_short_name(gobj)}: row '${row_id}' col '${col_id}' names no file`);
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/* The id is DATA, shown whole: it is what a person pastes into a
|
|
2781
|
+
* command or compares, and the cell could only show it cut. */
|
|
2782
|
+
let $items = ids.map((id) => createElement2(
|
|
2783
|
+
['div', {class: 'CELL_FILE_ITEM'}, [
|
|
2784
|
+
['div', {class: 'CELL_FILE_SLOT'}, [
|
|
2785
|
+
['span', {class: 'CELL_FILE_LOADING', i18n: 'loading'}, 'loading']
|
|
2786
|
+
]],
|
|
2787
|
+
['div', {class: 'CELL_FILE_CAPTION'}, [
|
|
2788
|
+
['span', {class: 'CELL_FILE_ID'}, id]
|
|
2789
|
+
]]
|
|
2790
|
+
]]
|
|
2791
|
+
));
|
|
2792
|
+
let $box = createElement2(['div', {class: 'TREEDB_CELL_FILE'}, $items]);
|
|
2793
|
+
|
|
2794
|
+
/* Title in two halves, same contract as the JSON cell: the record id
|
|
2795
|
+
* is DATA and the column carries its own i18n key. */
|
|
2796
|
+
let presented = present_json_popup(gobj, $box, {
|
|
2797
|
+
name: "filewin_" + clean_name(gobj_name(gobj)),
|
|
2798
|
+
logical_class: "TREEDB_CELL_FILE",
|
|
2799
|
+
title_prefix: String(row_id),
|
|
2800
|
+
title: col_id,
|
|
2801
|
+
icon: "yi-eye",
|
|
2802
|
+
forget_window: function() {
|
|
2803
|
+
priv.cell_file_win = null;
|
|
2804
|
+
},
|
|
2805
|
+
on_close: function() {
|
|
2806
|
+
teardown_cell_file(gobj);
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
if(!presented) {
|
|
2810
|
+
return; // Error already logged
|
|
2811
|
+
}
|
|
2812
|
+
priv.cell_file_box = $box;
|
|
2813
|
+
priv.cell_file_modal = presented.modal;
|
|
2814
|
+
priv.cell_file_win = presented.win;
|
|
2815
|
+
|
|
2816
|
+
ids.forEach((id, i) => {
|
|
2817
|
+
request_asset(gobj, id, {target: "popup", box: $box, slot: i});
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/************************************************************
|
|
2822
|
+
* Fill one slot of the file popup with what came back.
|
|
2823
|
+
************************************************************/
|
|
2824
|
+
function fill_cell_file_slot(gobj, pend, answer, error)
|
|
2825
|
+
{
|
|
2826
|
+
let priv = gobj.priv;
|
|
2827
|
+
if(!priv.cell_file_box || priv.cell_file_box !== pend.box) {
|
|
2828
|
+
return; /* that popup was closed or replaced: nobody to show it to */
|
|
2829
|
+
}
|
|
2830
|
+
let $item = pend.box.querySelectorAll('.CELL_FILE_ITEM')[pend.slot];
|
|
2831
|
+
let $slot = $item? $item.querySelector('.CELL_FILE_SLOT') : null;
|
|
2832
|
+
let $caption = $item? $item.querySelector('.CELL_FILE_CAPTION') : null;
|
|
2833
|
+
if(!$slot || !$caption) {
|
|
2834
|
+
log_error(`${gobj_short_name(gobj)}: file popup has no slot ${pend.slot}`);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
yui_asset_release($slot);
|
|
2839
|
+
$slot.replaceChildren(yui_asset_element(error? null: answer, {
|
|
2840
|
+
detail: error || pend.id,
|
|
2841
|
+
alt: pend.id,
|
|
2842
|
+
class: 'CELL_FILE_MEDIA'
|
|
2843
|
+
}));
|
|
2844
|
+
refresh_language($slot, t);
|
|
2845
|
+
|
|
2846
|
+
if(!error && answer) {
|
|
2847
|
+
let meta = [answer.content_type, yui_file_size_label(answer.size)]
|
|
2848
|
+
.filter((s) => !!s).join(" · ");
|
|
2849
|
+
if(meta) {
|
|
2850
|
+
$caption.appendChild(
|
|
2851
|
+
createElement2(['span', {class: 'CELL_FILE_META'}, meta])
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
/* Full size, in the browser's own viewer: a photo is read at its
|
|
2855
|
+
* size, not at the popup's. */
|
|
2856
|
+
let $open = yui_asset_open_link(answer);
|
|
2857
|
+
if($open) {
|
|
2858
|
+
$caption.appendChild($open);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
/************************************************************
|
|
2864
|
+
* Retire the file popup. Called from its on_close -- the
|
|
2865
|
+
* shell or the window has already removed the DOM. The blob
|
|
2866
|
+
* urls it made are revoked; answers still in flight for it
|
|
2867
|
+
* land on nobody (fill_cell_file_slot checks the box).
|
|
2868
|
+
************************************************************/
|
|
2869
|
+
function teardown_cell_file(gobj)
|
|
2870
|
+
{
|
|
2871
|
+
let priv = gobj.priv;
|
|
2872
|
+
yui_asset_release(priv.cell_file_box);
|
|
2873
|
+
priv.cell_file_box = null;
|
|
2874
|
+
let win = priv.cell_file_win;
|
|
2875
|
+
priv.cell_file_win = null;
|
|
2876
|
+
retire_popup_window(win);
|
|
2877
|
+
priv.cell_file_modal = null;
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/************************************************************
|
|
2881
|
+
* Close the file popup (teardown, or a second open).
|
|
2882
|
+
************************************************************/
|
|
2883
|
+
function close_cell_file_dialog(gobj)
|
|
2884
|
+
{
|
|
2885
|
+
let priv = gobj.priv;
|
|
2886
|
+
if(priv.cell_file_modal) {
|
|
2887
|
+
let modal = priv.cell_file_modal;
|
|
2888
|
+
priv.cell_file_modal = null;
|
|
2889
|
+
modal.close(); // -> on_close -> teardown_cell_file
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
teardown_cell_file(gobj);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2576
2895
|
/************************************************************
|
|
2577
2896
|
* True if the topic's pkey col carries the "rowid" flag.
|
|
2578
2897
|
************************************************************/
|
|
@@ -3836,6 +4155,72 @@ function ac_show_cell_json(gobj, event, kw, src)
|
|
|
3836
4155
|
return 0;
|
|
3837
4156
|
}
|
|
3838
4157
|
|
|
4158
|
+
/************************************************************
|
|
4159
|
+
* Show the asset(s) one `file` cell names
|
|
4160
|
+
* {
|
|
4161
|
+
* row_id:
|
|
4162
|
+
* col_id:
|
|
4163
|
+
* }
|
|
4164
|
+
************************************************************/
|
|
4165
|
+
function ac_show_cell_file(gobj, event, kw, src)
|
|
4166
|
+
{
|
|
4167
|
+
open_cell_file_dialog(gobj, kw.row_id, kw.col_id);
|
|
4168
|
+
|
|
4169
|
+
return 0;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
/************************************************************
|
|
4173
|
+
* Hand an asset answer -- or why there is none -- to whoever
|
|
4174
|
+
* asked for it, if it is still there to take it.
|
|
4175
|
+
************************************************************/
|
|
4176
|
+
function deliver_asset(gobj, pend, answer, error)
|
|
4177
|
+
{
|
|
4178
|
+
let priv = gobj.priv;
|
|
4179
|
+
if(pend.target === "form") {
|
|
4180
|
+
if(!priv.form || priv.form !== pend.form) {
|
|
4181
|
+
return; /* that form was closed: nobody to show it to */
|
|
4182
|
+
}
|
|
4183
|
+
gobj_send_event(priv.form, "EV_SET_FILE_PREVIEW", {
|
|
4184
|
+
name: pend.col,
|
|
4185
|
+
id: pend.id,
|
|
4186
|
+
answer: answer,
|
|
4187
|
+
error: error
|
|
4188
|
+
}, gobj);
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
fill_cell_file_slot(gobj, pend, answer, error);
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
/************************************************************
|
|
4195
|
+
* The host answered an asset.
|
|
4196
|
+
* {req_id, answer}
|
|
4197
|
+
************************************************************/
|
|
4198
|
+
function ac_asset_loaded(gobj, event, kw, src)
|
|
4199
|
+
{
|
|
4200
|
+
let pend = take_pending_asset(gobj, kw.req_id);
|
|
4201
|
+
if(!pend) {
|
|
4202
|
+
return 0; /* already settled: a late answer after a timeout */
|
|
4203
|
+
}
|
|
4204
|
+
deliver_asset(gobj, pend, kw.answer || null, "");
|
|
4205
|
+
return 0;
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
/************************************************************
|
|
4209
|
+
* The host could not answer it, or nobody did in time.
|
|
4210
|
+
* {req_id, error}
|
|
4211
|
+
************************************************************/
|
|
4212
|
+
function ac_asset_failed(gobj, event, kw, src)
|
|
4213
|
+
{
|
|
4214
|
+
let why = (kw && kw.error) || "asset request failed";
|
|
4215
|
+
let pend = take_pending_asset(gobj, kw.req_id);
|
|
4216
|
+
log_error(`${gobj_short_name(gobj)}: asset '${pend? pend.id : kw.req_id}': ${why}`);
|
|
4217
|
+
if(!pend) {
|
|
4218
|
+
return 0; /* already settled */
|
|
4219
|
+
}
|
|
4220
|
+
deliver_asset(gobj, pend, null, why);
|
|
4221
|
+
return 0;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
3839
4224
|
/************************************************************
|
|
3840
4225
|
* One cell of one record changed. Publish it UP as a field write;
|
|
3841
4226
|
* the host owns the treedb command and its options.
|
|
@@ -4336,6 +4721,9 @@ function create_gclass(gclass_name)
|
|
|
4336
4721
|
["EV_PASTE_ROWS", ac_paste_rows, null],
|
|
4337
4722
|
["EV_SHOW_HOOK_DATA", ac_show_hook_data, null],
|
|
4338
4723
|
["EV_SHOW_CELL_JSON", ac_show_cell_json, null],
|
|
4724
|
+
["EV_SHOW_CELL_FILE", ac_show_cell_file, null],
|
|
4725
|
+
["EV_ASSET_LOADED", ac_asset_loaded, null],
|
|
4726
|
+
["EV_ASSET_FAILED", ac_asset_failed, null],
|
|
4339
4727
|
["EV_CHANGE_LOCALE", ac_change_locale, null],
|
|
4340
4728
|
["EV_REFRESH", ac_refresh, null],
|
|
4341
4729
|
["EV_SHOW_SCHEMA", ac_show_schema, null],
|
|
@@ -4378,6 +4766,10 @@ function create_gclass(gclass_name)
|
|
|
4378
4766
|
["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
|
|
4379
4767
|
["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
|
|
4380
4768
|
["EV_SHOW_CELL_JSON", 0],
|
|
4769
|
+
["EV_SHOW_CELL_FILE", 0],
|
|
4770
|
+
["EV_ASSET_LOADED", 0],
|
|
4771
|
+
["EV_ASSET_FAILED", 0],
|
|
4772
|
+
["EV_REQUEST_ASSET", event_flag_t.EVF_OUTPUT_EVENT],
|
|
4381
4773
|
["EV_CHANGE_LOCALE", 0],
|
|
4382
4774
|
["EV_REFRESH", 0],
|
|
4383
4775
|
["EV_SHOW_SCHEMA", 0],
|
|
@@ -81,6 +81,7 @@ const attrs_table = [
|
|
|
81
81
|
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of output events"),
|
|
82
82
|
SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
|
|
83
83
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
|
|
84
|
+
SDATA(data_type_t.DTP_STRING, "assets_service", 0, "assets", "Remote service that answers `get-asset` (a C_ASSETS) for the bytes of the treedb's `file` columns: the form's preview and the table's file popup ask it. Empty: nothing is asked, and a file cell opens a marker saying the asset is not available"),
|
|
84
85
|
SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
|
|
85
86
|
SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. ⚠️ DO NOT TURN THIS ON: a partial topic BREAKS LINKING -- the link picker of a record is built from the rows the PARENT topic's table holds, which with paging is one page, so the right parent is usually not offered and an existing link the form cannot match is dropped on save. Off in every consumer until linking asks the backend instead of the sibling table"),
|
|
86
87
|
SDATA(data_type_t.DTP_INTEGER, "page_size", 0, 0, "Rows per page of each topic table (the DISPLAY; the fetch is whole). 0 leaves the table's own default"),
|
|
@@ -1883,6 +1884,33 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
1883
1884
|
return 0;
|
|
1884
1885
|
}
|
|
1885
1886
|
|
|
1887
|
+
/*
|
|
1888
|
+
* An asset, for a form's preview or a file popup. Before the generic
|
|
1889
|
+
* error path too: an asset that cannot be served is SHOWN where it
|
|
1890
|
+
* was asked for (the missing marker, with the reason), not raised as
|
|
1891
|
+
* an app-wide modal over the form that asked.
|
|
1892
|
+
*/
|
|
1893
|
+
if(command === "get-asset") {
|
|
1894
|
+
let asset_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
1895
|
+
let req_id = kw_get_str(gobj, kw_command, "req_id", "", 0);
|
|
1896
|
+
let gobj_topic_form = gobj_find_child(gobj, {
|
|
1897
|
+
__gobj_name__: `${gobj_name(gobj)}?${asset_topic}`
|
|
1898
|
+
});
|
|
1899
|
+
if(!gobj_topic_form || !req_id) {
|
|
1900
|
+
log_error(`${gobj_short_name(gobj)}: get-asset answer for nobody: ` +
|
|
1901
|
+
`topic '${asset_topic}', req_id '${req_id}'`);
|
|
1902
|
+
return 0;
|
|
1903
|
+
}
|
|
1904
|
+
if(result < 0) {
|
|
1905
|
+
gobj_send_event(gobj_topic_form, "EV_ASSET_FAILED",
|
|
1906
|
+
{req_id: req_id, error: comment || "get-asset failed"}, gobj);
|
|
1907
|
+
} else {
|
|
1908
|
+
gobj_send_event(gobj_topic_form, "EV_ASSET_LOADED",
|
|
1909
|
+
{req_id: req_id, answer: data}, gobj);
|
|
1910
|
+
}
|
|
1911
|
+
return 0;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1886
1914
|
if(result < 0) {
|
|
1887
1915
|
if(command === "descs") {
|
|
1888
1916
|
/* The schema couldn't load (not a treedb, no authz for it, backend
|
|
@@ -2356,6 +2384,56 @@ function ac_request_page(gobj, event, kw, src)
|
|
|
2356
2384
|
return 0;
|
|
2357
2385
|
}
|
|
2358
2386
|
|
|
2387
|
+
/********************************************
|
|
2388
|
+
* Event from formtable: the bytes of one asset a `file` column names,
|
|
2389
|
+
* for the form's preview or the table's file popup.
|
|
2390
|
+
*
|
|
2391
|
+
* Asked to the ASSETS service and not to the treedb: storing is the
|
|
2392
|
+
* treedb's, the way out is C_ASSETS's (a signed url when a web server
|
|
2393
|
+
* sits in front of the store, the bytes inline when there is none). The
|
|
2394
|
+
* answer goes back down to the table that asked, by `req_id`.
|
|
2395
|
+
*
|
|
2396
|
+
* A read: a read-only treedb still answers it.
|
|
2397
|
+
********************************************/
|
|
2398
|
+
function ac_request_asset(gobj, event, kw, src)
|
|
2399
|
+
{
|
|
2400
|
+
if(!kw.req_id || !kw.asset_id) {
|
|
2401
|
+
log_error(`${gobj_short_name(gobj)}: an asset request with no id`);
|
|
2402
|
+
return -1;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
let assets_service = gobj_read_str_attr(gobj, "assets_service");
|
|
2406
|
+
let gobj_remote_yuno = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
2407
|
+
if(!assets_service || !gobj_remote_yuno) {
|
|
2408
|
+
gobj_send_event(src, "EV_ASSET_FAILED", {
|
|
2409
|
+
req_id: kw.req_id,
|
|
2410
|
+
error: assets_service? "cannot reach the backend": "no assets service"
|
|
2411
|
+
}, gobj);
|
|
2412
|
+
return 0;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
let ret = gobj_command(
|
|
2416
|
+
gobj_remote_yuno,
|
|
2417
|
+
"get-asset",
|
|
2418
|
+
{
|
|
2419
|
+
service: assets_service,
|
|
2420
|
+
asset_id: kw.asset_id,
|
|
2421
|
+
__md_command__: { // Data to be returned
|
|
2422
|
+
topic_name: kw.topic_name || gobj_read_attr(src, "topic_name"),
|
|
2423
|
+
req_id: kw.req_id
|
|
2424
|
+
}
|
|
2425
|
+
},
|
|
2426
|
+
gobj
|
|
2427
|
+
);
|
|
2428
|
+
if(ret) {
|
|
2429
|
+
log_error(ret);
|
|
2430
|
+
/* The command never went out, so no answer will ever come. */
|
|
2431
|
+
gobj_send_event(src, "EV_ASSET_FAILED",
|
|
2432
|
+
{req_id: kw.req_id, error: "cannot reach the backend"}, gobj);
|
|
2433
|
+
}
|
|
2434
|
+
return 0;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2359
2437
|
/********************************************
|
|
2360
2438
|
* Event from formtable: ONE field of one record, edited in place.
|
|
2361
2439
|
*
|
|
@@ -2597,6 +2675,7 @@ function create_gclass(gclass_name)
|
|
|
2597
2675
|
["EV_UPDATE_RECORD", ac_update_record, null],
|
|
2598
2676
|
["EV_UPDATE_FIELD", ac_update_field, null],
|
|
2599
2677
|
["EV_REQUEST_PAGE", ac_request_page, null],
|
|
2678
|
+
["EV_REQUEST_ASSET", ac_request_asset, null],
|
|
2600
2679
|
["EV_DELETE_RECORD", ac_delete_record, null],
|
|
2601
2680
|
["EV_REFRESH_TOPIC", ac_refresh_topic, null],
|
|
2602
2681
|
["EV_OPEN_JSON", ac_open_json, null],
|
|
@@ -2626,6 +2705,7 @@ function create_gclass(gclass_name)
|
|
|
2626
2705
|
["EV_UPDATE_RECORD", 0],
|
|
2627
2706
|
["EV_UPDATE_FIELD", 0],
|
|
2628
2707
|
["EV_REQUEST_PAGE", 0],
|
|
2708
|
+
["EV_REQUEST_ASSET", 0],
|
|
2629
2709
|
["EV_DELETE_RECORD", 0],
|
|
2630
2710
|
["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
|
|
2631
2711
|
event_flag_t.EVF_NO_WARN_SUBS],
|
package/src/yui_asset.css
CHANGED
|
@@ -41,3 +41,54 @@
|
|
|
41
41
|
max-width: 12rem;
|
|
42
42
|
overflow-wrap: anywhere;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* A PDF, in the browser's own viewer. It has no height of its own, so it
|
|
47
|
+
* gets one; a host that wants it bigger sizes it through its own class.
|
|
48
|
+
*/
|
|
49
|
+
.ASSET_DOCUMENT {
|
|
50
|
+
display: block;
|
|
51
|
+
width: 100%;
|
|
52
|
+
min-height: 24rem;
|
|
53
|
+
border: 1px solid var(--bulma-border, #dbdbdb);
|
|
54
|
+
border-radius: var(--bulma-radius, 4px);
|
|
55
|
+
background: var(--bulma-scheme-main, #fff);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* A kind nothing here can draw: what it is, and the way to open it. The
|
|
60
|
+
* same footprint as the missing marker, and solid where that one is
|
|
61
|
+
* dashed -- this asset IS there.
|
|
62
|
+
*/
|
|
63
|
+
.ASSET_OTHER {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
gap: 0.35rem;
|
|
69
|
+
min-width: 5rem;
|
|
70
|
+
min-height: 5rem;
|
|
71
|
+
padding: 0.75rem;
|
|
72
|
+
border: 1px solid var(--bulma-border, #dbdbdb);
|
|
73
|
+
border-radius: var(--bulma-radius, 4px);
|
|
74
|
+
text-align: center;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ASSET_OTHER_TYPE {
|
|
78
|
+
font-family: monospace;
|
|
79
|
+
font-size: 0.85rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ASSET_OTHER_DETAIL {
|
|
83
|
+
font-size: 0.75rem;
|
|
84
|
+
opacity: 0.75;
|
|
85
|
+
max-width: 16rem;
|
|
86
|
+
overflow-wrap: anywhere;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ASSET_OPEN {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 0.25rem;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
}
|