Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable search for dev stores #5145

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type ListAppDevStoresQueryVariables = Types.Exact<{[key: string]: never}>
export type ListAppDevStoresQueryVariables = Types.Exact<{
searchTerm?: Types.InputMaybe<Types.Scalars['String']['input']>
}>

export type ListAppDevStoresQuery = {
organization?: {
Expand All @@ -20,6 +22,7 @@ export type ListAppDevStoresQuery = {
shortName?: string | null
}
}[]
pageInfo: {hasNextPage: boolean}
} | null
} | null
}
Expand All @@ -31,6 +34,13 @@ export const ListAppDevStores = {
kind: 'OperationDefinition',
operation: 'query',
name: {kind: 'Name', value: 'ListAppDevStores'},
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'searchTerm'}},
type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
Expand Down Expand Up @@ -70,6 +80,11 @@ export const ListAppDevStores = {
],
},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'search'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'searchTerm'}},
},
],
selectionSet: {
kind: 'SelectionSet',
Expand Down Expand Up @@ -100,6 +115,17 @@ export const ListAppDevStores = {
],
},
},
{
kind: 'Field',
name: {kind: 'Name', value: 'pageInfo'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'hasNextPage'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
query ListAppDevStores {
organization {
id
name
accessibleShops(filters: {field: STORE_TYPE, operator: EQUALS, value: "app_development"}) {
edges {
node {
id
externalId
name
storeType
primaryDomain
shortName
}
query ListAppDevStores($searchTerm: String) {
organization {
id
name
accessibleShops(filters: {field: STORE_TYPE, operator: EQUALS, value: "app_development"}, search: $searchTerm) {
edges {
node {
id
externalId
name
storeType
primaryDomain
shortName
}
}
pageInfo {
hasNextPage
}
}
}
}
2 changes: 1 addition & 1 deletion packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,7 +1329,7 @@ export function testDeveloperPlatformClient(stubs: Partial<DeveloperPlatformClie
Promise.resolve({organization: testOrganization(), apps: [testOrganizationApp()], hasMorePages: false}),
createApp: (_organization: Organization, _name: string, _options?: CreateAppOptions) =>
Promise.resolve(testOrganizationApp()),
devStoresForOrg: (_organizationId: string) => Promise.resolve([]),
devStoresForOrg: (_organizationId: string) => Promise.resolve({stores: [], hasMorePages: false}),
storeByDomain: (_orgId: string, _shopDomain: string) => Promise.resolve({organizations: {nodes: []}}),
appExtensionRegistrations: (_app: MinimalAppIdentifiers) => Promise.resolve(emptyAppExtensionRegistrations),
appVersions: (_app: MinimalAppIdentifiers) => Promise.resolve(emptyAppVersions),
Expand Down
12 changes: 8 additions & 4 deletions packages/app/src/cli/prompts/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('selectStore', () => {
const stores: OrganizationStore[] = []

// When
const got = await selectStorePrompt(stores, defaultShowDomainOnPrompt)
const got = await selectStorePrompt({stores, showDomainOnPrompt: defaultShowDomainOnPrompt})

// Then
expect(got).toEqual(undefined)
Expand All @@ -154,7 +154,7 @@ describe('selectStore', () => {
const outputMock = mockAndCaptureOutput()

// When
const got = await selectStorePrompt(stores, defaultShowDomainOnPrompt)
const got = await selectStorePrompt({stores, showDomainOnPrompt: defaultShowDomainOnPrompt})

// Then
expect(got).toEqual(STORE1)
Expand All @@ -168,7 +168,7 @@ describe('selectStore', () => {
vi.mocked(renderAutocompletePrompt).mockResolvedValue('2')

// When
const got = await selectStorePrompt(stores, defaultShowDomainOnPrompt)
const got = await selectStorePrompt({stores, showDomainOnPrompt: defaultShowDomainOnPrompt})

// Then
expect(got).toEqual(STORE2)
Expand All @@ -178,6 +178,8 @@ describe('selectStore', () => {
{label: 'store1', value: '1'},
{label: 'store2', value: '2'},
],
hasMorePages: false,
search: expect.any(Function),
})
})

Expand All @@ -187,7 +189,7 @@ describe('selectStore', () => {
vi.mocked(renderAutocompletePrompt).mockResolvedValue('2')

// When
const got = await selectStorePrompt(stores, true)
const got = await selectStorePrompt({stores, showDomainOnPrompt: true})

// Then
expect(got).toEqual(STORE2)
Expand All @@ -197,6 +199,8 @@ describe('selectStore', () => {
{label: 'store1 (domain1)', value: '1'},
{label: 'store2 (domain2)', value: '2'},
],
hasMorePages: false,
search: expect.any(Function),
})
})
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably add a test here for search terms (ie. renders correct store if searchTerm is not nil), wdyt?

Expand Down
38 changes: 31 additions & 7 deletions packages/app/src/cli/prompts/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {Organization, MinimalOrganizationApp, OrganizationStore, MinimalAppIdentifiers} from '../models/organization.js'
import {getTomls} from '../utilities/app/config/getTomls.js'
import {setCachedCommandTomlMap} from '../services/local-storage.js'
import {Paginateable} from '../utilities/developer-platform-client.js'
import {renderAutocompletePrompt, renderConfirmationPrompt, renderTextPrompt} from '@shopify/cli-kit/node/ui'
import {outputCompleted} from '@shopify/cli-kit/node/output'

Expand Down Expand Up @@ -58,27 +59,50 @@ export async function selectAppPrompt(
return currentAppChoices.find((app) => app.apiKey === apiKey)!
}

export async function selectStorePrompt(
stores: OrganizationStore[],
showDomainOnPrompt: boolean,
): Promise<OrganizationStore | undefined> {
interface SelectStorePromptOptions {
onSearchForStoresByName?: (term: string) => Promise<Paginateable<{stores: OrganizationStore[]}>>
stores: OrganizationStore[]
hasMorePages?: boolean
showDomainOnPrompt: boolean
}

export async function selectStorePrompt({
stores,
hasMorePages = false,
onSearchForStoresByName = (_term: string) => Promise.resolve({stores, hasMorePages}),
showDomainOnPrompt = true,
}: SelectStorePromptOptions): Promise<OrganizationStore | undefined> {
if (stores.length === 0) return undefined
if (stores.length === 1) {
outputCompleted(`Using your default dev store, ${stores[0]!.shopName}, to preview your project.`)
return stores[0]
}

const storeList = stores.map((store) => {
const toAnswer = (store: OrganizationStore) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's a bit unclear to me just by reading the name of this constant what it is returning; maybe something like displayedStoreLIst if we're indicating that this is the list of stores we show on the terminal?

let label = store.shopName
if (showDomainOnPrompt && store.shopDomain) {
label = `${store.shopName} (${store.shopDomain})`
}
return {label, value: store.shopId}
})
}

let currentStoreChoices = stores

const id = await renderAutocompletePrompt({
message: 'Which store would you like to use to view your project?',
choices: storeList,
choices: currentStoreChoices.map(toAnswer),
hasMorePages,
search: async (term) => {
const result = await onSearchForStoresByName(term)
currentStoreChoices = result.stores

return {
data: currentStoreChoices.map(toAnswer),
meta: {
hasNextPage: result.hasMorePages,
},
}
},
})
return stores.find((store) => store.shopId === id)
}
Expand Down
62 changes: 48 additions & 14 deletions packages/app/src/cli/services/dev/select-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ describe('selectStore', async () => {
vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE1)

// When
const got = await selectStore([STORE1, STORE2], ORG1, testDeveloperPlatformClient())
const got = await selectStore({stores: [STORE1, STORE2], hasMorePages: false}, ORG1, testDeveloperPlatformClient())

// Then
expect(got).toEqual(STORE1)
expect(selectStorePrompt).toHaveBeenCalledWith([STORE1, STORE2], defaultShowDomainOnPrompt)
expect(selectStorePrompt).toHaveBeenCalledWith(
expect.objectContaining({
stores: [STORE1, STORE2],
showDomainOnPrompt: defaultShowDomainOnPrompt,
}),
)
})

test('selectStorePrompt is called with showDomainOnPrompt = true if clientName is app-management', async () => {
Expand All @@ -73,11 +78,16 @@ describe('selectStore', async () => {
const developerPlatformClient = testDeveloperPlatformClient({clientName: ClientName.AppManagement})

// When
const got = await selectStore([STORE1, STORE2], ORG1, developerPlatformClient)
const got = await selectStore({stores: [STORE1, STORE2], hasMorePages: false}, ORG1, developerPlatformClient)

// Then
expect(got).toEqual(STORE1)
expect(selectStorePrompt).toHaveBeenCalledWith([STORE1, STORE2], true)
expect(selectStorePrompt).toHaveBeenCalledWith(
expect.objectContaining({
stores: [STORE1, STORE2],
showDomainOnPrompt: true,
}),
)
})

test('prompts user to convert store to non-transferable if selection is invalid', async () => {
Expand All @@ -86,11 +96,16 @@ describe('selectStore', async () => {
vi.mocked(confirmConversionToTransferDisabledStorePrompt).mockResolvedValueOnce(true)

// When
const got = await selectStore([STORE1, STORE2], ORG1, testDeveloperPlatformClient())
const got = await selectStore({stores: [STORE1, STORE2], hasMorePages: false}, ORG1, testDeveloperPlatformClient())

// Then
expect(got).toEqual(STORE2)
expect(selectStorePrompt).toHaveBeenCalledWith([STORE1, STORE2], defaultShowDomainOnPrompt)
expect(selectStorePrompt).toHaveBeenCalledWith(
expect.objectContaining({
stores: [STORE1, STORE2],
showDomainOnPrompt: defaultShowDomainOnPrompt,
}),
)
expect(confirmConversionToTransferDisabledStorePrompt).toHaveBeenCalled()
})

Expand All @@ -101,11 +116,16 @@ describe('selectStore', async () => {
vi.mocked(confirmConversionToTransferDisabledStorePrompt).mockResolvedValueOnce(false)

// When
const got = await selectStore([STORE1, STORE2], ORG1, testDeveloperPlatformClient())
const got = await selectStore({stores: [STORE1, STORE2], hasMorePages: false}, ORG1, testDeveloperPlatformClient())

// Then
expect(got).toEqual(STORE1)
expect(selectStorePrompt).toHaveBeenCalledWith([STORE1, STORE2], defaultShowDomainOnPrompt)
expect(selectStorePrompt).toHaveBeenCalledWith(
expect.objectContaining({
stores: [STORE1, STORE2],
showDomainOnPrompt: defaultShowDomainOnPrompt,
}),
)
expect(confirmConversionToTransferDisabledStorePrompt).toHaveBeenCalled()
})

Expand All @@ -117,20 +137,29 @@ describe('selectStore', async () => {
const developerPlatformClient = testDeveloperPlatformClient()

// When
const got = await selectStore([STORE1, STORE2], ORG1, developerPlatformClient)
const got = await selectStore({stores: [STORE1, STORE2], hasMorePages: false}, ORG1, developerPlatformClient)

// Then
expect(got).toEqual(STORE2)
expect(developerPlatformClient.convertToTransferDisabledStore).not.toHaveBeenCalled()
expect(selectStorePrompt).toHaveBeenCalledWith([STORE1, STORE2], defaultShowDomainOnPrompt)
expect(selectStorePrompt).toHaveBeenCalledWith(
expect.objectContaining({
stores: [STORE1, STORE2],
showDomainOnPrompt: defaultShowDomainOnPrompt,
}),
)
})

test('throws if store is non convertible', async () => {
// Given
vi.mocked(selectStorePrompt).mockResolvedValueOnce(STORE3)

// When
const got = selectStore([STORE1, STORE2, STORE3], ORG1, testDeveloperPlatformClient())
const got = selectStore(
{stores: [STORE1, STORE2, STORE3], hasMorePages: false},
ORG1,
testDeveloperPlatformClient(),
)

// Then
await expect(got).rejects.toThrow('The store you specified (domain3) is not a dev store')
Expand All @@ -142,11 +171,16 @@ describe('selectStore', async () => {
vi.mocked(reloadStoreListPrompt).mockResolvedValue(false)

// When
const got = () => selectStore([STORE1, STORE2], ORG1, testDeveloperPlatformClient())
const got = () => selectStore({stores: [STORE1, STORE2], hasMorePages: false}, ORG1, testDeveloperPlatformClient())

// Then
await expect(got).rejects.toThrowError()
expect(selectStorePrompt).toHaveBeenCalledWith([STORE1, STORE2], defaultShowDomainOnPrompt)
expect(selectStorePrompt).toHaveBeenCalledWith(
expect.objectContaining({
stores: [STORE1, STORE2],
showDomainOnPrompt: defaultShowDomainOnPrompt,
}),
)
})

test('prompts user to create & reload, fetches 10 times and tries again if reload is true', async () => {
Expand All @@ -157,7 +191,7 @@ describe('selectStore', async () => {
const developerPlatformClient = testDeveloperPlatformClient()

// When
const got = selectStore([], ORG1, developerPlatformClient)
const got = selectStore({stores: [], hasMorePages: false}, ORG1, developerPlatformClient)

// Then
await expect(got).rejects.toThrow()
Expand Down
Loading
Loading