Skip to content

[REF-1257] feat: enhance WorkflowRunForm with improved validation and file handling#2104

Merged
mrcfps merged 5 commits intomainfrom
fix/run-input-form
Jan 20, 2026
Merged

[REF-1257] feat: enhance WorkflowRunForm with improved validation and file handling#2104
mrcfps merged 5 commits intomainfrom
fix/run-input-form

Conversation

@anthhub
Copy link
Copy Markdown
Contributor

@anthhub anthhub commented Jan 20, 2026

  • Added new components for rendering form fields based on variable types, including string, option, and resource.
  • Implemented validation logic to ensure required fields are filled before submission.
  • Introduced file upload handling for resource type variables, including upload, removal, and refresh functionalities.
  • Enhanced user feedback for form validation errors and improved overall user experience with Tailwind CSS styling.
  • Utilized useMemo and useCallback for performance optimizations and to maintain referential equality in function props.

Summary by CodeRabbit

  • New Features

    • Redesigned workflow input form with per-field validation, required indicators, and template-driven variable rendering.
    • File upload and management for resource variables (upload, refresh, remove) with storage integration.
    • Pre-run checks for login, tool dependencies, and credit balance surfaced in the UI.
  • Bug Fixes

    • Improved validation flow and error messaging after attempted submits.
    • More robust error handling for file uploads and remote file creation.

✏️ Tip: You can customize this high-level summary in your review settings.

- Added new components for rendering form fields based on variable types, including string, option, and resource.
- Implemented validation logic to ensure required fields are filled before submission.
- Introduced file upload handling for resource type variables, including upload, removal, and refresh functionalities.
- Enhanced user feedback for form validation errors and improved overall user experience with Tailwind CSS styling.
- Utilized useMemo and useCallback for performance optimizations and to maintain referential equality in function props.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Refactors workflow-run-form to render an in-component Form, manage variable state/validation internally (including template-driven variables), add resource file upload flows (with optional DriveFile creation), and rework pre-run checks (login, tool dependencies, credits) and submission lifecycle.

Changes

Cohort / File(s) Summary
Form State & Validation
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx
Adds internal state (variableValues, templateVariables, attemptedSubmit, tooling UI flags), computed isFormDisabled/isFormValid, getInvalidFields/isFieldInvalid, and validation branching for template-driven vs form-driven submissions.
Form Rendering & Labels
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx
Replaces UserInputCollapse with direct Form rendering mapping workflowVariablesrenderFormField; introduces FormItemLabel and RequiredTagText, and integrates Input, Select, Typography, and styling updates.
File Upload / Resource Variables
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx
Integrates ResourceUpload, useFileUpload, getFileType, and proxiedRequest; implements handleFileUpload (uploads + optional DriveFile creation), handleFileRemove, handleRefreshFile, and stores storageKey/fileId in variable values.
Template Variable Handling
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx
Derives templateVariables from templateContent, renders only relevant variables, and adds conversion helpers and handleTemplateVariableChange for template ↔ variable synchronization.
Run Lifecycle & Pre-run Checks
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx
Introduces attemptedSubmit gating, inline validation replacing prior hook, login checks, ToolsDependencyChecker integration (_handleToolsDependencyOpenChange), credit checks, and adjusted run submission flow.
Cleanup & Misc
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx
Removes legacy prompt-check hook usage, adds memoization/utility converters, improves DriveFile error paths, and minor layout/translation tweaks.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Form as WorkflowRunForm
    participant Upload as ResourceUpload
    participant API as DriveFile API
    participant State as Local State

    User->>Form: Open form / interact
    Form->>Upload: Render ResourceUpload for resource variable
    User->>Upload: Select file
    Upload->>Upload: handleFileUpload()
    Upload->>API: Upload / create DriveFile (optional)
    API-->>Upload: fileId / response
    Upload->>State: Store storageKey and optional fileId
    State-->>Form: Update variable value
    Form->>User: Show file preview / controls
