chore(ui): Refactor action and connector form fields into single component

This commit is contained in:
Richard Palethorpe
2025-03-26 09:03:03 +00:00
parent 0f2731f9e8
commit 4dcc77372d
21 changed files with 964 additions and 753 deletions

View File

@@ -7,6 +7,7 @@ import GithubIssueCommenterAction from './actions/GithubIssueCommenterAction';
import GithubRepositoryAction from './actions/GithubRepositoryAction';
import TwitterPostAction from './actions/TwitterPostAction';
import SendMailAction from './actions/SendMailAction';
import GenerateImageAction from './actions/GenerateImageAction';
/**
* ActionForm component for configuring an action
@@ -103,46 +104,7 @@ const ActionForm = ({ actions = [], onChange, onRemove, onAdd }) => {
case 'send-mail':
return <SendMailAction {...actionProps} />;
case 'generate_image':
return (
<div className="generate-image-action">
<div className="form-group mb-3">
<label htmlFor={`apiKey${index}`}>OpenAI API Key</label>
<input
type="text"
id={`apiKey${index}`}
value={getConfigValue(action, 'apiKey', '')}
onChange={(e) => onActionConfigChange(index, 'apiKey', e.target.value)}
className="form-control"
placeholder="sk-..."
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`apiURL${index}`}>API URL (Optional)</label>
<input
type="text"
id={`apiURL${index}`}
value={getConfigValue(action, 'apiURL', 'https://api.openai.com/v1')}
onChange={(e) => onActionConfigChange(index, 'apiURL', e.target.value)}
className="form-control"
placeholder="https://api.openai.com/v1"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`model${index}`}>Model</label>
<input
type="text"
id={`model${index}`}
value={getConfigValue(action, 'model', 'dall-e-3')}
onChange={(e) => onActionConfigChange(index, 'model', e.target.value)}
className="form-control"
placeholder="dall-e-3"
/>
<small className="form-text text-muted">Image generation model (e.g., dall-e-3)</small>
</div>
</div>
);
return <GenerateImageAction {...actionProps} />;
default:
return <FallbackAction {...actionProps} />;
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import FormFieldDefinition from '../common/FormFieldDefinition';
/**
* Base action component that renders form fields based on field definitions
*
* @param {Object} props Component props
* @param {number} props.index Action index
* @param {Function} props.onActionConfigChange Handler for config changes
* @param {Function} props.getConfigValue Helper to get config values
* @param {Array} props.fields Field definitions for this action
*/
const BaseAction = ({
index,
onActionConfigChange,
getConfigValue,
fields = []
}) => {
// Create an object with all the current values
const currentValues = {};
// Pre-populate with current values or defaults
fields.forEach(field => {
currentValues[field.name] = getConfigValue(field.name, field.defaultValue);
});
// Handle field value changes
const handleFieldChange = (name, value) => {
onActionConfigChange(name, value);
};
return (
<div className="action-template">
<FormFieldDefinition
fields={fields}
values={currentValues}
onChange={handleFieldChange}
idPrefix={`action${index}_`}
/>
</div>
);
};
export default BaseAction;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* Generate Image action component
*/
const GenerateImageAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for Generate Image action
const fields = [
{
name: 'apiKey',
label: 'OpenAI API Key',
type: 'text',
defaultValue: '',
placeholder: 'sk-...',
helpText: 'Your OpenAI API key for image generation',
required: false,
},
{
name: 'apiURL',
label: 'API URL',
type: 'text',
defaultValue: '',
placeholder: 'http://localai:8081',
helpText: 'OpenAI compatible API endpoint URL',
required: false,
},
{
name: 'model',
label: 'Model',
type: 'text',
defaultValue: '',
placeholder: 'dall-e-3',
helpText: 'Image generation model',
required: false,
}
];
return (
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};
export default GenerateImageAction;

View File

