From 00c7afd961a9fbaafc44fbf7ee9883b6e77c9c28 Mon Sep 17 00:00:00 2001 From: Dave Clark Date: Wed, 18 Aug 2021 01:03:29 +0100 Subject: [PATCH] chore: update storybook IA (#1096) * chore: create a master storybook structure * chore: fill out the storybook structure * chore: add name spacing and use storybook structure for all components * chore: enable renaming and add legacy tags * chore: update CDAI story helper test --- packages/cdai/config.js | 8 - .../src/component_helpers/StorybookHelper.js | 13 +- .../component_helpers/StorybookHelper.spec.js | 4 +- .../src/global/js/utils/story-helper.js | 84 +----- packages/core/.storybook/manager.js | 28 ++ packages/core/.storybook/preview.js | 38 +-- packages/core/story-structure.js | 254 ++++++++++++++++++ .../src/ComboButton/ComboButton.stories.js | 5 +- 8 files changed, 321 insertions(+), 113 deletions(-) delete mode 100644 packages/cdai/config.js create mode 100644 packages/core/story-structure.js diff --git a/packages/cdai/config.js b/packages/cdai/config.js deleted file mode 100644 index 2e00f27da7..0000000000 --- a/packages/cdai/config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright IBM Corp. 2020, 2020 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -export const sectionTitle = 'Legacy/CD&AI'; diff --git a/packages/cdai/src/component_helpers/StorybookHelper.js b/packages/cdai/src/component_helpers/StorybookHelper.js index 24ac591a0c..698f046556 100644 --- a/packages/cdai/src/component_helpers/StorybookHelper.js +++ b/packages/cdai/src/component_helpers/StorybookHelper.js @@ -1,12 +1,17 @@ // -// Copyright IBM Corp. 2020, 2020 +// Copyright IBM Corp. 2020, 2021 // // This source code is licensed under the Apache-2.0 license found in the // LICENSE file in the root directory of this source tree. // -import { sectionTitle } from '../../config'; +import { getPathForComponent } from '../../../core/story-structure'; -export const getComponentLabel = (component) => { - return `${sectionTitle}/${component}`; +export const getComponentLabel = (componentName) => { + const title = + // if the component isn't in the master structure, put it in a lost+found section + getPathForComponent('a', componentName) || + `Cloud & Cognitive/Lost + found/${componentName}`; + + return title; }; diff --git a/packages/cdai/src/component_helpers/StorybookHelper.spec.js b/packages/cdai/src/component_helpers/StorybookHelper.spec.js index 3cb9f98db2..7fd2cdd0f8 100644 --- a/packages/cdai/src/component_helpers/StorybookHelper.spec.js +++ b/packages/cdai/src/component_helpers/StorybookHelper.spec.js @@ -9,6 +9,8 @@ import { getComponentLabel } from './StorybookHelper'; describe('getComponentLabel', () => { it('returns the expected output for a component', () => { - expect(getComponentLabel('IdeButton')).toEqual('Legacy/CD&AI/IdeButton'); + expect(getComponentLabel('IdeButton')).toEqual( + 'CD&AI legacy/Components/IdeButton#legacy' + ); }); }); diff --git a/packages/cloud-cognitive/src/global/js/utils/story-helper.js b/packages/cloud-cognitive/src/global/js/utils/story-helper.js index 6479c13c8a..29198e7c07 100644 --- a/packages/cloud-cognitive/src/global/js/utils/story-helper.js +++ b/packages/cloud-cognitive/src/global/js/utils/story-helper.js @@ -7,66 +7,7 @@ import { sanitize } from '@storybook/csf'; import pkg from '../package-settings'; - -// A set of title values, one for each component, that can be converted into -// a structure story title or into an individual story id in slug form. -const lib = 'Cloud & Cognitive'; -const storybookStructure = { - AboutModal: `${lib}/$rci/$comp`, - ActionBar: `${lib}/$rci/$comp`, - ActionSet: `${lib}/$rci/$comp`, - APIKeyModal: `${lib}/$rci/$comp`, - BreadcrumbWithOverflow: `${lib}/$rci/$comp`, - ButtonMenu: `${lib}/$rci/$comp`, - ButtonSetWithOverflow: `${lib}/$rci/$comp`, - - // cards - ExpressiveCard: `${lib}/$rci/Cards/$comp`, - ProductiveCard: `${lib}/$rci/Cards/$comp`, - - ComboButton: `${lib}/$rci/$comp`, - CreateFullPage: `${lib}/$rci/$comp`, - CreateModal: `${lib}/$rci/$comp`, - CreateSidePanel: `${lib}/$rci/$comp`, - CreateTearsheet: `${lib}/$rci/$comp`, - CreateTearsheetNarrow: `${lib}/$rci/$comp`, - - // empty states - EmptyState: `${lib}/$rci/EmptyStates/$comp`, - ErrorEmptyState: `${lib}/$rci/EmptyStates/$comp`, - NoDataEmptyState: `${lib}/$rci/EmptyStates/$comp`, - NoTagsEmptyState: `${lib}/$rci/EmptyStates/$comp`, - NotFoundEmptyState: `${lib}/$rci/EmptyStates/$comp`, - NotificationsEmptyState: `${lib}/$rci/EmptyStates/$comp`, - UnauthorizedEmptyState: `${lib}/$rci/EmptyStates/$comp`, - - ExampleComponent: `${lib}/$rci/$comp`, - ExportModal: `${lib}/$rci/$comp`, - - // HTTP errors - HTTPError403: `${lib}/$rci/HTTPErrors/$comp`, - HTTPError404: `${lib}/$rci/HTTPErrors/$comp`, - HTTPErrorOther: `${lib}/$rci/HTTPErrors/$comp`, - - ImportModal: `${lib}/$rci/$comp`, - LoadingBar: `${lib}/$rci/$comp`, - ModifiedTabs: `${lib}/$rci/$comp`, - NotificationsPanel: `${lib}/$rci/$comp`, - PageHeader: `${lib}/$rci/$comp`, - RemoveModal: `${lib}/$rci/$comp`, - Saving: `${lib}/$rci/$comp`, - SidePanel: `${lib}/$rci/$comp`, - StatusIcon: `${lib}/$rci/$comp`, - TagSet: `${lib}/$rci/$comp`, - Toolbar: `${lib}/$rci/$comp`, - UserProfileImage: `${lib}/$rci/$comp`, - WebTerminal: `${lib}/$rci/$comp`, - - // tearsheets - Tearsheet: `${lib}/$rci/Tearsheets/$comp`, - TearsheetNarrow: `${lib}/$rci/Tearsheets/$comp`, - TearsheetShell: `${lib}/$rci/$comp`, -}; +import { getPathForComponent } from '../../../../../core/story-structure'; /** * A helper function to return the structured story title for a component. @@ -74,19 +15,16 @@ const storybookStructure = { * @returns The structured story title. */ export const getStoryTitle = (componentName) => { - const replacements = { - $rci: pkg.isComponentEnabled(componentName, true) - ? 'Released' - : pkg.isComponentPublic(componentName, true) - ? 'Canary' - : 'Internal', - $comp: componentName, - }; - - return Object.keys(replacements).reduce( - (result, token) => result.replaceAll(token, replacements[token]), - storybookStructure[componentName] || `${lib}/Lost + Found/$comp` - ); + const title = + // if the component isn't in the master structure, put it in a lost+found section + getPathForComponent('c', componentName) || + `Cloud & Cognitive/Lost + found/${componentName}`; + + // add a canary tag if the component is public but not normally enabled + return !pkg.isComponentEnabled(componentName, true) && + pkg.isComponentPublic(componentName, true) + ? `${title}#canary` + : title; }; /** diff --git a/packages/core/.storybook/manager.js b/packages/core/.storybook/manager.js index 9f058ae09b..57d4560204 100644 --- a/packages/core/.storybook/manager.js +++ b/packages/core/.storybook/manager.js @@ -53,6 +53,34 @@ addons.setConfig({ ); + // if the name has #legacy then render a 'Legacy' tag on the right + case 'legacy': + return ( +
+ {parts[0]} +
+ Legacy +
+
+ ); + // if the name doesn't have a recognized # label, just render the name default: return parts[0]; diff --git a/packages/core/.storybook/preview.js b/packages/core/.storybook/preview.js index 0f551343a1..e78c82ae2f 100644 --- a/packages/core/.storybook/preview.js +++ b/packages/core/.storybook/preview.js @@ -13,6 +13,7 @@ import { withCarbonTheme } from '@carbon/storybook-addon-theme/react'; import { pkg } from '../../cloud-cognitive/src/settings'; import index from './index.scss'; +import { getSectionSequence } from '../story-structure'; // Enable all components, whether released or not, for storybook purposes pkg._silenceWarnings(true); @@ -45,18 +46,6 @@ const decorators = [ withCarbonTheme, ]; -const order = [ - 'Cloud & Cognitive/Released', - 'Cloud & Cognitive/Canary', - 'Cloud & Cognitive/Internal', - 'Legacy', -]; -const toOrder = (value) => { - const inOrder = order.findIndex((item) => value.startsWith(item)); - // length is last index + 1 - return inOrder < 0 ? order.length : inOrder; -}; - const makeViewport = (name, width, shadow) => ({ name, styles: { @@ -90,19 +79,18 @@ const parameters = { layout: 'centered', options: { storySort: (a, b) => { - // const storybookOrder = ['Cloud & Cognitive', ['Released', 'Canary'], 'Legacy']; - const aInOrder = toOrder(a[1].kind); - const bInOrder = toOrder(b[1].kind); - - if (aInOrder !== bInOrder) { - return aInOrder - bInOrder; - } else { - // from storybook doc example https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy - return a[1].kind === b[1].kind - ? (a[1]?.parameters?.ccsSettings?.sequence || 0) - - (b[1]?.parameters?.ccsSettings?.sequence || 0) - : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }); - } + const aPosition = getSectionSequence(a[1].kind); + const bPosition = getSectionSequence(b[1].kind); + + return aPosition !== bPosition + ? // if stories have different positions in the structure, sort by that + aPosition - bPosition + : a[1].kind === b[1].kind + ? // if they have the same kind, use their sequence numbers + (a[1]?.parameters?.ccsSettings?.sequence || 0) - + (b[1]?.parameters?.ccsSettings?.sequence || 0) + : // they must both be unrecognized: fall back to sorting by id (slug) + a[1].id.localeCompare(b[1].id, undefined, { numeric: true }); }, }, diff --git a/packages/core/story-structure.js b/packages/core/story-structure.js new file mode 100644 index 0000000000..20afac97da --- /dev/null +++ b/packages/core/story-structure.js @@ -0,0 +1,254 @@ +/** + * Copyright IBM Corp. 2021, 2021 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +// This is the structure of the storybook navigation. Each entry has a name (n) +// and a structure (s) which is an array of items, each of which can be a +// component name or another entry. Component names include a prefix to enable +// name-spacing: the prefix is used when searching, but not included in +// materialized paths. Component names may also be renamed for display by +// adding the new name after a colon: the new name is used in materialized +// paths but ignored during searching. +const s = [ + { + n: 'Cloud & Cognitive', + s: [ + { + n: 'Components', + s: [ + { n: 'Cards', s: ['c/ExpressiveCard', 'c/ProductiveCard'] }, + { n: 'Progressive loading bar', s: ['c/LoadingBar'] }, + { n: 'Side panel', s: ['c/SidePanel'] }, + { n: 'Tag set', s: ['c/TagSet'] }, + { n: 'Tearsheet', s: ['c/Tearsheet', 'c/TearsheetNarrow'] }, + { n: 'Modified tabs', s: ['c/ModifiedTabs'] }, + { n: 'Page header', s: ['c/PageHeader'] }, + ], + }, + { + n: 'Patterns', + s: [ + { n: 'About modal', s: ['c/AboutModal'] }, + { n: 'Generating an API key', s: ['c/APIKeyModal'] }, + { + n: 'Empty state', + s: [ + 'c/EmptyState', + 'c/ErrorEmptyState', + 'c/NoDataEmptyState', + 'c/NoTagsEmptyState', + 'c/NotFoundEmptyState', + 'c/NotificationsEmptyState', + 'c/UnauthorizedEmptyState', + ], + }, + { n: 'Export', s: ['c/ExportModal'] }, + { + n: 'Create flows', + s: [ + 'c/CreateFullPage', + 'c/CreateModal', + 'c/CreateTearsheet', + 'c/CreateTearsheetNarrow', + 'c/CreateSidePanel', + ], + }, + { + n: 'HTTP errors', + s: ['c/HTTPError403', 'c/HTTPError404', 'c/HTTPErrorOther'], + }, + { n: 'Import and upload', s: ['c/ImportModal'] }, + { n: 'Notifications', s: ['c/NotificationsPanel'] }, + { n: 'Remove', s: ['c/RemoveModal'] }, + { n: 'Saving', s: ['c/Saving'] }, + { n: 'Status icons', s: ['c/StatusIcon'] }, + { n: 'Toolbars', s: ['c/Toolbar'] }, + { n: 'User profile images', s: ['c/UserProfileImage'] }, + { n: 'Web terminal', s: ['c/WebTerminal'] }, + ], + }, + { + n: 'Internal', + s: [ + 'c/ActionBar', + 'c/ActionSet', + 'c/BreadcrumbWithOverflow', + 'c/ButtonMenu', + 'c/ButtonSetWithOverflow', + 'c/ComboButton', + 'c/ExampleComponent', + 'c/TearsheetShell', + ], + }, + ], + }, + { + n: 'Carbon', + s: [], + }, + { + n: 'Security', + s: [ + { + n: 'Components', + s: [ + 's/Card:Card#legacy', + 's/Decorator', + 's/DataTablePagination', + 's/DelimitedList', + 's/ExternalLink', + 's/ICA', + 's/Icon:Icon#legacy', + 's/IconButton', + 's/Nav:Nav#legacy', + 's/Pill', + 's/ProfileImage:ProfileImage#legacy', + 's/ScrollGradient', + 's/StackedNotification', + 's/StatusIcon:StatusIcon#legacy', + 's/StringFormatter', + 's/TruncatedList', + 's/TimeIndicator', + 's/TrendingCard:TrendingCard#legacy', + 's/TypeLayout', + ], + }, + { + n: 'Patterns', + s: [ + 's/ComboButton', + 's/DataDecorator', + 's/ErrorPage:ErrorPage#legacy', + 's/FilterPanel', + 's/Header:Header#legacy', + 's/IconButtonBar', + 's/NonEntitledSection', + 's/Panel:Panel#legacy', + 's/PanelV2:PanelV2#legacy', + 's/SearchBar', + 's/Shell:Shell#legacy', + 's/StatusIndicator:StatusIndicator#legacy', + 's/SummaryCard:SummaryCard#legacy', + 's/TagWall', + 's/TagWallFilter', + 's/Tearsheet:Tearsheet#legacy', + 's/TearsheetSmall:TearsheetSmall#legacy', + 's/Toolbar:Toolbar#legacy', + 's/Wizard:Wizard#legacy', + ], + }, + { + n: 'Layouts', + s: [], + }, + ], + }, + { + n: 'CD&AI legacy', + s: [ + { + n: 'Components', + s: [ + 'a/ContextHeader:ContextHeader#legacy', + 'a/IdeAPIKeyGeneration:IdeAPIKeyGeneration#legacy', + 'a/IdeButton:IdeButton#legacy', + 'a/IdeCard:IdeCard#legacy', + 'a/IdeCreate:IdeCreate#legacy', + 'a/IdeDataTable:IdeDataTable#legacy', + 'a/IdeEmptyState:IdeEmptyState#legacy', + 'a/IdeFilter:IdeFilter#legacy', + 'a/IdeHome:IdeHome#legacy', + 'a/IdeHTTPErrors:IdeHTTPErrors#legacy', + 'a/IdeImporting:IdeImporting#legacy', + 'a/IdeNavigation:IdeNavigation#legacy', + 'a/IdeRemove:IdeRemove#legacy', + 'a/IdeSaving:IdeSaving#legacy', + 'a/IdeSideNavMenu:IdeSideNavMenu#legacy', + 'a/IdeSlideOverPanel:IdeSlideOverPanel#legacy', + 'a/IdeTableToolbarSearch:IdeTableToolbarSearch#legacy', + ], + }, + ], + }, + { + n: 'Getting Started', + s: [], + }, +]; + +const getEntryDisplayName = (name) => { + const match = name.match(/.*?\/(?:(.*):(.*)|(.*))/); + return match[2] ?? match[3]; +}; + +// This function takes an s array and returns an array of the fully +// materialized paths in the storybook structure in order +const getSectionOrder = (sArray) => + sArray + .map((entry) => + typeof entry === 'string' + ? // if the entry is a string, return it (without the prefix) + getEntryDisplayName(entry) + : // if the entry is another structure, return its name, but first get + // the fully materialized paths it contains and add the entry name + // to the front of each + [ + getSectionOrder(entry.s).map((path) => `${entry.n}/${path}`), + entry.n, + ] + ) + .flat(Infinity); + +// An array of the names of storybook sections in the desired order. +const sectionOrder = getSectionOrder(s).concat(['']); + +/** + * Return the sequence number of a story kind in the storybook structure. + * This can be used to sort stories into the required hierarchy. + * @param {string} kind a story kind + * @returns the sequence number for the story kind in the storybook structure. + */ +export const getSectionSequence = (kind) => + sectionOrder.findIndex((item) => kind.startsWith(item)); + +const prepend = (elt, arr) => arr && [elt].concat(arr); + +const getEntryPrefixAndComponentName = (name) => { + const match = name.match(/(?:(.*):(.*)|(.*?\/.*))/); + return match[1] ?? match[3]; +}; + +// This function takes an s array and component name and returns the +// materialized path of the component name as an array of the nested section +// names, or null if the component name is not found +const getPath = (s, componentAndPrefix, componentName) => + s.reduce( + (found, next) => + // if a previous entry has already matched, pass it forward + found || + (typeof next === 'string' + ? // if this entry is a string, and it matches the component name + // we're looking for, return it as an array, else return null + getEntryPrefixAndComponentName(next) === componentAndPrefix + ? [getEntryDisplayName(next)] + : null + : // if this entry is another structure, find the materialized path + // into it and prepend its name if found and return null otherwise + prepend(next.n, getPath(next.s, componentAndPrefix, componentName))), + null + ); + +/** + * Return the storybook path for a component, given the prefix and name of the + * component. The prefix enables different components with the same name to be + * included in the storybook structure. + * @param {string} prefix The prefix for the component. + * @param {string} componentName The name of the component. + * @returns The path for the component storybook entry, or null if the + * component is not listed in the storybook structure. + */ +export const getPathForComponent = (prefix, componentName) => + getPath(s, `${prefix}/${componentName}`, componentName)?.join('/'); diff --git a/packages/security/src/ComboButton/ComboButton.stories.js b/packages/security/src/ComboButton/ComboButton.stories.js index d2ed761065..571bc0b12e 100644 --- a/packages/security/src/ComboButton/ComboButton.stories.js +++ b/packages/security/src/ComboButton/ComboButton.stories.js @@ -8,13 +8,14 @@ import CloudApp16 from '@carbon/icons-react/lib/cloud-app/16'; import React from 'react'; -import { sectionTitle } from '../../config'; import { ComboButton, ComboButtonItem } from '..'; +import { getPathForComponent } from '../../../core/story-structure'; + import styles from './_combo-button.scss'; export default { - title: `${sectionTitle}/${ComboButton.name}`, + title: getPathForComponent('s', ComboButton.name), component: ComboButton, subcomponents: { ComboButtonItem,