Loading
sequenceDiagram
    participant User
    participant Form as WorkflowRunForm
    participant Validator as Validation Logic
    participant Auth as Auth Service
    participant Tools as ToolsDependencyChecker
    participant Runner as Workflow Engine

    User->>Form: Click Run
    Form->>Form: set attemptedSubmit = true
    Form->>Validator: Validate (templateVariables or form fields)
    Validator-->>Form: Validation result
    alt Validation fails
        Form->>User: Display field errors
    else
        Form->>Auth: Check login status
        Auth-->>Form: Login state
        alt Not logged in
            Form->>User: Show login modal
        else
            Form->>Tools: Check tool dependencies
            Tools-->>Form: Dependencies OK / missing
            alt Missing dependencies
                Tools->>User: Show dependency UI
            else
                Form->>Runner: Execute workflow with converted variables
                Runner-->>Form: Execution result
                Form->>User: Show run status/result
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • CH1111
  • Siri-Ray
  • mrcfps

Poem

🐰 I hop through variables, tender and neat,

Files tuck in pockets and templates take seat,
I check for tools and whether you’re signed,
Then press the run — see the workflow unwind,
A carrot for coffee, the form looks complete.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: improved validation and file handling enhancements in WorkflowRunForm.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx`:
- Around line 222-244: The current validation in isFormValid can throw if value
isn't a string and treats unknown variable types as invalid while duplicating
logic in getInvalidFields; extract the per-variable validation into a shared
helper (e.g., validateVariable(variable, variableValues)) and use it from both
isFormValid and getInvalidFields, ensure the helper checks typeof value ===
'string' before calling trim for variable.variableType === 'string', handle
'option' and 'resource' as before, and for unrecognized variable.variableType
either return true or call a logger/warn then return true so new types don't
break validation.
- Around line 379-433: The handleFileUpload useCallback references
handleValueChange and form but they are missing from its dependency array;
update the dependency array for handleFileUpload to include handleValueChange
and form (in addition to uploadFile, variableValues, canvasId,
workflowVariables) so React's hook rules are satisfied—ensure handleValueChange
is memoized with useCallback where it's defined before adding it, and then add
both handleValueChange and form to the dependency list of handleFileUpload.
🧹 Nitpick comments (9)
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx (9)

58-65: Consider wrapping with React.memo.

Per coding guidelines, pure presentational components should be wrapped with React.memo to prevent unnecessary re-renders. Since RequiredTagText has no props and only uses a translation hook, it will re-render on every parent render.

Suggested improvement
-const RequiredTagText = () => {
+const RequiredTagText = React.memo(() => {
   const { t } = useTranslation();
   return (
     <div className="flex-shrink-0 text-[10px] text-refly-text-2 leading-[16px] px-1 border-[1px] border-solid border-refly-Card-Border rounded-[4px]">
       {t('canvas.workflow.variables.required') || 'Required'}
     </div>
   );
-};
+});

85-98: Wrap with React.memo and define explicit props type.

Per coding guidelines, pure components should be wrapped with React.memo. Also, props should be explicitly typed with an interface.

Suggested improvement
+interface FormItemLabelProps {
+  name: string;
+  required: boolean;
+}
+
-const FormItemLabel = ({ name, required }: { name: string; required: boolean }) => {
+const FormItemLabel = React.memo(({ name, required }: FormItemLabelProps) => {
   return (
     <div className="flex items-center gap-2 min-w-0">
       <Typography.Paragraph
         ellipsis={{ rows: 1, tooltip: <div className="max-h-[200px] overflow-y-auto">{name}</div> }}
         className="!m-0 text-xs font-semibold text-refly-text-0 leading-4 max-w-[100px]"
       >
         {name}
       </Typography.Paragraph>

       {required && <RequiredTagText />}
     </div>
   );
-};
+});

370-376: Wrap handleValueChange with useCallback.

Per coding guidelines, function props should use useCallback to maintain referential equality. This function is passed to form inputs and will cause unnecessary re-renders on every parent render.

Suggested fix
-  const handleValueChange = (variableName: string, value: any) => {
+  const handleValueChange = useCallback((variableName: string, value: any) => {
     setVariableValues((prev) => ({
       ...prev,
       [variableName]: value,
     }));
-  };
+  }, []);

498-503: Consider adding form to the dependency array.

While form from Form.useForm() is stable, including it in the dependency array follows best practices and satisfies exhaustive-deps lint rules.

Suggested fix
-  }, [workflowVariables, convertVariableToFormValue]);
+  }, [workflowVariables, convertVariableToFormValue, form]);

692-806: Inline object creation in render causes unnecessary re-renders.

The rules arrays (lines 707-711, 742-746, 777-781) and inline onChange handlers are recreated on every render. Consider memoizing the rules or extracting them as constants. Also, consider wrapping renderFormField with useCallback for potential optimization.

Example: Extract rules as constants
// Outside the component or memoized inside
const createRequiredRule = (message: string) => [{ required: true, message }];

// Then use:
rules={required ? createRequiredRule(t('canvas.workflow.variables.inputPlaceholder')) : []}

Alternatively, memoize the entire renderFormField function:

const renderFormField = useCallback((variable: WorkflowVariable) => {
  // ... existing implementation
}, [variableValues, isFieldInvalid, handleValueChange, isFormDisabled, uploading, t, form, handleFileUpload, handleFileRemove, handleRefreshFile]);

819-844: Dead code: Unreachable branch due to constant false condition.

The {false ? ( condition (line 821) means the entire MixedTextEditor block (lines 822-844) is unreachable. This appears to be intentional based on the biome-ignore comment, but keeping dead code increases maintenance burden.

Consider removing this block if it's not needed, or use a feature flag/environment variable if it's intended for future use.


630-681: Fragile CSS selector and magic number.

  1. Line 649: The hardcoded CSS selector '.p-4.flex-1.overflow-y-auto' is fragile. If styling changes, the scroll-to-error functionality will silently fail. Consider using a ref or data-* attribute instead.

  2. Line 670: The magic number 400 for the timeout should be a named constant.

  3. Line 632: The error object lacks proper typing. Consider adding a type guard or interface for the form validation error.

Suggested improvements
// Define constant
const FOCUS_DELAY_MS = 400;

// Use ref or data attribute for scroll container
const scrollContainerRef = useRef<HTMLDivElement>(null);
// In JSX: <div ref={scrollContainerRef} className="p-3 sm:p-4 flex-1 overflow-y-auto">

// Type guard for form error
interface FormValidationError {
  errorFields?: Array<{ name: (string | number)[]; errors: string[] }>;
}

const isFormValidationError = (error: unknown): error is FormValidationError => {
  return typeof error === 'object' && error !== null && 'errorFields' in error;
};

// In catch block:
if (isFormValidationError(error) && error.errorFields?.length) {
  // ...
}

117-130: Consider wrapping WorkflowRunForm with React.memo.

Per coding guidelines, components should be wrapped with React.memo to prevent unnecessary re-renders. While form components with internal state may re-render frequently anyway, memoizing can still help when parent components re-render without changing this component's props.

Suggested approach
-export const WorkflowRunForm = ({
+export const WorkflowRunForm = React.memo(({
   workflowVariables,
   // ... other props
 }: WorkflowRunFormProps) => {
   // ... component body
-};
+});

100-115: Unused prop and any type usage.

  1. Line 107: pollingError is declared in the interface but never used in the component. Remove it if not needed.

  2. Line 113: workflowApp is typed as any. Per coding guidelines, avoid any and use proper types or unknown with type guards.

Suggested fix
 interface WorkflowRunFormProps {
   workflowVariables: WorkflowVariable[];
   onSubmitVariables: (variables: WorkflowVariable[]) => Promise<void>;
   loading: boolean;
   executionId?: string | null;
   workflowStatus?: WorkflowExecutionStatus | null;
   isPolling?: boolean;
-  pollingError?: any;
   isRunning?: boolean;
   onRunningChange?: (isRunning: boolean) => void;
   canvasId?: string;
   className?: string;
   templateContent?: string;
-  workflowApp?: any;
+  workflowApp?: {
+    canvasData?: RawCanvasData;
+    // Add other known properties as needed
+  };
   creditUsage?: number | null;
 }

Comment on lines +222 to +244
const isFormValid = useMemo(() => {
return workflowVariables.every((variable) => {
if (!variable.required) {
return true;
}

const value = variableValues[variable.name];

if (variable.variableType === 'string') {
return value && value.trim() !== '';
}

if (variable.variableType === 'option') {
return value && (Array.isArray(value) ? value.length > 0 : value);
}

if (variable.variableType === 'resource') {
return value && Array.isArray(value) && value.length > 0;
}

return false;
});
}, [workflowVariables, variableValues]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential runtime error and code duplication.

  1. Line 231: If value is truthy but not a string (e.g., an object), calling value.trim() will throw a runtime error. Add a type check.

  2. Line 242: Returning false for unknown variable types means any new type will be treated as invalid. Consider returning true for unhandled types or logging a warning.

  3. The validation logic is duplicated in getInvalidFields (lines 247-268). Consider extracting a shared validation helper.

Suggested fix for type safety
       if (variable.variableType === 'string') {
-        return value && value.trim() !== '';
+        return typeof value === 'string' && value.trim() !== '';
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isFormValid = useMemo(() => {
return workflowVariables.every((variable) => {
if (!variable.required) {
return true;
}
const value = variableValues[variable.name];
if (variable.variableType === 'string') {
return value && value.trim() !== '';
}
if (variable.variableType === 'option') {
return value && (Array.isArray(value) ? value.length > 0 : value);
}
if (variable.variableType === 'resource') {
return value && Array.isArray(value) && value.length > 0;
}
return false;
});
}, [workflowVariables, variableValues]);
const isFormValid = useMemo(() => {
return workflowVariables.every((variable) => {
if (!variable.required) {
return true;
}
const value = variableValues[variable.name];
if (variable.variableType === 'string') {
return typeof value === 'string' && value.trim() !== '';
}
if (variable.variableType === 'option') {
return value && (Array.isArray(value) ? value.length > 0 : value);
}
if (variable.variableType === 'resource') {
return value && Array.isArray(value) && value.length > 0;
}
return false;
});
}, [workflowVariables, variableValues]);
🤖 Prompt for AI Agents
In
`@packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx`
around lines 222 - 244, The current validation in isFormValid can throw if value
isn't a string and treats unknown variable types as invalid while duplicating
logic in getInvalidFields; extract the per-variable validation into a shared
helper (e.g., validateVariable(variable, variableValues)) and use it from both
isFormValid and getInvalidFields, ensure the helper checks typeof value ===
'string' before calling trim for variable.variableType === 'string', handle
'option' and 'resource' as before, and for unrecognized variable.variableType
either return true or call a logger/warn then return true so new types don't
break validation.

Comment on lines +379 to +433
const handleFileUpload = useCallback(
async (file: File, variableName: string) => {
const currentFileList = variableValues[variableName] || [];
const result = await uploadFile(file, currentFileList);

if (result && typeof result === 'object' && 'storageKey' in result) {
let fileId: string | undefined;

// Create DriveFile if canvasId is available
if (canvasId) {
const variable = workflowVariables.find((v) => v.name === variableName);
if (variable?.variableId) {
try {
const { data: driveFileResponse, error } = await getClient().createDriveFile({
body: {
canvasId,
name: file.name,
type: file.type,
storageKey: result.storageKey,
source: 'variable',
variableId: variable.variableId,
},
});

if (!error && driveFileResponse?.data?.fileId) {
fileId = driveFileResponse.data.fileId;
}
} catch (error) {
console.error('Failed to create DriveFile:', error);
// Continue without fileId if creation fails
}
}
}

// Create new file with storageKey and optional fileId
const newFile: UploadFile = {
uid: fileId || result.uid,
name: file.name,
status: 'done',
url: result.storageKey, // Store storageKey in url field
...(fileId && { response: { fileId } }), // Store fileId in response for later retrieval
};

// Replace the file list with the new file (single file limit)
const newFileList = [newFile];
handleValueChange(variableName, newFileList);
form.setFieldsValue({
[variableName]: newFileList,
});
return false; // Prevent default upload behavior
}
return false;
},
[uploadFile, variableValues, canvasId, workflowVariables],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing handleValueChange in dependency array and form reference.

The callback uses handleValueChange (line 424) and form (line 425) but neither is included in the dependency array. Once handleValueChange is wrapped with useCallback, add it to the dependencies. The form instance from Form.useForm() is stable, but it's good practice to include it.

Suggested fix
-    [uploadFile, variableValues, canvasId, workflowVariables],
+    [uploadFile, variableValues, canvasId, workflowVariables, handleValueChange, form],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleFileUpload = useCallback(
async (file: File, variableName: string) => {
const currentFileList = variableValues[variableName] || [];
const result = await uploadFile(file, currentFileList);
if (result && typeof result === 'object' && 'storageKey' in result) {
let fileId: string | undefined;
// Create DriveFile if canvasId is available
if (canvasId) {
const variable = workflowVariables.find((v) => v.name === variableName);
if (variable?.variableId) {
try {
const { data: driveFileResponse, error } = await getClient().createDriveFile({
body: {
canvasId,
name: file.name,
type: file.type,
storageKey: result.storageKey,
source: 'variable',
variableId: variable.variableId,
},
});
if (!error && driveFileResponse?.data?.fileId) {
fileId = driveFileResponse.data.fileId;
}
} catch (error) {
console.error('Failed to create DriveFile:', error);
// Continue without fileId if creation fails
}
}
}
// Create new file with storageKey and optional fileId
const newFile: UploadFile = {
uid: fileId || result.uid,
name: file.name,
status: 'done',
url: result.storageKey, // Store storageKey in url field
...(fileId && { response: { fileId } }), // Store fileId in response for later retrieval
};
// Replace the file list with the new file (single file limit)
const newFileList = [newFile];
handleValueChange(variableName, newFileList);
form.setFieldsValue({
[variableName]: newFileList,
});
return false; // Prevent default upload behavior
}
return false;
},
[uploadFile, variableValues, canvasId, workflowVariables],
);
const handleFileUpload = useCallback(
async (file: File, variableName: string) => {
const currentFileList = variableValues[variableName] || [];
const result = await uploadFile(file, currentFileList);
if (result && typeof result === 'object' && 'storageKey' in result) {
let fileId: string | undefined;
// Create DriveFile if canvasId is available
if (canvasId) {
const variable = workflowVariables.find((v) => v.name === variableName);
if (variable?.variableId) {
try {
const { data: driveFileResponse, error } = await getClient().createDriveFile({
body: {
canvasId,
name: file.name,
type: file.type,
storageKey: result.storageKey,
source: 'variable',
variableId: variable.variableId,
},
});
if (!error && driveFileResponse?.data?.fileId) {
fileId = driveFileResponse.data.fileId;
}
} catch (error) {
console.error('Failed to create DriveFile:', error);
// Continue without fileId if creation fails
}
}
}
// Create new file with storageKey and optional fileId
const newFile: UploadFile = {
uid: fileId || result.uid,
name: file.name,
status: 'done',
url: result.storageKey, // Store storageKey in url field
...(fileId && { response: { fileId } }), // Store fileId in response for later retrieval
};
// Replace the file list with the new file (single file limit)
const newFileList = [newFile];
handleValueChange(variableName, newFileList);
form.setFieldsValue({
[variableName]: newFileList,
});
return false; // Prevent default upload behavior
}
return false;
},
[uploadFile, variableValues, canvasId, workflowVariables, handleValueChange, form],
);
🤖 Prompt for AI Agents
In
`@packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx`
around lines 379 - 433, The handleFileUpload useCallback references
handleValueChange and form but they are missing from its dependency array;
update the dependency array for handleFileUpload to include handleValueChange
and form (in addition to uploadFile, variableValues, canvasId,
workflowVariables) so React's hook rules are satisfied—ensure handleValueChange
is memoized with useCallback where it's defined before adding it, and then add
both handleValueChange and form to the dependency list of handleFileUpload.

- Removed unused state variables for tools panel and highlight install buttons.
- Eliminated the handleToolsDependencyOpenChange function to streamline the component.
- Ensured consistent use of optional chaining and nullish coalescing for safer property access.
…unForm

- Eliminated the _handleTemplateVariableChange function to streamline the component.
- Ensured consistent use of Tailwind CSS for styling and maintained performance optimizations.
- Eliminated the RequiredTagText component as it returned null, streamlining the code.
- Ensured consistent use of Tailwind CSS for styling and maintained performance optimizations.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx`:
- Around line 369-375: The handleValueChange function is recreated each render
causing needless re-renders in child components (e.g., Input.TextArea, Select)
and unstable dependencies; wrap handleValueChange in React.useCallback and
return the same updater signature, referencing setVariableValues in the
dependency array (e.g., useCallback((variableName, value) => {
setVariableValues(prev => ({ ...prev, [variableName]: value })); },
[setVariableValues])) so child props and other callbacks receive a stable
function reference.
- Around line 435-445: The handleFileRemove callback uses the `form` object
(calls `form.setFieldsValue`) but `form` is not included in the dependency
array, which can cause stale closures; update the dependency array for
`useCallback` on `handleFileRemove` to include `form` (i.e., [variableValues,
handleValueChange, form]) so the callback is recreated when the form instance
changes, and verify no other missing dependencies exist for `handleFileRemove`.
♻️ Duplicate comments (2)
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx (2)

