@aglyn/plugins-logic 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +45 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/host-functions-card.component.d.ts +19 -0
- package/src/lib/components/host-functions-card.component.js +1075 -0
- package/src/lib/components/host-functions-card.component.js.map +1 -0
- package/src/lib/components/host-reference-health-card.component.d.ts +15 -0
- package/src/lib/components/host-reference-health-card.component.js +279 -0
- package/src/lib/components/host-reference-health-card.component.js.map +1 -0
- package/src/lib/components/host-variables-card.component.d.ts +18 -0
- package/src/lib/components/host-variables-card.component.js +630 -0
- package/src/lib/components/host-variables-card.component.js.map +1 -0
- package/src/lib/components/logic-console-page.d.ts +12 -0
- package/src/lib/components/logic-console-page.js +68 -0
- package/src/lib/components/logic-console-page.js.map +1 -0
- package/src/lib/components/where-used-dialog.component.d.ts +20 -0
- package/src/lib/components/where-used-dialog.component.js +103 -0
- package/src/lib/components/where-used-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/index.d.ts +23 -0
- package/src/lib/model/index.js +23 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/parameter-options.d.ts +26 -0
- package/src/lib/model/parameter-options.js +29 -0
- package/src/lib/model/parameter-options.js.map +1 -0
- package/src/lib/model/reference-audit.d.ts +75 -0
- package/src/lib/model/reference-audit.js +155 -0
- package/src/lib/model/reference-audit.js.map +1 -0
- package/src/lib/plugin.d.ts +29 -0
- package/src/lib/plugin.js +82 -0
- package/src/lib/plugin.js.map +1 -0
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
19
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
20
|
+
import { checkQuota, evaluateHostFunction, FUNCTION_BUILTIN_NAMES, functionGlobals, functionReferencedNames, pluginDocsHelp, VARIABLE_NAME_PATTERN } from "@aglyn/aglyn";
|
|
21
|
+
/*
|
|
22
|
+
* The MODULE, not the barrel, for the two PURE helpers — every spec that
|
|
23
|
+
* renders this card mocks `@aglyn/tenant-feature-instance` wholesale to stage
|
|
24
|
+
* its Firestore hooks, and a query builder imported through that barrel
|
|
25
|
+
* disappears under the mock. Neither of these is a hook.
|
|
26
|
+
*/ import { ceilingedWindow, collectionCeiling } from "@aglyn/tenant-feature-instance/hooks/host-collection-queries";
|
|
27
|
+
import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
28
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
29
|
+
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
30
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
31
|
+
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
32
|
+
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider, IconButton, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
|
33
|
+
import { collection, doc, getCountFromServer, getDocs, limit, query, setDoc, updateDoc } from "firebase/firestore";
|
|
34
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
35
|
+
import { useFirestore, useFirestoreCollection, useHostResourceApi, useUser, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
36
|
+
import { formatParameterOptions, parseParameterOptions } from "../model/parameter-options.js";
|
|
37
|
+
import WhereUsedDialog from "./where-used-dialog.component.js";
|
|
38
|
+
import { fetchWhereUsed, summarizeDependents } from "@aglyn/aglyn/app-utils/where-used";
|
|
39
|
+
/**
|
|
40
|
+
* How many functions the card reads.
|
|
41
|
+
*
|
|
42
|
+
* A CEILING, not a page size — see the query, which explains why this list
|
|
43
|
+
* cannot be paged by the server without making the duplicate-name check lie.
|
|
44
|
+
*/ const FUNCTION_CEILING = 100;
|
|
45
|
+
const VALUE_TYPES = [
|
|
46
|
+
{
|
|
47
|
+
value: 'number',
|
|
48
|
+
label: 'Number'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: 'text',
|
|
52
|
+
label: 'Text'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: 'boolean',
|
|
56
|
+
label: 'True/false'
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
const COMPARATORS = [
|
|
60
|
+
'==',
|
|
61
|
+
'!=',
|
|
62
|
+
'<',
|
|
63
|
+
'<=',
|
|
64
|
+
'>',
|
|
65
|
+
'>='
|
|
66
|
+
];
|
|
67
|
+
function emptyDraft() {
|
|
68
|
+
return {
|
|
69
|
+
id: null,
|
|
70
|
+
name: '',
|
|
71
|
+
parameters: [
|
|
72
|
+
{
|
|
73
|
+
name: 'P1',
|
|
74
|
+
type: 'number',
|
|
75
|
+
required: true
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
variables: [
|
|
79
|
+
{
|
|
80
|
+
name: 'P3',
|
|
81
|
+
type: 'number'
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
operations: [
|
|
85
|
+
{
|
|
86
|
+
if: {
|
|
87
|
+
left: '',
|
|
88
|
+
comparator: '<=',
|
|
89
|
+
right: ''
|
|
90
|
+
},
|
|
91
|
+
then: [
|
|
92
|
+
{
|
|
93
|
+
set: '',
|
|
94
|
+
expression: ''
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
otherwise: []
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
returnValue: ''
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* What a VISITOR meets for one parameter (AGL-3202): the label above the
|
|
105
|
+
* input, what it starts with, and — for a question with fixed answers — the
|
|
106
|
+
* choices. Empty fields are dropped from the definition rather than stored
|
|
107
|
+
* as empty strings, so a function nobody has dressed for visitors saves
|
|
108
|
+
* exactly the shape it always did.
|
|
109
|
+
*
|
|
110
|
+
* The choices keep their own text while being typed. Parsing every keystroke
|
|
111
|
+
* back into the box would eat the space after a comma, and the colon after a
|
|
112
|
+
* value, the moment they were typed.
|
|
113
|
+
*/ function ParameterAudienceFields(props) {
|
|
114
|
+
var _parameter_label, _parameter_defaultValue;
|
|
115
|
+
const { parameter, onChange } = props;
|
|
116
|
+
const [choices, setChoices] = useState(()=>formatParameterOptions(parameter.options));
|
|
117
|
+
const assign = (field, value)=>{
|
|
118
|
+
const next = _extends({}, parameter);
|
|
119
|
+
if (value) next[field] = value;
|
|
120
|
+
else delete next[field];
|
|
121
|
+
onChange(next);
|
|
122
|
+
};
|
|
123
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
124
|
+
direction: "row",
|
|
125
|
+
spacing: 1,
|
|
126
|
+
sx: {
|
|
127
|
+
alignItems: 'center'
|
|
128
|
+
},
|
|
129
|
+
children: [
|
|
130
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
131
|
+
variant: "body2",
|
|
132
|
+
color: "text.secondary",
|
|
133
|
+
sx: {
|
|
134
|
+
width: 110,
|
|
135
|
+
flexShrink: 0
|
|
136
|
+
},
|
|
137
|
+
noWrap: true,
|
|
138
|
+
children: parameter.name || '—'
|
|
139
|
+
}),
|
|
140
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
141
|
+
label: "Label",
|
|
142
|
+
placeholder: parameter.name,
|
|
143
|
+
value: (_parameter_label = parameter.label) != null ? _parameter_label : '',
|
|
144
|
+
onChange: (event)=>assign('label', event.target.value.slice(0, 80)),
|
|
145
|
+
size: "small",
|
|
146
|
+
sx: {
|
|
147
|
+
flex: 2
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
151
|
+
label: "Starts as",
|
|
152
|
+
value: (_parameter_defaultValue = parameter.defaultValue) != null ? _parameter_defaultValue : '',
|
|
153
|
+
onChange: (event)=>assign('defaultValue', event.target.value.slice(0, 120)),
|
|
154
|
+
size: "small",
|
|
155
|
+
sx: {
|
|
156
|
+
flex: 1
|
|
157
|
+
}
|
|
158
|
+
}),
|
|
159
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
160
|
+
label: "Choices",
|
|
161
|
+
placeholder: "value: Label, value: Label",
|
|
162
|
+
value: choices,
|
|
163
|
+
onChange: (event)=>{
|
|
164
|
+
setChoices(event.target.value);
|
|
165
|
+
const options = parseParameterOptions(event.target.value);
|
|
166
|
+
const next = _extends({}, parameter);
|
|
167
|
+
if (options.length) next.options = options;
|
|
168
|
+
else delete next.options;
|
|
169
|
+
onChange(next);
|
|
170
|
+
},
|
|
171
|
+
size: "small",
|
|
172
|
+
sx: {
|
|
173
|
+
flex: 3
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Small labelled row header matching the mockup's section styling. */ function SectionLabel(props) {
|
|
180
|
+
return /*#__PURE__*/ _jsx(Typography, {
|
|
181
|
+
variant: "overline",
|
|
182
|
+
color: "text.secondary",
|
|
183
|
+
children: props.children
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* No-code function builder (Component Builder, AGL-92): the mockup's Edit
|
|
188
|
+
* Function dialog — Parameters with required toggles, local Variables,
|
|
189
|
+
* numbered conditional operation cards (If below is TRUE / Then do this /
|
|
190
|
+
* Otherwise do this), a Return value select, and a test-run panel driving
|
|
191
|
+
* the shared evaluator. Definitions persist to `hosts/{hostId}/functions`;
|
|
192
|
+
* prop/event wiring lands with AGL-93.
|
|
193
|
+
*/ export function HostFunctionsCard(props) {
|
|
194
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
|
|
195
|
+
const { hostId } = props;
|
|
196
|
+
const firestore = useFirestore();
|
|
197
|
+
const { data: user } = useUser();
|
|
198
|
+
const createHostResource = useHostResourceApi();
|
|
199
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
200
|
+
const { confirm } = useConfirmationContext();
|
|
201
|
+
const { org } = props;
|
|
202
|
+
const { data: functionDocs, status: functionsStatus, /**
|
|
203
|
+
* The rows this editor is seeded from are unconfirmed by the server
|
|
204
|
+
* (AGL-1358). Editing destructures a whole stored function into `draft`
|
|
205
|
+
* and writes `{...definition}` back, so `merge: true` protects nothing —
|
|
206
|
+
* the payload is literally every field of the definition. `parameters`
|
|
207
|
+
* and `operations` are arrays, which a merge replaces wholesale rather
|
|
208
|
+
* than diffing, so a cached seed does not lose one edit: it restores that
|
|
209
|
+
* snapshot's entire logic body and parameter list, and every caller's
|
|
210
|
+
* arguments are positional against the list it just reverted.
|
|
211
|
+
*/ fromCache: functionsFromCache } = useFirestoreCollection(/*
|
|
212
|
+
* ORDERED AND CEILINGED, deliberately not paged by the query (AGL-2501) —
|
|
213
|
+
* the same decision as the variables card beside it, for the same two
|
|
214
|
+
* reasons.
|
|
215
|
+
*
|
|
216
|
+
* `limit(100)` alone is answered in DOCUMENT-ID order, so the window is a
|
|
217
|
+
* pseudo-random hundred that the `localeCompare` below dresses up as an
|
|
218
|
+
* alphabetical list, and nothing said the list was bounded.
|
|
219
|
+
*
|
|
220
|
+
* `collectionCeiling` does not change WHICH hundred — document-id order is
|
|
221
|
+
* what the bare cap already returned. What it changes is that the order is
|
|
222
|
+
* NAMED, so the obvious next edit is caught: ordering on `name` would HIDE
|
|
223
|
+
* every function written without one rather than mis-sorting the list, and
|
|
224
|
+
* `/api/hosts/resources` validates no field for presence while
|
|
225
|
+
* `IMPORTABLE_FIELDS.functions` copies one only if the export carried it.
|
|
226
|
+
*
|
|
227
|
+
* The QUERY is not paged because `nameTaken` below tests the draft against
|
|
228
|
+
* these rows: case-insensitive uniqueness is what keeps legacy
|
|
229
|
+
* `{{fn:name(...)}}` resolution unambiguous (AGL-185), and on a ten-row
|
|
230
|
+
* server page that check would compare a new function against a tenth of
|
|
231
|
+
* the site and create the duplicate it exists to prevent.
|
|
232
|
+
*/ ()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'functions'), FUNCTION_CEILING), [
|
|
233
|
+
firestore,
|
|
234
|
+
hostId
|
|
235
|
+
], {
|
|
236
|
+
idField: '$id'
|
|
237
|
+
});
|
|
238
|
+
const { rows: readFunctions, truncated: functionsTruncated } = ceilingedWindow(functionDocs, FUNCTION_CEILING);
|
|
239
|
+
// Sorting is safe here in a way it is not on a paged list: these rows are
|
|
240
|
+
// the whole collection below the ceiling, not a slice of one.
|
|
241
|
+
const functions = useMemo(()=>readFunctions.filter((definition)=>!definition.deletedAt).sort((a, b)=>{
|
|
242
|
+
var _a_name, _b_name;
|
|
243
|
+
return String((_a_name = a.name) != null ? _a_name : '').localeCompare(String((_b_name = b.name) != null ? _b_name : ''));
|
|
244
|
+
}), [
|
|
245
|
+
readFunctions
|
|
246
|
+
]);
|
|
247
|
+
// The page is a SLICE: the rows are already in hand and `nameTaken` needs
|
|
248
|
+
// all of them.
|
|
249
|
+
const [page, setPage] = useState(0);
|
|
250
|
+
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
251
|
+
const visibleFunctions = useMemo(()=>functions.slice(page * pageSize, page * pageSize + pageSize), [
|
|
252
|
+
functions,
|
|
253
|
+
page,
|
|
254
|
+
pageSize
|
|
255
|
+
]);
|
|
256
|
+
/**
|
|
257
|
+
* The function HEAD-COUNT is a server aggregate, not the length of the
|
|
258
|
+
* capped listener (AGL-1716, the AGL-1706 shape) — the same fix as the
|
|
259
|
+
* variables card beside it, for the same reason.
|
|
260
|
+
*
|
|
261
|
+
* The listener is `limit(100)` and `functions.length` fed
|
|
262
|
+
* `checkQuota(org, 'functionsPerHost', …)`, whose bands are 1 / 10 / 50 /
|
|
263
|
+
* 250 / 500 / 1,000. Everything from Business up sits above the window, so
|
|
264
|
+
* on those plans the gate compared 100 against hundreds and could never
|
|
265
|
+
* refuse, while `api/hosts/resources` counted the collection and did.
|
|
266
|
+
*
|
|
267
|
+
* UNFILTERED, matching that route's plain `collection('functions').count()`
|
|
268
|
+
* — soft-deleted functions have always counted server-side, so this is the
|
|
269
|
+
* card catching up with the verdict it exists to predict. Only a create
|
|
270
|
+
* moves the number, so that is where it is re-read.
|
|
271
|
+
*/ const [functionCountEpoch, setFunctionCountEpoch] = useState(0);
|
|
272
|
+
const [serverFunctionCount, setServerFunctionCount] = useState(null);
|
|
273
|
+
useEffect(()=>{
|
|
274
|
+
let active = true;
|
|
275
|
+
void getCountFromServer(collection(firestore, 'hosts', hostId, 'functions')).then((snapshot)=>{
|
|
276
|
+
if (active) setServerFunctionCount(snapshot.data().count);
|
|
277
|
+
}).catch(()=>{
|
|
278
|
+
// Falls back to the loaded rows — a LOWER bound and the prior
|
|
279
|
+
// behaviour, never 0, which `checkQuota` would answer as "no usage"
|
|
280
|
+
// on a site that is over its band.
|
|
281
|
+
});
|
|
282
|
+
return ()=>{
|
|
283
|
+
active = false;
|
|
284
|
+
};
|
|
285
|
+
}, [
|
|
286
|
+
firestore,
|
|
287
|
+
hostId,
|
|
288
|
+
functionCountEpoch
|
|
289
|
+
]);
|
|
290
|
+
const functionCount = serverFunctionCount != null ? serverFunctionCount : functions.length;
|
|
291
|
+
const [draft, setDraft] = useState(null);
|
|
292
|
+
// Where-used dialog (AGL-193).
|
|
293
|
+
const [usage, setUsage] = useState(null);
|
|
294
|
+
const [usageLoading, setUsageLoading] = useState(null);
|
|
295
|
+
const [testArgs, setTestArgs] = useState({});
|
|
296
|
+
const [testResult, setTestResult] = useState(null);
|
|
297
|
+
const patch = useCallback((updater)=>setDraft((previous)=>previous ? updater(previous) : previous), []);
|
|
298
|
+
const names = [
|
|
299
|
+
...((_ref = draft == null ? void 0 : draft.parameters) != null ? _ref : []).map((parameter)=>parameter.name),
|
|
300
|
+
...((_ref1 = draft == null ? void 0 : draft.variables) != null ? _ref1 : []).map((variable)=>variable.name)
|
|
301
|
+
].filter((name)=>VARIABLE_NAME_PATTERN.test(name));
|
|
302
|
+
// Case-insensitive uniqueness (AGL-185): function names must stay
|
|
303
|
+
// unambiguous for legacy {{fn:name(...)}} token resolution.
|
|
304
|
+
const nameTaken = Boolean(draft && functions.some((definition)=>{
|
|
305
|
+
var _definition_name;
|
|
306
|
+
return String((_definition_name = definition.name) != null ? _definition_name : '').toLowerCase() === draft.name.trim().toLowerCase() && definition.$id !== draft.id;
|
|
307
|
+
}));
|
|
308
|
+
/**
|
|
309
|
+
* A test run sees what a published page sees (AGL-3202): the site
|
|
310
|
+
* variables the draft names. Read on the click, and only when the draft
|
|
311
|
+
* names something outside itself, so opening this card still costs the one
|
|
312
|
+
* functions window and nothing else — a listener here would bill a hundred
|
|
313
|
+
* variable reads to every author who never presses Run.
|
|
314
|
+
*/ const handleTestRun = useCallback(async ()=>{
|
|
315
|
+
if (!draft) return;
|
|
316
|
+
let globals = {};
|
|
317
|
+
if (functionReferencedNames(draft).length) {
|
|
318
|
+
try {
|
|
319
|
+
const snapshot = await getDocs(query(collection(firestore, 'hosts', hostId, 'variables'), limit(100)));
|
|
320
|
+
const lookup = {};
|
|
321
|
+
snapshot.forEach((row)=>{
|
|
322
|
+
const data = row.data();
|
|
323
|
+
if ((data == null ? void 0 : data.name) && !data.deletedAt) lookup[row.id] = data;
|
|
324
|
+
});
|
|
325
|
+
globals = functionGlobals(draft, lookup);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
// The run still happens: an unreadable variable list reads as the
|
|
328
|
+
// evaluator's own "Unknown name", which names what was missing.
|
|
329
|
+
console.error(error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const result = evaluateHostFunction(draft, testArgs, {
|
|
333
|
+
globals
|
|
334
|
+
});
|
|
335
|
+
setTestResult(result.ok === false ? `Error: ${result.error}` : `Result: ${String(result.value)}`);
|
|
336
|
+
}, [
|
|
337
|
+
draft,
|
|
338
|
+
testArgs,
|
|
339
|
+
firestore,
|
|
340
|
+
hostId
|
|
341
|
+
]);
|
|
342
|
+
const handleSave = useCallback(async ()=>{
|
|
343
|
+
if (!draft || !draft.name.trim() || nameTaken) return;
|
|
344
|
+
const { id: draftId } = draft, definition = _object_without_properties_loose(draft, [
|
|
345
|
+
"id"
|
|
346
|
+
]);
|
|
347
|
+
const fields = _extends({}, definition, {
|
|
348
|
+
name: draft.name.trim().slice(0, 60)
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
if (draftId) {
|
|
352
|
+
/**
|
|
353
|
+
* Edit stays client-direct (no quota consumed) — and is refused when
|
|
354
|
+
* its seed was never confirmed by the server (AGL-1358). `fields` is
|
|
355
|
+
* the whole definition spread out of the listener row, so `merge:
|
|
356
|
+
* true` has nothing left to protect.
|
|
357
|
+
*
|
|
358
|
+
* The guard WRAPS the write. An early return is a shape you can keep
|
|
359
|
+
* while losing the protection; here the write is only reachable
|
|
360
|
+
* through the verdict.
|
|
361
|
+
*/ const verdict = await writeGuardedBySeed({
|
|
362
|
+
subject: 'function',
|
|
363
|
+
unreadable: functionsStatus === 'error',
|
|
364
|
+
fromCache: functionsFromCache
|
|
365
|
+
}, async ()=>{
|
|
366
|
+
await setDoc(doc(firestore, 'hosts', hostId, 'functions', draftId), _extends({}, fields, {
|
|
367
|
+
updatedAt: Timestamp.now()
|
|
368
|
+
}), {
|
|
369
|
+
merge: true
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
// Before `setDraft(null)`, so a refusal keeps the dialog open with
|
|
373
|
+
// the whole edited body and its test run still on screen.
|
|
374
|
+
if (!verdict.ok) {
|
|
375
|
+
return void enqueueSnackbar(verdict.message, {
|
|
376
|
+
variant: 'warning',
|
|
377
|
+
persist: false
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
// New function rides the quota-enforcing resources API (AGL-473).
|
|
382
|
+
await createHostResource({
|
|
383
|
+
hostId,
|
|
384
|
+
resource: 'function',
|
|
385
|
+
data: fields
|
|
386
|
+
});
|
|
387
|
+
// A create moves the count; the one-shot aggregate below does not
|
|
388
|
+
// refresh itself the way the listener refreshes the rows.
|
|
389
|
+
setFunctionCountEpoch((epoch)=>epoch + 1);
|
|
390
|
+
}
|
|
391
|
+
setDraft(null);
|
|
392
|
+
setTestResult(null);
|
|
393
|
+
enqueueSnackbar('Function saved', {
|
|
394
|
+
variant: 'success',
|
|
395
|
+
persist: false
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
var _ref;
|
|
399
|
+
console.error(error);
|
|
400
|
+
enqueueSnackbar((_ref = error == null ? void 0 : error.message) != null ? _ref : 'An error has occurred', {
|
|
401
|
+
variant: 'error',
|
|
402
|
+
allowDuplicate: true
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}, [
|
|
406
|
+
draft,
|
|
407
|
+
nameTaken,
|
|
408
|
+
firestore,
|
|
409
|
+
hostId,
|
|
410
|
+
createHostResource,
|
|
411
|
+
enqueueSnackbar,
|
|
412
|
+
functionsStatus,
|
|
413
|
+
functionsFromCache
|
|
414
|
+
]);
|
|
415
|
+
const handleShowUsage = useCallback((definition)=>async ()=>{
|
|
416
|
+
setUsageLoading(definition.$id);
|
|
417
|
+
const result = await fetchWhereUsed(user, {
|
|
418
|
+
hostId,
|
|
419
|
+
kind: 'function',
|
|
420
|
+
id: definition.$id,
|
|
421
|
+
name: definition.name
|
|
422
|
+
});
|
|
423
|
+
setUsageLoading(null);
|
|
424
|
+
setUsage({
|
|
425
|
+
name: definition.name,
|
|
426
|
+
result
|
|
427
|
+
});
|
|
428
|
+
}, [
|
|
429
|
+
user,
|
|
430
|
+
hostId
|
|
431
|
+
]);
|
|
432
|
+
const handleDelete = useCallback((definition)=>async ()=>{
|
|
433
|
+
// Dependents warning (AGL-187): screens/layouts binding the function
|
|
434
|
+
// and workflow steps calling it.
|
|
435
|
+
const scan = await fetchWhereUsed(user, {
|
|
436
|
+
hostId,
|
|
437
|
+
kind: 'function',
|
|
438
|
+
id: definition.$id,
|
|
439
|
+
name: definition.name
|
|
440
|
+
});
|
|
441
|
+
const confirmed = await confirm({
|
|
442
|
+
title: 'Delete this function?',
|
|
443
|
+
description: scan.total ? `"${definition.name}" is used in ${summarizeDependents(scan)} ` + 'and those bindings and workflow steps will stop running.' : `"${definition.name}" will no longer be runnable.`,
|
|
444
|
+
confirmationText: 'Delete',
|
|
445
|
+
confirmationButtonProps: {
|
|
446
|
+
color: 'error'
|
|
447
|
+
}
|
|
448
|
+
}).then(()=>true).catch(()=>false);
|
|
449
|
+
if (!confirmed) return;
|
|
450
|
+
await updateDoc(doc(firestore, 'hosts', hostId, 'functions', definition.$id), {
|
|
451
|
+
deletedAt: Timestamp.now()
|
|
452
|
+
});
|
|
453
|
+
enqueueSnackbar('Function deleted', {
|
|
454
|
+
variant: 'success',
|
|
455
|
+
persist: false
|
|
456
|
+
});
|
|
457
|
+
}, [
|
|
458
|
+
user,
|
|
459
|
+
confirm,
|
|
460
|
+
firestore,
|
|
461
|
+
hostId,
|
|
462
|
+
enqueueSnackbar
|
|
463
|
+
]);
|
|
464
|
+
const setRow = (section, operationIndex, rowIndex, key, value)=>patch((previous)=>{
|
|
465
|
+
const operations = previous.operations.map((operation, index)=>index === operationIndex ? _extends({}, operation, {
|
|
466
|
+
[section]: operation[section].map((row, index2)=>index2 === rowIndex ? _extends({}, row, {
|
|
467
|
+
[key]: value
|
|
468
|
+
}) : row)
|
|
469
|
+
}) : operation);
|
|
470
|
+
return _extends({}, previous, {
|
|
471
|
+
operations
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
const renderSetRows = (section, operationIndex)=>{
|
|
475
|
+
const operation = draft == null ? void 0 : draft.operations[operationIndex];
|
|
476
|
+
if (!operation) return null;
|
|
477
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
478
|
+
spacing: 1,
|
|
479
|
+
children: [
|
|
480
|
+
operation[section].map((row, rowIndex)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
481
|
+
direction: "row",
|
|
482
|
+
spacing: 1,
|
|
483
|
+
sx: {
|
|
484
|
+
alignItems: 'center'
|
|
485
|
+
},
|
|
486
|
+
children: [
|
|
487
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
488
|
+
variant: "caption",
|
|
489
|
+
children: 'SET'
|
|
490
|
+
}),
|
|
491
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
492
|
+
value: row.set,
|
|
493
|
+
onChange: (event)=>setRow(section, operationIndex, rowIndex, 'set', event.target.value),
|
|
494
|
+
size: "small",
|
|
495
|
+
select: true,
|
|
496
|
+
sx: {
|
|
497
|
+
minWidth: 90
|
|
498
|
+
},
|
|
499
|
+
children: names.map((name)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
500
|
+
value: name,
|
|
501
|
+
children: name
|
|
502
|
+
}, name))
|
|
503
|
+
}),
|
|
504
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
505
|
+
variant: "caption",
|
|
506
|
+
children: 'equal to'
|
|
507
|
+
}),
|
|
508
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
509
|
+
value: row.expression,
|
|
510
|
+
placeholder: "P1 + P2",
|
|
511
|
+
onChange: (event)=>setRow(section, operationIndex, rowIndex, 'expression', event.target.value),
|
|
512
|
+
size: "small",
|
|
513
|
+
sx: {
|
|
514
|
+
flex: 1
|
|
515
|
+
}
|
|
516
|
+
}),
|
|
517
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
518
|
+
size: "small",
|
|
519
|
+
"aria-label": "remove operation row",
|
|
520
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
521
|
+
operations: previous.operations.map((op, index)=>index === operationIndex ? _extends({}, op, {
|
|
522
|
+
[section]: op[section].filter((_, index2)=>index2 !== rowIndex)
|
|
523
|
+
}) : op)
|
|
524
|
+
})),
|
|
525
|
+
children: '×'
|
|
526
|
+
})
|
|
527
|
+
]
|
|
528
|
+
}, rowIndex)),
|
|
529
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
530
|
+
size: "small",
|
|
531
|
+
sx: {
|
|
532
|
+
alignSelf: 'flex-start'
|
|
533
|
+
},
|
|
534
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
535
|
+
operations: previous.operations.map((op, index)=>index === operationIndex ? _extends({}, op, {
|
|
536
|
+
[section]: [
|
|
537
|
+
...op[section],
|
|
538
|
+
{
|
|
539
|
+
set: '',
|
|
540
|
+
expression: ''
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
}) : op)
|
|
544
|
+
})),
|
|
545
|
+
children: 'Add set'
|
|
546
|
+
})
|
|
547
|
+
]
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
551
|
+
header: 'Functions',
|
|
552
|
+
help: pluginDocsHelp('bindings', {
|
|
553
|
+
anchor: '#no-code-functions'
|
|
554
|
+
}),
|
|
555
|
+
contentGutterX: true,
|
|
556
|
+
contentGutterY: true,
|
|
557
|
+
children: [
|
|
558
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
559
|
+
spacing: 1,
|
|
560
|
+
children: [
|
|
561
|
+
functions.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
562
|
+
variant: "body2",
|
|
563
|
+
color: "text.secondary",
|
|
564
|
+
children: 'Build no-code logic: parameters in, conditional operations, ' + 'a value out. Wire functions into components and workflows ' + 'as the builder grows.'
|
|
565
|
+
}) : visibleFunctions.map((definition)=>{
|
|
566
|
+
var _definition_parameters;
|
|
567
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
568
|
+
direction: "row",
|
|
569
|
+
spacing: 1,
|
|
570
|
+
sx: {
|
|
571
|
+
alignItems: 'center'
|
|
572
|
+
},
|
|
573
|
+
children: [
|
|
574
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
575
|
+
sx: {
|
|
576
|
+
flex: 1,
|
|
577
|
+
minWidth: 0
|
|
578
|
+
},
|
|
579
|
+
children: [
|
|
580
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
581
|
+
variant: "body2",
|
|
582
|
+
noWrap: true,
|
|
583
|
+
children: definition.name
|
|
584
|
+
}),
|
|
585
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
586
|
+
variant: "caption",
|
|
587
|
+
color: "text.secondary",
|
|
588
|
+
noWrap: true,
|
|
589
|
+
children: `${((_definition_parameters = definition.parameters) != null ? _definition_parameters : []).map((p)=>p.name).join(', ') || 'no parameters'} → ${definition.returnValue || '—'}`
|
|
590
|
+
})
|
|
591
|
+
]
|
|
592
|
+
}),
|
|
593
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
594
|
+
size: "small",
|
|
595
|
+
disabled: usageLoading === definition.$id,
|
|
596
|
+
onClick: handleShowUsage(definition),
|
|
597
|
+
children: usageLoading === definition.$id ? 'Scanning…' : 'Usage'
|
|
598
|
+
}),
|
|
599
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
600
|
+
size: "small",
|
|
601
|
+
onClick: ()=>{
|
|
602
|
+
var _definition_name, _definition_parameters, _definition_variables, _definition_operations, _definition_returnValue;
|
|
603
|
+
setTestArgs({});
|
|
604
|
+
setTestResult(null);
|
|
605
|
+
setDraft({
|
|
606
|
+
id: definition.$id,
|
|
607
|
+
name: (_definition_name = definition.name) != null ? _definition_name : '',
|
|
608
|
+
parameters: (_definition_parameters = definition.parameters) != null ? _definition_parameters : [],
|
|
609
|
+
variables: (_definition_variables = definition.variables) != null ? _definition_variables : [],
|
|
610
|
+
operations: (_definition_operations = definition.operations) != null ? _definition_operations : [],
|
|
611
|
+
returnValue: (_definition_returnValue = definition.returnValue) != null ? _definition_returnValue : ''
|
|
612
|
+
});
|
|
613
|
+
},
|
|
614
|
+
children: 'Edit'
|
|
615
|
+
}),
|
|
616
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
617
|
+
size: "small",
|
|
618
|
+
color: "error",
|
|
619
|
+
onClick: handleDelete(definition),
|
|
620
|
+
children: 'Delete'
|
|
621
|
+
})
|
|
622
|
+
]
|
|
623
|
+
}, definition.$id);
|
|
624
|
+
}),
|
|
625
|
+
functions.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
|
|
626
|
+
page: page,
|
|
627
|
+
pageSize: pageSize,
|
|
628
|
+
rowCount: visibleFunctions.length,
|
|
629
|
+
// The functions the card HOLDS. Not `functionCount`, which is the
|
|
630
|
+
// site's total including soft-deleted rows and answers the quota's
|
|
631
|
+
// question rather than the reader's.
|
|
632
|
+
count: functions.length,
|
|
633
|
+
onPageChange: setPage,
|
|
634
|
+
onPageSizeChange: setPageSize
|
|
635
|
+
}),
|
|
636
|
+
functionsTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
637
|
+
severity: "info",
|
|
638
|
+
children: `Showing ${FUNCTION_CEILING} functions, ordered by id. This site ` + 'has more — the duplicate-name check below only covers the ' + 'ones listed here, so a name may already be taken by one that ' + 'is not.'
|
|
639
|
+
}) : null,
|
|
640
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
641
|
+
size: "small",
|
|
642
|
+
color: "primary",
|
|
643
|
+
sx: {
|
|
644
|
+
alignSelf: 'flex-start'
|
|
645
|
+
},
|
|
646
|
+
onClick: ()=>{
|
|
647
|
+
// Plan cap (AGL-99): dark-launch — plan-less workspaces uncapped.
|
|
648
|
+
// The site's functions, not this page of them (AGL-1716).
|
|
649
|
+
const quota = checkQuota(org, 'functionsPerHost', functionCount);
|
|
650
|
+
if (!quota.allowed) {
|
|
651
|
+
return void enqueueSnackbar(`Function limit reached (${quota.limit}) — upgrade in Billing`, {
|
|
652
|
+
variant: 'warning',
|
|
653
|
+
persist: false
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
setTestArgs({});
|
|
657
|
+
setTestResult(null);
|
|
658
|
+
setDraft(emptyDraft());
|
|
659
|
+
},
|
|
660
|
+
children: 'Add function'
|
|
661
|
+
})
|
|
662
|
+
]
|
|
663
|
+
}),
|
|
664
|
+
/*#__PURE__*/ _jsxs(Dialog, {
|
|
665
|
+
open: Boolean(draft),
|
|
666
|
+
onClose: ()=>setDraft(null),
|
|
667
|
+
maxWidth: "sm",
|
|
668
|
+
fullWidth: true,
|
|
669
|
+
children: [
|
|
670
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
671
|
+
children: (draft == null ? void 0 : draft.id) ? 'Edit Function' : 'Add Function'
|
|
672
|
+
}),
|
|
673
|
+
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
674
|
+
sx: {
|
|
675
|
+
display: 'flex',
|
|
676
|
+
flexDirection: 'column',
|
|
677
|
+
gap: 1.5,
|
|
678
|
+
pt: 1
|
|
679
|
+
},
|
|
680
|
+
children: [
|
|
681
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
682
|
+
label: "Name",
|
|
683
|
+
helperText: nameTaken ? 'A function with this name already exists' : 'Used to identify the function',
|
|
684
|
+
error: nameTaken,
|
|
685
|
+
value: (_ref2 = draft == null ? void 0 : draft.name) != null ? _ref2 : '',
|
|
686
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
687
|
+
name: event.target.value
|
|
688
|
+
})),
|
|
689
|
+
size: "small",
|
|
690
|
+
autoFocus: true,
|
|
691
|
+
sx: {
|
|
692
|
+
mt: 1
|
|
693
|
+
}
|
|
694
|
+
}),
|
|
695
|
+
/*#__PURE__*/ _jsx(SectionLabel, {
|
|
696
|
+
children: 'Parameters'
|
|
697
|
+
}),
|
|
698
|
+
((_ref3 = draft == null ? void 0 : draft.parameters) != null ? _ref3 : []).map((parameter, index)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
699
|
+
direction: "row",
|
|
700
|
+
spacing: 1,
|
|
701
|
+
sx: {
|
|
702
|
+
alignItems: 'center'
|
|
703
|
+
},
|
|
704
|
+
children: [
|
|
705
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
706
|
+
label: "Name",
|
|
707
|
+
value: parameter.name,
|
|
708
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
709
|
+
parameters: previous.parameters.map((p, index2)=>index2 === index ? _extends({}, p, {
|
|
710
|
+
name: event.target.value.replace(/[^a-zA-Z0-9_]/g, '')
|
|
711
|
+
}) : p)
|
|
712
|
+
})),
|
|
713
|
+
size: "small",
|
|
714
|
+
sx: {
|
|
715
|
+
width: 110
|
|
716
|
+
}
|
|
717
|
+
}),
|
|
718
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
719
|
+
label: "Type",
|
|
720
|
+
value: parameter.type,
|
|
721
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
722
|
+
parameters: previous.parameters.map((p, index2)=>index2 === index ? _extends({}, p, {
|
|
723
|
+
type: event.target.value
|
|
724
|
+
}) : p)
|
|
725
|
+
})),
|
|
726
|
+
size: "small",
|
|
727
|
+
select: true,
|
|
728
|
+
sx: {
|
|
729
|
+
width: 130
|
|
730
|
+
},
|
|
731
|
+
children: VALUE_TYPES.map((type)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
732
|
+
value: type.value,
|
|
733
|
+
children: type.label
|
|
734
|
+
}, type.value))
|
|
735
|
+
}),
|
|
736
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
737
|
+
direction: "row",
|
|
738
|
+
spacing: 0.5,
|
|
739
|
+
sx: {
|
|
740
|
+
alignItems: 'center',
|
|
741
|
+
flex: 1
|
|
742
|
+
},
|
|
743
|
+
children: [
|
|
744
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
745
|
+
variant: "caption",
|
|
746
|
+
color: "text.secondary",
|
|
747
|
+
children: 'Required'
|
|
748
|
+
}),
|
|
749
|
+
/*#__PURE__*/ _jsx(Switch, {
|
|
750
|
+
size: "small",
|
|
751
|
+
checked: Boolean(parameter.required),
|
|
752
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
753
|
+
parameters: previous.parameters.map((p, index2)=>index2 === index ? _extends({}, p, {
|
|
754
|
+
required: event.target.checked
|
|
755
|
+
}) : p)
|
|
756
|
+
}))
|
|
757
|
+
})
|
|
758
|
+
]
|
|
759
|
+
}),
|
|
760
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
761
|
+
size: "small",
|
|
762
|
+
"aria-label": "remove parameter",
|
|
763
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
764
|
+
parameters: previous.parameters.filter((_, index2)=>index2 !== index)
|
|
765
|
+
})),
|
|
766
|
+
children: '×'
|
|
767
|
+
})
|
|
768
|
+
]
|
|
769
|
+
}, index)),
|
|
770
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
771
|
+
size: "small",
|
|
772
|
+
sx: {
|
|
773
|
+
alignSelf: 'flex-start'
|
|
774
|
+
},
|
|
775
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
776
|
+
parameters: [
|
|
777
|
+
...previous.parameters,
|
|
778
|
+
{
|
|
779
|
+
name: `P${previous.parameters.length + 1}`,
|
|
780
|
+
type: 'number',
|
|
781
|
+
required: false
|
|
782
|
+
}
|
|
783
|
+
]
|
|
784
|
+
})),
|
|
785
|
+
children: 'Add parameter'
|
|
786
|
+
}),
|
|
787
|
+
((_ref4 = draft == null ? void 0 : draft.parameters) != null ? _ref4 : []).length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
788
|
+
children: [
|
|
789
|
+
/*#__PURE__*/ _jsx(SectionLabel, {
|
|
790
|
+
children: 'What visitors see'
|
|
791
|
+
}),
|
|
792
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
793
|
+
variant: "caption",
|
|
794
|
+
color: "text.secondary",
|
|
795
|
+
children: 'Used by a Function Widget on a page. A parameter with ' + 'choices is asked as a list, a number as a number box and ' + 'a true/false as a switch.'
|
|
796
|
+
}),
|
|
797
|
+
((_ref5 = draft == null ? void 0 : draft.parameters) != null ? _ref5 : []).map((parameter, index)=>/*#__PURE__*/ _jsx(ParameterAudienceFields, {
|
|
798
|
+
parameter: parameter,
|
|
799
|
+
onChange: (next)=>patch((previous)=>_extends({}, previous, {
|
|
800
|
+
parameters: previous.parameters.map((p, index2)=>index2 === index ? next : p)
|
|
801
|
+
}))
|
|
802
|
+
}, index))
|
|
803
|
+
]
|
|
804
|
+
}) : null,
|
|
805
|
+
/*#__PURE__*/ _jsx(SectionLabel, {
|
|
806
|
+
children: 'Variables'
|
|
807
|
+
}),
|
|
808
|
+
((_ref6 = draft == null ? void 0 : draft.variables) != null ? _ref6 : []).map((variable, index)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
809
|
+
direction: "row",
|
|
810
|
+
spacing: 1,
|
|
811
|
+
sx: {
|
|
812
|
+
alignItems: 'center'
|
|
813
|
+
},
|
|
814
|
+
children: [
|
|
815
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
816
|
+
label: "Name",
|
|
817
|
+
value: variable.name,
|
|
818
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
819
|
+
variables: previous.variables.map((v, index2)=>index2 === index ? _extends({}, v, {
|
|
820
|
+
name: event.target.value.replace(/[^a-zA-Z0-9_]/g, '')
|
|
821
|
+
}) : v)
|
|
822
|
+
})),
|
|
823
|
+
size: "small",
|
|
824
|
+
sx: {
|
|
825
|
+
width: 110
|
|
826
|
+
}
|
|
827
|
+
}),
|
|
828
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
829
|
+
label: "Type",
|
|
830
|
+
value: variable.type,
|
|
831
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
832
|
+
variables: previous.variables.map((v, index2)=>index2 === index ? _extends({}, v, {
|
|
833
|
+
type: event.target.value
|
|
834
|
+
}) : v)
|
|
835
|
+
})),
|
|
836
|
+
size: "small",
|
|
837
|
+
select: true,
|
|
838
|
+
sx: {
|
|
839
|
+
width: 130
|
|
840
|
+
},
|
|
841
|
+
children: VALUE_TYPES.map((type)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
842
|
+
value: type.value,
|
|
843
|
+
children: type.label
|
|
844
|
+
}, type.value))
|
|
845
|
+
}),
|
|
846
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
847
|
+
size: "small",
|
|
848
|
+
"aria-label": "remove variable",
|
|
849
|
+
sx: {
|
|
850
|
+
ml: 'auto'
|
|
851
|
+
},
|
|
852
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
853
|
+
variables: previous.variables.filter((_, index2)=>index2 !== index)
|
|
854
|
+
})),
|
|
855
|
+
children: '×'
|
|
856
|
+
})
|
|
857
|
+
]
|
|
858
|
+
}, index)),
|
|
859
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
860
|
+
size: "small",
|
|
861
|
+
sx: {
|
|
862
|
+
alignSelf: 'flex-start'
|
|
863
|
+
},
|
|
864
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
865
|
+
variables: [
|
|
866
|
+
...previous.variables,
|
|
867
|
+
{
|
|
868
|
+
name: `V${previous.variables.length + 1}`,
|
|
869
|
+
type: 'number'
|
|
870
|
+
}
|
|
871
|
+
]
|
|
872
|
+
})),
|
|
873
|
+
children: 'Add variable'
|
|
874
|
+
}),
|
|
875
|
+
/*#__PURE__*/ _jsx(SectionLabel, {
|
|
876
|
+
children: 'Operations'
|
|
877
|
+
}),
|
|
878
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
879
|
+
variant: "caption",
|
|
880
|
+
color: "text.secondary",
|
|
881
|
+
children: 'An expression can use + − × ÷ and parentheses, any site ' + 'variable by its name (a dictionary’s members as ' + 'name.member), and ' + `${FUNCTION_BUILTIN_NAMES.map((name)=>`${name}()`).join(', ')}.`
|
|
882
|
+
}),
|
|
883
|
+
((_ref7 = draft == null ? void 0 : draft.operations) != null ? _ref7 : []).map((operation, index)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
884
|
+
spacing: 1,
|
|
885
|
+
sx: {
|
|
886
|
+
border: 1,
|
|
887
|
+
borderColor: 'divider',
|
|
888
|
+
borderRadius: 1,
|
|
889
|
+
p: 1.5
|
|
890
|
+
},
|
|
891
|
+
children: [
|
|
892
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
893
|
+
variant: "caption",
|
|
894
|
+
color: "text.secondary",
|
|
895
|
+
children: `${index + 1} · If below is TRUE`
|
|
896
|
+
}),
|
|
897
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
898
|
+
direction: "row",
|
|
899
|
+
spacing: 1,
|
|
900
|
+
sx: {
|
|
901
|
+
alignItems: 'center'
|
|
902
|
+
},
|
|
903
|
+
children: [
|
|
904
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
905
|
+
value: operation.if.left,
|
|
906
|
+
placeholder: "P1",
|
|
907
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
908
|
+
operations: previous.operations.map((op, index2)=>index2 === index ? _extends({}, op, {
|
|
909
|
+
if: _extends({}, op.if, {
|
|
910
|
+
left: event.target.value
|
|
911
|
+
})
|
|
912
|
+
}) : op)
|
|
913
|
+
})),
|
|
914
|
+
size: "small",
|
|
915
|
+
sx: {
|
|
916
|
+
flex: 1
|
|
917
|
+
}
|
|
918
|
+
}),
|
|
919
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
920
|
+
value: operation.if.comparator,
|
|
921
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
922
|
+
operations: previous.operations.map((op, index2)=>index2 === index ? _extends({}, op, {
|
|
923
|
+
if: _extends({}, op.if, {
|
|
924
|
+
comparator: event.target.value
|
|
925
|
+
})
|
|
926
|
+
}) : op)
|
|
927
|
+
})),
|
|
928
|
+
size: "small",
|
|
929
|
+
select: true,
|
|
930
|
+
sx: {
|
|
931
|
+
width: 84
|
|
932
|
+
},
|
|
933
|
+
children: COMPARATORS.map((comparator)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
934
|
+
value: comparator,
|
|
935
|
+
children: comparator
|
|
936
|
+
}, comparator))
|
|
937
|
+
}),
|
|
938
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
939
|
+
value: operation.if.right,
|
|
940
|
+
placeholder: "P2",
|
|
941
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
942
|
+
operations: previous.operations.map((op, index2)=>index2 === index ? _extends({}, op, {
|
|
943
|
+
if: _extends({}, op.if, {
|
|
944
|
+
right: event.target.value
|
|
945
|
+
})
|
|
946
|
+
}) : op)
|
|
947
|
+
})),
|
|
948
|
+
size: "small",
|
|
949
|
+
sx: {
|
|
950
|
+
flex: 1
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
]
|
|
954
|
+
}),
|
|
955
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
956
|
+
variant: "caption",
|
|
957
|
+
color: "text.secondary",
|
|
958
|
+
children: 'Then do this'
|
|
959
|
+
}),
|
|
960
|
+
renderSetRows('then', index),
|
|
961
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
962
|
+
variant: "caption",
|
|
963
|
+
color: "text.secondary",
|
|
964
|
+
children: 'Otherwise do this'
|
|
965
|
+
}),
|
|
966
|
+
renderSetRows('otherwise', index)
|
|
967
|
+
]
|
|
968
|
+
}, index)),
|
|
969
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
970
|
+
size: "small",
|
|
971
|
+
sx: {
|
|
972
|
+
alignSelf: 'flex-start'
|
|
973
|
+
},
|
|
974
|
+
onClick: ()=>patch((previous)=>_extends({}, previous, {
|
|
975
|
+
operations: [
|
|
976
|
+
...previous.operations,
|
|
977
|
+
{
|
|
978
|
+
if: {
|
|
979
|
+
left: '',
|
|
980
|
+
comparator: '<=',
|
|
981
|
+
right: ''
|
|
982
|
+
},
|
|
983
|
+
then: [
|
|
984
|
+
{
|
|
985
|
+
set: '',
|
|
986
|
+
expression: ''
|
|
987
|
+
}
|
|
988
|
+
],
|
|
989
|
+
otherwise: []
|
|
990
|
+
}
|
|
991
|
+
]
|
|
992
|
+
})),
|
|
993
|
+
children: 'Add operation'
|
|
994
|
+
}),
|
|
995
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
996
|
+
label: "Return value",
|
|
997
|
+
value: (_ref8 = draft == null ? void 0 : draft.returnValue) != null ? _ref8 : '',
|
|
998
|
+
onChange: (event)=>patch((previous)=>_extends({}, previous, {
|
|
999
|
+
returnValue: event.target.value
|
|
1000
|
+
})),
|
|
1001
|
+
size: "small",
|
|
1002
|
+
select: true,
|
|
1003
|
+
children: names.map((name)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
1004
|
+
value: name,
|
|
1005
|
+
children: name
|
|
1006
|
+
}, name))
|
|
1007
|
+
}),
|
|
1008
|
+
/*#__PURE__*/ _jsx(Divider, {}),
|
|
1009
|
+
/*#__PURE__*/ _jsx(SectionLabel, {
|
|
1010
|
+
children: 'Test run'
|
|
1011
|
+
}),
|
|
1012
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
1013
|
+
direction: "row",
|
|
1014
|
+
spacing: 1,
|
|
1015
|
+
sx: {
|
|
1016
|
+
flexWrap: 'wrap'
|
|
1017
|
+
},
|
|
1018
|
+
children: [
|
|
1019
|
+
((_ref9 = draft == null ? void 0 : draft.parameters) != null ? _ref9 : []).map((parameter)=>{
|
|
1020
|
+
var _testArgs_parameter_name;
|
|
1021
|
+
return /*#__PURE__*/ _jsx(TextField, {
|
|
1022
|
+
label: parameter.name,
|
|
1023
|
+
value: (_testArgs_parameter_name = testArgs[parameter.name]) != null ? _testArgs_parameter_name : '',
|
|
1024
|
+
onChange: (event)=>setTestArgs((previous)=>_extends({}, previous, {
|
|
1025
|
+
[parameter.name]: event.target.value
|
|
1026
|
+
})),
|
|
1027
|
+
size: "small",
|
|
1028
|
+
sx: {
|
|
1029
|
+
width: 110
|
|
1030
|
+
}
|
|
1031
|
+
}, parameter.name);
|
|
1032
|
+
}),
|
|
1033
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
1034
|
+
size: "small",
|
|
1035
|
+
color: "primary",
|
|
1036
|
+
onClick: handleTestRun,
|
|
1037
|
+
children: 'Run'
|
|
1038
|
+
})
|
|
1039
|
+
]
|
|
1040
|
+
}),
|
|
1041
|
+
testResult ? /*#__PURE__*/ _jsx(Alert, {
|
|
1042
|
+
severity: testResult.startsWith('Error') ? 'warning' : 'success',
|
|
1043
|
+
children: testResult
|
|
1044
|
+
}) : null
|
|
1045
|
+
]
|
|
1046
|
+
}),
|
|
1047
|
+
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
1048
|
+
children: [
|
|
1049
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
1050
|
+
onClick: ()=>setDraft(null),
|
|
1051
|
+
children: 'Cancel'
|
|
1052
|
+
}),
|
|
1053
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
1054
|
+
variant: "contained",
|
|
1055
|
+
color: "primary",
|
|
1056
|
+
disabled: !(draft == null ? void 0 : draft.name.trim()) || nameTaken,
|
|
1057
|
+
onClick: handleSave,
|
|
1058
|
+
children: 'Done'
|
|
1059
|
+
})
|
|
1060
|
+
]
|
|
1061
|
+
})
|
|
1062
|
+
]
|
|
1063
|
+
}),
|
|
1064
|
+
/*#__PURE__*/ _jsx(WhereUsedDialog, {
|
|
1065
|
+
hostId: hostId,
|
|
1066
|
+
usage: usage,
|
|
1067
|
+
onClose: ()=>setUsage(null)
|
|
1068
|
+
})
|
|
1069
|
+
]
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
HostFunctionsCard.displayName = 'HostFunctionsCard';
|
|
1073
|
+
export default HostFunctionsCard;
|
|
1074
|
+
|
|
1075
|
+
//# sourceMappingURL=host-functions-card.component.js.map
|