@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.
@@ -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 {yui_file_read, yui_files_manifest, yui_file_id_label} from "./yui_file_field.js";
100
- import {yui_asset_id} from "./yui_asset.js";
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 is the tooltip. Empty is SAID, not left
1839
- * blank -- a blank cell and a cell whose asset failed to load look the
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 id = yui_asset_id(cell.getValue());
1845
- if(!id) {
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
- ["span", {class: "FILE_CELL", title: id}, yui_file_id_label(id)]
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
+ }