Feature/editor cursor jumping (#485)

* To avoid the cursor jumps in the Monoco editor, never use "value=xxx" in conjuntion with "onChange".  Use defaultValue instead, and make sure that the editor is rerendered when that value is changed.

* linting
This commit is contained in:
Dan Funk 2023-09-12 16:21:18 -04:00 committed by GitHub
parent b432f22fe0
commit e95b3b03f3
2 changed files with 95 additions and 29 deletions

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState, useRef } from 'react';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import merge from 'lodash/merge'; import merge from 'lodash/merge';
@ -37,6 +37,7 @@ export default function ReactFormBuilder({
const DATA_EXTENSION = '-exampledata.json'; const DATA_EXTENSION = '-exampledata.json';
const [fetchFailed, setFetchFailed] = useState<boolean>(false); const [fetchFailed, setFetchFailed] = useState<boolean>(false);
const [ready, setReady] = useState<boolean>(false);
const [strSchema, setStrSchema] = useState<string>(''); const [strSchema, setStrSchema] = useState<string>('');
const [debouncedStrSchema] = useDebounce(strSchema, 500); const [debouncedStrSchema] = useDebounce(strSchema, 500);
@ -55,6 +56,24 @@ export default function ReactFormBuilder({
const [baseFileName, setBaseFileName] = useState<string>(''); const [baseFileName, setBaseFileName] = useState<string>('');
const [newFileName, setNewFileName] = useState<string>(''); const [newFileName, setNewFileName] = useState<string>('');
/**
* This section gives us direct pointers to the monoco editors so that
* we can update their values. Using state variables directly on the monoco editor
* causes the cursor to jump to the bottom if two letters are pressed simultaneously.
*/
const schemaEditorRef = useRef(null);
const uiEditorRef = useRef(null);
const dataEditorRef = useRef(null);
function handleSchemaEditorDidMount(editor: any) {
schemaEditorRef.current = editor;
}
function handleUiEditorDidMount(editor: any) {
uiEditorRef.current = editor;
}
function handleDataEditorDidMount(editor: any) {
dataEditorRef.current = editor;
}
const saveFile = useCallback( const saveFile = useCallback(
(file: File, create: boolean = false) => { (file: File, create: boolean = false) => {
let httpMethod = 'PUT'; let httpMethod = 'PUT';
@ -88,7 +107,16 @@ export default function ReactFormBuilder({
}; };
const isReady = () => { const isReady = () => {
return strSchema !== '' && strUI !== '' && strFormData !== ''; // Use a ready flag so that we still allow people to completely delete
// the schema, ui or data if they want to clear it out.
if (ready) {
return true;
}
if (strSchema !== '' && strUI !== '' && strFormData !== '') {
setReady(true);
return true;
}
return false;
}; };
// Auto save schema changes // Auto save schema changes
@ -114,7 +142,7 @@ export default function ReactFormBuilder({
useEffect(() => { useEffect(() => {
/** /**
* we need to run the schema and ui through a backend call before rendering the form * we need to run the schema and ui through a backend call before rendering the form,
* so it can handle certain server side changes, such as jinja rendering and populating dropdowns, etc. * so it can handle certain server side changes, such as jinja rendering and populating dropdowns, etc.
*/ */
const url: string = '/tasks/prepare-form'; const url: string = '/tasks/prepare-form';
@ -173,6 +201,51 @@ export default function ReactFormBuilder({
setSelectedIndex(evt.selectedIndex); setSelectedIndex(evt.selectedIndex);
}; };
const updateStrSchema = (value: string) => {
if (schemaEditorRef && schemaEditorRef.current) {
// @ts-ignore
schemaEditorRef.current.setValue(value);
}
};
const updateStrUi = (value: string) => {
if (uiEditorRef && uiEditorRef.current) {
// @ts-ignore
uiEditorRef.current.setValue(value);
}
};
const updateStrData = (value: string) => {
// Only update the data if it is different from what is already there, this prevents
// cursor from jumping to the top each time you type a letter.
if (
dataEditorRef &&
dataEditorRef.current &&
// @ts-ignore
value !== dataEditorRef.current.getValue()
) {
// @ts-ignore
dataEditorRef.current.setValue(value);
}
};
function updateData(newData: object) {
setFormData(newData);
const newDataStr = JSON.stringify(newData, null, 2);
if (newDataStr !== strFormData) {
updateStrData(newDataStr);
}
}
function updateDataFromStr(newDataStr: string) {
try {
const newData = JSON.parse(newDataStr);
setFormData(newData);
} catch (e) {
/* empty */
}
}
function setJsonSchemaFromResponseJson(result: any) { function setJsonSchemaFromResponseJson(result: any) {
setStrSchema(result.file_contents); setStrSchema(result.file_contents);
} }
@ -232,29 +305,13 @@ export default function ReactFormBuilder({
function insertFields(schema: any, ui: any, data: any) { function insertFields(schema: any, ui: any, data: any) {
setFormData(merge(formData, data)); setFormData(merge(formData, data));
setStrFormData(JSON.stringify(formData, null, 2)); updateStrData(JSON.stringify(formData, null, 2));
const tempSchema = merge(JSON.parse(strSchema), schema); const tempSchema = merge(JSON.parse(strSchema), schema);
setStrSchema(JSON.stringify(tempSchema, null, 2)); updateStrSchema(JSON.stringify(tempSchema, null, 2));
const tempUI = merge(JSON.parse(strUI), ui); const tempUI = merge(JSON.parse(strUI), ui);
setStrUI(JSON.stringify(tempUI, null, 2)); updateStrUi(JSON.stringify(tempUI, null, 2));
}
function updateData(newData: object) {
setFormData(newData);
const newDataStr = JSON.stringify(newData, null, 2);
if (newDataStr !== strFormData) {
setStrFormData(newDataStr);
}
}
function updateDataFromStr(newDataStr: string) {
try {
const newData = JSON.parse(newDataStr);
setFormData(newData);
} catch (e) {
/* empty */
}
} }
if (!isReady()) { if (!isReady()) {
@ -340,8 +397,9 @@ export default function ReactFormBuilder({
height={600} height={600}
width="auto" width="auto"
defaultLanguage="json" defaultLanguage="json"
value={strSchema} defaultValue={strSchema}
onChange={(value) => setStrSchema(value || '')} onChange={(value) => setStrSchema(value || '')}
onMount={handleSchemaEditorDidMount}
/> />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
@ -359,8 +417,9 @@ export default function ReactFormBuilder({
height={600} height={600}
width="auto" width="auto"
defaultLanguage="json" defaultLanguage="json"
value={strUI} defaultValue={strUI}
onChange={(value) => setStrUI(value || '')} onChange={(value) => setStrUI(value || '')}
onMount={handleUiEditorDidMount}
/> />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
@ -372,8 +431,9 @@ export default function ReactFormBuilder({
height={600} height={600}
width="auto" width="auto"
defaultLanguage="json" defaultLanguage="json"
value={strFormData} defaultValue={strFormData}
onChange={(value: any) => updateDataFromStr(value || '')} onChange={(value: any) => updateDataFromStr(value || '')}
onMount={handleDataEditorDidMount}
/> />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>

View File

@ -432,7 +432,7 @@ export default function ProcessModelEditDiagram() {
const unitTestModdleElements = extensionElements const unitTestModdleElements = extensionElements
.get('values') .get('values')
.filter(function getInstanceOfType(e: any) { .filter(function getInstanceOfType(e: any) {
return e.$instanceOf('spiffworkflow:unitTests'); return e.$instanceOf('spiffworkflow:UnitTests');
})[0]; })[0];
if (unitTestModdleElements) { if (unitTestModdleElements) {
return unitTestModdleElements.unitTests; return unitTestModdleElements.unitTests;
@ -782,7 +782,7 @@ export default function ProcessModelEditDiagram() {
width="auto" width="auto"
defaultLanguage="json" defaultLanguage="json"
options={Object.assign(jsonEditorOptions(), {})} options={Object.assign(jsonEditorOptions(), {})}
value={inputJson} defaultValue={inputJson}
onChange={handleEditorScriptTestUnitInputChange} onChange={handleEditorScriptTestUnitInputChange}
/> />
</div> </div>
@ -795,7 +795,7 @@ export default function ProcessModelEditDiagram() {
width="auto" width="auto"
defaultLanguage="json" defaultLanguage="json"
options={Object.assign(jsonEditorOptions(), {})} options={Object.assign(jsonEditorOptions(), {})}
value={outputJson} defaultValue={outputJson}
onChange={handleEditorScriptTestUnitOutputChange} onChange={handleEditorScriptTestUnitOutputChange}
/> />
</div> </div>
@ -807,19 +807,25 @@ export default function ProcessModelEditDiagram() {
return null; return null;
}; };
const scriptEditor = () => { const scriptEditor = () => {
if (!showScriptEditor) {
return null;
}
return ( return (
<Editor <Editor
height={500} height={500}
width="auto" width="auto"
options={generalEditorOptions()} options={generalEditorOptions()}
defaultLanguage="python" defaultLanguage="python"
value={scriptText} defaultValue={scriptText}
onChange={handleEditorScriptChange} onChange={handleEditorScriptChange}
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
/> />
); );
}; };
const scriptEditorAndTests = () => { const scriptEditorAndTests = () => {
if (!showScriptEditor) {
return null;
}
let scriptName = ''; let scriptName = '';
if (scriptElement) { if (scriptElement) {
scriptName = (scriptElement as any).di.bpmnElement.name; scriptName = (scriptElement as any).di.bpmnElement.name;