Started work on TUI with ink via Cline
This commit is contained in:
530
src/tui/components/screens/ConfigurationScreen.jsx
Normal file
530
src/tui/components/screens/ConfigurationScreen.jsx
Normal file
@@ -0,0 +1,530 @@
|
||||
const React = require("react");
|
||||
const { Box, Text, useInput, useApp } = require("ink");
|
||||
const { useAppState } = require("../../providers/AppProvider.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();
|
||||
|
||||
// Form fields configuration
|
||||
const formFields = [
|
||||
{
|
||||
id: "shopDomain",
|
||||
label: "Shopify Shop Domain",
|
||||
placeholder: "your-store.myshopify.com",
|
||||
description: "Your Shopify store domain (without https://)",
|
||||
validator: (value) => {
|
||||
if (!value || value.trim() === "") return "Domain is required";
|
||||
if (!value.includes(".myshopify.com") && !value.includes(".")) {
|
||||
return "Must be a valid Shopify domain";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "accessToken",
|
||||
label: "Shopify Access Token",
|
||||
placeholder: "shpat_your_access_token_here",
|
||||
description: "Your Shopify Admin API access token",
|
||||
secret: true,
|
||||
validator: (value) => {
|
||||
if (!value || value.trim() === "") return "Access token is required";
|
||||
if (value.length < 10) return "Token appears to be too short";
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "targetTag",
|
||||
label: "Target Product Tag",
|
||||
placeholder: "sale",
|
||||
description: "Products with this tag will be updated",
|
||||
validator: (value) => {
|
||||
if (!value || value.trim() === "") return "Target tag is required";
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "priceAdjustment",
|
||||
label: "Price Adjustment Percentage",
|
||||
placeholder: "10",
|
||||
description:
|
||||
"Percentage to adjust prices (positive for increase, negative for decrease)",
|
||||
validator: (value) => {
|
||||
if (!value || value.trim() === "") return "Percentage is required";
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return "Must be a valid number";
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "operationMode",
|
||||
label: "Operation Mode",
|
||||
placeholder: "update",
|
||||
description:
|
||||
"Choose between updating prices or rolling back to compare-at prices",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "update", label: "Update Prices" },
|
||||
{ value: "rollback", label: "Rollback Prices" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// State for form inputs
|
||||
const [formValues, setFormValues] = React.useState(
|
||||
formFields.reduce((acc, field) => {
|
||||
acc[field.id] = appState.configuration[field.id] || "";
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const [errors, setErrors] = React.useState({});
|
||||
const [focusedField, setFocusedField] = React.useState(0);
|
||||
const [showValidation, setShowValidation] = React.useState(false);
|
||||
|
||||
// Validate all fields
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
let isValid = true;
|
||||
|
||||
formFields.forEach((field) => {
|
||||
const error = field.validator(formValues[field.id]);
|
||||
if (error) {
|
||||
newErrors[field.id] = error;
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// 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
|
||||
if (key.shift) {
|
||||
// Shift+Tab - previous field
|
||||
setFocusedField((prev) =>
|
||||
prev > 0 ? prev - 1 : formFields.length - 1
|
||||
);
|
||||
} else {
|
||||
// Tab - next field
|
||||
setFocusedField((prev) =>
|
||||
prev < formFields.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
}
|
||||
} else if (key.return || key.enter) {
|
||||
// Handle Enter key
|
||||
if (focusedField === formFields.length - 1) {
|
||||
// Last field (Save button) - save configuration
|
||||
handleSave();
|
||||
} else {
|
||||
// Move to next field
|
||||
setFocusedField((prev) =>
|
||||
prev < formFields.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
}
|
||||
} else if (key.upArrow) {
|
||||
// Navigate up
|
||||
setFocusedField((prev) => (prev > 0 ? prev - 1 : formFields.length - 1));
|
||||
} else if (key.downArrow) {
|
||||
// Navigate down
|
||||
setFocusedField((prev) => (prev < formFields.length - 1 ? prev + 1 : 0));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle input changes
|
||||
const handleInputChange = (fieldId, value) => {
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
[fieldId]: value,
|
||||
}));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[fieldId]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[fieldId];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle save configuration
|
||||
const handleSave = () => {
|
||||
if (validateForm()) {
|
||||
// Convert price adjustment to number
|
||||
const config = {
|
||||
...formValues,
|
||||
priceAdjustment: parseFloat(formValues.priceAdjustment),
|
||||
isValid: true,
|
||||
lastTested: null,
|
||||
};
|
||||
|
||||
updateConfiguration(config);
|
||||
|
||||
// Save to environment variables
|
||||
saveToEnvironment(config);
|
||||
|
||||
navigateBack();
|
||||
} else {
|
||||
// Show validation errors
|
||||
setShowValidation(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Save configuration to environment variables
|
||||
const saveToEnvironment = (config) => {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const envPath = path.resolve(__dirname, "../../../.env");
|
||||
|
||||
let envContent = "";
|
||||
try {
|
||||
envContent = fs.readFileSync(envPath, "utf8");
|
||||
} catch (err) {
|
||||
// If file doesn't exist, create it
|
||||
envContent = "";
|
||||
}
|
||||
|
||||
// Update or add each configuration value
|
||||
const envVars = {
|
||||
SHOPIFY_SHOP_DOMAIN: config.shopDomain,
|
||||
SHOPIFY_ACCESS_TOKEN: config.accessToken,
|
||||
TARGET_TAG: config.targetTag,
|
||||
PRICE_ADJUSTMENT_PERCENTAGE: config.priceAdjustment,
|
||||
OPERATION_MODE: config.operationMode,
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
const regex = new RegExp(`^${key}=.*`, "m");
|
||||
const line = `${key}=${value}`;
|
||||
|
||||
if (envContent.match(regex)) {
|
||||
envContent = envContent.replace(regex, line);
|
||||
} else {
|
||||
envContent += `\n${line}`;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
} catch (error) {
|
||||
console.error("Failed to save configuration to environment:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle test connection
|
||||
const handleTestConnection = async () => {
|
||||
// Validate required fields first
|
||||
const requiredFields = ["shopDomain", "accessToken"];
|
||||
const tempErrors = {};
|
||||
let hasError = false;
|
||||
|
||||
requiredFields.forEach((fieldId) => {
|
||||
const field = formFields.find((f) => f.id === fieldId);
|
||||
const error = field.validator(formValues[fieldId]);
|
||||
if (error) {
|
||||
tempErrors[fieldId] = error;
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
setErrors(tempErrors);
|
||||
setShowValidation(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update configuration temporarily for testing
|
||||
updateConfiguration({
|
||||
...formValues,
|
||||
priceAdjustment: parseFloat(formValues.priceAdjustment),
|
||||
isValid: false,
|
||||
});
|
||||
|
||||
// Test connection (this would integrate with the actual service)
|
||||
// For now, we'll simulate the test
|
||||
setFocusedField(formFields.length); // Show loading state
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate API call
|
||||
|
||||
// Simulate test result
|
||||
const testSuccess = Math.random() > 0.2; // 80% success rate for demo
|
||||
updateConfiguration({
|
||||
...formValues,
|
||||
priceAdjustment: parseFloat(formValues.priceAdjustment),
|
||||
isValid: testSuccess,
|
||||
lastTested: new Date(),
|
||||
});
|
||||
|
||||
setFocusedField(formFields.length - 1); // Return to save button
|
||||
};
|
||||
|
||||
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"
|
||||
)
|
||||
),
|
||||
|
||||
// Form fields
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 2 },
|
||||
formFields.map((field, index) => {
|
||||
const isFocused = focusedField === index;
|
||||
const hasError = errors[field.id];
|
||||
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",
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
bold: true,
|
||||
color: isFocused ? "blue" : "white",
|
||||
},
|
||||
`${field.label}:`
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexGrow: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
` ${field.description}`
|
||||
)
|
||||
)
|
||||
),
|
||||
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: "blue",
|
||||
paddingX: 2,
|
||||
paddingY: 0.5,
|
||||
marginBottom: 0.5,
|
||||
backgroundColor:
|
||||
formValues[field.id] === option.value
|
||||
? "blue"
|
||||
: undefined,
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color:
|
||||
formValues[field.id] === option.value
|
||||
? "white"
|
||||
: "gray",
|
||||
},
|
||||
option.label
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: React.createElement(TextInput, {
|
||||
value: currentValue,
|
||||
placeholder: field.placeholder,
|
||||
mask: field.secret ? "*" : null,
|
||||
showCursor: isFocused,
|
||||
focus: isFocused,
|
||||
onChange: (value) => handleInputChange(field.id, value),
|
||||
style: {
|
||||
color: hasError ? "red" : isFocused ? "blue" : "white",
|
||||
bold: isFocused,
|
||||
},
|
||||
})
|
||||
),
|
||||
hasError &&
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "red", italic: true },
|
||||
` Error: ${errors[field.id]}`
|
||||
)
|
||||
);
|
||||
})
|
||||
),
|
||||
|
||||
// Action buttons
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "48%" },
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingX: 2,
|
||||
paddingY: 1,
|
||||
alignItems: "center",
|
||||
backgroundColor:
|
||||
focusedField === formFields.length ? "yellow" : undefined,
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: focusedField === formFields.length ? "black" : "white",
|
||||
bold: true,
|
||||
},
|
||||
focusedField === formFields.length
|
||||
? "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"
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Configuration status
|
||||
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()}`
|
||||
)
|
||||
),
|
||||
|
||||
// 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")
|
||||
),
|
||||
|
||||
// Validation message
|
||||
showValidation &&
|
||||
Object.keys(errors).length > 0 &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "column",
|
||||
marginTop: 2,
|
||||
padding: 1,
|
||||
borderStyle: "single",
|
||||
borderColor: "red",
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "red", bold: true },
|
||||
"Validation Errors:"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "red" },
|
||||
"Please fix the errors before saving."
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ConfigurationScreen;
|
||||
Reference in New Issue
Block a user