diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c23f1075713..6bcddd666d1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ /addons/actions/ @rhalff /addons/backgrounds/ @ndelangen /addons/centered/ @kazupon +/addons/edit-page/ @atanasster /addons/events/ @z4o4z @ndelangen /addons/graphql/ @mnmtanish /addons/info/ @theinterned @z4o4z @UsulPro @dangreenisrael diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index 4a4e5cf9721..b648a169e5e 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -2,6 +2,7 @@ 'addon: actions': ["addons/actions/**"] 'addon: backgrounds': ["addons/backgrounds/**"] 'addon: centered': ["addons/centered/**"] +'addon: edit-page': ["addons/edit-page/**"] 'addon: events ': ["addons/events/**"] 'addon: graphql ': ["addons/graphql/**"] 'addon: info': ["addons/info/**"] diff --git a/README.md b/README.md index f80eb15121a..61f8d0989ce 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl | [contexts](addons/contexts/) | Interactively inject component contexts for stories in the Storybook UI | | [cssresources](addons/cssresources/) | Dynamically add/remove css resources to the component iframe | | [design assets](addons/design-assets/) | View images, videos, weblinks alongside your story | +| [edit-page](addons/edit-page/) | Can add 'edit this page' links to your preview and docs pages | | [events](addons/events/) | Interactively fire events to components that respond to EventEmitter | | [graphql](addons/graphql/) | Query a GraphQL server within Storybook stories | | [google-analytics](addons/google-analytics) | Reports google analytics on stories | diff --git a/addons/edit-page/README.md b/addons/edit-page/README.md new file mode 100644 index 00000000000..b6fbe55902e --- /dev/null +++ b/addons/edit-page/README.md @@ -0,0 +1,84 @@ +# Storybook Addon Edit Page + +Storybook Edit Page Addon can add 'edit this page' links in [Storybook](https://storybook.js.org). + +[Framework Support](https://github.com/storybookjs/storybook/blob/master/ADDONS_SUPPORT.md) + +![React Storybook Screenshot](https://storybook.js.org/img/addon-backgrounds.gif) + +## Installation + +```sh +npm i -D @storybook/addon-edit-page +``` + +## Configuration + +Then create a file called `addons.js` in your storybook config. + +Add following content to it (the configuration settings are optional): + +```js +import { editPage } from '@storybook/addon-edit-page'; + +const gitPageResolver = ({ fileName } ) => { + return fileName; +} +editPage({ + fileNameResolve: gitPageResolver, + editPageLabel: 'edit this page...', + render: ({ filePath, shortName, ...rest }) => ( +
+ {filePath && ( +
+

{shortName}

+ + here + +
+ )} +
+ ), +}); + +``` + +## Usage + +You can add the source file name to the stories metadata in CSF: + +```js +export default { + title: 'Stories|With edit', + component: Link, + parameters: { + edit: { + fileName: 'https://github.com/storybookjs/design-system/blob/master/src/components/Link.js' + }, + } +}; +``` + +Or to mdx files: +```md + + +``` +## Options + +**fileNameResolve**: function to resolve the file name, by default returns the supplied fileName
+**editPageLabel**: label for the Edit this page link - by default `Edit this page`
+**render**: function to custom render the `Edit this page` panel
+```js +parameters : { + filePath: string, //full file path + shortName: string, //short name of the story file (component name) + parameters: any, //parameters of the current story +} +``` diff --git a/addons/edit-page/package.json b/addons/edit-page/package.json new file mode 100644 index 00000000000..c12a213cf26 --- /dev/null +++ b/addons/edit-page/package.json @@ -0,0 +1,39 @@ +{ + "name": "@storybook/addon-edit-page", + "version": "5.2.0-rc.0", + "description": "A storybook addon that can insert 'edit this page' links", + "keywords": [ + "addon", + "edit", + "react", + "storybook" + ], + "homepage": "https://github.com/storybookjs/storybook/tree/master/addons/edit-page", + "bugs": { + "url": "https://github.com/storybookjs/storybook/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/storybookjs/storybook.git", + "directory": "addons/edit-page" + }, + "license": "MIT", + "author": "@atanasster", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "prepare": "node ../../scripts/prepare.js" + }, + "dependencies": { + "@storybook/addons": "5.2.0-rc.0", + "@storybook/api": "5.2.0-rc.0", + "@storybook/components": "5.2.0-rc.0", + "@storybook/theming": "5.2.0-rc.0", + "core-js": "^3.0.1", + "memoizerific": "^1.11.3", + "react": "^16.8.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/addons/edit-page/register.js b/addons/edit-page/register.js new file mode 100644 index 00000000000..cc38cb06f1f --- /dev/null +++ b/addons/edit-page/register.js @@ -0,0 +1 @@ +require('./dist/register'); diff --git a/addons/edit-page/src/components/PreviewPanel.tsx b/addons/edit-page/src/components/PreviewPanel.tsx new file mode 100644 index 00000000000..e2b85183cd4 --- /dev/null +++ b/addons/edit-page/src/components/PreviewPanel.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Combo, Consumer } from '@storybook/api'; +import { styled } from '@storybook/theming'; +import { Link } from '@storybook/components'; +import { H3 } from '@storybook/components/html'; +import { EditStoriesProps } from '../types'; + +const StyledContainer = styled.div<{}>(({ theme }) => ({ + padding: '10px', + background: theme.background.bar, + borderBottom: `1px solid ${theme.color.border}`, +})); + +interface PanelProps { + filePath?: string; + shortName?: string; + config?: EditStoriesProps; +} + +const EditInject = ({ filePath, shortName, config }: PanelProps) => ( + + {filePath && ( + +

{shortName}

+ + {config.editPageLabel} + +
+ )} +
+); + +const mapper = ({ state }: Combo): { story: any } => { + const story = state.storiesHash[state.storyId]; + return { story }; +}; + +export const PreviewPanel = (props: EditStoriesProps) => { + return ( + + + {({ story }: ReturnType) => { + if ( + story && + story.parameters && + story.parameters.edit && + story.parameters.edit.fileName + ) { + const rootSplit = story.kind.split(story.parameters.options.hierarchyRootSeparator); + const path = rootSplit[rootSplit.length - 1]; + const pathSplit = path.split(story.parameters.options.hierarchySeparator); + const shortName = pathSplit[pathSplit.length - 1]; + const filePath = props.fileNameResolve({ + id: story.id, + kind: story.kind, + name: story.name, + displayName: story.parameters.displayName, + fileName: story.parameters.edit.fileName, + shortName, + }); + if (filePath) { + if (typeof props.render === 'function') { + return props.render({ + filePath, + shortName, + parameters: story.parameters, + }); + } + return ; + } + } + return null; + }} + + + ); +}; diff --git a/addons/edit-page/src/index.tsx b/addons/edit-page/src/index.tsx new file mode 100644 index 00000000000..32832bddb2c --- /dev/null +++ b/addons/edit-page/src/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { addons, types } from '@storybook/addons'; +import { fileNameResolveType, EditStoriesProps } from './types'; +import { PreviewPanel } from './components/PreviewPanel'; + +const ADDON_ID = 'EDIT_PAGE_SOURCES'; + +const defaultFileNameResolve: fileNameResolveType = info => { + return info.fileName; +}; + +const defaultCMSProps: EditStoriesProps = { + fileNameResolve: defaultFileNameResolve, + editPageLabel: 'Edit this page', +}; + +export const editPage = (config: EditStoriesProps) => { + addons.register(ADDON_ID, () => { + addons.add(ADDON_ID, { + title: 'Edit source', + type: types.IFRAME_START, + render: () => , + }); + }); +}; + +if (module && (module as any).hot && (module as any).hot.decline) { + (module as any).hot.decline(); +} diff --git a/addons/edit-page/src/register.tsx b/addons/edit-page/src/register.tsx new file mode 100644 index 00000000000..ea465c2a34a --- /dev/null +++ b/addons/edit-page/src/register.tsx @@ -0,0 +1 @@ +export * from './index'; diff --git a/addons/edit-page/src/types.ts b/addons/edit-page/src/types.ts new file mode 100644 index 00000000000..26d43911b40 --- /dev/null +++ b/addons/edit-page/src/types.ts @@ -0,0 +1,22 @@ +export interface IFileInfo { + id?: string; + kind?: string; + name?: string; + displayName?: string; + fileName?: string; + shortName?: string; +} + +export type fileNameResolveType = (info: IFileInfo) => string; + +interface EditPageProps { + filePath?: string; + shortName?: string; + parameters?: any; +} + +export interface EditStoriesProps { + fileNameResolve?: fileNameResolveType; + editPageLabel?: string; + render?: (config: EditPageProps) => JSX.Element; +} diff --git a/addons/edit-page/tsconfig.json b/addons/edit-page/tsconfig.json new file mode 100644 index 00000000000..8876bb6737a --- /dev/null +++ b/addons/edit-page/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["webpack-env"] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/__tests__/**/*" + ] +} diff --git a/docs/src/new-components/basics/shared/site.js b/docs/src/new-components/basics/shared/site.js index 3ab939a7cc3..fab3da2f458 100644 --- a/docs/src/new-components/basics/shared/site.js +++ b/docs/src/new-components/basics/shared/site.js @@ -93,5 +93,6 @@ export const url = { accessibility: `${gitHubOrg}/storybook/tree/master/addons/a11y`, console: `${gitHubOrg}/storybook-addon-console`, links: `${gitHubOrg}/storybook/tree/master/addons/links`, + 'edit-page': `${gitHubOrg}/storybook/tree/master/addons/edit-page`, }, }; diff --git a/examples/official-storybook/addons.js b/examples/official-storybook/addons.js index 00d972b2185..5c36303a45e 100644 --- a/examples/official-storybook/addons.js +++ b/examples/official-storybook/addons.js @@ -14,7 +14,14 @@ import '@storybook/addon-jest/register'; import '@storybook/addon-viewport/register'; import '@storybook/addon-graphql/register'; import '@storybook/addon-contexts/register'; - +import { editPage } from '@storybook/addon-edit-page'; import addHeadWarning from './head-warning'; +const gitPageResolver = ({ fileName }) => { + return fileName; +}; +editPage({ + fileNameResolve: gitPageResolver, +}); + addHeadWarning('manager-head-not-loaded', 'Manager head not loaded'); diff --git a/examples/official-storybook/package.json b/examples/official-storybook/package.json index 80b3fb13a6e..040d81c303a 100644 --- a/examples/official-storybook/package.json +++ b/examples/official-storybook/package.json @@ -23,6 +23,7 @@ "@storybook/addon-cssresources": "5.2.0-rc.0", "@storybook/addon-design-assets": "5.2.0-rc.0", "@storybook/addon-docs": "5.2.0-rc.0", + "@storybook/addon-edit-page": "5.2.0-rc.0", "@storybook/addon-events": "5.2.0-rc.0", "@storybook/addon-graphql": "5.2.0-rc.0", "@storybook/addon-info": "5.2.0-rc.0", diff --git a/examples/official-storybook/stories/addon-a11y/base-button.stories.js b/examples/official-storybook/stories/addon-a11y/base-button.stories.js index a882bf92a4e..92bede8d780 100644 --- a/examples/official-storybook/stories/addon-a11y/base-button.stories.js +++ b/examples/official-storybook/stories/addon-a11y/base-button.stories.js @@ -9,6 +9,10 @@ export default { component: BaseButton, parameters: { options: { selectedPanel: 'storybook/a11y/panel' }, + edit: { + fileName: + 'https://github.com/storybookjs/storybook/blob/next/lib/components/src/Button/Button.tsx', + }, }, }; diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts index 35b7b2ba698..a780b562075 100644 --- a/lib/addons/src/types.ts +++ b/lib/addons/src/types.ts @@ -7,6 +7,8 @@ export enum types { TOOL = 'tool', PREVIEW = 'preview', NOTES_ELEMENT = 'notes-element', + IFRAME_START = 'iframe_start', + IFRAME_END = 'iframe_end', } export type Types = types | string; diff --git a/lib/ui/src/components/preview/preview.js b/lib/ui/src/components/preview/preview.js index dbbf46926d7..deaa4d2cdf5 100644 --- a/lib/ui/src/components/preview/preview.js +++ b/lib/ui/src/components/preview/preview.js @@ -53,13 +53,21 @@ const ActualPreview = ({ scale, queryParams, customCanvas, + iframeStart, + iframeEnd, }) => { const data = [storyId, viewMode, id, baseUrl, scale, queryParams]; const base = customCanvas ? customCanvas(...data) : renderIframe(...data); - + const iFrame = ( + + {iframeStart.map(({ render }) => render())} + {base} + {iframeEnd.map(({ render }) => render())} + + ); return wrappers.reduceRight( (acc, wrapper, index) => wrapper.render({ index, children: acc, id, storyId, active }), - base + iFrame ); }; @@ -233,7 +241,8 @@ class Preview extends Component { } = this.props; const toolbarHeight = options.isToolshown ? 40 : 0; - + const iframeStart = getElementList(getElements, types.IFRAME_START, []); + const iframeEnd = getElementList(getElements, types.IFRAME_END, []); const wrappers = getElementList(getElements, types.PREVIEW, defaultWrappers); const panels = getElementList(getElements, types.TAB, [ { @@ -252,6 +261,8 @@ class Preview extends Component { queryParams, scale: value, customCanvas, + iframeStart, + iframeEnd, }; return ;