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.
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Commit Validator (Chain of Responsibility Pattern)
3
+ *
4
+ * Validates commits through a chain of validators.
5
+ * Each validator checks one aspect of the commit.
6
+ */
7
+
8
+ const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/i;
9
+
10
+ /**
11
+ * Base validator class
12
+ */
13
+ class BaseValidator {
14
+ constructor() {
15
+ this.nextValidator = null;
16
+ }
17
+
18
+ /**
19
+ * Set the next validator in the chain
20
+ * @param {BaseValidator} validator - Next validator
21
+ * @returns {BaseValidator} The next validator
22
+ */
23
+ setNext(validator) {
24
+ this.nextValidator = validator;
25
+ return validator;
26
+ }
27
+
28
+ /**
29
+ * Validate commit and pass to next validator
30
+ * @param {string} commitHash - Commit hash to validate
31
+ * @param {object} context - Validation context
32
+ * @returns {object} Validation result
33
+ */
34
+ async validate(commitHash, context) {
35
+ const result = await this._validate(commitHash, context);
36
+
37
+ if (!result.isValid || !this.nextValidator) {
38
+ return result;
39
+ }
40
+
41
+ return this.nextValidator.validate(commitHash, context);
42
+ }
43
+
44
+ /**
45
+ * Internal validation logic (to be implemented by subclasses)
46
+ * @param {string} commitHash - Commit hash
47
+ * @param {object} context - Validation context
48
+ * @returns {object} Validation result
49
+ * @protected
50
+ */
51
+ async _validate(commitHash, context) {
52
+ throw new Error('_validate method must be implemented');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Format Validator - Checks if commit hash has valid format
58
+ */
59
+ class FormatValidator extends BaseValidator {
60
+ async _validate(commitHash, context) {
61
+ if (!commitHash || typeof commitHash !== 'string') {
62
+ return {
63
+ commit: commitHash,
64
+ isValid: false,
65
+ error: {
66
+ type: 'FORMAT_ERROR',
67
+ message: 'Commit hash is required and must be a string',
68
+ },
69
+ canIgnore: false,
70
+ commitInfo: null,
71
+ };
72
+ }
73
+
74
+ const trimmedHash = commitHash.trim();
75
+
76
+ if (!COMMIT_HASH_REGEX.test(trimmedHash)) {
77
+ return {
78
+ commit: trimmedHash,
79
+ isValid: false,
80
+ error: {
81
+ type: 'FORMAT_ERROR',
82
+ message: 'Invalid commit hash format. Must be 40 hexadecimal characters',
83
+ },
84
+ canIgnore: false,
85
+ commitInfo: null,
86
+ };
87
+ }
88
+
89
+ return {
90
+ commit: trimmedHash,
91
+ isValid: true,
92
+ error: null,
93
+ canIgnore: false,
94
+ commitInfo: null,
95
+ };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Existence Validator - Checks if commit exists in repository
101
+ */
102
+ class ExistenceValidator extends BaseValidator {
103
+ async _validate(commitHash, context) {
104
+ const { gitService } = context;
105
+
106
+ if (!gitService) {
107
+ throw new Error('GitService is required in validation context');
108
+ }
109
+
110
+ const exists = gitService.validateCommit(commitHash);
111
+
112
+ if (!exists) {
113
+ return {
114
+ commit: commitHash,
115
+ isValid: false,
116
+ error: {
117
+ type: 'NOT_FOUND',
118
+ message: 'Commit does not exist in repository. It may not have been fetched from remote yet.',
119
+ },
120
+ canIgnore: true, // User can ignore if they know the commit should exist
121
+ commitInfo: null,
122
+ };
123
+ }
124
+
125
+ // Get commit info for display
126
+ const commitInfo = gitService.getCommitInfo(commitHash);
127
+
128
+ return {
129
+ commit: commitHash,
130
+ isValid: true,
131
+ error: null,
132
+ canIgnore: false,
133
+ commitInfo,
134
+ };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Create the validation chain
140
+ * @returns {BaseValidator} The first validator in the chain
141
+ */
142
+ export function createValidationChain() {
143
+ const formatValidator = new FormatValidator();
144
+ const existenceValidator = new ExistenceValidator();
145
+
146
+ formatValidator.setNext(existenceValidator);
147
+
148
+ return formatValidator;
149
+ }
150
+
151
+ /**
152
+ * Validate a single commit
153
+ * @param {string} commitHash - Commit hash to validate
154
+ * @param {object} context - Validation context with gitService
155
+ * @returns {Promise<object>} Validation result
156
+ */
157
+ export async function validateCommit(commitHash, context) {
158
+ const chain = createValidationChain();
159
+ return chain.validate(commitHash, context);
160
+ }
package/cli.js DELETED
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env node
2
- import meow from 'meow';
3
- import fs from 'fs';
4
- import pickCommit from './lib/pick-commit.js';
5
- import { startServer } from './server.js';
6
-
7
- // Define CLI options and usage
8
- const cli = meow(`
9
- Usage
10
- $ epick [options] <file_path>
11
-
12
- <file_path> - Path to the CSV file containing commit and repository information.
13
-
14
- Options
15
- --commit-index, -c Index of the commit ID column in the CSV file. Default is 5.
16
- --repo-index, -r Index of the repository name column in the CSV file. Default is 1.
17
- --repo-name, -n Name of the repository to filter commits by. If empty, the first valid repository name found will be used.
18
- --execute, -x Execute the cherry-pick command. Without this flag, the command will only simulate execution.
19
- --allow-empty-commit, -a Allows empty commits. Default is false.
20
- --verbose, -v Print detailed logs.
21
- --serve, -s Start the server to serve the pick code UI.
22
- --port, -p Port to run the server on. Default is 3000.
23
-
24
- Examples
25
- $ epick --commit-index 3 --repo-index 2 --repo-name "your-repo" --execute ./commits.csv
26
- Executes the cherry-pick command on the commit of the repository named "your-repo".
27
-
28
- $ epick -c 4 -r 1 -n "example-repo" ./commits.csv
29
- Simulates the cherry-pick command for "example-repo".
30
- `, {
31
- importMeta: import.meta,
32
- flags: {
33
- commitIndex: {
34
- type: 'number',
35
- shortFlag: 'c',
36
- default: 5,
37
- },
38
- repoIndex: {
39
- type: 'number',
40
- shortFlag: 'r',
41
- default: 1,
42
- },
43
- repoName: {
44
- type: 'string',
45
- shortFlag: 'n',
46
- default: ''
47
- },
48
- execute: {
49
- type: 'boolean',
50
- shortFlag: 'x'
51
- },
52
- allowEmptyCommit: {
53
- type: 'boolean',
54
- shortFlag: 'a',
55
- default: false
56
- },
57
- verbose: {
58
- type: 'boolean',
59
- shortFlag: 'v',
60
- default: false
61
- },
62
- serve: {
63
- type: 'boolean',
64
- shortFlag: 's',
65
- default: false,
66
- },
67
- port: {
68
- type: 'number',
69
- shortFlag: 'p',
70
- default: 3000,
71
- },
72
- },
73
- allowUnknownFlags: false
74
- });
75
-
76
- // Start the server if the --serve flag is provided
77
- if (cli.flags.serve) {
78
- startServer(cli.flags.port);
79
- } else {
80
- // Ensure a file path is provided
81
- if (cli.input.length === 0) {
82
- console.error("Error: No file path provided. Please specify the path to the CSV file.");
83
- process.exit(1);
84
- }
85
-
86
- const filePath = cli.input[0];
87
-
88
- // Check if the file exists
89
- if (!fs.existsSync(filePath)) {
90
- console.error(`Error: The file "${filePath}" does not exist.`);
91
- process.exit(1);
92
- }
93
-
94
- const options = cli.flags;
95
-
96
- // Log options and file path if verbose flag is set
97
- if (options.verbose) {
98
- console.log("Options:", options);
99
- console.log("Processing file:", filePath);
100
- }
101
-
102
- // Process the file and execute the pickCommit function
103
- (async () => {
104
- try {
105
- await pickCommit(filePath, options);
106
- } catch (error) {
107
- console.error("Error processing the file:", error);
108
- process.exit(1);
109
- }
110
- })();
111
- }
@@ -1,165 +0,0 @@
1
- import fs from 'fs';
2
- import { execSync } from 'child_process';
3
-
4
- const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/;
5
- const isValidCommit = (commit_id) => COMMIT_HASH_REGEX.test(commit_id);
6
-
7
- const uniqueArray = (array) => [...new Set(array)];
8
-
9
- function isExistCommit(commitId, repoPath) {
10
- try {
11
- if (!isValidCommit(commitId)) {
12
- return false;
13
- }
14
- // Change the current working directory to the repository path
15
- const originalCwd = process.cwd();
16
- process.chdir(repoPath);
17
-
18
- // Execute the git command to check the commit type
19
- const result = execSync(`git cat-file -t ${commitId}`, { stdio: 'pipe' }).toString().trim();
20
-
21
- // Change back to the original working directory
22
- process.chdir(originalCwd);
23
-
24
- // If the command's output starts with "commit", the ID is valid
25
- return result === 'commit';
26
- } catch (error) {
27
- if (error.message.includes('fatal: git cat-file: could not get object info')) {
28
- return false;
29
- }
30
- console.error(error.message);
31
- process.exit(error.status || 128);
32
- }
33
- }
34
-
35
- function getSheetData({ file_path, commit_index, repo_index }) {
36
- // Read the CSV file
37
- let csv;
38
- try {
39
- csv = fs.readFileSync(file_path, 'utf8');
40
- } catch (err) {
41
- console.error(`Failed to read file ${file_path}`, err);
42
- process.exit(1);
43
- }
44
-
45
- // Split the CSV data into an array of arrays
46
- const rawData = csv
47
- .split('\n')
48
- .map((line) => line.split('\t').map((item) => item?.trim()))
49
- .filter((line) => line.filter(Boolean).length > 2);
50
-
51
- const rows = rawData.map((line) => ({
52
- commit_id: line[commit_index],
53
- repo_name: line[repo_index],
54
- }));
55
-
56
- if (!isValidCommit(rows[0]?.commit_id)) {
57
- rows.shift(); // Remove header row
58
- }
59
- return rows;
60
- }
61
-
62
- function isMatchRepoName(row, selected_repo) {
63
- return row?.repo_name?.toLowerCase() === selected_repo.toLowerCase();
64
- }
65
-
66
- /**
67
- * Extracts and returns the repository name from the provided object.
68
- *
69
- * @param {Object} params - The parameters object.
70
- * @param {string} params.repo_name - The name of the repository to be extracted.
71
- * @param {Array.<{commit_id: string, repo_name: string}>} params.data - An array of objects,
72
- * each containing a `commit_id` and a `repo_name`. This parameter is not used in the current implementation.
73
- *
74
- * @returns {string} The repository name extracted from the `repo_name` parameter.
75
- */
76
- function getRepoName({ repo_name, data }) {
77
- if (repo_name) {
78
- return repo_name;
79
- }
80
- const allRepos = uniqueArray(data.map((row) => row.repo_name).filter(Boolean));
81
-
82
- const matchRepos = allRepos.map((name) => {
83
- const commitId = data.find((row) => isMatchRepoName(row, name) && row?.commit_id)?.commit_id;
84
-
85
- const isExist = isExistCommit(commitId, process.cwd());
86
- return isExist ? name : null;
87
- });
88
-
89
- const validRepoName = matchRepos.find(Boolean);
90
-
91
- if (!validRepoName) {
92
- console.error('Invalid repo name', 'Please check repoName', allRepos);
93
- process.exit(1);
94
- }
95
-
96
- return validRepoName;
97
- }
98
-
99
- /**
100
- * Processes a CSV file containing commit and repository information to generate and optionally execute a git cherry-pick command.
101
- *
102
- * This function reads a CSV file specified by the `filePath` parameter, filters the data based on the provided options,
103
- * and generates a git cherry-pick command for the commits in the selected repository. If the `execute` option is true,
104
- * the cherry-pick command is executed in the current repository.
105
- *
106
- * @param {string} filePath - The path to the CSV file containing commit and repository information.
107
- * @param {Object} options - An object containing options for processing the CSV file.
108
- * @param {number} options.commitIndex - The index of the commit ID column in the CSV file.
109
- * @param {number} options.repoIndex - The index of the repository name column in the CSV file.
110
- * @param {string} options.repoName - The name of the repository to filter commits by. If empty, the first valid repository name found will be used.
111
- * @param {boolean} options.execute - If true, the generated cherry-pick command will be executed.
112
- * @param {boolean} options.allowEmptyCommit - If true, allows do not throw error empty commits.
113
- */
114
- export default async function pickCommit(filePath, options) {
115
- const { commitIndex, repoIndex, repoName, execute, allowEmptyCommit } = options;
116
- // Read the CSV file
117
- const data = getSheetData({ file_path: filePath, commit_index: commitIndex, repo_index: repoIndex });
118
-
119
- const selectedRepoName = getRepoName({ repo_name: repoName, data });
120
-
121
- // Check for invalid repo index
122
- const dataOfRepo = data.filter((line) => isMatchRepoName(line, selectedRepoName));
123
-
124
- // Check for invalid commit hashes
125
- const invalidCommit = dataOfRepo.find((line) => {
126
- const commitId = line.commit_id;
127
- return !isValidCommit(commitId) && (!allowEmptyCommit || commitId);
128
- });
129
-
130
- if (invalidCommit) {
131
- console.error('Invalid commit', 'Please check commitIndex', invalidCommit);
132
- process.exit(1);
133
- }
134
-
135
- // Extract commit hashes
136
- const commits = dataOfRepo.map((line) => line.commit_id);
137
-
138
- // Run git show command for each commit
139
- const command = `git show -s --format="%ci %H" ${commits.join(' ')}`;
140
-
141
- let output;
142
- try {
143
- output = execSync(command, { encoding: 'utf8' }).trim();
144
- } catch (err) {
145
- console.error(`Failed to execute command ${command}`, err);
146
- process.exit(1);
147
- }
148
-
149
- // Sort the output data by commit date and hash
150
- const outputData = output
151
- .split('\n')
152
- .sort()
153
- .map((line) => line.split(' '));
154
-
155
- // Extract sorted commit hashes
156
- const sortedCommits = outputData.map((line) => line[3]);
157
-
158
- // Generate cherry-pick command
159
- const pickCommand = `git cherry-pick ${sortedCommits.join(' ')}`;
160
- console.log(pickCommand);
161
-
162
- if (execute) {
163
- execSync(pickCommand, { stdio: 'inherit' });
164
- }
165
- }
package/public/script.js DELETED
@@ -1,263 +0,0 @@
1
- const COMMIT_HASH_REGEX = /^[0-9a-f]{40}$/;
2
- const isValidCommit = (commit_id) => COMMIT_HASH_REGEX.test(commit_id);
3
-
4
- document.addEventListener('DOMContentLoaded', () => {
5
- const fileInput = document.getElementById('fileInput');
6
- const loading = document.getElementById('loading');
7
- const searchContainer = document.getElementById('searchContainer');
8
- const searchBox = document.getElementById('searchBox');
9
- const sheetSuggestions = document.getElementById('sheetSuggestions');
10
- const selectedSheetList = document.getElementById('selectedSheetList');
11
- const tableContainer = document.getElementById('tableContainer');
12
- const submitContainer = document.getElementById('submitContainer');
13
- const headerSelector1 = document.getElementById('headerSelector1');
14
- const headerSelector2 = document.getElementById('headerSelector2');
15
- let workbook;
16
-
17
- fileInput.addEventListener('change', () => {
18
- console.log('File selected:', fileInput.files[0].name);
19
- if (fileInput.files.length) {
20
- uploadFile(fileInput.files[0]);
21
- }
22
- });
23
-
24
- function uploadFile(file) {
25
- console.log('Uploading file:', file.name);
26
- const reader = new FileReader();
27
-
28
- reader.onload = (e) => {
29
- const data = new Uint8Array(e.target.result);
30
- workbook = XLSX.read(data, { type: 'array' });
31
- console.log('Workbook loaded:', workbook);
32
-
33
- // Hide loading indicator
34
- loading.style.display = 'none';
35
-
36
- // Show search box
37
- searchContainer.style.display = 'block';
38
-
39
- // Populate datalist with sheet names and valid row counts
40
- sheetSuggestions.innerHTML = '';
41
- workbook.SheetNames.forEach(sheet => {
42
- const sheetData = XLSX.utils.sheet_to_json(workbook.Sheets[sheet]);
43
- const validRowCount = sheetData.filter(row => {
44
- const validCellCount = Object.values(row).filter(value => value !== undefined && value !== '').length;
45
- return validCellCount >= 3;
46
- }).length;
47
-
48
- const option = document.createElement('option');
49
- option.value = `${sheet} (Valid Rows: ${validRowCount})`;
50
- sheetSuggestions.appendChild(option);
51
- console.log(`Sheet: ${sheet}, Valid Rows: ${validRowCount}`);
52
- });
53
-
54
- // Handle sheet selection
55
- searchBox.addEventListener('change', () => {
56
- const selectedSheet = searchBox.value.split(' (')[0]; // Extract sheet name
57
- if (selectedSheet && !Array.from(selectedSheetList.children).some(item => item.textContent.includes(selectedSheet))) {
58
- console.log('Sheet selected:', selectedSheet);
59
- const listItem = document.createElement('li');
60
- listItem.textContent = selectedSheet;
61
-
62
- // Create remove button
63
- const removeButton = document.createElement('button');
64
- removeButton.textContent = 'x';
65
- removeButton.style.marginLeft = '10px';
66
- removeButton.addEventListener('click', () => {
67
- console.log('Removing sheet:', selectedSheet);
68
- selectedSheetList.removeChild(listItem);
69
- updateTable();
70
- });
71
-
72
- listItem.appendChild(removeButton);
73
- selectedSheetList.appendChild(listItem);
74
- searchBox.value = ''; // Clear the search box
75
- updateTable();
76
- }
77
- });
78
- };
79
-
80
- // Show loading indicator
81
- loading.style.display = 'block';
82
- searchContainer.style.display = 'none';
83
- selectedSheetList.innerHTML = ''; // Clear selected sheets
84
- tableContainer.innerHTML = ''; // Clear previous table
85
- submitContainer.innerHTML = ''; // Clear previous submit button
86
-
87
- reader.readAsArrayBuffer(file);
88
- }
89
-
90
- // Update the table with merged data from selected sheets
91
- function updateTable() {
92
- const selectedSheets = Array.from(selectedSheetList.children).map(item => item.textContent.replace('x', '').trim());
93
- let mergedData = [];
94
- let validRowCounts = {};
95
-
96
- selectedSheets.forEach(sheetName => {
97
- console.log(`Processing sheet: ${sheetName}`);
98
- const sheet = workbook.Sheets[sheetName];
99
- let sheetData = XLSX.utils.sheet_to_json(sheet);
100
-
101
- // Filter out rows with fewer than 3 valid (non-empty) cells
102
- sheetData = sheetData.filter(row => {
103
- const validCellCount = Object.values(row).filter(value => value !== undefined && value !== '').length;
104
- const isValid = validCellCount >= 3;
105
- console.log(`Row: ${JSON.stringify(row)}, Valid Cells: ${validCellCount}, Valid: ${isValid}`);
106
- return isValid;
107
- });
108
-
109
- validRowCounts[sheetName] = sheetData.length;
110
- console.log(`Valid rows in sheet ${sheetName}: ${validRowCounts[sheetName]}`);
111
- mergedData = mergedData.concat(sheetData);
112
- });
113
-
114
- console.log('Merged Data:', mergedData);
115
- displayTable(mergedData, validRowCounts);
116
- }
117
-
118
- // Display merged data in a table
119
- function displayTable(data, validRowCounts) {
120
- tableContainer.innerHTML = ''; // Clear previous table
121
- submitContainer.innerHTML = ''; // Clear previous submit button
122
-
123
- if (data.length === 0) {
124
- tableContainer.textContent = 'No valid data to display.';
125
- return;
126
- }
127
-
128
- const table = document.createElement('table');
129
- const thead = document.createElement('thead');
130
- const tbody = document.createElement('tbody');
131
-
132
- // Create table headers
133
- const headers = Object.keys(data[0]);
134
- const headerRow = document.createElement('tr');
135
- headers.forEach(header => {
136
- const th = document.createElement('th');
137
- th.textContent = header;
138
- headerRow.appendChild(th);
139
- });
140
- thead.appendChild(headerRow);
141
-
142
- // Populate header selectors
143
- headerSelector1.innerHTML = '';
144
- headerSelector2.innerHTML = '';
145
- headers.forEach(header => {
146
- const option1 = document.createElement('option');
147
- option1.value = header;
148
- option1.textContent = header;
149
- headerSelector1.appendChild(option1);
150
-
151
- const option2 = document.createElement('option');
152
- option2.value = header;
153
- option2.textContent = header;
154
- headerSelector2.appendChild(option2);
155
- });
156
-
157
- // Set default values for header selectors
158
- const defaultRepoHeader = headers.find(header => header.toLowerCase().includes('repo'));
159
- const defaultCommitHeader = headers.find(header => header.toLowerCase().includes('commit'));
160
-
161
- if (defaultRepoHeader) {
162
- headerSelector1.value = defaultRepoHeader;
163
- }
164
-
165
- if (defaultCommitHeader) {
166
- headerSelector2.value = defaultCommitHeader;
167
- }
168
-
169
- // Enable header selectors
170
- headerSelector1.disabled = false;
171
- headerSelector2.disabled = false;
172
-
173
- // Create table rows
174
- data.forEach(row => {
175
- const tr = document.createElement('tr');
176
- headers.forEach(header => {
177
- const td = document.createElement('td');
178
- td.textContent = row[header] || '';
179
- tr.appendChild(td);
180
- });
181
- tbody.appendChild(tr);
182
- });
183
-
184
- table.appendChild(thead);
185
- table.appendChild(tbody);
186
- tableContainer.appendChild(table);
187
-
188
- // Add title with valid row counts
189
- const title = document.createElement('h2');
190
- title.textContent = 'Merged Data (Valid Rows: ' + Object.entries(validRowCounts).map(([sheet, count]) => `${sheet}: ${count}`).join(', ') + ')';
191
- tableContainer.insertBefore(title, table);
192
-
193
- // Create and display submit button if there is valid data
194
- if (data.length > 0) {
195
- const submitButton = document.createElement('button');
196
- submitButton.textContent = 'Submit';
197
- submitButton.addEventListener('click', () => {
198
- console.log('Submit button clicked');
199
- submitData(data);
200
- });
201
- submitContainer.appendChild(submitButton);
202
- }
203
- }
204
-
205
- // Submit data to API
206
- function submitData(data) {
207
- const repoColumn = headerSelector1.value;
208
- const commitColumn = headerSelector2.value;
209
-
210
- const payload = data.map(row => ({
211
- repo: row[repoColumn],
212
- commit_id: row[commitColumn]
213
- }));
214
-
215
- console.log('Payload:', payload);
216
-
217
- fetch('/api/submit', {
218
- method: 'POST',
219
- headers: {
220
- 'Content-Type': 'application/json'
221
- },
222
- body: JSON.stringify(payload)
223
- })
224
- .then(response => response.json())
225
- .then(result => {
226
- if (result.error) {
227
- showModal(result.error, false);
228
- } else {
229
- console.log('Success:', result);
230
- showModal(result.command, true);
231
- }
232
- })
233
- .catch(error => {
234
- console.error('Error:', error);
235
- showModal(error.message, false);
236
- });
237
- }
238
-
239
- // Show modal with message and copy button
240
- function showModal(message, isSuccess) {
241
- const modal = document.getElementById('modal');
242
- const modalMessage = document.getElementById('modalMessage');
243
- const copyButton = document.getElementById('copyButton');
244
-
245
- modalMessage.textContent = message;
246
- copyButton.style.display = isSuccess ? 'block' : 'none';
247
-
248
- modal.style.display = 'block';
249
-
250
- copyButton.onclick = () => {
251
- navigator.clipboard.writeText(message).then(() => {
252
- alert('Command copied to clipboard!');
253
- }).catch(err => {
254
- console.error('Failed to copy command:', err);
255
- });
256
- };
257
- }
258
-
259
- // Close modal when clicking the close button
260
- document.getElementById('closeButton').onclick = () => {
261
- document.getElementById('modal').style.display = 'none';
262
- };
263
- });