@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.
Files changed (36) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +45 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/host-functions-card.component.d.ts +19 -0
  8. package/src/lib/components/host-functions-card.component.js +1075 -0
  9. package/src/lib/components/host-functions-card.component.js.map +1 -0
  10. package/src/lib/components/host-reference-health-card.component.d.ts +15 -0
  11. package/src/lib/components/host-reference-health-card.component.js +279 -0
  12. package/src/lib/components/host-reference-health-card.component.js.map +1 -0
  13. package/src/lib/components/host-variables-card.component.d.ts +18 -0
  14. package/src/lib/components/host-variables-card.component.js +630 -0
  15. package/src/lib/components/host-variables-card.component.js.map +1 -0
  16. package/src/lib/components/logic-console-page.d.ts +12 -0
  17. package/src/lib/components/logic-console-page.js +68 -0
  18. package/src/lib/components/logic-console-page.js.map +1 -0
  19. package/src/lib/components/where-used-dialog.component.d.ts +20 -0
  20. package/src/lib/components/where-used-dialog.component.js +103 -0
  21. package/src/lib/components/where-used-dialog.component.js.map +1 -0
  22. package/src/lib/constants/bundle-common.d.ts +8 -0
  23. package/src/lib/constants/bundle-common.js +9 -0
  24. package/src/lib/constants/bundle-common.js.map +1 -0
  25. package/src/lib/model/index.d.ts +23 -0
  26. package/src/lib/model/index.js +23 -0
  27. package/src/lib/model/index.js.map +1 -0
  28. package/src/lib/model/parameter-options.d.ts +26 -0
  29. package/src/lib/model/parameter-options.js +29 -0
  30. package/src/lib/model/parameter-options.js.map +1 -0
  31. package/src/lib/model/reference-audit.d.ts +75 -0
  32. package/src/lib/model/reference-audit.js +155 -0
  33. package/src/lib/model/reference-audit.js.map +1 -0
  34. package/src/lib/plugin.d.ts +29 -0
  35. package/src/lib/plugin.js +82 -0
  36. 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