From d057a38f505f4eb5d6cd3c2262955486bd88e4b5 Mon Sep 17 00:00:00 2001 From: Max Moroz <95914987+bc-max@users.noreply.github.com> Date: Fri, 14 Jan 2022 09:15:35 +0200 Subject: [PATCH] feat(storefront): strf-9582 stencil push: apply theme to multiple storefronts (#825) * feat: strf-9582 stencil push: apply theme to multiple storefronts - README.md - documentation updated; - theme-api-client.js - activateThemeByVariationId function updated to process multiple channels; - stencil-push.utils.js - updated to process multiple channels; - stencil-push.js - channel_ids option added; * feat(storefront): work in progress - stencil-push.js - added option to push a theme to all available channels; - stencil-push.utils.js - added option to push a theme to all available channels; - stencil-push.utils.spec.js - tests added (work in progress); - README.md - minor refactoring; * feat: tests added - stencil-push.utils.spec.js - tests added; --- README.md | 4 +- bin/stencil-push.js | 9 +-- lib/stencil-push.utils.js | 33 +++++---- lib/stencil-push.utils.spec.js | 121 ++++++++++++++++++++++++++++++++- lib/theme-api-client.js | 35 +++++----- 5 files changed, 166 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 6c3d4495..6dd8a9fd 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ This is useful for tracking your changes in your Theme, and is the tool we use t Run `stencil push` to bundle the local theme and upload it to your store, so it will be available in My Themes. To push the theme and also activate it, use `stencil push -a`. To automatically delete the oldest theme if you are at -your theme limit, use `stencil push -d`. These can be used together, as `stencil push -a -d`. +your theme limit, use `stencil push -d`. These can be used together, as `stencil push -a -d`. You can apply the theme to +multiple storefronts, just specify ids of desired storefronts/channels after `-c` option `stencil push -a -c 123 456 789`. +If you want to apply theme to all available storefronts, just use `-allc` option: `stencil push -a -allc`. Run `stencil pull` to sync changes to your theme configuration from your live store. For example, if Page Builder has been used to change certain theme settings, this will update those settings in config.json in your theme files so you diff --git a/bin/stencil-push.js b/bin/stencil-push.js index 1c8e6b0e..868ee465 100755 --- a/bin/stencil-push.js +++ b/bin/stencil-push.js @@ -15,10 +15,10 @@ program .option('-a, --activate [variationname]', 'specify the variation of the theme to activate') .option('-d, --delete', 'delete oldest private theme if upload limit reached') .option( - '-c, --channel_id [channelId]', - 'specify the channel ID of the storefront to push the theme to', - parseInt, + '-c, --channel_ids ', + 'specify the channel IDs of the storefront to push the theme to', ) + .option('-allc, --all_channels', 'push a theme to all available channels') .parse(process.argv); checkNodeVersion(); @@ -26,11 +26,12 @@ checkNodeVersion(); const cliOptions = program.opts(); const options = { apiHost: cliOptions.host, - channelId: cliOptions.channel_id, + channelIds: cliOptions.channel_ids, bundleZipPath: cliOptions.file, activate: cliOptions.activate, saveBundleName: cliOptions.save, deleteOldest: cliOptions.delete, + allChannels: cliOptions.all_channels, }; stencilPush(options, (err, result) => { if (err) { diff --git a/lib/stencil-push.utils.js b/lib/stencil-push.utils.js index 5d7ce8df..84be783d 100644 --- a/lib/stencil-push.utils.js +++ b/lib/stencil-push.utils.js @@ -274,14 +274,14 @@ utils.promptUserWhetherToApplyTheme = async (options) => { utils.getChannels = async (options) => { const { config: { accessToken }, - channelId, + channelIds, storeHash, applyTheme, } = options; const apiHost = options.apiHost || options.config.apiHost; - if (!applyTheme || channelId) { + if (!applyTheme || channelIds) { return options; } @@ -339,14 +339,19 @@ utils.getVariations = async (options) => { }; utils.promptUserForChannel = async (options) => { - const { applyTheme, channelId, channels } = options; + const { applyTheme, channelIds, channels, allChannels } = options; - if (!applyTheme || channelId) { + if (!applyTheme || channelIds) { return options; } - const selectedChannelId = await utils.promptUserToSelectChannel(channels); - return { ...options, channelId: selectedChannelId }; + if (allChannels) { + const allIds = channels.map((chanel) => chanel.channel_id); + return { ...options, channelIds: allIds }; + } + + const selectedChannelIds = await utils.promptUserToSelectChannel(channels); + return { ...options, channelIds: selectedChannelIds }; }; utils.promptUserToSelectChannel = async (channels) => { @@ -356,9 +361,9 @@ utils.promptUserToSelectChannel = async (channels) => { const questions = [ { - type: 'list', - name: 'channelId', - message: 'Which channel would you like to use?', + type: 'checkbox', + name: 'channelIds', + message: 'Which channel(s) would you like to use?', choices: channels.map((channel) => ({ name: channel.url, value: channel.channel_id, @@ -367,7 +372,7 @@ utils.promptUserToSelectChannel = async (channels) => { ]; const answer = await Inquirer.prompt(questions); - return answer.channelId; + return answer.channelIds; }; utils.promptUserForVariation = async (options) => { @@ -414,18 +419,18 @@ utils.requestToApplyVariation = async (options) => { config: { accessToken }, storeHash, variationId, - channelId, + channelIds, } = options; const apiHost = options.apiHost || options.config.apiHost; if (options.applyTheme) { await themeApiClient.activateThemeByVariationId({ - accessToken, + variationId, + channelIds, apiHost, storeHash, - variationId, - channelId, + accessToken, }); } diff --git a/lib/stencil-push.utils.spec.js b/lib/stencil-push.utils.spec.js index bcdaf94a..5dd442e1 100644 --- a/lib/stencil-push.utils.spec.js +++ b/lib/stencil-push.utils.spec.js @@ -1,7 +1,9 @@ const axios = require('axios'); const MockAdapter = require('axios-mock-adapter'); -const { getStoreHash } = require('./stencil-push.utils'); +const { getStoreHash, promptUserForChannel, getChannels } = require('./stencil-push.utils'); +const utils = require('./stencil-push.utils'); +const themeApiClient = require('./theme-api-client'); const axiosMock = new MockAdapter(axios); @@ -11,10 +13,37 @@ describe('stencil push utils', () => { port: 4000, accessToken: 'accessTokenValue', }; + const optionsApplyThemeIsFalse = { + config: { accessToken: 'asdasd33' }, + applyTheme: false, + apiHost: 'abc2342', + }; + const optionsApplyThemeIsTrueAndChannels = { + config: { accessToken: 'asdasd33' }, + applyTheme: true, + channelIds: [1, 2], + }; + const optionsResult = { + applyTheme: true, + apiHost: 'abc2342', + allChannels: true, + channels: [ + { + url: 'https://abc.com', + channel_id: 1, + }, + { + url: 'https://fff.com', + channel_id: 2, + }, + ], + channelIds: [1, 2], + }; afterEach(() => { jest.restoreAllMocks(); axiosMock.reset(); + jest.clearAllMocks(); }); describe('.getStoreHash()', () => { @@ -39,4 +68,94 @@ describe('stencil push utils', () => { ); }); }); + + describe('.getChannels', () => { + it('should return options when applyTheme is false', async () => { + const result = await getChannels(optionsApplyThemeIsFalse); + expect(result).toEqual(optionsApplyThemeIsFalse); + }); + + it('should return options when applyTheme is true and channelIds available', async () => { + const result = await getChannels(optionsApplyThemeIsTrueAndChannels); + expect(result).toEqual(optionsApplyThemeIsTrueAndChannels); + }); + + it('should call getStoreChannels', async () => { + const options = { + applyTheme: true, + config: { + accessToken: 'asdasdqweq', + }, + }; + const spy = jest.spyOn(themeApiClient, 'getStoreChannels').mockReturnValue([]); + + await getChannels(options); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('.promptUserForChannel', () => { + const mockPromptUserToSelectChannel = jest + .spyOn(utils, 'promptUserToSelectChannel') + .mockReturnValue({}); + + it('should return options when applyTheme is false and no channelIds available', async () => { + const result = await promptUserForChannel(optionsApplyThemeIsFalse); + + expect(mockPromptUserToSelectChannel).toHaveBeenCalledTimes(0); + expect(result).toEqual(optionsApplyThemeIsFalse); + }); + + it('should return options when applyTheme is true and channelIds available', async () => { + const result = await promptUserForChannel(optionsApplyThemeIsTrueAndChannels); + + expect(mockPromptUserToSelectChannel).toHaveBeenCalledTimes(0); + expect(result).toEqual(optionsApplyThemeIsTrueAndChannels); + }); + + it('should return options with all channelIds available when -allc option used', async () => { + const options = { + applyTheme: true, + apiHost: 'abc2342', + allChannels: true, + channels: [ + { + url: 'https://abc.com', + channel_id: 1, + }, + { + url: 'https://fff.com', + channel_id: 2, + }, + ], + }; + + const result = await promptUserForChannel(options); + + expect(mockPromptUserToSelectChannel).toHaveBeenCalledTimes(0); + expect(result).toEqual(optionsResult); + }); + + it('should call promptUserToSelectChannel when applyTheme is true and no channelIds available', async () => { + const options = { + applyTheme: true, + apiHost: 'abc2342', + channels: [ + { + url: 'https://abc.com', + channel_id: 1, + }, + ], + }; + + const utilsPromptUserForChannelStub = jest + .spyOn(utils, 'promptUserToSelectChannel') + .mockReturnValue([]); + + promptUserForChannel(options); + + expect(utilsPromptUserForChannelStub).toHaveBeenCalled(); + }); + }); }); diff --git a/lib/theme-api-client.js b/lib/theme-api-client.js index 8b1ada8c..b844ff53 100644 --- a/lib/theme-api-client.js +++ b/lib/theme-api-client.js @@ -65,33 +65,36 @@ async function checkCliVersion({ storeUrl, currentCliVersion = PACKAGE_INFO.vers /** * @param {object} options * @param {string} options.variationId - * @param {string} options.channelId + * @param {array} options.channelIds * @param {string} options.apiHost * @param {string} options.storeHash * @param {string} options.accessToken - * @returns {Promise} + * @returns {Promise} */ async function activateThemeByVariationId({ variationId, - channelId, + channelIds, apiHost, storeHash, accessToken, }) { try { - return await networkUtils.sendApiRequest({ - url: `${apiHost}/stores/${storeHash}/v3/themes/actions/activate`, - headers: { - 'content-type': 'application/json', - }, - method: 'POST', - accessToken, - data: { - variation_id: variationId, - channel_id: channelId, - which: 'original', - }, - }); + const promises = channelIds.map((id) => + networkUtils.sendApiRequest({ + url: `${apiHost}/stores/${storeHash}/v3/themes/actions/activate`, + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + accessToken, + data: { + variation_id: variationId, + channel_id: Number(id), + which: 'original', + }, + }), + ); + return Promise.all(promises); } catch (err) { err.name = err.response && err.response.status === 504