From 46d8bab23abe1c428b8dbd594b2eef27b70397cb Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Mon, 25 Mar 2024 11:47:44 -0700 Subject: [PATCH 1/5] Migrate save/launch buttons; improve save disabled/enabled state Signed-off-by: Tyler Ohlsen --- .../workflow_detail/components/header.tsx | 18 ++------ public/pages/workflow_detail/utils/utils.ts | 4 +- .../pages/workflow_detail/workflow_detail.tsx | 16 ++----- .../workspace/resizable_workspace.tsx | 46 +++++++++++++++++-- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx index 2b99e57e..94b63195 100644 --- a/public/pages/workflow_detail/components/header.tsx +++ b/public/pages/workflow_detail/components/header.tsx @@ -33,21 +33,9 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) { ) } rightSideItems={[ - // TODO: add launch logic - {}}> - Launch - , - { - // @ts-ignore - saveWorkflow(props.workflow, reactFlowInstance); - dispatch(removeDirty()); - }} - > - Save + // TODO: finalize if this is needed + {}}> + Delete , ]} tabs={props.tabs} diff --git a/public/pages/workflow_detail/utils/utils.ts b/public/pages/workflow_detail/utils/utils.ts index f536ab25..85f5ce07 100644 --- a/public/pages/workflow_detail/utils/utils.ts +++ b/public/pages/workflow_detail/utils/utils.ts @@ -11,7 +11,7 @@ import { validateWorkspaceFlow, } from '../../../../common'; -export function saveWorkflow(workflow: Workflow, rfInstance: any): void { +export function saveWorkflow(rfInstance: any, workflow?: Workflow): void { let curFlowState = rfInstance.toObject(); curFlowState = { @@ -26,7 +26,7 @@ export function saveWorkflow(workflow: Workflow, rfInstance: any): void { workspaceFlowState: curFlowState, workflows: toTemplateFlows(curFlowState), } as Workflow; - if (workflow.id) { + if (workflow && workflow.id) { // TODO: implement connection to update workflow API } else { // TODO: implement connection to create workflow API diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx index 38e15f2e..40b11474 100644 --- a/public/pages/workflow_detail/workflow_detail.tsx +++ b/public/pages/workflow_detail/workflow_detail.tsx @@ -98,22 +98,11 @@ export function WorkflowDetail(props: WorkflowDetailProps) { // On initial load: // - fetch workflow, if there is an existing workflow ID - // - add a window listener to warn users if they exit/refresh - // without saving latest changes useEffect(() => { if (!isNewWorkflow) { // TODO: can optimize to only fetch a single workflow dispatch(searchWorkflows({ query: { match_all: {} } })); } - - // TODO: below has the following issue: - // 1. user starts to create new unsaved workflow changes - // 2. user navigates to other parts of the plugin without refreshing - no warning happens - // 3. user refreshes at any later time: if isDirty is still true, shows browser warning - // tune to only handle the check if still on the workflow details page, or consider adding a check / warning - // if navigating away from the details page without refreshing (where it is currently not being triggered) - // window.onbeforeunload = (e) => - // isDirty || isNewWorkflow ? true : undefined; }, []); const tabs = [ @@ -156,7 +145,10 @@ export function WorkflowDetail(props: WorkflowDetailProps) { tabs={tabs} /> {selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && ( - + )} {selectedTabId === WORKFLOW_DETAILS_TAB.LAUNCHES && } {selectedTabId === WORKFLOW_DETAILS_TAB.PROTOTYPE && ( diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/workspace/resizable_workspace.tsx index 983e36f0..e6bf7620 100644 --- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx +++ b/public/pages/workflow_detail/workspace/resizable_workspace.tsx @@ -4,11 +4,12 @@ */ import React, { useRef, useState, useEffect, useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { useOnSelectionChange } from 'reactflow'; import { Form, Formik } from 'formik'; import * as yup from 'yup'; import { cloneDeep } from 'lodash'; -import { EuiButton, EuiResizableContainer } from '@elastic/eui'; +import { EuiButton, EuiPageHeader, EuiResizableContainer } from '@elastic/eui'; import { Workflow, WorkspaceFormValues, @@ -18,11 +19,13 @@ import { componentDataToFormik, getComponentSchema, } from '../../../../common'; -import { rfContext } from '../../../store'; +import { AppState, removeDirty, rfContext } from '../../../store'; import { Workspace } from './workspace'; import { ComponentDetails } from '../component_details'; +import { saveWorkflow } from '../utils'; interface ResizableWorkspaceProps { + isNewWorkflow: boolean; workflow?: Workflow; } @@ -33,6 +36,13 @@ const COMPONENT_DETAILS_PANEL_ID = 'component_details_panel_id'; * panels - the ReactFlow workspace panel and the selected component details panel. */ export function ResizableWorkspace(props: ResizableWorkspaceProps) { + const dispatch = useDispatch(); + + // Overall workspace state + const isDirty = useSelector((state: AppState) => state.workspace.isDirty); + const [isFirstSave, setIsFirstSave] = useState(props.isNewWorkflow); + const isSaveable = isFirstSave ? true : isDirty; + // Component details side panel state const [isDetailsPanelOpen, setisDetailsPanelOpen] = useState(true); const collapseFn = useRef( @@ -139,9 +149,36 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { > {(formikProps) => (
+ {}}> + Launch + , + { + // @ts-ignore + saveWorkflow(reactFlowInstance, props.workflow); + dispatch(removeDirty()); + if (isFirstSave) { + setIsFirstSave(false); + } + }} + > + Save + , + ]} + bottomBorder={false} + /> {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { if (togglePanel) { @@ -153,7 +190,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { <> @@ -164,6 +201,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { Date: Mon, 25 Mar 2024 17:01:01 -0700 Subject: [PATCH 2/5] Propagate form change to parent save button Signed-off-by: Tyler Ohlsen --- .../component_details/component_details.tsx | 6 +++++- .../component_details/component_inputs.tsx | 6 +++++- .../component_details/input_field_list.tsx | 3 +++ .../input_fields/select_field.tsx | 2 ++ .../input_fields/text_field.tsx | 3 +++ .../workspace/resizable_workspace.tsx | 19 ++++++++++++++++--- public/pages/workflows/workflows.tsx | 12 +++--------- 7 files changed, 37 insertions(+), 14 deletions(-) diff --git a/public/pages/workflow_detail/component_details/component_details.tsx b/public/pages/workflow_detail/component_details/component_details.tsx index d106092f..7ecaed6e 100644 --- a/public/pages/workflow_detail/component_details/component_details.tsx +++ b/public/pages/workflow_detail/component_details/component_details.tsx @@ -13,6 +13,7 @@ import { EmptyComponentInputs } from './empty_component_inputs'; import '../workspace/workspace-styles.scss'; interface ComponentDetailsProps { + onFormChange: () => void; selectedComponent?: ReactFlowComponent; } @@ -31,7 +32,10 @@ export function ComponentDetails(props: ComponentDetailsProps) { {props.selectedComponent ? ( - + ) : ( )} diff --git a/public/pages/workflow_detail/component_details/component_inputs.tsx b/public/pages/workflow_detail/component_details/component_inputs.tsx index bed3ad72..cae794df 100644 --- a/public/pages/workflow_detail/component_details/component_inputs.tsx +++ b/public/pages/workflow_detail/component_details/component_inputs.tsx @@ -10,6 +10,7 @@ import { ReactFlowComponent } from '../../../../common'; interface ComponentInputsProps { selectedComponent: ReactFlowComponent; + onFormChange: () => void; } export function ComponentInputs(props: ComponentInputsProps) { @@ -19,7 +20,10 @@ export function ComponentInputs(props: ComponentInputsProps) {

{props.selectedComponent.data.label || ''}

- + ); } diff --git a/public/pages/workflow_detail/component_details/input_field_list.tsx b/public/pages/workflow_detail/component_details/input_field_list.tsx index ced08c14..f7aed9d8 100644 --- a/public/pages/workflow_detail/component_details/input_field_list.tsx +++ b/public/pages/workflow_detail/component_details/input_field_list.tsx @@ -15,6 +15,7 @@ import { ReactFlowComponent } from '../../../../common'; interface InputFieldListProps { selectedComponent: ReactFlowComponent; + onFormChange: () => void; } export function InputFieldList(props: InputFieldListProps) { @@ -30,6 +31,7 @@ export function InputFieldList(props: InputFieldListProps) {
@@ -42,6 +44,7 @@ export function InputFieldList(props: InputFieldListProps) { ); diff --git a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx index 95d83772..b95d515e 100644 --- a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx @@ -36,6 +36,7 @@ const existingIndices = [ interface SelectFieldProps { field: IComponentField; componentId: string; + onFormChange: () => void; } /** @@ -59,6 +60,7 @@ export function SelectField(props: SelectFieldProps) { field.onChange(option); form.setFieldValue(formField, option); }} + onBlur={() => props.onFormChange()} isInvalid={isFieldInvalid( props.componentId, props.field.name, diff --git a/public/pages/workflow_detail/component_details/input_fields/text_field.tsx b/public/pages/workflow_detail/component_details/input_fields/text_field.tsx index fec071ac..a510f691 100644 --- a/public/pages/workflow_detail/component_details/input_fields/text_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/text_field.tsx @@ -17,6 +17,7 @@ import { interface TextFieldProps { field: IComponentField; componentId: string; + onFormChange: () => void; } /** @@ -46,6 +47,8 @@ export function TextField(props: TextFieldProps) { placeholder={props.field.placeholder || ''} compressed={false} value={field.value || getInitialValue(props.field.type)} + onChange={(e) => form.setFieldValue(formField, e.target.value)} + onBlur={() => props.onFormChange()} /> ); diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/workspace/resizable_workspace.tsx index e6bf7620..8aadf81b 100644 --- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx +++ b/public/pages/workflow_detail/workspace/resizable_workspace.tsx @@ -6,7 +6,7 @@ import React, { useRef, useState, useEffect, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useOnSelectionChange } from 'reactflow'; -import { Form, Formik } from 'formik'; +import { Form, Formik, useFormikContext } from 'formik'; import * as yup from 'yup'; import { cloneDeep } from 'lodash'; import { EuiButton, EuiPageHeader, EuiResizableContainer } from '@elastic/eui'; @@ -19,7 +19,7 @@ import { componentDataToFormik, getComponentSchema, } from '../../../../common'; -import { AppState, removeDirty, rfContext } from '../../../store'; +import { AppState, removeDirty, setDirty, rfContext } from '../../../store'; import { Workspace } from './workspace'; import { ComponentDetails } from '../component_details'; import { saveWorkflow } from '../utils'; @@ -139,6 +139,16 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { setFormSchema(updatedSchema); } + /** + * Function to pass down to the Formik components as a listener to propagate + * form changes to this parent component to re-enable save button, etc. + */ + function onFormChange() { + if (!isDirty) { + dispatch(setDirty()); + } + } + return ( onToggleChange()} > - +
); diff --git a/public/pages/workflows/workflows.tsx b/public/pages/workflows/workflows.tsx index 64f5f3f2..b745d888 100644 --- a/public/pages/workflows/workflows.tsx +++ b/public/pages/workflows/workflows.tsx @@ -58,20 +58,14 @@ export function Workflows(props: WorkflowsProps) { ] as WORKFLOWS_TAB; const [selectedTabId, setSelectedTabId] = useState(tabFromUrl); - // If there is no selected tab or invalid tab, default to a tab depending - // on if user has existing created workflows or not. + // If there is no selected tab or invalid tab, default to manage tab useEffect(() => { if ( !selectedTabId || !Object.values(WORKFLOWS_TAB).includes(selectedTabId) ) { - if (Object.keys(workflows).length > 0) { - setSelectedTabId(WORKFLOWS_TAB.MANAGE); - replaceActiveTab(WORKFLOWS_TAB.MANAGE, props); - } else { - setSelectedTabId(WORKFLOWS_TAB.CREATE); - replaceActiveTab(WORKFLOWS_TAB.CREATE, props); - } + setSelectedTabId(WORKFLOWS_TAB.MANAGE); + replaceActiveTab(WORKFLOWS_TAB.MANAGE, props); } }, [selectedTabId, workflows]); From 153b0e764a71536f7c09500740d467d54a41920c Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 26 Mar 2024 11:56:18 -0700 Subject: [PATCH 3/5] Add full form validation and flow validation + callouts Signed-off-by: Tyler Ohlsen --- common/utils.ts | 1 + .../input_fields/select_field.tsx | 3 +- public/pages/workflow_detail/utils/utils.ts | 37 ++---- .../workspace/resizable_workspace.tsx | 114 +++++++++++++++--- .../workflow_detail/workspace/workspace.tsx | 15 +-- 5 files changed, 110 insertions(+), 60 deletions(-) diff --git a/common/utils.ts b/common/utils.ts index fabdb327..45c7002b 100644 --- a/common/utils.ts +++ b/common/utils.ts @@ -74,6 +74,7 @@ export function toWorkspaceFlow( /** * Validates the UI workflow state. * Note we don't have to validate connections since that is done via input/output handlers. + * But we need to validate there are no open connections */ export function validateWorkspaceFlow( workspaceFlow: WorkspaceFlowState diff --git a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx index b95d515e..f76567a8 100644 --- a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx @@ -57,10 +57,9 @@ export function SelectField(props: SelectFieldProps) { options={options} valueOfSelected={field.value || getInitialValue(props.field.type)} onChange={(option) => { - field.onChange(option); form.setFieldValue(formField, option); + props.onFormChange(); }} - onBlur={() => props.onFormChange()} isInvalid={isFieldInvalid( props.componentId, props.field.name, diff --git a/public/pages/workflow_detail/utils/utils.ts b/public/pages/workflow_detail/utils/utils.ts index 85f5ce07..b3037a1c 100644 --- a/public/pages/workflow_detail/utils/utils.ts +++ b/public/pages/workflow_detail/utils/utils.ts @@ -3,41 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - WorkspaceFlowState, - Workflow, - ReactFlowComponent, - toTemplateFlows, - validateWorkspaceFlow, -} from '../../../../common'; +import { Workflow, ReactFlowComponent } from '../../../../common'; -export function saveWorkflow(rfInstance: any, workflow?: Workflow): void { - let curFlowState = rfInstance.toObject(); - - curFlowState = { - ...curFlowState, - nodes: processNodes(curFlowState.nodes), - }; - - const isValid = validateWorkspaceFlow(curFlowState); - if (isValid) { - const updatedWorkflow = { - ...workflow, - workspaceFlowState: curFlowState, - workflows: toTemplateFlows(curFlowState), - } as Workflow; - if (workflow && workflow.id) { - // TODO: implement connection to update workflow API - } else { - // TODO: implement connection to create workflow API - } +export function saveWorkflow(workflow?: Workflow): void { + if (workflow && workflow.id) { + // TODO: implement connection to update workflow API } else { - return; + // TODO: implement connection to create workflow API } } // Process the raw ReactFlow nodes to only persist the fields we need -function processNodes(nodes: ReactFlowComponent[]): ReactFlowComponent[] { +export function processNodes( + nodes: ReactFlowComponent[] +): ReactFlowComponent[] { return nodes .map((node: ReactFlowComponent) => { return Object.fromEntries( diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/workspace/resizable_workspace.tsx index 8aadf81b..43bc5d5b 100644 --- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx +++ b/public/pages/workflow_detail/workspace/resizable_workspace.tsx @@ -6,10 +6,15 @@ import React, { useRef, useState, useEffect, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useOnSelectionChange } from 'reactflow'; -import { Form, Formik, useFormikContext } from 'formik'; +import { Form, Formik } from 'formik'; import * as yup from 'yup'; import { cloneDeep } from 'lodash'; -import { EuiButton, EuiPageHeader, EuiResizableContainer } from '@elastic/eui'; +import { + EuiButton, + EuiCallOut, + EuiPageHeader, + EuiResizableContainer, +} from '@elastic/eui'; import { Workflow, WorkspaceFormValues, @@ -18,11 +23,15 @@ import { WorkspaceSchemaObj, componentDataToFormik, getComponentSchema, + toWorkspaceFlow, + validateWorkspaceFlow, + WorkspaceFlowState, + toTemplateFlows, } from '../../../../common'; import { AppState, removeDirty, setDirty, rfContext } from '../../../store'; import { Workspace } from './workspace'; import { ComponentDetails } from '../component_details'; -import { saveWorkflow } from '../utils'; +import { processNodes, saveWorkflow } from '../utils'; interface ResizableWorkspaceProps { isNewWorkflow: boolean; @@ -43,6 +52,20 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { const [isFirstSave, setIsFirstSave] = useState(props.isNewWorkflow); const isSaveable = isFirstSave ? true : isDirty; + // Workflow state + const [workflow, setWorkflow] = useState( + props.workflow + ); + + // Formik form state + const [formValues, setFormValues] = useState({}); + const [formSchema, setFormSchema] = useState(yup.object({})); + + // Validation states. Maintain separate state for form vs. overall flow so + // we can have fine-grained errors and action items for users + const [formValidOnSubmit, setFormValidOnSubmit] = useState(true); + const [flowValidOnSubmit, setFlowValidOnSubmit] = useState(true); + // Component details side panel state const [isDetailsPanelOpen, setisDetailsPanelOpen] = useState(true); const collapseFn = useRef( @@ -78,6 +101,24 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { }, }); + // Hook to update the workflow's flow state, if applicable. It may not exist if + // it is a backend-only-created workflow, or a new, unsaved workflow + useEffect(() => { + const workflowCopy = { ...props.workflow } as Workflow; + if (workflowCopy) { + if (!workflowCopy.workspaceFlowState) { + workflowCopy.workspaceFlowState = toWorkspaceFlow( + workflowCopy.workflows + ); + console.debug( + `There is no saved UI flow for workflow: ${workflowCopy.name}. Generating a default one.` + ); + } + setWorkflow(workflowCopy); + } + }, [props.workflow]); + + // Hook to updated the selected ReactFlow component useEffect(() => { reactFlowInstance?.setNodes((nodes: ReactFlowComponent[]) => nodes.map((node) => { @@ -90,16 +131,12 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { ); }, [selectedComponent]); - // Formik form state - const [formValues, setFormValues] = useState({}); - const [formSchema, setFormSchema] = useState(yup.object({})); - // Initialize the form state to an existing workflow, if applicable. useEffect(() => { - if (props.workflow?.workspaceFlowState) { + if (workflow?.workspaceFlowState) { const initFormValues = {} as WorkspaceFormValues; const initSchemaObj = {} as WorkspaceSchemaObj; - props.workflow.workspaceFlowState.nodes.forEach((node) => { + workflow.workspaceFlowState.nodes.forEach((node) => { initFormValues[node.id] = componentDataToFormik(node.data); initSchemaObj[node.id] = getComponentSchema(node.data); }); @@ -107,7 +144,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { setFormValues(initFormValues); setFormSchema(initFormSchema); } - }, [props.workflow]); + }, [workflow]); // Update the form values and validation schema when a node is added // or removed from the workspace. @@ -159,6 +196,27 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { > {(formikProps) => ( + {!formValidOnSubmit && ( + + Please address the highlighted fields and try saving again. + + )} + {!flowValidOnSubmit && ( + + Please ensure there are no open connections between the + components. + + )} { - // @ts-ignore - saveWorkflow(reactFlowInstance, props.workflow); dispatch(removeDirty()); if (isFirstSave) { setIsFirstSave(false); } + // Submit the form to bubble up any errors. + // Ideally we handle Promise accept/rejects with submitForm(), but there is + // open issues for that - see https://github.com/jaredpalmer/formik/issues/2057 + // The workaround is to additionally execute validateForm() which will return any errors found. + formikProps.submitForm(); + formikProps.validateForm().then((validationResults: {}) => { + if (Object.keys(validationResults).length > 0) { + setFormValidOnSubmit(false); + } else { + setFormValidOnSubmit(true); + // @ts-ignore + let curFlowState = reactFlowInstance.toObject() as WorkspaceFlowState; + curFlowState = { + ...curFlowState, + nodes: processNodes(curFlowState.nodes), + }; + if (validateWorkspaceFlow(curFlowState)) { + setFlowValidOnSubmit(true); + const updatedWorkflow = { + ...workflow, + workspaceFlowState: curFlowState, + workflows: toTemplateFlows(curFlowState), + } as Workflow; + saveWorkflow(updatedWorkflow); + } else { + setFlowValidOnSubmit(false); + } + } + }); }} > Save @@ -205,7 +290,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { paddingSize="s" > @@ -228,9 +313,6 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { ); }}
- formikProps.handleSubmit()}> - Submit - )} diff --git a/public/pages/workflow_detail/workspace/workspace.tsx b/public/pages/workflow_detail/workspace/workspace.tsx index 6fb9f5b4..a8ee3c8d 100644 --- a/public/pages/workflow_detail/workspace/workspace.tsx +++ b/public/pages/workflow_detail/workspace/workspace.tsx @@ -21,7 +21,6 @@ import { IComponentData, ReactFlowComponent, Workflow, - toWorkspaceFlow, } from '../../../../common'; import { generateId, initComponentData } from '../../../utils'; import { WorkspaceComponent } from '../workspace_component'; @@ -116,20 +115,10 @@ export function Workspace(props: WorkspaceProps) { [reactFlowInstance] ); - // Initialization. Set the nodes and edges to an existing workflow, - // if applicable. + // Initialization. Set the nodes and edges to an existing workflow state, useEffect(() => { const workflow = { ...props.workflow }; - if (workflow) { - if (!workflow.workspaceFlowState) { - // No existing workspace state. This could be due to it being a backend-only-created - // workflow, or a new, unsaved workflow - // @ts-ignore - workflow.workspaceFlowState = toWorkspaceFlow(workflow.workflows); - console.debug( - `There is no saved UI flow for workflow: ${workflow.name}. Generating a default one.` - ); - } + if (workflow && workflow.workspaceFlowState) { setNodes(workflow.workspaceFlowState.nodes); setEdges(workflow.workspaceFlowState.edges); } From 6972f0bd53a4b3c311224b769d17b19bb2b2144b Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 26 Mar 2024 12:26:37 -0700 Subject: [PATCH 4/5] Add few comments Signed-off-by: Tyler Ohlsen --- .../component_details/input_fields/text_field.tsx | 4 ++++ .../pages/workflow_detail/workspace/resizable_workspace.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/pages/workflow_detail/component_details/input_fields/text_field.tsx b/public/pages/workflow_detail/component_details/input_fields/text_field.tsx index a510f691..d3866a7e 100644 --- a/public/pages/workflow_detail/component_details/input_fields/text_field.tsx +++ b/public/pages/workflow_detail/component_details/input_fields/text_field.tsx @@ -48,6 +48,10 @@ export function TextField(props: TextFieldProps) { compressed={false} value={field.value || getInitialValue(props.field.type)} onChange={(e) => form.setFieldValue(formField, e.target.value)} + // This is a design decision to only trigger form updates onBlur() instead + // of onChange(). This is to rate limit the number of updates & re-renders made, as users + // typically rapidly type things into a text box, which would consequently trigger + // onChange() much more often. onBlur={() => props.onFormChange()} /> diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/workspace/resizable_workspace.tsx index 43bc5d5b..b827fdc9 100644 --- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx +++ b/public/pages/workflow_detail/workspace/resizable_workspace.tsx @@ -102,7 +102,8 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) { }); // Hook to update the workflow's flow state, if applicable. It may not exist if - // it is a backend-only-created workflow, or a new, unsaved workflow + // it is a backend-only-created workflow, or a new, unsaved workflow. If so, + // generate a default one based on the 'workflows' JSON field. useEffect(() => { const workflowCopy = { ...props.workflow } as Workflow; if (workflowCopy) { From 439b05f8a55a8db9c17dc79bfe51e3e87edc1df7 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Tue, 26 Mar 2024 12:28:41 -0700 Subject: [PATCH 5/5] Fix UT Signed-off-by: Tyler Ohlsen --- public/pages/workflows/workflows.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/pages/workflows/workflows.test.tsx b/public/pages/workflows/workflows.test.tsx index 3c12244c..cdf14c57 100644 --- a/public/pages/workflows/workflows.test.tsx +++ b/public/pages/workflows/workflows.test.tsx @@ -40,7 +40,7 @@ const renderWithRouter = () => ({ describe('Workflows', () => { test('renders the page', () => { - const { getByText } = renderWithRouter(); - expect(getByText('Workflows')).not.toBeNull(); + const { getAllByText } = renderWithRouter(); + expect(getAllByText('Workflows').length).toBeGreaterThan(0); }); });