Skip to content

Commit 73d238a

Browse files
authored
Merge pull request #6087 from Shopify/add-confirmation-to-export
add confirmation step to exports
2 parents fb15f26 + 76b7f22 commit 73d238a

File tree

6 files changed

+88
-7
lines changed

6 files changed

+88
-7
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {confirmExportPrompt} from './confirm_export.js'
2+
import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui'
3+
import {describe, expect, vi, test} from 'vitest'
4+
5+
vi.mock('@shopify/cli-kit/node/ui')
6+
7+
describe('confirmExportPrompt', () => {
8+
test('returns true when user confirms', async () => {
9+
const toFile = 'data.sqlite'
10+
const fromStore = 'shop.myshopify.com'
11+
const message = `Export data from ${fromStore} to ${toFile}?`
12+
const confirmationMessage = 'Yes, export'
13+
const cancellationMessage = 'Cancel'
14+
15+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true)
16+
17+
const result = await confirmExportPrompt(fromStore, toFile)
18+
19+
expect(renderConfirmationPrompt).toHaveBeenCalledWith({
20+
message,
21+
confirmationMessage,
22+
cancellationMessage,
23+
})
24+
expect(result).toBe(true)
25+
})
26+
27+
test('returns false when user cancels', async () => {
28+
const toFile = 'export.sqlite'
29+
const fromStore = 'test-shop.myshopify.com'
30+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(false)
31+
const result = await confirmExportPrompt(fromStore, toFile)
32+
expect(result).toBe(false)
33+
})
34+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui'
2+
3+
export async function confirmExportPrompt(fromStore: string, toFile: string): Promise<boolean> {
4+
return renderConfirmationPrompt({
5+
message: `Export data from ${fromStore} to ${toFile}?`,
6+
confirmationMessage: 'Yes, export',
7+
cancellationMessage: 'Cancel',
8+
})
9+
}

packages/store/src/prompts/export_results.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ describe('renderExportResult', () => {
5858
renderExportResult(sourceShop, exportOperation)
5959

6060
expect(renderSuccess).toHaveBeenCalledWith({
61-
body: ['Export operation from', {info: 'source-shop.myshopify.com'}, 'complete'],
61+
body: [
62+
'Export operation from',
63+
{info: 'source-shop.myshopify.com'},
64+
'complete',
65+
{link: {label: 'export file available for download', url: 'https://example.com/results'}},
66+
],
6267
})
6368
expect(renderWarning).not.toHaveBeenCalled()
6469
})

packages/store/src/prompts/export_results.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export function renderExportResult(sourceShop: Shop, exportOperation: BulkDataOp
77

88
const storeOperations = exportOperation.organization.bulkData.operation.storeOperations
99
const hasErrors = storeOperations.some((op) => op.remoteOperationStatus === 'FAILED')
10+
const url = storeOperations[0]?.url
1011

1112
if (hasErrors) {
1213
msg.push(`completed with`)
@@ -16,6 +17,10 @@ export function renderExportResult(sourceShop: Shop, exportOperation: BulkDataOp
1617
})
1718
} else {
1819
msg.push('complete')
20+
if (url) {
21+
const link = {link: {label: 'export file available for download', url}}
22+
msg.push(link)
23+
}
1924
renderSuccess({
2025
body: msg,
2126
})

packages/store/src/services/store/operations/store-export.test.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import {BulkDataStoreExportStartResponse, BulkDataOperationByIdResponse} from '.
1111
import {renderCopyInfo} from '../../../prompts/copy_info.js'
1212
import {renderExportResult} from '../../../prompts/export_results.js'
1313
import {ValidationError, OperationError, ErrorCodes} from '../errors/errors.js'
14+
import {confirmExportPrompt} from '../../../prompts/confirm_export.js'
1415
import {describe, vi, expect, test, beforeEach} from 'vitest'
1516
import {renderTasks} from '@shopify/cli-kit/node/ui'
1617

1718
vi.mock('../utils/result-file-handler.js')
1819
vi.mock('@shopify/cli-kit/node/ui')
1920
vi.mock('../../../prompts/copy_info.js')
2021
vi.mock('../../../prompts/export_results.js')
22+
vi.mock('../../../prompts/confirm_export.js')
2123

2224
describe('StoreExportOperation', () => {
2325
const mockBpSession = 'mock-bp-session-token'
@@ -55,9 +57,26 @@ describe('StoreExportOperation', () => {
5557
})
5658
})
5759

60+
test('should show confirm prompt before export', async () => {
61+
vi.mocked(confirmExportPrompt).mockResolvedValue(true)
62+
await operation.execute('source.myshopify.com', 'export.sqlite', {})
63+
64+
expect(confirmExportPrompt).toHaveBeenCalledWith('source.myshopify.com', 'export.sqlite')
65+
expect(renderExportResult).toHaveBeenCalled()
66+
})
67+
68+
test('should skip confirmation when --no-prompt flag is provided', async () => {
69+
await operation.execute('source.myshopify.com', 'export.sqlite', {'no-prompt': true})
70+
71+
expect(confirmExportPrompt).not.toHaveBeenCalled()
72+
expect(renderExportResult).toHaveBeenCalled()
73+
})
74+
5875
test('should successfully export data from source shop', async () => {
76+
vi.mocked(confirmExportPrompt).mockResolvedValue(true)
5977
await operation.execute('source.myshopify.com', 'output.sqlite', {})
6078

79+
expect(confirmExportPrompt).toHaveBeenCalledWith('source.myshopify.com', 'output.sqlite')
6180
expect(renderCopyInfo).toHaveBeenCalledWith('Export Operation', 'source.myshopify.com', 'output.sqlite')
6281
expect(renderExportResult).toHaveBeenCalledWith(mockSourceShop, mockCompletedOperation)
6382
expect(mockResultFileHandler.promptAndHandleResultFile).toHaveBeenCalledWith(
@@ -81,7 +100,7 @@ describe('StoreExportOperation', () => {
81100
},
82101
])
83102

84-
const promise = operation.execute('nonexistent.myshopify.com', 'output.sqlite', {})
103+
const promise = operation.execute('nonexistent.myshopify.com', 'output.sqlite', {'no-prompt': true})
85104
await expect(promise).rejects.toThrow(ValidationError)
86105
await expect(promise).rejects.toMatchObject({
87106
code: ErrorCodes.SHOP_NOT_FOUND,
@@ -103,7 +122,7 @@ describe('StoreExportOperation', () => {
103122
const singleShopOrg: Organization = TEST_MOCK_DATA.singleShopOrganization
104123
mockApiClient.fetchOrganizations.mockResolvedValue([mockOrganization, singleShopOrg])
105124

106-
await operation.execute('source.myshopify.com', 'output.sqlite', {})
125+
await operation.execute('source.myshopify.com', 'output.sqlite', {'no-prompt': true})
107126

108127
expect(renderExportResult).toHaveBeenCalled()
109128
})
@@ -121,7 +140,7 @@ describe('StoreExportOperation', () => {
121140
return ctx
122141
})
123142

124-
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {})
143+
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {'no-prompt': true})
125144
await expect(promise).rejects.toThrow(OperationError)
126145
await expect(promise).rejects.toMatchObject({
127146
operation: 'export',
@@ -153,7 +172,7 @@ describe('StoreExportOperation', () => {
153172
isComplete: true,
154173
})
155174

156-
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {})
175+
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {'no-prompt': true})
157176
await expect(promise).rejects.toThrow(OperationError)
158177
await expect(promise).rejects.toMatchObject({
159178
operation: 'export',
@@ -181,7 +200,7 @@ describe('StoreExportOperation', () => {
181200
isComplete: true,
182201
})
183202

184-
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {})
203+
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {'no-prompt': true})
185204
await expect(promise).rejects.toThrow(OperationError)
186205
await expect(promise).rejects.toMatchObject({
187206
operation: 'export',
@@ -218,7 +237,7 @@ describe('StoreExportOperation', () => {
218237
return ctx
219238
})
220239

221-
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {})
240+
const promise = operation.execute('source.myshopify.com', 'output.sqlite', {'no-prompt': true})
222241
await expect(promise).rejects.toThrow(OperationError)
223242
await expect(promise).rejects.toMatchObject({
224243
operation: 'export',

packages/store/src/services/store/operations/store-export.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {BulkOperationTaskGenerator, BulkOperationContext} from '../utils/bulk-op
1010
import {renderCopyInfo} from '../../../prompts/copy_info.js'
1111
import {ValidationError, OperationError, ErrorCodes} from '../errors/errors.js'
1212
import {renderExportResult} from '../../../prompts/export_results.js'
13+
import {confirmExportPrompt} from '../../../prompts/confirm_export.js'
1314
import {Task, renderTasks} from '@shopify/cli-kit/node/ui'
15+
import {outputInfo} from '@shopify/cli-kit/node/output'
1416

1517
export class StoreExportOperation implements StoreOperation {
1618
fromArg: string | undefined
@@ -32,6 +34,13 @@ export class StoreExportOperation implements StoreOperation {
3234
const sourceShop = findStore(fromStore, this.orgs)
3335
this.validateShop(sourceShop)
3436

37+
if (!flags['no-prompt']) {
38+
if (!(await confirmExportPrompt(sourceShop.domain, toFile))) {
39+
outputInfo('Exiting.')
40+
process.exit(0)
41+
}
42+
}
43+
3544
renderCopyInfo('Export Operation', sourceShop.domain, toFile)
3645
const exportOperation = await this.exportDataWithProgress(sourceShop.organizationId, sourceShop, this.bpSession)
3746

0 commit comments

Comments
 (0)