From 3f71c91b61d99bd4ec7680a345d9acb3475d98fb Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 24 Jun 2026 13:10:14 +0300 Subject: [PATCH 1/5] Render store info URLs as link tokens URLs (Admin, Access, Save) in the store info command wrapped awkwardly inside renderInfo's bordered box, interleaving URL text with box characters. Rendering them as link tokens lets the UI kit's footnote system print full URLs outside the box, fixing the layout. Assisted-By: devx/19f4c9df-1fa8-4696-9c82-8f5f86ac69da --- .../cli/services/store/info/result.test.ts | 51 ++++++++++++++----- .../src/cli/services/store/info/result.ts | 20 +++++--- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/packages/store/src/cli/services/store/info/result.test.ts b/packages/store/src/cli/services/store/info/result.test.ts index 06c817eed0f..f555a930fc2 100644 --- a/packages/store/src/cli/services/store/info/result.test.ts +++ b/packages/store/src/cli/services/store/info/result.test.ts @@ -15,14 +15,18 @@ function baseResult(overrides: Partial = {}): StoreInfoResult { } } -function storeDetailItems(): string[] { +function storeDetailItems(): unknown[] { const opts = vi.mocked(renderInfo).mock.calls[0]?.[0] as { - customSections: {title: string; body: {list: {items: string[]}}}[] + customSections: {title: string; body: {list: {items: unknown[]}}}[] } const section = opts.customSections.find((sec) => sec.title === 'Store details') return section?.body.list.items ?? [] } +function stringItems(): string[] { + return storeDetailItems().filter((item): item is string => typeof item === 'string') +} + describe('renderStoreInfoResult', () => { test('emits the doc-shaped JSON via outputResult when format is json', () => { renderStoreInfoResult( @@ -83,7 +87,7 @@ describe('renderStoreInfoResult', () => { 'text', ) - const items = storeDetailItems() + const items = stringItems() expect(items).toContain('ID: gid://shopify/Shop/1') expect(items).toContain('Display Name: My Shop') expect(items).toContain('Subdomain: shop.myshopify.com') @@ -91,34 +95,57 @@ describe('renderStoreInfoResult', () => { 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') + expect(storeDetailItems()).toContainEqual([ + 'Admin URL:', + { + link: { + label: 'https://admin.shopify.com/store/acme-widgets', + url: 'https://admin.shopify.com/store/acme-widgets', + }, + }, + ]) + expect(storeDetailItems()).toContainEqual([ + 'Access URL:', + { + link: { + label: 'https://app.shopify.com/auth/preview-store?token=access-token', + url: 'https://app.shopify.com/auth/preview-store?token=access-token', + }, + }, + ]) + expect(storeDetailItems()).toContainEqual([ + 'Save URL:', + { + link: { + label: 'https://admin.shopify.com/store-transfer/accept/claim-token', + url: 'https://admin.shopify.com/store-transfer/accept/claim-token', + }, + }, + ]) }) 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(stringItems()).toContain('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(stringItems()).toContain('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(stringItems()).toContain('Store owner: jane@acme.com') }) test('omits fields that are not present', () => { renderStoreInfoResult(baseResult(), 'text') - const items = storeDetailItems() + const items = stringItems() 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) + expect(storeDetailItems()).toHaveLength(2) }) }) diff --git a/packages/store/src/cli/services/store/info/result.ts b/packages/store/src/cli/services/store/info/result.ts index ebd9b94aefa..dca9a5dc4ef 100644 --- a/packages/store/src/cli/services/store/info/result.ts +++ b/packages/store/src/cli/services/store/info/result.ts @@ -2,6 +2,7 @@ import {outputResult} from '@shopify/cli-kit/node/output' import {renderInfo} from '@shopify/cli-kit/node/ui' import {capitalizeWords} from '@shopify/cli-kit/common/string' import type {StoreInfoResult, StoreInfoStoreOwner} from './types.js' +import type {InlineToken, TokenItem} from '@shopify/cli-kit/node/ui' type StoreInfoOutputFormat = 'text' | 'json' @@ -15,8 +16,8 @@ export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfo }) } -function storeDetailItems(result: StoreInfoResult): string[] { - const items: string[] = [] +function storeDetailItems(result: StoreInfoResult): TokenItem[] { + const items: TokenItem[] = [] push(items, 'ID', result.id) push(items, 'Display Name', result.displayName) push(items, 'Subdomain', result.subdomain) @@ -25,9 +26,9 @@ function storeDetailItems(result: StoreInfoResult): string[] { 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) + pushLink(items, 'Admin URL', result.adminUrl) + pushLink(items, 'Access URL', result.accessUrl) + pushLink(items, 'Save URL', result.saveUrl) return items } @@ -37,6 +38,13 @@ function formatOwner(owner: StoreInfoStoreOwner | undefined): string | undefined return owner.name ?? owner.email } -function push(items: string[], label: string, value: string | undefined): void { +function push(items: TokenItem[], label: string, value: string | undefined): void { if (value) items.push(`${label}: ${value}`) } + +// Render URLs as link tokens so they don't wrap awkwardly inside the bordered +// box. The UI kit renders link tokens as a compact footnote reference within +// the box and prints the full URL outside it, where it can wrap freely. +function pushLink(items: TokenItem[], label: string, url: string | undefined): void { + if (url) items.push([`${label}:`, {link: {label: url, url}}]) +} From 1dc4ed687a6e1f5cbd645ef5db2d038a920b988e Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 24 Jun 2026 13:24:50 +0300 Subject: [PATCH 2/5] Render store info details as a two-column data table Switch the 'store info' Store details section from a list to a TabularData two-column [label, value] table. URL values remain link tokens so the banner's LinksContext renders them as compact footnote references inside the box with the full URL outside it. --- .../cli/services/store/info/result.test.ts | 65 ++++++++++--------- .../src/cli/services/store/info/result.ts | 44 ++++++------- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/packages/store/src/cli/services/store/info/result.test.ts b/packages/store/src/cli/services/store/info/result.test.ts index f555a930fc2..5c0fe423ce2 100644 --- a/packages/store/src/cli/services/store/info/result.test.ts +++ b/packages/store/src/cli/services/store/info/result.test.ts @@ -15,16 +15,19 @@ function baseResult(overrides: Partial = {}): StoreInfoResult { } } -function storeDetailItems(): unknown[] { +function storeDetailRows(): unknown[][] { const opts = vi.mocked(renderInfo).mock.calls[0]?.[0] as { - customSections: {title: string; body: {list: {items: unknown[]}}}[] + 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 stringItems(): string[] { - return storeDetailItems().filter((item): item is string => typeof item === 'string') +// The labels of rows whose value cell is a plain string (i.e. not a link token). +function stringRowLabels(): string[] { + return storeDetailRows() + .filter((row) => typeof row[1] === 'string') + .map((row) => row[0] as string) } describe('renderStoreInfoResult', () => { @@ -87,37 +90,37 @@ describe('renderStoreInfoResult', () => { 'text', ) - const items = stringItems() - 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(storeDetailItems()).toContainEqual([ - 'Admin URL:', + 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']) + expect(rows).toContainEqual([ + 'Admin URL', { link: { - label: 'https://admin.shopify.com/store/acme-widgets', + label: 'Link', url: 'https://admin.shopify.com/store/acme-widgets', }, }, ]) - expect(storeDetailItems()).toContainEqual([ - 'Access URL:', + expect(rows).toContainEqual([ + 'Access URL', { link: { - label: 'https://app.shopify.com/auth/preview-store?token=access-token', + label: 'Link', url: 'https://app.shopify.com/auth/preview-store?token=access-token', }, }, ]) - expect(storeDetailItems()).toContainEqual([ - 'Save URL:', + expect(rows).toContainEqual([ + 'Save URL', { link: { - label: 'https://admin.shopify.com/store-transfer/accept/claim-token', + label: 'Link', url: 'https://admin.shopify.com/store-transfer/accept/claim-token', }, }, @@ -126,26 +129,26 @@ describe('renderStoreInfoResult', () => { test('formats the store owner as "name (email)" when both are present', () => { renderStoreInfoResult(baseResult({storeOwner: {name: 'Jane Doe', email: 'jane@acme.com'}}), 'text') - expect(stringItems()).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(stringItems()).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(stringItems()).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 = stringItems() - 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(storeDetailItems()).toHaveLength(2) + const labels = stringRowLabels() + 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) }) }) diff --git a/packages/store/src/cli/services/store/info/result.ts b/packages/store/src/cli/services/store/info/result.ts index dca9a5dc4ef..6d9ebbd5b6a 100644 --- a/packages/store/src/cli/services/store/info/result.ts +++ b/packages/store/src/cli/services/store/info/result.ts @@ -1,8 +1,7 @@ import {outputResult} from '@shopify/cli-kit/node/output' -import {renderInfo} from '@shopify/cli-kit/node/ui' +import {renderInfo, type InlineToken} from '@shopify/cli-kit/node/ui' import {capitalizeWords} from '@shopify/cli-kit/common/string' import type {StoreInfoResult, StoreInfoStoreOwner} from './types.js' -import type {InlineToken, TokenItem} from '@shopify/cli-kit/node/ui' type StoreInfoOutputFormat = 'text' | 'json' @@ -12,24 +11,24 @@ export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfo return } renderInfo({ - customSections: [{title: 'Store details', body: {list: {items: storeDetailItems(result)}}}], + customSections: [{title: 'Store details', body: {tabularData: storeDetailRows(result), firstColumnSubdued: true}}], }) } -function storeDetailItems(result: StoreInfoResult): TokenItem[] { - const items: TokenItem[] = [] - 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) - pushLink(items, 'Admin URL', result.adminUrl) - pushLink(items, 'Access URL', result.accessUrl) - pushLink(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) + pushLink(rows, 'Admin URL', result.adminUrl) + pushLink(rows, 'Access URL', result.accessUrl) + pushLink(rows, 'Save URL', result.saveUrl) + return rows } function formatOwner(owner: StoreInfoStoreOwner | undefined): string | undefined { @@ -38,13 +37,10 @@ function formatOwner(owner: StoreInfoStoreOwner | undefined): string | undefined return owner.name ?? owner.email } -function push(items: TokenItem[], 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]) } -// Render URLs as link tokens so they don't wrap awkwardly inside the bordered -// box. The UI kit renders link tokens as a compact footnote reference within -// the box and prints the full URL outside it, where it can wrap freely. -function pushLink(items: TokenItem[], label: string, url: string | undefined): void { - if (url) items.push([`${label}:`, {link: {label: url, url}}]) +function pushLink(rows: InlineToken[][], label: string, url: string | undefined): void { + if (url) rows.push([label, {link: {label: 'Link', url}}]) } From 82674f9ee5035362f3ce85ecf697bae93b8c0ca5 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 24 Jun 2026 13:25:27 +0300 Subject: [PATCH 3/5] Add changeset for store info data table --- .changeset/store-info-data-table.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/store-info-data-table.md diff --git a/.changeset/store-info-data-table.md b/.changeset/store-info-data-table.md new file mode 100644 index 00000000000..1feed33acb0 --- /dev/null +++ b/.changeset/store-info-data-table.md @@ -0,0 +1,5 @@ +--- +'@shopify/store': patch +--- + +Render `store info` details as a two-column data table and display store URLs as link footnotes so long URLs no longer wrap inside the bordered box. From d138819339c213d1147812b367c1a79256d827f6 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 24 Jun 2026 19:13:37 +0300 Subject: [PATCH 4/5] Render store info URLs as next-step action links Assisted-By: devx/077d295d-2405-4b15-92b2-4c6a72aa41f1 --- .changeset/store-info-data-table.md | 2 +- .../cli/services/store/info/result.test.ts | 72 +++++++++++-------- .../src/cli/services/store/info/result.ts | 16 +++-- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/.changeset/store-info-data-table.md b/.changeset/store-info-data-table.md index 1feed33acb0..9670ee18c89 100644 --- a/.changeset/store-info-data-table.md +++ b/.changeset/store-info-data-table.md @@ -2,4 +2,4 @@ '@shopify/store': patch --- -Render `store info` details as a two-column data table and display store URLs as link footnotes so long URLs no longer wrap inside the bordered box. +Render `store info` details as a two-column data table and surface store URLs as clickable next-step actions (e.g. "View the storefront."). diff --git a/packages/store/src/cli/services/store/info/result.test.ts b/packages/store/src/cli/services/store/info/result.test.ts index 5c0fe423ce2..bf9e0d697d8 100644 --- a/packages/store/src/cli/services/store/info/result.test.ts +++ b/packages/store/src/cli/services/store/info/result.test.ts @@ -23,11 +23,14 @@ function storeDetailRows(): unknown[][] { return section?.body.tabularData ?? [] } -// The labels of rows whose value cell is a plain string (i.e. not a link token). -function stringRowLabels(): string[] { - return storeDetailRows() - .filter((row) => typeof row[1] === 'string') - .map((row) => row[0] as string) +function storeActions(): unknown[] { + const opts = vi.mocked(renderInfo).mock.calls[0]?.[0] as {nextSteps?: unknown[]} + return opts.nextSteps ?? [] +} + +// The labels of the detail rows (first cell of each row). +function rowLabels(): string[] { + return storeDetailRows().map((row) => row[0] as string) } describe('renderStoreInfoResult', () => { @@ -83,9 +86,6 @@ describe('renderStoreInfoResult', () => { type: 'dev', plan: 'grow', featurePreview: 'extended_variants', - 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', ) @@ -98,33 +98,45 @@ describe('renderStoreInfoResult', () => { expect(rows).toContainEqual(['Type', 'Dev']) expect(rows).toContainEqual(['Plan', 'Grow']) expect(rows).toContainEqual(['Feature Preview', 'extended_variants']) - expect(rows).toContainEqual([ - 'Admin URL', - { - link: { - label: 'Link', - url: 'https://admin.shopify.com/store/acme-widgets', - }, - }, - ]) - expect(rows).toContainEqual([ - 'Access URL', - { - link: { - label: 'Link', - url: 'https://app.shopify.com/auth/preview-store?token=access-token', - }, - }, - ]) - expect(rows).toContainEqual([ - 'Save URL', + }) + + 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 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: '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', () => { @@ -144,7 +156,7 @@ describe('renderStoreInfoResult', () => { test('omits fields that are not present', () => { renderStoreInfoResult(baseResult(), 'text') - const labels = stringRowLabels() + const labels = rowLabels() expect(labels).not.toContain('Feature Preview') expect(labels).not.toContain('Store owner') expect(labels).not.toContain('Type') diff --git a/packages/store/src/cli/services/store/info/result.ts b/packages/store/src/cli/services/store/info/result.ts index 6d9ebbd5b6a..f352401ac02 100644 --- a/packages/store/src/cli/services/store/info/result.ts +++ b/packages/store/src/cli/services/store/info/result.ts @@ -12,6 +12,7 @@ export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfo } renderInfo({ customSections: [{title: 'Store details', body: {tabularData: storeDetailRows(result), firstColumnSubdued: true}}], + nextSteps: storeActions(result), }) } @@ -25,12 +26,17 @@ function storeDetailRows(result: StoreInfoResult): InlineToken[][] { push(rows, 'Type', result.type ? capitalizeWords(result.type) : undefined) push(rows, 'Plan', result.plan ? capitalizeWords(result.plan) : undefined) push(rows, 'Feature Preview', result.featurePreview) - pushLink(rows, 'Admin URL', result.adminUrl) - pushLink(rows, 'Access URL', result.accessUrl) - pushLink(rows, 'Save URL', result.saveUrl) return rows } +function storeActions(result: StoreInfoResult): InlineToken[] { + const actions: InlineToken[] = [] + 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 { if (!owner) return undefined if (owner.name && owner.email) return `${owner.name} (${owner.email})` @@ -41,6 +47,6 @@ function push(rows: InlineToken[][], label: string, value: string | undefined): if (value) rows.push([label, value]) } -function pushLink(rows: InlineToken[][], label: string, url: string | undefined): void { - if (url) rows.push([label, {link: {label: 'Link', url}}]) +function pushAction(actions: InlineToken[], url: string | undefined, label: string): void { + if (url) actions.push({link: {label, url}}) } From 7479589da57153fffb7baeaf6f9d06e9a0f91457 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 24 Jun 2026 19:34:38 +0300 Subject: [PATCH 5/5] Render store info next steps as bold custom section without trailing periods Assisted-By: devx/ae84811a-733c-4815-8d7d-604504428a4b --- .../cli/services/store/info/result.test.ts | 15 +++++++------ .../src/cli/services/store/info/result.ts | 21 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/store/src/cli/services/store/info/result.test.ts b/packages/store/src/cli/services/store/info/result.test.ts index bf9e0d697d8..d5679c50a30 100644 --- a/packages/store/src/cli/services/store/info/result.test.ts +++ b/packages/store/src/cli/services/store/info/result.test.ts @@ -24,8 +24,11 @@ function storeDetailRows(): unknown[][] { } function storeActions(): unknown[] { - const opts = vi.mocked(renderInfo).mock.calls[0]?.[0] as {nextSteps?: unknown[]} - return opts.nextSteps ?? [] + 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). @@ -112,11 +115,11 @@ describe('renderStoreInfoResult', () => { 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: '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.', + label: 'Save your progress on this store', url: 'https://admin.shopify.com/store-transfer/accept/claim-token', }, }, @@ -130,7 +133,7 @@ describe('renderStoreInfoResult', () => { 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'}}, + {link: {label: 'Manage this store in the Shopify admin', url: 'https://admin.shopify.com/store/acme-widgets'}}, ]) }) diff --git a/packages/store/src/cli/services/store/info/result.ts b/packages/store/src/cli/services/store/info/result.ts index f352401ac02..b6f81e24534 100644 --- a/packages/store/src/cli/services/store/info/result.ts +++ b/packages/store/src/cli/services/store/info/result.ts @@ -1,5 +1,5 @@ import {outputResult} from '@shopify/cli-kit/node/output' -import {renderInfo, type InlineToken} 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' @@ -10,9 +10,12 @@ export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfo outputResult(JSON.stringify(result, null, 2)) return } + const actions = storeActions(result) renderInfo({ - customSections: [{title: 'Store details', body: {tabularData: storeDetailRows(result), firstColumnSubdued: true}}], - nextSteps: storeActions(result), + customSections: [ + {title: 'Store details', body: {tabularData: storeDetailRows(result), firstColumnSubdued: true}}, + ...(actions.length > 0 ? [{body: {list: {title: {bold: 'Next steps'}, items: actions}}}] : []), + ], }) } @@ -29,11 +32,11 @@ function storeDetailRows(result: StoreInfoResult): InlineToken[][] { return rows } -function storeActions(result: StoreInfoResult): InlineToken[] { - const actions: InlineToken[] = [] - 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.') +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 } @@ -47,6 +50,6 @@ function push(rows: InlineToken[][], label: string, value: string | undefined): if (value) rows.push([label, value]) } -function pushAction(actions: InlineToken[], url: string | undefined, label: string): void { +function pushAction(actions: LinkToken[], url: string | undefined, label: string): void { if (url) actions.push({link: {label, url}}) }