storybook-addon-source-link
Version:
Add a button to the Storybook toolbar that opens the file containing the Story in an IDE like VSCode.
154 lines (149 loc) • 5.21 kB
JavaScript
import { useStorybookApi, useChannel, addons, types } from 'storybook/manager-api';
import * as iconsModule from '@storybook/icons';
import { CopyIcon, CheckIcon, JumpToIcon } from '@storybook/icons';
import React2, { memo, useState, useCallback, useMemo } from 'react';
import { WithTooltip, TooltipLinkList, IconButton } from 'storybook/internal/components';
import { STORY_CHANGED } from 'storybook/internal/core-events';
import { styled } from 'storybook/theming';
// src/manager.ts
// src/constants.ts
var ADDON_ID = "storybook-addon-source-link";
var TOOL_ID = `${ADDON_ID}/tool`;
var EVENTS = {
REQUEST_RESOLVABLE_PARAM: `${ADDON_ID}/request-resolve`,
RESPONSE_RESOLVABLE_PARAM: `${ADDON_ID}/response-resolve`
};
var resolveLinks = async (context) => {
const channel = addons.getChannel();
return new Promise((resolve) => {
channel.on(EVENTS.RESPONSE_RESOLVABLE_PARAM, (resolvedParam) => {
resolve(resolvedParam);
});
channel.emit(EVENTS.REQUEST_RESOLVABLE_PARAM, context);
});
};
var storybookIconMap = Object.fromEntries(
Object.entries(iconsModule).filter((entry) => entry[0][0] === entry[0][0].toUpperCase()).map(([key, value]) => [key, value])
);
var isIconName = (name) => Object.keys(storybookIconMap).includes(name);
var StorybookIcon = ({ name }) => {
const IconComponent = storybookIconMap[name];
if (!IconComponent) return null;
return /* @__PURE__ */ React2.createElement(IconComponent, null);
};
// src/manager/Tooltip.tsx
var ColoredCopyIcon = styled(CopyIcon)`
fill: ${({ theme }) => theme.color.dark};
`;
var ColoredCheckIcon = styled(CheckIcon)`
fill: ${({ theme }) => theme.color.dark};
`;
var checkIsStaticBuild = () => {
try {
return window.CONFIG_TYPE !== "DEVELOPMENT";
} catch {
console.warn(
"[storybook-addon-source-link] window.CONFIG_TYPE is not defined. The value of isStaticBuild may be incorrect."
);
return false;
}
};
var Tool = memo(function MyAddonSelector() {
const rootPath = process.env.SOURCE_LINK_PROJECT_ROOT_PATH ?? "";
const isStaticBuild = checkIsStaticBuild();
const api = useStorybookApi();
const storyData = api.getCurrentStoryData();
const [linksData, setLinksData] = useState();
const [copyClickedLink, setCopyClickedLink] = useState();
useChannel({
[STORY_CHANGED]: () => {
setLinksData(void 0);
}
});
const onOpenTooltip = useCallback(() => {
setCopyClickedLink(void 0);
if (!storyData) return;
if (linksData) return;
resolveLinks({
rootPath,
isStaticBuild,
type: storyData.type,
importPath: storyData.importPath,
id: storyData.id,
title: storyData.title,
name: storyData.name,
tags: storyData.tags
}).then((link) => {
setLinksData(link);
});
}, [rootPath, isStaticBuild, storyData, linksData]);
const links = useMemo(() => {
return linksData?.map((item) => {
if (item.type === "editor") {
return {
id: item.id,
title: item.label,
onClick: () => {
api.openInEditor({
file: item.href
});
},
icon: item.icon && isIconName(item.icon) && /* @__PURE__ */ React2.createElement(StorybookIcon, { name: item.icon }),
order: item.order ?? 0
};
}
if (item.type === "copy") {
const clicked = !!copyClickedLink;
return {
id: item.id,
title: item.label,
onClick: () => {
navigator.clipboard.writeText(item.href);
if (!clicked) {
setCopyClickedLink(item.id);
}
},
icon: item.icon && isIconName(item.icon) && /* @__PURE__ */ React2.createElement(StorybookIcon, { name: item.icon }),
right: clicked ? /* @__PURE__ */ React2.createElement(ColoredCheckIcon, null) : /* @__PURE__ */ React2.createElement(ColoredCopyIcon, null),
order: item.order ?? 0
};
}
return {
id: item.id,
title: item.label,
href: item.href,
target: (item.type ?? "linkBlank") === "linkBlank" ? "_blank" : void 0,
icon: item.icon && isIconName(item.icon) && /* @__PURE__ */ React2.createElement(StorybookIcon, { name: item.icon }),
order: item.order ?? 0
};
}).sort((a, b) => {
if (a.order === b.order) {
return a.title.localeCompare(b.title);
}
return a.order - b.order;
});
}, [linksData, copyClickedLink]);
return /* @__PURE__ */ React2.createElement(
WithTooltip,
{
placement: "top",
closeOnOutsideClick: true,
onVisibleChange: (state) => {
if (state) {
onOpenTooltip();
}
},
tooltip: () => /* @__PURE__ */ React2.createElement(TooltipLinkList, { links: links ?? [] })
},
/* @__PURE__ */ React2.createElement(IconButton, { key: "open-source-file", title: "Open source file" }, /* @__PURE__ */ React2.createElement(JumpToIcon, null))
);
});
// src/manager.ts
addons.register(ADDON_ID, () => {
addons.add(TOOL_ID, {
type: types.TOOL,
title: "Source Link",
match: ({ viewMode }) => !!viewMode?.match(/^(story|docs)$/),
render: Tool
});
});