Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/store-info-data-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/store': patch
---

Render `store info` details as a two-column data table and surface store URLs as clickable next-step actions (e.g. "View the storefront.").
93 changes: 69 additions & 24 deletions packages/store/src/cli/services/store/info/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@ function baseResult(overrides: Partial<StoreInfoResult> = {}): StoreInfoResult {
}
}

function storeDetailItems(): string[] {
function storeDetailRows(): unknown[][] {
const opts = vi.mocked(renderInfo).mock.calls[0]?.[0] as {
customSections: {title: string; body: {list: {items: string[]}}}[]
customSections: {title: string; body: {tabularData: unknown[][]}}[]
}
const section = opts.customSections.find((sec) => sec.title === 'Store details')
return section?.body.list.items ?? []
return section?.body.tabularData ?? []
}

function storeActions(): unknown[] {
const opts = vi.mocked(renderInfo).mock.calls[0]?.[0] as {
customSections: {title?: string; body: {list?: {items: unknown[]}}}[]
}
const section = opts.customSections.find((sec) => sec.body?.list)
return section?.body.list?.items ?? []
}

// The labels of the detail rows (first cell of each row).
function rowLabels(): string[] {
return storeDetailRows().map((row) => row[0] as string)
}

describe('renderStoreInfoResult', () => {
Expand Down Expand Up @@ -76,49 +89,81 @@ describe('renderStoreInfoResult', () => {
type: 'dev',
plan: 'grow',
featurePreview: 'extended_variants',
}),
'text',
)

const rows = storeDetailRows()
expect(rows).toContainEqual(['ID', 'gid://shopify/Shop/1'])
expect(rows).toContainEqual(['Display Name', 'My Shop'])
expect(rows).toContainEqual(['Subdomain', 'shop.myshopify.com'])
expect(rows).toContainEqual(['Organization', 'Acme Holdings'])
expect(rows).toContainEqual(['Type', 'Dev'])
expect(rows).toContainEqual(['Plan', 'Grow'])
expect(rows).toContainEqual(['Feature Preview', 'extended_variants'])
})

test('renders store URLs as next-step action links', () => {
renderStoreInfoResult(
baseResult({
adminUrl: 'https://admin.shopify.com/store/acme-widgets',
accessUrl: 'https://app.shopify.com/auth/preview-store?token=access-token',
saveUrl: 'https://admin.shopify.com/store-transfer/accept/claim-token',
}),
'text',
)

const items = storeDetailItems()
expect(items).toContain('ID: gid://shopify/Shop/1')
expect(items).toContain('Display Name: My Shop')
expect(items).toContain('Subdomain: shop.myshopify.com')
expect(items).toContain('Organization: Acme Holdings')
expect(items).toContain('Type: Dev')
expect(items).toContain('Plan: Grow')
expect(items).toContain('Feature Preview: extended_variants')
expect(items).toContain('Admin URL: https://admin.shopify.com/store/acme-widgets')
expect(items).toContain('Access URL: https://app.shopify.com/auth/preview-store?token=access-token')
expect(items).toContain('Save URL: https://admin.shopify.com/store-transfer/accept/claim-token')
const actions = storeActions()
expect(actions).toEqual([
{link: {label: 'Manage this store in the Shopify admin', url: 'https://admin.shopify.com/store/acme-widgets'}},
{link: {label: 'View the storefront', url: 'https://app.shopify.com/auth/preview-store?token=access-token'}},
{
link: {
label: 'Save your progress on this store',
url: 'https://admin.shopify.com/store-transfer/accept/claim-token',
},
},
])
// URLs are no longer rendered as detail rows.
expect(rowLabels()).not.toContain('Admin URL')
expect(rowLabels()).not.toContain('Access URL')
expect(rowLabels()).not.toContain('Save URL')
})

test('omits action links for URLs that are not present', () => {
renderStoreInfoResult(baseResult({adminUrl: 'https://admin.shopify.com/store/acme-widgets'}), 'text')
expect(storeActions()).toEqual([
{link: {label: 'Manage this store in the Shopify admin', url: 'https://admin.shopify.com/store/acme-widgets'}},
])
})

test('omits next steps entirely when no URLs are present', () => {
renderStoreInfoResult(baseResult(), 'text')
expect(storeActions()).toEqual([])
})

test('formats the store owner as "name (email)" when both are present', () => {
renderStoreInfoResult(baseResult({storeOwner: {name: 'Jane Doe', email: 'jane@acme.com'}}), 'text')
expect(storeDetailItems()).toContain('Store owner: Jane Doe (jane@acme.com)')
expect(storeDetailRows()).toContainEqual(['Store owner', 'Jane Doe (jane@acme.com)'])
})

