flexdesk 0.16.1 → 0.17.0
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/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3146 -164
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
package/dist/tiles.js
CHANGED
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
tile_registry_exports
|
|
13
13
|
} from "./chunk-FOOS3T5L.js";
|
|
14
14
|
import {
|
|
15
|
-
ActionDropdown
|
|
16
|
-
|
|
15
|
+
ActionDropdown,
|
|
16
|
+
SlideOutPanel
|
|
17
|
+
} from "./chunk-4A7SJC3T.js";
|
|
17
18
|
import {
|
|
18
19
|
showContextMenu
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import
|
|
20
|
+
} from "./chunk-UWO43BZU.js";
|
|
21
|
+
import {
|
|
22
|
+
openModal
|
|
23
|
+
} from "./chunk-D3COLZZ2.js";
|
|
21
24
|
import {
|
|
22
25
|
buildHistogramTrace,
|
|
23
26
|
buildLegendLayout,
|
|
@@ -32,10 +35,13 @@ import {
|
|
|
32
35
|
purge,
|
|
33
36
|
resize,
|
|
34
37
|
strideDownsampleShared
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
import "./chunk-
|
|
37
|
-
import
|
|
38
|
-
|
|
38
|
+
} from "./chunk-MCIXFNVF.js";
|
|
39
|
+
import "./chunk-Q36EPT2R.js";
|
|
40
|
+
import {
|
|
41
|
+
isHexColor
|
|
42
|
+
} from "./chunk-CVMCZZX2.js";
|
|
43
|
+
import "./chunk-HNU6MM2B.js";
|
|
44
|
+
import "./chunk-WVFGV5FT.js";
|
|
39
45
|
import {
|
|
40
46
|
__toCommonJS
|
|
41
47
|
} from "./chunk-JYWURG5T.js";
|
|
@@ -75,6 +81,11 @@ function validateField(value, field) {
|
|
|
75
81
|
return { valid: false, error: `${field.label} must be selected` };
|
|
76
82
|
}
|
|
77
83
|
break;
|
|
84
|
+
case FieldTypes.COLOR:
|
|
85
|
+
if (value != null && value !== "" && !isHexColor(value)) {
|
|
86
|
+
return { valid: false, error: `${field.label} must be a hex colour (#rgb or #rrggbb)` };
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
78
89
|
case FieldTypes.MULTISELECT:
|
|
79
90
|
if (field.required && (!Array.isArray(value) || value.length === 0)) {
|
|
80
91
|
return { valid: false, error: `${field.label} must have at least one selection` };
|
|
@@ -710,7 +721,7 @@ var TileBase = class {
|
|
|
710
721
|
* @private
|
|
711
722
|
*/
|
|
712
723
|
_onConfigClick() {
|
|
713
|
-
this.element?.closest(".tile-grid")?.querySelectorAll(".tile.tile--selected").forEach((
|
|
724
|
+
this.element?.closest(".tile-grid")?.querySelectorAll(".tile.tile--selected").forEach((el3) => el3.classList.remove("tile--selected"));
|
|
714
725
|
this.element?.classList.add("tile--selected");
|
|
715
726
|
this.eventBus?.emit?.("tile:config-request", {
|
|
716
727
|
tileId: this.id,
|
|
@@ -922,6 +933,234 @@ var TileBase = class {
|
|
|
922
933
|
}
|
|
923
934
|
};
|
|
924
935
|
|
|
936
|
+
// src/tiles/figure_tile.js
|
|
937
|
+
var FIGURE_CONFIG = Object.freeze({
|
|
938
|
+
scrollZoom: false,
|
|
939
|
+
displayModeBar: false,
|
|
940
|
+
responsive: true
|
|
941
|
+
});
|
|
942
|
+
var DOWNLOAD_PNG = Object.freeze({ action: "download-png", label: "Download PNG", icon: "image" });
|
|
943
|
+
var FigureTile = class extends TileBase {
|
|
944
|
+
static TYPE = "flexdesk.figure";
|
|
945
|
+
static TITLE = "Figure";
|
|
946
|
+
static EXPANDABLE = true;
|
|
947
|
+
/** The empty state's text when `figure()` answers `null`. */
|
|
948
|
+
static EMPTY_MESSAGE = "No data to show.";
|
|
949
|
+
/**
|
|
950
|
+
* @param {Object} options - TileBase's, plus:
|
|
951
|
+
* @param {Function} [options.onPointClick] - `({customdata, point, tile})`
|
|
952
|
+
* on a click on a mark. Also assignable later as `tile.onPointClick`.
|
|
953
|
+
*/
|
|
954
|
+
constructor(options = {}) {
|
|
955
|
+
super(options);
|
|
956
|
+
this.onPointClick = typeof options.onPointClick === "function" ? options.onPointClick : null;
|
|
957
|
+
this._plotEl = null;
|
|
958
|
+
this._drawn = false;
|
|
959
|
+
this._drawSeq = 0;
|
|
960
|
+
this._figure = null;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* The one override: what to draw for `data`.
|
|
964
|
+
* @param {*} _data
|
|
965
|
+
* @returns {{traces: Array, layout?: Object, config?: Object, frames?: Array}|null}
|
|
966
|
+
*/
|
|
967
|
+
figure(_data) {
|
|
968
|
+
throw new Error(`${this.constructor.name}.figure() must be implemented by the subclass`);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Hand this tile its data. Unlike `update()`, an empty or null value is
|
|
972
|
+
* drawn — as the empty state — rather than ignored.
|
|
973
|
+
*/
|
|
974
|
+
setData(data) {
|
|
975
|
+
if (this._disposed) return;
|
|
976
|
+
this.data = data ?? null;
|
|
977
|
+
if (this.contentElement) this.render(this.data);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Kick off the draw and return. Deliberately not `async`: nothing awaits
|
|
981
|
+
* `render`, so a returned promise would be a rejection nobody catches.
|
|
982
|
+
*/
|
|
983
|
+
render(data) {
|
|
984
|
+
this._draw(data);
|
|
985
|
+
}
|
|
986
|
+
/** One relayout when a resize settles; nothing while a draw is in flight. */
|
|
987
|
+
onResize() {
|
|
988
|
+
if (!this._plotEl || !this._drawn) return;
|
|
989
|
+
resize(this._plotEl).catch(() => {
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
menuItems() {
|
|
993
|
+
return [...super.menuItems(), ...this._figureMenuItems()];
|
|
994
|
+
}
|
|
995
|
+
/** The entries this class adds to the menu: Download PNG. For a subclass
|
|
996
|
+
* that wants them WITHOUT `TileBase`'s own. @protected */
|
|
997
|
+
_figureMenuItems() {
|
|
998
|
+
return [{ ...DOWNLOAD_PNG }];
|
|
999
|
+
}
|
|
1000
|
+
_onMenuAction(action) {
|
|
1001
|
+
if (action === DOWNLOAD_PNG.action) this._downloadPng();
|
|
1002
|
+
else super._onMenuAction(action);
|
|
1003
|
+
}
|
|
1004
|
+
/** The last figure drawn, for the expand window; `null` while there is none. */
|
|
1005
|
+
getExpandData() {
|
|
1006
|
+
const fig = this._figure;
|
|
1007
|
+
if (!fig) return null;
|
|
1008
|
+
return {
|
|
1009
|
+
title: this.config.title || this.constructor.TITLE,
|
|
1010
|
+
// Copies: Plotly writes onto the trace and layout objects it draws.
|
|
1011
|
+
traces: fig.traces.map((t) => ({ ...t })),
|
|
1012
|
+
layout: { ...fig.layout || {} },
|
|
1013
|
+
frames: Array.isArray(fig.frames) ? fig.frames : void 0
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
// ── the three states: each one invalidates the figure ───────────────
|
|
1017
|
+
showLoading() {
|
|
1018
|
+
this._invalidate();
|
|
1019
|
+
super.showLoading();
|
|
1020
|
+
}
|
|
1021
|
+
showEmpty(message) {
|
|
1022
|
+
this._invalidate();
|
|
1023
|
+
super.showEmpty(message);
|
|
1024
|
+
}
|
|
1025
|
+
showError(message) {
|
|
1026
|
+
this._invalidate();
|
|
1027
|
+
super.showError(message);
|
|
1028
|
+
}
|
|
1029
|
+
/** Release the figure BEFORE the element goes — `TileBase.dispose` nulls it. */
|
|
1030
|
+
dispose() {
|
|
1031
|
+
this._invalidate();
|
|
1032
|
+
super.dispose();
|
|
1033
|
+
}
|
|
1034
|
+
/** Any draw in flight is now stale, and the figure on screen is released. */
|
|
1035
|
+
_invalidate() {
|
|
1036
|
+
this._drawSeq += 1;
|
|
1037
|
+
this._releasePlot();
|
|
1038
|
+
}
|
|
1039
|
+
/** Hand the current figure back to Plotly and forget it. Idempotent, and
|
|
1040
|
+
* not awaited: a rejected purge (Plotly absent, already purged) must not
|
|
1041
|
+
* surface as an unhandled rejection. */
|
|
1042
|
+
_releasePlot() {
|
|
1043
|
+
const el3 = this._plotEl;
|
|
1044
|
+
this._plotEl = null;
|
|
1045
|
+
this._drawn = false;
|
|
1046
|
+
this._figure = null;
|
|
1047
|
+
if (el3) purge(el3).catch(() => {
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* The nodes the content holds while `plotEl` draws `fig`: the plot alone
|
|
1052
|
+
* here. A subclass adds what sits around the figure (a note, a caption).
|
|
1053
|
+
* @param {HTMLElement} plotEl
|
|
1054
|
+
* @param {Object} _fig - what `figure()` answered
|
|
1055
|
+
* @returns {HTMLElement[]}
|
|
1056
|
+
* @protected
|
|
1057
|
+
*/
|
|
1058
|
+
_figureContent(plotEl, _fig) {
|
|
1059
|
+
return [plotEl];
|
|
1060
|
+
}
|
|
1061
|
+
// ── the draw ────────────────────────────────────────────────────────
|
|
1062
|
+
async _draw(data) {
|
|
1063
|
+
this._drawSeq += 1;
|
|
1064
|
+
const seq = this._drawSeq;
|
|
1065
|
+
const content = this.contentElement;
|
|
1066
|
+
if (!content) return;
|
|
1067
|
+
const stale = () => this._disposed || seq !== this._drawSeq;
|
|
1068
|
+
this._releasePlot();
|
|
1069
|
+
let fig;
|
|
1070
|
+
try {
|
|
1071
|
+
fig = this.figure(data);
|
|
1072
|
+
if (fig != null && (typeof fig !== "object" || !Array.isArray(fig.traces))) {
|
|
1073
|
+
throw new Error("figure() must answer {traces, layout} or null");
|
|
1074
|
+
}
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
this._failed(err);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (!fig) {
|
|
1080
|
+
if (!stale()) this.showEmpty(this.constructor.EMPTY_MESSAGE);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const host = document.createElement("div");
|
|
1084
|
+
host.className = "fd-figure-tile";
|
|
1085
|
+
content.replaceChildren(...this._figureContent(host, fig));
|
|
1086
|
+
this._plotEl = host;
|
|
1087
|
+
this._drawn = false;
|
|
1088
|
+
try {
|
|
1089
|
+
const P = await ensurePlotly();
|
|
1090
|
+
if (stale()) return;
|
|
1091
|
+
await createChart(host, fig.traces, fig.layout || {}, { ...FIGURE_CONFIG, ...fig.config || {} });
|
|
1092
|
+
if (Array.isArray(fig.frames) && fig.frames.length && !stale()) {
|
|
1093
|
+
await P.addFrames(host, fig.frames);
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
if (stale()) return;
|
|
1097
|
+
this._failed(err);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (stale()) {
|
|
1101
|
+
purge(host).catch(() => {
|
|
1102
|
+
});
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
this._drawn = true;
|
|
1106
|
+
this._figure = fig;
|
|
1107
|
+
this._wireClick(host);
|
|
1108
|
+
}
|
|
1109
|
+
/** Plotly's click, reduced to the point's own `customdata`. A pie's point
|
|
1110
|
+
* carries its slice index as `pointNumber` (older builds: `pointIndex`),
|
|
1111
|
+
* and its `customdata` is read off the trace by that index. */
|
|
1112
|
+
_wireClick(host) {
|
|
1113
|
+
if (typeof host.on !== "function") return;
|
|
1114
|
+
host.on("plotly_click", (ev) => {
|
|
1115
|
+
const point = ev?.points?.[0];
|
|
1116
|
+
if (!point || typeof this.onPointClick !== "function") return;
|
|
1117
|
+
const trace = point.data || {};
|
|
1118
|
+
let customdata = point.customdata;
|
|
1119
|
+
if (trace.type === "pie" || customdata === void 0) {
|
|
1120
|
+
const i = point.pointNumber ?? point.pointIndex;
|
|
1121
|
+
if (Array.isArray(trace.customdata) && i != null) customdata = trace.customdata[i];
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
this.onPointClick({ customdata, point, tile: this });
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
console.error(`[${this.constructor.name}] onPointClick threw:`, err);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
async _downloadPng() {
|
|
1131
|
+
if (!this._plotEl || !this._drawn) return;
|
|
1132
|
+
try {
|
|
1133
|
+
const P = await ensurePlotly();
|
|
1134
|
+
const uri = await P.toImage(this._plotEl, { format: "png", scale: 2 });
|
|
1135
|
+
const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
|
|
1136
|
+
const dialogs = this.host?.dialogs;
|
|
1137
|
+
const result = dialogs ? await dialogs.saveFile({ data: uri.split(",")[1], filename, kind: "png" }) : null;
|
|
1138
|
+
if (result) {
|
|
1139
|
+
if (result.ok) this._notify("Download", `Chart saved to ${result.path}`, "success");
|
|
1140
|
+
else if (!result.cancelled) this._notify("Download failed", result.error || "The image could not be saved", "error");
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const a = document.createElement("a");
|
|
1144
|
+
a.href = uri;
|
|
1145
|
+
a.download = filename;
|
|
1146
|
+
a.click();
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
this._notify("Download failed", `The image could not be made: ${err?.message || err}`, "error");
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
_failed(err) {
|
|
1152
|
+
if (this._disposed) return;
|
|
1153
|
+
this.showError(`This chart could not be drawn: ${err?.message || err}`);
|
|
1154
|
+
this.eventBus?.emit?.("tile:render-failed", {
|
|
1155
|
+
tileId: this.id,
|
|
1156
|
+
message: err?.message || String(err)
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
function slug(text) {
|
|
1161
|
+
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "chart";
|
|
1162
|
+
}
|
|
1163
|
+
|
|
925
1164
|
// src/tiles/chart_tiles.js
|
|
926
1165
|
init_tile_registry();
|
|
927
1166
|
var CATEGORICAL_ONLY = /* @__PURE__ */ new Set(["pie", "donut", "funnel"]);
|
|
@@ -973,25 +1212,13 @@ function pointCount(s) {
|
|
|
973
1212
|
}
|
|
974
1213
|
return 0;
|
|
975
1214
|
}
|
|
976
|
-
var ChartTile = class extends
|
|
1215
|
+
var ChartTile = class extends FigureTile {
|
|
977
1216
|
static TYPE = "flexdesk.chart";
|
|
978
1217
|
static TITLE = "Chart";
|
|
979
1218
|
static ICON = "insert_chart";
|
|
980
1219
|
static DESCRIPTION = "Line, bar, area, pie, donut, funnel or histogram over series you supply.";
|
|
981
1220
|
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
982
1221
|
static SIZE_CONSTRAINTS = { minW: 3, minH: 3, maxW: 12, maxH: 10 };
|
|
983
|
-
/**
|
|
984
|
-
* @param {Object} options - TileBase's, plus:
|
|
985
|
-
* @param {Function} [options.onPointClick] - `({customdata, point, tile})`
|
|
986
|
-
* on a click on a mark. Also assignable later as `tile.onPointClick`.
|
|
987
|
-
*/
|
|
988
|
-
constructor(options = {}) {
|
|
989
|
-
super(options);
|
|
990
|
-
this.onPointClick = typeof options.onPointClick === "function" ? options.onPointClick : null;
|
|
991
|
-
this._plotEl = null;
|
|
992
|
-
this._drawn = false;
|
|
993
|
-
this._drawSeq = 0;
|
|
994
|
-
}
|
|
995
1222
|
/** Pure and static-only — it runs inside `TileBase`'s constructor. */
|
|
996
1223
|
getDefaultConfig() {
|
|
997
1224
|
return {
|
|
@@ -1011,21 +1238,21 @@ var ChartTile = class extends TileBase {
|
|
|
1011
1238
|
getConfigSchema() {
|
|
1012
1239
|
return {
|
|
1013
1240
|
fields: [
|
|
1014
|
-
{
|
|
1241
|
+
{ key: "title", type: "text", label: "Tile title" },
|
|
1015
1242
|
{
|
|
1016
|
-
|
|
1243
|
+
key: "chart_type",
|
|
1017
1244
|
type: "select",
|
|
1018
1245
|
label: "Chart type",
|
|
1019
1246
|
options: CHART_TILE_TYPES.map((o) => ({ ...o }))
|
|
1020
1247
|
},
|
|
1021
1248
|
{
|
|
1022
|
-
|
|
1249
|
+
key: "unit",
|
|
1023
1250
|
type: "text",
|
|
1024
1251
|
label: "Unit",
|
|
1025
|
-
|
|
1252
|
+
description: 'Shown with the value axis title, e.g. "pts" or "h".'
|
|
1026
1253
|
},
|
|
1027
1254
|
{
|
|
1028
|
-
|
|
1255
|
+
key: "legend",
|
|
1029
1256
|
type: "select",
|
|
1030
1257
|
label: "Legend",
|
|
1031
1258
|
visibleWhen: { field: "chart_type", in: [...READS.legend] },
|
|
@@ -1038,38 +1265,38 @@ var ChartTile = class extends TileBase {
|
|
|
1038
1265
|
]
|
|
1039
1266
|
},
|
|
1040
1267
|
{
|
|
1041
|
-
|
|
1268
|
+
key: "x_title",
|
|
1042
1269
|
type: "text",
|
|
1043
1270
|
label: "X axis title",
|
|
1044
1271
|
visibleWhen: { field: "chart_type", in: [...READS.axis_titles] }
|
|
1045
1272
|
},
|
|
1046
1273
|
{
|
|
1047
|
-
|
|
1274
|
+
key: "y_title",
|
|
1048
1275
|
type: "text",
|
|
1049
1276
|
label: "Y axis title",
|
|
1050
1277
|
visibleWhen: { field: "chart_type", in: [...READS.axis_titles] }
|
|
1051
1278
|
},
|
|
1052
1279
|
{
|
|
1053
|
-
|
|
1280
|
+
key: "y_scale",
|
|
1054
1281
|
type: "select",
|
|
1055
1282
|
label: "Y scale",
|
|
1056
1283
|
visibleWhen: { field: "chart_type", in: [...READS.y_scale] },
|
|
1057
1284
|
options: [{ value: "linear", label: "Linear" }, { value: "log", label: "Logarithmic" }]
|
|
1058
1285
|
},
|
|
1059
1286
|
{
|
|
1060
|
-
|
|
1287
|
+
key: "y_min",
|
|
1061
1288
|
type: "number",
|
|
1062
1289
|
label: "Y minimum",
|
|
1063
1290
|
visibleWhen: { field: "chart_type", in: [...READS.y_range] }
|
|
1064
1291
|
},
|
|
1065
1292
|
{
|
|
1066
|
-
|
|
1293
|
+
key: "y_max",
|
|
1067
1294
|
type: "number",
|
|
1068
1295
|
label: "Y maximum",
|
|
1069
1296
|
visibleWhen: { field: "chart_type", in: [...READS.y_range] }
|
|
1070
1297
|
},
|
|
1071
1298
|
{
|
|
1072
|
-
|
|
1299
|
+
key: "show_markers",
|
|
1073
1300
|
type: "checkbox",
|
|
1074
1301
|
label: "Show point markers",
|
|
1075
1302
|
visibleWhen: { field: "chart_type", in: [...READS.show_markers] }
|
|
@@ -1092,145 +1319,28 @@ var ChartTile = class extends TileBase {
|
|
|
1092
1319
|
super.update(data === null ? void 0 : data, config);
|
|
1093
1320
|
if (config) applyTitle(this);
|
|
1094
1321
|
}
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1097
|
-
* drawn — as the empty state — rather than ignored.
|
|
1098
|
-
*/
|
|
1099
|
-
setData(data) {
|
|
1100
|
-
if (this._disposed) return;
|
|
1101
|
-
this.data = data ?? null;
|
|
1102
|
-
if (this.contentElement) this.render(this.data);
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Kick off the draw and return. Deliberately not `async`: nothing awaits
|
|
1106
|
-
* `render`, so a returned promise would be a rejection nobody catches.
|
|
1107
|
-
*/
|
|
1108
|
-
render(data) {
|
|
1109
|
-
this._draw(data);
|
|
1110
|
-
}
|
|
1111
|
-
/** One relayout when a resize settles; nothing while a draw is in flight. */
|
|
1112
|
-
onResize() {
|
|
1113
|
-
if (!this._plotEl || !this._drawn) return;
|
|
1114
|
-
resize(this._plotEl).catch(() => {
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
menuItems() {
|
|
1118
|
-
return [{ action: "download-png", label: "Download PNG", icon: "image" }];
|
|
1119
|
-
}
|
|
1120
|
-
_onMenuAction(action) {
|
|
1121
|
-
if (action === "download-png") this._downloadPng();
|
|
1122
|
-
}
|
|
1123
|
-
// ── the three states: each one invalidates the figure ───────────────
|
|
1124
|
-
showLoading() {
|
|
1125
|
-
this._invalidate();
|
|
1126
|
-
super.showLoading();
|
|
1127
|
-
}
|
|
1128
|
-
showEmpty(message) {
|
|
1129
|
-
this._invalidate();
|
|
1130
|
-
super.showEmpty(message);
|
|
1131
|
-
}
|
|
1132
|
-
showError(message) {
|
|
1133
|
-
this._invalidate();
|
|
1134
|
-
super.showError(message);
|
|
1135
|
-
}
|
|
1136
|
-
/** Release the figure BEFORE the element goes — `TileBase.dispose` nulls it. */
|
|
1137
|
-
dispose() {
|
|
1138
|
-
this._invalidate();
|
|
1139
|
-
super.dispose();
|
|
1140
|
-
}
|
|
1141
|
-
/** Any draw in flight is now stale, and the figure on screen is released. */
|
|
1142
|
-
_invalidate() {
|
|
1143
|
-
this._drawSeq += 1;
|
|
1144
|
-
this._releasePlot();
|
|
1145
|
-
}
|
|
1146
|
-
/** Hand the current figure back to Plotly and forget it. Idempotent, and
|
|
1147
|
-
* not awaited: a rejected purge (Plotly absent, already purged) must not
|
|
1148
|
-
* surface as an unhandled rejection. */
|
|
1149
|
-
_releasePlot() {
|
|
1150
|
-
const el2 = this._plotEl;
|
|
1151
|
-
this._plotEl = null;
|
|
1152
|
-
this._drawn = false;
|
|
1153
|
-
if (el2) purge(el2).catch(() => {
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
// ── the draw ────────────────────────────────────────────────────────
|
|
1157
|
-
async _draw(data) {
|
|
1158
|
-
this._drawSeq += 1;
|
|
1159
|
-
const seq = this._drawSeq;
|
|
1160
|
-
const content = this.contentElement;
|
|
1161
|
-
if (!content) return;
|
|
1162
|
-
this._releasePlot();
|
|
1322
|
+
/** `null` (the empty state) when there is nothing to draw. */
|
|
1323
|
+
figure(data) {
|
|
1163
1324
|
const norm = normalizeChartData(data);
|
|
1164
|
-
if (!norm)
|
|
1165
|
-
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
let plan;
|
|
1169
|
-
try {
|
|
1170
|
-
plan = this._plan(norm);
|
|
1171
|
-
} catch (err) {
|
|
1172
|
-
this._failed(err);
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
if (!plan) return;
|
|
1176
|
-
const host = document.createElement("div");
|
|
1177
|
-
host.className = "fd-chart-tile";
|
|
1178
|
-
const parts = [host];
|
|
1179
|
-
if (norm.note) {
|
|
1180
|
-
const note = document.createElement("div");
|
|
1181
|
-
note.className = "fd-chart-tile__note";
|
|
1182
|
-
note.setAttribute("role", "note");
|
|
1183
|
-
note.textContent = norm.note;
|
|
1184
|
-
note.title = norm.note;
|
|
1185
|
-
parts.push(note);
|
|
1186
|
-
}
|
|
1187
|
-
content.replaceChildren(...parts);
|
|
1188
|
-
this._plotEl = host;
|
|
1189
|
-
this._drawn = false;
|
|
1190
|
-
try {
|
|
1191
|
-
await createChart(host, plan.traces, plan.layout, {
|
|
1192
|
-
scrollZoom: false,
|
|
1193
|
-
displayModeBar: false,
|
|
1194
|
-
responsive: true
|
|
1195
|
-
});
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
if (this._disposed || seq !== this._drawSeq) return;
|
|
1198
|
-
this._failed(err);
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
if (this._disposed || seq !== this._drawSeq) {
|
|
1202
|
-
purge(host).catch(() => {
|
|
1203
|
-
});
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
this._drawn = true;
|
|
1207
|
-
this._wireClick(host);
|
|
1208
|
-
}
|
|
1209
|
-
/** Plotly's click, reduced to the point's own `customdata`. A pie's point
|
|
1210
|
-
* carries its slice index as `pointNumber` (older builds: `pointIndex`),
|
|
1211
|
-
* and its `customdata` is read off the trace by that index. */
|
|
1212
|
-
_wireClick(host) {
|
|
1213
|
-
if (typeof host.on !== "function") return;
|
|
1214
|
-
host.on("plotly_click", (ev) => {
|
|
1215
|
-
const point = ev?.points?.[0];
|
|
1216
|
-
if (!point || typeof this.onPointClick !== "function") return;
|
|
1217
|
-
const trace = point.data || {};
|
|
1218
|
-
let customdata = point.customdata;
|
|
1219
|
-
if (trace.type === "pie" || customdata === void 0) {
|
|
1220
|
-
const i = point.pointNumber ?? point.pointIndex;
|
|
1221
|
-
if (Array.isArray(trace.customdata) && i != null) customdata = trace.customdata[i];
|
|
1222
|
-
}
|
|
1223
|
-
try {
|
|
1224
|
-
this.onPointClick({ customdata, point, tile: this });
|
|
1225
|
-
} catch (err) {
|
|
1226
|
-
console.error("[ChartTile] onPointClick threw:", err);
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1325
|
+
if (!norm) return null;
|
|
1326
|
+
return { ...this._plan(norm), note: norm.note };
|
|
1229
1327
|
}
|
|
1230
|
-
/**
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1328
|
+
/** Download PNG alone — see the header. */
|
|
1329
|
+
menuItems() {
|
|
1330
|
+
return this._figureMenuItems();
|
|
1331
|
+
}
|
|
1332
|
+
/** The figure, and the note on its own line under it. */
|
|
1333
|
+
_figureContent(plotEl, fig) {
|
|
1334
|
+
plotEl.classList.add("fd-chart-tile");
|
|
1335
|
+
if (!fig.note) return [plotEl];
|
|
1336
|
+
const note = document.createElement("div");
|
|
1337
|
+
note.className = "fd-chart-tile__note";
|
|
1338
|
+
note.setAttribute("role", "note");
|
|
1339
|
+
note.textContent = fig.note;
|
|
1340
|
+
note.title = fig.note;
|
|
1341
|
+
return [plotEl, note];
|
|
1342
|
+
}
|
|
1343
|
+
/** Turn normalised data into `{traces, layout}`. */
|
|
1234
1344
|
_plan(norm) {
|
|
1235
1345
|
const type = this.config.chart_type || "line";
|
|
1236
1346
|
const { series } = norm;
|
|
@@ -1364,35 +1474,6 @@ var ChartTile = class extends TileBase {
|
|
|
1364
1474
|
}
|
|
1365
1475
|
return out;
|
|
1366
1476
|
}
|
|
1367
|
-
async _downloadPng() {
|
|
1368
|
-
if (!this._plotEl || !this._drawn) return;
|
|
1369
|
-
try {
|
|
1370
|
-
const P = await ensurePlotly();
|
|
1371
|
-
const uri = await P.toImage(this._plotEl, { format: "png", scale: 2 });
|
|
1372
|
-
const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
|
|
1373
|
-
const dialogs = this.host?.dialogs;
|
|
1374
|
-
const result = dialogs ? await dialogs.saveFile({ data: uri.split(",")[1], filename, kind: "png" }) : null;
|
|
1375
|
-
if (result) {
|
|
1376
|
-
if (result.ok) this._notify("Download", `Chart saved to ${result.path}`, "success");
|
|
1377
|
-
else if (!result.cancelled) this._notify("Download failed", result.error || "The image could not be saved", "error");
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
const a = document.createElement("a");
|
|
1381
|
-
a.href = uri;
|
|
1382
|
-
a.download = filename;
|
|
1383
|
-
a.click();
|
|
1384
|
-
} catch (err) {
|
|
1385
|
-
this._notify("Download failed", `The image could not be made: ${err?.message || err}`, "error");
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
_failed(err) {
|
|
1389
|
-
if (this._disposed) return;
|
|
1390
|
-
this.showError(`This chart could not be drawn: ${err?.message || err}`);
|
|
1391
|
-
this.eventBus?.emit?.("tile:render-failed", {
|
|
1392
|
-
tileId: this.id,
|
|
1393
|
-
message: err?.message || String(err)
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
1477
|
};
|
|
1397
1478
|
var KpiTile = class extends TileBase {
|
|
1398
1479
|
static TYPE = "flexdesk.kpi";
|
|
@@ -1419,11 +1500,11 @@ var KpiTile = class extends TileBase {
|
|
|
1419
1500
|
getConfigSchema() {
|
|
1420
1501
|
return {
|
|
1421
1502
|
fields: [
|
|
1422
|
-
{
|
|
1423
|
-
{
|
|
1424
|
-
{
|
|
1503
|
+
{ key: "title", type: "text", label: "Tile title" },
|
|
1504
|
+
{ key: "label", type: "text", label: "Label" },
|
|
1505
|
+
{ key: "unit", type: "text", label: "Unit", description: 'Shown after the number, e.g. "pts".' },
|
|
1425
1506
|
{
|
|
1426
|
-
|
|
1507
|
+
key: "format",
|
|
1427
1508
|
type: "select",
|
|
1428
1509
|
label: "Format",
|
|
1429
1510
|
options: [
|
|
@@ -1434,23 +1515,23 @@ var KpiTile = class extends TileBase {
|
|
|
1434
1515
|
{ value: "text", label: "Text" }
|
|
1435
1516
|
]
|
|
1436
1517
|
},
|
|
1437
|
-
{
|
|
1518
|
+
{ key: "decimals", type: "number", label: "Decimals", min: 0, max: 6 },
|
|
1438
1519
|
{
|
|
1439
|
-
|
|
1520
|
+
key: "currency",
|
|
1440
1521
|
type: "text",
|
|
1441
1522
|
label: "Currency code",
|
|
1442
|
-
|
|
1523
|
+
description: "ISO 4217, e.g. EUR. Left empty, the number is shown without a symbol."
|
|
1443
1524
|
},
|
|
1444
|
-
{
|
|
1445
|
-
{
|
|
1446
|
-
{
|
|
1525
|
+
{ key: "prefix", type: "text", label: "Prefix" },
|
|
1526
|
+
{ key: "suffix", type: "text", label: "Suffix" },
|
|
1527
|
+
{ key: "delta_relative", type: "checkbox", label: "Show the change as a percentage" },
|
|
1447
1528
|
{
|
|
1448
|
-
|
|
1529
|
+
key: "invert_delta",
|
|
1449
1530
|
type: "checkbox",
|
|
1450
1531
|
label: "A fall is good",
|
|
1451
|
-
|
|
1532
|
+
description: "For a cost, a queue length or a latency: a smaller number is the better one."
|
|
1452
1533
|
},
|
|
1453
|
-
{
|
|
1534
|
+
{ key: "foot", type: "text", label: "Caption" }
|
|
1454
1535
|
]
|
|
1455
1536
|
};
|
|
1456
1537
|
}
|
|
@@ -1501,11 +1582,11 @@ var KpiTile = class extends TileBase {
|
|
|
1501
1582
|
if (delta) box.append(delta);
|
|
1502
1583
|
const foot = [this.config.foot, d.note].filter(Boolean).join(" \xB7 ");
|
|
1503
1584
|
if (foot) {
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
box.append(
|
|
1585
|
+
const el3 = document.createElement("div");
|
|
1586
|
+
el3.className = "fd-kpi__foot";
|
|
1587
|
+
el3.textContent = foot;
|
|
1588
|
+
el3.title = foot;
|
|
1589
|
+
box.append(el3);
|
|
1509
1590
|
}
|
|
1510
1591
|
content.append(box);
|
|
1511
1592
|
}
|
|
@@ -1517,19 +1598,19 @@ var KpiTile = class extends TileBase {
|
|
|
1517
1598
|
const reference = Number(rawReference);
|
|
1518
1599
|
if (!Number.isFinite(value) || !Number.isFinite(reference)) return null;
|
|
1519
1600
|
const change = value - reference;
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
if (change > 0)
|
|
1523
|
-
else if (change < 0)
|
|
1601
|
+
const el3 = document.createElement("div");
|
|
1602
|
+
el3.className = "fd-kpi__delta";
|
|
1603
|
+
if (change > 0) el3.classList.add(this.config.invert_delta ? "fd-kpi__delta--down" : "fd-kpi__delta--up");
|
|
1604
|
+
else if (change < 0) el3.classList.add(this.config.invert_delta ? "fd-kpi__delta--up" : "fd-kpi__delta--down");
|
|
1524
1605
|
const arrow = change > 0 ? "\u25B2" : change < 0 ? "\u25BC" : "\u2013";
|
|
1525
1606
|
const magnitude = this.config.delta_relative ? reference === 0 ? null : formatValue(Math.abs(change / reference), {
|
|
1526
1607
|
format: "percent",
|
|
1527
1608
|
decimals: this.config.decimals ?? 1
|
|
1528
1609
|
}) : this._format(Math.abs(change), false);
|
|
1529
1610
|
const sign = change > 0 ? "+" : change < 0 ? "-" : "";
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
return
|
|
1611
|
+
el3.textContent = magnitude === null ? `${arrow} \u2014` : `${arrow} ${sign}${magnitude}`;
|
|
1612
|
+
el3.title = `Compared with ${this._format(reference, false)}`;
|
|
1613
|
+
return el3;
|
|
1533
1614
|
}
|
|
1534
1615
|
_format(value, withAffixes) {
|
|
1535
1616
|
const format = this.config.format || "number";
|
|
@@ -1630,9 +1711,6 @@ function formatValue(value, {
|
|
|
1630
1711
|
})();
|
|
1631
1712
|
return `${prefix || ""}${body}${suffix || ""}`;
|
|
1632
1713
|
}
|
|
1633
|
-
function slug(text) {
|
|
1634
|
-
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "chart";
|
|
1635
|
-
}
|
|
1636
1714
|
|
|
1637
1715
|
// src/tiles/layout_persistence.js
|
|
1638
1716
|
var LAYOUT_VERSION = 3;
|
|
@@ -2205,19 +2283,19 @@ var RowsTile = class extends TileBase {
|
|
|
2205
2283
|
getConfigSchema() {
|
|
2206
2284
|
return {
|
|
2207
2285
|
fields: [
|
|
2208
|
-
{
|
|
2286
|
+
{ key: "title", type: "text", label: "Tile title" },
|
|
2209
2287
|
{
|
|
2210
|
-
|
|
2288
|
+
key: "empty_text",
|
|
2211
2289
|
type: "text",
|
|
2212
2290
|
label: "When there is nothing",
|
|
2213
|
-
|
|
2291
|
+
description: 'What the tile says when the list is empty, e.g. "Nothing is late."'
|
|
2214
2292
|
},
|
|
2215
2293
|
{
|
|
2216
|
-
|
|
2294
|
+
key: "max_rows",
|
|
2217
2295
|
type: "number",
|
|
2218
2296
|
label: "Rows at most",
|
|
2219
2297
|
min: 0,
|
|
2220
|
-
|
|
2298
|
+
description: "0 shows them all; the tile scrolls."
|
|
2221
2299
|
}
|
|
2222
2300
|
]
|
|
2223
2301
|
};
|
|
@@ -2437,10 +2515,10 @@ var PeopleTile = class extends RowsTile {
|
|
|
2437
2515
|
getConfigSchema() {
|
|
2438
2516
|
const { fields } = super.getConfigSchema();
|
|
2439
2517
|
return { fields: [...fields, {
|
|
2440
|
-
|
|
2518
|
+
key: "count_label",
|
|
2441
2519
|
type: "text",
|
|
2442
2520
|
label: "Count reads",
|
|
2443
|
-
|
|
2521
|
+
description: `After each person's number, e.g. "open" for "3 open".`
|
|
2444
2522
|
}] };
|
|
2445
2523
|
}
|
|
2446
2524
|
_renderItem(p) {
|
|
@@ -2546,6 +2624,346 @@ function createScopePicker({ options = [], value, label = "Scope", onChange } =
|
|
|
2546
2624
|
};
|
|
2547
2625
|
}
|
|
2548
2626
|
|
|
2627
|
+
// src/tiles/tile_config_form.js
|
|
2628
|
+
var _seq = 0;
|
|
2629
|
+
var FOCUSABLE = 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
|
|
2630
|
+
function el2(tag, className, text) {
|
|
2631
|
+
const n = document.createElement(tag);
|
|
2632
|
+
if (className) n.className = className;
|
|
2633
|
+
if (text != null) n.textContent = String(text);
|
|
2634
|
+
return n;
|
|
2635
|
+
}
|
|
2636
|
+
function isVisible(field, values) {
|
|
2637
|
+
const rule = field.visibleWhen;
|
|
2638
|
+
if (!rule) return true;
|
|
2639
|
+
const v = values[rule.field];
|
|
2640
|
+
if (Array.isArray(rule.in)) return rule.in.includes(v);
|
|
2641
|
+
if ("equals" in rule) return v === rule.equals;
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
var BUILT_IN = {
|
|
2645
|
+
[FieldTypes.TEXT]: (field, id) => {
|
|
2646
|
+
const input = el2("input", "fd-tile-config__input");
|
|
2647
|
+
input.type = "text";
|
|
2648
|
+
input.id = id;
|
|
2649
|
+
return {
|
|
2650
|
+
element: input,
|
|
2651
|
+
events: ["input"],
|
|
2652
|
+
read: () => input.value,
|
|
2653
|
+
write: (v) => {
|
|
2654
|
+
input.value = v == null ? "" : String(v);
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
},
|
|
2658
|
+
[FieldTypes.NUMBER]: (field, id) => numeric(field, id, "number"),
|
|
2659
|
+
[FieldTypes.RANGE]: (field, id) => numeric(field, id, "range"),
|
|
2660
|
+
[FieldTypes.CHECKBOX]: (field, id) => {
|
|
2661
|
+
const input = el2("input", "fd-tile-config__checkbox");
|
|
2662
|
+
input.type = "checkbox";
|
|
2663
|
+
input.id = id;
|
|
2664
|
+
return {
|
|
2665
|
+
element: input,
|
|
2666
|
+
events: ["change"],
|
|
2667
|
+
read: () => input.checked,
|
|
2668
|
+
write: (v) => {
|
|
2669
|
+
input.checked = !!v;
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
},
|
|
2673
|
+
[FieldTypes.COLOR]: (field, id) => {
|
|
2674
|
+
const input = el2("input", "fd-tile-config__color");
|
|
2675
|
+
input.type = "color";
|
|
2676
|
+
input.id = id;
|
|
2677
|
+
return {
|
|
2678
|
+
element: input,
|
|
2679
|
+
events: ["input"],
|
|
2680
|
+
read: () => input.value,
|
|
2681
|
+
write: (v) => {
|
|
2682
|
+
if (v) input.value = String(v);
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
},
|
|
2686
|
+
[FieldTypes.SELECT]: (field, id, data) => choice(field, id, data, false),
|
|
2687
|
+
[FieldTypes.MULTISELECT]: (field, id, data) => choice(field, id, data, true)
|
|
2688
|
+
};
|
|
2689
|
+
function numeric(field, id, kind) {
|
|
2690
|
+
const input = el2("input", `fd-tile-config__${kind === "range" ? "range" : "input"}`);
|
|
2691
|
+
input.type = kind;
|
|
2692
|
+
input.id = id;
|
|
2693
|
+
if (field.min != null) input.min = String(field.min);
|
|
2694
|
+
if (field.max != null) input.max = String(field.max);
|
|
2695
|
+
if (field.step != null) input.step = String(field.step);
|
|
2696
|
+
const out = kind === "range" ? el2("output", "fd-tile-config__range-value") : null;
|
|
2697
|
+
const element = out ? el2("span", "fd-tile-config__range-wrap") : input;
|
|
2698
|
+
if (out) element.append(input, out);
|
|
2699
|
+
return {
|
|
2700
|
+
element,
|
|
2701
|
+
events: ["input"],
|
|
2702
|
+
// The raw text goes to `validateField`; the form turns it into a number.
|
|
2703
|
+
read: () => input.value,
|
|
2704
|
+
write: (v) => {
|
|
2705
|
+
input.value = v == null ? "" : String(v);
|
|
2706
|
+
if (out) out.textContent = input.value;
|
|
2707
|
+
},
|
|
2708
|
+
shown: (v) => {
|
|
2709
|
+
if (out) out.textContent = v == null ? "" : String(v);
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
function choice(field, id, data, multiple) {
|
|
2714
|
+
const select = el2("select", "fd-tile-config__select");
|
|
2715
|
+
select.id = id;
|
|
2716
|
+
select.multiple = multiple;
|
|
2717
|
+
const options = getFieldOptions(field, data);
|
|
2718
|
+
options.forEach((o) => select.append(el2("option", null, o.label ?? o.value)));
|
|
2719
|
+
return {
|
|
2720
|
+
element: select,
|
|
2721
|
+
events: ["change"],
|
|
2722
|
+
read: () => multiple ? options.filter((_, i) => select.options[i].selected).map((o) => o.value) : options[select.selectedIndex]?.value ?? null,
|
|
2723
|
+
write: (v) => {
|
|
2724
|
+
if (multiple) {
|
|
2725
|
+
const want = Array.isArray(v) ? v : [];
|
|
2726
|
+
options.forEach((o, i) => {
|
|
2727
|
+
select.options[i].selected = want.includes(o.value);
|
|
2728
|
+
});
|
|
2729
|
+
} else {
|
|
2730
|
+
select.selectedIndex = options.findIndex((o) => o.value === v);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function coerce(field, raw) {
|
|
2736
|
+
if (field.type === FieldTypes.NUMBER || field.type === FieldTypes.RANGE) {
|
|
2737
|
+
return raw === "" || raw == null ? null : Number(raw);
|
|
2738
|
+
}
|
|
2739
|
+
return raw;
|
|
2740
|
+
}
|
|
2741
|
+
function mountTileConfigForm(host, {
|
|
2742
|
+
schema,
|
|
2743
|
+
config = {},
|
|
2744
|
+
data = null,
|
|
2745
|
+
onChange = null,
|
|
2746
|
+
renderers = {},
|
|
2747
|
+
commitOn = "input"
|
|
2748
|
+
} = {}) {
|
|
2749
|
+
if (!host) throw new TypeError("mountTileConfigForm: a host element is required");
|
|
2750
|
+
if (commitOn !== "input" && commitOn !== "change") {
|
|
2751
|
+
throw new TypeError(`mountTileConfigForm: unknown commitOn "${commitOn}"`);
|
|
2752
|
+
}
|
|
2753
|
+
const fields = Array.isArray(schema?.fields) ? schema.fields : [];
|
|
2754
|
+
fields.forEach((f, i) => {
|
|
2755
|
+
if (typeof f?.key !== "string" || !f.key) {
|
|
2756
|
+
const called = typeof f?.label === "string" && f.label ? ` "${f.label}"` : "";
|
|
2757
|
+
throw new TypeError(`mountTileConfigForm: field #${i + 1}${called} has no key`);
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
let values = mergeWithDefaults(config || {}, schema);
|
|
2761
|
+
const unlisten = [];
|
|
2762
|
+
const root = el2("div", "fd-tile-config");
|
|
2763
|
+
const rows = [];
|
|
2764
|
+
const showError = (r, message) => {
|
|
2765
|
+
r.invalid = !!message;
|
|
2766
|
+
r.error.textContent = message || "";
|
|
2767
|
+
r.error.hidden = !message;
|
|
2768
|
+
r.row.classList.toggle("fd-tile-config__row--invalid", !!message);
|
|
2769
|
+
};
|
|
2770
|
+
const applyVisibility = () => {
|
|
2771
|
+
for (const r of rows) r.row.hidden = !isVisible(r.field, values);
|
|
2772
|
+
};
|
|
2773
|
+
const commit = (r, raw) => {
|
|
2774
|
+
if (r.field.readonly) return { valid: false, error: `${r.named.label} is read-only` };
|
|
2775
|
+
const result = validateField(raw, r.named);
|
|
2776
|
+
if (!result.valid) {
|
|
2777
|
+
showError(r, result.error);
|
|
2778
|
+
return { valid: false, error: result.error };
|
|
2779
|
+
}
|
|
2780
|
+
showError(r, null);
|
|
2781
|
+
const value = coerce(r.field, raw);
|
|
2782
|
+
values = { ...values, [r.field.key]: value };
|
|
2783
|
+
applyVisibility();
|
|
2784
|
+
onChange?.(r.field.key, value, { ...values });
|
|
2785
|
+
return { valid: true };
|
|
2786
|
+
};
|
|
2787
|
+
for (const field of fields) {
|
|
2788
|
+
const id = `fd-tile-config-${++_seq}`;
|
|
2789
|
+
const row = el2("div", `fd-tile-config__row fd-tile-config__row--${field.type}`);
|
|
2790
|
+
row.setAttribute("data-field", field.key);
|
|
2791
|
+
const named = { ...field, label: field.label ?? field.key };
|
|
2792
|
+
const label = el2("label", "fd-tile-config__label", named.label);
|
|
2793
|
+
const error = el2("div", "fd-tile-config__error");
|
|
2794
|
+
error.hidden = true;
|
|
2795
|
+
const r = { field, named, row, error, control: null, invalid: false, pending: null };
|
|
2796
|
+
const custom = renderers?.[field.type];
|
|
2797
|
+
if (typeof custom === "function") {
|
|
2798
|
+
const made = custom({
|
|
2799
|
+
field,
|
|
2800
|
+
key: field.key,
|
|
2801
|
+
value: values[field.key],
|
|
2802
|
+
data,
|
|
2803
|
+
readonly: !!field.readonly,
|
|
2804
|
+
commit: (v) => commit(r, v)
|
|
2805
|
+
});
|
|
2806
|
+
const element = made instanceof HTMLElement ? made : made?.element;
|
|
2807
|
+
if (!(element instanceof HTMLElement)) {
|
|
2808
|
+
throw new TypeError(`mountTileConfigForm: the renderer for "${field.type}" returned no element`);
|
|
2809
|
+
}
|
|
2810
|
+
element.id ||= id;
|
|
2811
|
+
label.htmlFor = element.id;
|
|
2812
|
+
r.control = {
|
|
2813
|
+
element,
|
|
2814
|
+
write: made instanceof HTMLElement ? () => {
|
|
2815
|
+
} : (v) => made.setValue?.(v),
|
|
2816
|
+
dispose: made instanceof HTMLElement ? null : () => made.dispose?.()
|
|
2817
|
+
};
|
|
2818
|
+
} else {
|
|
2819
|
+
const build = BUILT_IN[field.type];
|
|
2820
|
+
if (!build) {
|
|
2821
|
+
throw new TypeError(`mountTileConfigForm: no control for type "${field.type}" (field "${field.key}"); pass a renderer`);
|
|
2822
|
+
}
|
|
2823
|
+
const c = build(field, id, data);
|
|
2824
|
+
label.htmlFor = id;
|
|
2825
|
+
c.write(values[field.key]);
|
|
2826
|
+
const input = c.element.matches("input, select") ? c.element : c.element.querySelector("input, select");
|
|
2827
|
+
if (field.readonly) {
|
|
2828
|
+
input.disabled = true;
|
|
2829
|
+
} else {
|
|
2830
|
+
const listen = (type, fn) => {
|
|
2831
|
+
input.addEventListener(type, fn);
|
|
2832
|
+
unlisten.push(() => input.removeEventListener(type, fn));
|
|
2833
|
+
};
|
|
2834
|
+
const onEdit = () => {
|
|
2835
|
+
r.pending = null;
|
|
2836
|
+
const raw = c.read();
|
|
2837
|
+
c.shown?.(raw);
|
|
2838
|
+
commit(r, raw);
|
|
2839
|
+
};
|
|
2840
|
+
if (commitOn === "change") {
|
|
2841
|
+
listen("change", onEdit);
|
|
2842
|
+
if (c.events.includes("input")) {
|
|
2843
|
+
listen("input", () => {
|
|
2844
|
+
r.pending = onEdit;
|
|
2845
|
+
c.shown?.(c.read());
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
} else {
|
|
2849
|
+
for (const type of c.events) listen(type, onEdit);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
r.control = c;
|
|
2853
|
+
}
|
|
2854
|
+
if (field.type === FieldTypes.CHECKBOX) row.append(r.control.element, label);
|
|
2855
|
+
else row.append(label, r.control.element);
|
|
2856
|
+
if (field.description) row.append(el2("div", "fd-tile-config__help", field.description));
|
|
2857
|
+
row.append(error);
|
|
2858
|
+
rows.push(r);
|
|
2859
|
+
root.append(row);
|
|
2860
|
+
}
|
|
2861
|
+
applyVisibility();
|
|
2862
|
+
host.append(root);
|
|
2863
|
+
return {
|
|
2864
|
+
getValues: () => ({ ...values }),
|
|
2865
|
+
setValues(next) {
|
|
2866
|
+
values = mergeWithDefaults(next || {}, schema);
|
|
2867
|
+
for (const r of rows) {
|
|
2868
|
+
r.pending = null;
|
|
2869
|
+
r.control.write(values[r.field.key]);
|
|
2870
|
+
showError(r, null);
|
|
2871
|
+
}
|
|
2872
|
+
applyVisibility();
|
|
2873
|
+
},
|
|
2874
|
+
isValid: () => rows.every((r) => r.row.hidden || !r.invalid),
|
|
2875
|
+
focusFirstInvalid() {
|
|
2876
|
+
const r = rows.find((x) => !x.row.hidden && x.invalid);
|
|
2877
|
+
if (!r) return false;
|
|
2878
|
+
const { element } = r.control;
|
|
2879
|
+
const target = element.matches(FOCUSABLE) ? element : element.querySelector(FOCUSABLE);
|
|
2880
|
+
(target || element).focus();
|
|
2881
|
+
return true;
|
|
2882
|
+
},
|
|
2883
|
+
flush() {
|
|
2884
|
+
for (const r of rows) r.pending?.();
|
|
2885
|
+
},
|
|
2886
|
+
dispose() {
|
|
2887
|
+
unlisten.splice(0).forEach((off) => off());
|
|
2888
|
+
for (const r of rows) {
|
|
2889
|
+
try {
|
|
2890
|
+
r.control.dispose?.();
|
|
2891
|
+
} catch (err) {
|
|
2892
|
+
console.warn("[tile-config] a renderer dispose threw", err);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
rows.length = 0;
|
|
2896
|
+
root.remove();
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
function openTileConfig(eventBus, { mode = "modal", host = null, width, renderers = {}, title = "Tile settings" } = {}) {
|
|
2901
|
+
if (typeof eventBus?.on !== "function") throw new TypeError("openTileConfig: an event bus with on() is required");
|
|
2902
|
+
if (mode !== "modal" && mode !== "slide-out") throw new TypeError(`openTileConfig: unknown mode "${mode}"`);
|
|
2903
|
+
if (mode === "slide-out" && !(host instanceof HTMLElement)) {
|
|
2904
|
+
throw new TypeError("openTileConfig: a slide-out needs a host element");
|
|
2905
|
+
}
|
|
2906
|
+
let form = null;
|
|
2907
|
+
let panel = null;
|
|
2908
|
+
let modalOpen = false;
|
|
2909
|
+
const drop = () => {
|
|
2910
|
+
form?.flush();
|
|
2911
|
+
form?.dispose();
|
|
2912
|
+
form = null;
|
|
2913
|
+
};
|
|
2914
|
+
const show = (detail = {}) => {
|
|
2915
|
+
if (modalOpen) return;
|
|
2916
|
+
const heading = typeof title === "function" ? title(detail) : title;
|
|
2917
|
+
const base = { schema: detail.schema, config: detail.config, data: detail.data ?? null, renderers };
|
|
2918
|
+
if (mode === "slide-out") {
|
|
2919
|
+
drop();
|
|
2920
|
+
if (!panel) {
|
|
2921
|
+
panel = new SlideOutPanel({ ...width ? { width } : {}, onClose: drop });
|
|
2922
|
+
panel.mount(host);
|
|
2923
|
+
}
|
|
2924
|
+
const body = panel.open(heading, "tune");
|
|
2925
|
+
form = mountTileConfigForm(body, {
|
|
2926
|
+
...base,
|
|
2927
|
+
commitOn: "change",
|
|
2928
|
+
onChange: (_k, _v, cfg) => detail.onSave?.(cfg)
|
|
2929
|
+
});
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
const content = el2("div", "fd-tile-config-host");
|
|
2933
|
+
const mine = mountTileConfigForm(content, base);
|
|
2934
|
+
modalOpen = true;
|
|
2935
|
+
openModal({
|
|
2936
|
+
title: heading,
|
|
2937
|
+
icon: "tune",
|
|
2938
|
+
content,
|
|
2939
|
+
...width ? { width } : {},
|
|
2940
|
+
actions: [
|
|
2941
|
+
{ label: "Cancel", value: null },
|
|
2942
|
+
// Refused while a field shows an error: its last valid value is
|
|
2943
|
+
// not what the user is looking at, so saving it would be a lie.
|
|
2944
|
+
{ label: "Save", value: "save", primary: true, keepOpen: true, onClick: (close) => {
|
|
2945
|
+
if (mine.isValid()) {
|
|
2946
|
+
close("save");
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
mine.focusFirstInvalid();
|
|
2950
|
+
} }
|
|
2951
|
+
]
|
|
2952
|
+
}).then((v) => {
|
|
2953
|
+
modalOpen = false;
|
|
2954
|
+
if (v === "save") detail.onSave?.(mine.getValues());
|
|
2955
|
+
mine.dispose();
|
|
2956
|
+
});
|
|
2957
|
+
};
|
|
2958
|
+
const sub = eventBus.on("grid:config-modal-show", show);
|
|
2959
|
+
return () => {
|
|
2960
|
+
sub.dispose();
|
|
2961
|
+
drop();
|
|
2962
|
+
panel?.dispose();
|
|
2963
|
+
panel = null;
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2549
2967
|
// src/tiles/tile_grid.js
|
|
2550
2968
|
init_tile_registry();
|
|
2551
2969
|
var TileGrid = class {
|
|
@@ -3218,6 +3636,8 @@ var TileGrid = class {
|
|
|
3218
3636
|
tileType,
|
|
3219
3637
|
config,
|
|
3220
3638
|
schema,
|
|
3639
|
+
// What `getFieldOptions` reads for a field with dynamic options.
|
|
3640
|
+
data: this.data,
|
|
3221
3641
|
variables: this.data ? this._getVariableList() : [],
|
|
3222
3642
|
onSave: (newConfig) => {
|
|
3223
3643
|
const tile = this.tiles.get(tileId);
|
|
@@ -3344,6 +3764,7 @@ export {
|
|
|
3344
3764
|
CHART_TILE_TYPES,
|
|
3345
3765
|
ChartTile,
|
|
3346
3766
|
FieldTypes,
|
|
3767
|
+
FigureTile,
|
|
3347
3768
|
KpiTile,
|
|
3348
3769
|
LayoutPersistence,
|
|
3349
3770
|
ListTile,
|
|
@@ -3366,9 +3787,11 @@ export {
|
|
|
3366
3787
|
groupByPerson,
|
|
3367
3788
|
hasWidget,
|
|
3368
3789
|
mergeWithDefaults,
|
|
3790
|
+
mountTileConfigForm,
|
|
3369
3791
|
normalizeChartData,
|
|
3370
3792
|
normalizeListData,
|
|
3371
3793
|
normalizePeopleData,
|
|
3794
|
+
openTileConfig,
|
|
3372
3795
|
registerChartTiles,
|
|
3373
3796
|
registerListTiles,
|
|
3374
3797
|
registerWidget,
|