Skip to content

Commit bbdbdd9

Browse files
committed
feat: input autocompletion
bup
1 parent bf24333 commit bbdbdd9

File tree

9 files changed

+330
-229
lines changed

9 files changed

+330
-229
lines changed

lib/components/Input/Input.jsx

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,60 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useMemo } from 'react';
2+
import { renderToStaticMarkup } from 'react-dom/server';
23

34
import { Link } from '@carbon/react';
45
import { Launch } from '@carbon/icons-react';
56

7+
import { map } from 'lodash';
8+
69
import InputEditor from './InputEditor';
710
import Button from '../Button/Button';
811

912
export default function Input({
1013
element,
1114
input,
1215
setInput,
13-
variables,
16+
reset,
17+
resolvedVariables,
18+
outputVariables,
1419
onRunTask,
1520
}) {
1621

1722
const [ error, setError ] = useState(false);
23+
const [ resetKey, setResetKey ] = useState(0);
24+
25+
const autocompletion = useMemo(() => {
26+
27+
const resolved = map(resolvedVariables, ({ name, detail, info }) => ({
28+
label: name,
29+
type: 'variable',
30+
info: () => createInfo(info),
31+
detail: detail ? `[${detail}]` : undefined,
32+
value: info ? info : undefined,
33+
})) ?? [];
34+
35+
const outputs = Object.entries(outputVariables)?.map(([ name, { value, source } ]) => ({
36+
label: name,
37+
type: 'constant',
38+
info: () => createInfo(value, source),
39+
detail: `[${typeof value}]`,
40+
value: value,
41+
}));
42+
43+
return [ ...resolved, ...outputs ];
44+
45+
}, [ resolvedVariables, outputVariables ]);
46+
47+
const handleRun = async () => {
48+
if (error) {
49+
return;
50+
}
51+
await onRunTask(input);
52+
};
53+
54+
const handleReset = () => {
55+
reset();
56+
setResetKey(prev => prev + 1); // Force InputEditor to re-render
57+
};
1858

1959
const elementName = element.name || element.id;
2060

@@ -52,12 +92,28 @@ export default function Input({
5292
</div>
5393
<div className="section__content">
5494
<InputEditor
95+
key={ resetKey }
5596
value={ input }
5697
onChange={ setInput }
5798
onErrorChange={ setError }
58-
variables={ variables }
99+
autocompletion={ autocompletion }
59100
/>
60101
</div>
61102
</div>
62103
);
63104
}
105+
106+
function createInfo(value, source) {
107+
const div = document.createElement('div');
108+
109+
const htmlString = renderToStaticMarkup(
110+
<div className="info">
111+
<span>{source ? `From output variables of "${source}"` : 'From process variables'}</span>
112+
{value && <pre>{typeof value === 'object' ? JSON.stringify(value, null, 2) : value}</pre>}
113+
</div>
114+
);
115+
116+
div.innerHTML = htmlString;
117+
118+
return div;
119+
}

lib/components/Input/InputEditor.jsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@ export default function InputEditor({
1212
value,
1313
onChange,
1414
onErrorChange,
15-
variables = []
15+
autocompletion = []
1616
}) {
1717

1818
const editorRef = useRef(null);
1919
const viewRef = useRef(null);
2020

21-
const jsonLinter = () => {
22-
return () => {
23-
return (view) => {
24-
const errors = jsonParseLinter()(view);
25-
onErrorChange(!!errors.length);
26-
return errors;
27-
};
28-
};
21+
const jsonLinter = (view) => {
22+
const errors = jsonParseLinter()(view);
23+
onErrorChange(!!errors.length);
24+
25+
if (errors && errors.length > 0) {
26+
const errorMessage = errors[0].message || 'Error';
27+
editorRef.current?.style.setProperty('--error-message', `"${errorMessage}"`);
28+
} else {
29+
editorRef.current?.style.removeProperty('--error-message');
30+
}
31+
32+
return errors;
2933
};
3034

3135
useEffect(() => {
@@ -36,8 +40,8 @@ export default function InputEditor({
3640
extensions: [
3741
basicSetup,
3842
json(),
39-
linter(jsonLinter(onErrorChange)()),
40-
...getAutocompletionExtensions(variables),
43+
linter(jsonLinter, { delay: 100 }),
44+
...getAutocompletionExtensions(autocompletion),
4145
placeholder('Provide process variables in JSON format'),
4246
EditorView.updateListener.of((update) => {
4347
if (update.docChanged) {
@@ -58,7 +62,7 @@ export default function InputEditor({
5862
return () => {
5963
view.destroy();
6064
};
61-
}, [ variables ]);
65+
}, [ autocompletion ]);
6266

6367
return <div ref={ editorRef } className="input-editor" />;
6468
}

lib/hooks/useInput.js

Lines changed: 103 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { useState, useCallback, useEffect } from 'react';
22

3-
import { getInputMapping } from '../utils/inputMapping.js';
3+
import { find } from 'lodash';
4+
5+
import { getInputMapping } from '../utils/bpmnUtils.js';
46

57
/**
68
* Get and set input variables for the given element,
79
* based on the input mapping and available process variables.
810
*
911
* @param {*} element - BPMN element
10-
* @param {Array} variables - Available process variables
12+
* @param {*} resolvedVariables - Available process variables
1113
*/
12-
export function useInput(element, variables) {
14+
export function useInput(element, resolvedVariables) {
1315

1416
const [ input, setInput ] = useState({});
1517
const [ dirty, setDirty ] = useState({});
@@ -19,66 +21,32 @@ export function useInput(element, variables) {
1921
return;
2022
}
2123

24+
setInitialInput();
25+
}, [ element, resolvedVariables ]);
26+
27+
/**
28+
* Create initial input from the BPMN element input mapping.
29+
*/
30+
const setInitialInput = useCallback(() => {
2231
const inputMapping = getInputMapping(element);
2332

24-
if (!inputMapping) {
33+
if (!inputMapping || !resolvedVariables) {
2534
return;
2635
}
2736

28-
const values = inputMapping.reduce((acc, { target, source }) => {
29-
30-
const feelValue = getFeelValue(source);
31-
32-
if (!feelValue) {
33-
return { ...acc, [target]: source };
34-
}
35-
36-
if (isFeelConstant(feelValue)) {
37-
return { ...acc, [target]: feelValue };
38-
}
39-
40-
const knownVariable = variables?.find(variable => variable.name === target);
41-
42-
if (!knownVariable) {
43-
console.log('no known variable for', target);
44-
return { ...acc, [feelValue]: '' };
45-
}
46-
47-
if (knownVariable.type === 'Context' && knownVariable.entries) {
48-
const sourceEntries = Object.fromEntries(knownVariable.entries.map(entry => [ entry.name, '' ]));
49-
return { ...acc, ...sourceEntries };
50-
}
51-
52-
if (knownVariable.info) {
53-
return { ...acc, [target]: knownVariable.info };
54-
}
55-
56-
return { ...acc, [feelValue]: '' };
57-
}, {});
58-
59-
// Convert flat object with dot notation to nested object
60-
const nestedValues = Object.entries(values).reduce((acc, [ key, value ]) => {
61-
const keys = key.split('.');
62-
let current = acc;
63-
64-
for (let i = 0; i < keys.length - 1; i++) {
65-
const keyPart = keys[i];
66-
if (!(keyPart in current)) {
67-
current[keyPart] = {};
68-
}
69-
current = current[keyPart];
70-
}
71-
72-
current[keys[keys.length - 1]] = value;
73-
return acc;
74-
}, {});
37+
const values = createFromInputMapping(inputMapping, resolvedVariables);
7538

7639
setInput(prev => ({
7740
...prev,
78-
[element.id]: JSON.stringify(nestedValues, null, 2)
41+
[element.id]: values
7942
}));
80-
}, [ element, variables ]);
43+
}, [ element, resolvedVariables ]);
8144

45+
/**
46+
* Set input for the currently selected element.
47+
*
48+
* Mark the input as `dirty` to indicate it has been modified.
49+
*/
8250
const handleInput = useCallback((value) => {
8351
setInput(prev => ({
8452
...prev,
@@ -92,25 +60,97 @@ export function useInput(element, variables) {
9260

9361
}, [ element ]);
9462

63+
/**
64+
* Set input back to the initial values based on the BPMN element input mapping.
65+
*/
66+
const handleReset = useCallback(() => {
67+
68+
setInitialInput();
69+
70+
setDirty(prev => ({
71+
...prev,
72+
[element.id]: false
73+
}));
74+
}, [ element, resolvedVariables ]);
75+
9576
return {
9677
input: input[element?.id],
9778
setInput: handleInput,
79+
reset: handleReset
9880
};
9981
}
10082

101-
function getFeelValue(string) {
102-
return string.startsWith('=') ? string.slice(1) : null;
83+
84+
// helpers
85+
86+
function createFromInputMapping(inputMapping, resolvedVariables) {
87+
88+
const values = inputMapping.reduce((acc, { target, source }) => {
89+
90+
const variable = find(resolvedVariables, { name: target });
91+
92+
if (!variable) {
93+
return acc;
94+
}
95+
96+
const { name, type, info } = variable;
97+
98+
if (type === 'Context') {
99+
100+
// TODO: We can't resolve Context variables yet
101+
return { ...acc, [name]: {} };
102+
}
103+
104+
if (!type && !info && source.startsWith('=')) {
105+
106+
// TODO: Remove when https://github.com/bpmn-io/variable-resolver/issues/52 is fixed.
107+
if (source === '=false') {
108+
return acc;
109+
}
110+
111+
return { ...acc, [source.slice(1)]: '' };
112+
}
113+
114+
return acc;
115+
}, {});
116+
117+
const nestedValues = parseNestedValues(values);
118+
119+
return JSON.stringify(nestedValues, null, 2);
103120
}
104121

105-
function isFeelConstant(value) {
122+
/**
123+
* Convert flat object with dot notation to nested object.
124+
*
125+
* Example:
126+
*
127+
* `{
128+
* 'foo.bar': 'baz'
129+
* }`
130+
* becomes:
131+
* `{
132+
* foo: {
133+
* bar: 'baz'
134+
* }
135+
* }`
136+
*/
137+
function parseNestedValues(values) {
106138

107-
if (!isNaN(parseInt(value))) {
108-
return true;
109-
}
139+
const nestedValues = Object.entries(values).reduce((acc, [ key, value ]) => {
140+
const keys = key.split('.');
141+
let current = acc;
142+
143+
for (let i = 0; i < keys.length - 1; i++) {
144+
const keyPart = keys[i];
145+
if (!(keyPart in current)) {
146+
current[keyPart] = {};
147+
}
148+
current = current[keyPart];
149+
}
110150

111-
if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
112-
return true;
113-
}
151+
current[keys[keys.length - 1]] = value;
152+
return acc;
153+
}, {});
114154

115-
return false;
155+
return nestedValues;
116156
}

0 commit comments

Comments
 (0)