diff --git a/packages/app/src/cli/prompts/dev.ts b/packages/app/src/cli/prompts/dev.ts index d745e5641bb..5f8abbebded 100644 --- a/packages/app/src/cli/prompts/dev.ts +++ b/packages/app/src/cli/prompts/dev.ts @@ -6,6 +6,7 @@ import {ApplicationURLs} from '../services/dev/urls.js' import { devStoreNamePrompt as sharedDevStoreNamePrompt, devStorePlanPrompt as sharedDevStorePlanPrompt, + devStoreDemoDataPrompt as sharedDevStoreDemoDataPrompt, } from '@shopify/organizations' import { RenderAutocompleteOptions, @@ -24,6 +25,10 @@ export function devStorePlanPrompt(): Promise { return sharedDevStorePlanPrompt() } +export function devStoreDemoDataPrompt(): Promise { + return sharedDevStoreDemoDataPrompt() +} + export async function selectAppPrompt( onSearchForAppsByName: (term: string) => Promise<{apps: MinimalOrganizationApp[]; hasMorePages: boolean}>, apps: MinimalOrganizationApp[], diff --git a/packages/app/src/cli/services/dev/select-store.test.ts b/packages/app/src/cli/services/dev/select-store.test.ts index 0e15477ee45..645b3e0fc13 100644 --- a/packages/app/src/cli/services/dev/select-store.test.ts +++ b/packages/app/src/cli/services/dev/select-store.test.ts @@ -2,7 +2,13 @@ import {selectStore} from './select-store.js' import {devStoreCapReached} from './cap.js' import {fetchStore, StoreNotFoundError} from './fetch.js' import {Organization, OrganizationSource, OrganizationStore} from '../../models/organization.js' -import {devStoreNamePrompt, devStorePlanPrompt, reloadStoreListPrompt, selectStorePrompt} from '../../prompts/dev.js' +import { + devStoreDemoDataPrompt, + devStoreNamePrompt, + devStorePlanPrompt, + reloadStoreListPrompt, + selectStorePrompt, +} from '../../prompts/dev.js' import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js' import {ClientName} from '../../utilities/developer-platform-client.js' import {sleep} from '@shopify/cli-kit/node/system' @@ -374,6 +380,7 @@ describe('selectStore', async () => { vi.mocked(devStoreCapReached).mockResolvedValue(false) vi.mocked(devStoreNamePrompt).mockResolvedValue('created-store') vi.mocked(devStorePlanPrompt).mockResolvedValue('grow') + vi.mocked(devStoreDemoDataPrompt).mockResolvedValue(false) vi.mocked(createDevStore).mockResolvedValue('created-store.myshopify.com') vi.mocked(fetchStore).mockResolvedValueOnce(STORE1) vi.mocked(renderTasks).mockImplementation(async (tasks: Task[]) => { @@ -392,6 +399,7 @@ describe('selectStore', async () => { expect(createDevStore).toHaveBeenCalledWith({ name: 'created-store', plan: 'grow', + withDemoData: false, organization: ORG1, json: false, summary: false, @@ -399,6 +407,30 @@ describe('selectStore', async () => { expect(renderSuccess).toHaveBeenCalledWith({headline: 'Development store "store1" created successfully.'}) }) + test('passes an affirmative demo data answer through to store creation', async () => { + const developerPlatformClient = testDeveloperPlatformClient({clientName: ClientName.AppManagement}) + vi.mocked(devStoreCapReached).mockResolvedValue(false) + vi.mocked(devStoreNamePrompt).mockResolvedValue('created-store') + vi.mocked(devStorePlanPrompt).mockResolvedValue('grow') + vi.mocked(devStoreDemoDataPrompt).mockResolvedValue(true) + vi.mocked(createDevStore).mockResolvedValue('created-store.myshopify.com') + vi.mocked(fetchStore).mockResolvedValueOnce(STORE1) + vi.mocked(renderTasks).mockImplementation(async (tasks: Task[]) => { + for (const task of tasks) { + // eslint-disable-next-line no-await-in-loop + await task.task({}, task) + } + return {} + }) + vi.mocked(selectStorePrompt).mockImplementation(async ({onCreateStore}) => onCreateStore!()) + + await expect( + selectStore({stores: [STORE2], hasMorePages: false}, ORG1, developerPlatformClient, 'selection-option'), + ).resolves.toEqual(STORE1) + expect(devStoreDemoDataPrompt).toHaveBeenCalled() + expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({withDemoData: true})) + }) + test('keeps the dashboard fallback when the app-management store prompt is cancelled', async () => { const developerPlatformClient = testDeveloperPlatformClient({clientName: ClientName.AppManagement}) vi.mocked(selectStorePrompt).mockResolvedValueOnce(undefined) @@ -418,6 +450,7 @@ describe('selectStore', async () => { vi.mocked(devStoreCapReached).mockResolvedValue(false) vi.mocked(devStoreNamePrompt).mockResolvedValue('created-store') vi.mocked(devStorePlanPrompt).mockResolvedValue('grow') + vi.mocked(devStoreDemoDataPrompt).mockResolvedValue(false) vi.mocked(createDevStore).mockResolvedValue('created-store.myshopify.com') vi.mocked(fetchStore) .mockRejectedValueOnce(new StoreNotFoundError('Store is still being provisioned')) @@ -437,6 +470,7 @@ describe('selectStore', async () => { expect(createDevStore).toHaveBeenCalledWith({ name: 'created-store', plan: 'grow', + withDemoData: false, organization: ORG1, json: false, summary: false, @@ -451,6 +485,7 @@ describe('selectStore', async () => { vi.mocked(devStoreCapReached).mockResolvedValue(false) vi.mocked(devStoreNamePrompt).mockResolvedValue('created-store') vi.mocked(devStorePlanPrompt).mockResolvedValue('grow') + vi.mocked(devStoreDemoDataPrompt).mockResolvedValue(false) vi.mocked(createDevStore).mockResolvedValue('created-store.myshopify.com') vi.mocked(fetchStore).mockRejectedValue(new StoreNotFoundError('Store is still being provisioned')) vi.mocked(renderTasks).mockImplementation(async (tasks: Task[]) => { @@ -476,6 +511,7 @@ describe('selectStore', async () => { vi.mocked(devStoreCapReached).mockResolvedValue(false) vi.mocked(devStoreNamePrompt).mockResolvedValue('created-store') vi.mocked(devStorePlanPrompt).mockResolvedValue('grow') + vi.mocked(devStoreDemoDataPrompt).mockResolvedValue(false) vi.mocked(createDevStore).mockResolvedValue('created-store.myshopify.com') vi.mocked(fetchStore).mockRejectedValueOnce(new AbortError('Fetching failed')) vi.mocked(renderTasks).mockImplementation(async (tasks: Task[]) => { diff --git a/packages/app/src/cli/services/dev/select-store.ts b/packages/app/src/cli/services/dev/select-store.ts index 52fd9606f0d..18eccf6c1de 100644 --- a/packages/app/src/cli/services/dev/select-store.ts +++ b/packages/app/src/cli/services/dev/select-store.ts @@ -1,7 +1,13 @@ import {devStoreCapReached} from './cap.js' import {fetchStore, StoreNotFoundError} from './fetch.js' import {Organization, OrganizationStore} from '../../models/organization.js' -import {devStoreNamePrompt, devStorePlanPrompt, reloadStoreListPrompt, selectStorePrompt} from '../../prompts/dev.js' +import { + devStoreDemoDataPrompt, + devStoreNamePrompt, + devStorePlanPrompt, + reloadStoreListPrompt, + selectStorePrompt, +} from '../../prompts/dev.js' import {ClientName, DeveloperPlatformClient, Paginateable} from '../../utilities/developer-platform-client.js' import {sleep} from '@shopify/cli-kit/node/system' import {isTTY, renderInfo, renderSuccess, renderTasks} from '@shopify/cli-kit/node/ui' @@ -30,7 +36,8 @@ export async function selectStore( const name = await devStoreNamePrompt() const plan = await devStorePlanPrompt() - const domain = await createDevStore({name, plan, organization: org, json: false, summary: false}) + const withDemoData = await devStoreDemoDataPrompt() + const domain = await createDevStore({name, plan, withDemoData, organization: org, json: false, summary: false}) const createdStore = await waitForCreatedStoreByDomain(org, domain, developerPlatformClient) renderSuccess({headline: `Development store "${createdStore.shopName}" created successfully.`}) return createdStore diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index e2a83181f36..f55a5b66b68 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -7191,7 +7191,7 @@ "examples": [ "<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\" --organization-id 1234567 --plan basic", - "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\" --organization-id 1234567 --plan basic --with-demo-data", + "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\" --organization-id 1234567 --plan basic --demo-data", "<%= config.bin %> <%= command.id %> --name \"Lavender Candles\" --organization-id 1234567 --plan basic --json" ], "flags": { @@ -7204,6 +7204,13 @@ "required": false, "type": "option" }, + "demo-data": { + "allowNo": true, + "description": "Populate the new development store with demo data.", + "env": "SHOPIFY_FLAG_STORE_DEMO_DATA", + "name": "demo-data", + "type": "boolean" + }, "feature-preview": { "description": "The handle of a feature preview to enable on the new development store.", "env": "SHOPIFY_FLAG_STORE_FEATURE_PREVIEW", @@ -7266,13 +7273,6 @@ "hidden": false, "name": "verbose", "type": "boolean" - }, - "with-demo-data": { - "allowNo": false, - "description": "Populate the new development store with demo data.", - "env": "SHOPIFY_FLAG_STORE_WITH_DEMO_DATA", - "name": "with-demo-data", - "type": "boolean" } }, "hasDynamicHelp": false, diff --git a/packages/organizations/src/cli/prompts/dev.ts b/packages/organizations/src/cli/prompts/dev.ts index 696fd87d4c1..ada6eeda95d 100644 --- a/packages/organizations/src/cli/prompts/dev.ts +++ b/packages/organizations/src/cli/prompts/dev.ts @@ -19,3 +19,12 @@ export function devStorePlanPrompt(): Promise { choices: devStorePlanHandles.map((handle) => ({label: PLAN_LABELS[handle], value: handle})), }) } + +export function devStoreDemoDataPrompt(): Promise { + return ui.renderConfirmationPrompt({ + message: 'Populate the store with demo data?', + confirmationMessage: 'Yes, add demo data', + cancellationMessage: 'No, start with an empty store', + defaultValue: true, + }) +} diff --git a/packages/organizations/src/index.ts b/packages/organizations/src/index.ts index 2f8ee150163..4d6a46a5f57 100644 --- a/packages/organizations/src/index.ts +++ b/packages/organizations/src/index.ts @@ -5,4 +5,4 @@ export type {Organization} from './cli/models/organization.js' export {businessPlatformTokenRefreshHandler} from './cli/services/business-platform.js' export {createDevStore, devStorePlanHandles} from './cli/services/dev/create-dev-store.js' export type {CreateDevStoreOptions, DevStorePlan} from './cli/services/dev/create-dev-store.js' -export {devStoreNamePrompt, devStorePlanPrompt} from './cli/prompts/dev.js' +export {devStoreNamePrompt, devStorePlanPrompt, devStoreDemoDataPrompt} from './cli/prompts/dev.js' diff --git a/packages/store/src/cli/commands/store/create/dev.test.ts b/packages/store/src/cli/commands/store/create/dev.test.ts index 962c04a6364..08a37266683 100644 --- a/packages/store/src/cli/commands/store/create/dev.test.ts +++ b/packages/store/src/cli/commands/store/create/dev.test.ts @@ -1,6 +1,6 @@ import StoreCreateDev from './dev.js' import {createDevStore} from '../../../services/store/create/dev.js' -import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js' +import {storeNamePrompt, storePlanPrompt, storeDemoDataPrompt} from '../../../prompts/store.js' import {selectOrg} from '@shopify/organizations' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' @@ -39,7 +39,15 @@ beforeEach(() => { describe('store create dev command', () => { test('resolves the organization and passes parsed flags through to the service', async () => { - await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345']) + await StoreCreateDev.run([ + '--name', + 'my-test-store', + '--plan', + 'plus', + '--organization-id', + '12345', + '--no-demo-data', + ]) expect(selectOrg).toHaveBeenCalledWith('12345') expect(createDevStore).toHaveBeenCalledWith({ @@ -54,7 +62,16 @@ describe('store create dev command', () => { }) test('passes json flag through to the service', async () => { - await StoreCreateDev.run(['--name', 'my-test-store', '--json', '--plan', 'plus', '--organization-id', '12345']) + await StoreCreateDev.run([ + '--name', + 'my-test-store', + '--json', + '--plan', + 'plus', + '--organization-id', + '12345', + '--no-demo-data', + ]) expect(createDevStore).toHaveBeenCalledWith({ name: 'my-test-store', @@ -67,7 +84,7 @@ describe('store create dev command', () => { }) }) - test('passes plan, feature-preview, and with-demo-data flags through to the service', async () => { + test('passes plan, feature-preview, and demo-data flags through to the service', async () => { await StoreCreateDev.run([ '--name', 'my-test-store', @@ -77,7 +94,7 @@ describe('store create dev command', () => { '12345', '--feature-preview', 'extended_variants', - '--with-demo-data', + '--demo-data', ]) expect(createDevStore).toHaveBeenCalledWith({ @@ -146,13 +163,53 @@ describe('store create dev command', () => { expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({plan: 'advanced'})) }) - test('does not prompt when all flags are provided', async () => { + test('does not prompt for the name or plan when both flags are provided', async () => { await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345']) expect(storeNamePrompt).not.toHaveBeenCalled() expect(storePlanPrompt).not.toHaveBeenCalled() }) + test('prompts for demo data when the flag is omitted in an interactive environment', async () => { + vi.mocked(storeDemoDataPrompt).mockResolvedValue(true) + + await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345']) + + expect(storeDemoDataPrompt).toHaveBeenCalled() + expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({withDemoData: true})) + }) + + test('defaults demo data to false in a non-interactive environment', async () => { + vi.mocked(terminalSupportsPrompting).mockReturnValue(false) + + await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345']) + + expect(storeDemoDataPrompt).not.toHaveBeenCalled() + expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({withDemoData: false})) + }) + + test('skips the demo data prompt when --demo-data is set explicitly', async () => { + await StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'plus', '--organization-id', '12345', '--demo-data']) + + expect(storeDemoDataPrompt).not.toHaveBeenCalled() + expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({withDemoData: true})) + }) + + test('skips the demo data prompt when --no-demo-data is set explicitly', async () => { + await StoreCreateDev.run([ + '--name', + 'my-test-store', + '--plan', + 'plus', + '--organization-id', + '12345', + '--no-demo-data', + ]) + + expect(storeDemoDataPrompt).not.toHaveBeenCalled() + expect(createDevStore).toHaveBeenCalledWith(expect.objectContaining({withDemoData: false})) + }) + test('rejects an invalid plan value without calling the service', async () => { await expect( StoreCreateDev.run(['--name', 'my-test-store', '--plan', 'enterprise', '--organization-id', '12345']), @@ -187,7 +244,7 @@ describe('store create dev command', () => { expect(StoreCreateDev.flags['organization-id']).toBeDefined() expect(StoreCreateDev.flags.plan).toBeDefined() expect(StoreCreateDev.flags['feature-preview']).toBeDefined() - expect(StoreCreateDev.flags['with-demo-data']).toBeDefined() + expect(StoreCreateDev.flags['demo-data']).toBeDefined() expect(StoreCreateDev.flags.country).toBeDefined() expect(StoreCreateDev.flags.json).toBeDefined() }) diff --git a/packages/store/src/cli/commands/store/create/dev.ts b/packages/store/src/cli/commands/store/create/dev.ts index e1f54dd458e..cc88146c04e 100644 --- a/packages/store/src/cli/commands/store/create/dev.ts +++ b/packages/store/src/cli/commands/store/create/dev.ts @@ -1,10 +1,11 @@ import {createDevStore} from '../../../services/store/create/dev.js' import {devStorePlanHandles, DevStorePlan} from '../../../services/store/constants.js' -import {storeNamePrompt, storePlanPrompt} from '../../../prompts/store.js' +import {storeNamePrompt, storePlanPrompt, storeDemoDataPrompt} from '../../../prompts/store.js' import {countryFlag, storeFlags} from '../../../flags.js' import {selectOrg} from '@shopify/organizations' import Command from '@shopify/cli-kit/node/base-command' import {globalFlags, jsonFlag, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' +import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' import {AbortError} from '@shopify/cli-kit/node/error' import {outputResult} from '@shopify/cli-kit/node/output' import {Flags} from '@oclif/core' @@ -21,7 +22,7 @@ export default class StoreCreateDev extends Command { static examples = [ '<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --name "Lavender Candles" --organization-id 1234567 --plan basic', - '<%= config.bin %> <%= command.id %> --name "Lavender Candles" --organization-id 1234567 --plan basic --with-demo-data', + '<%= config.bin %> <%= command.id %> --name "Lavender Candles" --organization-id 1234567 --plan basic --demo-data', '<%= config.bin %> <%= command.id %> --name "Lavender Candles" --organization-id 1234567 --plan basic --json', ] @@ -46,10 +47,10 @@ export default class StoreCreateDev extends Command { description: 'The handle of a feature preview to enable on the new development store.', env: 'SHOPIFY_FLAG_STORE_FEATURE_PREVIEW', }), - 'with-demo-data': Flags.boolean({ + 'demo-data': Flags.boolean({ description: 'Populate the new development store with demo data.', - default: false, - env: 'SHOPIFY_FLAG_STORE_WITH_DEMO_DATA', + allowNo: true, + env: 'SHOPIFY_FLAG_STORE_DEMO_DATA', }), country: countryFlag, } @@ -60,6 +61,8 @@ export default class StoreCreateDev extends Command { const organization = await selectOrg(flags['organization-id']?.toString()) const name = flags.name ?? (await storeNamePrompt()) const plan = (flags.plan as DevStorePlan | undefined) ?? (await storePlanPrompt()) + // An unspecified flag means "ask" where we can prompt and "no" where we can't. + const withDemoData = flags['demo-data'] ?? (terminalSupportsPrompting() ? await storeDemoDataPrompt() : false) try { await createDevStore({ @@ -67,7 +70,7 @@ export default class StoreCreateDev extends Command { organization, plan, featurePreview: flags['feature-preview'], - withDemoData: flags['with-demo-data'], + withDemoData, country: flags.country, json: flags.json, }) diff --git a/packages/store/src/cli/prompts/store.test.ts b/packages/store/src/cli/prompts/store.test.ts index f9dcf5c6485..25d25f0bda6 100644 --- a/packages/store/src/cli/prompts/store.test.ts +++ b/packages/store/src/cli/prompts/store.test.ts @@ -1,6 +1,6 @@ -import {storeNamePrompt, storePlanPrompt} from './store.js' +import {storeNamePrompt, storePlanPrompt, storeDemoDataPrompt} from './store.js' import {describe, expect, test, vi} from 'vitest' -import {renderSelectPrompt, renderTextPrompt} from '@shopify/cli-kit/node/ui' +import {renderConfirmationPrompt, renderSelectPrompt, renderTextPrompt} from '@shopify/cli-kit/node/ui' vi.mock('@shopify/cli-kit/node/ui') @@ -35,3 +35,19 @@ describe('storePlanPrompt', () => { }) }) }) + +describe('storeDemoDataPrompt', () => { + test('asks whether to add demo data and returns the answer', async () => { + vi.mocked(renderConfirmationPrompt).mockResolvedValue(true) + + const result = await storeDemoDataPrompt() + + expect(result).toBe(true) + expect(renderConfirmationPrompt).toHaveBeenCalledWith({ + message: 'Populate the store with demo data?', + confirmationMessage: 'Yes, add demo data', + cancellationMessage: 'No, start with an empty store', + defaultValue: true, + }) + }) +}) diff --git a/packages/store/src/cli/prompts/store.ts b/packages/store/src/cli/prompts/store.ts index e004a6cffb8..da17e933882 100644 --- a/packages/store/src/cli/prompts/store.ts +++ b/packages/store/src/cli/prompts/store.ts @@ -1,4 +1,5 @@ -import {devStoreNamePrompt, devStorePlanPrompt} from '@shopify/organizations' +import {devStoreNamePrompt, devStorePlanPrompt, devStoreDemoDataPrompt} from '@shopify/organizations' export const storeNamePrompt = devStoreNamePrompt export const storePlanPrompt = devStorePlanPrompt +export const storeDemoDataPrompt = devStoreDemoDataPrompt diff --git a/packages/store/src/cli/services/store/create/dev.test.ts b/packages/store/src/cli/services/store/create/dev.test.ts index 750c602a0c3..50cf1b48b01 100644 --- a/packages/store/src/cli/services/store/create/dev.test.ts +++ b/packages/store/src/cli/services/store/create/dev.test.ts @@ -125,7 +125,7 @@ describe('createDevStore', () => { ) }) - test('passes prepopulateTestData when --with-demo-data is set', async () => { + test('passes prepopulateTestData when withDemoData is set', async () => { vi.mocked(businessPlatformOrganizationsRequestDoc) .mockResolvedValueOnce(defaultMutationResult) .mockResolvedValueOnce({