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,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* attribute_list_editor.js — generic AttributeSpec list editor.
|
|
3
|
+
*
|
|
4
|
+
* One reusable component for every entity that declares a typed-slot
|
|
5
|
+
* list: Asset.attributes, Archetype.attributes, SectorKind.requires,
|
|
6
|
+
* MarketKind.requires, Flow.params, KPI.params. Mounts a SortableList
|
|
7
|
+
* (drag-reorder + add + remove + empty-state) with typed cells:
|
|
8
|
+
*
|
|
9
|
+
* - scalar (number / integer / string / boolean): typed `<input>`
|
|
10
|
+
* - expression: free-text input — engine evaluates each tick
|
|
11
|
+
* - ref:<target>: typeahead-ish `<select>` populated by a caller-
|
|
12
|
+
* supplied resolver returning {id, label} lists
|
|
13
|
+
* - list[ref:<target>]: comma-separated input (UI primitive — chip
|
|
14
|
+
* editor can land later without changing the data shape)
|
|
15
|
+
* - select: dropdown of options (auto when `options[]` is set)
|
|
16
|
+
*
|
|
17
|
+
* Each row exposes the columns in `showColumns` (defaults to the full
|
|
18
|
+
* set); callers narrow it (e.g. SectorKind.requires only needs name +
|
|
19
|
+
* type + description, not default/value). Layout follows the column
|
|
20
|
+
* spec — header strip aligned via CSS grid using --cols.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
*
|
|
24
|
+
* import { mountAttributeListEditor } from
|
|
25
|
+
* '../../ui/components/attribute_list_editor.js';
|
|
26
|
+
*
|
|
27
|
+
* const editor = mountAttributeListEditor(hostEl, {
|
|
28
|
+
* items: kind.attributes,
|
|
29
|
+
* showColumns: ['name', 'type', 'default', 'value', 'description'],
|
|
30
|
+
* refResolvers: {
|
|
31
|
+
* 'ref:asset': () => api.assets_list(),
|
|
32
|
+
* },
|
|
33
|
+
* onChange: (next) => { kind.attributes = next; save(); },
|
|
34
|
+
* });
|
|
35
|
+
* editor.refresh(); // call after `items` changes externally
|
|
36
|
+
* editor.dispose(); // teardown
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { SortableList } from './sortable_list.js';
|
|
40
|
+
|
|
41
|
+
// The types a generic attribute editor knows about, with no application loaded.
|
|
42
|
+
//
|
|
43
|
+
// This used to also list nine EcoAgent entity kinds — asset_kind, market,
|
|
44
|
+
// sub_sector, currency, sector, market_kind, asset_kind_template, archetype,
|
|
45
|
+
// kpi — and derive `ref:<kind>` and `list[ref:<kind>]` from them. `types` was
|
|
46
|
+
// ALREADY an injectable option; it simply had EcoAgent's ontology as its
|
|
47
|
+
// default, which meant every consumer who didn't override it inherited a type
|
|
48
|
+
// dropdown full of macroeconomics.
|
|
49
|
+
//
|
|
50
|
+
// Callers who want reference types build them with refTypesFor() and pass them
|
|
51
|
+
// in, alongside the `refResolvers` that make them resolvable.
|
|
52
|
+
const SCALAR_TYPES = ['number', 'integer', 'string', 'boolean'];
|
|
53
|
+
|
|
54
|
+
const DEFAULT_TYPES = [...SCALAR_TYPES, 'expression'];
|
|
55
|
+
|
|
56
|
+
/** `['agent']` -> `['ref:agent', 'list[ref:agent]']`. */
|
|
57
|
+
export function refTypesFor(kinds = []) {
|
|
58
|
+
return [
|
|
59
|
+
...kinds.map((k) => `ref:${k}`),
|
|
60
|
+
...kinds.map((k) => `list[ref:${k}]`),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { SCALAR_TYPES, DEFAULT_TYPES };
|
|
65
|
+
|
|
66
|
+
const DEFAULT_COLUMNS = ['name', 'type', 'default', 'value', 'description'];
|
|
67
|
+
|
|
68
|
+
const COLUMN_TEMPLATES = {
|
|
69
|
+
name: { label: 'name', width: 'minmax(140px, 1.2fr)' },
|
|
70
|
+
type: { label: 'type', width: '140px' },
|
|
71
|
+
default: { label: 'default', width: 'minmax(110px, 1fr)' },
|
|
72
|
+
value: { label: 'value', width: 'minmax(110px, 1fr)' },
|
|
73
|
+
description: { label: 'description', width: 'minmax(160px, 2fr)' },
|
|
74
|
+
required: { label: 'req', width: '60px' },
|
|
75
|
+
readonly: { label: 'r/o', width: '60px' },
|
|
76
|
+
expression: { label: 'expression', width: 'minmax(160px, 2fr)' },
|
|
77
|
+
// Layer N3 — extended AttributeSpec fields. Opt-in via showColumns
|
|
78
|
+
// so existing single-table layouts (asset_kind editor) stay tight;
|
|
79
|
+
// the agent params surface includes them.
|
|
80
|
+
min: { label: 'min', width: '90px' },
|
|
81
|
+
max: { label: 'max', width: '90px' },
|
|
82
|
+
step: { label: 'step', width: '70px' },
|
|
83
|
+
unit: { label: 'unit', width: '80px' },
|
|
84
|
+
distribution: { label: 'dist', width: '110px' },
|
|
85
|
+
// Distribution and "selection set" are two different concepts —
|
|
86
|
+
// numeric params sample from a continuous distribution (uniform /
|
|
87
|
+
// normal / lognormal / …), choice params pick from a discrete
|
|
88
|
+
// option set. Keep them in separate columns so the user can tell
|
|
89
|
+
// which is which at a glance.
|
|
90
|
+
choices: { label: 'choices', width: '130px' },
|
|
91
|
+
// Variation-override flags — R7 in
|
|
92
|
+
// docs/ARCHETYPE_RETHINK_REQUIREMENTS.md. Tight columns so they
|
|
93
|
+
// stay readable next to the scalar fields.
|
|
94
|
+
enabled: { label: 'on', width: '50px' },
|
|
95
|
+
force: { label: 'lock', width: '50px' },
|
|
96
|
+
optional: { label: 'opt', width: '50px' },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DISTRIBUTION_OPTIONS = [
|
|
100
|
+
'', 'constant', 'uniform', 'normal', 'lognormal', 'choice',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
/** Classify one attribute against its parent-archetype counterpart.
|
|
105
|
+
* Layer 8.J1 inheritance display:
|
|
106
|
+
*
|
|
107
|
+
* - 'inherited' — entity's effective value matches the archetype's
|
|
108
|
+
* default for the same attribute name. UI paints the row blue.
|
|
109
|
+
* - 'overridden' — entity's value is set AND differs from the
|
|
110
|
+
* archetype's default. UI paints the row solarized orange + shows
|
|
111
|
+
* a revert button.
|
|
112
|
+
* - 'neutral' — neither side declares a value (or there's no parent).
|
|
113
|
+
* UI paints the row in default style.
|
|
114
|
+
*/
|
|
115
|
+
function _classifyRow(row, parentByName) {
|
|
116
|
+
if (!parentByName) return 'neutral';
|
|
117
|
+
const p = parentByName.get(row.name);
|
|
118
|
+
if (!p) return 'neutral';
|
|
119
|
+
const ourValue = (row.value !== undefined && row.value !== null)
|
|
120
|
+
? row.value : row.default;
|
|
121
|
+
const parentValue = (p.value !== undefined && p.value !== null)
|
|
122
|
+
? p.value : p.default;
|
|
123
|
+
if (ourValue === undefined || ourValue === null) return 'neutral';
|
|
124
|
+
if (parentValue === undefined || parentValue === null) return 'overridden';
|
|
125
|
+
// Loose equality: handles number vs numeric-string from form inputs.
|
|
126
|
+
if (String(ourValue) === String(parentValue)) return 'inherited';
|
|
127
|
+
return 'overridden';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
export function mountAttributeListEditor(hostEl, options = {}) {
|
|
132
|
+
const {
|
|
133
|
+
items = [],
|
|
134
|
+
types = DEFAULT_TYPES,
|
|
135
|
+
showColumns = DEFAULT_COLUMNS,
|
|
136
|
+
refResolvers = {},
|
|
137
|
+
onChange = () => {},
|
|
138
|
+
onFlush = () => {},
|
|
139
|
+
addButtonText = '',
|
|
140
|
+
emptyMessage = 'No entries yet.',
|
|
141
|
+
containerId = `attrs-${Math.random().toString(36).slice(2, 8)}`,
|
|
142
|
+
allowReorder = true,
|
|
143
|
+
allowRemove = true,
|
|
144
|
+
// Layer 8.J1: parent-archetype attributes (the inherited baseline).
|
|
145
|
+
// The editor uses this to classify each row as inherited / overridden /
|
|
146
|
+
// neutral and to provide a revert action. Pass [] when no archetype
|
|
147
|
+
// applies; the editor degrades gracefully (all rows render neutral).
|
|
148
|
+
parentAttributes = null,
|
|
149
|
+
createItem = () => ({
|
|
150
|
+
name: '', type: 'string', default: null,
|
|
151
|
+
value: null, description: '',
|
|
152
|
+
}),
|
|
153
|
+
// Layer N3 — per-column renderer overrides. Callers can supply
|
|
154
|
+
// `cellOverrides: {column_name: (item, callbacks, ctx) => HTMLElement | null}`
|
|
155
|
+
// to swap in a custom cell. Returning `null` falls through to
|
|
156
|
+
// the default renderer. The `ctx` object exposes:
|
|
157
|
+
// refreshRow() — force a re-render (the wider list refreshes
|
|
158
|
+
// when the row's items change, e.g. type swap).
|
|
159
|
+
// Use cases: agent params' distribution-dict button cell;
|
|
160
|
+
// structured-type modal openers on the default cell.
|
|
161
|
+
cellOverrides = null,
|
|
162
|
+
// R7 — when true, this editor is mounted as a *variation* of the
|
|
163
|
+
// parent archetype (not on the archetype itself). Rows where
|
|
164
|
+
// the parent has `force=true` lock their editable cells; the
|
|
165
|
+
// `enabled/force/optional` flag columns themselves stay
|
|
166
|
+
// read-only (variations don't author flags, only the
|
|
167
|
+
// archetype does).
|
|
168
|
+
variationMode = false,
|
|
169
|
+
} = options;
|
|
170
|
+
|
|
171
|
+
// Lookup map for inheritance classification.
|
|
172
|
+
const parentByName = parentAttributes
|
|
173
|
+
? new Map(parentAttributes.map((p) => [p.name, p]))
|
|
174
|
+
: null;
|
|
175
|
+
|
|
176
|
+
if (!hostEl) {
|
|
177
|
+
return { refresh() {}, addItem() {}, dispose() {} };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
hostEl.classList.add('twm-attr-editor');
|
|
181
|
+
hostEl.style.setProperty('--cols',
|
|
182
|
+
showColumns.map((c) => COLUMN_TEMPLATES[c]?.width || '1fr').join(' '));
|
|
183
|
+
|
|
184
|
+
const refsCache = new Map();
|
|
185
|
+
const resolveRefOptions = async (target) => {
|
|
186
|
+
if (refsCache.has(target)) return refsCache.get(target);
|
|
187
|
+
const resolver = refResolvers[`ref:${target}`]
|
|
188
|
+
|| refResolvers[`list[ref:${target}]`];
|
|
189
|
+
let list = [];
|
|
190
|
+
if (typeof resolver === 'function') {
|
|
191
|
+
try { list = await resolver() || []; }
|
|
192
|
+
catch (e) { console.warn('ref resolver failed', target, e); }
|
|
193
|
+
}
|
|
194
|
+
const normalised = (list || []).map((o) => {
|
|
195
|
+
if (typeof o === 'string') return { id: o, label: o };
|
|
196
|
+
return { id: o.id ?? o.value ?? '', label: o.label ?? o.id ?? '' };
|
|
197
|
+
}).filter((o) => o.id);
|
|
198
|
+
refsCache.set(target, normalised);
|
|
199
|
+
return normalised;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Header strip — mimics the SortableList row layout exactly so
|
|
203
|
+
// header cells align with body cells. Row shape is:
|
|
204
|
+
// [drag-handle] [content-with-cols-grid] [remove-button]
|
|
205
|
+
// The header reuses `.sortable-list__row` chrome with placeholder
|
|
206
|
+
// spans for the handle / remove gutters, then the inner content
|
|
207
|
+
// div carries the same `--cols` grid as each row's content div.
|
|
208
|
+
const header = document.createElement('div');
|
|
209
|
+
header.className = 'twm-sortable-list__row twm-attr-editor__header-row';
|
|
210
|
+
if (allowReorder) {
|
|
211
|
+
const handlePh = document.createElement('span');
|
|
212
|
+
handlePh.className = 'twm-sortable-list__handle twm-attr-editor__header-placeholder';
|
|
213
|
+
header.appendChild(handlePh);
|
|
214
|
+
}
|
|
215
|
+
const headerContent = document.createElement('div');
|
|
216
|
+
headerContent.className = 'twm-sortable-list__content twm-attr-editor__headers';
|
|
217
|
+
for (const col of showColumns) {
|
|
218
|
+
const span = document.createElement('span');
|
|
219
|
+
span.textContent = COLUMN_TEMPLATES[col]?.label ?? col;
|
|
220
|
+
headerContent.appendChild(span);
|
|
221
|
+
}
|
|
222
|
+
header.appendChild(headerContent);
|
|
223
|
+
if (allowRemove) {
|
|
224
|
+
const removePh = document.createElement('span');
|
|
225
|
+
removePh.className = 'twm-sortable-list__remove twm-attr-editor__header-placeholder';
|
|
226
|
+
header.appendChild(removePh);
|
|
227
|
+
}
|
|
228
|
+
hostEl.appendChild(header);
|
|
229
|
+
|
|
230
|
+
const listHost = document.createElement('div');
|
|
231
|
+
listHost.className = 'ea-attr-editor__rows';
|
|
232
|
+
hostEl.appendChild(listHost);
|
|
233
|
+
|
|
234
|
+
let list = null;
|
|
235
|
+
|
|
236
|
+
const renderRow = (contentEl, item, _idx, callbacks) => {
|
|
237
|
+
contentEl.classList.add('twm-attr-editor__row');
|
|
238
|
+
// Layer 8.J1 — paint inheritance state.
|
|
239
|
+
const cls = _classifyRow(item, parentByName);
|
|
240
|
+
contentEl.classList.remove(
|
|
241
|
+
'twm-attr-editor__row--inherited',
|
|
242
|
+
'twm-attr-editor__row--overridden',
|
|
243
|
+
'twm-attr-editor__row--neutral',
|
|
244
|
+
);
|
|
245
|
+
contentEl.classList.add(`twm-attr-editor__row--${cls}`);
|
|
246
|
+
|
|
247
|
+
// R7 variation-override flags. `enabled` defaults true, so the
|
|
248
|
+
// disabled treatment only applies when explicitly false.
|
|
249
|
+
// `force` locks the row (variations can't override). `optional`
|
|
250
|
+
// signals the value is a suggestion — green border, no fill.
|
|
251
|
+
// The flag rules are applied additively so the inheritance
|
|
252
|
+
// classification's background still shows where applicable.
|
|
253
|
+
// In variation mode the relevant flags are the parent's; in
|
|
254
|
+
// archetype mode they're the row's own. (A variation row only
|
|
255
|
+
// has the flags it inherited; it doesn't author its own.)
|
|
256
|
+
const parentRow = parentByName?.get(item.name);
|
|
257
|
+
const flagSource = variationMode && parentRow ? parentRow : item;
|
|
258
|
+
contentEl.classList.toggle(
|
|
259
|
+
'twm-attr-editor__row--disabled', flagSource.enabled === false);
|
|
260
|
+
contentEl.classList.toggle(
|
|
261
|
+
'twm-attr-editor__row--forced', flagSource.force === true);
|
|
262
|
+
contentEl.classList.toggle(
|
|
263
|
+
'twm-attr-editor__row--optional', flagSource.optional === true);
|
|
264
|
+
|
|
265
|
+
// Attention indicator on overridden rows + revert button.
|
|
266
|
+
if (cls === 'overridden') {
|
|
267
|
+
const attention = document.createElement('span');
|
|
268
|
+
attention.className = 'twm-attr-editor__attention material-symbols-outlined';
|
|
269
|
+
attention.textContent = 'fiber_manual_record';
|
|
270
|
+
attention.title = 'Value overrides the parent default';
|
|
271
|
+
contentEl.appendChild(attention);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const col of showColumns) {
|
|
275
|
+
const cell = renderCell(col, item, callbacks);
|
|
276
|
+
contentEl.appendChild(cell);
|
|
277
|
+
}
|
|
278
|
+
// R7 — lock the row's editable inputs when the parent forced it.
|
|
279
|
+
// The flag columns themselves stay enabled on the archetype
|
|
280
|
+
// (where the author sets them); in variation mode they're
|
|
281
|
+
// read-only because variations don't author flags.
|
|
282
|
+
const FLAG_COLS = new Set(['enabled', 'force', 'optional']);
|
|
283
|
+
if (variationMode && flagSource.force === true) {
|
|
284
|
+
contentEl.querySelectorAll(
|
|
285
|
+
'input:not([type="checkbox"]), select, textarea'
|
|
286
|
+
).forEach((el) => { el.disabled = true; });
|
|
287
|
+
}
|
|
288
|
+
if (variationMode) {
|
|
289
|
+
contentEl.querySelectorAll(
|
|
290
|
+
'input[type="checkbox"].ea-attr-editor__cell--flag'
|
|
291
|
+
).forEach((el) => { el.disabled = true; });
|
|
292
|
+
}
|
|
293
|
+
void FLAG_COLS; // referenced by future per-column handling
|
|
294
|
+
|
|
295
|
+
// Revert button — only rendered for overridden rows. Resets
|
|
296
|
+
// `value` back to the archetype's inherited default. The row
|
|
297
|
+
// re-classifies as 'inherited' on the next render pass.
|
|
298
|
+
if (cls === 'overridden' && parentByName) {
|
|
299
|
+
const p = parentByName.get(item.name);
|
|
300
|
+
const parentDefault = (p?.value !== undefined && p?.value !== null)
|
|
301
|
+
? p.value : p?.default;
|
|
302
|
+
const revert = document.createElement('button');
|
|
303
|
+
revert.type = 'button';
|
|
304
|
+
revert.className = 'twm-attr-editor__revert material-symbols-outlined';
|
|
305
|
+
revert.textContent = 'undo';
|
|
306
|
+
revert.title =
|
|
307
|
+
`Revert to inherited default (${parentDefault ?? '—'})`;
|
|
308
|
+
revert.addEventListener('click', (e) => {
|
|
309
|
+
e.stopPropagation();
|
|
310
|
+
callbacks.update({ value: parentDefault });
|
|
311
|
+
onFlush();
|
|
312
|
+
// Force a re-render so the row re-classifies.
|
|
313
|
+
list?.setItems(currentItems());
|
|
314
|
+
});
|
|
315
|
+
contentEl.appendChild(revert);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const renderCell = (col, item, callbacks) => {
|
|
320
|
+
// Caller override wins — used by agent_tab to inject the
|
|
321
|
+
// distribution-dict button + structured-type modal openers.
|
|
322
|
+
if (cellOverrides && typeof cellOverrides[col] === 'function') {
|
|
323
|
+
const ctx = { refreshRow: () => list?.setItems(currentItems()) };
|
|
324
|
+
const custom = cellOverrides[col](item, callbacks, ctx);
|
|
325
|
+
if (custom) return custom;
|
|
326
|
+
}
|
|
327
|
+
if (col === 'name') return renderTextCell(item, 'name', callbacks, 'attr_name');
|
|
328
|
+
if (col === 'type') return renderTypeCell(item, callbacks);
|
|
329
|
+
if (col === 'description') return renderTextCell(item, 'description', callbacks, 'optional');
|
|
330
|
+
if (col === 'required') return renderBoolCell(item, 'required', callbacks);
|
|
331
|
+
if (col === 'readonly') return renderBoolCell(item, 'readonly', callbacks);
|
|
332
|
+
// R7 — three variation-override flags. `enabled` defaults true,
|
|
333
|
+
// so the checkbox renders checked when the field is undefined;
|
|
334
|
+
// the others default false.
|
|
335
|
+
if (col === 'enabled') return renderFlagCell(item, 'enabled', callbacks, true);
|
|
336
|
+
if (col === 'force') return renderFlagCell(item, 'force', callbacks, false);
|
|
337
|
+
if (col === 'optional') return renderFlagCell(item, 'optional', callbacks, false);
|
|
338
|
+
if (col === 'expression') return renderTextCell(item, 'expression', callbacks, 'expr');
|
|
339
|
+
if (col === 'min') return renderNumericCell(item, 'min', callbacks);
|
|
340
|
+
if (col === 'max') return renderNumericCell(item, 'max', callbacks);
|
|
341
|
+
if (col === 'step') return renderTextCell(item, 'step', callbacks, 'any');
|
|
342
|
+
if (col === 'unit') return renderTextCell(item, 'unit', callbacks, 'USD/%/…');
|
|
343
|
+
if (col === 'distribution') return renderDistributionCell(item, callbacks);
|
|
344
|
+
if (col === 'default' || col === 'value') return renderTypedValueCell(col, item, callbacks);
|
|
345
|
+
// Unknown column — render a stub.
|
|
346
|
+
const stub = document.createElement('span');
|
|
347
|
+
stub.textContent = '';
|
|
348
|
+
return stub;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const renderNumericCell = (item, field, callbacks) => {
|
|
352
|
+
const el = document.createElement('input');
|
|
353
|
+
el.type = 'number';
|
|
354
|
+
el.className = 'twm-sortable-list__input twm-attr-editor__cell';
|
|
355
|
+
el.step = 'any';
|
|
356
|
+
el.value = (item[field] === null || item[field] === undefined)
|
|
357
|
+
? '' : String(item[field]);
|
|
358
|
+
el.placeholder = '—';
|
|
359
|
+
el.addEventListener('input', () => {
|
|
360
|
+
const v = el.value === '' ? null : Number(el.value);
|
|
361
|
+
callbacks.update({ [field]: v });
|
|
362
|
+
});
|
|
363
|
+
el.addEventListener('blur', () => onFlush());
|
|
364
|
+
return el;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const renderDistributionCell = (item, callbacks) => {
|
|
368
|
+
const el = document.createElement('select');
|
|
369
|
+
el.className = 'twm-attr-editor__cell';
|
|
370
|
+
for (const d of DISTRIBUTION_OPTIONS) {
|
|
371
|
+
const o = document.createElement('option');
|
|
372
|
+
o.value = d; o.textContent = d || '(none)';
|
|
373
|
+
if ((item.distribution || '') === d) o.selected = true;
|
|
374
|
+
el.appendChild(o);
|
|
375
|
+
}
|
|
376
|
+
el.addEventListener('change', () => {
|
|
377
|
+
callbacks.update({ distribution: el.value || null });
|
|
378
|
+
onFlush();
|
|
379
|
+
});
|
|
380
|
+
return el;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const renderTextCell = (item, field, callbacks, placeholder) => {
|
|
384
|
+
const el = document.createElement('input');
|
|
385
|
+
el.type = 'text';
|
|
386
|
+
el.className = 'twm-sortable-list__input twm-attr-editor__cell';
|
|
387
|
+
el.value = item[field] ?? '';
|
|
388
|
+
el.placeholder = placeholder;
|
|
389
|
+
el.addEventListener('input', () => {
|
|
390
|
+
callbacks.update({ [field]: el.value });
|
|
391
|
+
});
|
|
392
|
+
el.addEventListener('blur', () => onFlush());
|
|
393
|
+
return el;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const renderBoolCell = (item, field, callbacks) => {
|
|
397
|
+
const el = document.createElement('input');
|
|
398
|
+
el.type = 'checkbox';
|
|
399
|
+
el.className = 'twm-attr-editor__cell';
|
|
400
|
+
el.checked = !!item[field];
|
|
401
|
+
el.addEventListener('change', () => {
|
|
402
|
+
callbacks.update({ [field]: el.checked });
|
|
403
|
+
onFlush();
|
|
404
|
+
});
|
|
405
|
+
return el;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** Boolean cell for R7 flags. Same as renderBoolCell but treats an
|
|
409
|
+
* undefined value as `defaultIfMissing`, so `enabled` (default
|
|
410
|
+
* true) renders checked on legacy rows that predate the flag. */
|
|
411
|
+
const renderFlagCell = (item, field, callbacks, defaultIfMissing) => {
|
|
412
|
+
const el = document.createElement('input');
|
|
413
|
+
el.type = 'checkbox';
|
|
414
|
+
el.className = 'twm-attr-editor__cell ea-attr-editor__cell--flag';
|
|
415
|
+
const cur = item[field];
|
|
416
|
+
el.checked = (cur === undefined || cur === null)
|
|
417
|
+
? !!defaultIfMissing : !!cur;
|
|
418
|
+
el.addEventListener('change', () => {
|
|
419
|
+
callbacks.update({ [field]: el.checked });
|
|
420
|
+
onFlush();
|
|
421
|
+
});
|
|
422
|
+
return el;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const renderTypeCell = (item, callbacks) => {
|
|
426
|
+
const el = document.createElement('select');
|
|
427
|
+
el.className = 'twm-attr-editor__cell';
|
|
428
|
+
// Group scalar, expression, ref, list[ref] for readability.
|
|
429
|
+
// Any type the caller hands us that doesn't fit one of those
|
|
430
|
+
// buckets lands in `other` — that's how param-mode types
|
|
431
|
+
// (float / int / str / bool / choice / map / schedule /
|
|
432
|
+
// string_list) appear without breaking the dispatch.
|
|
433
|
+
const seen = new Set();
|
|
434
|
+
const addOpts = (label, optList) => {
|
|
435
|
+
if (optList.length === 0) return;
|
|
436
|
+
const grp = document.createElement('optgroup');
|
|
437
|
+
grp.label = label;
|
|
438
|
+
for (const t of optList) {
|
|
439
|
+
const o = document.createElement('option');
|
|
440
|
+
o.value = t; o.textContent = t;
|
|
441
|
+
if (t === item.type) o.selected = true;
|
|
442
|
+
grp.appendChild(o);
|
|
443
|
+
seen.add(t);
|
|
444
|
+
}
|
|
445
|
+
el.appendChild(grp);
|
|
446
|
+
};
|
|
447
|
+
addOpts('scalar', types.filter((t) => SCALAR_TYPES.includes(t)));
|
|
448
|
+
addOpts('expression', types.filter((t) => t === 'expression'));
|
|
449
|
+
addOpts('typed reference', types.filter((t) => t.startsWith('ref:')));
|
|
450
|
+
addOpts('list of references',
|
|
451
|
+
types.filter((t) => t.startsWith('list[ref:')));
|
|
452
|
+
addOpts('other', types.filter((t) => !seen.has(t)));
|
|
453
|
+
el.addEventListener('change', () => {
|
|
454
|
+
// On type change, leave default/value as-is — the user
|
|
455
|
+
// may want to keep e.g. a numeric value when switching
|
|
456
|
+
// from number to integer. The typed value cell renderer
|
|
457
|
+
// coerces on display.
|
|
458
|
+
callbacks.update({ type: el.value });
|
|
459
|
+
onFlush();
|
|
460
|
+
// Force re-render so the value cell picks up the new
|
|
461
|
+
// input type (number → select, etc.).
|
|
462
|
+
list?.setItems(currentItems());
|
|
463
|
+
});
|
|
464
|
+
return el;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const renderTypedValueCell = (field, item, callbacks) => {
|
|
468
|
+
const t = item.type || 'string';
|
|
469
|
+
if (t === 'boolean') {
|
|
470
|
+
return renderBoolCell(item, field, callbacks);
|
|
471
|
+
}
|
|
472
|
+
if (t === 'integer' || t === 'number') {
|
|
473
|
+
const el = document.createElement('input');
|
|
474
|
+
el.type = 'number';
|
|
475
|
+
el.className = 'twm-sortable-list__input twm-attr-editor__cell';
|
|
476
|
+
el.step = (t === 'integer') ? '1' : 'any';
|
|
477
|
+
el.value = (item[field] === null || item[field] === undefined)
|
|
478
|
+
? '' : item[field];
|
|
479
|
+
el.placeholder = field;
|
|
480
|
+
el.addEventListener('input', () => {
|
|
481
|
+
const v = el.value === '' ? null
|
|
482
|
+
: (t === 'integer' ? parseInt(el.value, 10)
|
|
483
|
+
: Number(el.value));
|
|
484
|
+
callbacks.update({ [field]: v });
|
|
485
|
+
});
|
|
486
|
+
el.addEventListener('blur', () => onFlush());
|
|
487
|
+
return el;
|
|
488
|
+
}
|
|
489
|
+
if (t === 'expression') {
|
|
490
|
+
// For an expression-typed attribute, the value cell shows
|
|
491
|
+
// the expression itself (read-only by convention — the
|
|
492
|
+
// engine fills it). Stub: render the expression string;
|
|
493
|
+
// the actual expression authoring lives in the
|
|
494
|
+
// `expression` column when shown.
|
|
495
|
+
const el = document.createElement('span');
|
|
496
|
+
el.className = 'twm-attr-editor__cell twm-attr-editor__cell--readonly';
|
|
497
|
+
el.textContent = item.expression
|
|
498
|
+
? `→ ${item.expression}` : '— (engine-evaluated)';
|
|
499
|
+
return el;
|
|
500
|
+
}
|
|
501
|
+
if (t.startsWith('ref:')) {
|
|
502
|
+
return renderRefCell(t.slice(4), field, item, callbacks);
|
|
503
|
+
}
|
|
504
|
+
if (t.startsWith('list[ref:') && t.endsWith(']')) {
|
|
505
|
+
return renderListRefCell(t.slice(9, -1), field, item, callbacks);
|
|
506
|
+
}
|
|
507
|
+
if (Array.isArray(item.options) && item.options.length > 0) {
|
|
508
|
+
return renderEnumCell(item.options, field, item, callbacks);
|
|
509
|
+
}
|
|
510
|
+
// String fallback.
|
|
511
|
+
const el = document.createElement('input');
|
|
512
|
+
el.type = 'text';
|
|
513
|
+
el.className = 'twm-sortable-list__input twm-attr-editor__cell';
|
|
514
|
+
el.value = item[field] ?? '';
|
|
515
|
+
el.placeholder = field;
|
|
516
|
+
el.addEventListener('input', () => {
|
|
517
|
+
callbacks.update({ [field]: el.value });
|
|
518
|
+
});
|
|
519
|
+
el.addEventListener('blur', () => onFlush());
|
|
520
|
+
return el;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const renderRefCell = (target, field, item, callbacks) => {
|
|
524
|
+
const el = document.createElement('select');
|
|
525
|
+
el.className = 'twm-attr-editor__cell';
|
|
526
|
+
const placeholder = document.createElement('option');
|
|
527
|
+
placeholder.value = '';
|
|
528
|
+
placeholder.textContent = `— pick ${target} —`;
|
|
529
|
+
el.appendChild(placeholder);
|
|
530
|
+
// Render once with the cached options if we have them; resolve
|
|
531
|
+
// async otherwise and patch.
|
|
532
|
+
const populate = (opts) => {
|
|
533
|
+
// Clear everything except the placeholder.
|
|
534
|
+
while (el.childNodes.length > 1) el.removeChild(el.lastChild);
|
|
535
|
+
for (const o of opts) {
|
|
536
|
+
const node = document.createElement('option');
|
|
537
|
+
node.value = o.id;
|
|
538
|
+
node.textContent = o.label;
|
|
539
|
+
if (o.id === item[field]) node.selected = true;
|
|
540
|
+
el.appendChild(node);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
if (refsCache.has(target)) {
|
|
544
|
+
populate(refsCache.get(target));
|
|
545
|
+
} else {
|
|
546
|
+
resolveRefOptions(target).then(populate);
|
|
547
|
+
}
|
|
548
|
+
el.addEventListener('change', () => {
|
|
549
|
+
callbacks.update({ [field]: el.value || null });
|
|
550
|
+
onFlush();
|
|
551
|
+
});
|
|
552
|
+
return el;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const renderListRefCell = (target, field, item, callbacks) => {
|
|
556
|
+
// MVP: comma-separated text input over the underlying string-
|
|
557
|
+
// list. The chip-style picker can replace this without
|
|
558
|
+
// changing the on-disk shape.
|
|
559
|
+
const el = document.createElement('input');
|
|
560
|
+
el.type = 'text';
|
|
561
|
+
el.className = 'twm-sortable-list__input twm-attr-editor__cell';
|
|
562
|
+
const arr = Array.isArray(item[field]) ? item[field] : [];
|
|
563
|
+
el.value = arr.join(', ');
|
|
564
|
+
el.placeholder = `${target}, ${target}, …`;
|
|
565
|
+
el.addEventListener('input', () => {
|
|
566
|
+
const next = el.value.split(',')
|
|
567
|
+
.map((s) => s.trim()).filter(Boolean);
|
|
568
|
+
callbacks.update({ [field]: next });
|
|
569
|
+
});
|
|
570
|
+
el.addEventListener('blur', () => onFlush());
|
|
571
|
+
return el;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const renderEnumCell = (options, field, item, callbacks) => {
|
|
575
|
+
// `options` may be either a flat list of strings (legacy:
|
|
576
|
+
// `['fx', 'commodity', …]`) or a list of `{value, label}`
|
|
577
|
+
// pairs (newer callers that want display labels distinct from
|
|
578
|
+
// the stored value, e.g. market-rules ref fields where the
|
|
579
|
+
// value is a currency id and the label is the currency name).
|
|
580
|
+
const el = document.createElement('select');
|
|
581
|
+
el.className = 'twm-attr-editor__cell';
|
|
582
|
+
// Empty placeholder so the user can clear the selection.
|
|
583
|
+
const blank = document.createElement('option');
|
|
584
|
+
blank.value = '';
|
|
585
|
+
blank.textContent = '—';
|
|
586
|
+
if (item[field] === null || item[field] === undefined || item[field] === '') {
|
|
587
|
+
blank.selected = true;
|
|
588
|
+
}
|
|
589
|
+
el.appendChild(blank);
|
|
590
|
+
for (const opt of options) {
|
|
591
|
+
const o = document.createElement('option');
|
|
592
|
+
if (opt !== null && typeof opt === 'object') {
|
|
593
|
+
o.value = String(opt.value ?? opt.id ?? '');
|
|
594
|
+
o.textContent = String(opt.label ?? opt.value ?? opt.id ?? '');
|
|
595
|
+
} else {
|
|
596
|
+
o.value = String(opt);
|
|
597
|
+
o.textContent = String(opt);
|
|
598
|
+
}
|
|
599
|
+
if (o.value === String(item[field] ?? '')) o.selected = true;
|
|
600
|
+
el.appendChild(o);
|
|
601
|
+
}
|
|
602
|
+
el.addEventListener('change', () => {
|
|
603
|
+
callbacks.update({ [field]: el.value === '' ? null : el.value });
|
|
604
|
+
onFlush();
|
|
605
|
+
});
|
|
606
|
+
return el;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// ─── SortableList wiring ────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
const itemsCopy = (src) => (src || []).map((it) => ({ ...it }));
|
|
612
|
+
const currentItems = () => list?.getItems?.() || itemsCopy(items);
|
|
613
|
+
|
|
614
|
+
const buildList = (current) => {
|
|
615
|
+
try { list?.dispose?.(); } catch {}
|
|
616
|
+
listHost.innerHTML = '';
|
|
617
|
+
list = new SortableList({
|
|
618
|
+
containerId,
|
|
619
|
+
items: itemsCopy(current),
|
|
620
|
+
allowReorder,
|
|
621
|
+
allowRemove,
|
|
622
|
+
minItems: 0,
|
|
623
|
+
emptyMessage,
|
|
624
|
+
addButtonText,
|
|
625
|
+
createItem,
|
|
626
|
+
renderItem: renderRow,
|
|
627
|
+
onChange: (next) => onChange(next.map((it) => ({ ...it }))),
|
|
628
|
+
});
|
|
629
|
+
listHost.appendChild(list.render());
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
buildList(items);
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
addItem() { list?.addItem(); },
|
|
636
|
+
refresh(nextItems) {
|
|
637
|
+
// External update: rebuild to pick up new type-cell shapes.
|
|
638
|
+
// Cheaper than setItems when types changed; setItems
|
|
639
|
+
// wouldn't re-render the row controls.
|
|
640
|
+
buildList(nextItems !== undefined ? nextItems : currentItems());
|
|
641
|
+
},
|
|
642
|
+
items() {
|
|
643
|
+
return currentItems();
|
|
644
|
+
},
|
|
645
|
+
dispose() {
|
|
646
|
+
try { list?.dispose?.(); } catch {}
|
|
647
|
+
list = null;
|
|
648
|
+
hostEl.innerHTML = '';
|
|
649
|
+
hostEl.classList.remove('twm-attr-editor');
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|