Started work on TUI with ink via Cline

This commit is contained in:
2025-08-10 16:07:42 -05:00
parent ec6d49e37e
commit c528d0039d
12 changed files with 18886 additions and 51 deletions

View 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;