@aglyn/plugins-logic 1.0.0-beta.246 → 1.0.0-beta.249

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 (29) hide show
  1. package/package.json +8 -8
  2. package/src/lib/components/formula-builder-dialog.component.d.ts +33 -0
  3. package/src/lib/components/formula-builder-dialog.component.js +331 -0
  4. package/src/lib/components/formula-builder-dialog.component.js.map +1 -0
  5. package/src/lib/components/formula-builder.component.d.ts +30 -0
  6. package/src/lib/components/formula-builder.component.js +62 -0
  7. package/src/lib/components/formula-builder.component.js.map +1 -0
  8. package/src/lib/components/host-functions-card.component.js +167 -62
  9. package/src/lib/components/host-functions-card.component.js.map +1 -1
  10. package/src/lib/components/host-variables-card.component.js +14 -9
  11. package/src/lib/components/host-variables-card.component.js.map +1 -1
  12. package/src/lib/components/parameter-choices-editor.component.d.ts +31 -0
  13. package/src/lib/components/parameter-choices-editor.component.js +124 -0
  14. package/src/lib/components/parameter-choices-editor.component.js.map +1 -0
  15. package/src/lib/components/structured-value-editor.component.d.ts +30 -0
  16. package/src/lib/components/structured-value-editor.component.js +189 -0
  17. package/src/lib/components/structured-value-editor.component.js.map +1 -0
  18. package/src/lib/model/formula-names.d.ts +41 -0
  19. package/src/lib/model/formula-names.js +82 -0
  20. package/src/lib/model/formula-names.js.map +1 -0
  21. package/src/lib/model/formula.d.ts +72 -0
  22. package/src/lib/model/formula.js +264 -0
  23. package/src/lib/model/formula.js.map +1 -0
  24. package/src/lib/model/parameter-options.d.ts +16 -0
  25. package/src/lib/model/parameter-options.js +31 -0
  26. package/src/lib/model/parameter-options.js.map +1 -1
  27. package/src/lib/model/structured-value.d.ts +51 -0
  28. package/src/lib/model/structured-value.js +135 -0
  29. package/src/lib/model/structured-value.js.map +1 -0
@@ -34,8 +34,10 @@ import { collection, doc, getCountFromServer, getDocs, limit, query } from "fire
34
34
  import { useCallback, useEffect, useMemo, useState } from "react";
