994 lines
26 KiB
JavaScript
994 lines
26 KiB
JavaScript
const React = require("react");
|
|
const { Box, Text, useInput, useApp } = require("ink");
|
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
|
const InputField = require("../common/InputField.jsx");
|
|
const TextInput = require("ink-text-input").default;
|
|
|
|
/**
|
|
* Configuration Screen Component
|
|
* Form-based interface for setting up Shopify credentials and operation parameters
|
|
* Requirements: 5.2, 6.1, 6.2, 6.3
|
|
*/
|
|
const ConfigurationScreen = () => {
|
|
const { appState, updateConfiguration, navigateBack, updateUIState } =
|
|
useAppState();
|
|
const { exit } = useApp();
|
|
|
|
// Enhanced form fields configuration with improved validation
|
|
const formFields = [
|
|
{
|
|
id: "shopDomain",
|
|
label: "Shopify Shop Domain",
|
|
placeholder: "your-store.myshopify.com",
|
|
description: "Your Shopify store domain (without https://)",
|
|
required: true,
|
|
validator: (value) => {
|
|
if (!value || value.trim() === "") {
|
|
return { isValid: false, message: "Domain is required" };
|
|
}
|
|
const trimmedValue = value.trim();
|
|
|
|
// Check for valid domain format
|
|
if (!trimmedValue.includes(".")) {
|
|
return { isValid: false, message: "Must be a valid domain" };
|
|
}
|
|
|
|
// Check for Shopify domain or custom domain
|
|
if (
|
|
!trimmedValue.includes(".myshopify.com") &&
|
|
!trimmedValue.match(
|
|
/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.[a-zA-Z]{2,}$/
|
|
)
|
|
) {
|
|
return {
|
|
isValid: false,
|
|
message:
|
|
"Must be a valid Shopify domain (e.g., store.myshopify.com)",
|
|
};
|
|
}
|
|
|
|
// Check for protocol (should not include)
|
|
if (
|
|
trimmedValue.includes("http://") ||
|
|
trimmedValue.includes("https://")
|
|
) {
|
|
return {
|
|
isValid: false,
|
|
message: "Domain should not include http:// or https://",
|
|
};
|
|
}
|
|
|
|
return { isValid: true, message: "" };
|
|
},
|
|
},
|
|
{
|
|
id: "accessToken",
|
|
label: "Shopify Access Token",
|
|
placeholder: "shpat_your_access_token_here",
|
|
description: "Your Shopify Admin API access token",
|
|
secret: true,
|
|
required: true,
|
|
validator: (value) => {
|
|
if (!value || value.trim() === "") {
|
|
return { isValid: false, message: "Access token is required" };
|
|
}
|
|
const trimmedValue = value.trim();
|
|
|
|
if (trimmedValue.length < 10) {
|
|
return { isValid: false, message: "Token appears to be too short" };
|
|
}
|
|
|
|
// Basic format check for Shopify tokens
|
|
if (
|
|
!trimmedValue.startsWith("shpat_") &&
|
|
!trimmedValue.startsWith("shpca_") &&
|
|
!trimmedValue.startsWith("shppa_")
|
|
) {
|
|
return {
|
|
isValid: false,
|
|
message: "Token should start with shpat_, shpca_, or shppa_",
|
|
};
|
|
}
|
|
|
|
return { isValid: true, message: "" };
|
|
},
|
|
},
|
|
{
|
|
id: "targetTag",
|
|
label: "Target Product Tag",
|
|
placeholder: "sale",
|
|
description: "Products with this tag will be updated",
|
|
required: true,
|
|
validator: (value) => {
|
|
if (!value || value.trim() === "") {
|
|
return { isValid: false, message: "Target tag is required" };
|
|
}
|
|
const trimmedValue = value.trim();
|
|
|
|
// Check for valid tag format (no special characters except hyphens and underscores)
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(trimmedValue)) {
|
|
return {
|
|
isValid: false,
|
|
message:
|
|
"Tag can only contain letters, numbers, hyphens, and underscores",
|
|
};
|
|
}
|
|
|
|
if (trimmedValue.length > 255) {
|
|
return {
|
|
isValid: false,
|
|
message: "Tag must be 255 characters or less",
|
|
};
|
|
}
|
|
|
|
return { isValid: true, message: "" };
|
|
},
|
|
},
|
|
{
|
|
id: "priceAdjustment",
|
|
label: "Price Adjustment Percentage",
|
|
placeholder: "10",
|
|
description:
|
|
"Percentage to adjust prices (positive for increase, negative for decrease)",
|
|
required: true,
|
|
validator: (value) => {
|
|
if (!value || value.trim() === "") {
|
|
return { isValid: false, message: "Percentage is required" };
|
|
}
|
|
|
|
const num = parseFloat(value);
|
|
if (isNaN(num)) {
|
|
return { isValid: false, message: "Must be a valid number" };
|
|
}
|
|
|
|
if (num < -100) {
|
|
return {
|
|
isValid: false,
|
|
message: "Cannot decrease prices by more than 100%",
|
|
};
|
|
}
|
|
|
|
if (num > 1000) {
|
|
return {
|
|
isValid: false,
|
|
message: "Price increase cannot exceed 1000%",
|
|
};
|
|
}
|
|
|
|
if (num === 0) {
|
|
return { isValid: false, message: "Percentage cannot be zero" };
|
|
}
|
|
|
|
return { isValid: true, message: "" };
|
|
},
|
|
},
|
|
{
|
|
id: "operationMode",
|
|
label: "Operation Mode",
|
|
placeholder: "update",
|
|
description:
|
|
"Choose between updating prices or rolling back to compare-at prices",
|
|
type: "select",
|
|
required: true,
|
|
options: [
|
|
{ value: "update", label: "Update Prices" },
|
|
{ value: "rollback", label: "Rollback Prices" },
|
|
],
|
|
validator: (value) => {
|
|
const validModes = ["update", "rollback"];
|
|
if (!validModes.includes(value)) {
|
|
return {
|
|
isValid: false,
|
|
message: "Must select a valid operation mode",
|
|
};
|
|
}
|
|
return { isValid: true, message: "" };
|
|
},
|
|
},
|
|
];
|
|
|
|
// Enhanced state management for form inputs
|
|
const [formValues, setFormValues] = React.useState(() => {
|
|
const initialValues = {};
|
|
formFields.forEach((field) => {
|
|
initialValues[field.id] = appState.configuration[field.id] || "";
|
|
});
|
|
return initialValues;
|
|
});
|
|
|
|
const [fieldValidation, setFieldValidation] = React.useState({});
|
|
const [focusedField, setFocusedField] = React.useState(0);
|
|
const [showValidation, setShowValidation] = React.useState(false);
|
|
const [hasInteracted, setHasInteracted] = React.useState({});
|
|
|
|
// Real-time field validation
|
|
const validateField = React.useCallback(
|
|
(fieldId, value) => {
|
|
const field = formFields.find((f) => f.id === fieldId);
|
|
if (!field || !field.validator) {
|
|
return { isValid: true, message: "" };
|
|
}
|
|
|
|
const result = field.validator(value);
|
|
return typeof result === "object"
|
|
? result
|
|
: { isValid: !result, message: result || "" };
|
|
},
|
|
[formFields]
|
|
);
|
|
|
|
// Validate all fields
|
|
const validateForm = React.useCallback(() => {
|
|
const newValidation = {};
|
|
let isFormValid = true;
|
|
|
|
formFields.forEach((field) => {
|
|
const validation = validateField(field.id, formValues[field.id]);
|
|
newValidation[field.id] = validation;
|
|
if (!validation.isValid) {
|
|
isFormValid = false;
|
|
}
|
|
});
|
|
|
|
setFieldValidation(newValidation);
|
|
return isFormValid;
|
|
}, [formFields, formValues, validateField]);
|
|
|
|
// Update validation when form values change
|
|
React.useEffect(() => {
|
|
if (showValidation) {
|
|
validateForm();
|
|
}
|
|
}, [formValues, showValidation, validateForm]);
|
|
|
|
// Validate loaded configuration completeness
|
|
const validateLoadedConfiguration = React.useCallback((config) => {
|
|
if (!config) return false;
|
|
|
|
// Check if all required fields have values
|
|
const requiredFields = [
|
|
"shopDomain",
|
|
"accessToken",
|
|
"targetTag",
|
|
"priceAdjustment",
|
|
"operationMode",
|
|
];
|
|
return requiredFields.every((field) => {
|
|
const value = config[field];
|
|
return value !== undefined && value !== null && value !== "";
|
|
});
|
|
}, []);
|
|
|
|
// Load configuration from environment file on component mount
|
|
React.useEffect(() => {
|
|
const loadedConfig = loadFromEnvironment();
|
|
if (loadedConfig) {
|
|
// Update form values with loaded configuration
|
|
setFormValues(loadedConfig);
|
|
|
|
// Validate the loaded configuration
|
|
const isConfigComplete = validateLoadedConfiguration(loadedConfig);
|
|
|
|
// Update app state with loaded configuration
|
|
updateConfiguration({
|
|
...loadedConfig,
|
|
isValid: isConfigComplete,
|
|
lastTested: appState.configuration.lastTested, // Preserve test status
|
|
});
|
|
|
|
// If configuration is complete, validate all fields
|
|
if (isConfigComplete) {
|
|
const allInteracted = {};
|
|
formFields.forEach((field) => {
|
|
allInteracted[field.id] = true;
|
|
});
|
|
setHasInteracted(allInteracted);
|
|
}
|
|
}
|
|
}, [
|
|
loadFromEnvironment,
|
|
updateConfiguration,
|
|
validateLoadedConfiguration,
|
|
formFields,
|
|
appState.configuration.lastTested,
|
|
]);
|
|
|
|
// Handle keyboard input
|
|
useInput((input, key) => {
|
|
if (key.escape) {
|
|
// Go back to main menu
|
|
navigateBack();
|
|
} else if (key.tab || (key.tab && key.shift)) {
|
|
// Navigate between fields and action buttons
|
|
const totalFocusableItems = formFields.length + 2; // fields + test button + save button
|
|
if (key.shift) {
|
|
// Shift+Tab - previous field
|
|
setFocusedField((prev) =>
|
|
prev > 0 ? prev - 1 : totalFocusableItems - 1
|
|
);
|
|
} else {
|
|
// Tab - next field
|
|
setFocusedField((prev) =>
|
|
prev < totalFocusableItems - 1 ? prev + 1 : 0
|
|
);
|
|
}
|
|
} else if (key.return || key.enter) {
|
|
// Handle Enter key
|
|
if (focusedField === formFields.length) {
|
|
// Test Connection button
|
|
handleTestConnection();
|
|
} else if (focusedField === formFields.length + 1) {
|
|
// Save button
|
|
handleSave();
|
|
} else {
|
|
// Move to next field
|
|
setFocusedField((prev) =>
|
|
prev < formFields.length + 1 ? prev + 1 : 0
|
|
);
|
|
}
|
|
} else if (key.upArrow) {
|
|
// Navigate up
|
|
const totalFocusableItems = formFields.length + 2;
|
|
setFocusedField((prev) =>
|
|
prev > 0 ? prev - 1 : totalFocusableItems - 1
|
|
);
|
|
} else if (key.downArrow) {
|
|
// Navigate down
|
|
const totalFocusableItems = formFields.length + 2;
|
|
setFocusedField((prev) =>
|
|
prev < totalFocusableItems - 1 ? prev + 1 : 0
|
|
);
|
|
}
|
|
});
|
|
|
|
// Enhanced input change handler with real-time validation
|
|
const handleInputChange = React.useCallback(
|
|
(fieldId, value) => {
|
|
setFormValues((prev) => ({
|
|
...prev,
|
|
[fieldId]: value,
|
|
}));
|
|
|
|
// Mark field as interacted
|
|
setHasInteracted((prev) => ({
|
|
...prev,
|
|
[fieldId]: true,
|
|
}));
|
|
|
|
// Perform real-time validation for interacted fields
|
|
if (hasInteracted[fieldId] || showValidation) {
|
|
const validation = validateField(fieldId, value);
|
|
setFieldValidation((prev) => ({
|
|
...prev,
|
|
[fieldId]: validation,
|
|
}));
|
|
}
|
|
},
|
|
[hasInteracted, showValidation, validateField]
|
|
);
|
|
|
|
// Enhanced save handler with comprehensive validation
|
|
const handleSave = React.useCallback(() => {
|
|
setShowValidation(true);
|
|
|
|
// Mark all fields as interacted
|
|
const allInteracted = {};
|
|
formFields.forEach((field) => {
|
|
allInteracted[field.id] = true;
|
|
});
|
|
setHasInteracted(allInteracted);
|
|
|
|
if (validateForm()) {
|
|
try {
|
|
// Convert and validate price adjustment
|
|
const priceAdjustment = parseFloat(formValues.priceAdjustment);
|
|
|
|
const config = {
|
|
shopDomain: formValues.shopDomain.trim(),
|
|
accessToken: formValues.accessToken.trim(),
|
|
targetTag: formValues.targetTag.trim(),
|
|
priceAdjustment,
|
|
operationMode: formValues.operationMode,
|
|
isValid: true,
|
|
lastTested: appState.configuration.lastTested, // Preserve last test time
|
|
};
|
|
|
|
// Update application state
|
|
updateConfiguration(config);
|
|
|
|
// Save to environment file
|
|
saveToEnvironment(config);
|
|
|
|
// Navigate back to previous screen
|
|
navigateBack();
|
|
} catch (error) {
|
|
console.error("Error saving configuration:", error);
|
|
// Could add error state here for user feedback
|
|
}
|
|
}
|
|
}, [
|
|
formValues,
|
|
formFields,
|
|
validateForm,
|
|
updateConfiguration,
|
|
navigateBack,
|
|
appState.configuration.lastTested,
|
|
]);
|
|
|
|
// Load configuration from environment file
|
|
const loadFromEnvironment = React.useCallback(() => {
|
|
try {
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const envPath = path.resolve(process.cwd(), ".env");
|
|
|
|
// Check if .env file exists
|
|
if (!fs.existsSync(envPath)) {
|
|
return null; // No existing configuration
|
|
}
|
|
|
|
// Read and parse .env file
|
|
const envContent = fs.readFileSync(envPath, "utf8");
|
|
const envVars = {};
|
|
|
|
// Parse environment variables
|
|
envContent.split("\n").forEach((line) => {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine && !trimmedLine.startsWith("#")) {
|
|
const [key, ...valueParts] = trimmedLine.split("=");
|
|
if (key && valueParts.length > 0) {
|
|
envVars[key.trim()] = valueParts.join("=").trim();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Map environment variables to configuration
|
|
const config = {
|
|
shopDomain: envVars.SHOPIFY_SHOP_DOMAIN || "",
|
|
accessToken: envVars.SHOPIFY_ACCESS_TOKEN || "",
|
|
targetTag: envVars.TARGET_TAG || "",
|
|
priceAdjustment: parseFloat(envVars.PRICE_ADJUSTMENT_PERCENTAGE) || 0,
|
|
operationMode: envVars.OPERATION_MODE || "update",
|
|
};
|
|
|
|
// Validate loaded configuration
|
|
const hasValidData =
|
|
config.shopDomain || config.accessToken || config.targetTag;
|
|
|
|
return hasValidData ? config : null;
|
|
} catch (error) {
|
|
console.error("Failed to load configuration from environment:", error);
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
// Enhanced configuration persistence with better error handling and validation
|
|
const saveToEnvironment = React.useCallback(
|
|
(config) => {
|
|
try {
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const envPath = path.resolve(process.cwd(), ".env");
|
|
|
|
// Read existing .env file or create empty content
|
|
let envContent = "";
|
|
try {
|
|
envContent = fs.readFileSync(envPath, "utf8");
|
|
} catch (err) {
|
|
// If file doesn't exist, start with empty content
|
|
envContent = "";
|
|
}
|
|
|
|
// Prepare environment variables mapping
|
|
const envVars = {
|
|
SHOPIFY_SHOP_DOMAIN: config.shopDomain,
|
|
SHOPIFY_ACCESS_TOKEN: config.accessToken,
|
|
TARGET_TAG: config.targetTag,
|
|
PRICE_ADJUSTMENT_PERCENTAGE: config.priceAdjustment.toString(),
|
|
OPERATION_MODE: config.operationMode,
|
|
};
|
|
|
|
// Process each environment variable
|
|
for (const [key, value] of Object.entries(envVars)) {
|
|
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const regex = new RegExp(`^${escapedKey}=.*$`, "m");
|
|
const line = `${key}=${value}`;
|
|
|
|
if (envContent.match(regex)) {
|
|
// Update existing variable
|
|
envContent = envContent.replace(regex, line);
|
|
} else {
|
|
// Add new variable
|
|
if (envContent && !envContent.endsWith("\n")) {
|
|
envContent += "\n";
|
|
}
|
|
envContent += `${line}\n`;
|
|
}
|
|
}
|
|
|
|
// Write updated content to .env file
|
|
fs.writeFileSync(envPath, envContent, "utf8");
|
|
|
|
// Update UI state to show success
|
|
updateUIState({
|
|
lastSaveStatus: "success",
|
|
lastSaveTime: new Date(),
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to save configuration to environment:", error);
|
|
|
|
// Update UI state to show error
|
|
updateUIState({
|
|
lastSaveStatus: "error",
|
|
lastSaveError: error.message,
|
|
lastSaveTime: new Date(),
|
|
});
|
|
|
|
throw error; // Re-throw to handle in calling function
|
|
}
|
|
},
|
|
[updateUIState]
|
|
);
|
|
|
|
// Enhanced API connection testing with real Shopify service integration
|
|
const handleTestConnection = React.useCallback(async () => {
|
|
// Validate required fields first
|
|
const requiredFields = ["shopDomain", "accessToken"];
|
|
const tempValidation = {};
|
|
let hasError = false;
|
|
|
|
requiredFields.forEach((fieldId) => {
|
|
const field = formFields.find((f) => f.id === fieldId);
|
|
if (field && field.validator) {
|
|
const validation = field.validator(formValues[fieldId]);
|
|
const result =
|
|
typeof validation === "object"
|
|
? validation
|
|
: { isValid: !validation, message: validation || "" };
|
|
tempValidation[fieldId] = result;
|
|
if (!result.isValid) {
|
|
hasError = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (hasError) {
|
|
setFieldValidation((prev) => ({ ...prev, ...tempValidation }));
|
|
setShowValidation(true);
|
|
|
|
// Update UI state to show validation error
|
|
updateUIState({
|
|
lastTestStatus: "validation_error",
|
|
lastTestError: "Please fix validation errors before testing connection",
|
|
lastTestTime: new Date(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Update UI state to show testing in progress
|
|
updateUIState({
|
|
lastTestStatus: "testing",
|
|
lastTestError: null,
|
|
lastTestTime: new Date(),
|
|
});
|
|
|
|
// Create a temporary configuration for testing
|
|
const testConfig = {
|
|
shopDomain: formValues.shopDomain.trim(),
|
|
accessToken: formValues.accessToken.trim(),
|
|
targetTag: formValues.targetTag.trim(),
|
|
priceAdjustment: parseFloat(formValues.priceAdjustment),
|
|
operationMode: formValues.operationMode,
|
|
};
|
|
|
|
// Test the connection using ShopifyService
|
|
const ShopifyService = require("../../../services/shopify");
|
|
|
|
// Create a temporary service instance with test configuration
|
|
const originalEnv = process.env;
|
|
process.env = {
|
|
...originalEnv,
|
|
SHOPIFY_SHOP_DOMAIN: testConfig.shopDomain,
|
|
SHOPIFY_ACCESS_TOKEN: testConfig.accessToken,
|
|
TARGET_TAG: testConfig.targetTag,
|
|
PRICE_ADJUSTMENT_PERCENTAGE: testConfig.priceAdjustment.toString(),
|
|
OPERATION_MODE: testConfig.operationMode,
|
|
};
|
|
|
|
const shopifyService = new ShopifyService();
|
|
const testResult = await shopifyService.testConnection();
|
|
|
|
// Restore original environment
|
|
process.env = originalEnv;
|
|
|
|
if (testResult) {
|
|
// Connection successful
|
|
updateConfiguration({
|
|
...testConfig,
|
|
isValid: true,
|
|
lastTested: new Date(),
|
|
});
|
|
|
|
updateUIState({
|
|
lastTestStatus: "success",
|
|
lastTestError: null,
|
|
lastTestTime: new Date(),
|
|
});
|
|
} else {
|
|
// Connection failed
|
|
updateConfiguration({
|
|
...testConfig,
|
|
isValid: false,
|
|
lastTested: new Date(),
|
|
});
|
|
|
|
updateUIState({
|
|
lastTestStatus: "failed",
|
|
lastTestError:
|
|
"Failed to connect to Shopify API. Please check your credentials.",
|
|
lastTestTime: new Date(),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Connection test error:", error);
|
|
|
|
// Update configuration with test failure
|
|
updateConfiguration({
|
|
...formValues,
|
|
priceAdjustment: parseFloat(formValues.priceAdjustment),
|
|
isValid: false,
|
|
lastTested: new Date(),
|
|
});
|
|
|
|
updateUIState({
|
|
lastTestStatus: "error",
|
|
lastTestError:
|
|
error.message ||
|
|
"An unexpected error occurred during connection test",
|
|
lastTestTime: new Date(),
|
|
});
|
|
}
|
|
}, [formFields, formValues, updateConfiguration, updateUIState]);
|
|
|
|
return React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", padding: 1 },
|
|
// Header
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", marginBottom: 1 },
|
|
React.createElement(
|
|
Text,
|
|
{ bold: true, color: "cyan" },
|
|
"⚙️ Configuration"
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", fontSize: "small" },
|
|
"Set up your Shopify credentials and operation parameters"
|
|
)
|
|
),
|
|
|
|
// Enhanced form fields with improved validation display
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", marginBottom: 2 },
|
|
formFields.map((field, index) => {
|
|
const isFocused = focusedField === index;
|
|
const validation = fieldValidation[field.id] || {
|
|
isValid: true,
|
|
message: "",
|
|
};
|
|
const hasError =
|
|
!validation.isValid && (hasInteracted[field.id] || showValidation);
|
|
const currentValue = formValues[field.id] || "";
|
|
|
|
return React.createElement(
|
|
Box,
|
|
{
|
|
key: field.id,
|
|
borderStyle: "single",
|
|
borderColor: hasError ? "red" : isFocused ? "blue" : "gray",
|
|
paddingX: 1,
|
|
paddingY: 1,
|
|
marginBottom: 1,
|
|
flexDirection: "column",
|
|
},
|
|
// Field label and description
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
bold: true,
|
|
color: hasError ? "red" : isFocused ? "blue" : "white",
|
|
},
|
|
`${field.label}${field.required ? "*" : ""}:`
|
|
),
|
|
React.createElement(
|
|
Box,
|
|
{ flexGrow: 1 },
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray" },
|
|
` ${field.description}`
|
|
)
|
|
)
|
|
),
|
|
// Input field
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "row" },
|
|
field.type === "select"
|
|
? React.createElement(
|
|
Box,
|
|
{ flexDirection: "column" },
|
|
field.options.map((option, optIndex) =>
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
key: optIndex,
|
|
borderStyle:
|
|
formValues[field.id] === option.value
|
|
? "single"
|
|
: "none",
|
|
borderColor:
|
|
isFocused && formValues[field.id] === option.value
|
|
? "blue"
|
|
: "gray",
|
|
paddingX: 2,
|
|
paddingY: 0.5,
|
|
marginBottom: 0.5,
|
|
backgroundColor:
|
|
formValues[field.id] === option.value
|
|
? isFocused
|
|
? "blue"
|
|
: "gray"
|
|
: undefined,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color:
|
|
formValues[field.id] === option.value
|
|
? "white"
|
|
: "gray",
|
|
},
|
|
option.label
|
|
)
|
|
)
|
|
)
|
|
)
|
|
: React.createElement(InputField, {
|
|
value: currentValue,
|
|
placeholder: field.placeholder,
|
|
onChange: (value) => handleInputChange(field.id, value),
|
|
validation: field.validator,
|
|
showError: hasInteracted[field.id] || showValidation,
|
|
mask: field.secret ? "*" : undefined,
|
|
focus: isFocused,
|
|
required: field.required,
|
|
})
|
|
),
|
|
// Error message for select fields (InputField handles its own errors)
|
|
hasError &&
|
|
field.type === "select" &&
|
|
React.createElement(
|
|
Text,
|
|
{ color: "red", italic: true },
|
|
` ⚠ ${validation.message}`
|
|
),
|
|
// Success indicator for valid fields
|
|
!hasError &&
|
|
hasInteracted[field.id] &&
|
|
currentValue &&
|
|
React.createElement(
|
|
Text,
|
|
{ color: "green", italic: true },
|
|
` ✓ Valid`
|
|
)
|
|
);
|
|
})
|
|
),
|
|
|
|
// Action buttons
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "row", justifyContent: "space-between" },
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", width: "48%" },
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
borderStyle: "single",
|
|
borderColor:
|
|
appState.uiState?.lastTestStatus === "testing"
|
|
? "yellow"
|
|
: "gray",
|
|
paddingX: 2,
|
|
paddingY: 1,
|
|
alignItems: "center",
|
|
backgroundColor:
|
|
focusedField === formFields.length ? "yellow" : undefined,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color: focusedField === formFields.length ? "black" : "white",
|
|
bold: true,
|
|
},
|
|
appState.uiState?.lastTestStatus === "testing"
|
|
? "Testing..."
|
|
: "Test Connection"
|
|
)
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
|
"Verify your credentials"
|
|
)
|
|
),
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", width: "48%" },
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
borderStyle: "single",
|
|
borderColor: "green",
|
|
paddingX: 2,
|
|
paddingY: 1,
|
|
alignItems: "center",
|
|
backgroundColor:
|
|
focusedField === formFields.length + 1 ? "green" : undefined,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color: "white",
|
|
bold: true,
|
|
},
|
|
"Save & Exit"
|
|
)
|
|
),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
|
"Save configuration and return to menu"
|
|
)
|
|
)
|
|
),
|
|
|
|
// Enhanced configuration status with save information
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
flexDirection: "column",
|
|
marginTop: 2,
|
|
borderTopStyle: "single",
|
|
borderColor: "gray",
|
|
paddingTop: 2,
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color: appState.configuration.isValid ? "green" : "red",
|
|
bold: true,
|
|
},
|
|
`Configuration Status: ${
|
|
appState.configuration.isValid ? "✓ Valid" : "⚠ Incomplete"
|
|
}`
|
|
),
|
|
appState.configuration.lastTested &&
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", fontSize: "small" },
|
|
`Last tested: ${appState.configuration.lastTested.toLocaleString()}`
|
|
),
|
|
// Connection test status display
|
|
appState.uiState?.lastTestStatus &&
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color:
|
|
appState.uiState.lastTestStatus === "success"
|
|
? "green"
|
|
: appState.uiState.lastTestStatus === "testing"
|
|
? "yellow"
|
|
: "red",
|
|
fontSize: "small",
|
|
},
|
|
appState.uiState.lastTestStatus === "success"
|
|
? `✓ Connection test successful at ${appState.uiState.lastTestTime?.toLocaleTimeString()}`
|
|
: appState.uiState.lastTestStatus === "testing"
|
|
? "⏳ Testing connection..."
|
|
: `⚠ Connection test failed: ${
|
|
appState.uiState.lastTestError || "Unknown error"
|
|
}`
|
|
),
|
|
// Save status display
|
|
appState.uiState?.lastSaveStatus &&
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color:
|
|
appState.uiState.lastSaveStatus === "success" ? "green" : "red",
|
|
fontSize: "small",
|
|
},
|
|
appState.uiState.lastSaveStatus === "success"
|
|
? `✓ Saved to .env file at ${appState.uiState.lastSaveTime?.toLocaleTimeString()}`
|
|
: `⚠ Save failed: ${
|
|
appState.uiState.lastSaveError || "Unknown error"
|
|
}`
|
|
),
|
|
// Configuration file status
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", fontSize: "small" },
|
|
"Configuration will be saved to .env file in project root"
|
|
)
|
|
),
|
|
|
|
// Instructions
|
|
React.createElement(
|
|
Box,
|
|
{ flexDirection: "column", marginTop: 2 },
|
|
React.createElement(Text, { color: "gray" }, "Navigation:"),
|
|
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate fields"),
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray" },
|
|
" Tab/Shift+Tab - Next/Previous field"
|
|
),
|
|
React.createElement(Text, { color: "gray" }, " Enter - Save/Next field"),
|
|
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
|
),
|
|
|
|
// Enhanced validation summary
|
|
showValidation &&
|
|
React.createElement(
|
|
Box,
|
|
{
|
|
flexDirection: "column",
|
|
marginTop: 2,
|
|
padding: 1,
|
|
borderStyle: "single",
|
|
borderColor: Object.values(fieldValidation).some((v) => !v.isValid)
|
|
? "red"
|
|
: "green",
|
|
},
|
|
React.createElement(
|
|
Text,
|
|
{
|
|
color: Object.values(fieldValidation).some((v) => !v.isValid)
|
|
? "red"
|
|
: "green",
|
|
bold: true,
|
|
},
|
|
Object.values(fieldValidation).some((v) => !v.isValid)
|
|
? "⚠ Validation Errors:"
|
|
: "✓ All fields are valid"
|
|
),
|
|
Object.values(fieldValidation).some((v) => !v.isValid) &&
|
|
React.createElement(
|
|
Text,
|
|
{ color: "red" },
|
|
"Please fix the errors above before saving."
|
|
),
|
|
// Show count of valid/invalid fields
|
|
React.createElement(
|
|
Text,
|
|
{ color: "gray", fontSize: "small" },
|
|
`${Object.values(fieldValidation).filter((v) => v.isValid).length}/${
|
|
formFields.length
|
|
} fields valid`
|
|
)
|
|
)
|
|
);
|
|
};
|
|
|
|
module.exports = ConfigurationScreen;
|