221-275: Type safety and code duplication issues persist.

The previously flagged issues remain:

  1. Line 230: value.trim() is called without verifying value is a string. If value is an object, this throws a runtime error.

  2. Line 241: Returns false for unknown variable types, treating new types as invalid.

  3. Lines 254-260: Validation logic duplicates what's in isFormValid. Extract a shared helper.

Suggested refactor: extract shared validation helper
+// Shared validation helper for single variable
+const isVariableValid = (variable: WorkflowVariable, value: unknown): boolean => {
+  if (!variable.required) return true;
+
+  if (variable.variableType === 'string') {
+    return typeof value === 'string' && value.trim() !== '';
+  }
+  if (variable.variableType === 'option') {
+    return value != null && (Array.isArray(value) ? value.length > 0 : !!value);
+  }
+  if (variable.variableType === 'resource') {
+    return Array.isArray(value) && value.length > 0;
+  }
+  // Unknown types: return true to avoid blocking new types
+  return true;
+};

 const isFormValid = useMemo(() => {
-  return workflowVariables.every((variable) => {
-    if (!variable.required) {
-      return true;
-    }
-
-    const value = variableValues[variable.name];
-
-    if (variable.variableType === 'string') {
-      return value && value.trim() !== '';
-    }
-    // ... rest of logic
-    return false;
-  });
+  return workflowVariables.every((variable) =>
+    isVariableValid(variable, variableValues[variable.name])
+  );
 }, [workflowVariables, variableValues]);

 const getInvalidFields = useCallback(() => {
   const invalidFields: string[] = [];
   for (const variable of workflowVariables) {
-    if (!variable.required) continue;
-    const value = variableValues[variable.name];
-    let isValid = false;
-    // ... duplicated validation logic
+    if (!isVariableValid(variable, variableValues[variable.name])) {
+      invalidFields.push(variable.name);
+    }
   }
   return invalidFields;
 }, [workflowVariables, variableValues]);

