e-pick 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +339 -0
- package/README.md +109 -0
- package/package.json +39 -10
- package/public/css/styles.css +994 -0
- package/public/index.html +216 -34
- package/public/js/api-facade.js +113 -0
- package/public/js/app.js +285 -0
- package/public/js/cherry-pick-builder.js +165 -0
- package/public/js/commit-validator.js +183 -0
- package/public/js/file-parser.js +30 -0
- package/public/js/filter-strategy.js +225 -0
- package/public/js/observable.js +113 -0
- package/public/js/parsers/base-parser.js +92 -0
- package/public/js/parsers/csv-parser.js +88 -0
- package/public/js/parsers/excel-parser.js +142 -0
- package/public/js/parsers/parser-factory.js +69 -0
- package/public/js/stepper-states.js +319 -0
- package/public/js/ui-manager.js +668 -0
- package/src/cli.js +289 -0
- package/src/commands/cherry-pick.command.js +79 -0
- package/src/config/app.config.js +115 -0
- package/src/config/repo-manager.js +131 -0
- package/src/controllers/commit.controller.js +102 -0
- package/src/middleware/error.middleware.js +33 -0
- package/src/middleware/validation.middleware.js +61 -0
- package/src/server.js +121 -0
- package/src/services/git.service.js +277 -0
- package/src/services/validation.service.js +102 -0
- package/src/utils/error-handler.js +80 -0
- package/src/validators/commit.validator.js +160 -0
- package/cli.js +0 -111
- package/lib/pick-commit.js +0 -165
- package/public/script.js +0 -263
- package/public/styles.css +0 -179
- package/server.js +0 -154
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base File Parser (Template Method Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Defines the skeleton of the file parsing algorithm.
|
|
5
|
+
* Subclasses implement specific steps while the overall flow remains the same.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class BaseFileParser {
|
|
9
|
+
/**
|
|
10
|
+
* Template method - defines the parsing algorithm structure
|
|
11
|
+
* @param {File} file - File to parse
|
|
12
|
+
* @returns {Promise<object>} Parsed data
|
|
13
|
+
*/
|
|
14
|
+
async parse(file) {
|
|
15
|
+
// Step 1: Validate file
|
|
16
|
+
this.validateFile(file);
|
|
17
|
+
|
|
18
|
+
// Step 2: Read file content
|
|
19
|
+
const rawData = await this.readFile(file);
|
|
20
|
+
|
|
21
|
+
// Step 3: Structure the data
|
|
22
|
+
const structured = this.structureData(rawData);
|
|
23
|
+
|
|
24
|
+
// Step 4: Format and return result
|
|
25
|
+
return this.formatResult(structured);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate file (can be overridden by subclasses)
|
|
30
|
+
* @param {File} file - File to validate
|
|
31
|
+
* @throws {Error} If file is invalid
|
|
32
|
+
*/
|
|
33
|
+
validateFile(file) {
|
|
34
|
+
if (!file) {
|
|
35
|
+
throw new Error('No file provided');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!this.isValidFileType(file)) {
|
|
39
|
+
throw new Error(`Invalid file type: ${file.type}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if file type is valid (must be implemented by subclasses)
|
|
45
|
+
* @param {File} file - File to check
|
|
46
|
+
* @returns {boolean} True if valid
|
|
47
|
+
*/
|
|
48
|
+
isValidFileType(file) {
|
|
49
|
+
throw new Error('isValidFileType() must be implemented by subclass');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read file content (must be implemented by subclasses)
|
|
54
|
+
* @param {File} file - File to read
|
|
55
|
+
* @returns {Promise<any>} Raw file data
|
|
56
|
+
*/
|
|
57
|
+
async readFile(file) {
|
|
58
|
+
throw new Error('readFile() must be implemented by subclass');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Structure the raw data (must be implemented by subclasses)
|
|
63
|
+
* @param {any} rawData - Raw data from file
|
|
64
|
+
* @returns {object} Structured data with columns and rows
|
|
65
|
+
*/
|
|
66
|
+
structureData(rawData) {
|
|
67
|
+
throw new Error('structureData() must be implemented by subclass');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format the final result (can be overridden)
|
|
72
|
+
* @param {object} data - Structured data
|
|
73
|
+
* @returns {object} Formatted result
|
|
74
|
+
*/
|
|
75
|
+
formatResult(data) {
|
|
76
|
+
// Format columns as objects with name and index
|
|
77
|
+
const formattedColumns = data.columns.map((col, index) => ({
|
|
78
|
+
name: col != null && col !== '' ? String(col) : `Column ${index + 1}`,
|
|
79
|
+
index: index
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
columns: formattedColumns,
|
|
85
|
+
rows: data.rows,
|
|
86
|
+
meta: data.meta || {}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Export for use in other scripts
|
|
92
|
+
window.BaseFileParser = BaseFileParser;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV Parser (Template Method Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Implements CSV-specific parsing logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class CSVParser extends BaseFileParser {
|
|
8
|
+
/**
|
|
9
|
+
* Check if file is valid CSV type
|
|
10
|
+
* @param {File} file - File to check
|
|
11
|
+
* @returns {boolean} True if valid
|
|
12
|
+
*/
|
|
13
|
+
isValidFileType(file) {
|
|
14
|
+
const validTypes = ['text/csv', 'text/plain', 'application/csv'];
|
|
15
|
+
const validExtensions = ['.csv', '.tsv', '.txt'];
|
|
16
|
+
|
|
17
|
+
const hasValidType = validTypes.includes(file.type);
|
|
18
|
+
const hasValidExtension = validExtensions.some(ext =>
|
|
19
|
+
file.name.toLowerCase().endsWith(ext)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return hasValidType || hasValidExtension || file.type === '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read CSV file
|
|
27
|
+
* @param {File} file - CSV file
|
|
28
|
+
* @returns {Promise<object>} Parsed CSV data
|
|
29
|
+
*/
|
|
30
|
+
async readFile(file) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
Papa.parse(file, {
|
|
33
|
+
complete: (results) => {
|
|
34
|
+
resolve(results);
|
|
35
|
+
},
|
|
36
|
+
error: (error) => {
|
|
37
|
+
reject(new Error(`Failed to parse CSV: ${error.message}`));
|
|
38
|
+
},
|
|
39
|
+
skipEmptyLines: true
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structure CSV data
|
|
46
|
+
* @param {object} parsed - PapaParse result
|
|
47
|
+
* @returns {object} Structured data
|
|
48
|
+
*/
|
|
49
|
+
structureData(parsed) {
|
|
50
|
+
if (!parsed.data || parsed.data.length === 0) {
|
|
51
|
+
throw new Error('CSV file is empty');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// First row is headers
|
|
55
|
+
const columns = parsed.data[0];
|
|
56
|
+
const rows = parsed.data.slice(1).filter(row =>
|
|
57
|
+
row.some(cell => cell !== null && cell !== '')
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
columns,
|
|
62
|
+
rows,
|
|
63
|
+
meta: {
|
|
64
|
+
totalRows: rows.length,
|
|
65
|
+
errors: parsed.errors
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format result with CSV-specific metadata
|
|
72
|
+
* @param {object} data - Structured data
|
|
73
|
+
* @returns {object} Formatted result
|
|
74
|
+
*/
|
|
75
|
+
formatResult(data) {
|
|
76
|
+
const result = super.formatResult(data);
|
|
77
|
+
|
|
78
|
+
// Add CSV-specific warnings if there were parsing errors
|
|
79
|
+
if (data.meta.errors && data.meta.errors.length > 0) {
|
|
80
|
+
result.warnings = data.meta.errors.map(err => err.message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Export for use in other scripts
|
|
88
|
+
window.CSVParser = CSVParser;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel Parser (Template Method Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Implements Excel-specific parsing logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class ExcelParser extends BaseFileParser {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.workbook = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if file is valid Excel type
|
|
15
|
+
* @param {File} file - File to check
|
|
16
|
+
* @returns {boolean} True if valid
|
|
17
|
+
*/
|
|
18
|
+
isValidFileType(file) {
|
|
19
|
+
const validTypes = [
|
|
20
|
+
'application/vnd.ms-excel',
|
|
21
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
22
|
+
'application/vnd.ms-excel.sheet.macroEnabled.12'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const validExtensions = ['.xlsx', '.xls', '.xlsm'];
|
|
26
|
+
const hasValidType = validTypes.includes(file.type);
|
|
27
|
+
const hasValidExtension = validExtensions.some(ext =>
|
|
28
|
+
file.name.toLowerCase().endsWith(ext)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return hasValidType || hasValidExtension;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read Excel file
|
|
36
|
+
* @param {File} file - Excel file
|
|
37
|
+
* @returns {Promise<object>} Workbook object
|
|
38
|
+
*/
|
|
39
|
+
async readFile(file) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const reader = new FileReader();
|
|
42
|
+
|
|
43
|
+
reader.onload = (e) => {
|
|
44
|
+
try {
|
|
45
|
+
const data = new Uint8Array(e.target.result);
|
|
46
|
+
const workbook = XLSX.read(data, { type: 'array' });
|
|
47
|
+
this.workbook = workbook;
|
|
48
|
+
resolve(workbook);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
reject(new Error(`Failed to read Excel file: ${error.message}`));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
reader.onerror = () => {
|
|
55
|
+
reject(new Error('Failed to read file'));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
reader.readAsArrayBuffer(file);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Structure workbook data
|
|
64
|
+
* @param {object} workbook - XLSX workbook
|
|
65
|
+
* @returns {object} Structured data
|
|
66
|
+
*/
|
|
67
|
+
structureData(workbook) {
|
|
68
|
+
const sheetNames = workbook.SheetNames;
|
|
69
|
+
|
|
70
|
+
// Check if multiple sheets exist
|
|
71
|
+
if (sheetNames.length > 1) {
|
|
72
|
+
return {
|
|
73
|
+
needsSheetSelection: true,
|
|
74
|
+
sheets: sheetNames,
|
|
75
|
+
parser: this,
|
|
76
|
+
meta: {
|
|
77
|
+
sheetCount: sheetNames.length
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Single sheet - parse directly
|
|
83
|
+
const sheetName = sheetNames[0];
|
|
84
|
+
return this.parseSheet(workbook, sheetName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Parse a specific sheet from workbook
|
|
89
|
+
* @param {object} workbook - XLSX workbook
|
|
90
|
+
* @param {string} sheetName - Sheet name to parse
|
|
91
|
+
* @returns {object} Parsed sheet data
|
|
92
|
+
*/
|
|
93
|
+
parseSheet(workbook, sheetName) {
|
|
94
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
95
|
+
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
|
|
96
|
+
header: 1,
|
|
97
|
+
defval: ''
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (jsonData.length === 0) {
|
|
101
|
+
throw new Error('Sheet is empty');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// First row is headers
|
|
105
|
+
const columns = jsonData[0];
|
|
106
|
+
const rows = jsonData.slice(1).filter(row =>
|
|
107
|
+
row.some(cell => cell !== null && cell !== '')
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
columns,
|
|
112
|
+
rows,
|
|
113
|
+
meta: {
|
|
114
|
+
sheetName,
|
|
115
|
+
totalRows: rows.length
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Format result with Excel-specific metadata
|
|
122
|
+
* @param {object} data - Structured data
|
|
123
|
+
* @returns {object} Formatted result
|
|
124
|
+
*/
|
|
125
|
+
formatResult(data) {
|
|
126
|
+
// If needs sheet selection, return early
|
|
127
|
+
if (data.needsSheetSelection) {
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
needsSheetSelection: true,
|
|
131
|
+
sheets: data.sheets,
|
|
132
|
+
parser: this,
|
|
133
|
+
meta: data.meta
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return super.formatResult(data);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Export for use in other scripts
|
|
142
|
+
window.ExcelParser = ExcelParser;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser Factory (Factory Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Creates appropriate parser based on file type
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class ParserFactory {
|
|
8
|
+
/**
|
|
9
|
+
* Create parser for file
|
|
10
|
+
* @param {File} file - File to parse
|
|
11
|
+
* @returns {BaseFileParser} Appropriate parser instance
|
|
12
|
+
* @throws {Error} If no suitable parser found
|
|
13
|
+
*/
|
|
14
|
+
static createParser(file) {
|
|
15
|
+
if (!file) {
|
|
16
|
+
throw new Error('No file provided');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try Excel parser first
|
|
20
|
+
const excelParser = new ExcelParser();
|
|
21
|
+
if (excelParser.isValidFileType(file)) {
|
|
22
|
+
return excelParser;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try CSV parser
|
|
26
|
+
const csvParser = new CSVParser();
|
|
27
|
+
if (csvParser.isValidFileType(file)) {
|
|
28
|
+
return csvParser;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// No suitable parser found
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Unsupported file type: ${file.type || 'unknown'}. ` +
|
|
34
|
+
`Please upload .xlsx, .xls, or .csv files.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse file using appropriate parser
|
|
40
|
+
* @param {File} file - File to parse
|
|
41
|
+
* @returns {Promise<object>} Parsed result
|
|
42
|
+
*/
|
|
43
|
+
static async parseFile(file) {
|
|
44
|
+
const parser = ParserFactory.createParser(file);
|
|
45
|
+
return await parser.parse(file);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse specific sheet from Excel file
|
|
50
|
+
* @param {ExcelParser} parser - Excel parser instance
|
|
51
|
+
* @param {string} sheetName - Sheet name to parse
|
|
52
|
+
* @returns {object} Parsed sheet data
|
|
53
|
+
*/
|
|
54
|
+
static parseSheet(parser, sheetName) {
|
|
55
|
+
if (!(parser instanceof ExcelParser)) {
|
|
56
|
+
throw new Error('Parser must be an ExcelParser instance');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!parser.workbook) {
|
|
60
|
+
throw new Error('No workbook loaded');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = parser.parseSheet(parser.workbook, sheetName);
|
|
64
|
+
return parser.formatResult(data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Export for use in other scripts
|
|
69
|
+
window.ParserFactory = ParserFactory;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stepper States (State Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Each step in the wizard is represented as a state object
|
|
5
|
+
* with specific behaviors and validation logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base Step State
|
|
10
|
+
*/
|
|
11
|
+
class StepState {
|
|
12
|
+
constructor(context) {
|
|
13
|
+
this.context = context;
|
|
14
|
+
this.stepNumber = 0;
|
|
15
|
+
this.stepName = 'Unknown';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Called when entering this step
|
|
20
|
+
*/
|
|
21
|
+
enter() {
|
|
22
|
+
this.context.setCurrentStep(this.stepNumber);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called when leaving this step
|
|
27
|
+
*/
|
|
28
|
+
exit() {
|
|
29
|
+
// Override in subclasses if needed
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate if we can proceed to next step
|
|
34
|
+
* @returns {boolean} True if can proceed
|
|
35
|
+
*/
|
|
36
|
+
canProceed() {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get next state
|
|
42
|
+
* @returns {StepState|null} Next state or null if last step
|
|
43
|
+
*/
|
|
44
|
+
next() {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get previous state
|
|
50
|
+
* @returns {StepState|null} Previous state or null if first step
|
|
51
|
+
*/
|
|
52
|
+
previous() {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Step 1: Upload File
|
|
59
|
+
*/
|
|
60
|
+
class UploadFileState extends StepState {
|
|
61
|
+
constructor(context) {
|
|
62
|
+
super(context);
|
|
63
|
+
this.stepNumber = 1;
|
|
64
|
+
this.stepName = 'Upload File';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
enter() {
|
|
68
|
+
super.enter();
|
|
69
|
+
// File upload UI is already visible
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
canProceed() {
|
|
73
|
+
return this.context.hasFile();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
next() {
|
|
77
|
+
return new SelectColumnsState(this.context);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Step 2: Select Columns
|
|
83
|
+
*/
|
|
84
|
+
class SelectColumnsState extends StepState {
|
|
85
|
+
constructor(context) {
|
|
86
|
+
super(context);
|
|
87
|
+
this.stepNumber = 2;
|
|
88
|
+
this.stepName = 'Select Columns';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
enter() {
|
|
92
|
+
super.enter();
|
|
93
|
+
// Column selectors should be visible
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
canProceed() {
|
|
97
|
+
return this.context.hasSelectedColumns();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
next() {
|
|
101
|
+
return new ValidateState(this.context);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
previous() {
|
|
105
|
+
return new UploadFileState(this.context);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Step 3: Validate
|
|
111
|
+
*/
|
|
112
|
+
class ValidateState extends StepState {
|
|
113
|
+
constructor(context) {
|
|
114
|
+
super(context);
|
|
115
|
+
this.stepNumber = 3;
|
|
116
|
+
this.stepName = 'Validate';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
enter() {
|
|
120
|
+
super.enter();
|
|
121
|
+
// Show validation progress
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
canProceed() {
|
|
125
|
+
return this.context.hasValidationResults();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
next() {
|
|
129
|
+
return new ReviewResultsState(this.context);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
previous() {
|
|
133
|
+
return new SelectColumnsState(this.context);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Step 4: Review Results & Get Command
|
|
139
|
+
*/
|
|
140
|
+
class ReviewResultsState extends StepState {
|
|
141
|
+
constructor(context) {
|
|
142
|
+
super(context);
|
|
143
|
+
this.stepNumber = 4;
|
|
144
|
+
this.stepName = 'Review & Get Command';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
enter() {
|
|
148
|
+
super.enter();
|
|
149
|
+
// Show validation results and command
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
canProceed() {
|
|
153
|
+
return this.context.hasGeneratedCommand();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
next() {
|
|
157
|
+
return new ViewFilesState(this.context);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
previous() {
|
|
161
|
+
return new ValidateState(this.context);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Step 5: View Changed Files
|
|
167
|
+
*/
|
|
168
|
+
class ViewFilesState extends StepState {
|
|
169
|
+
constructor(context) {
|
|
170
|
+
super(context);
|
|
171
|
+
this.stepNumber = 5;
|
|
172
|
+
this.stepName = 'View Changed Files';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
enter() {
|
|
176
|
+
super.enter();
|
|
177
|
+
// Show changed files with diff commands
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
canProceed() {
|
|
181
|
+
return false; // Last step
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
next() {
|
|
185
|
+
return null; // No next step
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
previous() {
|
|
189
|
+
return new ReviewResultsState(this.context);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Stepper Context
|
|
195
|
+
* Manages state transitions
|
|
196
|
+
*/
|
|
197
|
+
class StepperContext {
|
|
198
|
+
constructor(uiManager) {
|
|
199
|
+
this.uiManager = uiManager;
|
|
200
|
+
this.currentState = new UploadFileState(this);
|
|
201
|
+
this.parsedData = null;
|
|
202
|
+
this.validationResults = null;
|
|
203
|
+
this.generatedCommand = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Set current step in UI
|
|
208
|
+
* @param {number} stepNumber - Step number
|
|
209
|
+
*/
|
|
210
|
+
setCurrentStep(stepNumber) {
|
|
211
|
+
if (this.uiManager && this.uiManager.advanceToStep) {
|
|
212
|
+
this.uiManager.advanceToStep(stepNumber);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Advance to next step
|
|
218
|
+
* @returns {boolean} True if advanced successfully
|
|
219
|
+
*/
|
|
220
|
+
async advanceToNext() {
|
|
221
|
+
if (!this.currentState.canProceed()) {
|
|
222
|
+
console.warn(`Cannot proceed from ${this.currentState.stepName}`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const nextState = this.currentState.next();
|
|
227
|
+
if (!nextState) {
|
|
228
|
+
console.warn('Already at last step');
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.currentState.exit();
|
|
233
|
+
this.currentState = nextState;
|
|
234
|
+
this.currentState.enter();
|
|
235
|
+
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Go back to previous step
|
|
241
|
+
* @returns {boolean} True if went back successfully
|
|
242
|
+
*/
|
|
243
|
+
goToPrevious() {
|
|
244
|
+
const previousState = this.currentState.previous();
|
|
245
|
+
if (!previousState) {
|
|
246
|
+
console.warn('Already at first step');
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.currentState.exit();
|
|
251
|
+
this.currentState = previousState;
|
|
252
|
+
this.currentState.enter();
|
|
253
|
+
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Go directly to a specific step
|
|
259
|
+
* @param {number} stepNumber - Step number to go to
|
|
260
|
+
*/
|
|
261
|
+
goToStep(stepNumber) {
|
|
262
|
+
const stateMap = {
|
|
263
|
+
1: UploadFileState,
|
|
264
|
+
2: SelectColumnsState,
|
|
265
|
+
3: ValidateState,
|
|
266
|
+
4: ReviewResultsState,
|
|
267
|
+
5: ViewFilesState
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const StateClass = stateMap[stepNumber];
|
|
271
|
+
if (!StateClass) {
|
|
272
|
+
console.error(`Invalid step number: ${stepNumber}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.currentState.exit();
|
|
277
|
+
this.currentState = new StateClass(this);
|
|
278
|
+
this.currentState.enter();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Context state checks
|
|
282
|
+
hasFile() {
|
|
283
|
+
return this.parsedData !== null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
hasSelectedColumns() {
|
|
287
|
+
return true; // In current implementation, columns are always selected
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
hasValidationResults() {
|
|
291
|
+
return this.validationResults !== null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
hasGeneratedCommand() {
|
|
295
|
+
return this.generatedCommand !== null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Context state setters
|
|
299
|
+
setParsedData(data) {
|
|
300
|
+
this.parsedData = data;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
setValidationResults(results) {
|
|
304
|
+
this.validationResults = results;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
setGeneratedCommand(command) {
|
|
308
|
+
this.generatedCommand = command;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Export for use in other scripts
|
|
313
|
+
window.StepState = StepState;
|
|
314
|
+
window.UploadFileState = UploadFileState;
|
|
315
|
+
window.SelectColumnsState = SelectColumnsState;
|
|
316
|
+
window.ValidateState = ValidateState;
|
|
317
|
+
window.ReviewResultsState = ReviewResultsState;
|
|
318
|
+
window.ViewFilesState = ViewFilesState;
|
|
319
|
+
window.StepperContext = StepperContext;
|