├── src ├── react-app-env.d.ts ├── specs │ ├── PathSuggestions.feature │ ├── EditorToggling.feature │ ├── TypeValidation.feature │ ├── TypeCoercion.feature │ └── BinaryFlattening.feature ├── schema │ ├── SchemaProvider.ts │ └── PathSuggester.ts ├── util │ ├── Validators.tsx │ └── DefaultProvider.ts ├── theme │ ├── ButtonHelp.tsx │ ├── DefaultMathTheme.tsx │ ├── PathEditor.tsx │ └── DefaultTheme.tsx ├── Consts.tsx ├── Types.ts ├── example │ ├── PurchaseEvent.schema.js │ └── Flowchart.tsx ├── Theme.ts ├── index.tsx └── AstEditor.tsx ├── .gitignore ├── README.md ├── tsconfig.json ├── LICENSE ├── package.json ├── public └── index.html └── CHANGELOG.md /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | .rts2_cache_cjs 5 | .rts2_cache_umd 6 | .rts2_cache_es 7 | -------------------------------------------------------------------------------- /src/specs/PathSuggestions.feature: -------------------------------------------------------------------------------- 1 | Feature: Suggesting Paths from a JSON Schema source 2 | 3 | When a customer is sending us data, or has configured data, then we can 4 | suggest paths automatically based on the JSON Schema definitions they have. 5 | 6 | This makes writing examples easier. -------------------------------------------------------------------------------- /src/schema/SchemaProvider.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema4 } from "json-schema"; 2 | import { SchemaProvider, AST } from "../Types"; 3 | import getPaths from "./PathSuggester"; 4 | 5 | export function makeSchemaProvider(schema: JSONSchema4): SchemaProvider { 6 | return { 7 | getPaths(ast: AST) { 8 | return getPaths(schema); 9 | }, 10 | getTypeAtPath(path: AST) { 11 | return null; 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonata-visual-editor 2 | 3 | A visual expression builder for [jsonata](http://jsonata.org/); 4 | 5 | Demo: https://codesandbox.io/s/jsonata-visual-editor-es4p2 6 | 7 | Uses cases: 8 | 9 | - Conditionals `a < 10 ? "Tier 1" : a < 20 ? "Tier 2" : "Tier 3"` 10 | - Logic builders `user.name = "Logan" and age > 10` 11 | - JSON Mapping `{ "name": user.firstName & " " & user.lastName, "ltv": $sum(user.purchases.total) }` 12 | 13 | 14 | ## Sponsors 15 | 16 | Sponsored by [SaaSquatch](http://saasquatch.com). Loyalty, point and referral programs for forward-looking companies. 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react", 20 | "allowJs": true 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/specs/EditorToggling.feature: -------------------------------------------------------------------------------- 1 | Feature: Toggling between Basic and advanced 2 | 3 | JSONata is a big complex language, and it's not easy to allow all posibilities with a visual editor. 4 | Advanced users can use custom expressions, and a visual builder lets basic users get started quickly. 5 | 6 | Scenario: Can switch to Basic editor on binary expressions 7 | Given the expression is simple 8 | """ 9 | revenue > 30 10 | """ 11 | Then [Switch to Basic] is enabled 12 | 13 | 14 | Scenario: Can't switch on errors 15 | Given the expression is invalid JSONata 16 | """ 17 | error nd 111 -9=-9 18 | """ 19 | Then [Switch to Basic] is disabled 20 | 21 | 22 | Scenario: Can't switch on complex expressions 23 | Given the expression is advanced 24 | """ 25 | "12bbasc" in Product.SKU ? (Product.Price > 30) : (Product.Price > 100) 26 | """ 27 | Then [Switch to Basic] is disabled 28 | 29 | -------------------------------------------------------------------------------- /src/specs/TypeValidation.feature: -------------------------------------------------------------------------------- 1 | Feature: Type Validation 2 | 3 | Certain operators really only make sense for numbers or strings. For example 4 | greater than ">" and less than "<" make sense for numbers, and CAN be used 5 | for strings but in most cases that doesn't make sense. 6 | 7 | To help guide people towards using numbers or strings for the right thing, 8 | we add additional real-time validation. 9 | 10 | Scenario Outline: Number comparisons warn for booleans and strings 11 | Given I'm editing an expression in the basic editor 12 | """ 13 | 14 | """ 15 | Then I will see validation error 16 | """ 17 | Comparisons should compare to a string 18 | """ 19 | 20 | Examples: 21 | | expression | 22 | | a > "foo" | 23 | | a < "foo" | 24 | | a >= "foo" | 25 | | a <= "foo" | 26 | | a > true | 27 | | a < true | 28 | | a >= true | 29 | | a <= true | 30 | -------------------------------------------------------------------------------- /src/util/Validators.tsx: -------------------------------------------------------------------------------- 1 | import { SchemaProvider, ValidatorError, AST } from "../Types"; 2 | import { isPathNode, isNumberNode } from "../AstEditor"; 3 | 4 | export function Validators(schemaProvider: SchemaProvider) { 5 | return { 6 | onlyNumberValidator(ast: AST) { 7 | let error: ValidatorError; 8 | if (isPathNode(ast)) { 9 | const pathType = 10 | schemaProvider && schemaProvider.getTypeAtPath(ast); 11 | if (!pathType) { 12 | error = null; 13 | } else if (["integer", "number", "float"].includes(pathType)) { 14 | error = null; 15 | } else { 16 | error = { 17 | error: "non-number-schema", 18 | message: "Use a variable that is a number" 19 | }; 20 | } 21 | } else if (!isNumberNode(ast)) { 22 | error = { 23 | error: "non-number", 24 | message: "Use a number" 25 | }; 26 | } 27 | return error; 28 | } 29 | }; 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/specs/TypeCoercion.feature: -------------------------------------------------------------------------------- 1 | Feature: Type Coercion 2 | 3 | Usually when people type numbers, they are intended to be used as numbers, not as strings. 4 | Similarly when people write "true" or "false", they intend for those to be used as boolean values, not as strings. 5 | The type coersion in the value editor uses these assumptions to make it faster 6 | and easier for people to just type what they want, and it's processed accordingly 7 | 8 | Scenario Outline: Automaticly switching expression based on what you type 9 | Given I'm typing into the String editor 10 | When I type 11 | """ 12 | 13 | """ 14 | Then the expression should be 15 | """ 16 | 17 | """ 18 | 19 | Examples: 20 | | input | output | 21 | | foo | "foo" | 22 | | 123 | 123 | 23 | | 3.14 | 3.14 | ## TODO: Breaks when typing `3.` 24 | | true | true | 25 | | false | false | 26 | | tru | "tru" | 27 | | null | null | 28 | | has"quotes | "has\"quotes" | 29 | 30 | -------------------------------------------------------------------------------- /src/specs/BinaryFlattening.feature: -------------------------------------------------------------------------------- 1 | Feature: Binary Flattening 2 | 3 | In JSONata the expression `a and b and c` is turned into an AST that looks like `(a and b) and c`. 4 | 5 | This works great for the backend logic, but isn't great for building a visual editor. 6 | 7 | To support a nicer boolean logic building experience, our visual builder needs to flatten 8 | nested binary `and` and `or` conditions when the binary logic permits it. 9 | 10 | 11 | Scenario: Simple binary expressions are flattened 12 | Given an expression 13 | """ 14 | a and b and c 15 | """ 16 | When the basic editor renders 17 | Then it shows three conditions grouped together 18 | | a | 19 | | b | 20 | | c | 21 | 22 | Scenario: Switching and/or changes all the nodes 23 | Given an expression 24 | """ 25 | a and b and c 26 | """ 27 | When the basic editor renders 28 | And I change the type from "and" to "or" 29 | Then the output expression should backend 30 | """ 31 | a or b or c 32 | """ 33 | -------------------------------------------------------------------------------- /src/theme/ButtonHelp.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from "react"; 2 | import { OverlayTrigger, Tooltip, Button } from "react-bootstrap"; 3 | 4 | type ButtonProps = React.ComponentProps; 5 | type ButtonHelpProps = ButtonProps & { 6 | disabledHelp: string; 7 | children: React.ReactNode; 8 | }; 9 | export default function ButtonHelp(props: ButtonHelpProps) { 10 | const { disabledHelp, ...btnProps } = props; 11 | 12 | if (props.disabled) { 13 | /* 14 | Need to ignore the actual Click and Disabled events. 15 | If you actually disable the button, then hover events 16 | and other mouse events won't fire, and the tooltip won't work 17 | */ 18 | const { onClick, disabled, ...subProps } = btnProps; 19 | return ( 20 | {disabledHelp}} 24 | > 25 | 26 |