Skip to content

Commit

Permalink
feat(storefront): strf-9582 stencil push: apply theme to multiple sto…
Browse files Browse the repository at this point in the history
…refronts (bigcommerce#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;
  • Loading branch information
bc-max authored and jairo-bc committed Jan 14, 2022
1 parent acb418c commit d057a38
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 36 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions bin/stencil-push.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,23 @@ 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 <channelIds...>',
'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();

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) {
Expand Down
33 changes: 19 additions & 14 deletions lib/stencil-push.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) => {
Expand All @@ -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,
Expand All @@ -367,7 +372,7 @@ utils.promptUserToSelectChannel = async (channels) => {
];

const answer = await Inquirer.prompt(questions);
return answer.channelId;
return answer.channelIds;
};

utils.promptUserForVariation = async (options) => {
Expand Down Expand Up @@ -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,
});
}

Expand Down
121 changes: 120 additions & 1 deletion lib/stencil-push.utils.spec.js
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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()', () => {
Expand All @@ -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();
});
});
});
35 changes: 19 additions & 16 deletions lib/theme-api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>}
* @returns {Promise<Object[]>}
*/
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
Expand Down

0 comments on commit d057a38

Please sign in to comment.