378-432: Missing dependencies in useCallback.

handleValueChange (line 423) and form (lines 424-426) are used inside this callback but are not in the dependency array. Once handleValueChange is wrapped with useCallback, add both to the dependencies.

Suggested fix
-  [uploadFile, variableValues, canvasId, workflowVariables],
+  [uploadFile, variableValues, canvasId, workflowVariables, handleValueChange, form],
🧹 Nitpick comments (7)
packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx (7)

57-64: Wrap pure component with React.memo.

Per coding guidelines, pure presentational components should be wrapped with React.memo to prevent unnecessary re-renders.

Suggested fix
-const RequiredTagText = () => {
+const RequiredTagText = React.memo(() => {
   const { t } = useTranslation();
   return (
     <div className="flex-shrink-0 text-[10px] text-refly-text-2 leading-[16px] px-1 border-[1px] border-solid border-refly-Card-Border rounded-[4px]">
       {t('canvas.workflow.variables.required') || 'Required'}
     </div>
   );
-};
+});
+RequiredTagText.displayName = 'RequiredTagText';

84-97: Wrap pure component with React.memo.

FormItemLabel is a pure presentational component and should be memoized to prevent unnecessary re-renders when parent state changes.

Suggested fix
-const FormItemLabel = ({ name, required }: { name: string; required: boolean }) => {
+const FormItemLabel = React.memo(({ name, required }: { name: string; required: boolean }) => {
   return (
     <div className="flex items-center gap-2 min-w-0">
       <Typography.Paragraph
         ellipsis={{ rows: 1, tooltip: <div className="max-h-[200px] overflow-y-auto">{name}</div> }}
         className="!m-0 text-xs font-semibold text-refly-text-0 leading-4 max-w-[100px]"
       >
         {name}
       </Typography.Paragraph>

       {required && <RequiredTagText />}
     </div>
   );
-};
+});
+FormItemLabel.displayName = 'FormItemLabel';

