Started work on TUI with ink via Cline
This commit is contained in:
55
.aicodeprep-gui
Normal file
55
.aicodeprep-gui
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# .aicodeprep-gui LLM/AI context helper settings file
|
||||||
|
# This file stores your preferences (checked code files, window size) for this folder.
|
||||||
|
# Generated by aicodeprep-gui.
|
||||||
|
# Homepage: https://wuu73.org/aicp
|
||||||
|
# GitHub: https://github.com/detroittommy879/aicodeprep-gui
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# aicodeprep-gui preferences file version 1.0
|
||||||
|
version=1.0
|
||||||
|
|
||||||
|
[window]
|
||||||
|
width=1920
|
||||||
|
height=1009
|
||||||
|
splitter_state=AAAA/wAAAAEAAAACAAADAAAAAPwB/////wEAAAACAA==
|
||||||
|
|
||||||
|
[format]
|
||||||
|
output_format=xml
|
||||||
|
|
||||||
|
[files]
|
||||||
|
.env.example
|
||||||
|
package.json
|
||||||
|
README.md
|
||||||
|
.kiro\specs\windows-compatible-tui\design.md
|
||||||
|
.kiro\specs\windows-compatible-tui\requirements.md
|
||||||
|
.kiro\specs\windows-compatible-tui\tasks.md
|
||||||
|
.kiro\steering\product.md
|
||||||
|
.kiro\steering\structure.md
|
||||||
|
.kiro\steering\tech.md
|
||||||
|
backend\.env
|
||||||
|
docs\enhanced-signal-handling.md
|
||||||
|
src\index.js
|
||||||
|
src\tui-entry.js
|
||||||
|
src\config\environment.js
|
||||||
|
src\services\product.js
|
||||||
|
src\services\progress.js
|
||||||
|
src\services\schedule.js
|
||||||
|
src\services\shopify.js
|
||||||
|
src\tui\TuiApplication.jsx
|
||||||
|
src\tui\components\Router.jsx
|
||||||
|
src\tui\components\StatusBar.jsx
|
||||||
|
src\tui\components\common\ProgressBar.js
|
||||||
|
src\tui\providers\AppProvider.jsx
|
||||||
|
src\utils\logger.js
|
||||||
|
src\utils\price.js
|
||||||
|
tests\index.test.js
|
||||||
|
tests\config\environment.test.js
|
||||||
|
tests\integration\rollback-workflow.test.js
|
||||||
|
tests\integration\scheduled-execution-workflow.test.js
|
||||||
|
tests\services\product.test.js
|
||||||
|
tests\services\progress.test.js
|
||||||
|
tests\services\schedule-error-handling.test.js
|
||||||
|
tests\services\schedule-signal-handling.test.js
|
||||||
|
tests\services\schedule.test.js
|
||||||
|
tests\services\shopify.test.js
|
||||||
|
tests\utils\logger.test.js
|
||||||
|
tests\utils\price.test.js
|
||||||
16032
fullcode.txt
Normal file
16032
fullcode.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { Box, Text } = require("ink");
|
const { Box, Text } = require("ink");
|
||||||
const AppProvider = require("./providers/AppProvider");
|
const AppProvider = require("./providers/AppProvider.jsx");
|
||||||
const Router = require("./components/Router");
|
const Router = require("./components/Router.jsx");
|
||||||
const StatusBar = require("./components/StatusBar");
|
const StatusBar = require("./components/StatusBar.jsx");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main TUI Application Component
|
* Main TUI Application Component
|
||||||
@@ -15,7 +15,7 @@ const TuiApplication = () => {
|
|||||||
null,
|
null,
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{ flexDirection: "column", height: "100%" },
|
{ flexDirection: "column" },
|
||||||
React.createElement(StatusBar),
|
React.createElement(StatusBar),
|
||||||
React.createElement(Router)
|
React.createElement(Router)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { Box, Text } = require("ink");
|
const { Box, Text } = require("ink");
|
||||||
const { useAppState } = require("../providers/AppProvider");
|
const { useAppState } = require("../providers/AppProvider.jsx");
|
||||||
|
|
||||||
// Import screen components (will be created in later tasks)
|
// Import screen components
|
||||||
// const MainMenuScreen = require('./screens/MainMenuScreen');
|
const MainMenuScreen = require("./screens/MainMenuScreen.jsx");
|
||||||
// const ConfigurationScreen = require('./screens/ConfigurationScreen');
|
const ConfigurationScreen = require("./screens/ConfigurationScreen.jsx");
|
||||||
// const OperationScreen = require('./screens/OperationScreen');
|
const OperationScreen = require("./screens/OperationScreen.jsx");
|
||||||
// const SchedulingScreen = require('./screens/SchedulingScreen');
|
const SchedulingScreen = require("./screens/SchedulingScreen.jsx");
|
||||||
// const LogViewerScreen = require('./screens/LogViewerScreen');
|
const LogViewerScreen = require("./screens/LogViewerScreen.jsx");
|
||||||
// const TagAnalysisScreen = require('./screens/TagAnalysisScreen');
|
const TagAnalysisScreen = require("./screens/TagAnalysisScreen.jsx");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router Component
|
* Router Component
|
||||||
@@ -18,44 +18,14 @@ const { useAppState } = require("../providers/AppProvider");
|
|||||||
const Router = () => {
|
const Router = () => {
|
||||||
const { appState } = useAppState();
|
const { appState } = useAppState();
|
||||||
|
|
||||||
// Temporary placeholder screens until actual screens are implemented
|
// Screen components mapping
|
||||||
const screens = {
|
const screens = {
|
||||||
"main-menu": () =>
|
"main-menu": MainMenuScreen,
|
||||||
React.createElement(
|
configuration: ConfigurationScreen,
|
||||||
Box,
|
operation: OperationScreen,
|
||||||
{ padding: 2 },
|
scheduling: SchedulingScreen,
|
||||||
React.createElement(Text, null, "Main Menu Screen - Coming Soon")
|
logs: LogViewerScreen,
|
||||||
),
|
"tag-analysis": TagAnalysisScreen,
|
||||||
configuration: () =>
|
|
||||||
React.createElement(
|
|
||||||
Box,
|
|
||||||
{ padding: 2 },
|
|
||||||
React.createElement(Text, null, "Configuration Screen - Coming Soon")
|
|
||||||
),
|
|
||||||
operation: () =>
|
|
||||||
React.createElement(
|
|
||||||
Box,
|
|
||||||
{ padding: 2 },
|
|
||||||
React.createElement(Text, null, "Operation Screen - Coming Soon")
|
|
||||||
),
|
|
||||||
scheduling: () =>
|
|
||||||
React.createElement(
|
|
||||||
Box,
|
|
||||||
{ padding: 2 },
|
|
||||||
React.createElement(Text, null, "Scheduling Screen - Coming Soon")
|
|
||||||
),
|
|
||||||
logs: () =>
|
|
||||||
React.createElement(
|
|
||||||
Box,
|
|
||||||
{ padding: 2 },
|
|
||||||
React.createElement(Text, null, "Log Viewer Screen - Coming Soon")
|
|
||||||
),
|
|
||||||
"tag-analysis": () =>
|
|
||||||
React.createElement(
|
|
||||||
Box,
|
|
||||||
{ padding: 2 },
|
|
||||||
React.createElement(Text, null, "Tag Analysis Screen - Coming Soon")
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the current screen component
|
// Get the current screen component
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { Box, Text } = require("ink");
|
const { Box, Text } = require("ink");
|
||||||
const { useAppState } = require("../providers/AppProvider");
|
const { useAppState } = require("../providers/AppProvider.jsx");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* StatusBar Component
|
* StatusBar Component
|
||||||
|
|||||||
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;
|
||||||
411
src/tui/components/screens/LogViewerScreen.jsx
Normal file
411
src/tui/components/screens/LogViewerScreen.jsx
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
const React = require("react");
|
||||||
|
const { Box, Text, useInput, useApp } = require("ink");
|
||||||
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||||
|
const SelectInput = require("ink-select-input").default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log Viewer Screen Component
|
||||||
|
* Displays application logs with filtering and navigation capabilities
|
||||||
|
* Requirements: 7.5, 7.6
|
||||||
|
*/
|
||||||
|
const LogViewerScreen = () => {
|
||||||
|
const { appState, navigateBack } = useAppState();
|
||||||
|
const { exit } = useApp();
|
||||||
|
|
||||||
|
// Mock log data (in a real implementation, this would read from log files)
|
||||||
|
const mockLogs = [
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 5),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Application started",
|
||||||
|
details: "Shopify Price Updater v1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 4),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Configuration loaded",
|
||||||
|
details: "Target tag: 'sale', Adjustment: 10%",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 3),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Testing Shopify connection",
|
||||||
|
details: "Connecting to store...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 2),
|
||||||
|
level: "SUCCESS",
|
||||||
|
message: "Connection successful",
|
||||||
|
details: "API version: 2023-01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 1),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Fetching products",
|
||||||
|
details: "Query: tag:sale",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 60 * 1),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Products found",
|
||||||
|
details: "Found 15 products with tag 'sale'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 30),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Starting price updates",
|
||||||
|
details: "Processing 15 products, 42 variants total",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 25),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Updating product",
|
||||||
|
details: "Product: 'Summer T-Shirt' - New price: $22.00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 20),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Updating product",
|
||||||
|
details: "Product: 'Winter Jacket' - New price: $110.00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 15),
|
||||||
|
level: "WARNING",
|
||||||
|
message: "Price update failed",
|
||||||
|
details: "Product: 'Limited Edition Sneaker' - Insufficient permissions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 10),
|
||||||
|
level: "INFO",
|
||||||
|
message: "Updating product",
|
||||||
|
details: "Product: 'Casual Jeans' - New price: $45.00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date(Date.now() - 1000 * 5),
|
||||||
|
level: "SUCCESS",
|
||||||
|
message: "Operation completed",
|
||||||
|
details: "Updated 40 of 42 variants (95.2% success rate)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// State for log viewing
|
||||||
|
const [logs, setLogs] = React.useState(mockLogs);
|
||||||
|
const [filteredLogs, setFilteredLogs] = React.useState(mockLogs);
|
||||||
|
const [filterLevel, setFilterLevel] = React.useState("ALL");
|
||||||
|
const [selectedLog, setSelectedLog] = React.useState(null);
|
||||||
|
const [showDetails, setShowDetails] = React.useState(false);
|
||||||
|
const [scrollPosition, setScrollPosition] = React.useState(0);
|
||||||
|
const [maxScroll, setMaxScroll] = React.useState(0);
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
const filterOptions = [
|
||||||
|
{ value: "ALL", label: "All Levels" },
|
||||||
|
{ value: "ERROR", label: "Errors" },
|
||||||
|
{ value: "WARNING", label: "Warnings" },
|
||||||
|
{ value: "INFO", label: "Info" },
|
||||||
|
{ value: "SUCCESS", label: "Success" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filter logs based on selected level
|
||||||
|
const filterLogs = () => {
|
||||||
|
if (filterLevel === "ALL") {
|
||||||
|
setFilteredLogs(logs);
|
||||||
|
} else {
|
||||||
|
setFilteredLogs(logs.filter((log) => log.level === filterLevel));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle keyboard input
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (key.escape) {
|
||||||
|
// Go back to main menu
|
||||||
|
navigateBack();
|
||||||
|
} else if (key.upArrow) {
|
||||||
|
// Navigate up in log list
|
||||||
|
if (selectedLog > 0) {
|
||||||
|
setSelectedLog(selectedLog - 1);
|
||||||
|
setShowDetails(false);
|
||||||
|
}
|
||||||
|
} else if (key.downArrow) {
|
||||||
|
// Navigate down in log list
|
||||||
|
if (selectedLog < filteredLogs.length - 1) {
|
||||||
|
setSelectedLog(selectedLog + 1);
|
||||||
|
setShowDetails(false);
|
||||||
|
}
|
||||||
|
} else if (key.return || key.enter) {
|
||||||
|
// Toggle log details
|
||||||
|
if (selectedLog !== null) {
|
||||||
|
setShowDetails(!showDetails);
|
||||||
|
}
|
||||||
|
} else if (key.r) {
|
||||||
|
// Refresh logs
|
||||||
|
setLogs(mockLogs);
|
||||||
|
setFilteredLogs(mockLogs);
|
||||||
|
setSelectedLog(null);
|
||||||
|
setShowDetails(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle filter change
|
||||||
|
const handleFilterChange = (option) => {
|
||||||
|
setFilterLevel(option.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get log level color
|
||||||
|
const getLogLevelColor = (level) => {
|
||||||
|
switch (level) {
|
||||||
|
case "ERROR":
|
||||||
|
return "red";
|
||||||
|
case "WARNING":
|
||||||
|
return "yellow";
|
||||||
|
case "INFO":
|
||||||
|
return "blue";
|
||||||
|
case "SUCCESS":
|
||||||
|
return "green";
|
||||||
|
default:
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format timestamp
|
||||||
|
const formatTimestamp = (date) => {
|
||||||
|
return date.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||||
|
// Header
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 2 },
|
||||||
|
React.createElement(Text, { bold: true, color: "cyan" }, "📋 Log Viewer"),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
"View application logs and operation history"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Filter controls
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "blue",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "blue" },
|
||||||
|
"Log Filters:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(Text, { color: "white" }, "Level: "),
|
||||||
|
React.createElement(SelectInput, {
|
||||||
|
items: filterOptions,
|
||||||
|
selectedIndex: filterOptions.findIndex(
|
||||||
|
(opt) => opt.value === filterLevel
|
||||||
|
),
|
||||||
|
onSelect: handleFilterChange,
|
||||||
|
itemComponent: ({ label, isSelected }) =>
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "blue" : "white",
|
||||||
|
bold: isSelected,
|
||||||
|
},
|
||||||
|
label
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", fontSize: "small" },
|
||||||
|
`Showing ${filteredLogs.length} of ${logs.length} log entries`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Log list
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexGrow: 1,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
filteredLogs.map((log, index) => {
|
||||||
|
const isSelected = selectedLog === index;
|
||||||
|
const isHighlighted = isSelected && !showDetails;
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
key: index,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: isSelected ? "blue" : "transparent",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 0.5,
|
||||||
|
backgroundColor: isHighlighted ? "blue" : undefined,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: getLogLevelColor(log.level),
|
||||||
|
bold: true,
|
||||||
|
width: 8,
|
||||||
|
},
|
||||||
|
log.level
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isHighlighted ? "white" : "gray",
|
||||||
|
width: 8,
|
||||||
|
},
|
||||||
|
formatTimestamp(log.timestamp)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isHighlighted ? "white" : "white",
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
log.message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
// Log details (when selected)
|
||||||
|
showDetails &&
|
||||||
|
selectedLog !== null &&
|
||||||
|
filteredLogs[selectedLog] &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "green",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "green" },
|
||||||
|
"Log Details:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", bold: true },
|
||||||
|
"Level: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: getLogLevelColor(filteredLogs[selectedLog].level) },
|
||||||
|
filteredLogs[selectedLog].level
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(Text, { color: "white", bold: true }, "Time: "),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
filteredLogs[selectedLog].timestamp.toLocaleString()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginTop: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", bold: true },
|
||||||
|
"Message:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
filteredLogs[selectedLog].message
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginTop: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", bold: true },
|
||||||
|
"Details:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", italic: true },
|
||||||
|
filteredLogs[selectedLog].details
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 2,
|
||||||
|
borderTopStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingTop: 2,
|
||||||
|
},
|
||||||
|
React.createElement(Text, { color: "gray" }, "Controls:"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate logs"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " Enter - View details"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " R - Refresh logs"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 0.5,
|
||||||
|
marginTop: 1,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", fontSize: "small" },
|
||||||
|
`Selected: ${
|
||||||
|
selectedLog !== null ? `Log #${selectedLog + 1}` : "None"
|
||||||
|
} | Details: ${showDetails ? "Visible" : "Hidden"}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = LogViewerScreen;
|
||||||
203
src/tui/components/screens/MainMenuScreen.jsx
Normal file
203
src/tui/components/screens/MainMenuScreen.jsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
const React = require("react");
|
||||||
|
const { Box, Text, useInput } = require("ink");
|
||||||
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Menu Screen Component
|
||||||
|
* Provides the primary navigation interface for the application
|
||||||
|
* Requirements: 5.1, 5.3, 7.1
|
||||||
|
*/
|
||||||
|
const MainMenuScreen = () => {
|
||||||
|
const { appState, navigateTo, updateUIState } = useAppState();
|
||||||
|
|
||||||
|
// Menu items configuration
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
id: "configuration",
|
||||||
|
label: "Configuration",
|
||||||
|
description: "Set up Shopify credentials and operation parameters",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "operation",
|
||||||
|
label: "Run Operation",
|
||||||
|
description: "Execute price update or rollback operation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scheduling",
|
||||||
|
label: "Scheduling",
|
||||||
|
description: "Configure scheduled operations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tag-analysis",
|
||||||
|
label: "Tag Analysis",
|
||||||
|
description: "Explore product tags in your store",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "logs",
|
||||||
|
label: "View Logs",
|
||||||
|
description: "Browse operation logs and history",
|
||||||
|
},
|
||||||
|
{ id: "exit", label: "Exit", description: "Quit the application" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle keyboard input
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (key.upArrow) {
|
||||||
|
// Navigate up in menu
|
||||||
|
const newIndex = Math.max(0, appState.uiState.selectedMenuIndex - 1);
|
||||||
|
updateUIState({ selectedMenuIndex: newIndex });
|
||||||
|
} else if (key.downArrow) {
|
||||||
|
// Navigate down in menu
|
||||||
|
const newIndex = Math.min(
|
||||||
|
menuItems.length - 1,
|
||||||
|
appState.uiState.selectedMenuIndex + 1
|
||||||
|
);
|
||||||
|
updateUIState({ selectedMenuIndex: newIndex });
|
||||||
|
} else if (key.return || key.enter || input === " ") {
|
||||||
|
// Select menu item
|
||||||
|
const selectedItem = menuItems[appState.uiState.selectedMenuIndex];
|
||||||
|
if (selectedItem.id === "exit") {
|
||||||
|
// Exit the application
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
// Navigate to selected screen
|
||||||
|
navigateTo(selectedItem.id);
|
||||||
|
}
|
||||||
|
} else if (input === "q" || input === "Q") {
|
||||||
|
// Quick exit with 'q'
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", padding: 1 },
|
||||||
|
// Header
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "cyan" },
|
||||||
|
"🛍️ Shopify Price Updater"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", fontSize: "small" },
|
||||||
|
"Terminal User Interface"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Welcome message
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "green", fontSize: "small" },
|
||||||
|
"Shopify Price Updater TUI"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", fontSize: "small" },
|
||||||
|
"Arrow keys: Navigate | Enter: Select"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Menu items
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
menuItems.map((item, index) => {
|
||||||
|
const isSelected = index === appState.uiState.selectedMenuIndex;
|
||||||
|
const isConfigured = appState.configuration.isValid;
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
key: item.id,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: isSelected ? "blue" : "gray",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 1,
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
bold: isSelected,
|
||||||
|
color: isSelected ? "blue" : "white",
|
||||||
|
},
|
||||||
|
`${isSelected ? "▶" : " "} ${item.label}`
|
||||||
|
),
|
||||||
|
// Configuration status indicator
|
||||||
|
item.id === "operation" &&
|
||||||
|
!isConfigured &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ marginLeft: 2 },
|
||||||
|
React.createElement(Text, { color: "red" }, "⚠️ Not Configured")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "cyan" : "gray",
|
||||||
|
italic: true,
|
||||||
|
},
|
||||||
|
` ${item.description}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
// Footer with instructions
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 3,
|
||||||
|
borderTopStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingTop: 2,
|
||||||
|
},
|
||||||
|
React.createElement(Text, { color: "gray" }, "Navigation:"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate menu"),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
" Enter/Space - Select item"
|
||||||
|
),
|
||||||
|
React.createElement(Text, { color: "gray" }, " q - Quick exit"),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
" Esc - Back (when available)"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Configuration status
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", justifyContent: "space-between", marginTop: 2 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: appState.configuration.isValid ? "green" : "red" },
|
||||||
|
`Configuration: ${
|
||||||
|
appState.configuration.isValid ? "✓ Complete" : "⚠ Incomplete"
|
||||||
|
}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
`Mode: ${appState.configuration.operationMode.toUpperCase()}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = MainMenuScreen;
|
||||||
484
src/tui/components/screens/OperationScreen.jsx
Normal file
484
src/tui/components/screens/OperationScreen.jsx
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
const React = require("react");
|
||||||
|
const { Box, Text, useInput, useApp } = require("ink");
|
||||||
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||||
|
const Spinner = require("ink-spinner").default;
|
||||||
|
const ProgressBar = require("../common/ProgressBar.jsx");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operation Screen Component
|
||||||
|
* Handles execution of price update or rollback operations with real-time progress monitoring
|
||||||
|
* Requirements: 7.2, 8.1, 8.2, 8.3, 8.4
|
||||||
|
*/
|
||||||
|
const OperationScreen = () => {
|
||||||
|
const { appState, navigateBack, updateOperationState } = useAppState();
|
||||||
|
const { exit } = useApp();
|
||||||
|
|
||||||
|
// Operation states
|
||||||
|
const [operationStatus, setOperationStatus] = React.useState("ready"); // ready, running, completed, cancelled, error
|
||||||
|
const [progress, setProgress] = React.useState(0);
|
||||||
|
const [currentStep, setCurrentStep] = React.useState("");
|
||||||
|
const [productsProcessed, setProductsProcessed] = React.useState(0);
|
||||||
|
const [totalProducts, setTotalProducts] = React.useState(0);
|
||||||
|
const [errors, setErrors] = React.useState([]);
|
||||||
|
const [results, setResults] = React.useState(null);
|
||||||
|
|
||||||
|
// Simulate operation execution (would integrate with actual services)
|
||||||
|
const executeOperation = async () => {
|
||||||
|
if (!appState.configuration.isValid) {
|
||||||
|
setOperationStatus("error");
|
||||||
|
setErrors([
|
||||||
|
"Configuration is not valid. Please configure your settings first.",
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOperationStatus("running");
|
||||||
|
setProgress(0);
|
||||||
|
setErrors([]);
|
||||||
|
setResults(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate fetching products
|
||||||
|
setCurrentStep("Fetching products...");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Simulate finding products
|
||||||
|
const mockProducts = [
|
||||||
|
{ id: "1", title: "Product A", variants: [{ id: "v1", price: 19.99 }] },
|
||||||
|
{ id: "2", title: "Product B", variants: [{ id: "v2", price: 29.99 }] },
|
||||||
|
{ id: "3", title: "Product C", variants: [{ id: "v3", price: 39.99 }] },
|
||||||
|
{ id: "4", title: "Product D", variants: [{ id: "v4", price: 49.99 }] },
|
||||||
|
{ id: "5", title: "Product E", variants: [{ id: "v5", price: 59.99 }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
setTotalProducts(mockProducts.length);
|
||||||
|
setProductsProcessed(0);
|
||||||
|
|
||||||
|
// Simulate processing each product
|
||||||
|
for (let i = 0; i < mockProducts.length; i++) {
|
||||||
|
const product = mockProducts[i];
|
||||||
|
|
||||||
|
// Simulate API call delay
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
const newProgress = ((i + 1) / mockProducts.length) * 100;
|
||||||
|
setProgress(newProgress);
|
||||||
|
setProductsProcessed(i + 1);
|
||||||
|
setCurrentStep(`Processing: ${product.title}`);
|
||||||
|
|
||||||
|
// Simulate occasional errors
|
||||||
|
if (Math.random() < 0.1 && i > 0) {
|
||||||
|
// 10% chance of error after first product
|
||||||
|
const error = {
|
||||||
|
productTitle: product.title,
|
||||||
|
productId: product.id,
|
||||||
|
errorMessage: "Simulated API error",
|
||||||
|
};
|
||||||
|
setErrors((prev) => [...prev, error]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete operation
|
||||||
|
setOperationStatus("completed");
|
||||||
|
setCurrentStep("Operation completed!");
|
||||||
|
setResults({
|
||||||
|
totalProducts: mockProducts.length,
|
||||||
|
totalVariants: mockProducts.reduce(
|
||||||
|
(sum, p) => sum + p.variants.length,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
successfulUpdates: mockProducts.length - errors.length,
|
||||||
|
failedUpdates: errors.length,
|
||||||
|
errors: errors,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setOperationStatus("error");
|
||||||
|
setErrors([
|
||||||
|
{ productTitle: "System Error", errorMessage: error.message },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle keyboard input
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (key.escape) {
|
||||||
|
// Cancel operation if running
|
||||||
|
if (operationStatus === "running") {
|
||||||
|
setOperationStatus("cancelled");
|
||||||
|
setCurrentStep("Operation cancelled by user");
|
||||||
|
} else {
|
||||||
|
// Go back to main menu
|
||||||
|
navigateBack();
|
||||||
|
}
|
||||||
|
} else if ((key.return || key.enter) && operationStatus === "ready") {
|
||||||
|
// Start operation
|
||||||
|
executeOperation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update operation state in context
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateOperationState({
|
||||||
|
status: operationStatus,
|
||||||
|
progress: progress,
|
||||||
|
currentStep: currentStep,
|
||||||
|
errors: errors,
|
||||||
|
results: results,
|
||||||
|
});
|
||||||
|
}, [operationStatus, progress, currentStep, errors, results]);
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", padding: 1 },
|
||||||
|
// Header
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
bold: true,
|
||||||
|
color: operationStatus === "running" ? "yellow" : "cyan",
|
||||||
|
},
|
||||||
|
`🚀 ${
|
||||||
|
appState.configuration.operationMode === "rollback"
|
||||||
|
? "Price Rollback"
|
||||||
|
: "Price Update"
|
||||||
|
} Operation`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", fontSize: "small" },
|
||||||
|
`Target tag: ${appState.configuration.targetTag || "Not set"}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Configuration summary
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "blue",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "blue" },
|
||||||
|
"Configuration Summary:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Store: ${appState.configuration.shopDomain || "Not set"}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Target Tag: ${appState.configuration.targetTag || "Not set"}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Mode: ${appState.configuration.operationMode.toUpperCase()}`
|
||||||
|
),
|
||||||
|
appState.configuration.operationMode === "update" &&
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Adjustment: ${appState.configuration.priceAdjustment || 0}%`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Operation status
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 3 },
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
bold: true,
|
||||||
|
color: getStatusColor(operationStatus),
|
||||||
|
},
|
||||||
|
getStatusIcon(operationStatus) + " " + getStatusText(operationStatus)
|
||||||
|
),
|
||||||
|
operationStatus === "running" &&
|
||||||
|
React.createElement(Spinner, { type: "dots" })
|
||||||
|
),
|
||||||
|
currentStep &&
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", italic: true },
|
||||||
|
`Current step: ${currentStep}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
operationStatus !== "ready" &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 3 },
|
||||||
|
React.createElement(ProgressBar, {
|
||||||
|
value: progress,
|
||||||
|
max: 100,
|
||||||
|
label: "Operation Progress",
|
||||||
|
showPercentage: true,
|
||||||
|
showValue: false,
|
||||||
|
style: {
|
||||||
|
bar: { fg: "green" },
|
||||||
|
empty: { fg: "gray" },
|
||||||
|
label: { fg: "white", bold: true },
|
||||||
|
percentage: { fg: "cyan" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", justifyContent: "space-between" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
`Products: ${productsProcessed}/${totalProducts}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
`${Math.round(progress)}%`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Results (when operation is completed)
|
||||||
|
operationStatus === "completed" &&
|
||||||
|
results &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "green",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "green" },
|
||||||
|
"Operation Results:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Total Products: ${results.totalProducts}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Total Variants: ${results.totalVariants}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "green" },
|
||||||
|
` Successful: ${results.successfulUpdates}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: results.failedUpdates > 0 ? "yellow" : "green" },
|
||||||
|
` Failed: ${results.failedUpdates}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Errors (if any)
|
||||||
|
errors.length > 0 &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "red",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "red" },
|
||||||
|
"Errors Encountered:"
|
||||||
|
),
|
||||||
|
errors
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((error, index) =>
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ key: index, flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "red" },
|
||||||
|
` • ${error.productTitle}: ${error.errorMessage}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
errors.length > 3 &&
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "red", italic: true },
|
||||||
|
` ... and ${errors.length - 3} more errors`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", justifyContent: "space-between", marginTop: 2 },
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", width: "48%" },
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: operationStatus === "ready" ? "green" : "gray",
|
||||||
|
paddingX: 2,
|
||||||
|
paddingY: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: operationStatus === "ready" ? "green" : undefined,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: operationStatus === "ready" ? "white" : "gray",
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
operationStatus === "ready"
|
||||||
|
? "Start Operation"
|
||||||
|
: "Operation Running"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
||||||
|
operationStatus === "ready"
|
||||||
|
? "Begin price update"
|
||||||
|
: "Processing products..."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", width: "48%" },
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "red",
|
||||||
|
paddingX: 2,
|
||||||
|
paddingY: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: operationStatus === "running" ? "red" : undefined,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: "white",
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
"Cancel Operation"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
||||||
|
operationStatus === "running"
|
||||||
|
? "Stop current operation"
|
||||||
|
: "Not running"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 2,
|
||||||
|
borderTopStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingTop: 2,
|
||||||
|
},
|
||||||
|
React.createElement(Text, { color: "gray" }, "Controls:"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " Enter - Start operation"),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
" Esc - Cancel/Back to menu"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function getStatusColor(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "ready":
|
||||||
|
return "blue";
|
||||||
|
case "running":
|
||||||
|
return "yellow";
|
||||||
|
case "completed":
|
||||||
|
return "green";
|
||||||
|
case "cancelled":
|
||||||
|
return "yellow";
|
||||||
|
case "error":
|
||||||
|
return "red";
|
||||||
|
default:
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusIcon(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "ready":
|
||||||
|
return "⏳";
|
||||||
|
case "running":
|
||||||
|
return "🔄";
|
||||||
|
case "completed":
|
||||||
|
return "✅";
|
||||||
|
case "cancelled":
|
||||||
|
return "⏹️";
|
||||||
|
case "error":
|
||||||
|
return "❌";
|
||||||
|
default:
|
||||||
|
return "❓";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "ready":
|
||||||
|
return "Ready to Start";
|
||||||
|
case "running":
|
||||||
|
return "Operation in Progress";
|
||||||
|
case "completed":
|
||||||
|
return "Operation Completed";
|
||||||
|
case "cancelled":
|
||||||
|
return "Operation Cancelled";
|
||||||
|
case "error":
|
||||||
|
return "Operation Error";
|
||||||
|
default:
|
||||||
|
return "Unknown Status";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = OperationScreen;
|
||||||
505
src/tui/components/screens/SchedulingScreen.jsx
Normal file
505
src/tui/components/screens/SchedulingScreen.jsx
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
const React = require("react");
|
||||||
|
const { Box, Text, useInput, useApp } = require("ink");
|
||||||
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||||
|
const TextInput = require("ink-text-input").default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheduling Screen Component
|
||||||
|
* Interface for configuring scheduled price update operations
|
||||||
|
* Requirements: 7.3, 7.4
|
||||||
|
*/
|
||||||
|
const SchedulingScreen = () => {
|
||||||
|
const { appState, navigateBack, updateConfiguration } = useAppState();
|
||||||
|
const { exit } = useApp();
|
||||||
|
|
||||||
|
// Form fields configuration
|
||||||
|
const formFields = [
|
||||||
|
{
|
||||||
|
id: "scheduledTime",
|
||||||
|
label: "Scheduled Execution Time",
|
||||||
|
placeholder: "2023-12-25T15:30:00",
|
||||||
|
description: "When to run the operation (ISO 8601 format)",
|
||||||
|
validator: (value) => {
|
||||||
|
if (!value || value.trim() === "") return "Scheduled time is required";
|
||||||
|
try {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (isNaN(date.getTime())) return "Invalid date format";
|
||||||
|
if (date <= new Date()) return "Scheduled time must be in the future";
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
return "Invalid date format";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scheduleType",
|
||||||
|
label: "Schedule Type",
|
||||||
|
placeholder: "one-time",
|
||||||
|
description: "Type of schedule",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ value: "one-time", label: "One-time" },
|
||||||
|
{ value: "daily", label: "Daily" },
|
||||||
|
{ value: "weekly", label: "Weekly" },
|
||||||
|
{ value: "monthly", label: "Monthly" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scheduleTime",
|
||||||
|
label: "Schedule Time",
|
||||||
|
placeholder: "15:30",
|
||||||
|
description: "Time of day for recurring schedules",
|
||||||
|
validator: (value) => {
|
||||||
|
if (!value || value.trim() === "") return "Time is required";
|
||||||
|
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
||||||
|
if (!timeRegex.test(value)) return "Invalid time format (HH:MM)";
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
const [nextRunTime, setNextRunTime] = React.useState(null);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate next run time
|
||||||
|
const calculateNextRunTime = () => {
|
||||||
|
if (!formValues.scheduledTime) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scheduledDate = new Date(formValues.scheduledTime);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Format for display
|
||||||
|
const options = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
};
|
||||||
|
|
||||||
|
return scheduledDate.toLocaleString(undefined, options);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update next run time when scheduled time changes
|
||||||
|
if (fieldId === "scheduledTime") {
|
||||||
|
setNextRunTime(calculateNextRunTime());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle save configuration
|
||||||
|
const handleSave = () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
// Update configuration
|
||||||
|
const config = {
|
||||||
|
...formValues,
|
||||||
|
isScheduled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConfiguration(config);
|
||||||
|
navigateBack();
|
||||||
|
} else {
|
||||||
|
// Show validation errors
|
||||||
|
setShowValidation(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle test schedule
|
||||||
|
const handleTestSchedule = () => {
|
||||||
|
// Validate required fields
|
||||||
|
const requiredFields = ["scheduledTime"];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate and display next run time
|
||||||
|
setNextRunTime(calculateNextRunTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||||
|
// Header
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 2 },
|
||||||
|
React.createElement(Text, { bold: true, color: "cyan" }, "⏰ Scheduling"),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
"Configure when to run price update operations"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Schedule information
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "blue",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "blue" },
|
||||||
|
"Schedule Information:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Current Mode: ${appState.configuration.operationMode.toUpperCase()}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Target Tag: ${appState.configuration.targetTag || "Not set"}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
null,
|
||||||
|
` Store: ${appState.configuration.shopDomain || "Not set"}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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: 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]}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
// Next run time display
|
||||||
|
nextRunTime &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "green",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "green" },
|
||||||
|
"Next Scheduled Run:"
|
||||||
|
),
|
||||||
|
React.createElement(Text, { color: "green" }, ` ${nextRunTime}`)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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 Schedule"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", italic: true, marginTop: 0.5 },
|
||||||
|
"Calculate next run time"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
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 schedule and return to menu"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 2,
|
||||||
|
borderTopStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingTop: 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 = SchedulingScreen;
|
||||||
645
src/tui/components/screens/TagAnalysisScreen.jsx
Normal file
645
src/tui/components/screens/TagAnalysisScreen.jsx
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
const React = require("react");
|
||||||
|
const { Box, Text, useInput, useApp } = require("ink");
|
||||||
|
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||||
|
const SelectInput = require("ink-select-input").default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag Analysis Screen Component
|
||||||
|
* Analyzes product tags and provides insights for price update operations
|
||||||
|
* Requirements: 7.7, 7.8
|
||||||
|
*/
|
||||||
|
const TagAnalysisScreen = () => {
|
||||||
|
const { appState, navigateBack } = useAppState();
|
||||||
|
const { exit } = useApp();
|
||||||
|
|
||||||
|
// Mock tag analysis data
|
||||||
|
const mockTagAnalysis = {
|
||||||
|
totalProducts: 150,
|
||||||
|
tagCounts: [
|
||||||
|
{ tag: "sale", count: 45, percentage: 30.0 },
|
||||||
|
{ tag: "new", count: 32, percentage: 21.3 },
|
||||||
|
{ tag: "featured", count: 28, percentage: 18.7 },
|
||||||
|
{ tag: "clearance", count: 22, percentage: 14.7 },
|
||||||
|
{ tag: "limited", count: 15, percentage: 10.0 },
|
||||||
|
{ tag: "seasonal", count: 8, percentage: 5.3 },
|
||||||
|
],
|
||||||
|
priceRanges: {
|
||||||
|
sale: { min: 9.99, max: 199.99, average: 59.5 },
|
||||||
|
new: { min: 19.99, max: 299.99, average: 89.75 },
|
||||||
|
featured: { min: 29.99, max: 399.99, average: 129.5 },
|
||||||
|
clearance: { min: 4.99, max: 149.99, average: 39.25 },
|
||||||
|
limited: { min: 49.99, max: 499.99, average: 199.5 },
|
||||||
|
seasonal: { min: 14.99, max: 249.99, average: 74.25 },
|
||||||
|
},
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
type: "high_impact",
|
||||||
|
title: "High-Impact Tags",
|
||||||
|
description:
|
||||||
|
"Tags with many products that would benefit most from price updates",
|
||||||
|
tags: ["sale", "clearance"],
|
||||||
|
reason:
|
||||||
|
"These tags have the highest product counts and are most likely to need price adjustments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "high_value",
|
||||||
|
title: "High-Value Tags",
|
||||||
|
description: "Tags with products having higher average prices",
|
||||||
|
tags: ["limited", "featured"],
|
||||||
|
reason:
|
||||||
|
"These tags contain premium products where price adjustments have the most financial impact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "caution",
|
||||||
|
title: "Use Caution",
|
||||||
|
description: "Tags that may require special handling",
|
||||||
|
tags: ["new", "seasonal"],
|
||||||
|
reason:
|
||||||
|
"These tags may have products with special pricing strategies that shouldn't be automatically adjusted",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// State for tag analysis
|
||||||
|
const [analysisData, setAnalysisData] = React.useState(mockTagAnalysis);
|
||||||
|
const [selectedTag, setSelectedTag] = React.useState(null);
|
||||||
|
const [showDetails, setShowDetails] = React.useState(false);
|
||||||
|
const [analysisType, setAnalysisType] = React.useState("overview");
|
||||||
|
|
||||||
|
// Analysis type options
|
||||||
|
const analysisOptions = [
|
||||||
|
{ value: "overview", label: "Overview" },
|
||||||
|
{ value: "distribution", label: "Tag Distribution" },
|
||||||
|
{ value: "pricing", label: "Pricing Analysis" },
|
||||||
|
{ value: "recommendations", label: "Recommendations" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle keyboard input
|
||||||
|
useInput((input, key) => {
|
||||||
|
if (key.escape) {
|
||||||
|
// Go back to main menu
|
||||||
|
navigateBack();
|
||||||
|
} else if (key.upArrow) {
|
||||||
|
// Navigate up in list
|
||||||
|
if (selectedTag > 0) {
|
||||||
|
setSelectedTag(selectedTag - 1);
|
||||||
|
setShowDetails(false);
|
||||||
|
}
|
||||||
|
} else if (key.downArrow) {
|
||||||
|
// Navigate down in list
|
||||||
|
if (selectedTag < analysisData.tagCounts.length - 1) {
|
||||||
|
setSelectedTag(selectedTag + 1);
|
||||||
|
setShowDetails(false);
|
||||||
|
}
|
||||||
|
} else if (key.return || key.enter) {
|
||||||
|
// Toggle tag details
|
||||||
|
if (selectedTag !== null) {
|
||||||
|
setShowDetails(!showDetails);
|
||||||
|
}
|
||||||
|
} else if (key.r) {
|
||||||
|
// Refresh analysis
|
||||||
|
setAnalysisData(mockTagAnalysis);
|
||||||
|
setSelectedTag(null);
|
||||||
|
setShowDetails(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle analysis type change
|
||||||
|
const handleAnalysisTypeChange = (option) => {
|
||||||
|
setAnalysisType(option.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get tag color based on count
|
||||||
|
const getTagColor = (count) => {
|
||||||
|
if (count >= 40) return "red";
|
||||||
|
if (count >= 25) return "yellow";
|
||||||
|
if (count >= 15) return "blue";
|
||||||
|
return "green";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render overview section
|
||||||
|
const renderOverview = () =>
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "blue",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "blue" },
|
||||||
|
"Tag Analysis Overview"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`Total Products Analyzed: ${analysisData.totalProducts}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`Unique Tags Found: ${analysisData.tagCounts.length}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`Most Common Tag: ${analysisData.tagCounts[0]?.tag || "N/A"} (${
|
||||||
|
analysisData.tagCounts[0]?.count || 0
|
||||||
|
} products)`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "cyan", marginBottom: 1 },
|
||||||
|
"Tag Distribution:"
|
||||||
|
),
|
||||||
|
analysisData.tagCounts.map((tagInfo, index) => {
|
||||||
|
const isSelected = selectedTag === index;
|
||||||
|
const barWidth = Math.round(
|
||||||
|
(tagInfo.count / analysisData.totalProducts) * 40
|
||||||
|
);
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
key: index,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: isSelected ? "blue" : "transparent",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 0.5,
|
||||||
|
marginBottom: 0.5,
|
||||||
|
backgroundColor: isSelected ? "blue" : undefined,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center", width: "100%" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "white",
|
||||||
|
bold: true,
|
||||||
|
width: 15,
|
||||||
|
},
|
||||||
|
tagInfo.tag
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "gray",
|
||||||
|
width: 8,
|
||||||
|
},
|
||||||
|
`${tagInfo.count}`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "gray",
|
||||||
|
width: 6,
|
||||||
|
},
|
||||||
|
`${tagInfo.percentage.toFixed(1)}%`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: getTagColor(tagInfo.count),
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
"█".repeat(barWidth) + "░".repeat(40 - barWidth)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render pricing analysis
|
||||||
|
const renderPricingAnalysis = () =>
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "cyan", marginBottom: 1 },
|
||||||
|
"Price Analysis by Tag:"
|
||||||
|
),
|
||||||
|
analysisData.tagCounts.map((tagInfo, index) => {
|
||||||
|
const priceRange = analysisData.priceRanges[tagInfo.tag];
|
||||||
|
if (!priceRange) return null;
|
||||||
|
|
||||||
|
const isSelected = selectedTag === index;
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
key: index,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: isSelected ? "blue" : "gray",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 1,
|
||||||
|
backgroundColor: isSelected ? "blue" : undefined,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "white",
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
tagInfo.tag
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "gray",
|
||||||
|
fontSize: "small",
|
||||||
|
},
|
||||||
|
`${tagInfo.count} products`
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", marginTop: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "cyan",
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
"Range: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "white",
|
||||||
|
},
|
||||||
|
`$${priceRange.min} - $${priceRange.max}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "cyan",
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
"Avg: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "white" : "white",
|
||||||
|
},
|
||||||
|
`$${priceRange.average.toFixed(2)}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render recommendations
|
||||||
|
const renderRecommendations = () =>
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "cyan", marginBottom: 1 },
|
||||||
|
"Recommendations:"
|
||||||
|
),
|
||||||
|
analysisData.recommendations.map((rec, index) => {
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "high_impact":
|
||||||
|
return "green";
|
||||||
|
case "high_value":
|
||||||
|
return "blue";
|
||||||
|
case "caution":
|
||||||
|
return "yellow";
|
||||||
|
default:
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeIcon = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "high_impact":
|
||||||
|
return "⭐";
|
||||||
|
case "high_value":
|
||||||
|
return "💎";
|
||||||
|
case "caution":
|
||||||
|
return "⚠️";
|
||||||
|
default:
|
||||||
|
return "ℹ️";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
key: index,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: getTypeColor(rec.type),
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 1,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: getTypeColor(rec.type),
|
||||||
|
bold: true,
|
||||||
|
},
|
||||||
|
`${getTypeIcon(rec.type)} ${rec.title}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", marginBottom: 1 },
|
||||||
|
rec.description
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", italic: true, marginBottom: 1 },
|
||||||
|
rec.reason
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "cyan", bold: true },
|
||||||
|
"Tags: " + rec.tags.join(", ")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render current analysis view
|
||||||
|
const renderCurrentView = () => {
|
||||||
|
switch (analysisType) {
|
||||||
|
case "overview":
|
||||||
|
return renderOverview();
|
||||||
|
case "pricing":
|
||||||
|
return renderPricingAnalysis();
|
||||||
|
case "recommendations":
|
||||||
|
return renderRecommendations();
|
||||||
|
default:
|
||||||
|
return renderOverview();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||||
|
// Header
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginBottom: 2 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "cyan" },
|
||||||
|
"🏷️ Tag Analysis"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray" },
|
||||||
|
"Analyze product tags to optimize price update operations"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Analysis type selector
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "blue",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "blue" },
|
||||||
|
"Analysis Type:"
|
||||||
|
),
|
||||||
|
React.createElement(SelectInput, {
|
||||||
|
items: analysisOptions,
|
||||||
|
selectedIndex: analysisOptions.findIndex(
|
||||||
|
(opt) => opt.value === analysisType
|
||||||
|
),
|
||||||
|
onSelect: handleAnalysisTypeChange,
|
||||||
|
itemComponent: ({ label, isSelected }) =>
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{
|
||||||
|
color: isSelected ? "blue" : "white",
|
||||||
|
bold: isSelected,
|
||||||
|
},
|
||||||
|
label
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Current analysis view
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexGrow: 1,
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: 1,
|
||||||
|
},
|
||||||
|
renderCurrentView()
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tag details (when selected)
|
||||||
|
showDetails &&
|
||||||
|
selectedTag !== null &&
|
||||||
|
analysisData.tagCounts[selectedTag] &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "green",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 1,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ bold: true, color: "green" },
|
||||||
|
"Tag Details:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(Text, { color: "white", bold: true }, "Tag: "),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
analysisData.tagCounts[selectedTag].tag
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", bold: true },
|
||||||
|
"Product Count: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
analysisData.tagCounts[selectedTag].count
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", bold: true },
|
||||||
|
"Percentage: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`${analysisData.tagCounts[selectedTag].percentage.toFixed(1)}%`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
analysisData.priceRanges[analysisData.tagCounts[selectedTag].tag] &&
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "column", marginTop: 1 },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white", bold: true },
|
||||||
|
"Pricing Information:"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "cyan", bold: true },
|
||||||
|
"Min: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`$${
|
||||||
|
analysisData.priceRanges[
|
||||||
|
analysisData.tagCounts[selectedTag].tag
|
||||||
|
].min
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "cyan", bold: true },
|
||||||
|
"Max: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`$${
|
||||||
|
analysisData.priceRanges[
|
||||||
|
analysisData.tagCounts[selectedTag].tag
|
||||||
|
].max
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{ flexDirection: "row", alignItems: "center" },
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "cyan", bold: true },
|
||||||
|
"Average: "
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "white" },
|
||||||
|
`$${analysisData.priceRanges[
|
||||||
|
analysisData.tagCounts[selectedTag].tag
|
||||||
|
].average.toFixed(2)}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: 2,
|
||||||
|
borderTopStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingTop: 2,
|
||||||
|
},
|
||||||
|
React.createElement(Text, { color: "gray" }, "Controls:"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate items"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " Enter - View details"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " R - Refresh analysis"),
|
||||||
|
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
React.createElement(
|
||||||
|
Box,
|
||||||
|
{
|
||||||
|
borderStyle: "single",
|
||||||
|
borderColor: "gray",
|
||||||
|
paddingX: 1,
|
||||||
|
paddingY: 0.5,
|
||||||
|
marginTop: 1,
|
||||||
|
},
|
||||||
|
React.createElement(
|
||||||
|
Text,
|
||||||
|
{ color: "gray", fontSize: "small" },
|
||||||
|
`View: ${
|
||||||
|
analysisOptions.find((opt) => opt.value === analysisType)?.label ||
|
||||||
|
"Overview"
|
||||||
|
} | Selected: ${selectedTag !== null ? `#${selectedTag + 1}` : "None"}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TagAnalysisScreen;
|
||||||
@@ -14,7 +14,7 @@ const initialState = {
|
|||||||
currentScreen: "main-menu",
|
currentScreen: "main-menu",
|
||||||
navigationHistory: [],
|
navigationHistory: [],
|
||||||
configuration: {
|
configuration: {
|
||||||
shopifyDomain: "",
|
shopDomain: "",
|
||||||
accessToken: "",
|
accessToken: "",
|
||||||
targetTag: "",
|
targetTag: "",
|
||||||
priceAdjustment: 0,
|
priceAdjustment: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user