flexdesk 0.16.0 → 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 +2261 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3373 -238
- 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-UWO43BZU.js +111 -0
- 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 +3373 -238
- 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 +1131 -253
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3254 -2112
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -345
- 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 +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +12 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +38 -1
- 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 +26 -4
- 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-CWPMLX67.js +0 -475
- package/dist/chunk-CWPMLX67.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/src/ui/controllers/panel_state_machine.js +0 -634
package/dist/tiles.js
CHANGED
|
@@ -12,8 +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";
|
|
18
|
+
import {
|
|
19
|
+
showContextMenu
|
|
20
|
+
} from "./chunk-UWO43BZU.js";
|
|
21
|
+
import {
|
|
22
|
+
openModal
|
|
23
|
+
} from "./chunk-D3COLZZ2.js";
|
|
17
24
|
import {
|
|
18
25
|
buildHistogramTrace,
|
|
19
26
|
buildLegendLayout,
|
|
@@ -28,10 +35,13 @@ import {
|
|
|
28
35
|
purge,
|
|
29
36
|
resize,
|
|
30
37
|
strideDownsampleShared
|
|
31
|
-
} from "./chunk-
|
|
32
|
-
import "./chunk-
|
|
33
|
-
import
|
|
34
|
-
|
|
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";
|
|
35
45
|
import {
|
|
36
46
|
__toCommonJS
|
|
37
47
|
} from "./chunk-JYWURG5T.js";
|
|
@@ -71,6 +81,11 @@ function validateField(value, field) {
|
|
|
71
81
|
return { valid: false, error: `${field.label} must be selected` };
|
|
72
82
|
}
|
|
73
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;
|
|
74
89
|
case FieldTypes.MULTISELECT:
|
|
75
90
|
if (field.required && (!Array.isArray(value) || value.length === 0)) {
|
|
76
91
|
return { valid: false, error: `${field.label} must have at least one selection` };
|
|
@@ -234,6 +249,16 @@ var TileBase = class {
|
|
|
234
249
|
this._lastLoadedAt = null;
|
|
235
250
|
this._loadSeq = 0;
|
|
236
251
|
this.comparisonData = null;
|
|
252
|
+
this.scope = null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Called when the board's scope changes (`TileGrid.setScope`), after
|
|
256
|
+
* `this.scope` is set. A no-op here: a tile whose rows the grid's data
|
|
257
|
+
* source loads is reloaded by the grid, and one fed through `setData` is
|
|
258
|
+
* fed again by whoever owns its data.
|
|
259
|
+
* @param {*} _scope
|
|
260
|
+
*/
|
|
261
|
+
onScopeChange(_scope) {
|
|
237
262
|
}
|
|
238
263
|
/**
|
|
239
264
|
* Set comparison data for overlay rendering.
|
|
@@ -696,7 +721,7 @@ var TileBase = class {
|
|
|
696
721
|
* @private
|
|
697
722
|
*/
|
|
698
723
|
_onConfigClick() {
|
|
699
|
-
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"));
|
|
700
725
|
this.element?.classList.add("tile--selected");
|
|
701
726
|
this.eventBus?.emit?.("tile:config-request", {
|
|
702
727
|
tileId: this.id,
|
|
@@ -908,64 +933,19 @@ var TileBase = class {
|
|
|
908
933
|
}
|
|
909
934
|
};
|
|
910
935
|
|
|
911
|
-
// src/tiles/
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
{ value: "bar", label: "Bar" },
|
|
917
|
-
{ value: "bar-stacked", label: "Bar (stacked)" },
|
|
918
|
-
{ value: "bar-horizontal", label: "Bar (horizontal)" },
|
|
919
|
-
{ value: "area", label: "Area" },
|
|
920
|
-
{ value: "area-stacked", label: "Area (stacked %)" },
|
|
921
|
-
{ value: "scatter", label: "Scatter" },
|
|
922
|
-
{ value: "pie", label: "Pie" },
|
|
923
|
-
{ value: "donut", label: "Donut" },
|
|
924
|
-
{ value: "funnel", label: "Funnel" },
|
|
925
|
-
{ value: "histogram", label: "Histogram" }
|
|
926
|
-
]);
|
|
927
|
-
var AXIS_CHARTS = ["bar", "bar-stacked", "bar-horizontal", "line", "area", "area-stacked", "scatter"];
|
|
928
|
-
var READS = Object.freeze({
|
|
929
|
-
legend: /* @__PURE__ */ new Set([...AXIS_CHARTS, "pie", "donut", "histogram"]),
|
|
930
|
-
axis_titles: /* @__PURE__ */ new Set([...AXIS_CHARTS, "histogram"]),
|
|
931
|
-
y_scale: /* @__PURE__ */ new Set(["bar", "bar-horizontal", "line", "area", "scatter"]),
|
|
932
|
-
y_range: /* @__PURE__ */ new Set(["bar", "bar-stacked", "bar-horizontal", "line", "area", "scatter"]),
|
|
933
|
-
show_markers: /* @__PURE__ */ new Set(["line", "area", "area-stacked"])
|
|
936
|
+
// src/tiles/figure_tile.js
|
|
937
|
+
var FIGURE_CONFIG = Object.freeze({
|
|
938
|
+
scrollZoom: false,
|
|
939
|
+
displayModeBar: false,
|
|
940
|
+
responsive: true
|
|
934
941
|
});
|
|
935
|
-
var
|
|
936
|
-
var
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
else if (!Array.isArray(d.series)) {
|
|
943
|
-
const single = ["x", "y", "labels", "values"].some((k) => Array.isArray(d[k]));
|
|
944
|
-
d = { ...d, series: single ? [d] : [] };
|
|
945
|
-
}
|
|
946
|
-
const series = d.series.filter((s) => s && pointCount(s) > 0);
|
|
947
|
-
if (!series.length) return null;
|
|
948
|
-
return {
|
|
949
|
-
series,
|
|
950
|
-
markers: Array.isArray(d.markers) ? d.markers : [],
|
|
951
|
-
referenceLines: Array.isArray(d.referenceLines) ? d.referenceLines : [],
|
|
952
|
-
xKind: d.xKind || null,
|
|
953
|
-
note: typeof d.note === "string" ? d.note : ""
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
function pointCount(s) {
|
|
957
|
-
for (const k of ["y", "values", "x", "labels"]) {
|
|
958
|
-
if (Array.isArray(s[k])) return s[k].length;
|
|
959
|
-
}
|
|
960
|
-
return 0;
|
|
961
|
-
}
|
|
962
|
-
var ChartTile = class extends TileBase {
|
|
963
|
-
static TYPE = "flexdesk.chart";
|
|
964
|
-
static TITLE = "Chart";
|
|
965
|
-
static ICON = "insert_chart";
|
|
966
|
-
static DESCRIPTION = "Line, bar, area, pie, donut, funnel or histogram over series you supply.";
|
|
967
|
-
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
968
|
-
static SIZE_CONSTRAINTS = { minW: 3, minH: 3, maxW: 12, maxH: 10 };
|
|
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.";
|
|
969
949
|
/**
|
|
970
950
|
* @param {Object} options - TileBase's, plus:
|
|
971
951
|
* @param {Function} [options.onPointClick] - `({customdata, point, tile})`
|
|
@@ -977,109 +957,18 @@ var ChartTile = class extends TileBase {
|
|
|
977
957
|
this._plotEl = null;
|
|
978
958
|
this._drawn = false;
|
|
979
959
|
this._drawSeq = 0;
|
|
980
|
-
|
|
981
|
-
/** Pure and static-only — it runs inside `TileBase`'s constructor. */
|
|
982
|
-
getDefaultConfig() {
|
|
983
|
-
return {
|
|
984
|
-
title: "",
|
|
985
|
-
chart_type: "line",
|
|
986
|
-
unit: "",
|
|
987
|
-
legend: "bottom",
|
|
988
|
-
x_title: "",
|
|
989
|
-
y_title: "",
|
|
990
|
-
y_scale: "linear",
|
|
991
|
-
y_min: null,
|
|
992
|
-
y_max: null,
|
|
993
|
-
show_markers: false,
|
|
994
|
-
max_points: MAX_POINTS
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
getConfigSchema() {
|
|
998
|
-
return {
|
|
999
|
-
fields: [
|
|
1000
|
-
{ name: "title", type: "text", label: "Tile title" },
|
|
1001
|
-
{
|
|
1002
|
-
name: "chart_type",
|
|
1003
|
-
type: "select",
|
|
1004
|
-
label: "Chart type",
|
|
1005
|
-
options: CHART_TILE_TYPES.map((o) => ({ ...o }))
|
|
1006
|
-
},
|
|
1007
|
-
{
|
|
1008
|
-
name: "unit",
|
|
1009
|
-
type: "text",
|
|
1010
|
-
label: "Unit",
|
|
1011
|
-
help: 'Shown with the value axis title, e.g. "pts" or "h".'
|
|
1012
|
-
},
|
|
1013
|
-
{
|
|
1014
|
-
name: "legend",
|
|
1015
|
-
type: "select",
|
|
1016
|
-
label: "Legend",
|
|
1017
|
-
visibleWhen: { field: "chart_type", in: [...READS.legend] },
|
|
1018
|
-
options: [
|
|
1019
|
-
{ value: "bottom", label: "Below" },
|
|
1020
|
-
{ value: "top", label: "Above" },
|
|
1021
|
-
{ value: "right", label: "Right" },
|
|
1022
|
-
{ value: "left", label: "Left" },
|
|
1023
|
-
{ value: "hidden", label: "None" }
|
|
1024
|
-
]
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
name: "x_title",
|
|
1028
|
-
type: "text",
|
|
1029
|
-
label: "X axis title",
|
|
1030
|
-
visibleWhen: { field: "chart_type", in: [...READS.axis_titles] }
|
|
1031
|
-
},
|
|
1032
|
-
{
|
|
1033
|
-
name: "y_title",
|
|
1034
|
-
type: "text",
|
|
1035
|
-
label: "Y axis title",
|
|
1036
|
-
visibleWhen: { field: "chart_type", in: [...READS.axis_titles] }
|
|
1037
|
-
},
|
|
1038
|
-
{
|
|
1039
|
-
name: "y_scale",
|
|
1040
|
-
type: "select",
|
|
1041
|
-
label: "Y scale",
|
|
1042
|
-
visibleWhen: { field: "chart_type", in: [...READS.y_scale] },
|
|
1043
|
-
options: [{ value: "linear", label: "Linear" }, { value: "log", label: "Logarithmic" }]
|
|
1044
|
-
},
|
|
1045
|
-
{
|
|
1046
|
-
name: "y_min",
|
|
1047
|
-
type: "number",
|
|
1048
|
-
label: "Y minimum",
|
|
1049
|
-
visibleWhen: { field: "chart_type", in: [...READS.y_range] }
|
|
1050
|
-
},
|
|
1051
|
-
{
|
|
1052
|
-
name: "y_max",
|
|
1053
|
-
type: "number",
|
|
1054
|
-
label: "Y maximum",
|
|
1055
|
-
visibleWhen: { field: "chart_type", in: [...READS.y_range] }
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
name: "show_markers",
|
|
1059
|
-
type: "checkbox",
|
|
1060
|
-
label: "Show point markers",
|
|
1061
|
-
visibleWhen: { field: "chart_type", in: [...READS.show_markers] }
|
|
1062
|
-
}
|
|
1063
|
-
]
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
mount(container) {
|
|
1067
|
-
super.mount(container);
|
|
1068
|
-
applyTitle(this);
|
|
960
|
+
this._figure = null;
|
|
1069
961
|
}
|
|
1070
962
|
/**
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
* it would drop the tile's series and skip the redraw in the new chart type.
|
|
1075
|
-
* `setData(null)` is how a tile is cleared.
|
|
963
|
+
* The one override: what to draw for `data`.
|
|
964
|
+
* @param {*} _data
|
|
965
|
+
* @returns {{traces: Array, layout?: Object, config?: Object, frames?: Array}|null}
|
|
1076
966
|
*/
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
if (config) applyTitle(this);
|
|
967
|
+
figure(_data) {
|
|
968
|
+
throw new Error(`${this.constructor.name}.figure() must be implemented by the subclass`);
|
|
1080
969
|
}
|
|
1081
970
|
/**
|
|
1082
|
-
* Hand this tile its
|
|
971
|
+
* Hand this tile its data. Unlike `update()`, an empty or null value is
|
|
1083
972
|
* drawn — as the empty state — rather than ignored.
|
|
1084
973
|
*/
|
|
1085
974
|
setData(data) {
|
|
@@ -1101,10 +990,28 @@ var ChartTile = class extends TileBase {
|
|
|
1101
990
|
});
|
|
1102
991
|
}
|
|
1103
992
|
menuItems() {
|
|
1104
|
-
return [
|
|
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 }];
|
|
1105
999
|
}
|
|
1106
1000
|
_onMenuAction(action) {
|
|
1107
|
-
if (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
|
+
};
|
|
1108
1015
|
}
|
|
1109
1016
|
// ── the three states: each one invalidates the figure ───────────────
|
|
1110
1017
|
showLoading() {
|
|
@@ -1133,63 +1040,70 @@ var ChartTile = class extends TileBase {
|
|
|
1133
1040
|
* not awaited: a rejected purge (Plotly absent, already purged) must not
|
|
1134
1041
|
* surface as an unhandled rejection. */
|
|
1135
1042
|
_releasePlot() {
|
|
1136
|
-
const
|
|
1043
|
+
const el3 = this._plotEl;
|
|
1137
1044
|
this._plotEl = null;
|
|
1138
1045
|
this._drawn = false;
|
|
1139
|
-
|
|
1046
|
+
this._figure = null;
|
|
1047
|
+
if (el3) purge(el3).catch(() => {
|
|
1140
1048
|
});
|
|
1141
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
|
+
}
|
|
1142
1061
|
// ── the draw ────────────────────────────────────────────────────────
|
|
1143
1062
|
async _draw(data) {
|
|
1144
1063
|
this._drawSeq += 1;
|
|
1145
1064
|
const seq = this._drawSeq;
|
|
1146
1065
|
const content = this.contentElement;
|
|
1147
1066
|
if (!content) return;
|
|
1067
|
+
const stale = () => this._disposed || seq !== this._drawSeq;
|
|
1148
1068
|
this._releasePlot();
|
|
1149
|
-
|
|
1150
|
-
if (!norm) {
|
|
1151
|
-
this.showEmpty("No data to show.");
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
let plan;
|
|
1069
|
+
let fig;
|
|
1155
1070
|
try {
|
|
1156
|
-
|
|
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
|
+
}
|
|
1157
1075
|
} catch (err) {
|
|
1158
1076
|
this._failed(err);
|
|
1159
1077
|
return;
|
|
1160
1078
|
}
|
|
1161
|
-
if (!
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const parts = [host];
|
|
1165
|
-
if (norm.note) {
|
|
1166
|
-
const note = document.createElement("div");
|
|
1167
|
-
note.className = "fd-chart-tile__note";
|
|
1168
|
-
note.setAttribute("role", "note");
|
|
1169
|
-
note.textContent = norm.note;
|
|
1170
|
-
note.title = norm.note;
|
|
1171
|
-
parts.push(note);
|
|
1079
|
+
if (!fig) {
|
|
1080
|
+
if (!stale()) this.showEmpty(this.constructor.EMPTY_MESSAGE);
|
|
1081
|
+
return;
|
|
1172
1082
|
}
|
|
1173
|
-
|
|
1083
|
+
const host = document.createElement("div");
|
|
1084
|
+
host.className = "fd-figure-tile";
|
|
1085
|
+
content.replaceChildren(...this._figureContent(host, fig));
|
|
1174
1086
|
this._plotEl = host;
|
|
1175
1087
|
this._drawn = false;
|
|
1176
1088
|
try {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
+
}
|
|
1182
1095
|
} catch (err) {
|
|
1183
|
-
if (
|
|
1096
|
+
if (stale()) return;
|
|
1184
1097
|
this._failed(err);
|
|
1185
1098
|
return;
|
|
1186
1099
|
}
|
|
1187
|
-
if (
|
|
1100
|
+
if (stale()) {
|
|
1188
1101
|
purge(host).catch(() => {
|
|
1189
1102
|
});
|
|
1190
1103
|
return;
|
|
1191
1104
|
}
|
|
1192
1105
|
this._drawn = true;
|
|
1106
|
+
this._figure = fig;
|
|
1193
1107
|
this._wireClick(host);
|
|
1194
1108
|
}
|
|
1195
1109
|
/** Plotly's click, reduced to the point's own `customdata`. A pie's point
|
|
@@ -1209,14 +1123,224 @@ var ChartTile = class extends TileBase {
|
|
|
1209
1123
|
try {
|
|
1210
1124
|
this.onPointClick({ customdata, point, tile: this });
|
|
1211
1125
|
} catch (err) {
|
|
1212
|
-
console.error(
|
|
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;
|
|
1213
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)
|
|
1214
1157
|
});
|
|
1215
1158
|
}
|
|
1159
|
+
};
|
|
1160
|
+
function slug(text) {
|
|
1161
|
+
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "chart";
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/tiles/chart_tiles.js
|
|
1165
|
+
init_tile_registry();
|
|
1166
|
+
var CATEGORICAL_ONLY = /* @__PURE__ */ new Set(["pie", "donut", "funnel"]);
|
|
1167
|
+
var CHART_TILE_TYPES = Object.freeze([
|
|
1168
|
+
{ value: "line", label: "Line" },
|
|
1169
|
+
{ value: "bar", label: "Bar" },
|
|
1170
|
+
{ value: "bar-stacked", label: "Bar (stacked)" },
|
|
1171
|
+
{ value: "bar-horizontal", label: "Bar (horizontal)" },
|
|
1172
|
+
{ value: "area", label: "Area" },
|
|
1173
|
+
{ value: "area-stacked", label: "Area (stacked %)" },
|
|
1174
|
+
{ value: "scatter", label: "Scatter" },
|
|
1175
|
+
{ value: "pie", label: "Pie" },
|
|
1176
|
+
{ value: "donut", label: "Donut" },
|
|
1177
|
+
{ value: "funnel", label: "Funnel" },
|
|
1178
|
+
{ value: "histogram", label: "Histogram" }
|
|
1179
|
+
]);
|
|
1180
|
+
var AXIS_CHARTS = ["bar", "bar-stacked", "bar-horizontal", "line", "area", "area-stacked", "scatter"];
|
|
1181
|
+
var READS = Object.freeze({
|
|
1182
|
+
legend: /* @__PURE__ */ new Set([...AXIS_CHARTS, "pie", "donut", "histogram"]),
|
|
1183
|
+
axis_titles: /* @__PURE__ */ new Set([...AXIS_CHARTS, "histogram"]),
|
|
1184
|
+
y_scale: /* @__PURE__ */ new Set(["bar", "bar-horizontal", "line", "area", "scatter"]),
|
|
1185
|
+
y_range: /* @__PURE__ */ new Set(["bar", "bar-stacked", "bar-horizontal", "line", "area", "scatter"]),
|
|
1186
|
+
show_markers: /* @__PURE__ */ new Set(["line", "area", "area-stacked"])
|
|
1187
|
+
});
|
|
1188
|
+
var TILE_MARGIN = { l: 48, r: 16, t: 8, b: 36 };
|
|
1189
|
+
var MAX_POINTS = 2e3;
|
|
1190
|
+
function normalizeChartData(data) {
|
|
1191
|
+
if (data == null) return null;
|
|
1192
|
+
let d = data;
|
|
1193
|
+
if (Array.isArray(d)) d = { series: d };
|
|
1194
|
+
else if (typeof d !== "object") return null;
|
|
1195
|
+
else if (!Array.isArray(d.series)) {
|
|
1196
|
+
const single = ["x", "y", "labels", "values"].some((k) => Array.isArray(d[k]));
|
|
1197
|
+
d = { ...d, series: single ? [d] : [] };
|
|
1198
|
+
}
|
|
1199
|
+
const series = d.series.filter((s) => s && pointCount(s) > 0);
|
|
1200
|
+
if (!series.length) return null;
|
|
1201
|
+
return {
|
|
1202
|
+
series,
|
|
1203
|
+
markers: Array.isArray(d.markers) ? d.markers : [],
|
|
1204
|
+
referenceLines: Array.isArray(d.referenceLines) ? d.referenceLines : [],
|
|
1205
|
+
xKind: d.xKind || null,
|
|
1206
|
+
note: typeof d.note === "string" ? d.note : ""
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function pointCount(s) {
|
|
1210
|
+
for (const k of ["y", "values", "x", "labels"]) {
|
|
1211
|
+
if (Array.isArray(s[k])) return s[k].length;
|
|
1212
|
+
}
|
|
1213
|
+
return 0;
|
|
1214
|
+
}
|
|
1215
|
+
var ChartTile = class extends FigureTile {
|
|
1216
|
+
static TYPE = "flexdesk.chart";
|
|
1217
|
+
static TITLE = "Chart";
|
|
1218
|
+
static ICON = "insert_chart";
|
|
1219
|
+
static DESCRIPTION = "Line, bar, area, pie, donut, funnel or histogram over series you supply.";
|
|
1220
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
1221
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 3, maxW: 12, maxH: 10 };
|
|
1222
|
+
/** Pure and static-only — it runs inside `TileBase`'s constructor. */
|
|
1223
|
+
getDefaultConfig() {
|
|
1224
|
+
return {
|
|
1225
|
+
title: "",
|
|
1226
|
+
chart_type: "line",
|
|
1227
|
+
unit: "",
|
|
1228
|
+
legend: "bottom",
|
|
1229
|
+
x_title: "",
|
|
1230
|
+
y_title: "",
|
|
1231
|
+
y_scale: "linear",
|
|
1232
|
+
y_min: null,
|
|
1233
|
+
y_max: null,
|
|
1234
|
+
show_markers: false,
|
|
1235
|
+
max_points: MAX_POINTS
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
getConfigSchema() {
|
|
1239
|
+
return {
|
|
1240
|
+
fields: [
|
|
1241
|
+
{ key: "title", type: "text", label: "Tile title" },
|
|
1242
|
+
{
|
|
1243
|
+
key: "chart_type",
|
|
1244
|
+
type: "select",
|
|
1245
|
+
label: "Chart type",
|
|
1246
|
+
options: CHART_TILE_TYPES.map((o) => ({ ...o }))
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
key: "unit",
|
|
1250
|
+
type: "text",
|
|
1251
|
+
label: "Unit",
|
|
1252
|
+
description: 'Shown with the value axis title, e.g. "pts" or "h".'
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
key: "legend",
|
|
1256
|
+
type: "select",
|
|
1257
|
+
label: "Legend",
|
|
1258
|
+
visibleWhen: { field: "chart_type", in: [...READS.legend] },
|
|
1259
|
+
options: [
|
|
1260
|
+
{ value: "bottom", label: "Below" },
|
|
1261
|
+
{ value: "top", label: "Above" },
|
|
1262
|
+
{ value: "right", label: "Right" },
|
|
1263
|
+
{ value: "left", label: "Left" },
|
|
1264
|
+
{ value: "hidden", label: "None" }
|
|
1265
|
+
]
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
key: "x_title",
|
|
1269
|
+
type: "text",
|
|
1270
|
+
label: "X axis title",
|
|
1271
|
+
visibleWhen: { field: "chart_type", in: [...READS.axis_titles] }
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
key: "y_title",
|
|
1275
|
+
type: "text",
|
|
1276
|
+
label: "Y axis title",
|
|
1277
|
+
visibleWhen: { field: "chart_type", in: [...READS.axis_titles] }
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
key: "y_scale",
|
|
1281
|
+
type: "select",
|
|
1282
|
+
label: "Y scale",
|
|
1283
|
+
visibleWhen: { field: "chart_type", in: [...READS.y_scale] },
|
|
1284
|
+
options: [{ value: "linear", label: "Linear" }, { value: "log", label: "Logarithmic" }]
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
key: "y_min",
|
|
1288
|
+
type: "number",
|
|
1289
|
+
label: "Y minimum",
|
|
1290
|
+
visibleWhen: { field: "chart_type", in: [...READS.y_range] }
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
key: "y_max",
|
|
1294
|
+
type: "number",
|
|
1295
|
+
label: "Y maximum",
|
|
1296
|
+
visibleWhen: { field: "chart_type", in: [...READS.y_range] }
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
key: "show_markers",
|
|
1300
|
+
type: "checkbox",
|
|
1301
|
+
label: "Show point markers",
|
|
1302
|
+
visibleWhen: { field: "chart_type", in: [...READS.show_markers] }
|
|
1303
|
+
}
|
|
1304
|
+
]
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
mount(container) {
|
|
1308
|
+
super.mount(container);
|
|
1309
|
+
applyTitle(this);
|
|
1310
|
+
}
|
|
1216
1311
|
/**
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1312
|
+
* `null` here is "the grid has no data", not "clear this tile": TileGrid's
|
|
1313
|
+
* config save calls `update(grid.data, newConfig)`, and a grid whose tiles
|
|
1314
|
+
* are fed one by one through `setData` has `grid.data === null` — passed on,
|
|
1315
|
+
* it would drop the tile's series and skip the redraw in the new chart type.
|
|
1316
|
+
* `setData(null)` is how a tile is cleared.
|
|
1219
1317
|
*/
|
|
1318
|
+
update(data, config) {
|
|
1319
|
+
super.update(data === null ? void 0 : data, config);
|
|
1320
|
+
if (config) applyTitle(this);
|
|
1321
|
+
}
|
|
1322
|
+
/** `null` (the empty state) when there is nothing to draw. */
|
|
1323
|
+
figure(data) {
|
|
1324
|
+
const norm = normalizeChartData(data);
|
|
1325
|
+
if (!norm) return null;
|
|
1326
|
+
return { ...this._plan(norm), note: norm.note };
|
|
1327
|
+
}
|
|
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}`. */
|
|
1220
1344
|
_plan(norm) {
|
|
1221
1345
|
const type = this.config.chart_type || "line";
|
|
1222
1346
|
const { series } = norm;
|
|
@@ -1350,35 +1474,6 @@ var ChartTile = class extends TileBase {
|
|
|
1350
1474
|
}
|
|
1351
1475
|
return out;
|
|
1352
1476
|
}
|
|
1353
|
-
async _downloadPng() {
|
|
1354
|
-
if (!this._plotEl || !this._drawn) return;
|
|
1355
|
-
try {
|
|
1356
|
-
const P = await ensurePlotly();
|
|
1357
|
-
const uri = await P.toImage(this._plotEl, { format: "png", scale: 2 });
|
|
1358
|
-
const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
|
|
1359
|
-
const dialogs = this.host?.dialogs;
|
|
1360
|
-
const result = dialogs ? await dialogs.saveFile({ data: uri.split(",")[1], filename, kind: "png" }) : null;
|
|
1361
|
-
if (result) {
|
|
1362
|
-
if (result.ok) this._notify("Download", `Chart saved to ${result.path}`, "success");
|
|
1363
|
-
else if (!result.cancelled) this._notify("Download failed", result.error || "The image could not be saved", "error");
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
const a = document.createElement("a");
|
|
1367
|
-
a.href = uri;
|
|
1368
|
-
a.download = filename;
|
|
1369
|
-
a.click();
|
|
1370
|
-
} catch (err) {
|
|
1371
|
-
this._notify("Download failed", `The image could not be made: ${err?.message || err}`, "error");
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
_failed(err) {
|
|
1375
|
-
if (this._disposed) return;
|
|
1376
|
-
this.showError(`This chart could not be drawn: ${err?.message || err}`);
|
|
1377
|
-
this.eventBus?.emit?.("tile:render-failed", {
|
|
1378
|
-
tileId: this.id,
|
|
1379
|
-
message: err?.message || String(err)
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
1477
|
};
|
|
1383
1478
|
var KpiTile = class extends TileBase {
|
|
1384
1479
|
static TYPE = "flexdesk.kpi";
|
|
@@ -1405,11 +1500,11 @@ var KpiTile = class extends TileBase {
|
|
|
1405
1500
|
getConfigSchema() {
|
|
1406
1501
|
return {
|
|
1407
1502
|
fields: [
|
|
1408
|
-
{
|
|
1409
|
-
{
|
|
1410
|
-
{
|
|
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".' },
|
|
1411
1506
|
{
|
|
1412
|
-
|
|
1507
|
+
key: "format",
|
|
1413
1508
|
type: "select",
|
|
1414
1509
|
label: "Format",
|
|
1415
1510
|
options: [
|
|
@@ -1420,23 +1515,23 @@ var KpiTile = class extends TileBase {
|
|
|
1420
1515
|
{ value: "text", label: "Text" }
|
|
1421
1516
|
]
|
|
1422
1517
|
},
|
|
1423
|
-
{
|
|
1518
|
+
{ key: "decimals", type: "number", label: "Decimals", min: 0, max: 6 },
|
|
1424
1519
|
{
|
|
1425
|
-
|
|
1520
|
+
key: "currency",
|
|
1426
1521
|
type: "text",
|
|
1427
1522
|
label: "Currency code",
|
|
1428
|
-
|
|
1523
|
+
description: "ISO 4217, e.g. EUR. Left empty, the number is shown without a symbol."
|
|
1429
1524
|
},
|
|
1430
|
-
{
|
|
1431
|
-
{
|
|
1432
|
-
{
|
|
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" },
|
|
1433
1528
|
{
|
|
1434
|
-
|
|
1529
|
+
key: "invert_delta",
|
|
1435
1530
|
type: "checkbox",
|
|
1436
1531
|
label: "A fall is good",
|
|
1437
|
-
|
|
1532
|
+
description: "For a cost, a queue length or a latency: a smaller number is the better one."
|
|
1438
1533
|
},
|
|
1439
|
-
{
|
|
1534
|
+
{ key: "foot", type: "text", label: "Caption" }
|
|
1440
1535
|
]
|
|
1441
1536
|
};
|
|
1442
1537
|
}
|
|
@@ -1487,11 +1582,11 @@ var KpiTile = class extends TileBase {
|
|
|
1487
1582
|
if (delta) box.append(delta);
|
|
1488
1583
|
const foot = [this.config.foot, d.note].filter(Boolean).join(" \xB7 ");
|
|
1489
1584
|
if (foot) {
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
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);
|
|
1495
1590
|
}
|
|
1496
1591
|
content.append(box);
|
|
1497
1592
|
}
|
|
@@ -1503,19 +1598,19 @@ var KpiTile = class extends TileBase {
|
|
|
1503
1598
|
const reference = Number(rawReference);
|
|
1504
1599
|
if (!Number.isFinite(value) || !Number.isFinite(reference)) return null;
|
|
1505
1600
|
const change = value - reference;
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
if (change > 0)
|
|
1509
|
-
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");
|
|
1510
1605
|
const arrow = change > 0 ? "\u25B2" : change < 0 ? "\u25BC" : "\u2013";
|
|
1511
1606
|
const magnitude = this.config.delta_relative ? reference === 0 ? null : formatValue(Math.abs(change / reference), {
|
|
1512
1607
|
format: "percent",
|
|
1513
1608
|
decimals: this.config.decimals ?? 1
|
|
1514
1609
|
}) : this._format(Math.abs(change), false);
|
|
1515
1610
|
const sign = change > 0 ? "+" : change < 0 ? "-" : "";
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
return
|
|
1611
|
+
el3.textContent = magnitude === null ? `${arrow} \u2014` : `${arrow} ${sign}${magnitude}`;
|
|
1612
|
+
el3.title = `Compared with ${this._format(reference, false)}`;
|
|
1613
|
+
return el3;
|
|
1519
1614
|
}
|
|
1520
1615
|
_format(value, withAffixes) {
|
|
1521
1616
|
const format = this.config.format || "number";
|
|
@@ -1616,9 +1711,6 @@ function formatValue(value, {
|
|
|
1616
1711
|
})();
|
|
1617
1712
|
return `${prefix || ""}${body}${suffix || ""}`;
|
|
1618
1713
|
}
|
|
1619
|
-
function slug(text) {
|
|
1620
|
-
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "chart";
|
|
1621
|
-
}
|
|
1622
1714
|
|
|
1623
1715
|
// src/tiles/layout_persistence.js
|
|
1624
1716
|
var LAYOUT_VERSION = 3;
|
|
@@ -2139,6 +2231,739 @@ function getLayoutPersistence() {
|
|
|
2139
2231
|
return instance;
|
|
2140
2232
|
}
|
|
2141
2233
|
|
|
2234
|
+
// src/tiles/list_tiles.js
|
|
2235
|
+
init_tile_registry();
|
|
2236
|
+
var TONES = /* @__PURE__ */ new Set(["danger", "warning", "ok", "muted"]);
|
|
2237
|
+
function normalize(data, key) {
|
|
2238
|
+
if (data == null) return null;
|
|
2239
|
+
const d = Array.isArray(data) ? { [key]: data } : data;
|
|
2240
|
+
if (typeof d !== "object" || !Array.isArray(d[key])) return null;
|
|
2241
|
+
const items = d[key].filter((r) => r && typeof r === "object" && r.key != null);
|
|
2242
|
+
const total = Number.isFinite(Number(d.total)) && d.total !== null && d.total !== "" ? Number(d.total) : items.length;
|
|
2243
|
+
return {
|
|
2244
|
+
items,
|
|
2245
|
+
total: Math.max(total, items.length),
|
|
2246
|
+
note: typeof d.note === "string" ? d.note : "",
|
|
2247
|
+
empty: typeof d.empty === "string" ? d.empty : "",
|
|
2248
|
+
action: d.action && typeof d.action === "object" && d.action.label ? d.action : null
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
var normalizeListData = (data) => normalize(data, "rows");
|
|
2252
|
+
var normalizePeopleData = (data) => normalize(data, "people");
|
|
2253
|
+
function el(tag, cls, text) {
|
|
2254
|
+
const e = document.createElement(tag);
|
|
2255
|
+
if (cls) e.className = cls;
|
|
2256
|
+
if (text != null) e.textContent = String(text);
|
|
2257
|
+
return e;
|
|
2258
|
+
}
|
|
2259
|
+
function glyph(name, cls = "") {
|
|
2260
|
+
const g = el("span", `material-symbols-outlined${cls ? ` ${cls}` : ""}`, name);
|
|
2261
|
+
g.setAttribute("aria-hidden", "true");
|
|
2262
|
+
return g;
|
|
2263
|
+
}
|
|
2264
|
+
var toneClass = (base, tone) => TONES.has(tone) ? ` ${base}--${tone}` : "";
|
|
2265
|
+
var RowsTile = class extends TileBase {
|
|
2266
|
+
static _KEY = "rows";
|
|
2267
|
+
constructor(options = {}) {
|
|
2268
|
+
super(options);
|
|
2269
|
+
this.onRowActivate = typeof options.onRowActivate === "function" ? options.onRowActivate : null;
|
|
2270
|
+
this.rowMenu = typeof options.rowMenu === "function" ? options.rowMenu : null;
|
|
2271
|
+
this.onRowMenuAction = typeof options.onRowMenuAction === "function" ? options.onRowMenuAction : null;
|
|
2272
|
+
this.onAction = typeof options.onAction === "function" ? options.onAction : null;
|
|
2273
|
+
this.rowsDraggable = options.rowsDraggable === true;
|
|
2274
|
+
this._items = /* @__PURE__ */ new Map();
|
|
2275
|
+
this._onClick = (e) => this._click(e);
|
|
2276
|
+
this._onKey = (e) => this._key(e);
|
|
2277
|
+
this._onMenu = (e) => this._menu(e);
|
|
2278
|
+
}
|
|
2279
|
+
/** Pure and static-only — it runs inside `TileBase`'s constructor. */
|
|
2280
|
+
getDefaultConfig() {
|
|
2281
|
+
return { title: "", empty_text: "", max_rows: 0 };
|
|
2282
|
+
}
|
|
2283
|
+
getConfigSchema() {
|
|
2284
|
+
return {
|
|
2285
|
+
fields: [
|
|
2286
|
+
{ key: "title", type: "text", label: "Tile title" },
|
|
2287
|
+
{
|
|
2288
|
+
key: "empty_text",
|
|
2289
|
+
type: "text",
|
|
2290
|
+
label: "When there is nothing",
|
|
2291
|
+
description: 'What the tile says when the list is empty, e.g. "Nothing is late."'
|
|
2292
|
+
},
|
|
2293
|
+
{
|
|
2294
|
+
key: "max_rows",
|
|
2295
|
+
type: "number",
|
|
2296
|
+
label: "Rows at most",
|
|
2297
|
+
min: 0,
|
|
2298
|
+
description: "0 shows them all; the tile scrolls."
|
|
2299
|
+
}
|
|
2300
|
+
]
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
mount(container) {
|
|
2304
|
+
super.mount(container);
|
|
2305
|
+
applyTitle2(this);
|
|
2306
|
+
const c = this.contentElement;
|
|
2307
|
+
if (!c) return;
|
|
2308
|
+
c.addEventListener("click", this._onClick);
|
|
2309
|
+
c.addEventListener("keydown", this._onKey);
|
|
2310
|
+
c.addEventListener("contextmenu", this._onMenu);
|
|
2311
|
+
}
|
|
2312
|
+
/** `null` is "the grid has no data" — see `ChartTile.update`. */
|
|
2313
|
+
update(data, config) {
|
|
2314
|
+
if (this._disposed) return;
|
|
2315
|
+
super.update(data === null ? void 0 : data, config);
|
|
2316
|
+
if (config) applyTitle2(this);
|
|
2317
|
+
}
|
|
2318
|
+
/** Hand this tile its rows; null is the empty state. */
|
|
2319
|
+
setData(data) {
|
|
2320
|
+
if (this._disposed) return;
|
|
2321
|
+
this.data = data ?? null;
|
|
2322
|
+
if (this.contentElement) this.render(this.data);
|
|
2323
|
+
}
|
|
2324
|
+
/** The item drawn under `key`, or null. */
|
|
2325
|
+
itemFor(key) {
|
|
2326
|
+
return this._items.get(String(key)) ?? null;
|
|
2327
|
+
}
|
|
2328
|
+
/** The element of the item drawn under `key`, or null. */
|
|
2329
|
+
rowElement(key) {
|
|
2330
|
+
const want = String(key);
|
|
2331
|
+
return [...this.contentElement?.querySelectorAll("[data-row-key]") || []].find((r) => r.dataset.rowKey === want) || null;
|
|
2332
|
+
}
|
|
2333
|
+
render(data) {
|
|
2334
|
+
const content = this.contentElement;
|
|
2335
|
+
if (!content) return;
|
|
2336
|
+
const norm = normalize(data, this.constructor._KEY);
|
|
2337
|
+
this._items.clear();
|
|
2338
|
+
if (!norm) {
|
|
2339
|
+
this.showEmpty("Nothing to show.");
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
const max = Math.max(0, Math.floor(Number(this.config.max_rows) || 0));
|
|
2343
|
+
const shown = max ? norm.items.slice(0, max) : norm.items;
|
|
2344
|
+
const box = el("div", `fd-list-tile fd-list-tile--${this.constructor._KEY}`);
|
|
2345
|
+
const bar = el("div", "fd-list-tile__bar");
|
|
2346
|
+
const count = el(
|
|
2347
|
+
"span",
|
|
2348
|
+
"fd-list-tile__count",
|
|
2349
|
+
shown.length < norm.total ? `${shown.length} of ${norm.total}` : String(norm.total)
|
|
2350
|
+
);
|
|
2351
|
+
count.title = `${norm.total} in all`;
|
|
2352
|
+
bar.append(count, el("span", "fd-list-tile__spacer"));
|
|
2353
|
+
if (norm.action) {
|
|
2354
|
+
const b = el("button", "fd-list-tile__action");
|
|
2355
|
+
b.type = "button";
|
|
2356
|
+
b.dataset.listAction = "action";
|
|
2357
|
+
if (norm.action.icon) b.append(glyph(norm.action.icon));
|
|
2358
|
+
b.append(el("span", null, norm.action.label));
|
|
2359
|
+
bar.append(b);
|
|
2360
|
+
}
|
|
2361
|
+
box.append(bar);
|
|
2362
|
+
const list = el("div", "fd-list-tile__rows");
|
|
2363
|
+
list.setAttribute("role", "list");
|
|
2364
|
+
if (!shown.length) {
|
|
2365
|
+
list.append(el("div", "fd-list-tile__empty", norm.empty || this.config.empty_text || "Nothing here."));
|
|
2366
|
+
}
|
|
2367
|
+
for (const item of shown) {
|
|
2368
|
+
const key = String(item.key);
|
|
2369
|
+
if (this._items.has(key)) continue;
|
|
2370
|
+
this._items.set(key, item);
|
|
2371
|
+
const row = this._renderItem(item);
|
|
2372
|
+
row.classList.add("fd-list-tile__row");
|
|
2373
|
+
row.dataset.rowKey = key;
|
|
2374
|
+
row.setAttribute("role", "button");
|
|
2375
|
+
row.tabIndex = 0;
|
|
2376
|
+
if (this.rowsDraggable) row.draggable = true;
|
|
2377
|
+
const wrap = el("div", "fd-list-tile__item");
|
|
2378
|
+
wrap.setAttribute("role", "listitem");
|
|
2379
|
+
wrap.append(row);
|
|
2380
|
+
list.append(wrap);
|
|
2381
|
+
}
|
|
2382
|
+
box.append(list);
|
|
2383
|
+
if (norm.note) {
|
|
2384
|
+
const note = el("div", "fd-list-tile__note", norm.note);
|
|
2385
|
+
note.setAttribute("role", "note");
|
|
2386
|
+
note.title = norm.note;
|
|
2387
|
+
box.append(note);
|
|
2388
|
+
}
|
|
2389
|
+
content.replaceChildren(box);
|
|
2390
|
+
}
|
|
2391
|
+
/** One item's row element. @abstract */
|
|
2392
|
+
_renderItem(_item) {
|
|
2393
|
+
return el("div");
|
|
2394
|
+
}
|
|
2395
|
+
// ── input ───────────────────────────────────────────────────────────
|
|
2396
|
+
_rowOf(target) {
|
|
2397
|
+
const row = target?.closest?.("[data-row-key]");
|
|
2398
|
+
return row && this.contentElement?.contains(row) ? row : null;
|
|
2399
|
+
}
|
|
2400
|
+
_click(e) {
|
|
2401
|
+
if (e.target?.closest?.("[data-list-action]")) {
|
|
2402
|
+
this._call(this.onAction, { tile: this, event: e });
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const row = this._rowOf(e.target);
|
|
2406
|
+
if (row) this._activate(row, e);
|
|
2407
|
+
}
|
|
2408
|
+
_activate(rowEl, event) {
|
|
2409
|
+
const row = this.itemFor(rowEl.dataset.rowKey);
|
|
2410
|
+
if (row) this._call(this.onRowActivate, { row, event, tile: this });
|
|
2411
|
+
}
|
|
2412
|
+
_key(e) {
|
|
2413
|
+
const rowEl = this._rowOf(e.target);
|
|
2414
|
+
if (!rowEl) return;
|
|
2415
|
+
if (e.key === "ContextMenu" || e.key === "F10" && e.shiftKey) {
|
|
2416
|
+
const r = rowEl.getBoundingClientRect();
|
|
2417
|
+
if (this._openMenu(rowEl, e, r.left + 16, r.bottom)) e.preventDefault();
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if ((e.key === "Enter" || e.key === " ") && !e.altKey) {
|
|
2421
|
+
e.preventDefault();
|
|
2422
|
+
this._activate(rowEl, e);
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const rows = [...this.contentElement.querySelectorAll("[data-row-key]")];
|
|
2426
|
+
const i = rows.indexOf(rowEl);
|
|
2427
|
+
const to = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: rows.length - 1 }[e.key];
|
|
2428
|
+
if (to === void 0 || !rows[to]) return;
|
|
2429
|
+
e.preventDefault();
|
|
2430
|
+
rows[to].focus();
|
|
2431
|
+
}
|
|
2432
|
+
_menu(e) {
|
|
2433
|
+
const rowEl = this._rowOf(e.target);
|
|
2434
|
+
if (rowEl && this._openMenu(rowEl, e, e.clientX, e.clientY)) e.preventDefault();
|
|
2435
|
+
}
|
|
2436
|
+
/** Show the row's menu, if it has one. → whether it did. */
|
|
2437
|
+
_openMenu(rowEl, event, x, y) {
|
|
2438
|
+
const row = this.itemFor(rowEl.dataset.rowKey);
|
|
2439
|
+
if (!row || typeof this.rowMenu !== "function") return false;
|
|
2440
|
+
let items = null;
|
|
2441
|
+
try {
|
|
2442
|
+
items = this.rowMenu({ row, event, tile: this });
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
console.error(`[${this.constructor.name}] rowMenu threw:`, err);
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
if (!Array.isArray(items) || !items.length) return false;
|
|
2448
|
+
showContextMenu(x, y, items, (action) => this._call(this.onRowMenuAction, { action, row, tile: this }));
|
|
2449
|
+
return true;
|
|
2450
|
+
}
|
|
2451
|
+
/** A hook, called so that one that throws is reported and the tile goes on. */
|
|
2452
|
+
_call(fn, arg) {
|
|
2453
|
+
if (typeof fn !== "function") return;
|
|
2454
|
+
try {
|
|
2455
|
+
fn(arg);
|
|
2456
|
+
} catch (err) {
|
|
2457
|
+
console.error(`[${this.constructor.name}] a callback threw:`, err);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
dispose() {
|
|
2461
|
+
const c = this.contentElement;
|
|
2462
|
+
if (c) {
|
|
2463
|
+
c.removeEventListener("click", this._onClick);
|
|
2464
|
+
c.removeEventListener("keydown", this._onKey);
|
|
2465
|
+
c.removeEventListener("contextmenu", this._onMenu);
|
|
2466
|
+
}
|
|
2467
|
+
this._items.clear();
|
|
2468
|
+
super.dispose();
|
|
2469
|
+
}
|
|
2470
|
+
};
|
|
2471
|
+
var ListTile = class extends RowsTile {
|
|
2472
|
+
static TYPE = "flexdesk.list";
|
|
2473
|
+
static TITLE = "List";
|
|
2474
|
+
static ICON = "list";
|
|
2475
|
+
static DESCRIPTION = "Rows you supply, with a count, a click and a right-click menu per row.";
|
|
2476
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
2477
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
|
|
2478
|
+
static _KEY = "rows";
|
|
2479
|
+
_renderItem(r) {
|
|
2480
|
+
const row = el("div", r.className ? String(r.className) : null);
|
|
2481
|
+
if (r.icon) {
|
|
2482
|
+
const g = glyph(r.icon, "fd-list-tile__icon");
|
|
2483
|
+
if (r.iconTitle) {
|
|
2484
|
+
g.title = r.iconTitle;
|
|
2485
|
+
g.removeAttribute("aria-hidden");
|
|
2486
|
+
}
|
|
2487
|
+
row.append(g);
|
|
2488
|
+
}
|
|
2489
|
+
if (r.lead != null && r.lead !== "") row.append(el("span", "fd-list-tile__lead", r.lead));
|
|
2490
|
+
const title = el("span", "fd-list-tile__title", r.title ?? "");
|
|
2491
|
+
title.title = r.tip || String(r.title ?? "");
|
|
2492
|
+
row.append(title);
|
|
2493
|
+
for (const c of Array.isArray(r.cells) ? r.cells : []) {
|
|
2494
|
+
if (!c) continue;
|
|
2495
|
+
const cell = el("span", `fd-list-tile__cell${toneClass("fd-list-tile__cell", c.tone)}${c.className ? ` ${c.className}` : ""}`);
|
|
2496
|
+
if (c.icon) cell.append(glyph(c.icon));
|
|
2497
|
+
if (c.text != null && c.text !== "") cell.append(document.createTextNode(String(c.text)));
|
|
2498
|
+
if (c.title) cell.title = c.title;
|
|
2499
|
+
row.append(cell);
|
|
2500
|
+
}
|
|
2501
|
+
return row;
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
var PeopleTile = class extends RowsTile {
|
|
2505
|
+
static TYPE = "flexdesk.people";
|
|
2506
|
+
static TITLE = "People";
|
|
2507
|
+
static ICON = "groups";
|
|
2508
|
+
static DESCRIPTION = "One row per person, with how much is on them.";
|
|
2509
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
2510
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
|
|
2511
|
+
static _KEY = "people";
|
|
2512
|
+
getDefaultConfig() {
|
|
2513
|
+
return { ...super.getDefaultConfig(), count_label: "open" };
|
|
2514
|
+
}
|
|
2515
|
+
getConfigSchema() {
|
|
2516
|
+
const { fields } = super.getConfigSchema();
|
|
2517
|
+
return { fields: [...fields, {
|
|
2518
|
+
key: "count_label",
|
|
2519
|
+
type: "text",
|
|
2520
|
+
label: "Count reads",
|
|
2521
|
+
description: `After each person's number, e.g. "open" for "3 open".`
|
|
2522
|
+
}] };
|
|
2523
|
+
}
|
|
2524
|
+
_renderItem(p) {
|
|
2525
|
+
const row = el("div", "fd-people-tile__row");
|
|
2526
|
+
row.append(glyph(p.icon || (p.muted ? "person_off" : "person"), "fd-list-tile__icon"));
|
|
2527
|
+
row.append(el("span", `fd-people-tile__name${p.muted ? " fd-people-tile__name--muted" : ""}`, p.name ?? p.key));
|
|
2528
|
+
const pills = el("span", "fd-people-tile__pills");
|
|
2529
|
+
for (const pill of Array.isArray(p.pills) ? p.pills : []) {
|
|
2530
|
+
if (!pill?.text) continue;
|
|
2531
|
+
const s = el("span", `fd-people-tile__pill${toneClass("fd-people-tile__pill", pill.tone)}`, pill.text);
|
|
2532
|
+
if (pill.title) s.title = pill.title;
|
|
2533
|
+
pills.append(s);
|
|
2534
|
+
}
|
|
2535
|
+
row.append(pills);
|
|
2536
|
+
const label = p.countLabel ?? this.config.count_label ?? "";
|
|
2537
|
+
row.append(el("span", "fd-people-tile__count", `${Number(p.count) || 0}${label ? ` ${label}` : ""}`));
|
|
2538
|
+
if (p.title) row.title = p.title;
|
|
2539
|
+
return row;
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
function groupByPerson(items, personOf) {
|
|
2543
|
+
const by = /* @__PURE__ */ new Map();
|
|
2544
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
2545
|
+
const who = String(personOf(item) || "");
|
|
2546
|
+
if (!by.has(who)) by.set(who, { key: who, items: [] });
|
|
2547
|
+
by.get(who).items.push(item);
|
|
2548
|
+
}
|
|
2549
|
+
return [...by.values()].sort((a, b) => b.items.length - a.items.length || (a.key === "") - (b.key === "") || a.key.localeCompare(b.key));
|
|
2550
|
+
}
|
|
2551
|
+
function registerListTiles() {
|
|
2552
|
+
for (const Tile of [ListTile, PeopleTile]) {
|
|
2553
|
+
if (getWidget(Tile.TYPE) !== Tile) registerWidget(Tile);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function applyTitle2(tile) {
|
|
2557
|
+
tile.setTitle(tile.config.title || tile.constructor.TITLE);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/tiles/scope_picker.js
|
|
2561
|
+
function createScopePicker({ options = [], value, label = "Scope", onChange } = {}) {
|
|
2562
|
+
const opts = options.filter((o) => o && o.value != null).map((o) => ({ ...o, value: String(o.value) }));
|
|
2563
|
+
const known = (v) => opts.some((o) => o.value === String(v));
|
|
2564
|
+
let current = known(value) ? String(value) : opts[0]?.value ?? "";
|
|
2565
|
+
const element = document.createElement("span");
|
|
2566
|
+
element.className = "fd-scope-picker";
|
|
2567
|
+
element.setAttribute("role", "group");
|
|
2568
|
+
element.setAttribute("aria-label", label);
|
|
2569
|
+
const buttons = opts.map((o) => {
|
|
2570
|
+
const b = document.createElement("button");
|
|
2571
|
+
b.type = "button";
|
|
2572
|
+
b.className = "fd-scope-picker__btn";
|
|
2573
|
+
b.dataset.scope = o.value;
|
|
2574
|
+
b.textContent = o.label ?? o.value;
|
|
2575
|
+
if (o.hint) b.title = o.hint;
|
|
2576
|
+
element.append(b);
|
|
2577
|
+
return b;
|
|
2578
|
+
});
|
|
2579
|
+
const paint = () => {
|
|
2580
|
+
for (const b of buttons) {
|
|
2581
|
+
const on = b.dataset.scope === current;
|
|
2582
|
+
b.classList.toggle("fd-scope-picker__btn--on", on);
|
|
2583
|
+
b.setAttribute("aria-pressed", String(on));
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
paint();
|
|
2587
|
+
const onClick = (e) => {
|
|
2588
|
+
const b = e.target?.closest?.("[data-scope]");
|
|
2589
|
+
if (!b || !element.contains(b) || b.dataset.scope === current) return;
|
|
2590
|
+
current = b.dataset.scope;
|
|
2591
|
+
paint();
|
|
2592
|
+
try {
|
|
2593
|
+
onChange?.(current);
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
console.error("[ScopePicker] onChange threw:", err);
|
|
2596
|
+
}
|
|
2597
|
+
};
|
|
2598
|
+
const onKey = (e) => {
|
|
2599
|
+
const i = buttons.indexOf(e.target);
|
|
2600
|
+
if (i < 0) return;
|
|
2601
|
+
const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: buttons.length - 1 }[e.key];
|
|
2602
|
+
if (to === void 0 || !buttons[to]) return;
|
|
2603
|
+
e.preventDefault();
|
|
2604
|
+
buttons[to].focus();
|
|
2605
|
+
};
|
|
2606
|
+
element.addEventListener("click", onClick);
|
|
2607
|
+
element.addEventListener("keydown", onKey);
|
|
2608
|
+
return {
|
|
2609
|
+
element,
|
|
2610
|
+
get value() {
|
|
2611
|
+
return current;
|
|
2612
|
+
},
|
|
2613
|
+
/** Press `v` without calling `onChange` (a value restored from elsewhere). */
|
|
2614
|
+
setValue(v) {
|
|
2615
|
+
if (!known(v)) return;
|
|
2616
|
+
current = String(v);
|
|
2617
|
+
paint();
|
|
2618
|
+
},
|
|
2619
|
+
dispose() {
|
|
2620
|
+
element.removeEventListener("click", onClick);
|
|
2621
|
+
element.removeEventListener("keydown", onKey);
|
|
2622
|
+
element.remove();
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
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
|
+
|
|
2142
2967
|
// src/tiles/tile_grid.js
|
|
2143
2968
|
init_tile_registry();
|
|
2144
2969
|
var TileGrid = class {
|
|
@@ -2157,6 +2982,9 @@ var TileGrid = class {
|
|
|
2157
2982
|
* @param {{resolve: Function}} [options.dataSource] - Per-tile data provider,
|
|
2158
2983
|
* handed to every tile. With none, the grid is the broadcast-only grid
|
|
2159
2984
|
* it has always been and no tile is ever asked to load anything.
|
|
2985
|
+
* @param {*} [options.scope=null] - The board's scope, a filter every tile
|
|
2986
|
+
* follows (0.16.1); see `setScope`.
|
|
2987
|
+
* @param {Function} [options.onScopeChange] - `(scope)` after `setScope` changed it.
|
|
2160
2988
|
*/
|
|
2161
2989
|
constructor({
|
|
2162
2990
|
container,
|
|
@@ -2179,9 +3007,13 @@ var TileGrid = class {
|
|
|
2179
3007
|
stateGuard = null,
|
|
2180
3008
|
// Injected, never constructed here: the grid does not know what
|
|
2181
3009
|
// a binding is, only that a tile may have somewhere to ask.
|
|
2182
|
-
dataSource = null
|
|
3010
|
+
dataSource = null,
|
|
3011
|
+
scope = null,
|
|
3012
|
+
onScopeChange = null
|
|
2183
3013
|
}) {
|
|
2184
3014
|
this.host = host;
|
|
3015
|
+
this.scope = scope;
|
|
3016
|
+
this._onScopeChangeCallback = onScopeChange;
|
|
2185
3017
|
this.stateGuard = stateGuard;
|
|
2186
3018
|
this.dataSource = dataSource;
|
|
2187
3019
|
this.container = container;
|
|
@@ -2304,6 +3136,7 @@ var TileGrid = class {
|
|
|
2304
3136
|
dataSource: this.dataSource
|
|
2305
3137
|
});
|
|
2306
3138
|
if (!tile) return null;
|
|
3139
|
+
tile.scope = this.scope;
|
|
2307
3140
|
const layoutEntry = {
|
|
2308
3141
|
id: tileId,
|
|
2309
3142
|
type,
|
|
@@ -2374,6 +3207,39 @@ var TileGrid = class {
|
|
|
2374
3207
|
tile.loadData?.({ force: true });
|
|
2375
3208
|
});
|
|
2376
3209
|
}
|
|
3210
|
+
/**
|
|
3211
|
+
* THE BOARD'S SCOPE: one filter every tile follows ("Everyone", "Assigned
|
|
3212
|
+
* by me", …), set from a `createScopePicker` or from anything else.
|
|
3213
|
+
*
|
|
3214
|
+
* The grid only carries the value; what it means is the embedder's. Every
|
|
3215
|
+
* tile gets it as `tile.scope` and through `tile.onScopeChange(scope)`, the
|
|
3216
|
+
* grid's `onScopeChange(scope)` is called, and a grid with a data source
|
|
3217
|
+
* reloads its tiles, since their rows depend on it. The scope is not part
|
|
3218
|
+
* of the layout: it is how the board is being looked at, not what is on it.
|
|
3219
|
+
*
|
|
3220
|
+
* @param {*} scope - Compared by JSON: setting the same scope again does nothing.
|
|
3221
|
+
* @returns {boolean} whether it changed
|
|
3222
|
+
*/
|
|
3223
|
+
setScope(scope) {
|
|
3224
|
+
if (JSON.stringify(scope ?? null) === JSON.stringify(this.scope ?? null)) return false;
|
|
3225
|
+
this.scope = scope ?? null;
|
|
3226
|
+
this.tiles.forEach((tile) => {
|
|
3227
|
+
tile.scope = this.scope;
|
|
3228
|
+
try {
|
|
3229
|
+
tile.onScopeChange?.(this.scope);
|
|
3230
|
+
} catch (err) {
|
|
3231
|
+
console.error("[TileGrid] a tile's onScopeChange threw:", err);
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
3234
|
+
try {
|
|
3235
|
+
this._onScopeChangeCallback?.(this.scope);
|
|
3236
|
+
} catch (err) {
|
|
3237
|
+
console.error("[TileGrid] onScopeChange threw:", err);
|
|
3238
|
+
}
|
|
3239
|
+
this.eventBus?.emit?.("grid:scope-changed", { scope: this.scope });
|
|
3240
|
+
if (this.dataSource) this.reloadAll();
|
|
3241
|
+
return true;
|
|
3242
|
+
}
|
|
2377
3243
|
/**
|
|
2378
3244
|
* Get current layout.
|
|
2379
3245
|
* @returns {Array} Layout array
|
|
@@ -2770,6 +3636,8 @@ var TileGrid = class {
|
|
|
2770
3636
|
tileType,
|
|
2771
3637
|
config,
|
|
2772
3638
|
schema,
|
|
3639
|
+
// What `getFieldOptions` reads for a field with dynamic options.
|
|
3640
|
+
data: this.data,
|
|
2773
3641
|
variables: this.data ? this._getVariableList() : [],
|
|
2774
3642
|
onSave: (newConfig) => {
|
|
2775
3643
|
const tile = this.tiles.get(tileId);
|
|
@@ -2896,12 +3764,16 @@ export {
|
|
|
2896
3764
|
CHART_TILE_TYPES,
|
|
2897
3765
|
ChartTile,
|
|
2898
3766
|
FieldTypes,
|
|
3767
|
+
FigureTile,
|
|
2899
3768
|
KpiTile,
|
|
2900
3769
|
LayoutPersistence,
|
|
3770
|
+
ListTile,
|
|
3771
|
+
PeopleTile,
|
|
2901
3772
|
TileBase,
|
|
2902
3773
|
TileGrid,
|
|
2903
3774
|
TileRegistry,
|
|
2904
3775
|
configureLayoutPersistence,
|
|
3776
|
+
createScopePicker,
|
|
2905
3777
|
createWidget,
|
|
2906
3778
|
formatVariableLabel,
|
|
2907
3779
|
getAllWidgetTypes,
|
|
@@ -2912,10 +3784,16 @@ export {
|
|
|
2912
3784
|
getWidget,
|
|
2913
3785
|
getWidgetCatalog,
|
|
2914
3786
|
getWidgetMetadata,
|
|
3787
|
+
groupByPerson,
|
|
2915
3788
|
hasWidget,
|
|
2916
3789
|
mergeWithDefaults,
|
|
3790
|
+
mountTileConfigForm,
|
|
2917
3791
|
normalizeChartData,
|
|
3792
|
+
normalizeListData,
|
|
3793
|
+
normalizePeopleData,
|
|
3794
|
+
openTileConfig,
|
|
2918
3795
|
registerChartTiles,
|
|
3796
|
+
registerListTiles,
|
|
2919
3797
|
registerWidget,
|
|
2920
3798
|
setTileLogger,
|
|
2921
3799
|
validateConfig,
|