Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storefront): strf-9582 stencil push: apply theme to multiple storefronts #825

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
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