Skip to content

Commit

Permalink
feat(editor): use a fancy editor for fallback hop editing
Browse files Browse the repository at this point in the history
  • Loading branch information
sabberworm committed Oct 10, 2024
1 parent aef18f2 commit 83ee303
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 52 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@uidotdev/usehooks": "^2.4.1",
"concurrently": "^9.0.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
Expand Down
6 changes: 6 additions & 0 deletions src/main/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ const RootElement = styled('div')`
> .output {
grid-area: output;
}
label:not(coral-checkbox label) {
display: inline-flex;
align-items: baseline;
gap: 6px;
}
`;

export const App: FC<{ runEndpoint: string }> = props => {
Expand Down
11 changes: 5 additions & 6 deletions src/main/frontend/sections/RunControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, FormEvent, useContext, useId, useRef } from 'react';
import React, { FC, FormEvent, useContext, useRef } from 'react';

import { styled } from 'goober';
import { RunEndpointContext, ScriptContext } from '../App';
Expand Down Expand Up @@ -32,18 +32,17 @@ export const RunControls: FC<{ runWith: (data: FormData) => Promise<void> }> = (
await runWith(data);
}

const id = useId();

return (
<Elm className="run-controls" ref={formRef} method="POST" action={endpoint} encType="multipart/form-data" onSubmit={run}>
{script.parameters.length ? (
<fieldset>
<legend>Arguments</legend>
<div className="arguments">
{script.parameters.map(({ name, type }, i) => (
{script.parameters.map(({ name, type }) => (
<React.Fragment key={name}>
<label htmlFor={`${id}_${i}`}>{name}: </label>
<input id={`${id}_${i}`} is="coral-textfield" type={type} name={name} />
<label>
{name}:<input is="coral-textfield" type={type} name={name} />
</label>
</React.Fragment>
))}
</div>
Expand Down
38 changes: 11 additions & 27 deletions src/main/frontend/sections/editor/types/FallbackStep.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,32 @@
import React, { FC, useContext, useEffect, useRef, useState } from 'react';
import React, { FC, useContext } from 'react';

import { AnyHop, Hop } from '../../../model/hops';
import { StepEditor } from '../../../widgets/StepEditor';
import { ScriptContext } from '../../../App';
import { CodeEditor } from '../../../widgets/CodeEditor';

export const FallbackStep: FC<{ parentHops: Hop[]; hop: AnyHop }> = ({ parentHops, hop }) => {
const scriptContext = useContext(ScriptContext);

const { type: _, ...hopWithoutType } = hop;
const cleaned = JSON.stringify(hopWithoutType, null, ' ');

const [code, setCode] = useState(cleaned);
const updated = useRef(false);

useEffect(() => {
setCode(cleaned);
updated.current = false;
}, [hop]);
const { type: hopType, ...hopWithoutType } = hop;
const code = JSON.stringify(hopWithoutType, null, ' ');

return (
<StepEditor parentHops={parentHops} hop={hop} title={`Unknown hop type ${hop.type}`}>
<textarea
cols={120}
rows={10}
onInput={e => {
updated.current = true;
setCode(e.currentTarget.value);
}}
<StepEditor parentHops={parentHops} hop={hop} title={`Unknown Hop (${hopType})`}>
<CodeEditor
value={code}
onBlur={() => {
if (!updated.current) {
// No change since last set
changed={(value, hasError) => {
if (hasError) {
return;
}
try {
const editedSettings = JSON.parse(code);

Object.assign(hop, editedSettings, { type: hop.type });
Object.assign(hop, JSON.parse(value), { type: hopType });
scriptContext.commit();
} catch (e) {
// Not currently valid JSON
// Invalid JSON
}
}}
style={{ fontFamily: 'monospace' }}
language="json"
/>
</StepEditor>
);
Expand Down
59 changes: 59 additions & 0 deletions src/main/frontend/widgets/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useEffect, useId, useRef, useState } from 'react';

import { useDebounce } from '@uidotdev/usehooks';

import { Editor } from '@monaco-editor/react';
import type * as monaco from 'monaco-editor';

export const CodeEditor: React.FC<{
value: string;
changed(val: string, hasErrors: boolean): void;
language: 'json' | 'groovy' | 'jexl';
}> = ({ value: outsideValue, changed, language }) => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);

const hasError = useRef(false);
const [value, setValue] = useState(outsideValue);
const prevValue = useRef<string>(outsideValue);
const debouncedValue = useDebounce(prevValue.current, 600);

// Call the change listener when the debounced value changes
useEffect(() => {
if (outsideValue !== debouncedValue) {
changed(debouncedValue, hasError.current);
}
}, [debouncedValue, hasError.current]);

// Allow value updates from outside
useEffect(() => {
if (editorRef.current && outsideValue !== prevValue.current) {
prevValue.current = outsideValue;
editorRef.current.setValue(outsideValue);
}
}, [outsideValue]);

return (
<Editor
onMount={ed => (editorRef.current = ed)}
defaultValue={value}
defaultLanguage={language}
defaultPath={`inmemory://model/${useId()}.${language}`}
theme="light"
options={{
formatOnType: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}}
height={250}
onValidate={markers => {
hasError.current = markers.length > 0;
}}
onChange={val => {
prevValue.current = val ?? '';
setValue(val ?? '');
}}
/>
);
};
36 changes: 17 additions & 19 deletions src/main/frontend/widgets/StepEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,37 +72,36 @@ const Elm = styled('div')`
summary {
display: grid;
grid-template-columns: auto 1fr auto auto auto;
grid-template-columns: auto 1fr auto;
color: var(--contrast-color);
cursor: pointer;
font-size: 18px;
> * {
font-size: inherit;
}
&::-webkit-details-marker {
display: none;
}
&::before {
font-weight: 600;
display: inline-block;
align-self: center;
content: counter(steps);
color: var(--contrast-color);
min-width: 2em;
align-self: center;
text-align: center;
}
h3 {
display: flex;
flex-direction: column;
justify-content: center;
h2 {
display: inline-block;
font-weight: normal;
align-self: center;
padding: 2px 5px;
margin: 0;
overflow: hidden;
span {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
margin: 0;
text-overflow: ellipsis;
}
.menu {
Expand All @@ -114,8 +113,9 @@ const Elm = styled('div')`
}
.edit {
margin-left: 4px;
background-color: rgba(255, 255, 255, 0.9);
padding: 10px;
padding-right: 10%;
background: #ffffffce;
}
&[open] {
Expand Down Expand Up @@ -186,11 +186,9 @@ export const StepEditor: FC<{ parentHops: Hop[]; hop: AnyHop; title: string; chi

return (
<Elm className={`hop-config ${hop.type}`}>
<details>
<details open={false}>
<summary draggable="true">
<h3>
<span>{title}</span>
</h3>
<h2>{title}</h2>
<div className="menu">
<button
is="coral-button"
Expand Down

0 comments on commit 83ee303

Please sign in to comment.