data-solectrus 0.2.10
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/.github/workflows/npm-publish.yml +32 -0
- package/CHANGELOG.md +138 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/admin/custom/customComponents.js +2691 -0
- package/admin/data-solectrus.png +0 -0
- package/admin/i18n/de/translations.json +94 -0
- package/admin/i18n/en/translations.json +94 -0
- package/admin/jsonConfig.json +59 -0
- package/io-package.json +39 -0
- package/lib/formula.js +249 -0
- package/lib/jsonpath.js +202 -0
- package/lib/services/evaluator.js +266 -0
- package/lib/services/itemIds.js +56 -0
- package/lib/services/itemManager.js +217 -0
- package/lib/services/snapshot.js +48 -0
- package/lib/services/sourceDiscovery.js +67 -0
- package/lib/services/stateRegistry.js +462 -0
- package/lib/services/subscriptions.js +83 -0
- package/lib/services/tickRunner.js +282 -0
- package/main.js +253 -0
- package/package.json +36 -0
|
@@ -0,0 +1,2691 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/* eslint-disable prettier/prettier */
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// Custom Master/Detail editor for ioBroker Admin jsonConfig.
|
|
6
|
+
// - Supports both modern (module federation) and legacy (global customComponents) loading.
|
|
7
|
+
// - Exposes: DataSolectrusItems/Components -> default export object containing { DataSolectrusItemsEditor }.
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const REMOTE_NAME = 'DataSolectrusItems';
|
|
12
|
+
const UI_VERSION = '2026-01-25 20260125-4';
|
|
13
|
+
const DEBUG = false;
|
|
14
|
+
let shareScope;
|
|
15
|
+
|
|
16
|
+
// Neutral (self-created) inline logo for the editor header.
|
|
17
|
+
// Intentionally NOT using third-party trademarks/logos.
|
|
18
|
+
const HEADER_LOGO_SVG = `<?xml version="1.0" encoding="UTF-8"?>
|
|
19
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
|
20
|
+
<defs>
|
|
21
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
22
|
+
<stop offset="0" stop-color="#ffb000"/>
|
|
23
|
+
<stop offset="1" stop-color="#ff5a00"/>
|
|
24
|
+
</linearGradient>
|
|
25
|
+
</defs>
|
|
26
|
+
<rect x="12" y="18" width="104" height="92" rx="18" fill="#1f2937"/>
|
|
27
|
+
<circle cx="44" cy="56" r="16" fill="url(#g)"/>
|
|
28
|
+
<path d="M44 34v-8M44 86v-8M22 56h-8M74 56h-8M29 41l-6-6M65 77l-6-6M29 71l-6 6M65 35l-6 6" stroke="#ffb000" stroke-width="4" stroke-linecap="round" opacity="0.9"/>
|
|
29
|
+
<path d="M78 44h26M78 58h26M78 72h26" stroke="#93c5fd" stroke-width="6" stroke-linecap="round"/>
|
|
30
|
+
<path d="M78 88h18" stroke="#34d399" stroke-width="6" stroke-linecap="round"/>
|
|
31
|
+
</svg>`;
|
|
32
|
+
|
|
33
|
+
function compareVersions(a, b) {
|
|
34
|
+
const pa = String(a)
|
|
35
|
+
.split('.')
|
|
36
|
+
.map(n => parseInt(n, 10));
|
|
37
|
+
const pb = String(b)
|
|
38
|
+
.split('.')
|
|
39
|
+
.map(n => parseInt(n, 10));
|
|
40
|
+
const len = Math.max(pa.length, pb.length);
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
const da = Number.isFinite(pa[i]) ? pa[i] : 0;
|
|
43
|
+
const db = Number.isFinite(pb[i]) ? pb[i] : 0;
|
|
44
|
+
if (da !== db) {
|
|
45
|
+
return da - db;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadShared(moduleName) {
|
|
52
|
+
const scope = shareScope;
|
|
53
|
+
if (!scope || !scope[moduleName]) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const versions = Object.keys(scope[moduleName]);
|
|
58
|
+
if (!versions.length) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
versions.sort(compareVersions);
|
|
63
|
+
const best = versions[versions.length - 1];
|
|
64
|
+
const entry = scope[moduleName][best];
|
|
65
|
+
if (!entry || typeof entry.get !== 'function') {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const factory = await entry.get();
|
|
70
|
+
const mod = typeof factory === 'function' ? factory() : null;
|
|
71
|
+
return mod && mod.__esModule && mod.default ? mod.default : mod;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeItems(value) {
|
|
75
|
+
return Array.isArray(value) ? value.filter(v => v && typeof v === 'object') : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function calcTitle(item, t) {
|
|
79
|
+
const enabled = !!(item && item.enabled);
|
|
80
|
+
const group = item && item.group ? String(item.group).trim() : '';
|
|
81
|
+
const targetId = item && item.targetId ? String(item.targetId).trim() : '';
|
|
82
|
+
const id = (group && targetId) ? `${group}.${targetId}` : (targetId || group);
|
|
83
|
+
const name = item && (item.name || id) ? String(item.name || id) : (t ? t('Item') : 'Item');
|
|
84
|
+
return `${enabled ? '🟢 ' : '⚪ '}${name}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ensureTitle(item, t) {
|
|
88
|
+
return Object.assign({}, item || {}, { _title: calcTitle(item || {}, t) });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makeNewItem(t) {
|
|
92
|
+
const item = {
|
|
93
|
+
enabled: false,
|
|
94
|
+
name: '',
|
|
95
|
+
group: '',
|
|
96
|
+
targetId: '',
|
|
97
|
+
mode: 'formula',
|
|
98
|
+
sourceState: '',
|
|
99
|
+
jsonPath: '',
|
|
100
|
+
inputs: [],
|
|
101
|
+
formula: '',
|
|
102
|
+
type: '',
|
|
103
|
+
role: '',
|
|
104
|
+
unit: '',
|
|
105
|
+
noNegative: false,
|
|
106
|
+
clamp: false,
|
|
107
|
+
min: '',
|
|
108
|
+
max: '',
|
|
109
|
+
};
|
|
110
|
+
return ensureTitle(item, t);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stringifyCompact(value, maxLen = 70) {
|
|
114
|
+
if (value === null) return 'null';
|
|
115
|
+
if (value === undefined) return 'undefined';
|
|
116
|
+
let str;
|
|
117
|
+
try {
|
|
118
|
+
if (typeof value === 'string') {
|
|
119
|
+
str = value;
|
|
120
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
121
|
+
str = String(value);
|
|
122
|
+
} else {
|
|
123
|
+
str = JSON.stringify(value);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
str = String(value);
|
|
127
|
+
}
|
|
128
|
+
str = String(str);
|
|
129
|
+
if (str.length <= maxLen) return str;
|
|
130
|
+
return str.slice(0, Math.max(0, maxLen - 1)) + '…';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function safeNumForPreview(v) {
|
|
134
|
+
const n = Number(v);
|
|
135
|
+
return Number.isFinite(n) ? n : 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Keep in sync with adapter-side minimal JSONPath subset.
|
|
139
|
+
// Supported examples:
|
|
140
|
+
// - $.apower
|
|
141
|
+
// - $.aenergy.by_minute[2]
|
|
142
|
+
// - $['temperature']['tC']
|
|
143
|
+
function applyJsonPathForPreview(obj, path) {
|
|
144
|
+
if (!path) return undefined;
|
|
145
|
+
let p = String(path).trim();
|
|
146
|
+
if (!p) return undefined;
|
|
147
|
+
|
|
148
|
+
// Accept both "$.x" and ".x" as a convenience.
|
|
149
|
+
if (p.startsWith('.')) {
|
|
150
|
+
p = `$${p}`;
|
|
151
|
+
}
|
|
152
|
+
if (!p.startsWith('$')) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let cur = obj;
|
|
157
|
+
let i = 1; // skip '$'
|
|
158
|
+
const len = p.length;
|
|
159
|
+
const isDangerousKey = k => k === '__proto__' || k === 'prototype' || k === 'constructor';
|
|
160
|
+
while (i < len) {
|
|
161
|
+
const ch = p[i];
|
|
162
|
+
if (ch === '.') {
|
|
163
|
+
i++;
|
|
164
|
+
const start = i;
|
|
165
|
+
while (i < len && /[A-Za-z0-9_]/.test(p[i])) i++;
|
|
166
|
+
const key = p.slice(start, i);
|
|
167
|
+
if (!key) return undefined;
|
|
168
|
+
if (isDangerousKey(key)) return undefined;
|
|
169
|
+
if (cur === null || cur === undefined) return undefined;
|
|
170
|
+
cur = cur[key];
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (ch === '[') {
|
|
174
|
+
i++;
|
|
175
|
+
while (i < len && /\s/.test(p[i])) i++;
|
|
176
|
+
if (i >= len) return undefined;
|
|
177
|
+
const quote = p[i] === '"' || p[i] === "'" ? p[i] : null;
|
|
178
|
+
if (quote) {
|
|
179
|
+
i++;
|
|
180
|
+
let str = '';
|
|
181
|
+
while (i < len) {
|
|
182
|
+
const c = p[i];
|
|
183
|
+
if (c === '\\') {
|
|
184
|
+
if (i + 1 < len) {
|
|
185
|
+
str += p[i + 1];
|
|
186
|
+
i += 2;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
if (c === quote) {
|
|
192
|
+
i++;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
str += c;
|
|
196
|
+
i++;
|
|
197
|
+
}
|
|
198
|
+
while (i < len && /\s/.test(p[i])) i++;
|
|
199
|
+
if (p[i] !== ']') return undefined;
|
|
200
|
+
i++;
|
|
201
|
+
if (isDangerousKey(str)) return undefined;
|
|
202
|
+
if (cur === null || cur === undefined) return undefined;
|
|
203
|
+
cur = cur[str];
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// array index
|
|
208
|
+
const start = i;
|
|
209
|
+
while (i < len && /[0-9]/.test(p[i])) i++;
|
|
210
|
+
const numStr = p.slice(start, i);
|
|
211
|
+
while (i < len && /\s/.test(p[i])) i++;
|
|
212
|
+
if (p[i] !== ']') return undefined;
|
|
213
|
+
i++;
|
|
214
|
+
const idx = Number(numStr);
|
|
215
|
+
if (!Number.isInteger(idx)) return undefined;
|
|
216
|
+
if (!Array.isArray(cur)) return undefined;
|
|
217
|
+
cur = cur[idx];
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Unknown token
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return cur;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getValueFromJsonPathForPreview(rawValue, jsonPath) {
|
|
228
|
+
const jp = jsonPath !== undefined && jsonPath !== null ? String(jsonPath).trim() : '';
|
|
229
|
+
if (!jp) {
|
|
230
|
+
return rawValue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let obj = null;
|
|
234
|
+
if (rawValue && typeof rawValue === 'object') {
|
|
235
|
+
obj = rawValue;
|
|
236
|
+
} else if (typeof rawValue === 'string') {
|
|
237
|
+
const s = rawValue.trim();
|
|
238
|
+
if (!s) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
obj = JSON.parse(s);
|
|
243
|
+
} catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const extracted = applyJsonPathForPreview(obj, jp);
|
|
251
|
+
if (extracted === undefined) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
if (extracted === null) return null;
|
|
255
|
+
const t = typeof extracted;
|
|
256
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return extracted;
|
|
257
|
+
if (extracted instanceof Date && typeof extracted.toISOString === 'function') return extracted.toISOString();
|
|
258
|
+
// Keep formulas deterministic: do not expose objects/arrays.
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function computePreviewInputValue(item, inp, rawValue) {
|
|
263
|
+
const hasJsonPath = inp && inp.jsonPath !== undefined && inp.jsonPath !== null && String(inp.jsonPath).trim() !== '';
|
|
264
|
+
let value;
|
|
265
|
+
if (hasJsonPath) {
|
|
266
|
+
const extracted = getValueFromJsonPathForPreview(rawValue, inp && inp.jsonPath);
|
|
267
|
+
if (typeof extracted === 'string') {
|
|
268
|
+
const n = Number(extracted);
|
|
269
|
+
value = Number.isFinite(n) ? n : extracted;
|
|
270
|
+
} else {
|
|
271
|
+
value = extracted;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
value = safeNumForPreview(rawValue);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Clamp negative inputs BEFORE formula evaluation (only if numeric).
|
|
278
|
+
// Keep in sync with adapter behavior: item.noNegative is an OUTPUT rule.
|
|
279
|
+
// Only per-input noNegative should clamp that specific source.
|
|
280
|
+
if (typeof value === 'number' && (inp && inp.noNegative) && value < 0) {
|
|
281
|
+
value = 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isNumericOutputItemForPreview(item) {
|
|
288
|
+
const t = item && item.type ? String(item.type) : '';
|
|
289
|
+
return t === '' || t === 'number';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function applyResultRulesForPreview(item, value) {
|
|
293
|
+
let v = safeNumForPreview(value);
|
|
294
|
+
|
|
295
|
+
const toOptionalNumber = val => {
|
|
296
|
+
if (val === undefined || val === null) return NaN;
|
|
297
|
+
if (typeof val === 'string' && val.trim() === '') return NaN;
|
|
298
|
+
const n = Number(val);
|
|
299
|
+
return Number.isFinite(n) ? n : NaN;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (item && item.noNegative && v < 0) {
|
|
303
|
+
v = 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (item && item.clamp) {
|
|
307
|
+
const min = toOptionalNumber(item.min);
|
|
308
|
+
const max = toOptionalNumber(item.max);
|
|
309
|
+
if (Number.isFinite(min) && v < min) v = min;
|
|
310
|
+
if (Number.isFinite(max) && v > max) v = max;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return v;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sanitizeInputKey(raw) {
|
|
317
|
+
const keyRaw = raw ? String(raw).trim() : '';
|
|
318
|
+
const key = keyRaw.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
319
|
+
if (key === '__proto__' || key === 'prototype' || key === 'constructor') return '';
|
|
320
|
+
return key;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function normalizeFormulaForPreview(expr) {
|
|
324
|
+
// Keep in sync (loosely) with adapter-side normalization, but only for preview.
|
|
325
|
+
// - AND/OR/NOT -> &&/||/! outside strings
|
|
326
|
+
// - single '=' -> '==' outside strings
|
|
327
|
+
let s = String(expr || '');
|
|
328
|
+
let out = '';
|
|
329
|
+
let inStr = null;
|
|
330
|
+
for (let i = 0; i < s.length; i++) {
|
|
331
|
+
const ch = s[i];
|
|
332
|
+
if (inStr) {
|
|
333
|
+
out += ch;
|
|
334
|
+
if (ch === '\\') {
|
|
335
|
+
// skip escaped char
|
|
336
|
+
i++;
|
|
337
|
+
if (i < s.length) out += s[i];
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (ch === inStr) inStr = null;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (ch === '"' || ch === "'") {
|
|
344
|
+
inStr = ch;
|
|
345
|
+
out += ch;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
out += ch;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Replace logical words outside strings by splitting again.
|
|
352
|
+
// This is conservative (word boundaries) and good enough for preview.
|
|
353
|
+
inStr = null;
|
|
354
|
+
let buf = '';
|
|
355
|
+
for (let i = 0; i < out.length; i++) {
|
|
356
|
+
const ch = out[i];
|
|
357
|
+
if (inStr) {
|
|
358
|
+
buf += ch;
|
|
359
|
+
if (ch === '\\') {
|
|
360
|
+
i++;
|
|
361
|
+
if (i < out.length) buf += out[i];
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (ch === inStr) inStr = null;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (ch === '"' || ch === "'") {
|
|
368
|
+
inStr = ch;
|
|
369
|
+
buf += ch;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
buf += ch;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let normalized = buf
|
|
376
|
+
.replace(/\bAND\b/gi, '&&')
|
|
377
|
+
.replace(/\bOR\b/gi, '||')
|
|
378
|
+
.replace(/\bNOT\b/gi, '!');
|
|
379
|
+
|
|
380
|
+
// Replace standalone '=' with '==' (skip >=, <=, ==, !=, =>)
|
|
381
|
+
let eqOut = '';
|
|
382
|
+
inStr = null;
|
|
383
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
384
|
+
const ch = normalized[i];
|
|
385
|
+
if (inStr) {
|
|
386
|
+
eqOut += ch;
|
|
387
|
+
if (ch === '\\') {
|
|
388
|
+
i++;
|
|
389
|
+
if (i < normalized.length) eqOut += normalized[i];
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (ch === inStr) inStr = null;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (ch === '"' || ch === "'") {
|
|
396
|
+
inStr = ch;
|
|
397
|
+
eqOut += ch;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (ch === '=') {
|
|
401
|
+
const prev = i > 0 ? normalized[i - 1] : '';
|
|
402
|
+
const next = i + 1 < normalized.length ? normalized[i + 1] : '';
|
|
403
|
+
if (prev === '=' || prev === '!' || prev === '<' || prev === '>' || next === '=' || next === '>') {
|
|
404
|
+
eqOut += ch;
|
|
405
|
+
} else {
|
|
406
|
+
eqOut += '==';
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
eqOut += ch;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return eqOut;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function evalPreviewExpression(expr, vars, t) {
|
|
417
|
+
const T = text => {
|
|
418
|
+
try {
|
|
419
|
+
return t ? t(text) : text;
|
|
420
|
+
} catch {
|
|
421
|
+
return text;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const src = normalizeFormulaForPreview(expr);
|
|
426
|
+
|
|
427
|
+
// State functions can't be safely previewed in-browser.
|
|
428
|
+
if (/\b(s|v|jp)\s*\(/.test(src)) {
|
|
429
|
+
throw new Error(T('Preview not supported for state functions'));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let i = 0;
|
|
433
|
+
const s = src;
|
|
434
|
+
const tokens = [];
|
|
435
|
+
|
|
436
|
+
const isSpace = c => c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
437
|
+
const isDigit = c => c >= '0' && c <= '9';
|
|
438
|
+
const isIdStart = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_';
|
|
439
|
+
const isId = c => isIdStart(c) || isDigit(c);
|
|
440
|
+
|
|
441
|
+
const readString = quote => {
|
|
442
|
+
i++; // skip quote
|
|
443
|
+
let out = '';
|
|
444
|
+
while (i < s.length) {
|
|
445
|
+
const ch = s[i];
|
|
446
|
+
if (ch === '\\') {
|
|
447
|
+
i++;
|
|
448
|
+
if (i >= s.length) break;
|
|
449
|
+
out += s[i];
|
|
450
|
+
i++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (ch === quote) {
|
|
454
|
+
i++;
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
out += ch;
|
|
458
|
+
i++;
|
|
459
|
+
}
|
|
460
|
+
throw new Error(T('Unterminated string'));
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const readNumber = () => {
|
|
464
|
+
let start = i;
|
|
465
|
+
while (i < s.length && isDigit(s[i])) i++;
|
|
466
|
+
if (i < s.length && s[i] === '.') {
|
|
467
|
+
i++;
|
|
468
|
+
while (i < s.length && isDigit(s[i])) i++;
|
|
469
|
+
}
|
|
470
|
+
if (i < s.length && (s[i] === 'e' || s[i] === 'E')) {
|
|
471
|
+
i++;
|
|
472
|
+
if (i < s.length && (s[i] === '+' || s[i] === '-')) i++;
|
|
473
|
+
while (i < s.length && isDigit(s[i])) i++;
|
|
474
|
+
}
|
|
475
|
+
const raw = s.slice(start, i);
|
|
476
|
+
const n = Number(raw);
|
|
477
|
+
if (!Number.isFinite(n)) throw new Error(T('Invalid number'));
|
|
478
|
+
return n;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const readIdent = () => {
|
|
482
|
+
let start = i;
|
|
483
|
+
i++;
|
|
484
|
+
while (i < s.length && isId(s[i])) i++;
|
|
485
|
+
return s.slice(start, i);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const pushOp = op => tokens.push({ t: 'op', v: op });
|
|
489
|
+
const pushPunc = p => tokens.push({ t: p, v: p });
|
|
490
|
+
|
|
491
|
+
while (i < s.length) {
|
|
492
|
+
const ch = s[i];
|
|
493
|
+
if (isSpace(ch)) {
|
|
494
|
+
i++;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (ch === '"' || ch === "'") {
|
|
498
|
+
tokens.push({ t: 'str', v: readString(ch) });
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (isDigit(ch) || (ch === '.' && i + 1 < s.length && isDigit(s[i + 1]))) {
|
|
502
|
+
tokens.push({ t: 'num', v: readNumber() });
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (isIdStart(ch)) {
|
|
506
|
+
const id = readIdent();
|
|
507
|
+
if (id === 'true') tokens.push({ t: 'bool', v: true });
|
|
508
|
+
else if (id === 'false') tokens.push({ t: 'bool', v: false });
|
|
509
|
+
else if (id === 'null') tokens.push({ t: 'null', v: null });
|
|
510
|
+
else tokens.push({ t: 'id', v: id });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
// two-char ops
|
|
514
|
+
const two = s.slice(i, i + 2);
|
|
515
|
+
if (two === '&&' || two === '||' || two === '==' || two === '!=' || two === '>=' || two === '<=') {
|
|
516
|
+
pushOp(two);
|
|
517
|
+
i += 2;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
// single-char
|
|
521
|
+
if (ch === '+' || ch === '-' || ch === '*' || ch === '/' || ch === '%' || ch === '!' || ch === '<' || ch === '>') {
|
|
522
|
+
pushOp(ch);
|
|
523
|
+
i++;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (ch === '(' || ch === ')' || ch === ',' || ch === '?' || ch === ':') {
|
|
527
|
+
pushPunc(ch);
|
|
528
|
+
i++;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
throw new Error(`${T('Unexpected character')}: ${ch}`);
|
|
532
|
+
}
|
|
533
|
+
tokens.push({ t: 'eof', v: '' });
|
|
534
|
+
|
|
535
|
+
let pos = 0;
|
|
536
|
+
const peek = () => tokens[pos];
|
|
537
|
+
const next = () => tokens[pos++];
|
|
538
|
+
const expect = tt => {
|
|
539
|
+
const tok = next();
|
|
540
|
+
if (!tok || tok.t !== tt) throw new Error(`${T('Expected')} ${tt}`);
|
|
541
|
+
return tok;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const fns = {
|
|
545
|
+
min: (a, b) => Math.min(Number(a), Number(b)),
|
|
546
|
+
max: (a, b) => Math.max(Number(a), Number(b)),
|
|
547
|
+
clamp: (value, min, max) => Math.min(Math.max(Number(value), Number(min)), Number(max)),
|
|
548
|
+
IF: (cond, vt, vf) => (cond ? vt : vf),
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const lbp = op => {
|
|
552
|
+
if (op === '||') return 10;
|
|
553
|
+
if (op === '&&') return 20;
|
|
554
|
+
if (op === '==' || op === '!=') return 30;
|
|
555
|
+
if (op === '<' || op === '<=' || op === '>' || op === '>=') return 40;
|
|
556
|
+
if (op === '+' || op === '-') return 50;
|
|
557
|
+
if (op === '*' || op === '/' || op === '%') return 60;
|
|
558
|
+
return 0;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const parsePrimary = () => {
|
|
562
|
+
const tok = next();
|
|
563
|
+
if (!tok) throw new Error(T('Unexpected end'));
|
|
564
|
+
if (tok.t === 'num' || tok.t === 'str' || tok.t === 'bool' || tok.t === 'null') return tok.v;
|
|
565
|
+
if (tok.t === 'id') {
|
|
566
|
+
// function call?
|
|
567
|
+
if (peek().t === '(') {
|
|
568
|
+
next();
|
|
569
|
+
const args = [];
|
|
570
|
+
if (peek().t !== ')') {
|
|
571
|
+
while (true) {
|
|
572
|
+
args.push(parseExpr(0));
|
|
573
|
+
if (peek().t === ',') {
|
|
574
|
+
next();
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
expect(')');
|
|
581
|
+
const fn = fns[tok.v];
|
|
582
|
+
if (!fn) throw new Error(`${T('Unknown function')}: ${tok.v}`);
|
|
583
|
+
return fn.apply(null, args);
|
|
584
|
+
}
|
|
585
|
+
return vars && Object.prototype.hasOwnProperty.call(vars, tok.v) ? vars[tok.v] : undefined;
|
|
586
|
+
}
|
|
587
|
+
if (tok.t === '(') {
|
|
588
|
+
const v = parseExpr(0);
|
|
589
|
+
expect(')');
|
|
590
|
+
return v;
|
|
591
|
+
}
|
|
592
|
+
if (tok.t === 'op' && (tok.v === '+' || tok.v === '-' || tok.v === '!')) {
|
|
593
|
+
const v = parseExpr(70);
|
|
594
|
+
if (tok.v === '+') return Number(v);
|
|
595
|
+
if (tok.v === '-') return -Number(v);
|
|
596
|
+
return !v;
|
|
597
|
+
}
|
|
598
|
+
throw new Error(`${T('Unexpected token')}: ${tok.t}`);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const applyOp = (op, a, b) => {
|
|
602
|
+
switch (op) {
|
|
603
|
+
case '+':
|
|
604
|
+
return Number(a) + Number(b);
|
|
605
|
+
case '-':
|
|
606
|
+
return Number(a) - Number(b);
|
|
607
|
+
case '*':
|
|
608
|
+
return Number(a) * Number(b);
|
|
609
|
+
case '/':
|
|
610
|
+
return Number(a) / Number(b);
|
|
611
|
+
case '%':
|
|
612
|
+
return Number(a) % Number(b);
|
|
613
|
+
case '==':
|
|
614
|
+
// eslint-disable-next-line eqeqeq
|
|
615
|
+
return a == b;
|
|
616
|
+
case '!=':
|
|
617
|
+
// eslint-disable-next-line eqeqeq
|
|
618
|
+
return a != b;
|
|
619
|
+
case '<':
|
|
620
|
+
return a < b;
|
|
621
|
+
case '<=':
|
|
622
|
+
return a <= b;
|
|
623
|
+
case '>':
|
|
624
|
+
return a > b;
|
|
625
|
+
case '>=':
|
|
626
|
+
return a >= b;
|
|
627
|
+
case '&&':
|
|
628
|
+
return a && b;
|
|
629
|
+
case '||':
|
|
630
|
+
return a || b;
|
|
631
|
+
default:
|
|
632
|
+
throw new Error(`${T('Unsupported operator')}: ${op}`);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const parseExpr = minBp => {
|
|
637
|
+
let left = parsePrimary();
|
|
638
|
+
while (true) {
|
|
639
|
+
const tok = peek();
|
|
640
|
+
if (!tok) break;
|
|
641
|
+
if (tok.t === '?') {
|
|
642
|
+
if (minBp > 5) break;
|
|
643
|
+
next();
|
|
644
|
+
const tVal = parseExpr(0);
|
|
645
|
+
expect(':');
|
|
646
|
+
const fVal = parseExpr(0);
|
|
647
|
+
left = left ? tVal : fVal;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (tok.t !== 'op') break;
|
|
651
|
+
const bp = lbp(tok.v);
|
|
652
|
+
if (bp < minBp) break;
|
|
653
|
+
next();
|
|
654
|
+
const right = parseExpr(bp + 1);
|
|
655
|
+
left = applyOp(tok.v, left, right);
|
|
656
|
+
}
|
|
657
|
+
return left;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const value = parseExpr(0);
|
|
661
|
+
if (peek().t !== 'eof') {
|
|
662
|
+
throw new Error(T('Unexpected token'));
|
|
663
|
+
}
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function createDataSolectrusItemsEditor(React, AdapterReact) {
|
|
668
|
+
return function DataSolectrusItemsEditor(props) {
|
|
669
|
+
const DEFAULT_ITEMS_ATTR = 'items';
|
|
670
|
+
const attr = (props && typeof props.attr === 'string' && props.attr) ? props.attr : DEFAULT_ITEMS_ATTR;
|
|
671
|
+
const dataIsArray = Array.isArray(props && props.data);
|
|
672
|
+
const dataIsObject = !!(props && props.data && typeof props.data === 'object' && !dataIsArray);
|
|
673
|
+
|
|
674
|
+
const getDomThemeType = () => {
|
|
675
|
+
try {
|
|
676
|
+
const doc = globalThis.document;
|
|
677
|
+
const root = doc && doc.documentElement ? doc.documentElement : null;
|
|
678
|
+
|
|
679
|
+
const rootAttrTheme = root ? root.getAttribute('data-theme') : '';
|
|
680
|
+
if (rootAttrTheme === 'dark' || rootAttrTheme === 'light') return rootAttrTheme;
|
|
681
|
+
|
|
682
|
+
// MUI v5 color scheme (some Admin versions)
|
|
683
|
+
const muiScheme = root ? root.getAttribute('data-mui-color-scheme') : '';
|
|
684
|
+
if (muiScheme === 'dark' || muiScheme === 'light') return muiScheme;
|
|
685
|
+
|
|
686
|
+
const colorScheme = root ? root.getAttribute('data-color-scheme') : '';
|
|
687
|
+
if (colorScheme === 'dark' || colorScheme === 'light') return colorScheme;
|
|
688
|
+
|
|
689
|
+
const body = doc ? doc.body : null;
|
|
690
|
+
if (body && body.classList) {
|
|
691
|
+
if (body.classList.contains('mui-theme-dark') || body.classList.contains('iob-theme-dark')) {
|
|
692
|
+
return 'dark';
|
|
693
|
+
}
|
|
694
|
+
if (body.classList.contains('mui-theme-light') || body.classList.contains('iob-theme-light')) {
|
|
695
|
+
return 'light';
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (root && root.classList) {
|
|
700
|
+
if (root.classList.contains('mui-theme-dark') || root.classList.contains('iob-theme-dark')) return 'dark';
|
|
701
|
+
if (root.classList.contains('mui-theme-light') || root.classList.contains('iob-theme-light')) return 'light';
|
|
702
|
+
}
|
|
703
|
+
} catch {
|
|
704
|
+
// ignore
|
|
705
|
+
}
|
|
706
|
+
return '';
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const getThemeType = () => {
|
|
710
|
+
// DOM is the source of truth for the currently active Admin theme.
|
|
711
|
+
// Some Admin theme switches do not trigger a re-render of custom components.
|
|
712
|
+
const domTheme = getDomThemeType();
|
|
713
|
+
if (domTheme === 'dark' || domTheme === 'light') {
|
|
714
|
+
return domTheme;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Some Admin versions switch theme by swapping CSS variables / styles only,
|
|
718
|
+
// without changing attributes/classes we can observe. Infer mode from computed colors.
|
|
719
|
+
const getComputedThemeType = () => {
|
|
720
|
+
try {
|
|
721
|
+
const doc = globalThis.document;
|
|
722
|
+
if (!doc || !globalThis.getComputedStyle) return '';
|
|
723
|
+
|
|
724
|
+
const parseRgb = value => {
|
|
725
|
+
const s = String(value || '').trim();
|
|
726
|
+
const m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
|
|
727
|
+
if (!m) return null;
|
|
728
|
+
const r = Number(m[1]);
|
|
729
|
+
const g = Number(m[2]);
|
|
730
|
+
const b = Number(m[3]);
|
|
731
|
+
const a = m[4] === undefined ? 1 : Number(m[4]);
|
|
732
|
+
if (![r, g, b, a].every(n => Number.isFinite(n))) return null;
|
|
733
|
+
return { r, g, b, a };
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const luminance = rgb => {
|
|
737
|
+
// Simple perceived luminance (0..255)
|
|
738
|
+
return 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const candidates = [];
|
|
742
|
+
if (doc.body) candidates.push(doc.body);
|
|
743
|
+
if (doc.documentElement) candidates.push(doc.documentElement);
|
|
744
|
+
try {
|
|
745
|
+
const q = sel => doc.querySelector(sel);
|
|
746
|
+
candidates.push(
|
|
747
|
+
q('.MuiPaper-root'),
|
|
748
|
+
q('.MuiDrawer-paper'),
|
|
749
|
+
q('.MuiDialog-paper'),
|
|
750
|
+
q('#root'),
|
|
751
|
+
q('.root')
|
|
752
|
+
);
|
|
753
|
+
} catch {
|
|
754
|
+
// ignore
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
758
|
+
const el = candidates[i];
|
|
759
|
+
if (!el) continue;
|
|
760
|
+
const cs = globalThis.getComputedStyle(el);
|
|
761
|
+
if (!cs) continue;
|
|
762
|
+
const bg = parseRgb(cs.backgroundColor);
|
|
763
|
+
if (!bg) continue;
|
|
764
|
+
if (bg.a === 0) continue; // transparent
|
|
765
|
+
const l = luminance(bg);
|
|
766
|
+
// Threshold chosen to be robust; typical dark backgrounds are well below this.
|
|
767
|
+
return l < 140 ? 'dark' : 'light';
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
// ignore
|
|
771
|
+
}
|
|
772
|
+
return '';
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const computedTheme = getComputedThemeType();
|
|
776
|
+
if (computedTheme === 'dark' || computedTheme === 'light') {
|
|
777
|
+
return computedTheme;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (props && typeof props.themeType === 'string' && (props.themeType === 'dark' || props.themeType === 'light')) {
|
|
781
|
+
return props.themeType;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const mode = props && props.theme && props.theme.palette && props.theme.palette.mode;
|
|
785
|
+
if (mode === 'dark' || mode === 'light') {
|
|
786
|
+
return mode;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return '';
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const [themeType, setThemeType] = React.useState(() => getThemeType());
|
|
793
|
+
|
|
794
|
+
React.useEffect(() => {
|
|
795
|
+
let observer;
|
|
796
|
+
let interval;
|
|
797
|
+
let media;
|
|
798
|
+
|
|
799
|
+
const update = () => {
|
|
800
|
+
const next = getThemeType();
|
|
801
|
+
if (next === 'dark' || next === 'light') {
|
|
802
|
+
setThemeType(prev => (prev === next ? prev : next));
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// Sync once on mount.
|
|
807
|
+
update();
|
|
808
|
+
|
|
809
|
+
// Best-effort: detect theme changes without relying on custom component re-render.
|
|
810
|
+
try {
|
|
811
|
+
const doc = globalThis.document;
|
|
812
|
+
if (doc && typeof globalThis.MutationObserver === 'function') {
|
|
813
|
+
observer = new globalThis.MutationObserver(update);
|
|
814
|
+
|
|
815
|
+
// Observe root/body + body subtree for attribute-based theme markers.
|
|
816
|
+
const attributeFilter = ['data-theme', 'data-mui-color-scheme', 'data-color-scheme', 'class'];
|
|
817
|
+
if (doc.documentElement) {
|
|
818
|
+
observer.observe(doc.documentElement, { attributes: true, attributeFilter });
|
|
819
|
+
}
|
|
820
|
+
if (doc.body) {
|
|
821
|
+
observer.observe(doc.body, { attributes: true, attributeFilter: ['class'] });
|
|
822
|
+
observer.observe(doc.body, { attributes: true, subtree: true, attributeFilter });
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
// ignore
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Fallback: periodic check (cheap) for Admin versions that don't mutate attributes we track.
|
|
830
|
+
try {
|
|
831
|
+
interval = globalThis.setInterval(update, 500);
|
|
832
|
+
} catch {
|
|
833
|
+
// ignore
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Browser/OS theme changes.
|
|
837
|
+
try {
|
|
838
|
+
if (globalThis.matchMedia) {
|
|
839
|
+
media = globalThis.matchMedia('(prefers-color-scheme: dark)');
|
|
840
|
+
if (media && typeof media.addEventListener === 'function') {
|
|
841
|
+
media.addEventListener('change', update);
|
|
842
|
+
} else if (media && typeof media.addListener === 'function') {
|
|
843
|
+
media.addListener(update);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch {
|
|
847
|
+
// ignore
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return () => {
|
|
851
|
+
try {
|
|
852
|
+
observer && observer.disconnect();
|
|
853
|
+
} catch {
|
|
854
|
+
// ignore
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
interval && globalThis.clearInterval(interval);
|
|
858
|
+
} catch {
|
|
859
|
+
// ignore
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
if (media && typeof media.removeEventListener === 'function') {
|
|
863
|
+
media.removeEventListener('change', update);
|
|
864
|
+
} else if (media && typeof media.removeListener === 'function') {
|
|
865
|
+
media.removeListener(update);
|
|
866
|
+
}
|
|
867
|
+
} catch {
|
|
868
|
+
// ignore
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}, []);
|
|
872
|
+
|
|
873
|
+
const isDark = themeType === 'dark';
|
|
874
|
+
const theme = (props && props.theme) || null;
|
|
875
|
+
const themePalette = theme && theme.palette ? theme.palette : null;
|
|
876
|
+
const paletteMatches = !!(themePalette && (themePalette.mode === 'dark' || themePalette.mode === 'light') && themePalette.mode === themeType);
|
|
877
|
+
const effectivePalette = paletteMatches ? themePalette : null;
|
|
878
|
+
|
|
879
|
+
const fallbackColors = isDark
|
|
880
|
+
? {
|
|
881
|
+
panelBg: '#1f1f1f',
|
|
882
|
+
panelBg2: '#242424',
|
|
883
|
+
text: '#ffffff',
|
|
884
|
+
textMuted: 'rgba(255,255,255,0.75)',
|
|
885
|
+
border: 'rgba(255,255,255,0.16)',
|
|
886
|
+
rowBorder: 'rgba(255,255,255,0.10)',
|
|
887
|
+
hover: 'rgba(255,255,255,0.06)',
|
|
888
|
+
active: 'rgba(255,255,255,0.10)',
|
|
889
|
+
}
|
|
890
|
+
: {
|
|
891
|
+
panelBg: '#ffffff',
|
|
892
|
+
panelBg2: '#ffffff',
|
|
893
|
+
text: '#111111',
|
|
894
|
+
textMuted: 'rgba(0,0,0,0.70)',
|
|
895
|
+
border: 'rgba(0,0,0,0.15)',
|
|
896
|
+
rowBorder: 'rgba(0,0,0,0.10)',
|
|
897
|
+
hover: 'rgba(0,0,0,0.05)',
|
|
898
|
+
active: 'rgba(0,0,0,0.08)',
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// Prefer the surrounding Admin theme to keep this editor visually consistent.
|
|
902
|
+
// Fallback to the self-defined palette if theme is unavailable.
|
|
903
|
+
const colors = Object.assign({}, fallbackColors, {
|
|
904
|
+
panelBg: (effectivePalette && effectivePalette.background && effectivePalette.background.paper) || fallbackColors.panelBg,
|
|
905
|
+
panelBg2:
|
|
906
|
+
(effectivePalette && effectivePalette.background && (effectivePalette.background.paper || effectivePalette.background.default)) ||
|
|
907
|
+
fallbackColors.panelBg2,
|
|
908
|
+
text: (effectivePalette && effectivePalette.text && effectivePalette.text.primary) || fallbackColors.text,
|
|
909
|
+
textMuted: (effectivePalette && effectivePalette.text && effectivePalette.text.secondary) || fallbackColors.textMuted,
|
|
910
|
+
border: (effectivePalette && effectivePalette.divider) || fallbackColors.border,
|
|
911
|
+
rowBorder: (effectivePalette && effectivePalette.divider) || fallbackColors.rowBorder,
|
|
912
|
+
hover: (effectivePalette && effectivePalette.action && effectivePalette.action.hover) || fallbackColors.hover,
|
|
913
|
+
active: (effectivePalette && effectivePalette.action && effectivePalette.action.selected) || fallbackColors.active,
|
|
914
|
+
inputBg:
|
|
915
|
+
(effectivePalette && effectivePalette.background && effectivePalette.background.paper) ||
|
|
916
|
+
(isDark ? 'rgba(255,255,255,0.06)' : '#ffffff'),
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const DialogSelectID = AdapterReact && (AdapterReact.DialogSelectID || AdapterReact.SelectID);
|
|
920
|
+
const socket = (props && props.socket) || globalThis.socket || globalThis._socket || null;
|
|
921
|
+
|
|
922
|
+
const t = text => {
|
|
923
|
+
try {
|
|
924
|
+
if (props && typeof props.t === 'function') {
|
|
925
|
+
return props.t(text);
|
|
926
|
+
}
|
|
927
|
+
} catch {
|
|
928
|
+
// ignore
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const I18n =
|
|
932
|
+
(AdapterReact && AdapterReact.I18n) ||
|
|
933
|
+
globalThis.I18n ||
|
|
934
|
+
(globalThis.window && globalThis.window.I18n);
|
|
935
|
+
|
|
936
|
+
try {
|
|
937
|
+
if (I18n && typeof I18n.t === 'function') {
|
|
938
|
+
return I18n.t(text);
|
|
939
|
+
}
|
|
940
|
+
if (I18n && typeof I18n.getTranslation === 'function') {
|
|
941
|
+
return I18n.getTranslation(text);
|
|
942
|
+
}
|
|
943
|
+
} catch {
|
|
944
|
+
// ignore
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return text;
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
const items = dataIsArray
|
|
951
|
+
? normalizeItems(props.data)
|
|
952
|
+
: normalizeItems(
|
|
953
|
+
(props.data && props.data[DEFAULT_ITEMS_ATTR]) ||
|
|
954
|
+
(props.data && props.data[attr]) ||
|
|
955
|
+
(props.data && props.data.itemsEditor)
|
|
956
|
+
);
|
|
957
|
+
|
|
958
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
959
|
+
const [selectContext, setSelectContext] = React.useState(null);
|
|
960
|
+
const [openDropdown, setOpenDropdown] = React.useState(null);
|
|
961
|
+
|
|
962
|
+
const [formulaBuilderOpen, setFormulaBuilderOpen] = React.useState(false);
|
|
963
|
+
const [formulaDraft, setFormulaDraft] = React.useState('');
|
|
964
|
+
const formulaEditorRef = React.useRef(null);
|
|
965
|
+
const [formulaLiveValues, setFormulaLiveValues] = React.useState({});
|
|
966
|
+
const [formulaLiveTs, setFormulaLiveTs] = React.useState({});
|
|
967
|
+
const [formulaLivePollNonce, setFormulaLivePollNonce] = React.useState(0);
|
|
968
|
+
const [formulaLiveLoading, setFormulaLiveLoading] = React.useState(false);
|
|
969
|
+
const [formulaPreview, setFormulaPreview] = React.useState(null);
|
|
970
|
+
const [formulaPreviewLoading, setFormulaPreviewLoading] = React.useState(false);
|
|
971
|
+
|
|
972
|
+
React.useEffect(() => {
|
|
973
|
+
const onDocMouseDown = e => {
|
|
974
|
+
if (!openDropdown) return;
|
|
975
|
+
try {
|
|
976
|
+
const target = e && e.target;
|
|
977
|
+
if (target && target.closest && target.closest('[data-ds-dropdown="1"]')) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
} catch {
|
|
981
|
+
// ignore
|
|
982
|
+
}
|
|
983
|
+
setOpenDropdown(null);
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
globalThis.document && globalThis.document.addEventListener('mousedown', onDocMouseDown);
|
|
988
|
+
} catch {
|
|
989
|
+
// ignore
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return () => {
|
|
993
|
+
try {
|
|
994
|
+
globalThis.document && globalThis.document.removeEventListener('mousedown', onDocMouseDown);
|
|
995
|
+
} catch {
|
|
996
|
+
// ignore
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
}, [openDropdown]);
|
|
1000
|
+
|
|
1001
|
+
React.useEffect(() => {
|
|
1002
|
+
if (selectedIndex > items.length - 1) {
|
|
1003
|
+
setSelectedIndex(Math.max(0, items.length - 1));
|
|
1004
|
+
}
|
|
1005
|
+
}, [items.length, selectedIndex]);
|
|
1006
|
+
|
|
1007
|
+
React.useEffect(() => {
|
|
1008
|
+
if (!formulaBuilderOpen) return;
|
|
1009
|
+
const onKeyDown = e => {
|
|
1010
|
+
if (e && e.key === 'Escape') {
|
|
1011
|
+
setFormulaBuilderOpen(false);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
globalThis.document && globalThis.document.addEventListener('keydown', onKeyDown);
|
|
1016
|
+
} catch {
|
|
1017
|
+
// ignore
|
|
1018
|
+
}
|
|
1019
|
+
return () => {
|
|
1020
|
+
try {
|
|
1021
|
+
globalThis.document && globalThis.document.removeEventListener('keydown', onKeyDown);
|
|
1022
|
+
} catch {
|
|
1023
|
+
// ignore
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}, [formulaBuilderOpen]);
|
|
1027
|
+
|
|
1028
|
+
const setByPath = (rootObj, path, value) => {
|
|
1029
|
+
if (!path) {
|
|
1030
|
+
return value;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parts = String(path).split('.').filter(Boolean);
|
|
1034
|
+
const clonedRoot = Array.isArray(rootObj)
|
|
1035
|
+
? rootObj.slice()
|
|
1036
|
+
: Object.assign({}, rootObj || {});
|
|
1037
|
+
let cursor = clonedRoot;
|
|
1038
|
+
|
|
1039
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1040
|
+
const part = parts[i];
|
|
1041
|
+
const isArrayIndex = Array.isArray(cursor) && /^\d+$/.test(part);
|
|
1042
|
+
const key = isArrayIndex ? parseInt(part, 10) : part;
|
|
1043
|
+
const existing = cursor[key];
|
|
1044
|
+
|
|
1045
|
+
const next = Array.isArray(existing)
|
|
1046
|
+
? existing.slice()
|
|
1047
|
+
: existing && typeof existing === 'object'
|
|
1048
|
+
? Object.assign({}, existing)
|
|
1049
|
+
: {};
|
|
1050
|
+
|
|
1051
|
+
cursor[key] = next;
|
|
1052
|
+
cursor = next;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const last = parts[parts.length - 1];
|
|
1056
|
+
const lastKey = Array.isArray(cursor) && /^\d+$/.test(last) ? parseInt(last, 10) : last;
|
|
1057
|
+
cursor[lastKey] = value;
|
|
1058
|
+
|
|
1059
|
+
return clonedRoot;
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const updateItems = nextItems => {
|
|
1063
|
+
if (typeof props.onChange !== 'function') {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const onChange = props.onChange;
|
|
1068
|
+
const cb = () => {
|
|
1069
|
+
try {
|
|
1070
|
+
if (props && typeof props.forceUpdate === 'function') {
|
|
1071
|
+
props.forceUpdate([attr], props.data);
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
// ignore
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const safeItems = normalizeItems(nextItems).map(it => ensureTitle(it, t));
|
|
1079
|
+
|
|
1080
|
+
if (props && props.custom) {
|
|
1081
|
+
// Some Admin versions do NOT allow passing `attr` in jsonConfig for custom controls.
|
|
1082
|
+
// So we always write to native.items, regardless of the schema field name.
|
|
1083
|
+
try {
|
|
1084
|
+
onChange(DEFAULT_ITEMS_ATTR, safeItems);
|
|
1085
|
+
} catch {
|
|
1086
|
+
// ignore
|
|
1087
|
+
}
|
|
1088
|
+
// Best-effort: also update the field that hosts this custom control to keep the UI in sync.
|
|
1089
|
+
if (attr !== DEFAULT_ITEMS_ATTR) {
|
|
1090
|
+
try {
|
|
1091
|
+
onChange(attr, safeItems);
|
|
1092
|
+
} catch {
|
|
1093
|
+
// ignore
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (dataIsObject) {
|
|
1100
|
+
const nextData = setByPath(props.data, attr, safeItems);
|
|
1101
|
+
onChange(nextData);
|
|
1102
|
+
cb();
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
onChange(safeItems);
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
const selectedItem = items[selectedIndex] || null;
|
|
1110
|
+
const formulaInputSignature = (() => {
|
|
1111
|
+
if (!formulaBuilderOpen || !selectedItem) return '';
|
|
1112
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs : [];
|
|
1113
|
+
return inputs.map(inp => (inp && inp.sourceState ? String(inp.sourceState) : '')).filter(Boolean).join('|');
|
|
1114
|
+
})();
|
|
1115
|
+
|
|
1116
|
+
const formulaLiveSignature = (() => {
|
|
1117
|
+
if (!formulaBuilderOpen || !selectedItem) return '';
|
|
1118
|
+
const ids = formulaInputSignature ? String(formulaInputSignature).split('|').filter(Boolean) : [];
|
|
1119
|
+
const parts = ids.map(id => {
|
|
1120
|
+
const ts = formulaLiveTs && Object.prototype.hasOwnProperty.call(formulaLiveTs, id) ? formulaLiveTs[id] : undefined;
|
|
1121
|
+
const val = formulaLiveValues && Object.prototype.hasOwnProperty.call(formulaLiveValues, id) ? formulaLiveValues[id] : undefined;
|
|
1122
|
+
return `${id}:${ts === undefined ? '' : String(ts)}:${stringifyCompact(val, 30)}`;
|
|
1123
|
+
});
|
|
1124
|
+
parts.push(`_poll:${String(formulaLivePollNonce || 0)}`);
|
|
1125
|
+
return parts.join('|');
|
|
1126
|
+
})();
|
|
1127
|
+
|
|
1128
|
+
const getAdapterInstanceId = () => {
|
|
1129
|
+
const adapterName = (props && (props.adapterName || props.adapter)) || 'data-solectrus';
|
|
1130
|
+
const instanceId = props && typeof props.instanceId === 'string' ? props.instanceId : '';
|
|
1131
|
+
if (instanceId && String(instanceId).startsWith('system.adapter.')) return String(instanceId);
|
|
1132
|
+
if (instanceId && /^[a-zA-Z0-9_-]+\.\d+$/.test(String(instanceId))) return String(instanceId);
|
|
1133
|
+
const inst = props && Number.isFinite(props.instance) ? props.instance : 0;
|
|
1134
|
+
return `${adapterName}.${inst}`;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const getAdapterSendToTargets = () => {
|
|
1138
|
+
const base = getAdapterInstanceId();
|
|
1139
|
+
if (!base) return [];
|
|
1140
|
+
if (String(base).startsWith('system.adapter.')) {
|
|
1141
|
+
const short = String(base).slice('system.adapter.'.length);
|
|
1142
|
+
return [String(base), short].filter(Boolean);
|
|
1143
|
+
}
|
|
1144
|
+
return [String(base), `system.adapter.${String(base)}`];
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const getAdapterAliveId = () => {
|
|
1148
|
+
const base = getAdapterInstanceId();
|
|
1149
|
+
if (!base) return '';
|
|
1150
|
+
return String(base).startsWith('system.adapter.') ? `${base}.alive` : `system.adapter.${base}.alive`;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const openFormulaBuilder = () => {
|
|
1154
|
+
if (!selectedItem) return;
|
|
1155
|
+
setFormulaDraft(String(selectedItem.formula || ''));
|
|
1156
|
+
setFormulaBuilderOpen(true);
|
|
1157
|
+
try {
|
|
1158
|
+
globalThis.requestAnimationFrame(() => {
|
|
1159
|
+
const el = formulaEditorRef.current;
|
|
1160
|
+
if (el && typeof el.focus === 'function') {
|
|
1161
|
+
el.focus();
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
} catch {
|
|
1165
|
+
// ignore
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
const refreshFormulaLiveValues = async opts => {
|
|
1170
|
+
const reason = opts && opts.reason ? String(opts.reason) : '';
|
|
1171
|
+
if (!formulaBuilderOpen) return;
|
|
1172
|
+
if (!selectedItem) return;
|
|
1173
|
+
if (!socket || typeof socket.getState !== 'function') return;
|
|
1174
|
+
|
|
1175
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs : [];
|
|
1176
|
+
const ids = inputs
|
|
1177
|
+
.map(inp => (inp && inp.sourceState ? String(inp.sourceState) : ''))
|
|
1178
|
+
.filter(Boolean);
|
|
1179
|
+
const uniqueIds = Array.from(new Set(ids));
|
|
1180
|
+
if (!uniqueIds.length) {
|
|
1181
|
+
setFormulaLiveValues({});
|
|
1182
|
+
setFormulaLiveTs({});
|
|
1183
|
+
setFormulaLivePollNonce(n => n + 1);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
setFormulaLiveLoading(true);
|
|
1188
|
+
try {
|
|
1189
|
+
const results = await Promise.all(
|
|
1190
|
+
uniqueIds.map(async id => {
|
|
1191
|
+
try {
|
|
1192
|
+
const st = await socket.getState(id);
|
|
1193
|
+
return { id, st: st || null };
|
|
1194
|
+
} catch {
|
|
1195
|
+
return { id, st: null };
|
|
1196
|
+
}
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
const nextVals = {};
|
|
1200
|
+
const nextTs = {};
|
|
1201
|
+
for (const r of results) {
|
|
1202
|
+
nextVals[r.id] = r.st ? r.st.val : undefined;
|
|
1203
|
+
nextTs[r.id] = r.st && typeof r.st.ts === 'number' ? r.st.ts : undefined;
|
|
1204
|
+
}
|
|
1205
|
+
setFormulaLiveValues(nextVals);
|
|
1206
|
+
setFormulaLiveTs(nextTs);
|
|
1207
|
+
setFormulaLivePollNonce(n => n + 1);
|
|
1208
|
+
if (reason && props && props.onDebug) {
|
|
1209
|
+
try {
|
|
1210
|
+
props.onDebug('formulaLiveValues', { reason, count: uniqueIds.length });
|
|
1211
|
+
} catch {
|
|
1212
|
+
// ignore
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
} finally {
|
|
1216
|
+
setFormulaLiveLoading(false);
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const refreshFormulaPreview = async opts => {
|
|
1221
|
+
const reason = opts && opts.reason ? String(opts.reason) : '';
|
|
1222
|
+
const showLoading = !!(opts && opts.showLoading);
|
|
1223
|
+
if (!formulaBuilderOpen) return;
|
|
1224
|
+
if (!selectedItem) return;
|
|
1225
|
+
|
|
1226
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs : [];
|
|
1227
|
+
const vars = Object.create(null);
|
|
1228
|
+
for (const inp of inputs) {
|
|
1229
|
+
const key = sanitizeInputKey(inp && inp.key ? inp.key : '');
|
|
1230
|
+
if (!key) continue;
|
|
1231
|
+
const id = inp && inp.sourceState ? String(inp.sourceState) : '';
|
|
1232
|
+
const raw = id ? formulaLiveValues[id] : undefined;
|
|
1233
|
+
vars[key] = computePreviewInputValue(selectedItem, inp, raw);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (showLoading) {
|
|
1237
|
+
setFormulaPreviewLoading(true);
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const val = evalPreviewExpression(String(formulaDraft || ''), vars, t);
|
|
1241
|
+
const previewValue = isNumericOutputItemForPreview(selectedItem)
|
|
1242
|
+
? applyResultRulesForPreview(selectedItem, val)
|
|
1243
|
+
: val;
|
|
1244
|
+
setFormulaPreview({ ok: true, value: previewValue });
|
|
1245
|
+
if (reason && props && props.onDebug) {
|
|
1246
|
+
try {
|
|
1247
|
+
props.onDebug('formulaPreview', { reason, ok: true });
|
|
1248
|
+
} catch {
|
|
1249
|
+
// ignore
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
const err = e && e.message ? String(e.message) : String(e);
|
|
1254
|
+
setFormulaPreview({ ok: false, error: err });
|
|
1255
|
+
} finally {
|
|
1256
|
+
if (showLoading) {
|
|
1257
|
+
setFormulaPreviewLoading(false);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
React.useEffect(() => {
|
|
1263
|
+
if (!formulaBuilderOpen) return;
|
|
1264
|
+
let timer = null;
|
|
1265
|
+
try {
|
|
1266
|
+
timer = setTimeout(() => {
|
|
1267
|
+
refreshFormulaPreview({ reason: 'debounced' });
|
|
1268
|
+
}, 250);
|
|
1269
|
+
} catch {
|
|
1270
|
+
// ignore
|
|
1271
|
+
}
|
|
1272
|
+
return () => {
|
|
1273
|
+
if (timer) {
|
|
1274
|
+
try {
|
|
1275
|
+
clearTimeout(timer);
|
|
1276
|
+
} catch {
|
|
1277
|
+
// ignore
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}, [formulaBuilderOpen, formulaDraft, formulaLiveSignature]);
|
|
1282
|
+
|
|
1283
|
+
React.useEffect(() => {
|
|
1284
|
+
if (!formulaBuilderOpen) return;
|
|
1285
|
+
let alive = true;
|
|
1286
|
+
let timer = null;
|
|
1287
|
+
|
|
1288
|
+
const run = async () => {
|
|
1289
|
+
if (!alive) return;
|
|
1290
|
+
await refreshFormulaLiveValues({ reason: 'auto' });
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
run();
|
|
1294
|
+
try {
|
|
1295
|
+
timer = setInterval(() => {
|
|
1296
|
+
run();
|
|
1297
|
+
}, 2000);
|
|
1298
|
+
} catch {
|
|
1299
|
+
// ignore
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return () => {
|
|
1303
|
+
alive = false;
|
|
1304
|
+
if (timer) {
|
|
1305
|
+
try {
|
|
1306
|
+
clearInterval(timer);
|
|
1307
|
+
} catch {
|
|
1308
|
+
// ignore
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}, [formulaBuilderOpen, selectedIndex, formulaInputSignature]);
|
|
1313
|
+
|
|
1314
|
+
const insertIntoFormulaDraft = opts => {
|
|
1315
|
+
const text = opts && opts.text !== undefined ? String(opts.text) : '';
|
|
1316
|
+
const el = formulaEditorRef.current;
|
|
1317
|
+
const curValue = String(formulaDraft || '');
|
|
1318
|
+
const selStart = el && typeof el.selectionStart === 'number' ? el.selectionStart : curValue.length;
|
|
1319
|
+
const selEnd = el && typeof el.selectionEnd === 'number' ? el.selectionEnd : curValue.length;
|
|
1320
|
+
const before = curValue.slice(0, selStart);
|
|
1321
|
+
const after = curValue.slice(selEnd);
|
|
1322
|
+
const next = before + text + after;
|
|
1323
|
+
|
|
1324
|
+
const startWithin = opts && typeof opts.selectStartWithinText === 'number' ? opts.selectStartWithinText : text.length;
|
|
1325
|
+
const endWithin = opts && typeof opts.selectEndWithinText === 'number' ? opts.selectEndWithinText : text.length;
|
|
1326
|
+
const nextSelStart = selStart + Math.max(0, startWithin);
|
|
1327
|
+
const nextSelEnd = selStart + Math.max(0, endWithin);
|
|
1328
|
+
|
|
1329
|
+
setFormulaDraft(next);
|
|
1330
|
+
try {
|
|
1331
|
+
globalThis.requestAnimationFrame(() => {
|
|
1332
|
+
const el2 = formulaEditorRef.current;
|
|
1333
|
+
if (!el2 || typeof el2.focus !== 'function') return;
|
|
1334
|
+
el2.focus();
|
|
1335
|
+
try {
|
|
1336
|
+
el2.setSelectionRange(nextSelStart, nextSelEnd);
|
|
1337
|
+
} catch {
|
|
1338
|
+
// ignore
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
} catch {
|
|
1342
|
+
// ignore
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
const updateSelected = (field, value) => {
|
|
1347
|
+
const nextItems = items.map((it, i) => {
|
|
1348
|
+
if (i !== selectedIndex) return it;
|
|
1349
|
+
const next = Object.assign({}, it || {});
|
|
1350
|
+
next[field] = value;
|
|
1351
|
+
return ensureTitle(next, t);
|
|
1352
|
+
});
|
|
1353
|
+
updateItems(nextItems);
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
const moveSelected = direction => {
|
|
1357
|
+
const from = selectedIndex;
|
|
1358
|
+
const to = from + direction;
|
|
1359
|
+
if (to < 0 || to >= items.length) return;
|
|
1360
|
+
|
|
1361
|
+
const nextItems = items.slice();
|
|
1362
|
+
const tmp = nextItems[from];
|
|
1363
|
+
nextItems[from] = nextItems[to];
|
|
1364
|
+
nextItems[to] = tmp;
|
|
1365
|
+
|
|
1366
|
+
updateItems(nextItems);
|
|
1367
|
+
setSelectedIndex(to);
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
const addItem = () => {
|
|
1371
|
+
const nextItems = items.concat([makeNewItem(t)]);
|
|
1372
|
+
updateItems(nextItems);
|
|
1373
|
+
setSelectedIndex(nextItems.length - 1);
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
const cloneSelected = () => {
|
|
1377
|
+
if (!selectedItem) return;
|
|
1378
|
+
const clone = ensureTitle(Object.assign({}, selectedItem), t);
|
|
1379
|
+
const nextItems = items.slice();
|
|
1380
|
+
nextItems.splice(selectedIndex + 1, 0, clone);
|
|
1381
|
+
updateItems(nextItems);
|
|
1382
|
+
setSelectedIndex(selectedIndex + 1);
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
const deleteSelected = () => {
|
|
1386
|
+
if (!selectedItem) return;
|
|
1387
|
+
const nextItems = items.slice();
|
|
1388
|
+
nextItems.splice(selectedIndex, 1);
|
|
1389
|
+
updateItems(nextItems);
|
|
1390
|
+
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const updateInput = (index, field, value) => {
|
|
1394
|
+
if (!selectedItem) return;
|
|
1395
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs.slice() : [];
|
|
1396
|
+
const cur = inputs[index] && typeof inputs[index] === 'object' ? Object.assign({}, inputs[index]) : {};
|
|
1397
|
+
cur[field] = value;
|
|
1398
|
+
inputs[index] = cur;
|
|
1399
|
+
updateSelected('inputs', inputs);
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
const addInput = () => {
|
|
1403
|
+
if (!selectedItem) return;
|
|
1404
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs.slice() : [];
|
|
1405
|
+
inputs.push({ key: '', sourceState: '', jsonPath: '', noNegative: false });
|
|
1406
|
+
updateSelected('inputs', inputs);
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const deleteInput = index => {
|
|
1410
|
+
if (!selectedItem) return;
|
|
1411
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs.slice() : [];
|
|
1412
|
+
inputs.splice(index, 1);
|
|
1413
|
+
updateSelected('inputs', inputs);
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
const rootStyle = {
|
|
1417
|
+
display: 'flex',
|
|
1418
|
+
gap: 12,
|
|
1419
|
+
width: '100%',
|
|
1420
|
+
minHeight: 360,
|
|
1421
|
+
height: '70vh',
|
|
1422
|
+
color: colors.text,
|
|
1423
|
+
position: 'relative',
|
|
1424
|
+
alignItems: 'stretch',
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
const leftStyle = {
|
|
1428
|
+
width: 340,
|
|
1429
|
+
maxWidth: '40%',
|
|
1430
|
+
border: `1px solid ${colors.border}`,
|
|
1431
|
+
borderRadius: 6,
|
|
1432
|
+
overflow: 'hidden',
|
|
1433
|
+
display: 'flex',
|
|
1434
|
+
flexDirection: 'column',
|
|
1435
|
+
background: colors.panelBg,
|
|
1436
|
+
height: '100%',
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
const rightStyle = {
|
|
1440
|
+
flex: 1,
|
|
1441
|
+
border: `1px solid ${colors.border}`,
|
|
1442
|
+
borderRadius: 6,
|
|
1443
|
+
padding: 12,
|
|
1444
|
+
background: colors.panelBg2,
|
|
1445
|
+
height: '100%',
|
|
1446
|
+
overflow: 'auto',
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
const toolbarStyle = {
|
|
1450
|
+
display: 'flex',
|
|
1451
|
+
gap: 8,
|
|
1452
|
+
padding: 10,
|
|
1453
|
+
borderBottom: `1px solid ${colors.rowBorder}`,
|
|
1454
|
+
flexWrap: 'wrap',
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
const listStyle = {
|
|
1458
|
+
overflowY: 'auto',
|
|
1459
|
+
overflowX: 'hidden',
|
|
1460
|
+
flex: 1,
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const btnStyle = {
|
|
1464
|
+
padding: '6px 10px',
|
|
1465
|
+
borderRadius: 6,
|
|
1466
|
+
border: `1px solid ${colors.border}`,
|
|
1467
|
+
background: 'transparent',
|
|
1468
|
+
cursor: 'pointer',
|
|
1469
|
+
color: colors.text,
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
const btnDangerStyle = Object.assign({}, btnStyle, {
|
|
1473
|
+
border: `1px solid ${isDark ? 'rgba(255,120,120,0.5)' : 'rgba(200,0,0,0.25)'}`,
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
const listBtnStyle = isActive => ({
|
|
1477
|
+
width: '100%',
|
|
1478
|
+
textAlign: 'left',
|
|
1479
|
+
padding: '10px 10px',
|
|
1480
|
+
border: 'none',
|
|
1481
|
+
borderBottom: `1px solid ${colors.rowBorder}`,
|
|
1482
|
+
background: isActive ? colors.active : 'transparent',
|
|
1483
|
+
cursor: 'pointer',
|
|
1484
|
+
fontFamily: 'inherit',
|
|
1485
|
+
fontSize: 14,
|
|
1486
|
+
display: 'flex',
|
|
1487
|
+
gap: 8,
|
|
1488
|
+
alignItems: 'center',
|
|
1489
|
+
overflow: 'hidden',
|
|
1490
|
+
color: colors.text,
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
const labelStyle = { display: 'block', fontSize: 12, color: colors.textMuted, marginTop: 10 };
|
|
1494
|
+
const inputStyle = {
|
|
1495
|
+
width: '100%',
|
|
1496
|
+
padding: '8px 10px',
|
|
1497
|
+
borderRadius: 6,
|
|
1498
|
+
border: `1px solid ${colors.border}`,
|
|
1499
|
+
fontFamily: 'inherit',
|
|
1500
|
+
fontSize: 14,
|
|
1501
|
+
color: colors.text,
|
|
1502
|
+
background: colors.inputBg,
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// Chrome/OS dropdowns may render <option> on a light surface even in dark mode,
|
|
1506
|
+
// but inherit the white text color -> white on white. Force readable option styling.
|
|
1507
|
+
const selectStyle = Object.assign({}, inputStyle, {
|
|
1508
|
+
backgroundColor: colors.panelBg,
|
|
1509
|
+
color: colors.text,
|
|
1510
|
+
colorScheme: isDark ? 'dark' : 'light',
|
|
1511
|
+
WebkitTextFillColor: colors.text,
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
const optionStyle = {
|
|
1515
|
+
background: colors.panelBg,
|
|
1516
|
+
color: colors.text,
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
// Native <select>/<option> popups can ignore styles in Chrome dark mode (OS-rendered).
|
|
1520
|
+
// Use a custom dropdown to ensure readable options.
|
|
1521
|
+
// Match the visual style of normal inputs in this editor.
|
|
1522
|
+
// In dark mode, inputStyle.background is slightly transparent (rgba). For dropdown menus
|
|
1523
|
+
// that looks wrong because the page background shines through. Use an opaque panel color.
|
|
1524
|
+
const bgStr = String(inputStyle.background || '');
|
|
1525
|
+
const isTranslucent = isDark && bgStr.startsWith('rgba(');
|
|
1526
|
+
const dropdownBg = isTranslucent ? colors.panelBg : inputStyle.background;
|
|
1527
|
+
const dropdownText = colors.text;
|
|
1528
|
+
|
|
1529
|
+
const dropdownButtonStyle = Object.assign({}, inputStyle, {
|
|
1530
|
+
display: 'flex',
|
|
1531
|
+
alignItems: 'center',
|
|
1532
|
+
justifyContent: 'space-between',
|
|
1533
|
+
background: dropdownBg,
|
|
1534
|
+
color: dropdownText,
|
|
1535
|
+
cursor: 'pointer',
|
|
1536
|
+
userSelect: 'none',
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
const dropdownMenuStyle = {
|
|
1540
|
+
position: 'absolute',
|
|
1541
|
+
zIndex: 2000,
|
|
1542
|
+
left: 0,
|
|
1543
|
+
right: 0,
|
|
1544
|
+
marginTop: 6,
|
|
1545
|
+
borderRadius: 8,
|
|
1546
|
+
border: `1px solid ${colors.border}`,
|
|
1547
|
+
background: dropdownBg,
|
|
1548
|
+
color: dropdownText,
|
|
1549
|
+
boxShadow: isDark ? '0 10px 30px rgba(0,0,0,0.45)' : '0 10px 30px rgba(0,0,0,0.18)',
|
|
1550
|
+
maxHeight: 260,
|
|
1551
|
+
overflowY: 'auto',
|
|
1552
|
+
padding: 6,
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const dropdownItemStyle = isActive => ({
|
|
1556
|
+
padding: '8px 10px',
|
|
1557
|
+
borderRadius: 6,
|
|
1558
|
+
cursor: 'pointer',
|
|
1559
|
+
background: isActive ? colors.active : 'transparent',
|
|
1560
|
+
color: dropdownText,
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
const rowStyle2 = { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 };
|
|
1564
|
+
|
|
1565
|
+
const headerBarStyle = {
|
|
1566
|
+
display: 'flex',
|
|
1567
|
+
alignItems: 'center',
|
|
1568
|
+
justifyContent: 'space-between',
|
|
1569
|
+
gap: 12,
|
|
1570
|
+
padding: '10px 12px',
|
|
1571
|
+
border: `1px solid ${colors.border}`,
|
|
1572
|
+
borderRadius: 8,
|
|
1573
|
+
background: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)',
|
|
1574
|
+
marginBottom: 12,
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
const logoUrl = 'data:image/svg+xml;utf8,' + encodeURIComponent(HEADER_LOGO_SVG);
|
|
1578
|
+
|
|
1579
|
+
const renderSelectButton = onClick =>
|
|
1580
|
+
React.createElement(
|
|
1581
|
+
'button',
|
|
1582
|
+
{
|
|
1583
|
+
type: 'button',
|
|
1584
|
+
style: Object.assign({}, btnStyle, { padding: '8px 10px' }),
|
|
1585
|
+
disabled: !(DialogSelectID && socket && theme),
|
|
1586
|
+
title: DialogSelectID && socket && theme ? t('Select from existing states') : t('Selection dialog not available'),
|
|
1587
|
+
onClick,
|
|
1588
|
+
},
|
|
1589
|
+
t('Select')
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1592
|
+
const renderStatePicker = () => {
|
|
1593
|
+
if (!selectContext || !(DialogSelectID && socket && theme)) return null;
|
|
1594
|
+
|
|
1595
|
+
const selected = (() => {
|
|
1596
|
+
if (!selectedItem) return '';
|
|
1597
|
+
if (selectContext.kind === 'itemSource') {
|
|
1598
|
+
return selectedItem.sourceState || '';
|
|
1599
|
+
}
|
|
1600
|
+
if (selectContext.kind === 'input' && Number.isFinite(selectContext.index)) {
|
|
1601
|
+
const inputs = Array.isArray(selectedItem.inputs) ? selectedItem.inputs : [];
|
|
1602
|
+
const inp = inputs[selectContext.index];
|
|
1603
|
+
return inp && inp.sourceState ? inp.sourceState : '';
|
|
1604
|
+
}
|
|
1605
|
+
if (selectContext.kind === 'formulaFn') {
|
|
1606
|
+
return '';
|
|
1607
|
+
}
|
|
1608
|
+
return '';
|
|
1609
|
+
})();
|
|
1610
|
+
|
|
1611
|
+
return React.createElement(DialogSelectID, {
|
|
1612
|
+
key: 'selectStateId',
|
|
1613
|
+
imagePrefix: '../..',
|
|
1614
|
+
dialogName: (props && (props.adapterName || props.adapter)) || 'data-solectrus',
|
|
1615
|
+
themeType: themeType,
|
|
1616
|
+
theme: theme,
|
|
1617
|
+
socket: socket,
|
|
1618
|
+
types: 'state',
|
|
1619
|
+
selected: selected,
|
|
1620
|
+
onClose: () => setSelectContext(null),
|
|
1621
|
+
onOk: sel => {
|
|
1622
|
+
const selectedStr = Array.isArray(sel) ? sel[0] : sel;
|
|
1623
|
+
setSelectContext(null);
|
|
1624
|
+
if (!selectedStr) return;
|
|
1625
|
+
if (selectContext.kind === 'itemSource') {
|
|
1626
|
+
updateSelected('sourceState', selectedStr);
|
|
1627
|
+
}
|
|
1628
|
+
if (selectContext.kind === 'input' && Number.isFinite(selectContext.index)) {
|
|
1629
|
+
updateInput(selectContext.index, 'sourceState', selectedStr);
|
|
1630
|
+
}
|
|
1631
|
+
if (selectContext.kind === 'formulaFn') {
|
|
1632
|
+
const fn = selectContext.fn;
|
|
1633
|
+
if (fn === 's') {
|
|
1634
|
+
insertIntoFormulaDraft({ text: `s("${selectedStr}")`, selectStartWithinText: (`s("`).length + String(selectedStr).length + 2, selectEndWithinText: (`s("`).length + String(selectedStr).length + 2 });
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (fn === 'v') {
|
|
1638
|
+
insertIntoFormulaDraft({ text: `v("${selectedStr}")`, selectStartWithinText: (`v("`).length + String(selectedStr).length + 2, selectEndWithinText: (`v("`).length + String(selectedStr).length + 2 });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
if (fn === 'jp') {
|
|
1642
|
+
const txt = `jp("${selectedStr}", "$.value")`;
|
|
1643
|
+
const start = txt.indexOf('$.value');
|
|
1644
|
+
insertIntoFormulaDraft({ text: txt, selectStartWithinText: start, selectEndWithinText: start + '$.value'.length });
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
});
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
const renderFormulaBuilderModal = () => {
|
|
1653
|
+
if (!formulaBuilderOpen || !selectedItem) return null;
|
|
1654
|
+
|
|
1655
|
+
const overlayStyle = {
|
|
1656
|
+
position: 'fixed',
|
|
1657
|
+
inset: 0,
|
|
1658
|
+
background: isDark ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0.35)',
|
|
1659
|
+
zIndex: 5000,
|
|
1660
|
+
display: 'flex',
|
|
1661
|
+
alignItems: 'center',
|
|
1662
|
+
justifyContent: 'center',
|
|
1663
|
+
padding: 16,
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
const modalStyle = {
|
|
1667
|
+
width: 'min(1100px, 92vw)',
|
|
1668
|
+
height: 'min(780px, 88vh)',
|
|
1669
|
+
borderRadius: 12,
|
|
1670
|
+
border: `1px solid ${colors.border}`,
|
|
1671
|
+
background: colors.panelBg,
|
|
1672
|
+
boxShadow: isDark ? '0 18px 50px rgba(0,0,0,0.55)' : '0 18px 50px rgba(0,0,0,0.22)',
|
|
1673
|
+
display: 'flex',
|
|
1674
|
+
flexDirection: 'column',
|
|
1675
|
+
overflow: 'hidden',
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
const modalHeaderStyle = {
|
|
1679
|
+
display: 'flex',
|
|
1680
|
+
alignItems: 'center',
|
|
1681
|
+
justifyContent: 'space-between',
|
|
1682
|
+
gap: 12,
|
|
1683
|
+
padding: '10px 12px',
|
|
1684
|
+
borderBottom: `1px solid ${colors.rowBorder}`,
|
|
1685
|
+
background: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)',
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
const modalBodyStyle = {
|
|
1689
|
+
flex: 1,
|
|
1690
|
+
display: 'flex',
|
|
1691
|
+
minHeight: 0,
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
const modalLeftStyle = {
|
|
1695
|
+
width: 320,
|
|
1696
|
+
maxWidth: '44%',
|
|
1697
|
+
borderRight: `1px solid ${colors.rowBorder}`,
|
|
1698
|
+
padding: 12,
|
|
1699
|
+
overflow: 'auto',
|
|
1700
|
+
background: colors.panelBg2,
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
const modalRightStyle = {
|
|
1704
|
+
flex: 1,
|
|
1705
|
+
padding: 12,
|
|
1706
|
+
display: 'flex',
|
|
1707
|
+
flexDirection: 'column',
|
|
1708
|
+
gap: 10,
|
|
1709
|
+
minWidth: 0,
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
const sectionTitleStyle = {
|
|
1713
|
+
fontSize: 12,
|
|
1714
|
+
fontWeight: 600,
|
|
1715
|
+
color: colors.text,
|
|
1716
|
+
marginTop: 10,
|
|
1717
|
+
marginBottom: 6,
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
const chipBtnStyle = {
|
|
1721
|
+
display: 'inline-flex',
|
|
1722
|
+
alignItems: 'center',
|
|
1723
|
+
gap: 8,
|
|
1724
|
+
padding: '7px 10px',
|
|
1725
|
+
borderRadius: 999,
|
|
1726
|
+
border: `1px solid ${colors.border}`,
|
|
1727
|
+
background: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
|
|
1728
|
+
cursor: 'pointer',
|
|
1729
|
+
color: colors.text,
|
|
1730
|
+
fontFamily: 'inherit',
|
|
1731
|
+
fontSize: 13,
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
const chipBtnDisabledStyle = Object.assign({}, chipBtnStyle, {
|
|
1735
|
+
opacity: 0.45,
|
|
1736
|
+
cursor: 'not-allowed',
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
const valuePillStyle = {
|
|
1740
|
+
display: 'inline-flex',
|
|
1741
|
+
alignItems: 'center',
|
|
1742
|
+
padding: '4px 8px',
|
|
1743
|
+
borderRadius: 999,
|
|
1744
|
+
border: `1px solid ${colors.border}`,
|
|
1745
|
+
background: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)',
|
|
1746
|
+
color: colors.textMuted,
|
|
1747
|
+
fontSize: 12,
|
|
1748
|
+
maxWidth: '100%',
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
const previewOkPillStyle = Object.assign({}, valuePillStyle, {
|
|
1752
|
+
color: colors.text,
|
|
1753
|
+
background: isDark ? 'rgba(46, 204, 113, 0.12)' : 'rgba(46, 204, 113, 0.10)',
|
|
1754
|
+
border: `1px solid ${isDark ? 'rgba(46, 204, 113, 0.35)' : 'rgba(46, 204, 113, 0.25)'}`,
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
const previewErrPillStyle = Object.assign({}, valuePillStyle, {
|
|
1758
|
+
color: colors.text,
|
|
1759
|
+
background: isDark ? 'rgba(231, 76, 60, 0.12)' : 'rgba(231, 76, 60, 0.10)',
|
|
1760
|
+
border: `1px solid ${isDark ? 'rgba(231, 76, 60, 0.35)' : 'rgba(231, 76, 60, 0.25)'}`,
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
const vars = Array.isArray(selectedItem.inputs)
|
|
1764
|
+
? selectedItem.inputs
|
|
1765
|
+
.map(inp => {
|
|
1766
|
+
const rawKey = inp && inp.key ? String(inp.key) : '';
|
|
1767
|
+
const key = sanitizeInputKey(rawKey);
|
|
1768
|
+
return {
|
|
1769
|
+
rawKey,
|
|
1770
|
+
key,
|
|
1771
|
+
sourceState: inp && inp.sourceState ? String(inp.sourceState) : '',
|
|
1772
|
+
jsonPath: inp && inp.jsonPath ? String(inp.jsonPath) : '',
|
|
1773
|
+
noNegative: !!(inp && inp.noNegative),
|
|
1774
|
+
};
|
|
1775
|
+
})
|
|
1776
|
+
.filter(v => !!v.key)
|
|
1777
|
+
: [];
|
|
1778
|
+
|
|
1779
|
+
const close = () => setFormulaBuilderOpen(false);
|
|
1780
|
+
const apply = () => {
|
|
1781
|
+
updateSelected('formula', String(formulaDraft || ''));
|
|
1782
|
+
setFormulaBuilderOpen(false);
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
const onOverlayMouseDown = e => {
|
|
1786
|
+
if (e && e.target === e.currentTarget) {
|
|
1787
|
+
close();
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
return React.createElement(
|
|
1792
|
+
'div',
|
|
1793
|
+
{ style: overlayStyle, onMouseDown: onOverlayMouseDown },
|
|
1794
|
+
React.createElement(
|
|
1795
|
+
'div',
|
|
1796
|
+
{ style: modalStyle },
|
|
1797
|
+
React.createElement(
|
|
1798
|
+
'div',
|
|
1799
|
+
{ style: modalHeaderStyle },
|
|
1800
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column' } },
|
|
1801
|
+
React.createElement('div', { style: { fontSize: 14, fontWeight: 700 } }, t('Formula Builder')),
|
|
1802
|
+
React.createElement('div', { style: { fontSize: 12, color: colors.textMuted } }, t('Insert building blocks on the left. The editor uses current (unsaved) inputs.'))
|
|
1803
|
+
),
|
|
1804
|
+
React.createElement(
|
|
1805
|
+
'button',
|
|
1806
|
+
{ type: 'button', style: btnStyle, onClick: close, title: t('Close') },
|
|
1807
|
+
t('Close')
|
|
1808
|
+
)
|
|
1809
|
+
),
|
|
1810
|
+
React.createElement(
|
|
1811
|
+
'div',
|
|
1812
|
+
{ style: modalBodyStyle },
|
|
1813
|
+
React.createElement(
|
|
1814
|
+
'div',
|
|
1815
|
+
{ style: modalLeftStyle },
|
|
1816
|
+
React.createElement('div', { style: sectionTitleStyle }, t('Variables (Inputs)')),
|
|
1817
|
+
React.createElement(
|
|
1818
|
+
'div',
|
|
1819
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 } },
|
|
1820
|
+
React.createElement('div', { style: { fontSize: 12, color: colors.textMuted } }, t('Live values')),
|
|
1821
|
+
React.createElement(
|
|
1822
|
+
'button',
|
|
1823
|
+
{
|
|
1824
|
+
type: 'button',
|
|
1825
|
+
style: Object.assign({}, btnStyle, { padding: '5px 9px', fontSize: 12 }),
|
|
1826
|
+
disabled: formulaLiveLoading || !(socket && typeof socket.getState === 'function'),
|
|
1827
|
+
onClick: () => refreshFormulaLiveValues({ reason: 'manual' }),
|
|
1828
|
+
title: t('Refresh'),
|
|
1829
|
+
},
|
|
1830
|
+
formulaLiveLoading ? t('Loading…') : t('Refresh')
|
|
1831
|
+
)
|
|
1832
|
+
),
|
|
1833
|
+
vars.length
|
|
1834
|
+
? vars.map((v, idx) => {
|
|
1835
|
+
const title = v.sourceState ? `${v.rawKey} ← ${v.sourceState}` : v.rawKey;
|
|
1836
|
+
const liveId = v.sourceState;
|
|
1837
|
+
const rawLiveVal = liveId ? formulaLiveValues[liveId] : undefined;
|
|
1838
|
+
const liveVal = liveId ? computePreviewInputValue(selectedItem, v, rawLiveVal) : undefined;
|
|
1839
|
+
const liveTs = liveId ? formulaLiveTs[liveId] : undefined;
|
|
1840
|
+
const liveText = liveId
|
|
1841
|
+
? liveVal === undefined
|
|
1842
|
+
? t('n/a')
|
|
1843
|
+
: stringifyCompact(liveVal)
|
|
1844
|
+
: t('n/a');
|
|
1845
|
+
return React.createElement(
|
|
1846
|
+
'div',
|
|
1847
|
+
{
|
|
1848
|
+
key: `${v.key}|${idx}`,
|
|
1849
|
+
style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 8, alignItems: 'center' },
|
|
1850
|
+
},
|
|
1851
|
+
React.createElement(
|
|
1852
|
+
'button',
|
|
1853
|
+
{
|
|
1854
|
+
type: 'button',
|
|
1855
|
+
style: chipBtnStyle,
|
|
1856
|
+
onClick: () => insertIntoFormulaDraft({ text: v.key }),
|
|
1857
|
+
title,
|
|
1858
|
+
},
|
|
1859
|
+
v.key,
|
|
1860
|
+
v.rawKey && v.rawKey !== v.key
|
|
1861
|
+
? React.createElement(
|
|
1862
|
+
'span',
|
|
1863
|
+
{ style: { fontSize: 11, opacity: 0.75 } },
|
|
1864
|
+
`(${v.rawKey})`
|
|
1865
|
+
)
|
|
1866
|
+
: null
|
|
1867
|
+
),
|
|
1868
|
+
v.sourceState
|
|
1869
|
+
? React.createElement(
|
|
1870
|
+
'span',
|
|
1871
|
+
{
|
|
1872
|
+
style: valuePillStyle,
|
|
1873
|
+
title: liveTs
|
|
1874
|
+
? `${liveText} @ ${new Date(liveTs).toLocaleString()}`
|
|
1875
|
+
: liveText,
|
|
1876
|
+
},
|
|
1877
|
+
liveText
|
|
1878
|
+
)
|
|
1879
|
+
: null
|
|
1880
|
+
);
|
|
1881
|
+
})
|
|
1882
|
+
: React.createElement(
|
|
1883
|
+
'div',
|
|
1884
|
+
{ style: { fontSize: 12, color: colors.textMuted } },
|
|
1885
|
+
t('No inputs configured yet.')
|
|
1886
|
+
),
|
|
1887
|
+
|
|
1888
|
+
React.createElement('div', { style: sectionTitleStyle }, t('Operators')),
|
|
1889
|
+
React.createElement(
|
|
1890
|
+
'div',
|
|
1891
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
1892
|
+
['+', '-', '*', '/', '%', '(', ')', '&&', '||', '!', '==', '!=', '>=', '<=', '>', '<', '?', ':'].map(op =>
|
|
1893
|
+
React.createElement(
|
|
1894
|
+
'button',
|
|
1895
|
+
{
|
|
1896
|
+
key: op,
|
|
1897
|
+
type: 'button',
|
|
1898
|
+
style: chipBtnStyle,
|
|
1899
|
+
onClick: () => insertIntoFormulaDraft({ text: op }),
|
|
1900
|
+
},
|
|
1901
|
+
op
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
),
|
|
1905
|
+
|
|
1906
|
+
React.createElement('div', { style: sectionTitleStyle }, t('Functions')),
|
|
1907
|
+
React.createElement(
|
|
1908
|
+
'div',
|
|
1909
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
1910
|
+
React.createElement(
|
|
1911
|
+
'button',
|
|
1912
|
+
{
|
|
1913
|
+
type: 'button',
|
|
1914
|
+
style: chipBtnStyle,
|
|
1915
|
+
onClick: () =>
|
|
1916
|
+
insertIntoFormulaDraft({ text: 'min(a, b)', selectStartWithinText: 4, selectEndWithinText: 5 }),
|
|
1917
|
+
},
|
|
1918
|
+
t('min')
|
|
1919
|
+
),
|
|
1920
|
+
React.createElement(
|
|
1921
|
+
'button',
|
|
1922
|
+
{
|
|
1923
|
+
type: 'button',
|
|
1924
|
+
style: chipBtnStyle,
|
|
1925
|
+
onClick: () =>
|
|
1926
|
+
insertIntoFormulaDraft({ text: 'max(a, b)', selectStartWithinText: 4, selectEndWithinText: 5 }),
|
|
1927
|
+
},
|
|
1928
|
+
t('max')
|
|
1929
|
+
),
|
|
1930
|
+
React.createElement(
|
|
1931
|
+
'button',
|
|
1932
|
+
{
|
|
1933
|
+
type: 'button',
|
|
1934
|
+
style: chipBtnStyle,
|
|
1935
|
+
onClick: () =>
|
|
1936
|
+
insertIntoFormulaDraft({
|
|
1937
|
+
text: 'clamp(value, min, max)',
|
|
1938
|
+
selectStartWithinText: 6,
|
|
1939
|
+
selectEndWithinText: 11,
|
|
1940
|
+
}),
|
|
1941
|
+
},
|
|
1942
|
+
t('clamp')
|
|
1943
|
+
),
|
|
1944
|
+
React.createElement(
|
|
1945
|
+
'button',
|
|
1946
|
+
{
|
|
1947
|
+
type: 'button',
|
|
1948
|
+
style: chipBtnStyle,
|
|
1949
|
+
onClick: () =>
|
|
1950
|
+
insertIntoFormulaDraft({
|
|
1951
|
+
text: 'IF(condition, valueIfTrue, valueIfFalse)',
|
|
1952
|
+
selectStartWithinText: 3,
|
|
1953
|
+
selectEndWithinText: 12,
|
|
1954
|
+
}),
|
|
1955
|
+
},
|
|
1956
|
+
t('IF')
|
|
1957
|
+
)
|
|
1958
|
+
),
|
|
1959
|
+
|
|
1960
|
+
React.createElement('div', { style: sectionTitleStyle }, t('State functions')),
|
|
1961
|
+
React.createElement(
|
|
1962
|
+
'div',
|
|
1963
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
1964
|
+
React.createElement(
|
|
1965
|
+
'button',
|
|
1966
|
+
{
|
|
1967
|
+
type: 'button',
|
|
1968
|
+
style: DialogSelectID && socket && theme ? chipBtnStyle : chipBtnDisabledStyle,
|
|
1969
|
+
disabled: !(DialogSelectID && socket && theme),
|
|
1970
|
+
onClick: () => setSelectContext({ kind: 'formulaFn', fn: 's' }),
|
|
1971
|
+
title: t('Pick a state id and insert s("id")'),
|
|
1972
|
+
},
|
|
1973
|
+
t('Insert s()')
|
|
1974
|
+
),
|
|
1975
|
+
React.createElement(
|
|
1976
|
+
'button',
|
|
1977
|
+
{
|
|
1978
|
+
type: 'button',
|
|
1979
|
+
style: DialogSelectID && socket && theme ? chipBtnStyle : chipBtnDisabledStyle,
|
|
1980
|
+
disabled: !(DialogSelectID && socket && theme),
|
|
1981
|
+
onClick: () => setSelectContext({ kind: 'formulaFn', fn: 'v' }),
|
|
1982
|
+
title: t('Pick a state id and insert v("id")'),
|
|
1983
|
+
},
|
|
1984
|
+
t('Insert v()')
|
|
1985
|
+
),
|
|
1986
|
+
React.createElement(
|
|
1987
|
+
'button',
|
|
1988
|
+
{
|
|
1989
|
+
type: 'button',
|
|
1990
|
+
style: DialogSelectID && socket && theme ? chipBtnStyle : chipBtnDisabledStyle,
|
|
1991
|
+
disabled: !(DialogSelectID && socket && theme),
|
|
1992
|
+
onClick: () => setSelectContext({ kind: 'formulaFn', fn: 'jp' }),
|
|
1993
|
+
title: t('Pick a state id and insert jp("id", "$.value")'),
|
|
1994
|
+
},
|
|
1995
|
+
t('Insert jp()')
|
|
1996
|
+
)
|
|
1997
|
+
)
|
|
1998
|
+
),
|
|
1999
|
+
React.createElement(
|
|
2000
|
+
'div',
|
|
2001
|
+
{ style: modalRightStyle },
|
|
2002
|
+
React.createElement(
|
|
2003
|
+
'div',
|
|
2004
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 } },
|
|
2005
|
+
React.createElement(
|
|
2006
|
+
'label',
|
|
2007
|
+
{ style: Object.assign({}, labelStyle, { marginTop: 0 }) },
|
|
2008
|
+
t('Formula expression')
|
|
2009
|
+
),
|
|
2010
|
+
React.createElement(
|
|
2011
|
+
'div',
|
|
2012
|
+
{
|
|
2013
|
+
style: {
|
|
2014
|
+
display: 'flex',
|
|
2015
|
+
alignItems: 'center',
|
|
2016
|
+
justifyContent: 'flex-end',
|
|
2017
|
+
gap: 8,
|
|
2018
|
+
flexWrap: 'wrap',
|
|
2019
|
+
},
|
|
2020
|
+
},
|
|
2021
|
+
React.createElement(
|
|
2022
|
+
'span',
|
|
2023
|
+
{ style: { fontSize: 12, color: colors.textMuted } },
|
|
2024
|
+
t('Result')
|
|
2025
|
+
),
|
|
2026
|
+
React.createElement(
|
|
2027
|
+
'button',
|
|
2028
|
+
{
|
|
2029
|
+
type: 'button',
|
|
2030
|
+
style: Object.assign({}, btnStyle, { padding: '5px 9px', fontSize: 12 }),
|
|
2031
|
+
disabled: formulaPreviewLoading,
|
|
2032
|
+
onClick: () => refreshFormulaPreview({ reason: 'manual', showLoading: true }),
|
|
2033
|
+
title: t('Refresh preview'),
|
|
2034
|
+
},
|
|
2035
|
+
formulaPreviewLoading ? t('Loading…') : t('Refresh')
|
|
2036
|
+
),
|
|
2037
|
+
formulaPreview && formulaPreview.ok
|
|
2038
|
+
? React.createElement(
|
|
2039
|
+
'span',
|
|
2040
|
+
{ style: previewOkPillStyle, title: stringifyCompact(formulaPreview.value, 200) },
|
|
2041
|
+
stringifyCompact(formulaPreview.value)
|
|
2042
|
+
)
|
|
2043
|
+
: formulaPreview && !formulaPreview.ok
|
|
2044
|
+
? React.createElement(
|
|
2045
|
+
'span',
|
|
2046
|
+
{ style: previewErrPillStyle, title: formulaPreview.error ? String(formulaPreview.error) : '' },
|
|
2047
|
+
formulaPreview.error ? stringifyCompact(formulaPreview.error) : t('n/a')
|
|
2048
|
+
)
|
|
2049
|
+
: React.createElement('span', { style: valuePillStyle }, t('n/a'))
|
|
2050
|
+
)
|
|
2051
|
+
),
|
|
2052
|
+
React.createElement('textarea', {
|
|
2053
|
+
ref: formulaEditorRef,
|
|
2054
|
+
style: Object.assign({}, inputStyle, {
|
|
2055
|
+
minHeight: 260,
|
|
2056
|
+
flex: 1,
|
|
2057
|
+
resize: 'none',
|
|
2058
|
+
fontFamily:
|
|
2059
|
+
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
2060
|
+
lineHeight: 1.45,
|
|
2061
|
+
}),
|
|
2062
|
+
value: formulaDraft,
|
|
2063
|
+
onChange: e => setFormulaDraft(e.target.value),
|
|
2064
|
+
placeholder: t('e.g. pv1 + pv2 + pv3'),
|
|
2065
|
+
spellCheck: false,
|
|
2066
|
+
}),
|
|
2067
|
+
React.createElement(
|
|
2068
|
+
'div',
|
|
2069
|
+
{ style: { display: 'flex', justifyContent: 'space-between', gap: 8 } },
|
|
2070
|
+
React.createElement(
|
|
2071
|
+
'div',
|
|
2072
|
+
{ style: { fontSize: 12, color: colors.textMuted, alignSelf: 'center' } },
|
|
2073
|
+
t('Tip: You can still edit the formula as plain text anytime.')
|
|
2074
|
+
),
|
|
2075
|
+
React.createElement(
|
|
2076
|
+
'div',
|
|
2077
|
+
{ style: { display: 'flex', gap: 8 } },
|
|
2078
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: close }, t('Cancel')),
|
|
2079
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: apply }, t('Apply'))
|
|
2080
|
+
)
|
|
2081
|
+
)
|
|
2082
|
+
)
|
|
2083
|
+
),
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
return React.createElement(
|
|
2089
|
+
'div',
|
|
2090
|
+
{ style: rootStyle },
|
|
2091
|
+
DEBUG
|
|
2092
|
+
? React.createElement(
|
|
2093
|
+
'div',
|
|
2094
|
+
{
|
|
2095
|
+
style: {
|
|
2096
|
+
position: 'absolute',
|
|
2097
|
+
right: 14,
|
|
2098
|
+
marginTop: -22,
|
|
2099
|
+
fontSize: 11,
|
|
2100
|
+
opacity: 0.7,
|
|
2101
|
+
color: colors.textMuted,
|
|
2102
|
+
pointerEvents: 'none',
|
|
2103
|
+
},
|
|
2104
|
+
},
|
|
2105
|
+
`Items UI ${UI_VERSION}`
|
|
2106
|
+
)
|
|
2107
|
+
: null,
|
|
2108
|
+
React.createElement(
|
|
2109
|
+
'div',
|
|
2110
|
+
{ style: leftStyle },
|
|
2111
|
+
React.createElement(
|
|
2112
|
+
'div',
|
|
2113
|
+
{ style: toolbarStyle },
|
|
2114
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: addItem }, t('Add')),
|
|
2115
|
+
React.createElement(
|
|
2116
|
+
'button',
|
|
2117
|
+
{ type: 'button', style: btnStyle, onClick: cloneSelected, disabled: !selectedItem },
|
|
2118
|
+
t('Duplicate')
|
|
2119
|
+
),
|
|
2120
|
+
React.createElement(
|
|
2121
|
+
'button',
|
|
2122
|
+
{ type: 'button', style: btnDangerStyle, onClick: deleteSelected, disabled: !selectedItem },
|
|
2123
|
+
t('Delete')
|
|
2124
|
+
),
|
|
2125
|
+
React.createElement(
|
|
2126
|
+
'button',
|
|
2127
|
+
{ type: 'button', style: btnStyle, onClick: () => moveSelected(-1), disabled: selectedIndex <= 0 },
|
|
2128
|
+
t('Up')
|
|
2129
|
+
),
|
|
2130
|
+
React.createElement(
|
|
2131
|
+
'button',
|
|
2132
|
+
{
|
|
2133
|
+
type: 'button',
|
|
2134
|
+
style: btnStyle,
|
|
2135
|
+
onClick: () => moveSelected(1),
|
|
2136
|
+
disabled: selectedIndex >= items.length - 1,
|
|
2137
|
+
},
|
|
2138
|
+
t('Down')
|
|
2139
|
+
)
|
|
2140
|
+
),
|
|
2141
|
+
React.createElement(
|
|
2142
|
+
'div',
|
|
2143
|
+
{ style: listStyle },
|
|
2144
|
+
items.length
|
|
2145
|
+
? items.map((it, i) =>
|
|
2146
|
+
React.createElement(
|
|
2147
|
+
'button',
|
|
2148
|
+
{
|
|
2149
|
+
key: i,
|
|
2150
|
+
type: 'button',
|
|
2151
|
+
style: listBtnStyle(i === selectedIndex),
|
|
2152
|
+
onClick: () => setSelectedIndex(i),
|
|
2153
|
+
},
|
|
2154
|
+
React.createElement('span', { style: { width: 22 } }, it.enabled ? '🟢' : '⚪'),
|
|
2155
|
+
React.createElement(
|
|
2156
|
+
'span',
|
|
2157
|
+
{
|
|
2158
|
+
style: {
|
|
2159
|
+
fontWeight: 600,
|
|
2160
|
+
flex: 1,
|
|
2161
|
+
minWidth: 0,
|
|
2162
|
+
overflow: 'hidden',
|
|
2163
|
+
textOverflow: 'ellipsis',
|
|
2164
|
+
whiteSpace: 'nowrap',
|
|
2165
|
+
},
|
|
2166
|
+
title: it.name || it.targetId || t('Unnamed'),
|
|
2167
|
+
},
|
|
2168
|
+
it.name || it.targetId || t('Unnamed')
|
|
2169
|
+
)
|
|
2170
|
+
)
|
|
2171
|
+
)
|
|
2172
|
+
: React.createElement(
|
|
2173
|
+
'div',
|
|
2174
|
+
{ style: { padding: 12, opacity: 0.9, color: colors.textMuted } },
|
|
2175
|
+
t('No items configured.')
|
|
2176
|
+
)
|
|
2177
|
+
)
|
|
2178
|
+
),
|
|
2179
|
+
React.createElement(
|
|
2180
|
+
'div',
|
|
2181
|
+
{ style: rightStyle },
|
|
2182
|
+
selectedItem
|
|
2183
|
+
? React.createElement(
|
|
2184
|
+
React.Fragment,
|
|
2185
|
+
null,
|
|
2186
|
+
React.createElement(
|
|
2187
|
+
'div',
|
|
2188
|
+
{ style: headerBarStyle },
|
|
2189
|
+
React.createElement(
|
|
2190
|
+
'div',
|
|
2191
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 } },
|
|
2192
|
+
React.createElement('img', {
|
|
2193
|
+
src: logoUrl,
|
|
2194
|
+
width: 28,
|
|
2195
|
+
height: 28,
|
|
2196
|
+
style: { display: 'block', borderRadius: 6 },
|
|
2197
|
+
alt: 'Data-SOLECTRUS',
|
|
2198
|
+
}),
|
|
2199
|
+
React.createElement(
|
|
2200
|
+
'div',
|
|
2201
|
+
{ style: { minWidth: 0 } },
|
|
2202
|
+
React.createElement(
|
|
2203
|
+
'div',
|
|
2204
|
+
{ style: { fontSize: 14, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } },
|
|
2205
|
+
'Data-SOLECTRUS'
|
|
2206
|
+
),
|
|
2207
|
+
React.createElement(
|
|
2208
|
+
'div',
|
|
2209
|
+
{ style: { fontSize: 12, opacity: 0.75, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } },
|
|
2210
|
+
t('Configured values')
|
|
2211
|
+
)
|
|
2212
|
+
)
|
|
2213
|
+
),
|
|
2214
|
+
React.createElement(
|
|
2215
|
+
'div',
|
|
2216
|
+
{ style: { fontSize: 11, opacity: 0.7, color: colors.textMuted } },
|
|
2217
|
+
`UI ${UI_VERSION}`
|
|
2218
|
+
)
|
|
2219
|
+
),
|
|
2220
|
+
React.createElement(
|
|
2221
|
+
'div',
|
|
2222
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
2223
|
+
React.createElement(
|
|
2224
|
+
'div',
|
|
2225
|
+
{ style: { fontSize: 16, fontWeight: 700 } },
|
|
2226
|
+
calcTitle(selectedItem, t)
|
|
2227
|
+
)
|
|
2228
|
+
),
|
|
2229
|
+
React.createElement(
|
|
2230
|
+
'label',
|
|
2231
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10 } },
|
|
2232
|
+
React.createElement('input', {
|
|
2233
|
+
type: 'checkbox',
|
|
2234
|
+
checked: !!selectedItem.enabled,
|
|
2235
|
+
onChange: e => updateSelected('enabled', !!e.target.checked),
|
|
2236
|
+
}),
|
|
2237
|
+
React.createElement('span', null, t('Enabled'))
|
|
2238
|
+
),
|
|
2239
|
+
React.createElement('label', { style: labelStyle }, t('Name')),
|
|
2240
|
+
React.createElement('input', {
|
|
2241
|
+
style: inputStyle,
|
|
2242
|
+
type: 'text',
|
|
2243
|
+
value: selectedItem.name || '',
|
|
2244
|
+
onChange: e => updateSelected('name', e.target.value),
|
|
2245
|
+
}),
|
|
2246
|
+
React.createElement('label', { style: labelStyle }, t('Folder/Group')),
|
|
2247
|
+
React.createElement('input', {
|
|
2248
|
+
style: inputStyle,
|
|
2249
|
+
type: 'text',
|
|
2250
|
+
value: selectedItem.group || '',
|
|
2251
|
+
onChange: e => updateSelected('group', e.target.value),
|
|
2252
|
+
placeholder: 'pv',
|
|
2253
|
+
}),
|
|
2254
|
+
React.createElement('label', { style: labelStyle }, t('Target ID')),
|
|
2255
|
+
React.createElement('input', {
|
|
2256
|
+
style: inputStyle,
|
|
2257
|
+
type: 'text',
|
|
2258
|
+
value: selectedItem.targetId || '',
|
|
2259
|
+
onChange: e => updateSelected('targetId', e.target.value),
|
|
2260
|
+
placeholder: 'pv.pvGesamt',
|
|
2261
|
+
}),
|
|
2262
|
+
React.createElement('label', { style: labelStyle }, t('Mode')),
|
|
2263
|
+
React.createElement(
|
|
2264
|
+
'div',
|
|
2265
|
+
{ style: { position: 'relative' }, 'data-ds-dropdown': '1' },
|
|
2266
|
+
React.createElement(
|
|
2267
|
+
'div',
|
|
2268
|
+
{
|
|
2269
|
+
style: dropdownButtonStyle,
|
|
2270
|
+
role: 'button',
|
|
2271
|
+
tabIndex: 0,
|
|
2272
|
+
onClick: () => setOpenDropdown(openDropdown === 'mode' ? null : 'mode'),
|
|
2273
|
+
onKeyDown: e => {
|
|
2274
|
+
if (e && (e.key === 'Enter' || e.key === ' ')) {
|
|
2275
|
+
e.preventDefault();
|
|
2276
|
+
setOpenDropdown(openDropdown === 'mode' ? null : 'mode');
|
|
2277
|
+
}
|
|
2278
|
+
},
|
|
2279
|
+
},
|
|
2280
|
+
React.createElement(
|
|
2281
|
+
'span',
|
|
2282
|
+
null,
|
|
2283
|
+
(selectedItem.mode || 'formula') === 'source' ? t('Source') : t('Formula')
|
|
2284
|
+
),
|
|
2285
|
+
React.createElement('span', { style: { opacity: 0.75 } }, '▾')
|
|
2286
|
+
),
|
|
2287
|
+
openDropdown === 'mode'
|
|
2288
|
+
? React.createElement(
|
|
2289
|
+
'div',
|
|
2290
|
+
{ style: dropdownMenuStyle },
|
|
2291
|
+
React.createElement(
|
|
2292
|
+
'div',
|
|
2293
|
+
{
|
|
2294
|
+
style: dropdownItemStyle((selectedItem.mode || 'formula') === 'formula'),
|
|
2295
|
+
onClick: () => {
|
|
2296
|
+
updateSelected('mode', 'formula');
|
|
2297
|
+
setOpenDropdown(null);
|
|
2298
|
+
},
|
|
2299
|
+
},
|
|
2300
|
+
t('Formula')
|
|
2301
|
+
),
|
|
2302
|
+
React.createElement(
|
|
2303
|
+
'div',
|
|
2304
|
+
{
|
|
2305
|
+
style: dropdownItemStyle((selectedItem.mode || 'formula') === 'source'),
|
|
2306
|
+
onClick: () => {
|
|
2307
|
+
updateSelected('mode', 'source');
|
|
2308
|
+
setOpenDropdown(null);
|
|
2309
|
+
},
|
|
2310
|
+
},
|
|
2311
|
+
t('Source')
|
|
2312
|
+
)
|
|
2313
|
+
)
|
|
2314
|
+
: null
|
|
2315
|
+
),
|
|
2316
|
+
(selectedItem.mode || 'formula') === 'source'
|
|
2317
|
+
? React.createElement(
|
|
2318
|
+
React.Fragment,
|
|
2319
|
+
null,
|
|
2320
|
+
React.createElement('label', { style: labelStyle }, t('ioBroker Source State')),
|
|
2321
|
+
React.createElement(
|
|
2322
|
+
'div',
|
|
2323
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
2324
|
+
React.createElement('input', {
|
|
2325
|
+
style: Object.assign({}, inputStyle, { flex: 1 }),
|
|
2326
|
+
type: 'text',
|
|
2327
|
+
value: selectedItem.sourceState || '',
|
|
2328
|
+
onChange: e => updateSelected('sourceState', e.target.value),
|
|
2329
|
+
placeholder: t('e.g. some.adapter.0.channel.state'),
|
|
2330
|
+
}),
|
|
2331
|
+
renderSelectButton(() => setSelectContext({ kind: 'itemSource' }))
|
|
2332
|
+
),
|
|
2333
|
+
React.createElement('label', { style: labelStyle }, t('JSONPath (optional)')),
|
|
2334
|
+
React.createElement('input', {
|
|
2335
|
+
style: inputStyle,
|
|
2336
|
+
type: 'text',
|
|
2337
|
+
value: selectedItem.jsonPath || '',
|
|
2338
|
+
onChange: e => updateSelected('jsonPath', e.target.value),
|
|
2339
|
+
placeholder: t('e.g. $.apower'),
|
|
2340
|
+
})
|
|
2341
|
+
)
|
|
2342
|
+
: React.createElement(
|
|
2343
|
+
React.Fragment,
|
|
2344
|
+
null,
|
|
2345
|
+
React.createElement(
|
|
2346
|
+
'div',
|
|
2347
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 10 } },
|
|
2348
|
+
React.createElement('div', { style: labelStyle }, t('Inputs')),
|
|
2349
|
+
React.createElement(
|
|
2350
|
+
'button',
|
|
2351
|
+
{ type: 'button', style: btnStyle, onClick: addInput },
|
|
2352
|
+
t('Add input')
|
|
2353
|
+
)
|
|
2354
|
+
),
|
|
2355
|
+
(Array.isArray(selectedItem.inputs) ? selectedItem.inputs : []).map((inp, idx) =>
|
|
2356
|
+
React.createElement(
|
|
2357
|
+
'div',
|
|
2358
|
+
{
|
|
2359
|
+
key: idx,
|
|
2360
|
+
style: {
|
|
2361
|
+
display: 'grid',
|
|
2362
|
+
gridTemplateColumns: '140px 1fr 160px 90px 90px',
|
|
2363
|
+
gap: 8,
|
|
2364
|
+
alignItems: 'center',
|
|
2365
|
+
marginTop: 8,
|
|
2366
|
+
},
|
|
2367
|
+
},
|
|
2368
|
+
React.createElement('input', {
|
|
2369
|
+
style: inputStyle,
|
|
2370
|
+
type: 'text',
|
|
2371
|
+
value: (inp && inp.key) || '',
|
|
2372
|
+
placeholder: t('Key'),
|
|
2373
|
+
onChange: e => updateInput(idx, 'key', e.target.value),
|
|
2374
|
+
}),
|
|
2375
|
+
React.createElement('input', {
|
|
2376
|
+
style: inputStyle,
|
|
2377
|
+
type: 'text',
|
|
2378
|
+
value: (inp && inp.sourceState) || '',
|
|
2379
|
+
placeholder: t('ioBroker Source State'),
|
|
2380
|
+
onChange: e => updateInput(idx, 'sourceState', e.target.value),
|
|
2381
|
+
}),
|
|
2382
|
+
React.createElement('input', {
|
|
2383
|
+
style: inputStyle,
|
|
2384
|
+
type: 'text',
|
|
2385
|
+
value: (inp && inp.jsonPath) || '',
|
|
2386
|
+
placeholder: t('JSONPath (optional)'),
|
|
2387
|
+
onChange: e => updateInput(idx, 'jsonPath', e.target.value),
|
|
2388
|
+
title: t('e.g. $.apower'),
|
|
2389
|
+
}),
|
|
2390
|
+
React.createElement(
|
|
2391
|
+
'div',
|
|
2392
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'stretch' } },
|
|
2393
|
+
React.createElement(
|
|
2394
|
+
'label',
|
|
2395
|
+
{
|
|
2396
|
+
style: {
|
|
2397
|
+
display: 'flex',
|
|
2398
|
+
alignItems: 'center',
|
|
2399
|
+
gap: 6,
|
|
2400
|
+
fontSize: 11,
|
|
2401
|
+
color: colors.textMuted,
|
|
2402
|
+
cursor: 'pointer',
|
|
2403
|
+
},
|
|
2404
|
+
title: t('Clamp input negative to 0'),
|
|
2405
|
+
},
|
|
2406
|
+
React.createElement('input', {
|
|
2407
|
+
type: 'checkbox',
|
|
2408
|
+
checked: !!(inp && inp.noNegative),
|
|
2409
|
+
onChange: e => updateInput(idx, 'noNegative', !!e.target.checked),
|
|
2410
|
+
}),
|
|
2411
|
+
React.createElement('span', null, 'neg→0')
|
|
2412
|
+
),
|
|
2413
|
+
renderSelectButton(() => setSelectContext({ kind: 'input', index: idx }))
|
|
2414
|
+
),
|
|
2415
|
+
React.createElement(
|
|
2416
|
+
'button',
|
|
2417
|
+
{ type: 'button', style: btnDangerStyle, onClick: () => deleteInput(idx) },
|
|
2418
|
+
t('Delete')
|
|
2419
|
+
)
|
|
2420
|
+
)
|
|
2421
|
+
),
|
|
2422
|
+
React.createElement(
|
|
2423
|
+
'div',
|
|
2424
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, marginTop: 10 } },
|
|
2425
|
+
React.createElement('label', { style: Object.assign({}, labelStyle, { marginTop: 0 }) }, t('Formula expression')),
|
|
2426
|
+
React.createElement(
|
|
2427
|
+
'button',
|
|
2428
|
+
{ type: 'button', style: Object.assign({}, btnStyle, { padding: '6px 10px' }), onClick: openFormulaBuilder },
|
|
2429
|
+
t('Builder…')
|
|
2430
|
+
)
|
|
2431
|
+
),
|
|
2432
|
+
React.createElement('textarea', {
|
|
2433
|
+
style: Object.assign({}, inputStyle, { minHeight: 80 }),
|
|
2434
|
+
value: selectedItem.formula || '',
|
|
2435
|
+
onChange: e => updateSelected('formula', e.target.value),
|
|
2436
|
+
placeholder: t('e.g. pv1 + pv2 + pv3'),
|
|
2437
|
+
})
|
|
2438
|
+
),
|
|
2439
|
+
React.createElement(
|
|
2440
|
+
'div',
|
|
2441
|
+
{ style: rowStyle2 },
|
|
2442
|
+
React.createElement(
|
|
2443
|
+
'div',
|
|
2444
|
+
null,
|
|
2445
|
+
React.createElement('label', { style: labelStyle }, t('Datatype')),
|
|
2446
|
+
React.createElement(
|
|
2447
|
+
'div',
|
|
2448
|
+
{ style: { position: 'relative' }, 'data-ds-dropdown': '1' },
|
|
2449
|
+
React.createElement(
|
|
2450
|
+
'div',
|
|
2451
|
+
{
|
|
2452
|
+
style: dropdownButtonStyle,
|
|
2453
|
+
role: 'button',
|
|
2454
|
+
tabIndex: 0,
|
|
2455
|
+
onClick: () => setOpenDropdown(openDropdown === 'datatype' ? null : 'datatype'),
|
|
2456
|
+
onKeyDown: e => {
|
|
2457
|
+
if (e && (e.key === 'Enter' || e.key === ' ')) {
|
|
2458
|
+
e.preventDefault();
|
|
2459
|
+
setOpenDropdown(openDropdown === 'datatype' ? null : 'datatype');
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
},
|
|
2463
|
+
React.createElement(
|
|
2464
|
+
'span',
|
|
2465
|
+
null,
|
|
2466
|
+
selectedItem.type === 'number'
|
|
2467
|
+
? t('Number')
|
|
2468
|
+
: selectedItem.type === 'boolean'
|
|
2469
|
+
? t('Boolean')
|
|
2470
|
+
: selectedItem.type === 'string'
|
|
2471
|
+
? t('String')
|
|
2472
|
+
: selectedItem.type === 'mixed'
|
|
2473
|
+
? t('Mixed')
|
|
2474
|
+
: t('Standard')
|
|
2475
|
+
),
|
|
2476
|
+
React.createElement('span', { style: { opacity: 0.75 } }, '▾')
|
|
2477
|
+
),
|
|
2478
|
+
openDropdown === 'datatype'
|
|
2479
|
+
? React.createElement(
|
|
2480
|
+
'div',
|
|
2481
|
+
{ style: dropdownMenuStyle },
|
|
2482
|
+
React.createElement(
|
|
2483
|
+
'div',
|
|
2484
|
+
{
|
|
2485
|
+
style: dropdownItemStyle(!selectedItem.type),
|
|
2486
|
+
onClick: () => {
|
|
2487
|
+
updateSelected('type', '');
|
|
2488
|
+
setOpenDropdown(null);
|
|
2489
|
+
},
|
|
2490
|
+
},
|
|
2491
|
+
t('Standard')
|
|
2492
|
+
),
|
|
2493
|
+
React.createElement(
|
|
2494
|
+
'div',
|
|
2495
|
+
{
|
|
2496
|
+
style: dropdownItemStyle(selectedItem.type === 'number'),
|
|
2497
|
+
onClick: () => {
|
|
2498
|
+
updateSelected('type', 'number');
|
|
2499
|
+
setOpenDropdown(null);
|
|
2500
|
+
},
|
|
2501
|
+
},
|
|
2502
|
+
t('Number')
|
|
2503
|
+
),
|
|
2504
|
+
React.createElement(
|
|
2505
|
+
'div',
|
|
2506
|
+
{
|
|
2507
|
+
style: dropdownItemStyle(selectedItem.type === 'boolean'),
|
|
2508
|
+
onClick: () => {
|
|
2509
|
+
updateSelected('type', 'boolean');
|
|
2510
|
+
setOpenDropdown(null);
|
|
2511
|
+
},
|
|
2512
|
+
},
|
|
2513
|
+
t('Boolean')
|
|
2514
|
+
),
|
|
2515
|
+
React.createElement(
|
|
2516
|
+
'div',
|
|
2517
|
+
{
|
|
2518
|
+
style: dropdownItemStyle(selectedItem.type === 'string'),
|
|
2519
|
+
onClick: () => {
|
|
2520
|
+
updateSelected('type', 'string');
|
|
2521
|
+
setOpenDropdown(null);
|
|
2522
|
+
},
|
|
2523
|
+
},
|
|
2524
|
+
t('String')
|
|
2525
|
+
),
|
|
2526
|
+
React.createElement(
|
|
2527
|
+
'div',
|
|
2528
|
+
{
|
|
2529
|
+
style: dropdownItemStyle(selectedItem.type === 'mixed'),
|
|
2530
|
+
onClick: () => {
|
|
2531
|
+
updateSelected('type', 'mixed');
|
|
2532
|
+
setOpenDropdown(null);
|
|
2533
|
+
},
|
|
2534
|
+
},
|
|
2535
|
+
t('Mixed')
|
|
2536
|
+
)
|
|
2537
|
+
)
|
|
2538
|
+
: null
|
|
2539
|
+
)
|
|
2540
|
+
),
|
|
2541
|
+
React.createElement(
|
|
2542
|
+
'div',
|
|
2543
|
+
null,
|
|
2544
|
+
React.createElement('label', { style: labelStyle }, t('Role')),
|
|
2545
|
+
React.createElement('input', {
|
|
2546
|
+
style: inputStyle,
|
|
2547
|
+
type: 'text',
|
|
2548
|
+
value: selectedItem.role || '',
|
|
2549
|
+
onChange: e => updateSelected('role', e.target.value),
|
|
2550
|
+
placeholder: 'value.power',
|
|
2551
|
+
})
|
|
2552
|
+
)
|
|
2553
|
+
),
|
|
2554
|
+
React.createElement(
|
|
2555
|
+
'div',
|
|
2556
|
+
{ style: rowStyle2 },
|
|
2557
|
+
React.createElement(
|
|
2558
|
+
'div',
|
|
2559
|
+
null,
|
|
2560
|
+
React.createElement('label', { style: labelStyle }, t('Unit')),
|
|
2561
|
+
React.createElement('input', {
|
|
2562
|
+
style: inputStyle,
|
|
2563
|
+
type: 'text',
|
|
2564
|
+
value: selectedItem.unit || '',
|
|
2565
|
+
onChange: e => updateSelected('unit', e.target.value),
|
|
2566
|
+
placeholder: 'W',
|
|
2567
|
+
})
|
|
2568
|
+
),
|
|
2569
|
+
React.createElement(
|
|
2570
|
+
'div',
|
|
2571
|
+
null,
|
|
2572
|
+
React.createElement(
|
|
2573
|
+
'label',
|
|
2574
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10 } },
|
|
2575
|
+
React.createElement('input', {
|
|
2576
|
+
type: 'checkbox',
|
|
2577
|
+
checked: !!selectedItem.clamp,
|
|
2578
|
+
onChange: e => updateSelected('clamp', !!e.target.checked),
|
|
2579
|
+
}),
|
|
2580
|
+
React.createElement('span', null, t('Clamp result'))
|
|
2581
|
+
)
|
|
2582
|
+
)
|
|
2583
|
+
),
|
|
2584
|
+
React.createElement(
|
|
2585
|
+
'label',
|
|
2586
|
+
{
|
|
2587
|
+
style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10 },
|
|
2588
|
+
title: t('Clamp negative to 0 (tooltip)')
|
|
2589
|
+
},
|
|
2590
|
+
React.createElement('input', {
|
|
2591
|
+
type: 'checkbox',
|
|
2592
|
+
checked: !!selectedItem.noNegative,
|
|
2593
|
+
onChange: e => updateSelected('noNegative', !!e.target.checked),
|
|
2594
|
+
}),
|
|
2595
|
+
React.createElement('span', null, t('Clamp negative to 0'))
|
|
2596
|
+
),
|
|
2597
|
+
React.createElement(
|
|
2598
|
+
'div',
|
|
2599
|
+
{ style: { marginLeft: 26, marginTop: 4, fontSize: 12, color: colors.textMuted } },
|
|
2600
|
+
t(selectedItem && selectedItem.mode === 'formula'
|
|
2601
|
+
? 'Clamp negative to 0 (hint formula)'
|
|
2602
|
+
: 'Clamp negative to 0 (hint source)')
|
|
2603
|
+
),
|
|
2604
|
+
selectedItem.clamp
|
|
2605
|
+
? React.createElement(
|
|
2606
|
+
'div',
|
|
2607
|
+
{ style: rowStyle2 },
|
|
2608
|
+
React.createElement(
|
|
2609
|
+
'div',
|
|
2610
|
+
null,
|
|
2611
|
+
React.createElement('label', { style: labelStyle }, t('Min')),
|
|
2612
|
+
React.createElement('input', {
|
|
2613
|
+
style: inputStyle,
|
|
2614
|
+
type: 'number',
|
|
2615
|
+
value: selectedItem.min || '',
|
|
2616
|
+
onChange: e => updateSelected('min', e.target.value),
|
|
2617
|
+
})
|
|
2618
|
+
),
|
|
2619
|
+
React.createElement(
|
|
2620
|
+
'div',
|
|
2621
|
+
null,
|
|
2622
|
+
React.createElement('label', { style: labelStyle }, t('Max')),
|
|
2623
|
+
React.createElement('input', {
|
|
2624
|
+
style: inputStyle,
|
|
2625
|
+
type: 'number',
|
|
2626
|
+
value: selectedItem.max || '',
|
|
2627
|
+
onChange: e => updateSelected('max', e.target.value),
|
|
2628
|
+
})
|
|
2629
|
+
)
|
|
2630
|
+
)
|
|
2631
|
+
: null,
|
|
2632
|
+
renderFormulaBuilderModal(),
|
|
2633
|
+
renderStatePicker()
|
|
2634
|
+
)
|
|
2635
|
+
: React.createElement(
|
|
2636
|
+
'div',
|
|
2637
|
+
{ style: { opacity: 0.9, color: colors.textMuted } },
|
|
2638
|
+
t('Select an item on the left or add a new one.')
|
|
2639
|
+
)
|
|
2640
|
+
)
|
|
2641
|
+
);
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
const moduleMap = {
|
|
2646
|
+
'./Components': async function () {
|
|
2647
|
+
const React = globalThis.React || (await loadShared('react'));
|
|
2648
|
+
const AdapterReact = await loadShared('@iobroker/adapter-react-v5');
|
|
2649
|
+
if (!React) {
|
|
2650
|
+
throw new Error('DataSolectrusItems custom UI: React not available.');
|
|
2651
|
+
}
|
|
2652
|
+
const DataSolectrusItemsEditor = createDataSolectrusItemsEditor(React, AdapterReact);
|
|
2653
|
+
|
|
2654
|
+
// Legacy global registry (best-effort)
|
|
2655
|
+
try {
|
|
2656
|
+
globalThis.customComponents = globalThis.customComponents || {};
|
|
2657
|
+
globalThis.customComponents.DataSolectrusItemsEditor = DataSolectrusItemsEditor;
|
|
2658
|
+
} catch {
|
|
2659
|
+
// ignore
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
return {
|
|
2663
|
+
default: {
|
|
2664
|
+
DataSolectrusItemsEditor,
|
|
2665
|
+
},
|
|
2666
|
+
};
|
|
2667
|
+
},
|
|
2668
|
+
'Components': async function () {
|
|
2669
|
+
return moduleMap['./Components']();
|
|
2670
|
+
},
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
function get(module) {
|
|
2674
|
+
const factoryFn = moduleMap[module];
|
|
2675
|
+
if (!factoryFn) {
|
|
2676
|
+
return Promise.reject(new Error(`Module ${module} not found in ${REMOTE_NAME}`));
|
|
2677
|
+
}
|
|
2678
|
+
return Promise.resolve()
|
|
2679
|
+
.then(() => factoryFn())
|
|
2680
|
+
.then(mod => () => mod);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function init(scope) {
|
|
2684
|
+
shareScope = scope;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
globalThis[REMOTE_NAME] = {
|
|
2688
|
+
get,
|
|
2689
|
+
init,
|
|
2690
|
+
};
|
|
2691
|
+
})();
|