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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list_tiles.js — a LIST tile and a PEOPLE tile for a `TileGrid` (0.16.1).
|
|
3
|
+
*
|
|
4
|
+
* Both are rows you hand over through `setData()`, the way the chart tile takes
|
|
5
|
+
* series: which records are in the list, how they sort and what a row says is
|
|
6
|
+
* the embedder's job, because only the embedder knows what a record is. What a
|
|
7
|
+
* tile here owns is everything a list on a board needs and every embedder would
|
|
8
|
+
* otherwise write again: the count, the empty line, the "open all of these"
|
|
9
|
+
* button, a click, the keyboard, and a right-click menu per row.
|
|
10
|
+
*
|
|
11
|
+
* import { registerListTiles } from '@flexdesk/tiles';
|
|
12
|
+
* registerListTiles(); // idempotent; nothing registers at import
|
|
13
|
+
* const tile = grid.addTile('flexdesk.list', {x: 0, y: 0, w: 6, h: 4},
|
|
14
|
+
* { title: 'Overdue', empty_text: 'Nothing is late.' });
|
|
15
|
+
* tile.onRowActivate = ({ row, event }) => open(row.key, event.ctrlKey);
|
|
16
|
+
* tile.rowMenu = ({ row }) => [{ label: 'Open', icon: 'open_in_new', action: 'open' }];
|
|
17
|
+
* tile.onRowMenuAction = ({ action, row }) => { … };
|
|
18
|
+
* tile.setData({
|
|
19
|
+
* rows: [{ key: '7', icon: 'task', lead: 'TASK-0007', title: 'Sign-off',
|
|
20
|
+
* cells: [{ text: 'priya' }, { text: '9 days late', tone: 'danger' }] }],
|
|
21
|
+
* total: 12, // "7 of 12" when only 7 are handed over
|
|
22
|
+
* action: { label: 'Open as a list' }, // the button in the bar; see onAction
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* ══ THE DATA ════════════════════════════════════════════════════════════
|
|
26
|
+
*
|
|
27
|
+
* list { rows: [Row], total?, note?, empty?, action?: {label, icon?} } (an array is {rows})
|
|
28
|
+
* Row = { key, title, icon?, iconTitle?, lead?, className?, tip?,
|
|
29
|
+
* cells?: [{ text, title?, tone?, icon?, className? }] }
|
|
30
|
+
* people { people: [Person], total?, note?, empty?, action? } (an array is {people})
|
|
31
|
+
* Person = { key, name, icon?, muted?, count, countLabel?, title?,
|
|
32
|
+
* pills?: [{ text, tone?, title? }] }
|
|
33
|
+
*
|
|
34
|
+
* `tone` is one of `danger`, `warning`, `ok`, `muted` and only picks a colour;
|
|
35
|
+
* the text says the same thing, because colour alone does not survive a
|
|
36
|
+
* monochrome display. `className` is the embedder's own class, added as it is,
|
|
37
|
+
* so a status pill can keep the app's look.
|
|
38
|
+
*
|
|
39
|
+
* AN EMPTY LIST IS NOT THE EMPTY STATE. `{rows: []}` keeps the bar (count 0,
|
|
40
|
+
* the button) and says `empty` (or `config.empty_text`) where the rows would be:
|
|
41
|
+
* "nothing is late" is the most useful thing a list of late work can say, and a
|
|
42
|
+
* tile that shows a grey inbox icon instead cannot say it. `setData(null)` is
|
|
43
|
+
* the empty state; `showEmpty(message)` is how an embedder says "not set up".
|
|
44
|
+
*
|
|
45
|
+
* ══ INPUT ═══════════════════════════════════════════════════════════════
|
|
46
|
+
*
|
|
47
|
+
* A row is `role="button"` with a tab stop. Click, Enter and Space call
|
|
48
|
+
* `onRowActivate({row, event, tile})` with the event, so the modifier keys
|
|
49
|
+
* decide how it opens. Up and Down move between rows, Home and End to the ends.
|
|
50
|
+
* Right-click, the context-menu key and Shift+F10 open the row's menu:
|
|
51
|
+
* `rowMenu({row, event, tile})` answers the items (`showContextMenu`'s; none or
|
|
52
|
+
* `[]` means no menu, and the browser's own is left alone) and the choice goes
|
|
53
|
+
* to `onRowMenuAction({action, row, tile})`. The bar's button calls
|
|
54
|
+
* `onAction({tile, event})`. Every hook can also be passed to the constructor.
|
|
55
|
+
* A row carries `data-row-key`, so an embedder can find it for its own
|
|
56
|
+
* listeners (a drag source, say); `rowsDraggable` makes rows `draggable`.
|
|
57
|
+
*
|
|
58
|
+
* The header reads `config.title`, else the type's title; the count is in the
|
|
59
|
+
* bar, not the header, so a title the embedder sets is never rewritten.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { TileBase } from './tile_base.js';
|
|
63
|
+
import { registerWidget, getWidget } from './tile_registry.js';
|
|
64
|
+
import { showContextMenu } from '../ui/components/context_menu.js';
|
|
65
|
+
|
|
66
|
+
const TONES = new Set(['danger', 'warning', 'ok', 'muted']);
|
|
67
|
+
|
|
68
|
+
/** `{items, total, note, empty, action}` from what `setData` was handed under
|
|
69
|
+
* `key` (`rows` or `people`), or null when there is nothing at all. */
|
|
70
|
+
function normalize(data, key) {
|
|
71
|
+
if (data == null) return null;
|
|
72
|
+
const d = Array.isArray(data) ? { [key]: data } : data;
|
|
73
|
+
if (typeof d !== 'object' || !Array.isArray(d[key])) return null;
|
|
74
|
+
const items = d[key].filter((r) => r && typeof r === 'object' && r.key != null);
|
|
75
|
+
const total = Number.isFinite(Number(d.total)) && d.total !== null && d.total !== '' ? Number(d.total) : items.length;
|
|
76
|
+
return {
|
|
77
|
+
items,
|
|
78
|
+
total: Math.max(total, items.length),
|
|
79
|
+
note: typeof d.note === 'string' ? d.note : '',
|
|
80
|
+
empty: typeof d.empty === 'string' ? d.empty : '',
|
|
81
|
+
action: d.action && typeof d.action === 'object' && d.action.label ? d.action : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Exported for tests and for an embedder that wants to know what the tile will make of its data. */
|
|
86
|
+
export const normalizeListData = (data) => normalize(data, 'rows');
|
|
87
|
+
export const normalizePeopleData = (data) => normalize(data, 'people');
|
|
88
|
+
|
|
89
|
+
function el(tag, cls, text) {
|
|
90
|
+
const e = document.createElement(tag);
|
|
91
|
+
if (cls) e.className = cls;
|
|
92
|
+
if (text != null) e.textContent = String(text);
|
|
93
|
+
return e;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function glyph(name, cls = '') {
|
|
97
|
+
const g = el('span', `material-symbols-outlined${cls ? ` ${cls}` : ''}`, name);
|
|
98
|
+
g.setAttribute('aria-hidden', 'true');
|
|
99
|
+
return g;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const toneClass = (base, tone) => (TONES.has(tone) ? ` ${base}--${tone}` : '');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The rows, the bar and the input, shared by both tiles. A subclass says what
|
|
106
|
+
* its items are called in the data (`_KEY`) and draws one (`_renderItem`).
|
|
107
|
+
* @abstract
|
|
108
|
+
*/
|
|
109
|
+
class RowsTile extends TileBase {
|
|
110
|
+
static _KEY = 'rows';
|
|
111
|
+
|
|
112
|
+
constructor(options = {}) {
|
|
113
|
+
super(options);
|
|
114
|
+
this.onRowActivate = typeof options.onRowActivate === 'function' ? options.onRowActivate : null;
|
|
115
|
+
this.rowMenu = typeof options.rowMenu === 'function' ? options.rowMenu : null;
|
|
116
|
+
this.onRowMenuAction = typeof options.onRowMenuAction === 'function' ? options.onRowMenuAction : null;
|
|
117
|
+
this.onAction = typeof options.onAction === 'function' ? options.onAction : null;
|
|
118
|
+
this.rowsDraggable = options.rowsDraggable === true;
|
|
119
|
+
this._items = new Map(); // key → the item drawn for it
|
|
120
|
+
this._onClick = (e) => this._click(e);
|
|
121
|
+
this._onKey = (e) => this._key(e);
|
|
122
|
+
this._onMenu = (e) => this._menu(e);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Pure and static-only — it runs inside `TileBase`'s constructor. */
|
|
126
|
+
getDefaultConfig() {
|
|
127
|
+
return { title: '', empty_text: '', max_rows: 0 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getConfigSchema() {
|
|
131
|
+
return {
|
|
132
|
+
fields: [
|
|
133
|
+
{ key: 'title', type: 'text', label: 'Tile title' },
|
|
134
|
+
{ key: 'empty_text', type: 'text', label: 'When there is nothing',
|
|
135
|
+
description: 'What the tile says when the list is empty, e.g. "Nothing is late."' },
|
|
136
|
+
{ key: 'max_rows', type: 'number', label: 'Rows at most', min: 0,
|
|
137
|
+
description: '0 shows them all; the tile scrolls.' },
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
mount(container) {
|
|
143
|
+
super.mount(container);
|
|
144
|
+
applyTitle(this);
|
|
145
|
+
const c = this.contentElement;
|
|
146
|
+
if (!c) return;
|
|
147
|
+
// On the content element, which outlives every render (they replace
|
|
148
|
+
// its children, never it), so one set of listeners serves them all.
|
|
149
|
+
c.addEventListener('click', this._onClick);
|
|
150
|
+
c.addEventListener('keydown', this._onKey);
|
|
151
|
+
c.addEventListener('contextmenu', this._onMenu);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `null` is "the grid has no data" — see `ChartTile.update`. */
|
|
155
|
+
update(data, config) {
|
|
156
|
+
if (this._disposed) return;
|
|
157
|
+
super.update(data === null ? undefined : data, config);
|
|
158
|
+
if (config) applyTitle(this);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Hand this tile its rows; null is the empty state. */
|
|
162
|
+
setData(data) {
|
|
163
|
+
if (this._disposed) return;
|
|
164
|
+
this.data = data ?? null;
|
|
165
|
+
if (this.contentElement) this.render(this.data);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The item drawn under `key`, or null. */
|
|
169
|
+
itemFor(key) { return this._items.get(String(key)) ?? null; }
|
|
170
|
+
|
|
171
|
+
/** The element of the item drawn under `key`, or null. */
|
|
172
|
+
rowElement(key) {
|
|
173
|
+
const want = String(key);
|
|
174
|
+
return [...(this.contentElement?.querySelectorAll('[data-row-key]') || [])]
|
|
175
|
+
.find((r) => r.dataset.rowKey === want) || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
render(data) {
|
|
179
|
+
const content = this.contentElement;
|
|
180
|
+
if (!content) return;
|
|
181
|
+
const norm = normalize(data, this.constructor._KEY);
|
|
182
|
+
this._items.clear();
|
|
183
|
+
if (!norm) { this.showEmpty('Nothing to show.'); return; }
|
|
184
|
+
|
|
185
|
+
const max = Math.max(0, Math.floor(Number(this.config.max_rows) || 0));
|
|
186
|
+
const shown = max ? norm.items.slice(0, max) : norm.items;
|
|
187
|
+
const box = el('div', `fd-list-tile fd-list-tile--${this.constructor._KEY}`);
|
|
188
|
+
|
|
189
|
+
const bar = el('div', 'fd-list-tile__bar');
|
|
190
|
+
const count = el('span', 'fd-list-tile__count',
|
|
191
|
+
shown.length < norm.total ? `${shown.length} of ${norm.total}` : String(norm.total));
|
|
192
|
+
count.title = `${norm.total} in all`;
|
|
193
|
+
bar.append(count, el('span', 'fd-list-tile__spacer'));
|
|
194
|
+
if (norm.action) {
|
|
195
|
+
const b = el('button', 'fd-list-tile__action');
|
|
196
|
+
b.type = 'button';
|
|
197
|
+
b.dataset.listAction = 'action';
|
|
198
|
+
if (norm.action.icon) b.append(glyph(norm.action.icon));
|
|
199
|
+
b.append(el('span', null, norm.action.label));
|
|
200
|
+
bar.append(b);
|
|
201
|
+
}
|
|
202
|
+
box.append(bar);
|
|
203
|
+
|
|
204
|
+
const list = el('div', 'fd-list-tile__rows');
|
|
205
|
+
list.setAttribute('role', 'list');
|
|
206
|
+
if (!shown.length) {
|
|
207
|
+
list.append(el('div', 'fd-list-tile__empty', norm.empty || this.config.empty_text || 'Nothing here.'));
|
|
208
|
+
}
|
|
209
|
+
for (const item of shown) {
|
|
210
|
+
const key = String(item.key);
|
|
211
|
+
if (this._items.has(key)) continue; // one row per key: a second would be unreachable
|
|
212
|
+
this._items.set(key, item);
|
|
213
|
+
const row = this._renderItem(item);
|
|
214
|
+
row.classList.add('fd-list-tile__row');
|
|
215
|
+
row.dataset.rowKey = key;
|
|
216
|
+
row.setAttribute('role', 'button');
|
|
217
|
+
row.tabIndex = 0;
|
|
218
|
+
if (this.rowsDraggable) row.draggable = true;
|
|
219
|
+
const wrap = el('div', 'fd-list-tile__item');
|
|
220
|
+
wrap.setAttribute('role', 'listitem');
|
|
221
|
+
wrap.append(row);
|
|
222
|
+
list.append(wrap);
|
|
223
|
+
}
|
|
224
|
+
box.append(list);
|
|
225
|
+
if (norm.note) {
|
|
226
|
+
const note = el('div', 'fd-list-tile__note', norm.note);
|
|
227
|
+
note.setAttribute('role', 'note');
|
|
228
|
+
note.title = norm.note;
|
|
229
|
+
box.append(note);
|
|
230
|
+
}
|
|
231
|
+
content.replaceChildren(box);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** One item's row element. @abstract */
|
|
235
|
+
_renderItem(_item) { return el('div'); }
|
|
236
|
+
|
|
237
|
+
// ── input ───────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
_rowOf(target) {
|
|
240
|
+
const row = target?.closest?.('[data-row-key]');
|
|
241
|
+
return row && this.contentElement?.contains(row) ? row : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_click(e) {
|
|
245
|
+
if (e.target?.closest?.('[data-list-action]')) {
|
|
246
|
+
this._call(this.onAction, { tile: this, event: e });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const row = this._rowOf(e.target);
|
|
250
|
+
if (row) this._activate(row, e);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_activate(rowEl, event) {
|
|
254
|
+
const row = this.itemFor(rowEl.dataset.rowKey);
|
|
255
|
+
if (row) this._call(this.onRowActivate, { row, event, tile: this });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_key(e) {
|
|
259
|
+
const rowEl = this._rowOf(e.target);
|
|
260
|
+
if (!rowEl) return;
|
|
261
|
+
if (e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)) {
|
|
262
|
+
const r = rowEl.getBoundingClientRect();
|
|
263
|
+
if (this._openMenu(rowEl, e, r.left + 16, r.bottom)) e.preventDefault();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if ((e.key === 'Enter' || e.key === ' ') && !e.altKey) {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
this._activate(rowEl, e);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const rows = [...this.contentElement.querySelectorAll('[data-row-key]')];
|
|
272
|
+
const i = rows.indexOf(rowEl);
|
|
273
|
+
const to = { ArrowDown: i + 1, ArrowUp: i - 1, Home: 0, End: rows.length - 1 }[e.key];
|
|
274
|
+
if (to === undefined || !rows[to]) return;
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
rows[to].focus();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
_menu(e) {
|
|
280
|
+
const rowEl = this._rowOf(e.target);
|
|
281
|
+
if (rowEl && this._openMenu(rowEl, e, e.clientX, e.clientY)) e.preventDefault();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Show the row's menu, if it has one. → whether it did. */
|
|
285
|
+
_openMenu(rowEl, event, x, y) {
|
|
286
|
+
const row = this.itemFor(rowEl.dataset.rowKey);
|
|
287
|
+
if (!row || typeof this.rowMenu !== 'function') return false;
|
|
288
|
+
let items = null;
|
|
289
|
+
try { items = this.rowMenu({ row, event, tile: this }); }
|
|
290
|
+
catch (err) { console.error(`[${this.constructor.name}] rowMenu threw:`, err); return false; }
|
|
291
|
+
if (!Array.isArray(items) || !items.length) return false;
|
|
292
|
+
showContextMenu(x, y, items, (action) => this._call(this.onRowMenuAction, { action, row, tile: this }));
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A hook, called so that one that throws is reported and the tile goes on. */
|
|
297
|
+
_call(fn, arg) {
|
|
298
|
+
if (typeof fn !== 'function') return;
|
|
299
|
+
try { fn(arg); } catch (err) { console.error(`[${this.constructor.name}] a callback threw:`, err); }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
dispose() {
|
|
303
|
+
const c = this.contentElement;
|
|
304
|
+
if (c) {
|
|
305
|
+
c.removeEventListener('click', this._onClick);
|
|
306
|
+
c.removeEventListener('keydown', this._onKey);
|
|
307
|
+
c.removeEventListener('contextmenu', this._onMenu);
|
|
308
|
+
}
|
|
309
|
+
this._items.clear();
|
|
310
|
+
super.dispose();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A LIST OF RECORDS: an icon, a lead (a reference, say), the title, and cells.
|
|
316
|
+
* See the header for the data and the input.
|
|
317
|
+
*/
|
|
318
|
+
export class ListTile extends RowsTile {
|
|
319
|
+
static TYPE = 'flexdesk.list';
|
|
320
|
+
static TITLE = 'List';
|
|
321
|
+
static ICON = 'list';
|
|
322
|
+
static DESCRIPTION = 'Rows you supply, with a count, a click and a right-click menu per row.';
|
|
323
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
324
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
|
|
325
|
+
static _KEY = 'rows';
|
|
326
|
+
|
|
327
|
+
_renderItem(r) {
|
|
328
|
+
const row = el('div', r.className ? String(r.className) : null);
|
|
329
|
+
if (r.icon) {
|
|
330
|
+
const g = glyph(r.icon, 'fd-list-tile__icon');
|
|
331
|
+
if (r.iconTitle) { g.title = r.iconTitle; g.removeAttribute('aria-hidden'); }
|
|
332
|
+
row.append(g);
|
|
333
|
+
}
|
|
334
|
+
if (r.lead != null && r.lead !== '') row.append(el('span', 'fd-list-tile__lead', r.lead));
|
|
335
|
+
const title = el('span', 'fd-list-tile__title', r.title ?? '');
|
|
336
|
+
title.title = r.tip || String(r.title ?? '');
|
|
337
|
+
row.append(title);
|
|
338
|
+
for (const c of Array.isArray(r.cells) ? r.cells : []) {
|
|
339
|
+
if (!c) continue;
|
|
340
|
+
const cell = el('span', `fd-list-tile__cell${toneClass('fd-list-tile__cell', c.tone)}${c.className ? ` ${c.className}` : ''}`);
|
|
341
|
+
if (c.icon) cell.append(glyph(c.icon));
|
|
342
|
+
if (c.text != null && c.text !== '') cell.append(document.createTextNode(String(c.text)));
|
|
343
|
+
if (c.title) cell.title = c.title;
|
|
344
|
+
row.append(cell);
|
|
345
|
+
}
|
|
346
|
+
return row;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* WHO HAS WHAT: one row per person, with a count and pills. The order is the
|
|
352
|
+
* embedder's (the list is read top-down and stopped at, so the order should BE
|
|
353
|
+
* the priority); `groupByPerson` is a starting point for building it.
|
|
354
|
+
*/
|
|
355
|
+
export class PeopleTile extends RowsTile {
|
|
356
|
+
static TYPE = 'flexdesk.people';
|
|
357
|
+
static TITLE = 'People';
|
|
358
|
+
static ICON = 'groups';
|
|
359
|
+
static DESCRIPTION = 'One row per person, with how much is on them.';
|
|
360
|
+
static DEFAULT_SIZE = { w: 6, h: 4 };
|
|
361
|
+
static SIZE_CONSTRAINTS = { minW: 3, minH: 2, maxW: 12, maxH: 12 };
|
|
362
|
+
static _KEY = 'people';
|
|
363
|
+
|
|
364
|
+
getDefaultConfig() {
|
|
365
|
+
return { ...super.getDefaultConfig(), count_label: 'open' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getConfigSchema() {
|
|
369
|
+
const { fields } = super.getConfigSchema();
|
|
370
|
+
return { fields: [...fields, { key: 'count_label', type: 'text', label: 'Count reads',
|
|
371
|
+
description: 'After each person\'s number, e.g. "open" for "3 open".' }] };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_renderItem(p) {
|
|
375
|
+
const row = el('div', 'fd-people-tile__row');
|
|
376
|
+
row.append(glyph(p.icon || (p.muted ? 'person_off' : 'person'), 'fd-list-tile__icon'));
|
|
377
|
+
row.append(el('span', `fd-people-tile__name${p.muted ? ' fd-people-tile__name--muted' : ''}`, p.name ?? p.key));
|
|
378
|
+
const pills = el('span', 'fd-people-tile__pills');
|
|
379
|
+
for (const pill of Array.isArray(p.pills) ? p.pills : []) {
|
|
380
|
+
if (!pill?.text) continue;
|
|
381
|
+
const s = el('span', `fd-people-tile__pill${toneClass('fd-people-tile__pill', pill.tone)}`, pill.text);
|
|
382
|
+
if (pill.title) s.title = pill.title;
|
|
383
|
+
pills.append(s);
|
|
384
|
+
}
|
|
385
|
+
row.append(pills);
|
|
386
|
+
const label = p.countLabel ?? this.config.count_label ?? '';
|
|
387
|
+
row.append(el('span', 'fd-people-tile__count', `${Number(p.count) || 0}${label ? ` ${label}` : ''}`));
|
|
388
|
+
if (p.title) row.title = p.title;
|
|
389
|
+
return row;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Items grouped by person: `[{key, items}]`, most items first, then by key, the
|
|
395
|
+
* nobody group (key `''`) last among equals. `personOf(item)` answers the
|
|
396
|
+
* person's key, or a falsy value for nobody.
|
|
397
|
+
*/
|
|
398
|
+
export function groupByPerson(items, personOf) {
|
|
399
|
+
const by = new Map();
|
|
400
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
401
|
+
const who = String(personOf(item) || '');
|
|
402
|
+
if (!by.has(who)) by.set(who, { key: who, items: [] });
|
|
403
|
+
by.get(who).items.push(item);
|
|
404
|
+
}
|
|
405
|
+
return [...by.values()].sort((a, b) => b.items.length - a.items.length
|
|
406
|
+
|| (a.key === '') - (b.key === '') || a.key.localeCompare(b.key));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Register both tiles. Idempotent, and nothing registers at import time — see
|
|
411
|
+
* `registerChartTiles`.
|
|
412
|
+
*/
|
|
413
|
+
export function registerListTiles() {
|
|
414
|
+
for (const Tile of [ListTile, PeopleTile]) {
|
|
415
|
+
if (getWidget(Tile.TYPE) !== Tile) registerWidget(Tile);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** The header shows `config.title`, else the type's own title. */
|
|
420
|
+
function applyTitle(tile) {
|
|
421
|
+
tile.setTitle(tile.config.title || tile.constructor.TITLE);
|
|
422
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scope_picker.js — ONE FILTER FOR THE WHOLE BOARD (0.16.1).
|
|
3
|
+
*
|
|
4
|
+
* A row of buttons, one per scope ("Everyone", "Assigned by me", …), exactly
|
|
5
|
+
* one of them pressed. It is the control half of a dashboard-level filter; the
|
|
6
|
+
* other half is `TileGrid.setScope()`, which hands the value to every tile
|
|
7
|
+
* (`tile.scope`, `tile.onScopeChange`) and to the grid's `onScopeChange`:
|
|
8
|
+
*
|
|
9
|
+
* const picker = createScopePicker({
|
|
10
|
+
* options: [{ value: 'all', label: 'Everyone' },
|
|
11
|
+
* { value: 'mine', label: 'Mine', hint: 'only what is on me' }],
|
|
12
|
+
* value: 'all',
|
|
13
|
+
* onChange: (value) => grid.setScope(value),
|
|
14
|
+
* });
|
|
15
|
+
* header.append(picker.element);
|
|
16
|
+
*
|
|
17
|
+
* What a scope MEANS is the embedder's: the grid only carries the value, and a
|
|
18
|
+
* tile that knows nothing about scopes is unaffected by one.
|
|
19
|
+
*
|
|
20
|
+
* Each button has `aria-pressed`, the group a label. Left and Right move the
|
|
21
|
+
* focus along the row and Enter or Space presses, as on any button; a click on
|
|
22
|
+
* the button already pressed changes nothing and calls nothing.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Object} o
|
|
27
|
+
* @param {Array<{value: string, label: string, hint?: string}>} o.options
|
|
28
|
+
* @param {string} [o.value] - The pressed one; the first option when absent or unknown.
|
|
29
|
+
* @param {string} [o.label='Scope'] - The group's accessible name.
|
|
30
|
+
* @param {(value: string) => void} [o.onChange] - After the user presses another one.
|
|
31
|
+
* @returns {{element: HTMLElement, readonly value: string, setValue: (v: string) => void, dispose: () => void}}
|
|
32
|
+
*/
|
|
33
|
+
export function createScopePicker({ options = [], value, label = 'Scope', onChange } = {}) {
|
|
34
|
+
const opts = options.filter((o) => o && o.value != null).map((o) => ({ ...o, value: String(o.value) }));
|
|
35
|
+
const known = (v) => opts.some((o) => o.value === String(v));
|
|
36
|
+
let current = known(value) ? String(value) : opts[0]?.value ?? '';
|
|
37
|
+
|
|
38
|
+
const element = document.createElement('span');
|
|
39
|
+
element.className = 'fd-scope-picker';
|
|
40
|
+
element.setAttribute('role', 'group');
|
|
41
|
+
element.setAttribute('aria-label', label);
|
|
42
|
+
|
|
43
|
+
const buttons = opts.map((o) => {
|
|
44
|
+
const b = document.createElement('button');
|
|
45
|
+
b.type = 'button';
|
|
46
|
+
b.className = 'fd-scope-picker__btn';
|
|
47
|
+
b.dataset.scope = o.value;
|
|
48
|
+
b.textContent = o.label ?? o.value;
|
|
49
|
+
if (o.hint) b.title = o.hint;
|
|
50
|
+
element.append(b);
|
|
51
|
+
return b;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const paint = () => {
|
|
55
|
+
for (const b of buttons) {
|
|
56
|
+
const on = b.dataset.scope === current;
|
|
57
|
+
b.classList.toggle('fd-scope-picker__btn--on', on);
|
|
58
|
+
b.setAttribute('aria-pressed', String(on));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
paint();
|
|
62
|
+
|
|
63
|
+
const onClick = (e) => {
|
|
64
|
+
const b = e.target?.closest?.('[data-scope]');
|
|
65
|
+
if (!b || !element.contains(b) || b.dataset.scope === current) return;
|
|
66
|
+
current = b.dataset.scope;
|
|
67
|
+
paint();
|
|
68
|
+
try { onChange?.(current); } catch (err) { console.error('[ScopePicker] onChange threw:', err); }
|
|
69
|
+
};
|
|
70
|
+
const onKey = (e) => {
|
|
71
|
+
const i = buttons.indexOf(e.target);
|
|
72
|
+
if (i < 0) return;
|
|
73
|
+
const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: buttons.length - 1 }[e.key];
|
|
74
|
+
if (to === undefined || !buttons[to]) return;
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
buttons[to].focus();
|
|
77
|
+
};
|
|
78
|
+
element.addEventListener('click', onClick);
|
|
79
|
+
element.addEventListener('keydown', onKey);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
element,
|
|
83
|
+
get value() { return current; },
|
|
84
|
+
/** Press `v` without calling `onChange` (a value restored from elsewhere). */
|
|
85
|
+
setValue(v) {
|
|
86
|
+
if (!known(v)) return;
|
|
87
|
+
current = String(v);
|
|
88
|
+
paint();
|
|
89
|
+
},
|
|
90
|
+
dispose() {
|
|
91
|
+
element.removeEventListener('click', onClick);
|
|
92
|
+
element.removeEventListener('keydown', onKey);
|
|
93
|
+
element.remove();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/tiles/tile_base.js
CHANGED
|
@@ -61,7 +61,7 @@ export class TileBase {
|
|
|
61
61
|
// without one behaves exactly as it did before this line existed, and the
|
|
62
62
|
// broadcast path is untouched.
|
|
63
63
|
this.dataSource = dataSource;
|
|
64
|
-
//
|
|
64
|
+
// This used to be read off an application's window global — reached for
|
|
65
65
|
// by a tile. The StateGuard is a FRAMEWORK service (@flexdesk/core); the INSTANCE is
|
|
66
66
|
// the application's, and it is handed down through the grid. No guard => the
|
|
67
67
|
// dataset writes happen directly, which is what the optional chain already did.
|
|
@@ -87,8 +87,19 @@ export class TileBase {
|
|
|
87
87
|
this._loadSeq = 0;
|
|
88
88
|
/** @type {Map<string, {name: string, analytics: Object, color: string}>|null} */
|
|
89
89
|
this.comparisonData = null;
|
|
90
|
+
/** The board's scope (`TileGrid.setScope`), or null. Set before mount. */
|
|
91
|
+
this.scope = null;
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Called when the board's scope changes (`TileGrid.setScope`), after
|
|
96
|
+
* `this.scope` is set. A no-op here: a tile whose rows the grid's data
|
|
97
|
+
* source loads is reloaded by the grid, and one fed through `setData` is
|
|
98
|
+
* fed again by whoever owns its data.
|
|
99
|
+
* @param {*} _scope
|
|
100
|
+
*/
|
|
101
|
+
onScopeChange(_scope) {}
|
|
102
|
+
|
|
92
103
|
/**
|
|
93
104
|
* Set comparison data for overlay rendering.
|
|
94
105
|
* Only meaningful for widgets where SUPPORTS_COMPARISON = true.
|