35
35
  import { useFirestore, useFirestoreCollection, useHostResourceApi, useUser, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
36
36
  import { writeSiteWideChange } from "@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change";
37
- import { formatParameterOptions, parseParameterOptions } from "../model/parameter-options.js";
37
+ import { ParameterChoicesEditor } from "./parameter-choices-editor.component.js";
38
38
  import WhereUsedDialog from "./where-used-dialog.component.js";
39
+ import { FormulaBuilderButton } from "./formula-builder.component.js";
40
+ import { formulaNames } from "../model/formula-names.js";
39
41
  import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
40
42
  import { HOST_LOGIC_ZONE, LOGIC_FUNCTION_EDITOR_ZONE } from "./logic-zones.js";
41
43
  import { fetchWhereUsed, summarizeDependents } from "@aglyn/aglyn/app-utils/where-used";
@@ -106,74 +108,118 @@ function emptyDraft() {
106
108
  /**
107
109
  * What a VISITOR meets for one parameter (AGL-3202): the label above the
108
110
  * input, what it starts with, and — for a question with fixed answers — the
109
- * choices. Empty fields are dropped from the definition rather than stored
110
- * as empty strings, so a function nobody has dressed for visitors saves
111
- * exactly the shape it always did.
111
+ * choices, one row each (AGL-3768). Empty fields are dropped from the
112
+ * definition rather than stored as empty strings, so a function nobody has
113
+ * dressed for visitors saves exactly the shape it always did.
112
114
  *
113
- * The choices keep their own text while being typed. Parsing every keystroke
114
- * back into the box would eat the space after a comma, and the colon after a
115
- * value, the moment they were typed.
115
+ * "Starts as" is picked, not typed, wherever there is a list to pick from:
116
+ * the choices when there are any, on or off for a true/false.
116
117
  */ function ParameterAudienceFields(props) {
117
- var _parameter_label, _parameter_defaultValue;
118
+ var _parameter_options, _parameter_label, _parameter_defaultValue;
118
119
  const { parameter, onChange } = props;
119
- const [choices, setChoices] = useState(()=>formatParameterOptions(parameter.options));
120
120
  const assign = (field, value)=>{
121
121
  const next = _extends({}, parameter);
122
122
  if (value) next[field] = value;
123
123
  else delete next[field];
124
124
  onChange(next);
125
125
  };
126
+ const options = (_parameter_options = parameter.options) != null ? _parameter_options : [];
127
+ const startsAsChoices = parameter.type === 'boolean' ? [
128
+ {
129
+ value: 'true',
130
+ label: 'On'
131
+ },
132
+ {
133
+ value: 'false',
134
+ label: 'Off'
135
+ }
136
+ ] : options.length ? options.map((option)=>{
137
+ var _option_label;
138
+ return {
139
+ value: option.value,
140
+ label: (_option_label = option.label) != null ? _option_label : option.value
141
+ };
142
+ }) : null;
126
143
  return /*#__PURE__*/ _jsxs(Stack, {
127
- direction: "row",
128
144
  spacing: 1,
129
- sx: {
130
- alignItems: 'center'
131
- },
132
145
  children: [
133
- /*#__PURE__*/ _jsx(Typography, {
134
- variant: "body2",
135
- color: "text.secondary",
146
+ /*#__PURE__*/ _jsxs(Stack, {
147
+ direction: "row",
148
+ spacing: 1,
136
149
  sx: {
137
- width: 110,
138
- flexShrink: 0
150
+ alignItems: 'center'
139
151
  },
140
- noWrap: true,
141
- children: parameter.name || '—'
142
- }),
143
- /*#__PURE__*/ _jsx(TextField, {
144
- label: "Label",
145
- placeholder: parameter.name,
146
- value: (_parameter_label = parameter.label) != null ? _parameter_label : '',
147
- onChange: (event)=>assign('label', event.target.value.slice(0, 80)),
148
- size: "small",
149
- sx: {
150
- flex: 2
151
- }
152
- }),
153
- /*#__PURE__*/ _jsx(TextField, {
154
- label: "Starts as",
155
- value: (_parameter_defaultValue = parameter.defaultValue) != null ? _parameter_defaultValue : '',
156
- onChange: (event)=>assign('defaultValue', event.target.value.slice(0, 120)),
157
- size: "small",
158
- sx: {
159
- flex: 1
160
- }
152
+ children: [
153
+ /*#__PURE__*/ _jsx(Typography, {
154
+ variant: "body2",
155
+ color: "text.secondary",
156
+ sx: {
157
+ width: 110,
158
+ flexShrink: 0
159
+ },
160
+ noWrap: true,
161
+ children: parameter.name || '—'
162
+ }),
163
+ /*#__PURE__*/ _jsx(TextField, {
164
+ label: "Label",
165
+ placeholder: parameter.name,
166
+ value: (_parameter_label = parameter.label) != null ? _parameter_label : '',
167
+ onChange: (event)=>assign('label', event.target.value.slice(0, 80)),
168
+ size: "small",
169
+ sx: {
170
+ flex: 2
171
+ }
172
+ }),
173
+ startsAsChoices ? /*#__PURE__*/ _jsxs(TextField, {
174
+ select: true,
175
+ label: "Starts as",
176
+ value: startsAsChoices.some((choice)=>choice.value === parameter.defaultValue) ? parameter.defaultValue : '',
177
+ onChange: (event)=>assign('defaultValue', event.target.value),
178
+ size: "small",
179
+ sx: {
180
+ flex: 1
181
+ },
182
+ children: [
183
+ /*#__PURE__*/ _jsx(MenuItem, {
184
+ value: "",
185
+ children: /*#__PURE__*/ _jsx("em", {
186
+ children: 'Nothing chosen'
187
+ })
188
+ }),
189
+ startsAsChoices.map((choice)=>/*#__PURE__*/ _jsx(MenuItem, {
190
+ value: choice.value,
191
+ children: choice.label
192
+ }, choice.value))
193
+ ]
194
+ }) : /*#__PURE__*/ _jsx(TextField, {
195
+ label: "Starts as",
196
+ value: (_parameter_defaultValue = parameter.defaultValue) != null ? _parameter_defaultValue : '',
197
+ onChange: (event)=>assign('defaultValue', parameter.type === 'number' ? event.target.value.replace(/[^0-9.-]/g, '') : event.target.value.slice(0, 120)),
198
+ slotProps: parameter.type === 'number' ? {
199
+ htmlInput: {
200
+ inputMode: 'decimal'
201
+ }
202
+ } : undefined,
203
+ size: "small",
204
+ sx: {
205
+ flex: 1
206
+ }
207
+ })
208
+ ]
161
209
  }),
162
- /*#__PURE__*/ _jsx(TextField, {
163
- label: "Choices",
164
- placeholder: "value: Label, value: Label",
165
- value: choices,
166
- onChange: (event)=>{
167
- setChoices(event.target.value);
168
- const options = parseParameterOptions(event.target.value);
210
+ parameter.type === 'boolean' ? null : /*#__PURE__*/ _jsx(ParameterChoicesEditor, {
211
+ parameterName: parameter.name,
212
+ type: parameter.type,
213
+ options: parameter.options,
214
+ onChange: (nextOptions)=>{
169
215
  const next = _extends({}, parameter);
170
- if (options.length) next.options = options;
216
+ if (nextOptions.length) next.options = nextOptions;
171
217
  else delete next.options;
218
+ // A start that is no longer a choice would preselect nothing.
219
+ if (nextOptions.length && next.defaultValue !== undefined && !nextOptions.some((option)=>option.value === next.defaultValue)) {
220
+ delete next.defaultValue;
221
+ }
172
222
  onChange(next);
173
- },
174
- size: "small",
175
- sx: {
176
- flex: 3
177
223
  }
178
224
  })
179
225
  ]
@@ -194,7 +240,7 @@ function emptyDraft() {
194
240
  * the shared evaluator. Definitions persist to `hosts/{hostId}/functions`;
195
241
  * prop/event wiring lands with AGL-93.
196
242
  */ export function HostFunctionsCard(props) {
197
- var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10;
243
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12;
198
244
  var _functions_find;
199
245
  const { hostId } = props;
200
246
  const firestore = useFirestore();
@@ -340,6 +386,35 @@ function emptyDraft() {
340
386
  ...((_ref1 = draft == null ? void 0 : draft.parameters) != null ? _ref1 : []).map((parameter)=>parameter.name),
341
387
  ...((_ref2 = draft == null ? void 0 : draft.variables) != null ? _ref2 : []).map((variable)=>variable.name)
342
388
  ].filter((name)=>VARIABLE_NAME_PATTERN.test(name));
389
+ /**
390
+ * The site's variables, for the formula builder's value picker
391
+ * (AGL-3757). Read the first time a builder opens, like the test run's
392
+ * read below, so an author who never builds a formula costs nothing.
393
+ */ const [siteVariables, setSiteVariables] = useState(null);
394
+ const loadSiteVariables = useCallback(()=>{
395
+ if (siteVariables) return;
396
+ setSiteVariables([]);
397
+ getDocs(query(collection(firestore, 'hosts', hostId, 'variables'), limit(100))).then((snapshot)=>{
398
+ const rows = [];
399
+ snapshot.forEach((row)=>{
400
+ const data = row.data();
401
+ if ((data == null ? void 0 : data.name) && !data.deletedAt) rows.push(data);
402
+ });
403
+ setSiteVariables(rows);
404
+ }).catch((error)=>{
405
+ // The builder still offers the function's own names.
406
+ console.error(error);
407
+ });
408
+ }, [
409
+ siteVariables,
410
+ firestore,
411
+ hostId
412
+ ]);
413
+ const formulaNameOptions = formulaNames({
414
+ parameters: (_ref3 = draft == null ? void 0 : draft.parameters) != null ? _ref3 : [],
415
+ locals: (_ref4 = draft == null ? void 0 : draft.variables) != null ? _ref4 : [],
416
+ siteVariables: siteVariables != null ? siteVariables : []
417
+ });
343
418
  // Case-insensitive uniqueness (AGL-185): function names must stay
344
419
  // unambiguous for legacy {{fn:name(...)}} token resolution.
345
420
  const nameTaken = Boolean(draft && functions.some((definition)=>{
@@ -566,6 +641,12 @@ function emptyDraft() {
566
641
  flex: 1
567
642
  }
568
643
  }),
644
+ /*#__PURE__*/ _jsx(FormulaBuilderButton, {
645
+ current: row.expression,
646
+ names: formulaNameOptions,
647
+ onOpen: loadSiteVariables,
648
+ onUse: (expression)=>setRow(section, operationIndex, rowIndex, 'expression', expression)
649
+ }),
569
650
  /*#__PURE__*/ _jsx(IconButton, {
570
651
  size: "small",
571
652
  "aria-label": "remove operation row",
@@ -752,7 +833,7 @@ function emptyDraft() {
752
833
  label: "Name",
753
834
  helperText: nameTaken ? 'A function with this name already exists' : 'Used to identify the function',
754
835
  error: nameTaken,
755
- value: (_ref3 = draft == null ? void 0 : draft.name) != null ? _ref3 : '',
836
+ value: (_ref5 = draft == null ? void 0 : draft.name) != null ? _ref5 : '',
756
837
  onChange: (event)=>patch((previous)=>_extends({}, previous, {
757
838
  name: event.target.value
758
839
  })),
@@ -765,7 +846,7 @@ function emptyDraft() {
765
846
  /*#__PURE__*/ _jsx(SectionLabel, {
766
847
  children: 'Parameters'
767
848
  }),
768
- ((_ref4 = draft == null ? void 0 : draft.parameters) != null ? _ref4 : []).map((parameter, index)=>/*#__PURE__*/ _jsxs(Stack, {
849
+ ((_ref6 = draft == null ? void 0 : draft.parameters) != null ? _ref6 : []).map((parameter, index)=>/*#__PURE__*/ _jsxs(Stack, {
769
850
  direction: "row",
770
851
  spacing: 1,
771
852
  sx: {
@@ -854,7 +935,7 @@ function emptyDraft() {
854
935
  })),
855
936
  children: 'Add parameter'
856
937
  }),
857
- ((_ref5 = draft == null ? void 0 : draft.parameters) != null ? _ref5 : []).length ? /*#__PURE__*/ _jsxs(_Fragment, {
938
+ ((_ref7 = draft == null ? void 0 : draft.parameters) != null ? _ref7 : []).length ? /*#__PURE__*/ _jsxs(_Fragment, {
858
939
  children: [
859
940
  /*#__PURE__*/ _jsx(SectionLabel, {
860
941
  children: 'What visitors see'
@@ -862,9 +943,9 @@ function emptyDraft() {
862
943
  /*#__PURE__*/ _jsx(Typography, {
863
944
  variant: "caption",
864
945
  color: "text.secondary",
865
- 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.'
946
+ children: 'Used by a Function Widget on a page. A parameter with ' + 'choices is asked as a list — Add choice gives it one — a ' + 'number as a number box and a true/false as a switch.'
866
947
  }),
867
- ((_ref6 = draft == null ? void 0 : draft.parameters) != null ? _ref6 : []).map((parameter, index)=>/*#__PURE__*/ _jsx(ParameterAudienceFields, {
948
+ ((_ref8 = draft == null ? void 0 : draft.parameters) != null ? _ref8 : []).map((parameter, index)=>/*#__PURE__*/ _jsx(ParameterAudienceFields, {
868
949
  parameter: parameter,
869
950
  onChange: (next)=>patch((previous)=>_extends({}, previous, {
870
951
  parameters: previous.parameters.map((p, index2)=>index2 === index ? next : p)
@@ -875,7 +956,7 @@ function emptyDraft() {
875
956
  /*#__PURE__*/ _jsx(SectionLabel, {
876
957
  children: 'Variables'
877
958
  }),
878
- ((_ref7 = draft == null ? void 0 : draft.variables) != null ? _ref7 : []).map((variable, index)=>/*#__PURE__*/ _jsxs(Stack, {
959
+ ((_ref9 = draft == null ? void 0 : draft.variables) != null ? _ref9 : []).map((variable, index)=>/*#__PURE__*/ _jsxs(Stack, {
879
960
  direction: "row",
880
961
  spacing: 1,
881
962
  sx: {
@@ -948,9 +1029,9 @@ function emptyDraft() {
948
1029
  /*#__PURE__*/ _jsx(Typography, {
949
1030
  variant: "caption",
950
1031
  color: "text.secondary",
951
- 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(', ')}.`
1032
+ children: 'Press ƒx beside a box to build its formula by picking values — ' + 'add, smallest of, format as money and more. A box also takes ' + 'a formula written out: + − × ÷ and parentheses, any site ' + 'variable by its name (a dictionary’s members as ' + 'name.member), and ' + `${FUNCTION_BUILTIN_NAMES.map((name)=>`${name}()`).join(', ')}.`
952
1033
  }),
953
- ((_ref8 = draft == null ? void 0 : draft.operations) != null ? _ref8 : []).map((operation, index)=>/*#__PURE__*/ _jsxs(Stack, {
1034
+ ((_ref10 = draft == null ? void 0 : draft.operations) != null ? _ref10 : []).map((operation, index)=>/*#__PURE__*/ _jsxs(Stack, {
954
1035
  spacing: 1,
955
1036
  sx: {
956
1037
  border: 1,
@@ -986,6 +1067,18 @@ function emptyDraft() {
986
1067
  flex: 1
987
1068
  }
988
1069
  }),
1070
+ /*#__PURE__*/ _jsx(FormulaBuilderButton, {
1071
+ current: operation.if.left,
1072
+ names: formulaNameOptions,
1073
+ onOpen: loadSiteVariables,
1074
+ onUse: (expression)=>patch((previous)=>_extends({}, previous, {
1075
+ operations: previous.operations.map((op, index2)=>index2 === index ? _extends({}, op, {
1076
+ if: _extends({}, op.if, {
1077
+ left: expression
1078
+ })
1079
+ }) : op)
1080
+ }))
1081
+ }),
989
1082
  /*#__PURE__*/ _jsx(TextField, {
990
1083
  value: operation.if.comparator,
991
1084
  onChange: (event)=>patch((previous)=>_extends({}, previous, {
@@ -1019,6 +1112,18 @@ function emptyDraft() {
1019
1112
  sx: {
1020
1113
  flex: 1
1021
1114
  }
1115
+ }),
1116
+ /*#__PURE__*/ _jsx(FormulaBuilderButton, {
1117
+ current: operation.if.right,
1118
+ names: formulaNameOptions,
1119
+ onOpen: loadSiteVariables,
1120
+ onUse: (expression)=>patch((previous)=>_extends({}, previous, {
1121
+ operations: previous.operations.map((op, index2)=>index2 === index ? _extends({}, op, {
1122
+ if: _extends({}, op.if, {
1123
+ right: expression
1124
+ })
1125
+ }) : op)
1126
+ }))
1022
1127
  })
1023
1128
  ]
1024
1129
  }),
@@ -1064,7 +1169,7 @@ function emptyDraft() {
1064
1169
  }),
1065
1170
  /*#__PURE__*/ _jsx(TextField, {
1066
1171
  label: "Return value",
1067
- value: (_ref9 = draft == null ? void 0 : draft.returnValue) != null ? _ref9 : '',
1172
+ value: (_ref11 = draft == null ? void 0 : draft.returnValue) != null ? _ref11 : '',
1068
1173
  onChange: (event)=>patch((previous)=>_extends({}, previous, {
1069
1174
  returnValue: event.target.value
1070
1175
  })),
@@ -1086,7 +1191,7 @@ function emptyDraft() {
1086
1191
  flexWrap: 'wrap'
1087
1192
  },
1088
1193
  children: [
1089
- ((_ref10 = draft == null ? void 0 : draft.parameters) != null ? _ref10 : []).map((parameter)=>{
1194
+ ((_ref12 = draft == null ? void 0 : draft.parameters) != null ? _ref12 : []).map((parameter)=>{
1090
1195
  var _testArgs_parameter_name;
1091
1196
  return /*#__PURE__*/ _jsx(TextField, {
1092
1197
  label: parameter.name,