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)
+
+
+
+## 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 && (
+
+ )}
+
+ ),
+});
+
+```
+
+## 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 ;