Skip to content

Commit

Permalink
Add import admin link extensions to CLI and migration on deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonso-noriega committed Oct 25, 2024
1 parent b564bd8 commit 9848bad
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/app/src/cli/commands/app/import-extensions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {buildTomlObject as buildPaymentsTomlObject} from '../../services/payments/extension-to-toml.js'
import {buildTomlObject as buildFlowTomlObject} from '../../services/flow/extension-to-toml.js'
import {buildTomlObject as buildAdminLinkTomlObject} from '../../services/admin-link/extension-to-toml.js'
import {buildTomlObject as buildMarketingActivityTomlObject} from '../../services/marketing_activity/extension-to-toml.js'
import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js'
import {appFlags} from '../../flags.js'
Expand Down Expand Up @@ -49,6 +50,12 @@ const getMigrationChoices = (isShopifolk: boolean): MigrationChoice[] => [
extensionTypes: ['marketing_activity_extension'],
buildTomlObject: buildMarketingActivityTomlObject,
},
{
label: 'Admin Link extensions',
value: 'link extension',
extensionTypes: ['app_link', 'bulk_action'],
buildTomlObject: buildAdminLinkTomlObject,
},
]
: []),
]
Expand Down
63 changes: 63 additions & 0 deletions packages/app/src/cli/services/admin-link/extension-to-toml.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {buildTomlObject} from './extension-to-toml.js'
import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js'
import {describe, expect, test} from 'vitest'

describe('extension-to-toml', () => {
test('correctly builds a toml string for a app_link', () => {
// Given
const extension1: ExtensionRegistration = {
id: '26237698049',
uuid: 'ad9947a9-bc0b-4855-82da-008aefbc1c71',
title: 'Admin link title',
type: 'app_link',
draftVersion: {
context: 'COLLECTIONS#SHOW',
config: '{"text":"admin link label","url":"https://google.es"}',
},
}

// When
const got = buildTomlObject(extension1)

// Then
expect(got).toEqual(`[[extensions]]
type = "admin_link"
name = "Admin link title"
handle = "admin-link-title"
[[extensions.targeting]]
text = "admin link label"
url = "https://google.es"
target = "admin.collection.item.action"
`)
})

test('correctly builds a toml string for a bulk_action', () => {
// Given
const extension1: ExtensionRegistration = {
id: '26237698049',
uuid: 'ad9947a9-bc0b-4855-82da-008aefbc1c71',
title: 'Bulk action title',
type: 'bulk_action',
draftVersion: {
context: 'PRODUCTS#ACTION',
config: '{"text":"bulk action label","url":"https://google.es"}',
},
}

// When
const got = buildTomlObject(extension1)

// Then
expect(got).toEqual(`[[extensions]]
type = "admin_link"
name = "Bulk action title"
handle = "bulk-action-title"
[[extensions.targeting]]
text = "bulk action label"
url = "https://google.es"
target = "admin.product.selection.action"
`)
})
})
42 changes: 42 additions & 0 deletions packages/app/src/cli/services/admin-link/extension-to-toml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {contextToTarget} from './utils.js'
import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js'
import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js'
import {encodeToml} from '@shopify/cli-kit/node/toml'
import {slugify} from '@shopify/cli-kit/common/string'

interface AdminLinkConfig {
text: string
url: string
}

