Skip to content

Commit

Permalink
Setup release notes logic on the client
Browse files Browse the repository at this point in the history
  • Loading branch information
kylesuss committed Jul 7, 2020
1 parent 94a5e90 commit 298ad9e
Show file tree
Hide file tree
Showing 21 changed files with 658 additions and 189 deletions.
4 changes: 4 additions & 0 deletions lib/api/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import * as provider from './modules/provider';
import * as addons from './modules/addons';
import * as channel from './modules/channel';
import * as notifications from './modules/notifications';
import * as settings from './modules/settings';
import * as releaseNotes from './modules/release-notes';
import * as stories from './modules/stories';
import * as refs from './modules/refs';
import * as layout from './modules/layout';
Expand Down Expand Up @@ -187,6 +189,8 @@ class ManagerProvider extends Component<ManagerProviderProps, State> {
addons,
layout,
notifications,
settings,
releaseNotes,
shortcuts,
stories,
refs,
Expand Down
60 changes: 60 additions & 0 deletions lib/api/src/modules/release-notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { RELEASE_NOTES_DATA } from 'global';
import memoize from 'memoizerific';

import { ModuleFn, State } from '../index';

export interface ReleaseNotes {
success?: boolean;
currentVersion?: string;
showOnFirstLaunch?: boolean;
}

const getReleaseNotesData = memoize(1)(
(): ReleaseNotes => {
try {
return { ...(JSON.parse(RELEASE_NOTES_DATA) || {}) };
} catch (e) {
return {};
}
}
);

export interface SubAPI {
releaseNotesVersion: () => string;
showReleaseNotesOnLaunch: () => Promise<boolean>;
}

export const init: ModuleFn = ({ fullAPI, store }) => {
let initStoreState: Promise<State[]>;
const releaseNotesData = getReleaseNotesData();

const api: SubAPI = {
releaseNotesVersion: () => releaseNotesData.currentVersion,
showReleaseNotesOnLaunch: async () => {
// Make sure any consumers of this function's return value have waited for
// this module's state to have been setup. Otherwise, it may be called
// before the store state is set and therefore have inaccurate data.
await initStoreState;
const { showReleaseNotesOnLaunch } = store.getState();
return showReleaseNotesOnLaunch;
},
};

const initModule = () => {
const { releaseNotesViewed: persistedReleaseNotesViewed } = store.getState();
let releaseNotesViewed = persistedReleaseNotesViewed || [];
const didViewReleaseNotes = releaseNotesViewed.includes(releaseNotesData.currentVersion);
const showReleaseNotesOnLaunch = releaseNotesData.showOnFirstLaunch && !didViewReleaseNotes;

if (showReleaseNotesOnLaunch) {
releaseNotesViewed = [...releaseNotesViewed, releaseNotesData.currentVersion];
}

initStoreState = Promise.all([
store.setState({ showReleaseNotesOnLaunch }),
store.setState({ releaseNotesViewed }, { persistence: 'permanent' }),
]);
};

return { init: initModule, api };
};
49 changes: 49 additions & 0 deletions lib/api/src/modules/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ModuleFn } from '../index';

export interface SubAPI {
changeSettingsTab: (tab: string) => void;
closeSettings: () => void;
isSettingsScreenActive: () => boolean;
navigateToSettingsPage: (path: string) => Promise<void>;
}

