flexdesk 0.1.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/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sortable List Component
|
|
3
|
+
* Location: ui/components/sortable_list.js
|
|
4
|
+
*
|
|
5
|
+
* Purpose: Provide a reusable list component with add, remove, reorder, and rename
|
|
6
|
+
* capabilities. Used for named inputs in function nodes, rules in switch nodes, etc.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Add/remove items
|
|
10
|
+
* - Drag-and-drop reordering
|
|
11
|
+
* - Inline rename
|
|
12
|
+
* - Custom item renderer
|
|
13
|
+
* - Persistence via onChange callback
|
|
14
|
+
*
|
|
15
|
+
* Reordering is delegated to the shared DragReorder module (the same
|
|
16
|
+
* ghost + drop-placeholder technique used by the tab bars and the schema
|
|
17
|
+
* editor) so the reorder behaviour is implemented once.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { DragReorder } from './drag_reorder.js';
|
|
21
|
+
|
|
22
|
+
export class SortableList {
|
|
23
|
+
/**
|
|
24
|
+
* Create a sortable list.
|
|
25
|
+
* @param {Object} options - Configuration options
|
|
26
|
+
* @param {string} options.containerId - Unique ID for the container
|
|
27
|
+
* @param {Array} options.items - Initial items array
|
|
28
|
+
* @param {Function} options.onChange - Called with updated items array when list changes
|
|
29
|
+
* @param {Function} options.renderItem - Custom renderer for item content (receives item, index, callbacks)
|
|
30
|
+
* @param {Function} options.createItem - Factory function to create new items
|
|
31
|
+
* @param {string} options.emptyMessage - Message to show when list is empty
|
|
32
|
+
* @param {string} options.addButtonText - Text for add button
|
|
33
|
+
* @param {boolean} options.allowReorder - Whether to allow drag-and-drop reordering
|
|
34
|
+
* @param {boolean} options.allowRemove - Whether to allow removing items
|
|
35
|
+
* @param {number} options.minItems - Minimum number of items (blocks removal if at min)
|
|
36
|
+
*/
|
|
37
|
+
constructor({
|
|
38
|
+
containerId,
|
|
39
|
+
items = [],
|
|
40
|
+
onChange = null,
|
|
41
|
+
renderItem = null,
|
|
42
|
+
createItem = null,
|
|
43
|
+
emptyMessage = 'No items',
|
|
44
|
+
addButtonText = 'Add Item',
|
|
45
|
+
allowReorder = true,
|
|
46
|
+
allowRemove = true,
|
|
47
|
+
minItems = 0,
|
|
48
|
+
} = {}) {
|
|
49
|
+
this.containerId = containerId || `sortable-list-${Date.now()}`;
|
|
50
|
+
this.items = Array.isArray(items) ? [...items] : [];
|
|
51
|
+
this.onChange = typeof onChange === 'function' ? onChange : null;
|
|
52
|
+
this.renderItem = typeof renderItem === 'function' ? renderItem : this.#defaultRenderItem.bind(this);
|
|
53
|
+
this.createItem = typeof createItem === 'function' ? createItem : this.#defaultCreateItem.bind(this);
|
|
54
|
+
this.emptyMessage = emptyMessage;
|
|
55
|
+
this.addButtonText = addButtonText;
|
|
56
|
+
this.allowReorder = allowReorder;
|
|
57
|
+
this.allowRemove = allowRemove;
|
|
58
|
+
this.minItems = minItems;
|
|
59
|
+
|
|
60
|
+
this.root = null;
|
|
61
|
+
this.listEl = null;
|
|
62
|
+
this._dragReorder = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render the sortable list.
|
|
67
|
+
* @returns {HTMLElement} The root container element
|
|
68
|
+
*/
|
|
69
|
+
render() {
|
|
70
|
+
if (this.root) {
|
|
71
|
+
this.#refresh();
|
|
72
|
+
return this.root;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.root = document.createElement('div');
|
|
76
|
+
this.root.className = 'twm-sortable-list';
|
|
77
|
+
this.root.id = this.containerId;
|
|
78
|
+
|
|
79
|
+
this.listEl = document.createElement('div');
|
|
80
|
+
this.listEl.className = 'twm-sortable-list__items';
|
|
81
|
+
|
|
82
|
+
this.root.appendChild(this.listEl);
|
|
83
|
+
|
|
84
|
+
// Only show add button if addButtonText is provided
|
|
85
|
+
if (this.addButtonText) {
|
|
86
|
+
const addBtn = document.createElement('button');
|
|
87
|
+
addBtn.type = 'button';
|
|
88
|
+
addBtn.className = 'twm-sortable-list__add';
|
|
89
|
+
addBtn.innerHTML = `<span class="material-symbols-outlined">add</span> ${this.#escapeHtml(this.addButtonText)}`;
|
|
90
|
+
addBtn.addEventListener('click', () => this.addItem());
|
|
91
|
+
this.root.appendChild(addBtn);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.#refresh();
|
|
95
|
+
return this.root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get current items.
|
|
100
|
+
* @returns {Array} Copy of items array
|
|
101
|
+
*/
|
|
102
|
+
getItems() {
|
|
103
|
+
return [...this.items];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set items and re-render.
|
|
108
|
+
* @param {Array} items - New items array
|
|
109
|
+
*/
|
|
110
|
+
setItems(items) {
|
|
111
|
+
this.items = Array.isArray(items) ? [...items] : [];
|
|
112
|
+
this.#refresh();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Add a new item.
|
|
117
|
+
*/
|
|
118
|
+
addItem() {
|
|
119
|
+
const newItem = this.createItem(this.items.length);
|
|
120
|
+
this.items.push(newItem);
|
|
121
|
+
this.#notifyChange();
|
|
122
|
+
this.#refresh();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Remove an item by index.
|
|
127
|
+
* @param {number} index - Index of item to remove
|
|
128
|
+
*/
|
|
129
|
+
removeItem(index) {
|
|
130
|
+
if (index < 0 || index >= this.items.length) return;
|
|
131
|
+
if (this.items.length <= this.minItems) return;
|
|
132
|
+
|
|
133
|
+
this.items.splice(index, 1);
|
|
134
|
+
this.#notifyChange();
|
|
135
|
+
this.#refresh();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update an item at index.
|
|
140
|
+
* @param {number} index - Index of item to update
|
|
141
|
+
* @param {Object} updates - Properties to merge into item
|
|
142
|
+
*/
|
|
143
|
+
updateItem(index, updates) {
|
|
144
|
+
if (index < 0 || index >= this.items.length) return;
|
|
145
|
+
|
|
146
|
+
this.items[index] = { ...this.items[index], ...updates };
|
|
147
|
+
this.#notifyChange();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Move an item from one index to another.
|
|
152
|
+
* @param {number} fromIndex - Source index
|
|
153
|
+
* @param {number} toIndex - Destination index
|
|
154
|
+
*/
|
|
155
|
+
moveItem(fromIndex, toIndex) {
|
|
156
|
+
if (fromIndex < 0 || fromIndex >= this.items.length) return;
|
|
157
|
+
if (toIndex < 0 || toIndex >= this.items.length) return;
|
|
158
|
+
if (fromIndex === toIndex) return;
|
|
159
|
+
|
|
160
|
+
const [item] = this.items.splice(fromIndex, 1);
|
|
161
|
+
this.items.splice(toIndex, 0, item);
|
|
162
|
+
this.#notifyChange();
|
|
163
|
+
this.#refresh();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Dispose the component.
|
|
168
|
+
*/
|
|
169
|
+
dispose() {
|
|
170
|
+
this._dragReorder?.destroy();
|
|
171
|
+
this._dragReorder = null;
|
|
172
|
+
this.root = null;
|
|
173
|
+
this.listEl = null;
|
|
174
|
+
this.items = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Private Methods ───────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
#refresh() {
|
|
180
|
+
if (!this.listEl) return;
|
|
181
|
+
|
|
182
|
+
this.listEl.innerHTML = '';
|
|
183
|
+
|
|
184
|
+
if (this.items.length === 0) {
|
|
185
|
+
const empty = document.createElement('div');
|
|
186
|
+
empty.className = 'twm-sortable-list__empty';
|
|
187
|
+
empty.textContent = this.emptyMessage;
|
|
188
|
+
this.listEl.appendChild(empty);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.items.forEach((item, index) => {
|
|
193
|
+
const row = this.#createRow(item, index);
|
|
194
|
+
this.listEl.appendChild(row);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Drag-to-reorder via the shared DragReorder module (ghost +
|
|
198
|
+
// placeholder). Created once, bound to the persistent list
|
|
199
|
+
// element; re-attached after each refresh to wire the rebuilt
|
|
200
|
+
// rows. Drag is restricted to the grip handle.
|
|
201
|
+
if (this.allowReorder) {
|
|
202
|
+
if (!this._dragReorder) {
|
|
203
|
+
this._dragReorder = new DragReorder({
|
|
204
|
+
container: this.listEl,
|
|
205
|
+
itemSelector: '.sortable-list__row',
|
|
206
|
+
keyAttr: 'index',
|
|
207
|
+
axis: 'y',
|
|
208
|
+
indicatorClass: 'twm-sortable-list__drop',
|
|
209
|
+
draggableGuard: (e) => !!e.target.closest('.twm-sortable-list__handle'),
|
|
210
|
+
onReorder: (order) => this.#applyReorder(order),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
this._dragReorder.attach();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Reorder `items` to match the new DOM order produced by a drag,
|
|
218
|
+
* then notify + re-render. `order` holds the rows' pre-drag
|
|
219
|
+
* `data-index` values in their new arrangement. */
|
|
220
|
+
#applyReorder(order) {
|
|
221
|
+
const next = order
|
|
222
|
+
.map((i) => this.items[Number(i)])
|
|
223
|
+
.filter((it) => it !== undefined);
|
|
224
|
+
if (next.length !== this.items.length) return; // guard against drift
|
|
225
|
+
this.items = next;
|
|
226
|
+
this.#notifyChange();
|
|
227
|
+
this.#refresh();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#createRow(item, index) {
|
|
231
|
+
const row = document.createElement('div');
|
|
232
|
+
row.className = 'twm-sortable-list__row';
|
|
233
|
+
row.dataset.index = index;
|
|
234
|
+
|
|
235
|
+
// Drag handle (if reordering enabled). The drag mechanics are
|
|
236
|
+
// owned by the shared DragReorder instance (see #refresh); the
|
|
237
|
+
// handle is just the grab affordance the drag is restricted to.
|
|
238
|
+
if (this.allowReorder) {
|
|
239
|
+
const handle = document.createElement('div');
|
|
240
|
+
handle.className = 'twm-sortable-list__handle';
|
|
241
|
+
handle.innerHTML = '<span class="material-symbols-outlined">drag_indicator</span>';
|
|
242
|
+
row.appendChild(handle);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Item content (custom renderer)
|
|
246
|
+
const content = document.createElement('div');
|
|
247
|
+
content.className = 'twm-sortable-list__content';
|
|
248
|
+
|
|
249
|
+
const callbacks = {
|
|
250
|
+
update: (updates) => this.updateItem(index, updates),
|
|
251
|
+
remove: () => this.removeItem(index),
|
|
252
|
+
moveUp: () => this.moveItem(index, index - 1),
|
|
253
|
+
moveDown: () => this.moveItem(index, index + 1),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
this.renderItem(content, item, index, callbacks);
|
|
257
|
+
row.appendChild(content);
|
|
258
|
+
|
|
259
|
+
// Remove button (if removal enabled)
|
|
260
|
+
if (this.allowRemove) {
|
|
261
|
+
const canRemove = this.items.length > this.minItems;
|
|
262
|
+
const removeBtn = document.createElement('button');
|
|
263
|
+
removeBtn.type = 'button';
|
|
264
|
+
removeBtn.className = 'twm-sortable-list__remove';
|
|
265
|
+
removeBtn.disabled = !canRemove;
|
|
266
|
+
removeBtn.title = canRemove ? 'Remove' : `Minimum ${this.minItems} items required`;
|
|
267
|
+
removeBtn.innerHTML = '<span class="material-symbols-outlined">close</span>';
|
|
268
|
+
removeBtn.addEventListener('click', () => {
|
|
269
|
+
if (canRemove) this.removeItem(index);
|
|
270
|
+
});
|
|
271
|
+
row.appendChild(removeBtn);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return row;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#defaultRenderItem(container, item, index, callbacks) {
|
|
278
|
+
const input = document.createElement('input');
|
|
279
|
+
input.type = 'text';
|
|
280
|
+
input.className = 'twm-sortable-list__input';
|
|
281
|
+
input.value = item.name ?? item.label ?? '';
|
|
282
|
+
input.placeholder = `Item ${index + 1}`;
|
|
283
|
+
input.addEventListener('change', () => {
|
|
284
|
+
callbacks.update({ name: input.value });
|
|
285
|
+
});
|
|
286
|
+
container.appendChild(input);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#defaultCreateItem(index) {
|
|
290
|
+
return {
|
|
291
|
+
id: `item_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
|
|
292
|
+
name: `item${index + 1}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#notifyChange() {
|
|
297
|
+
if (this.onChange) {
|
|
298
|
+
this.onChange([...this.items]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
#escapeHtml(str) {
|
|
304
|
+
return String(str ?? '')
|
|
305
|
+
.replace(/&/g, '&')
|
|
306
|
+
.replace(/</g, '<')
|
|
307
|
+
.replace(/>/g, '>');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table_state_store.js — persist DataTable view-state (sort, filters,
|
|
3
|
+
* column widths), keyed by a caller-supplied `persistKey`.
|
|
4
|
+
*
|
|
5
|
+
* A FACTORY, not a singleton: the store reads and writes through a Host's
|
|
6
|
+
* `state` capability (see ui/js/host/host.js) under one logical state key.
|
|
7
|
+
* One blob holds a map of `{ [persistKey]: state }` for every persisted table.
|
|
8
|
+
* Where that key lands on disk is the embedder's business, not ours — it is
|
|
9
|
+
* decided by the host's `resolvePath`.
|
|
10
|
+
*
|
|
11
|
+
* A host without a `state` capability is legal: tables simply stay ephemeral.
|
|
12
|
+
*
|
|
13
|
+
* The in-memory cache must be dropped when the project changes (the app swaps
|
|
14
|
+
* projects in-place without a page reload) — `reset()` is wired to the
|
|
15
|
+
* `project:opened` event in app_bootstrap.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} opts
|
|
20
|
+
* @param {object} opts.host a Host. May lack `state` -> tables become ephemeral.
|
|
21
|
+
* @param {string} opts.key logical state key, e.g. 'datatable_state'
|
|
22
|
+
* @param {object} [opts.logger]
|
|
23
|
+
* @param {number} [opts.debounceMs=500]
|
|
24
|
+
*/
|
|
25
|
+
export function createTableStateStore({ host, key, logger = console, debounceMs = 500 }) {
|
|
26
|
+
if (!key) throw new Error('createTableStateStore requires { key }');
|
|
27
|
+
const state = host?.state || null;
|
|
28
|
+
|
|
29
|
+
let cache = null; // { [persistKey]: state } — null until first load
|
|
30
|
+
let loadPromise = null;
|
|
31
|
+
let saveTimer = null;
|
|
32
|
+
|
|
33
|
+
const scheduleSave = () => {
|
|
34
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
35
|
+
saveTimer = setTimeout(async () => {
|
|
36
|
+
saveTimer = null;
|
|
37
|
+
if (!state || !cache) return;
|
|
38
|
+
try { await state.write(key, cache); }
|
|
39
|
+
catch (err) { logger.warn?.('[table-state] save failed', err); }
|
|
40
|
+
}, debounceMs);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
/** Resolve once the persisted blob has been read into the cache. Safe
|
|
45
|
+
* to call repeatedly — the read happens at most once per project. */
|
|
46
|
+
ready() {
|
|
47
|
+
if (loadPromise) return loadPromise;
|
|
48
|
+
loadPromise = (async () => {
|
|
49
|
+
if (!state) { cache = {}; return cache; }
|
|
50
|
+
try { cache = (await state.read(key)) || {}; }
|
|
51
|
+
catch (err) { logger.warn?.('[table-state] load failed', err); cache = {}; }
|
|
52
|
+
return cache;
|
|
53
|
+
})();
|
|
54
|
+
return loadPromise;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/** Synchronous read of a table's persisted state. Returns null until
|
|
58
|
+
* the cache is loaded (await `ready()` first to be sure). */
|
|
59
|
+
get(k) {
|
|
60
|
+
if (!k || !cache) return null;
|
|
61
|
+
return cache[k] || null;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/** Merge-and-persist a table's state (debounced). Pass null to forget
|
|
65
|
+
* a key. Writes the whole map back atomically through the host. */
|
|
66
|
+
set(k, s) {
|
|
67
|
+
if (!k) return;
|
|
68
|
+
if (!cache) cache = {};
|
|
69
|
+
if (s == null) delete cache[k]; else cache[k] = s;
|
|
70
|
+
scheduleSave();
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Drop the cache + pending read so the next access reloads from the
|
|
74
|
+
* newly-opened project. Cancels any in-flight save for the old one. */
|
|
75
|
+
reset() {
|
|
76
|
+
cache = null;
|
|
77
|
+
loadPromise = null;
|
|
78
|
+
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toast.js — thin wrapper around the global NotificationCenter for
|
|
3
|
+
* callers that don't already hold an eventBus reference.
|
|
4
|
+
*
|
|
5
|
+
* The NotificationCenter (../notification_center.js) subscribes to
|
|
6
|
+
* `toast:show` on the global event bus; we re-export tiny helpers
|
|
7
|
+
* that emit the same payload shape.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { toastError, toastSuccess, toastWarn, toastInfo } from './toast.js';
|
|
11
|
+
* toastError('Save failed', 'sector_update refused: …');
|
|
12
|
+
*
|
|
13
|
+
* The 1-arg form treats the string as the message (no title); the
|
|
14
|
+
* 2-arg form is title + message.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function _bus() {
|
|
18
|
+
return window.__ecoagent?.eventBus ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _emit(severity, persistent, titleOrMsg, msg) {
|
|
22
|
+
const bus = _bus();
|
|
23
|
+
const title = (msg !== undefined) ? titleOrMsg : null;
|
|
24
|
+
const message = (msg !== undefined) ? msg : titleOrMsg;
|
|
25
|
+
if (!bus) {
|
|
26
|
+
// Fall back to console so the message isn't lost if the bus is
|
|
27
|
+
// unavailable (early bootstrap, isolated test pages).
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
(severity === 'error' || severity === 'warn' ? console.warn : console.info)(
|
|
30
|
+
`[toast/${severity}]`, title ? `${title}:` : '', message);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
bus.emit('toast:show', {
|
|
34
|
+
title, message, severity,
|
|
35
|
+
persistent,
|
|
36
|
+
durationMs: persistent ? 0 : 5000,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toastError(titleOrMsg, msg) { _emit('error', true, titleOrMsg, msg); }
|
|
41
|
+
export function toastWarn(titleOrMsg, msg) { _emit('warn', false, titleOrMsg, msg); }
|
|
42
|
+
export function toastInfo(titleOrMsg, msg) { _emit('info', false, titleOrMsg, msg); }
|
|
43
|
+
export function toastSuccess(titleOrMsg, msg) { _emit('success', false, titleOrMsg, msg); }
|
|
44
|
+
|
|
45
|
+
/** Coerce a bridge-call result/exception into a user-facing error toast.
|
|
46
|
+
* Drops the toast only if `res?.ok === false` *or* an exception was caught.
|
|
47
|
+
* Returns true if the call succeeded (caller can continue), false otherwise. */
|
|
48
|
+
export function reportBridgeError(opName, resOrErr) {
|
|
49
|
+
if (!resOrErr) return true;
|
|
50
|
+
if (resOrErr instanceof Error) {
|
|
51
|
+
toastError(opName, resOrErr.message || String(resOrErr));
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (resOrErr && resOrErr.ok === false) {
|
|
55
|
+
toastError(opName, resOrErr.error || 'operation refused');
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|