/**
* Given an app_link or bulk_action extension config file, convert it to toml
*/
export function buildTomlObject(extension: ExtensionRegistration): string {
const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config
if (!versionConfig) throw new Error('No config found for extension')

const context = extension.activeVersion?.context ?? extension.draftVersion?.context
if (!context) throw new Error('No context found for link extension')

const config: AdminLinkConfig = JSON.parse(versionConfig)

const localExtensionRepresentation = {
extensions: [
{
type: 'admin_link',
name: extension.title,
handle: slugify(extension.title.substring(0, MAX_EXTENSION_HANDLE_LENGTH)),
targeting: [
{
text: config.text,
url: config.url,
target: contextToTarget(context),
},
],
},
],
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return encodeToml(localExtensionRepresentation as any)
}
25 changes: 25 additions & 0 deletions packages/app/src/cli/services/admin-link/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {contextToTarget} from './utils.js'
import {describe, expect, test} from 'vitest'

describe('admin link utils', () => {
test('correctly parses from context `COLLECTIONS#SHOW` to target', () => {
// Given
const context = 'COLLECTIONS#SHOW'

// When
const target = contextToTarget(context)

// Then
expect(target).toEqual('admin.collection.item.action')
})
test('correctly parses from context `ORDERS#INDEX` to target', () => {
// Given
const context = 'ORDERS#INDEX'

// When
const target = contextToTarget(context)

// Then
expect(target).toEqual('admin.order.index.action')
})
})
30 changes: 30 additions & 0 deletions packages/app/src/cli/services/admin-link/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const contextToTarget = (context: string) => {
const splitContext = context.split('#')
if (splitContext.length !== 2 || splitContext.some((part) => part === '' || part === undefined)) {
throw new Error('Invalid context')
}
const domain = 'admin'
const subDomain = typeToSubDomain(splitContext[0] || '')

Check warning on line 7 in packages/app/src/cli/services/admin-link/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/admin-link/utils.ts#L7

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
const entity = locationToEntity(splitContext[1] || '')

Check warning on line 8 in packages/app/src/cli/services/admin-link/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/admin-link/utils.ts#L8

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
const action = 'action'

return [domain, subDomain, entity, action].join('.')
}

const locationToEntity = (location: string) => {
switch (location.toLocaleLowerCase()) {
case 'show':
return 'item'
case 'index':
return 'index'
case 'action':
return 'selection'
case 'fulfilled_card':
return 'fulfilled_card'
default:
throw new Error(`Invalid context location: ${location}`)
}
}
const typeToSubDomain = (word: string) => {
return word.toLocaleLowerCase().replace(new RegExp(`(s)$`), '')
}
19 changes: 19 additions & 0 deletions packages/app/src/cli/services/context/identifiers-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {getPaymentsExtensionsToMigrate, migrateAppModules} from '../dev/migrate-
import {ExtensionSpecification} from '../../models/extensions/specification.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {SingleWebhookSubscriptionType} from '../../models/extensions/specifications/app_config_webhook_schemas/webhooks_schema.js'
import {getAdminLinkExtensionsToMigrate} from '../dev/migrate-admin-link-extension.js'
import {outputCompleted} from '@shopify/cli-kit/node/output'
import {AbortSilentError} from '@shopify/cli-kit/node/error'
import {groupBy} from '@shopify/cli-kit/common/collection'
Expand Down Expand Up @@ -45,6 +46,11 @@ export async function ensureExtensionsIds(
dashboardOnlyExtensions,
validIdentifiers,
)
const adminLinkExtensionsToMigrate = getAdminLinkExtensionsToMigrate(
localExtensions,
dashboardOnlyExtensions,
validIdentifiers,
)

if (uiExtensionsToMigrate.length > 0) {
const confirmedMigration = await extensionMigrationPrompt(uiExtensionsToMigrate)
Expand Down Expand Up @@ -95,6 +101,19 @@ export async function ensureExtensionsIds(
remoteExtensions = remoteExtensions.concat(newRemoteExtensions)
}

if (adminLinkExtensionsToMigrate.length > 0) {
const confirmedMigration = await extensionMigrationPrompt(adminLinkExtensionsToMigrate, false)
if (!confirmedMigration) throw new AbortSilentError()
const newRemoteExtensions = await migrateAppModules(
adminLinkExtensionsToMigrate,
options.appId,
'admin_link',
dashboardOnlyExtensions,
options.developerPlatformClient,
)
remoteExtensions = remoteExtensions.concat(newRemoteExtensions)
}

const matchExtensions = await automaticMatchmaking(
localExtensions,
remoteExtensions,
Expand Down
38 changes: 38 additions & 0 deletions packages/app/src/cli/services/dev/migrate-admin-link-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {LocalSource, RemoteSource} from '../context/identifiers.js'
import {IdentifiersExtensions} from '../../models/app/identifiers.js'
import {getExtensionIds, LocalRemoteSource} from '../context/id-matching.js'
import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js'
import {slugify} from '@shopify/cli-kit/common/string'

export function getAdminLinkExtensionsToMigrate(
localSources: LocalSource[],
remoteSources: RemoteSource[],
identifiers: IdentifiersExtensions,
) {
const ids = getExtensionIds(localSources, identifiers)
const localExtensionTypesToMigrate = ['admin_link']
const remoteExtensionTypesToMigrate = ['app_link', 'bulk_action']
const typesMap = new Map<string, string[]>()
typesMap.set('admin_link', ['app_link', 'bulk_action'])

const local = localSources.filter((source) => localExtensionTypesToMigrate.includes(source.type))
const remote = remoteSources.filter((source) => remoteExtensionTypesToMigrate.includes(source.type))

// Map remote sources by uuid and slugified title (the slugified title is used for matching with local folder)
const remoteSourcesMap = new Map<string, RemoteSource>()
remote.forEach((remoteSource) => {
remoteSourcesMap.set(remoteSource.uuid, remoteSource)
remoteSourcesMap.set(slugify(remoteSource.title.substring(0, MAX_EXTENSION_HANDLE_LENGTH)), remoteSource)
})

return local.reduce<LocalRemoteSource[]>((accumulator, localSource) => {
const localSourceId = ids[localSource.localIdentifier] ?? 'unknown'
const remoteSource = remoteSourcesMap.get(localSourceId) || remoteSourcesMap.get(localSource.localIdentifier)

Check warning on line 30 in packages/app/src/cli/services/dev/migrate-admin-link-extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/dev/migrate-admin-link-extension.ts#L30

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
const typeMatch = typesMap.get(localSource.type)?.includes(remoteSource?.type ?? 'undefined')

if (remoteSource && typeMatch) {
accumulator.push({local: localSource, remote: remoteSource})
}
return accumulator
}, [])
}

0 comments on commit 9848bad

Please sign in to comment.