export const init: ModuleFn = ({ store, navigate, fullAPI }) => {
const isSettingsScreenActive = () => {
const { path } = fullAPI.getUrlState();
return !!(path || '').match(/^\/settings/);
};
const api: SubAPI = {
closeSettings: () => {
const {
settings: { lastTrackedStoryId },
} = store.getState();

if (lastTrackedStoryId) {
fullAPI.selectStory(lastTrackedStoryId);
} else {
fullAPI.selectFirstStory();
}
},
changeSettingsTab: (tab: string) => {
navigate(`/settings/${tab}`);
},
isSettingsScreenActive,
navigateToSettingsPage: async (path) => {
if (!isSettingsScreenActive()) {
const { settings, storyId } = store.getState();

await store.setState({
settings: { ...settings, lastTrackedStoryId: storyId },
});
}

navigate(path);
},
};

const initModule = async () => {
await store.setState({ settings: { lastTrackedStoryId: null } });
};

return { init: initModule, api };
};
22 changes: 21 additions & 1 deletion lib/api/src/modules/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface SubState {
export interface SubAPI {
storyId: typeof toId;
resolveStory: (storyId: StoryId, refsId?: string) => Story | Group | Root;
selectFirstStory: () => void;
selectStory: (
kindOrId: string,
story?: string,
Expand Down Expand Up @@ -223,6 +224,20 @@ export const init: ModuleFn = ({
storiesFailed: error,
});
},
selectFirstStory: () => {
const { storiesHash } = store.getState();
const lookupList = Object.keys(storiesHash).filter(
(k) => !(storiesHash[k].children || Array.isArray(storiesHash[k]))
);
const firstStory = lookupList[0];

if (firstStory) {
api.selectStory(firstStory);
return;
}

navigate('/story/*');
},
selectStory: (kindOrId, story = undefined, options = {}) => {
const { ref, viewMode: viewModeFromArgs } = options;
const {
Expand Down Expand Up @@ -301,10 +316,15 @@ export const init: ModuleFn = ({
// Later when we change story via the manager (or SELECT_STORY below), we'll already be at the
// correct path before CURRENT_STORY_WAS_SET is emitted, so this is less important (the navigate is a no-op)
// Note this is the case for refs also.
fullAPI.on(CURRENT_STORY_WAS_SET, function handleCurrentStoryWasSet({ storyId, viewMode }) {
fullAPI.on(CURRENT_STORY_WAS_SET, async function handleCurrentStoryWasSet({
storyId,
viewMode,
}) {
const { source }: { source: string } = this;
const [sourceType] = getSourceType(source);

if (fullAPI.isSettingsScreenActive()) return;

if (sourceType === 'local' && storyId && viewMode) {
navigate(`/${viewMode}/${storyId}`);
}
Expand Down
6 changes: 5 additions & 1 deletion lib/api/src/modules/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,14 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r
},
};

const initModule = () => {
const initModule = async () => {
fullAPI.on(NAVIGATE_URL, (url: string, options: { [k: string]: any }) => {
fullAPI.navigateUrl(url, options);
});

if (await fullAPI.showReleaseNotesOnLaunch()) {
navigate('/settings/release-notes');
}
};

return {
Expand Down
14 changes: 12 additions & 2 deletions lib/api/src/tests/stories.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,12 @@ describe('stories API', () => {
describe('CURRENT_STORY_WAS_SET event', () => {
it('navigates to the story', async () => {
const navigate = jest.fn();
const api = new LocalEventEmitter();
class EventEmitterWithSettings extends LocalEventEmitter {
isSettingsScreenActive() {
return false;
}
}
const api = new EventEmitterWithSettings();
const store = createMockStore({});
const { init } = initStories({ store, navigate, provider, fullAPI: api });

Expand All @@ -350,7 +355,12 @@ describe('stories API', () => {

it('DOES not navigate if a settings page was selected', async () => {
const navigate = jest.fn();
const api = new LocalEventEmitter();
class EventEmitterWithSettings extends LocalEventEmitter {
isSettingsScreenActive() {
return true;
}
}
const api = new EventEmitterWithSettings();
const store = createMockStore({ viewMode: 'settings', storyId: 'about' });
initStories({ store, navigate, provider, fullAPI: api });

Expand Down
78 changes: 69 additions & 9 deletions lib/core/src/server/build-dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,74 @@ const updateCheck = async (version) => {
return result;
};

const getReleaseNotesHistory = async (version) => {
// We only expect to have release notes available for major and minor releases.
// For this reason, we convert the actual version of the build here so that
// every place that relies on this data can reference the version of the
// release notes that we expect to use.
const getReleaseNotesVersion = (version) => {
const { major, minor } = semver.parse(version);
const { version: releaseNotesVersion } = semver.coerce(`${major}.${minor}`);
return releaseNotesVersion;
};

const getReleaseNotesFailedState = (version) => {
return {
success: false,
currentVersion: getReleaseNotesVersion(version),
showOnFirstLaunch: false,
};
};

export const RELEASE_NOTES_CACHE_KEY = 'releaseNotesData';

export const getReleaseNotesData = async (currentVersionToParse, fileSystemCache) => {
let result;
try {
const fromCache = await cache.get('releaseNotesHistory', []);
if (!fromCache.includes(version)) {
await cache.set('releaseNotesHistory', [...fromCache, version]);
const fromCache = await fileSystemCache.get('releaseNotesData', []);
const releaseNotesVersion = getReleaseNotesVersion(currentVersionToParse);
const versionHasNotBeenSeen = !fromCache.includes(releaseNotesVersion);

if (versionHasNotBeenSeen) {
await fileSystemCache.set('releaseNotesData', [...fromCache, releaseNotesVersion]);
}
result = { success: true, current: version, history: fromCache };

const sortedHistory = semver.sort(fromCache);
const highestVersionSeenInThePast = sortedHistory.slice(-1)[0];

let isUpgrading = false;
let isMajorOrMinorDiff = false;

if (highestVersionSeenInThePast) {
isUpgrading = semver.gt(releaseNotesVersion, highestVersionSeenInThePast);
const versionDiff = semver.diff(releaseNotesVersion, highestVersionSeenInThePast);
isMajorOrMinorDiff = versionDiff === 'major' || versionDiff === 'minor';
}

result = {
success: true,
showOnFirstLaunch:
versionHasNotBeenSeen &&
// Only show the release notes if this is not the first time Storybook
// has been built.
!!highestVersionSeenInThePast &&
isUpgrading &&
isMajorOrMinorDiff,
currentVersion: releaseNotesVersion,
};
} catch (error) {
result = { success: false };
console.log(`
ERROR
`);
console.log(error);
result = getReleaseNotesFailedState(currentVersionToParse);
}
return result;
};
Expand Down Expand Up @@ -270,17 +328,19 @@ export async function buildDevStandalone(options) {
const { host, extendServer, packageJson, versionUpdates, releaseNotes } = options;
const { version } = packageJson;

const [port, versionCheck, releaseNotesHistory] = await Promise.all([
const [port, versionCheck, releaseNotesData] = await Promise.all([
getFreePort(options.port),
versionUpdates
? updateCheck(version)
: Promise.resolve({ success: false, data: {}, time: Date.now() }),
releaseNotes ? getReleaseNotesHistory(version) : Promise.resolve({ success: false }),
releaseNotes
? getReleaseNotesData(version, cache)
: Promise.resolve(getReleaseNotesFailedState(version)),
]);

/* eslint-disable no-param-reassign */
options.versionCheck = versionCheck;
options.releaseNotesHistory = releaseNotesHistory;
options.releaseNotesData = releaseNotesData;
/* eslint-enable no-param-reassign */

if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) {
Expand Down
Loading

0 comments on commit 298ad9e

Please sign in to comment.