497-502: Consider adding form to dependency array.

form.setFieldsValue is called but form is not in the dependencies. While the form instance from Form.useForm() is stable, including it follows React's exhaustive-deps best practice.

Suggested fix
-}, [workflowVariables, convertVariableToFormValue]);
+}, [workflowVariables, convertVariableToFormValue, form]);

691-805: Consider extracting renderFormField into a memoized component.

This render function is recreated on every render and captures multiple state variables. Per coding guidelines, nested components with closures should be split into separate components. Extracting this to a FormField component with props would improve performance and maintainability.

Outline of suggested refactor
// Extract to a separate memoized component
interface FormFieldProps {
  variable: WorkflowVariable;
  value: any;
  hasError: boolean;
  isDisabled: boolean;
  uploading: boolean;
  onValueChange: (name: string, value: any) => void;
  onFileUpload: (file: File, name: string) => Promise<boolean>;
  onFileRemove: (file: UploadFile, name: string) => void;
  onRefresh: (name: string) => void;
}

const FormField = React.memo(({ variable, value, hasError, ... }: FormFieldProps) => {
  const { t } = useTranslation();
  // ... render logic based on variable.variableType
});

// Then in the parent:
{workflowVariables.map((variable) => (
  <FormField
    key={variable.name}
    variable={variable}
    value={variableValues[variable.name]}
    hasError={isFieldInvalid(variable.name)}
    isDisabled={isFormDisabled}
    uploading={uploading}
    onValueChange={handleValueChange}
    onFileUpload={handleFileUpload}
    onFileRemove={handleFileRemove}
    onRefresh={handleRefreshFile}
  />
))}