test('falls back to the available store owner field when only one is present', () => {
renderStoreInfoResult(baseResult({storeOwner: {name: 'Jane Doe'}}), 'text')
expect(storeDetailItems()).toContain('Store owner: Jane Doe')
expect(storeDetailRows()).toContainEqual(['Store owner', 'Jane Doe'])
})

test('falls back to the email when the store owner has no name', () => {
renderStoreInfoResult(baseResult({storeOwner: {email: 'jane@acme.com'}}), 'text')
expect(storeDetailItems()).toContain('Store owner: jane@acme.com')
expect(storeDetailRows()).toContainEqual(['Store owner', 'jane@acme.com'])
})

test('omits fields that are not present', () => {
renderStoreInfoResult(baseResult(), 'text')
const items = storeDetailItems()
expect(items.some((item) => item.startsWith('Feature Preview'))).toBe(false)
expect(items.some((item) => item.startsWith('Store owner'))).toBe(false)
expect(items.some((item) => item.startsWith('Type'))).toBe(false)
expect(items.some((item) => item.startsWith('Plan'))).toBe(false)
expect(items.some((item) => item.startsWith('Access URL'))).toBe(false)
expect(items.some((item) => item.startsWith('Save URL'))).toBe(false)
const labels = rowLabels()
expect(labels).not.toContain('Feature Preview')
expect(labels).not.toContain('Store owner')
expect(labels).not.toContain('Type')
expect(labels).not.toContain('Plan')
expect(storeDetailRows()).toHaveLength(2)
})
})
49 changes: 31 additions & 18 deletions packages/store/src/cli/services/store/info/result.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {outputResult} from '@shopify/cli-kit/node/output'
import {renderInfo} from '@shopify/cli-kit/node/ui'
import {renderInfo, type InlineToken, type LinkToken} from '@shopify/cli-kit/node/ui'
import {capitalizeWords} from '@shopify/cli-kit/common/string'
import type {StoreInfoResult, StoreInfoStoreOwner} from './types.js'

Expand All @@ -10,25 +10,34 @@ export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfo
outputResult(JSON.stringify(result, null, 2))
return
}
const actions = storeActions(result)
renderInfo({
customSections: [{title: 'Store details', body: {list: {items: storeDetailItems(result)}}}],
customSections: [
{title: 'Store details', body: {tabularData: storeDetailRows(result), firstColumnSubdued: true}},
...(actions.length > 0 ? [{body: {list: {title: {bold: 'Next steps'}, items: actions}}}] : []),
],
})
}

function storeDetailItems(result: StoreInfoResult): string[] {
const items: string[] = []
push(items, 'ID', result.id)
push(items, 'Display Name', result.displayName)
push(items, 'Subdomain', result.subdomain)
push(items, 'Organization', result.organizationName)
push(items, 'Store owner', formatOwner(result.storeOwner))
push(items, 'Type', result.type ? capitalizeWords(result.type) : undefined)
push(items, 'Plan', result.plan ? capitalizeWords(result.plan) : undefined)
push(items, 'Feature Preview', result.featurePreview)
push(items, 'Admin URL', result.adminUrl)
push(items, 'Access URL', result.accessUrl)
push(items, 'Save URL', result.saveUrl)
return items
function storeDetailRows(result: StoreInfoResult): InlineToken[][] {
const rows: InlineToken[][] = []
push(rows, 'ID', result.id)
push(rows, 'Display Name', result.displayName)
push(rows, 'Subdomain', result.subdomain)
push(rows, 'Organization', result.organizationName)
push(rows, 'Store owner', formatOwner(result.storeOwner))
push(rows, 'Type', result.type ? capitalizeWords(result.type) : undefined)
push(rows, 'Plan', result.plan ? capitalizeWords(result.plan) : undefined)
push(rows, 'Feature Preview', result.featurePreview)
return rows
}

function storeActions(result: StoreInfoResult): LinkToken[] {
const actions: LinkToken[] = []
pushAction(actions, result.adminUrl, 'Manage this store in the Shopify admin')
pushAction(actions, result.accessUrl, 'View the storefront')
pushAction(actions, result.saveUrl, 'Save your progress on this store')
return actions
}

function formatOwner(owner: StoreInfoStoreOwner | undefined): string | undefined {
Expand All @@ -37,6 +46,10 @@ function formatOwner(owner: StoreInfoStoreOwner | undefined): string | undefined
return owner.name ?? owner.email
}

function push(items: string[], label: string, value: string | undefined): void {
if (value) items.push(`${label}: ${value}`)
function push(rows: InlineToken[][], label: string, value: string | undefined): void {
if (value) rows.push([label, value])
}

function pushAction(actions: LinkToken[], url: string | undefined, label: string): void {
if (url) actions.push({link: {label, url}})
}
Loading