@@ -1,61 +1,57 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* GitHub Issue Closer action component
*/
const GithubIssueCloserAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for GitHub Issue Closer action
const fields = [
{
name: 'token',
label: 'GitHub Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: '',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: '',
required: true,
},
{
name: 'customActionName',
label: 'Custom Action Name (Optional)',
type: 'text',
defaultValue: '',
placeholder: 'close_github_issue',
helpText: 'Custom name for this action (optional)',
required: false,
},
];
return (
<div className="github-issue-closer-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="close_github_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,61 +1,57 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* GitHub Issue Commenter action component
*/
const GithubIssueCommenterAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for GitHub Issue Commenter action
const fields = [
{
name: 'token',
label: 'GitHub Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: 'Owner of the repository',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: 'Name of the repository',
required: true,
},
{
name: 'customActionName',
label: 'Custom Action Name (Optional)',
type: 'text',
defaultValue: '',
placeholder: 'comment_on_github_issue',
helpText: 'Custom name for this action (optional)',
required: false,
},
];
return (
<div className="github-issue-commenter-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="comment_on_github_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,74 +1,66 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* GitHub Issue Labeler action component
*/
const GithubIssueLabelerAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for GitHub Issue Labeler action
const fields = [
{
name: 'token',
label: 'GitHub Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: '',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: '',
required: true,
},
{
name: 'availableLabels',
label: 'Available Labels',
type: 'text',
defaultValue: 'bug,enhancement',
placeholder: 'bug,enhancement,documentation',
helpText: 'Comma-separated list of available labels',
required: true,
},
{
name: 'customActionName',
label: 'Custom Action Name (Optional)',
type: 'text',
defaultValue: '',
placeholder: 'add_label_to_issue',
helpText: 'Custom name for this action (optional)',
required: false,
},
];
return (
<div className="github-issue-labeler-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`availableLabels${index}`}>Available Labels</label>
<input
type="text"
id={`availableLabels${index}`}
value={getConfigValue('availableLabels', 'bug,enhancement')}
onChange={(e) => onActionConfigChange('availableLabels', e.target.value)}
className="form-control"
placeholder="bug,enhancement,documentation"
/>
<small className="form-text text-muted">Comma-separated list of available labels</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="add_label_to_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,61 +1,57 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* GitHub Issue Opener action component
*/
const GithubIssueOpenerAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for GitHub Issue Opener action
const fields = [
{
name: 'token',
label: 'GitHub Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: '',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: '',
required: true,
},
{
name: 'customActionName',
label: 'Custom Action Name (Optional)',
type: 'text',
defaultValue: '',
placeholder: 'open_github_issue',
helpText: 'Custom name for this action (optional)',
required: false,
},
];
return (
<div className="github-issue-opener-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="open_github_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,4 +1,5 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* GitHub Repository action component for repository-related actions
@@ -8,58 +9,53 @@ import React from 'react';
* - github-readme
*/
const GithubRepositoryAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for GitHub Repository action
const fields = [
{
name: 'token',
label: 'GitHub Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: '',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: '',
required: true,
},
{
name: 'customActionName',
label: 'Custom Action Name (Optional)',
type: 'text',
defaultValue: '',
placeholder: 'github_repo_action',
helpText: 'Custom name for this action (optional)',
required: false,
},
];
return (
<div className="github-repository-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="github_repo_action"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,74 +1,66 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* SendMail action component
*/
const SendMailAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for SendMail action
const fields = [
{
name: 'email',
label: 'Email',
type: 'email',
defaultValue: '',
placeholder: 'your-email@example.com',
helpText: 'Email address to send from',
required: true,
},
{
name: 'username',
label: 'Username',
type: 'text',
defaultValue: '',
placeholder: 'SMTP username (often same as email)',
helpText: '',
required: true,
},
{
name: 'password',
label: 'Password',
type: 'password',
defaultValue: '',
placeholder: 'SMTP password or app password',
helpText: 'For Gmail, use an app password',
required: true,
},
{
name: 'smtpHost',
label: 'SMTP Host',
type: 'text',
defaultValue: '',
placeholder: 'smtp.gmail.com',
helpText: '',
required: true,
},
{
name: 'smtpPort',
label: 'SMTP Port',
type: 'text',
defaultValue: '587',
placeholder: '587',
helpText: 'Common ports: 587 (TLS), 465 (SSL)',
required: true,
},
];
return (
<div className="send-mail-action">
<div className="form-group mb-3">
<label htmlFor={`email${index}`}>Email</label>
<input
type="email"
id={`email${index}`}
value={getConfigValue('email', '')}
onChange={(e) => onActionConfigChange('email', e.target.value)}
className="form-control"
placeholder="your-email@example.com"
/>
<small className="form-text text-muted">Email address to send from</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`username${index}`}>Username</label>
<input
type="text"
id={`username${index}`}
value={getConfigValue('username', '')}
onChange={(e) => onActionConfigChange('username', e.target.value)}
className="form-control"
placeholder="SMTP username (often same as email)"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`password${index}`}>Password</label>
<input
type="password"
id={`password${index}`}
value={getConfigValue('password', '')}
onChange={(e) => onActionConfigChange('password', e.target.value)}
className="form-control"
placeholder="SMTP password or app password"
/>
<small className="form-text text-muted">For Gmail, use an app password</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`smtpHost${index}`}>SMTP Host</label>
<input
type="text"
id={`smtpHost${index}`}
value={getConfigValue('smtpHost', '')}
onChange={(e) => onActionConfigChange('smtpHost', e.target.value)}
className="form-control"
placeholder="smtp.gmail.com"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`smtpPort${index}`}>SMTP Port</label>
<input
type="text"
id={`smtpPort${index}`}
value={getConfigValue('smtpPort', '587')}
onChange={(e) => onActionConfigChange('smtpPort', e.target.value)}
className="form-control"
placeholder="587"
/>
<small className="form-text text-muted">Common ports: 587 (TLS), 465 (SSL)</small>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,40 +1,38 @@
import React from 'react';
import BaseAction from './BaseAction';
/**
* Twitter Post action component
*/
const TwitterPostAction = ({ index, onActionConfigChange, getConfigValue }) => {
// Field definitions for Twitter Post action
const fields = [
{
name: 'token',
label: 'Twitter API Token',
type: 'text',
defaultValue: '',
placeholder: 'Twitter API token',
helpText: 'Twitter API token with posting permissions',
required: true,
},
{
name: 'noCharacterLimits',
label: 'Disable character limit (280 characters)',
type: 'checkbox',
defaultValue: 'false',
helpText: 'Enable to bypass the 280 character limit check',
required: false,
},
];
return (
<div className="twitter-post-action">
<div className="form-group mb-3">
<label htmlFor={`twitterToken${index}`}>Twitter API Token</label>
<input
type="text"
id={`twitterToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="Twitter API token"
/>
<small className="form-text text-muted">Twitter API token with posting permissions</small>
</div>
<div className="form-group mb-3">
<div className="form-check">
<input
type="checkbox"
id={`noCharacterLimits${index}`}
checked={getConfigValue('noCharacterLimits', '') === 'true'}
onChange={(e) => onActionConfigChange('noCharacterLimits', e.target.checked ? 'true' : 'false')}
className="form-check-input"
/>
<label className="form-check-label" htmlFor={`noCharacterLimits${index}`}>
Disable character limit (280 characters)
</label>
<small className="form-text text-muted d-block">Enable to bypass the 280 character limit check</small>
</div>
</div>
</div>
<BaseAction
index={index}
onActionConfigChange={onActionConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -0,0 +1,107 @@
import React from 'react';
/**
* Reusable form field component that handles different input types
*
* @param {Object} props Component props
* @param {string} props.id Unique identifier for the input
* @param {string} props.label Label text for the field
* @param {string} props.type Input type (text, checkbox, select, textarea)
* @param {string|boolean} props.value Current value of the field
* @param {Function} props.onChange Handler for value changes
* @param {string} props.placeholder Placeholder text
* @param {string} props.helpText Help text to display below the field
* @param {Array} props.options Options for select inputs
* @param {boolean} props.required Whether the field is required
*/
const FormField = ({
id,
label,
type = 'text',
value,
onChange,
placeholder = '',
helpText = '',
options = [],
required = false,
}) => {
// Render different input types
const renderInput = () => {
switch (type) {
case 'checkbox':
return (
<div className="form-check">
<label className="checkbox-label" htmlFor={id}>
<input
type="checkbox"
id={id}
checked={value === true || value === 'true'}
onChange={(e) => onChange(e.target.checked ? 'true' : 'false')}
/>
{label}
</label>
{helpText && <small className="form-text text-muted d-block">{helpText}</small>}
</div>
);
case 'select':
return (
<>
<label htmlFor={id}>{label}</label>
<select
id={id}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
className="form-control"
required={required}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{helpText && <small className="form-text text-muted">{helpText}</small>}
</>
);
case 'textarea':
return (
<>
<label htmlFor={id}>{label}</label>
<textarea
id={id}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
className="form-control"
placeholder={placeholder}
required={required}
/>
{helpText && <small className="form-text text-muted">{helpText}</small>}
</>
);
default:
return (
<>
<label htmlFor={id}>{label}</label>
<input
type={type}
id={id}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
className="form-control"
placeholder={placeholder}
required={required}
/>
{helpText && <small className="form-text text-muted">{helpText}</small>}
</>
);
}
};
return (
<div className="form-group mb-3">
{renderInput()}
</div>
);
};
export default FormField;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import FormField from './FormField';
/**
* Component that renders a form based on a field definition
*
* @param {Object} props Component props
* @param {Array} props.fields Array of field definitions
* @param {Object} props.values Current values for the fields
* @param {Function} props.onChange Handler for field value changes
* @param {string} props.idPrefix Prefix for field IDs
*/
const FormFieldDefinition = ({
fields,
values,
onChange,
idPrefix = '',
}) => {
// Ensure values is an object
const safeValues = values || {};
return (
<div className="form-fields">
{fields.map((field) => (
<FormField
key={field.name}
id={`${idPrefix}${field.name}`}
label={field.label}
type={field.type}
value={safeValues[field.name] !== undefined ? safeValues[field.name] : field.defaultValue}
onChange={(value) => onChange(field.name, value)}
placeholder={field.placeholder || ''}
helpText={field.helpText || ''}
options={field.options || []}
required={field.required || false}
/>
))}
</div>
);
};
export default FormFieldDefinition;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import FormFieldDefinition from '../common/FormFieldDefinition';
/**
* Base connector component that renders form fields based on field definitions
*
* @param {Object} props Component props
* @param {Object} props.connector Connector data
* @param {number} props.index Connector index
* @param {Function} props.onConnectorConfigChange Handler for config changes
* @param {Function} props.getConfigValue Helper to get config values
* @param {Array} props.fields Field definitions for this connector
*/
const BaseConnector = ({
connector,
index,
onConnectorConfigChange,
getConfigValue,
fields = []
}) => {
// Create an object with all the current values
const currentValues = {};
// Pre-populate with current values or defaults
fields.forEach(field => {
currentValues[field.name] = getConfigValue(connector, field.name, field.defaultValue);
});
// Handle field value changes
const handleFieldChange = (name, value) => {
onConnectorConfigChange(index, name, value);
};
return (
<div className="connector-template">
<FormFieldDefinition
fields={fields}
values={currentValues}
onChange={handleFieldChange}
idPrefix={`connector${index}_`}
/>
</div>
);
};
export default BaseConnector;

View File

@@ -1,37 +1,40 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* Discord connector template
*/
const DiscordConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
// Field definitions for Discord connector
const fields = [
{
name: 'token',
label: 'Discord Bot Token',
type: 'text',
defaultValue: '',
placeholder: 'Bot token from Discord Developer Portal',
helpText: 'Get this from the Discord Developer Portal',
required: true,
},
{
name: 'defaultChannel',
label: 'Default Channel',
type: 'text',
defaultValue: '',
placeholder: '123456789012345678',
helpText: 'Channel ID to always answer even if not mentioned',
required: false,
},
];
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`discordToken${index}`}>Discord Bot Token</label>
<input
type="text"
id={`discordToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="Bot token from Discord Developer Portal"
/>
<small className="form-text text-muted">Get this from the Discord Developer Portal</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`discordDefaultChannel${index}`}>Default Channel</label>
<input
type="text"
id={`discordDefaultChannel${index}`}
value={getConfigValue(connector, 'defaultChannel', '')}
onChange={(e) => onConnectorConfigChange(index, 'defaultChannel', e.target.value)}
className="form-control"
placeholder="123456789012345678"
/>
<small className="form-text text-muted">Channel ID to always answer even if not mentioned</small>
</div>
</div>
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,74 +1,70 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* GitHub Issues connector template
*/
const GithubIssuesConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
// Field definitions for GitHub Issues connector
const fields = [
{
name: 'token',
label: 'GitHub Personal Access Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: '',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: '',
required: true,
},
{
name: 'replyIfNoReplies',
label: 'Reply Behavior',
type: 'select',
defaultValue: 'false',
options: [
{ value: 'false', label: 'Reply to all issues' },
{ value: 'true', label: 'Only reply to issues with no comments' },
],
helpText: '',
required: false,
},
{
name: 'pollInterval',
label: 'Poll Interval',
type: 'text',
defaultValue: '10m',
placeholder: '10m',
helpText: 'How often to check for new issues (e.g., 10m, 1h)',
required: false,
},
];
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Personal Access Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue(connector, 'owner', '')}
onChange={(e) => onConnectorConfigChange(index, 'owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue(connector, 'repository', '')}
onChange={(e) => onConnectorConfigChange(index, 'repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`replyIfNoReplies${index}`}>Reply Behavior</label>
<select
id={`replyIfNoReplies${index}`}
value={getConfigValue(connector, 'replyIfNoReplies', 'false')}
onChange={(e) => onConnectorConfigChange(index, 'replyIfNoReplies', e.target.value)}
className="form-control"
>
<option value="false">Reply to all issues</option>
<option value="true">Only reply to issues with no comments</option>
</select>
</div>
<div className="form-group mb-3">
<label htmlFor={`pollInterval${index}`}>Poll Interval</label>
<input
type="text"
id={`pollInterval${index}`}
value={getConfigValue(connector, 'pollInterval', '10m')}
onChange={(e) => onConnectorConfigChange(index, 'pollInterval', e.target.value)}
className="form-control"
placeholder="10m"
/>
<small className="form-text text-muted">How often to check for new issues (e.g., 10m, 1h)</small>
</div>
</div>
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,74 +1,70 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* GitHub PRs connector template
*/
const GithubPRsConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
// Field definitions for GitHub PRs connector
const fields = [
{
name: 'token',
label: 'GitHub Personal Access Token',
type: 'text',
defaultValue: '',
placeholder: 'ghp_...',
helpText: 'Personal access token with repo scope',
required: true,
},
{
name: 'owner',
label: 'Repository Owner',
type: 'text',
defaultValue: '',
placeholder: 'username or organization',
helpText: '',
required: true,
},
{
name: 'repository',
label: 'Repository Name',
type: 'text',
defaultValue: '',
placeholder: 'repository-name',
helpText: '',
required: true,
},
{
name: 'replyIfNoReplies',
label: 'Reply Behavior',
type: 'select',
defaultValue: 'false',
options: [
{ value: 'false', label: 'Reply to all PRs' },
{ value: 'true', label: 'Only reply to PRs with no comments' },
],
helpText: '',
required: false,
},
{
name: 'pollInterval',
label: 'Poll Interval',
type: 'text',
defaultValue: '10m',
placeholder: '10m',
helpText: 'How often to check for new PRs (e.g., 10m, 1h)',
required: false,
},
];
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Personal Access Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue(connector, 'owner', '')}
onChange={(e) => onConnectorConfigChange(index, 'owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue(connector, 'repository', '')}
onChange={(e) => onConnectorConfigChange(index, 'repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`replyIfNoReplies${index}`}>Reply Behavior</label>
<select
id={`replyIfNoReplies${index}`}
value={getConfigValue(connector, 'replyIfNoReplies', 'false')}
onChange={(e) => onConnectorConfigChange(index, 'replyIfNoReplies', e.target.value)}
className="form-control"
>
<option value="false">Reply to all PRs</option>
<option value="true">Only reply to PRs with no comments</option>
</select>
</div>
<div className="form-group mb-3">
<label htmlFor={`pollInterval${index}`}>Poll Interval</label>
<input
type="text"
id={`pollInterval${index}`}
value={getConfigValue(connector, 'pollInterval', '10m')}
onChange={(e) => onConnectorConfigChange(index, 'pollInterval', e.target.value)}
className="form-control"
placeholder="10m"
/>
<small className="form-text text-muted">How often to check for new PRs (e.g., 10m, 1h)</small>
</div>
</div>
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,75 +1,66 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* IRC connector template
*/
const IRCConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`ircServer${index}`}>IRC Server</label>
<input
type="text"
id={`ircServer${index}`}
value={getConfigValue(connector, 'server', '')}
onChange={(e) => onConnectorConfigChange(index, 'server', e.target.value)}
className="form-control"
placeholder="irc.libera.chat"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`ircPort${index}`}>Port</label>
<input
type="text"
id={`ircPort${index}`}
value={getConfigValue(connector, 'port', '6667')}
onChange={(e) => onConnectorConfigChange(index, 'port', e.target.value)}
className="form-control"
placeholder="6667"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`ircNick${index}`}>Nickname</label>
<input
type="text"
id={`ircNick${index}`}
value={getConfigValue(connector, 'nickname', '')}
onChange={(e) => onConnectorConfigChange(index, 'nickname', e.target.value)}
className="form-control"
placeholder="MyAgentBot"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`ircChannels${index}`}>Channel</label>
<input
type="text"
id={`ircChannels${index}`}
value={getConfigValue(connector, 'channel', '')}
onChange={(e) => onConnectorConfigChange(index, 'channel', e.target.value)}
className="form-control"
placeholder="#channel1"
/>
<small className="form-text text-muted">Channel to join</small>
</div>
// Field definitions for IRC connector
const fields = [
{
name: 'server',
label: 'IRC Server',
type: 'text',
defaultValue: '',
placeholder: 'irc.libera.chat',
helpText: 'IRC server address',
required: true,
},
{
name: 'port',
label: 'Port',
type: 'text',
defaultValue: '6667',
placeholder: '6667',
helpText: 'IRC server port',
required: true,
},
{
name: 'nickname',
label: 'Nickname',
type: 'text',
defaultValue: '',
placeholder: 'MyAgentBot',
helpText: 'Bot nickname',
required: true,
},
{
name: 'channel',
label: 'Channel',
type: 'text',
defaultValue: '',
placeholder: '#channel1',
helpText: 'Channel to join',
required: true,
},
{
name: 'alwaysReply',
label: 'Always Reply',
type: 'checkbox',
defaultValue: 'false',
helpText: 'If checked, the agent will reply to all messages in the channel',
required: false,
},
];
<div className="form-group mb-3">
<div className="form-check">
<label className="checkbox-label" htmlFor={`ircAlwaysReply${index}`}>
<input
type="checkbox"
id={`ircAlwaysReply${index}`}
checked={getConfigValue(connector, 'alwaysReply', '') === 'true'}
onChange={(e) => onConnectorConfigChange(index, 'alwaysReply', e.target.checked ? 'true' : 'false')}
/>
Always Reply
</label>
<small className="form-text text-muted d-block">If checked, the agent will reply to all messages in the channel</small>
</div>
</div>
</div>
return (
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,66 +1,57 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* Slack connector template
*/
const SlackConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`slackAppToken${index}`}>Slack App Token</label>
<input
type="text"
id={`slackAppToken${index}`}
value={getConfigValue(connector, 'appToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'appToken', e.target.value)}
className="form-control"
placeholder="xapp-..."
/>
<small className="form-text text-muted">App-level token starting with xapp-</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`slackBotToken${index}`}>Slack Bot Token</label>
<input
type="text"
id={`slackBotToken${index}`}
value={getConfigValue(connector, 'botToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'botToken', e.target.value)}
className="form-control"
placeholder="xoxb-..."
/>
<small className="form-text text-muted">Bot token starting with xoxb-</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`slackChannelID${index}`}>Slack Channel ID</label>
<input
type="text"
id={`slackChannelID${index}`}
value={getConfigValue(connector, 'channelID', '')}
onChange={(e) => onConnectorConfigChange(index, 'channelID', e.target.value)}
className="form-control"
placeholder="C1234567890"
/>
<small className="form-text text-muted">Optional: Specific channel ID to join</small>
</div>
// Field definitions for Slack connector
const fields = [
{
name: 'appToken',
label: 'Slack App Token',
type: 'text',
defaultValue: '',
placeholder: 'xapp-...',
helpText: 'App-level token starting with xapp-',
required: true,
},
{
name: 'botToken',
label: 'Slack Bot Token',
type: 'text',
defaultValue: '',
placeholder: 'xoxb-...',
helpText: 'Bot token starting with xoxb-',
required: true,
},
{
name: 'channelID',
label: 'Slack Channel ID',
type: 'text',
defaultValue: '',
placeholder: 'C1234567890',
helpText: 'Optional: Specific channel ID to join',
required: false,
},
{
name: 'alwaysReply',
label: 'Always Reply',
type: 'checkbox',
defaultValue: 'false',
helpText: 'If checked, the agent will reply to all messages in the channel',
required: false,
},
];
<div className="form-group mb-3">
<div className="form-check">
<input
type="checkbox"
id={`slackAlwaysReply${index}`}
checked={getConfigValue(connector, 'alwaysReply', '') === 'true'}
onChange={(e) => onConnectorConfigChange(index, 'alwaysReply', e.target.checked ? 'true' : 'false')}
className="form-check-input"
/>
<label className="form-check-label" htmlFor={`slackAlwaysReply${index}`}>
Always Reply
</label>
<small className="form-text text-muted d-block">If checked, the agent will reply to all messages in the channel</small>
</div>
</div>
</div>
return (
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,24 +1,31 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* Telegram connector template
*/
const TelegramConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
// Field definitions for Telegram connector
const fields = [
{
name: 'token',
label: 'Telegram Bot Token',
type: 'text',
defaultValue: '',
placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
helpText: 'Get this from @BotFather on Telegram',
required: true,
},
];
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`telegramToken${index}`}>Telegram Bot Token</label>
<input
type="text"
id={`telegramToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
/>
<small className="form-text text-muted">Get this from @BotFather on Telegram</small>
</div>
</div>
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};

View File

@@ -1,71 +1,67 @@
import React from 'react';
import BaseConnector from './BaseConnector';
/**
* Twitter connector template
*/
const TwitterConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
// Field definitions for Twitter connector
const fields = [
{
name: 'apiKey',
label: 'API Key',
type: 'text',
defaultValue: '',
placeholder: 'Twitter API Key',
helpText: '',
required: true,
},
{
name: 'apiSecret',
label: 'API Secret',
type: 'password',
defaultValue: '',
placeholder: 'Twitter API Secret',
helpText: '',
required: true,
},
{
name: 'accessToken',
label: 'Access Token',
type: 'text',
defaultValue: '',
placeholder: 'Twitter Access Token',
helpText: '',
required: true,
},
{
name: 'accessSecret',
label: 'Access Token Secret',
type: 'password',
defaultValue: '',
placeholder: 'Twitter Access Token Secret',
helpText: '',
required: true,
},
{
name: 'bearerToken',
label: 'Bearer Token',
type: 'password',
defaultValue: '',
placeholder: 'Twitter Bearer Token',
helpText: '',
required: true,
},
];
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`twitterApiKey${index}`}>API Key</label>
<input
type="text"
id={`twitterApiKey${index}`}
value={getConfigValue(connector, 'apiKey', '')}
onChange={(e) => onConnectorConfigChange(index, 'apiKey', e.target.value)}
className="form-control"
placeholder="Twitter API Key"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterApiSecret${index}`}>API Secret</label>
<input
type="password"
id={`twitterApiSecret${index}`}
value={getConfigValue(connector, 'apiSecret', '')}
onChange={(e) => onConnectorConfigChange(index, 'apiSecret', e.target.value)}
className="form-control"
placeholder="Twitter API Secret"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterAccessToken${index}`}>Access Token</label>
<input
type="text"
id={`twitterAccessToken${index}`}
value={getConfigValue(connector, 'accessToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'accessToken', e.target.value)}
className="form-control"
placeholder="Twitter Access Token"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterAccessSecret${index}`}>Access Token Secret</label>
<input
type="password"
id={`twitterAccessSecret${index}`}
value={getConfigValue(connector, 'accessSecret', '')}
onChange={(e) => onConnectorConfigChange(index, 'accessSecret', e.target.value)}
className="form-control"
placeholder="Twitter Access Token Secret"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterBearerToken${index}`}>Bearer Token</label>
<input
type="password"
id={`twitterBearerToken${index}`}
value={getConfigValue(connector, 'bearerToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'bearerToken', e.target.value)}
className="form-control"
placeholder="Twitter Bearer Token"
/>
</div>
</div>
<BaseConnector
connector={connector}
index={index}
onConnectorConfigChange={onConnectorConfigChange}
getConfigValue={getConfigValue}
fields={fields}
/>
);
};