form0-core 0.0.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/SECURITY.md +179 -0
- package/package.json +49 -7
- package/scripts/postinstall.cjs +38 -0
- package/src/builtins/choice/choicelabel.js +43 -0
- package/src/builtins/choice/choicelabels.js +43 -0
- package/src/builtins/choice/choicevalue.js +43 -0
- package/src/builtins/choice/choicevalues.js +43 -0
- package/src/builtins/choice/hasother.js +37 -0
- package/src/builtins/choice/other.js +44 -0
- package/src/builtins/control/eval.js +223 -0
- package/src/builtins/control/setresult.js +33 -0
- package/src/builtins/docs/choice/choicelabel.mdx +110 -0
- package/src/builtins/docs/choice/choicelabels.mdx +230 -0
- package/src/builtins/docs/choice/choicevalue.mdx +109 -0
- package/src/builtins/docs/choice/choicevalues.mdx +196 -0
- package/src/builtins/docs/choice/hasother.mdx +133 -0
- package/src/builtins/docs/choice/other.mdx +150 -0
- package/src/builtins/docs/control/setresult.mdx +203 -0
- package/src/builtins/docs/event/field/setvalue.mdx +239 -0
- package/src/builtins/docs/index.mdx +234 -0
- package/src/builtins/docs/logical/and.mdx +141 -0
- package/src/builtins/docs/logical/if.mdx +94 -0
- package/src/builtins/docs/logical/or.mdx +191 -0
- package/src/builtins/docs/overview.mdx +255 -0
- package/src/builtins/event/control/off.js +60 -0
- package/src/builtins/event/control/on.js +47 -0
- package/src/builtins/event/event-operations-collector.js +20 -0
- package/src/builtins/event/field/setvalue.js +49 -0
- package/src/builtins/event/ui/alert.js +26 -0
- package/src/builtins/logical/and.js +13 -0
- package/src/builtins/logical/array.js +92 -0
- package/src/builtins/logical/count.js +24 -0
- package/src/builtins/logical/counta.js +24 -0
- package/src/builtins/logical/countblank.js +26 -0
- package/src/builtins/logical/if.js +15 -0
- package/src/builtins/logical/or.js +13 -0
- package/src/builtins/math/abs.js +16 -0
- package/src/builtins/math/ceiling.js +22 -0
- package/src/builtins/math/cos.js +16 -0
- package/src/builtins/math/round.js +27 -0
- package/src/builtins/math/sin.js +16 -0
- package/src/builtins/registry.js +99 -0
- package/src/builtins/schema/datanames.js +77 -0
- package/src/builtins/schema/form.js +20 -0
- package/src/builtins/string/upper.js +21 -0
- package/src/engine/calculation.js +206 -0
- package/src/engine/conditions.js +153 -0
- package/src/engine/context-resolver.js +318 -0
- package/src/engine/evaluator.js +75 -0
- package/src/engine/event-registry.js +76 -0
- package/src/engine/events.js +483 -0
- package/src/engine/field-validation.js +18 -0
- package/src/engine/form-engine.js +242 -0
- package/src/engine/warning-system.js +256 -0
- package/src/index.js +48 -0
- package/src/schema/ai-metadata-validator.js +297 -0
- package/src/schema/attribute-validator.js +237 -0
- package/src/schema/building-plan-blueprint.js +1204 -0
- package/src/schema/building-plan-expander.js +261 -0
- package/src/schema/field-schema-registry.js +232 -0
- package/src/schema/field-specs.js +1537 -0
- package/src/schema/field-value-registry.js +314 -0
- package/src/schema/form-link-validators.js +131 -0
- package/src/schema/operators.js +246 -0
- package/src/schema/schema-validator.js +284 -0
- package/src/security/config.js +37 -0
- package/src/security/validation.js +154 -0
- package/src/utilities/ai-metadata.js +93 -0
- package/src/utilities/field-helpers.js +189 -0
- package/src/utilities/field-types.js +7 -0
- package/src/utilities/form-link-helpers.js +301 -0
- package/src/utilities/hash.js +17 -0
- package/src/utilities/record-transformer.js +666 -0
- package/src/utilities/repeatable-helpers.js +142 -0
- package/src/utilities/uuid.js +45 -0
- package/src/utilities/version-utils.js +97 -0
- package/index.js +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 paqu.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# form0-core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/form0-core)
|
|
4
|
+
[](https://www.npmjs.com/package/form0-core)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://docs.form0.dev)
|
|
7
|
+
[](https://form0.dev)
|
|
8
|
+
|
|
9
|
+
> [!WARNING]
|
|
10
|
+
> form0 is in active, very early development. Do not use in production. Expect breaking
|
|
11
|
+
> changes and unstable behavior.
|
|
12
|
+
|
|
13
|
+
form0-core is the schema-driven engine that powers form0 ecosystem. It is framework-agnostic and runs in any JavaScript runtime (Node.js, browsers, React Native, etc.)
|
|
14
|
+
|
|
15
|
+
## 🚀 Start with the CLI (recommended)
|
|
16
|
+
|
|
17
|
+
The entry point for most users is form0-cli. Follow the quickstart to create a project and preview
|
|
18
|
+
your schema:
|
|
19
|
+
- https://docs.form0.dev/getting-started/quickstart
|
|
20
|
+
|
|
21
|
+
## 🗂️ Documentation
|
|
22
|
+
|
|
23
|
+
- Docs home: https://docs.form0.dev
|
|
24
|
+
- form0-core overview: https://docs.form0.dev/core/overview
|
|
25
|
+
- form0-core concepts: https://docs.form0.dev/core/concepts
|
|
26
|
+
- Main site: https://form0.dev
|
|
27
|
+
|
|
28
|
+
## Direct usage (advanced)
|
|
29
|
+
|
|
30
|
+
If you are integrating the engine directly:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install form0-core
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Security
|
|
37
|
+
|
|
38
|
+
See `SECURITY.md` for security modes and configuration.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Security Features
|
|
2
|
+
|
|
3
|
+
form0-core provides configurable security options for expression evaluation to balance flexibility and safety.
|
|
4
|
+
|
|
5
|
+
## Security Modes
|
|
6
|
+
|
|
7
|
+
### TRUSTED (Default)
|
|
8
|
+
|
|
9
|
+
- **Full JavaScript access** - Current behavior maintained
|
|
10
|
+
- **No restrictions** - All expressions execute as-is
|
|
11
|
+
- **Use case**: Internal tools, trusted developers, client-side only
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { createFormEngine } from 'form0-core';
|
|
15
|
+
|
|
16
|
+
// Default behavior - no security parameter needed
|
|
17
|
+
const engine = createFormEngine({ schema });
|
|
18
|
+
|
|
19
|
+
// Or explicitly set trusted mode
|
|
20
|
+
const engine = createFormEngine({
|
|
21
|
+
schema,
|
|
22
|
+
security: { mode: 'trusted' },
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### SAFE
|
|
27
|
+
|
|
28
|
+
- **Restricted context** - Only whitelisted globals available
|
|
29
|
+
- **Pattern blocking** - Dangerous patterns are blocked
|
|
30
|
+
- **Use case**: User-generated expressions, safer environments
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { createFormEngine, SECURITY_MODES, SAFE_SECURITY_CONFIG } from 'form0-core';
|
|
34
|
+
|
|
35
|
+
// Simple safe mode
|
|
36
|
+
const engine = createFormEngine({
|
|
37
|
+
schema,
|
|
38
|
+
security: { mode: SECURITY_MODES.SAFE },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Or use predefined safe config
|
|
42
|
+
const engine = createFormEngine({
|
|
43
|
+
schema,
|
|
44
|
+
security: SAFE_SECURITY_CONFIG,
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### CUSTOM
|
|
49
|
+
|
|
50
|
+
- **User-defined rules** - Configure your own security settings
|
|
51
|
+
- **Flexible restrictions** - Mix and match security features
|
|
52
|
+
- **Use case**: Specific security requirements
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
const engine = createFormEngine({
|
|
56
|
+
schema,
|
|
57
|
+
security: {
|
|
58
|
+
mode: SECURITY_MODES.CUSTOM,
|
|
59
|
+
maxExecutionTime: 1000,
|
|
60
|
+
allowedGlobals: ['Math', 'Date', 'Number'],
|
|
61
|
+
blockedPatterns: [/\bwindow\b/, /\bdocument\b/, /\bfetch\b/],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Security Configuration Options
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// TRUSTED mode (default) - no additional config needed
|
|
70
|
+
const trustedConfig = {
|
|
71
|
+
mode: 'trusted', // Full JavaScript access
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// SAFE mode - uses predefined safe settings
|
|
75
|
+
const safeConfig = {
|
|
76
|
+
mode: 'safe', // Automatically applies safe defaults
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// CUSTOM mode - define your own rules
|
|
80
|
+
const customConfig = {
|
|
81
|
+
mode: 'custom',
|
|
82
|
+
maxExecutionTime: 1000, // Milliseconds (future feature)
|
|
83
|
+
maxCallStackDepth: 100, // Maximum recursion depth (future feature)
|
|
84
|
+
allowedGlobals: ['Math', 'Date', 'JSON'], // Whitelisted global objects
|
|
85
|
+
blockedPatterns: [/\beval\b/, /\bwindow\b/], // Regex patterns to block
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Default Blocked Patterns (Safe Mode)
|
|
90
|
+
|
|
91
|
+
The following patterns are blocked by default in safe mode:
|
|
92
|
+
|
|
93
|
+
- `eval`, `Function` - Code execution
|
|
94
|
+
- `window`, `document` - Browser globals
|
|
95
|
+
- `process`, `require` - Node.js globals
|
|
96
|
+
- `fetch`, `XMLHttpRequest` - Network requests
|
|
97
|
+
- `localStorage`, `sessionStorage` - Storage APIs
|
|
98
|
+
- `__proto__`, `constructor`, `prototype` - Prototype pollution
|
|
99
|
+
|
|
100
|
+
## Migration Guide
|
|
101
|
+
|
|
102
|
+
### No Changes Needed
|
|
103
|
+
|
|
104
|
+
Existing code continues to work unchanged:
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
// This still works exactly as before
|
|
108
|
+
const engine = createFormEngine({ schema, initialValues });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Adding Security
|
|
112
|
+
|
|
113
|
+
To add security, simply include the security parameter:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
// Add safe mode
|
|
117
|
+
const engine = createFormEngine({
|
|
118
|
+
schema,
|
|
119
|
+
initialValues,
|
|
120
|
+
security: { mode: 'safe' },
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Examples
|
|
125
|
+
|
|
126
|
+
### Safe Mathematical Calculations
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
const schema = {
|
|
130
|
+
form: {
|
|
131
|
+
elements: [
|
|
132
|
+
{
|
|
133
|
+
type: 'CalculatedField',
|
|
134
|
+
data_name: 'result',
|
|
135
|
+
calculate: 'Math.max($value1, $value2) * 1.1', // ✅ Works in safe mode
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Blocked Dangerous Expressions
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
const schema = {
|
|
146
|
+
form: {
|
|
147
|
+
elements: [
|
|
148
|
+
{
|
|
149
|
+
type: 'CalculatedField',
|
|
150
|
+
data_name: 'result',
|
|
151
|
+
calculate: 'window.alert("hello")', // ❌ Blocked in safe mode
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Custom Security Rules
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
const engine = createFormEngine({
|
|
162
|
+
schema,
|
|
163
|
+
security: {
|
|
164
|
+
mode: 'custom',
|
|
165
|
+
allowedGlobals: ['Math'], // Only Math allowed
|
|
166
|
+
blockedPatterns: [/\bDate\b/], // Block Date usage
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Testing Security
|
|
172
|
+
|
|
173
|
+
Use the included test file to verify security behavior:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
node test/security-test.js
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This will show how different expressions behave under different security modes.
|
package/package.json
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"registry": "https://registry.npmjs.org/"
|
|
6
|
+
},
|
|
7
|
+
"description": "Schema-driven engine that powers form0 ecosystem.",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"type": "module",
|
|
5
10
|
"scripts": {
|
|
6
|
-
"test": "
|
|
11
|
+
"test": "node tests/form0-core.test.js && node tests/security-test.js",
|
|
12
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
13
|
+
"format": "prettier --write \"**/*.{js,jsx,json,ts,tsx,md}\"",
|
|
14
|
+
"format:check": "prettier --check \"**/*.{js,jsx,json,ts,tsx,md}\""
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./src/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/paqu-io/form0-core.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"form",
|
|
27
|
+
"form-builder",
|
|
28
|
+
"form-generator",
|
|
29
|
+
"form-engine",
|
|
30
|
+
"schema",
|
|
31
|
+
"json-schema",
|
|
32
|
+
"react",
|
|
33
|
+
"react-native",
|
|
34
|
+
"dynamic-forms"
|
|
35
|
+
],
|
|
36
|
+
"author": "paqu.io <hello@paqu.io> (https://paqu.io/)",
|
|
37
|
+
"contributors": [
|
|
38
|
+
{
|
|
39
|
+
"name": "aragornii"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/paqu-io/form0-core/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://form0.dev/",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
7
49
|
},
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"ignore": "^7.0.5",
|
|
52
|
+
"prettier": "^3.5.3"
|
|
53
|
+
}
|
|
12
54
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const PACKAGE_NAME = 'form0-core';
|
|
2
|
+
|
|
3
|
+
const npmUserAgent = process.env.npm_config_user_agent || '';
|
|
4
|
+
const npmArgv = process.env.npm_config_argv;
|
|
5
|
+
|
|
6
|
+
const isSupportedManager = /(npm|pnpm|yarn|bun)\//.test(npmUserAgent);
|
|
7
|
+
|
|
8
|
+
const parseNpmArgv = (raw) => {
|
|
9
|
+
if (!raw) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const includesExplicitPackage = (argv) => {
|
|
18
|
+
if (!argv) return false;
|
|
19
|
+
const args = Array.isArray(argv.original) ? argv.original : argv.cooked || [];
|
|
20
|
+
return args.some(
|
|
21
|
+
(arg) => arg === PACKAGE_NAME || arg.startsWith(`${PACKAGE_NAME}@`),
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (isSupportedManager && includesExplicitPackage(parseNpmArgv(npmArgv))) {
|
|
26
|
+
// Keep message short to avoid noisy installs.
|
|
27
|
+
console.warn(
|
|
28
|
+
[
|
|
29
|
+
'',
|
|
30
|
+
'form0-core is the engine only.',
|
|
31
|
+
'For the full form0 ecosystem, install the CLI:',
|
|
32
|
+
' npm install -g form0-cli',
|
|
33
|
+
'Docs: https://docs.form0.dev/getting-started/quickstart',
|
|
34
|
+
'If you are integrating the engine directly, you can ignore this warning.',
|
|
35
|
+
'',
|
|
36
|
+
].join('\n'),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @builtin CHOICELABEL
|
|
3
|
+
* @description Retrieves the currently selected choice field label, preserving the type (if the label is a number preserves the number type, otherwise string). If no selection it returns null.
|
|
4
|
+
* @param {Object} choiceField - The choice field object with choice and other arrays
|
|
5
|
+
* @returns {*} The selected choice label with preserved type, or null if no selection
|
|
6
|
+
* @example
|
|
7
|
+
* // Get the selected city label
|
|
8
|
+
* CHOICELABEL($city)
|
|
9
|
+
* @example
|
|
10
|
+
* // Use in display logic
|
|
11
|
+
* "You selected: " + CHOICELABEL($city)
|
|
12
|
+
*/
|
|
13
|
+
export const CHOICELABEL = (choiceField) => {
|
|
14
|
+
// Handle null/undefined input
|
|
15
|
+
if (!choiceField || typeof choiceField !== 'object') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate structure
|
|
20
|
+
if (!Array.isArray(choiceField.choice)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get the selected choice (single selection)
|
|
25
|
+
if (choiceField.choice.length > 0) {
|
|
26
|
+
const selectedChoice = choiceField.choice[0];
|
|
27
|
+
if (selectedChoice && selectedChoice.label !== undefined) {
|
|
28
|
+
// Preserve type - if it's a number, keep it as number
|
|
29
|
+
const label = selectedChoice.label;
|
|
30
|
+
|
|
31
|
+
// Try to parse as number if it's a string that represents a number
|
|
32
|
+
if (typeof label === 'string' && !isNaN(label) && !isNaN(parseFloat(label))) {
|
|
33
|
+
// Check if it's an integer or float
|
|
34
|
+
const numLabel = parseFloat(label);
|
|
35
|
+
return Number.isInteger(numLabel) ? parseInt(label, 10) : numLabel;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return label;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @builtin CHOICELABELS
|
|
3
|
+
* @description Retrieves an array of all selected choice field labels from a MultiChoiceField, preserving the type of each label. Returns an empty array if no selections.
|
|
4
|
+
* @param {Object} multiChoiceField - The multi choice field object with choices and other arrays
|
|
5
|
+
* @returns {Array} Array of selected choice labels with preserved types
|
|
6
|
+
* @example
|
|
7
|
+
* // Get all selected color labels
|
|
8
|
+
* CHOICELABELS($colors)
|
|
9
|
+
* @example
|
|
10
|
+
* // Display selected colors
|
|
11
|
+
* "Selected colors: " + CHOICELABELS($colors).join(", ")
|
|
12
|
+
*/
|
|
13
|
+
export const CHOICELABELS = (multiChoiceField) => {
|
|
14
|
+
// Handle null/undefined input
|
|
15
|
+
if (!multiChoiceField || typeof multiChoiceField !== 'object') {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate structure
|
|
20
|
+
if (!Array.isArray(multiChoiceField.choices)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get all selected choices
|
|
25
|
+
const labels = [];
|
|
26
|
+
for (const choice of multiChoiceField.choices) {
|
|
27
|
+
if (choice && choice.label !== undefined) {
|
|
28
|
+
// Preserve type - if it's a number, keep it as number
|
|
29
|
+
const label = choice.label;
|
|
30
|
+
|
|
31
|
+
// Try to parse as number if it's a string that represents a number
|
|
32
|
+
if (typeof label === 'string' && !isNaN(label) && !isNaN(parseFloat(label))) {
|
|
33
|
+
// Check if it's an integer or float
|
|
34
|
+
const numLabel = parseFloat(label);
|
|
35
|
+
labels.push(Number.isInteger(numLabel) ? parseInt(label, 10) : numLabel);
|
|
36
|
+
} else {
|
|
37
|
+
labels.push(label);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return labels;
|
|
43
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @builtin CHOICEVALUE
|
|
3
|
+
* @description Retrieves the currently selected choice field value, preserving the type (if the value is a number preserves the number type, otherwise string). If no selection it returns null.
|
|
4
|
+
* @param {Object} choiceField - The choice field object with choice and other arrays
|
|
5
|
+
* @returns {*} The selected choice value with preserved type, or null if no selection
|
|
6
|
+
* @example
|
|
7
|
+
* // Get the selected city value
|
|
8
|
+
* CHOICEVALUE($city)
|
|
9
|
+
* @example
|
|
10
|
+
* // Use in conditional logic
|
|
11
|
+
* IF(CHOICEVALUE($city) === "bogota", "Welcome to Bogotá!", "Welcome!")
|
|
12
|
+
*/
|
|
13
|
+
export const CHOICEVALUE = (choiceField) => {
|
|
14
|
+
// Handle null/undefined input
|
|
15
|
+
if (!choiceField || typeof choiceField !== 'object') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate structure
|
|
20
|
+
if (!Array.isArray(choiceField.choice)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get the selected choice (single selection)
|
|
25
|
+
if (choiceField.choice.length > 0) {
|
|
26
|
+
const selectedChoice = choiceField.choice[0];
|
|
27
|
+
if (selectedChoice && selectedChoice.value !== undefined) {
|
|
28
|
+
// Preserve type - if it's a number, keep it as number
|
|
29
|
+
const value = selectedChoice.value;
|
|
30
|
+
|
|
31
|
+
// Try to parse as number if it's a string that represents a number
|
|
32
|
+
if (typeof value === 'string' && !isNaN(value) && !isNaN(parseFloat(value))) {
|
|
33
|
+
// Check if it's an integer or float
|
|
34
|
+
const numValue = parseFloat(value);
|
|
35
|
+
return Number.isInteger(numValue) ? parseInt(value, 10) : numValue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @builtin CHOICEVALUES
|
|
3
|
+
* @description Retrieves an array of all selected choice field values from a MultiChoiceField, preserving the type of each value. Returns an empty array if no selections.
|
|
4
|
+
* @param {Object} multiChoiceField - The multi choice field object with choices and other arrays
|
|
5
|
+
* @returns {Array} Array of selected choice values with preserved types
|
|
6
|
+
* @example
|
|
7
|
+
* // Get all selected color values
|
|
8
|
+
* CHOICEVALUES($colors)
|
|
9
|
+
* @example
|
|
10
|
+
* // Check if "red" is selected
|
|
11
|
+
* CHOICEVALUES($colors).includes("red")
|
|
12
|
+
*/
|
|
13
|
+
export const CHOICEVALUES = (multiChoiceField) => {
|
|
14
|
+
// Handle null/undefined input
|
|
15
|
+
if (!multiChoiceField || typeof multiChoiceField !== 'object') {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate structure
|
|
20
|
+
if (!Array.isArray(multiChoiceField.choices)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get all selected choices
|
|
25
|
+
const values = [];
|
|
26
|
+
for (const choice of multiChoiceField.choices) {
|
|
27
|
+
if (choice && choice.value !== undefined) {
|
|
28
|
+
// Preserve type - if it's a number, keep it as number
|
|
29
|
+
const value = choice.value;
|
|
30
|
+
|
|
31
|
+
// Try to parse as number if it's a string that represents a number
|
|
32
|
+
if (typeof value === 'string' && !isNaN(value) && !isNaN(parseFloat(value))) {
|
|
33
|
+
// Check if it's an integer or float
|
|
34
|
+
const numValue = parseFloat(value);
|
|
35
|
+
values.push(Number.isInteger(numValue) ? parseInt(value, 10) : numValue);
|
|
36
|
+
} else {
|
|
37
|
+
values.push(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return values;
|
|
43
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @builtin HASOTHER
|
|
3
|
+
* @description Returns true if user entered an other option, false otherwise. Works with both SingleChoiceField and MultiChoiceField.
|
|
4
|
+
* @param {Object} choiceField - The choice field object (SingleChoiceField with choice array or MultiChoiceField with choices array) and other array
|
|
5
|
+
* @returns {boolean} True if user entered an other option, false otherwise
|
|
6
|
+
* @example
|
|
7
|
+
* // Check if user entered other option in single choice field
|
|
8
|
+
* HASOTHER($city)
|
|
9
|
+
* @example
|
|
10
|
+
* // Check if user entered other option in multi choice field
|
|
11
|
+
* HASOTHER($colors)
|
|
12
|
+
* @example
|
|
13
|
+
* // Use in conditional logic
|
|
14
|
+
* IF(HASOTHER($city), "Custom city: " + OTHER($city), "Selected city: " + CHOICELABEL($city))
|
|
15
|
+
*/
|
|
16
|
+
export const HASOTHER = (choiceField) => {
|
|
17
|
+
// Handle null/undefined input
|
|
18
|
+
if (!choiceField || typeof choiceField !== 'object') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validate structure - must have either choice array (SingleChoiceField) or choices array (MultiChoiceField)
|
|
23
|
+
const hasChoiceArray = Array.isArray(choiceField.choice);
|
|
24
|
+
const hasChoicesArray = Array.isArray(choiceField.choices);
|
|
25
|
+
|
|
26
|
+
if (!hasChoiceArray && !hasChoicesArray) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate other array structure
|
|
31
|
+
if (!Array.isArray(choiceField.other)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if there are any other entries
|
|
36
|
+
return choiceField.other.length > 0;
|
|
37
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @builtin OTHER
|
|
3
|
+
* @description Retrieves the other label if user entered the other option, otherwise null. Works with both SingleChoiceField and MultiChoiceField.
|
|
4
|
+
* @param {Object} choiceField - The choice field object (SingleChoiceField with choice array or MultiChoiceField with choices array) and other array
|
|
5
|
+
* @returns {string|null} The other label if user entered an other option, null otherwise
|
|
6
|
+
* @example
|
|
7
|
+
* // Get the other value from single choice field
|
|
8
|
+
* OTHER($city)
|
|
9
|
+
* @example
|
|
10
|
+
* // Get the other value from multi choice field
|
|
11
|
+
* OTHER($colors)
|
|
12
|
+
* @example
|
|
13
|
+
* // Use in conditional logic
|
|
14
|
+
* IF(HASOTHER($city), "Custom city: " + OTHER($city), "No custom city entered")
|
|
15
|
+
*/
|
|
16
|
+
export const OTHER = (choiceField) => {
|
|
17
|
+
// Handle null/undefined input
|
|
18
|
+
if (!choiceField || typeof choiceField !== 'object') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Validate structure - must have either choice array (SingleChoiceField) or choices array (MultiChoiceField)
|
|
23
|
+
const hasChoiceArray = Array.isArray(choiceField.choice);
|
|
24
|
+
const hasChoicesArray = Array.isArray(choiceField.choices);
|
|
25
|
+
|
|
26
|
+
if (!hasChoiceArray && !hasChoicesArray) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate other array structure
|
|
31
|
+
if (!Array.isArray(choiceField.other)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get the other entry (single selection - user can only add 1 other option)
|
|
36
|
+
if (choiceField.other.length > 0) {
|
|
37
|
+
const otherEntry = choiceField.other[0];
|
|
38
|
+
if (otherEntry && otherEntry.label !== undefined) {
|
|
39
|
+
return otherEntry.label;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
};
|