629-680: Consider typing the catch block error.

The error variable is implicitly any. Adding a type guard or explicit typing would improve safety:

Suggested improvement
   } catch (error) {
+    // Type guard for Ant Design form validation error
+    const formError = error as { errorFields?: Array<{ name: (string | number)[] }> };
     // Form validation failed, scroll to first error
-    if (error?.errorFields && error.errorFields.length > 0) {
-      const firstErrorField = error.errorFields[0];
+    if (formError?.errorFields && formError.errorFields.length > 0) {
+      const firstErrorField = formError.errorFields[0];

818-821: Remove unnecessary constant-condition wrapper.

The biome-ignore comment and the always-true condition suggest leftover code from a refactor. The wrapper can be removed to simplify the JSX structure.

Suggested simplification
         <>
-          {/* default show Form */}
-          {/* biome-ignore lint/correctness/noConstantCondition: <explanation> */}
-          {
-            <div className="p-3 sm:p-4 flex-1 overflow-y-auto">
+          <div className="p-3 sm:p-4 flex-1 overflow-y-auto">
             {/* Show loading state when loading */}
             {workflowVariables.length > 0 ? (
               ...
             )}
-            </div>
-          }
+          </div>

702-731: Verify label-input accessibility association.

Using a custom FormItemLabel component as the label prop may not automatically associate the label with the input for screen readers. Ant Design's Form.Item expects a string or ReactNode, but the htmlFor association may not work properly with a custom component.

Consider verifying that the label is properly associated with the input, or add explicit aria-labelledby attributes. Based on learnings, ensure proper labeling and accessibility for form elements.

Comment on lines +369 to +375
// Handle form value changes
const handleValueChange = (variableName: string, value: any) => {
setVariableValues((prev) => ({
...prev,
[variableName]: value,
}));
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Wrap handleValueChange with useCallback.

This function is passed to child components (e.g., Input.TextArea, Select) and used in other callbacks' dependency arrays. Without useCallback, a new function reference is created on every render, causing unnecessary re-renders.

Suggested fix
-const handleValueChange = (variableName: string, value: any) => {
+const handleValueChange = useCallback((variableName: string, value: any) => {
   setVariableValues((prev) => ({
     ...prev,
     [variableName]: value,
   }));
-};
+}, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle form value changes
const handleValueChange = (variableName: string, value: any) => {
setVariableValues((prev) => ({
...prev,
[variableName]: value,
}));
};
// Handle form value changes
const handleValueChange = useCallback((variableName: string, value: any) => {
setVariableValues((prev) => ({
...prev,
[variableName]: value,
}));
}, []);
🤖 Prompt for AI Agents
In
`@packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx`
around lines 369 - 375, The handleValueChange function is recreated each render
causing needless re-renders in child components (e.g., Input.TextArea, Select)
and unstable dependencies; wrap handleValueChange in React.useCallback and
return the same updater signature, referencing setVariableValues in the
dependency array (e.g., useCallback((variableName, value) => {
setVariableValues(prev => ({ ...prev, [variableName]: value })); },
[setVariableValues])) so child props and other callbacks receive a stable
function reference.

Comment on lines +435 to +445
const handleFileRemove = useCallback(
(file: UploadFile, variableName: string) => {
const currentFileList = variableValues[variableName] || [];
const newFileList = currentFileList.filter((f: UploadFile) => f.uid !== file.uid);
handleValueChange(variableName, newFileList);
form.setFieldsValue({
[variableName]: newFileList,
});
},
[variableValues, handleValueChange],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing form in dependency array.

The callback uses form.setFieldsValue but form is not included in the dependency array.

Suggested fix
-  [variableValues, handleValueChange],
+  [variableValues, handleValueChange, form],
🤖 Prompt for AI Agents
In
`@packages/ai-workspace-common/src/components/canvas/workflow-run/workflow-run-form.tsx`
around lines 435 - 445, The handleFileRemove callback uses the `form` object
(calls `form.setFieldsValue`) but `form` is not included in the dependency
array, which can cause stale closures; update the dependency array for
`useCallback` on `handleFileRemove` to include `form` (i.e., [variableValues,
handleValueChange, form]) so the callback is recreated when the form instance
changes, and verify no other missing dependencies exist for `handleFileRemove`.

@mrcfps mrcfps changed the title feat: enhance WorkflowRunForm with improved validation and file handling [REF-1257] feat: enhance WorkflowRunForm with improved validation and file handling Jan 20, 2026
@linear
Copy link
Copy Markdown

linear Bot commented Jan 20, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants