Skip to content

Commit 03290ec

Browse files
feat(open stackblitz): API Key modal (#7365)
* feat(OptionsTile): open in stackblitz * feat(option tile): refactor previwer * feat(option tile): open in stackblitz * feat(open in stackblitz): refactor previewer * fix(test): test * fix(open in stackblitz): refactor code previwer * fix(eslint): lint staged * chore(saving): open in stackblitz * feat(open in stackblitz): refactor previwer * feat(open in stackblitz): modals * fix(modal): components passed as props is added to custom impports * fix(Previwer): conditionally add args to the app * feat(Modals): add storybook styles to stackblitz * chore(examples): add storybook style to examples * chore(examples): move constants inside templates * chore(examles): add prefix to stackblitz * fix(modals): stackblitzPrefillConfig args to single object
1 parent 06cb10f commit 03290ec

File tree

8 files changed

+253
-33
lines changed

8 files changed

+253
-33
lines changed

packages/ibm-products/previewer/codePreviewer.tsx

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,34 @@ interface ComponentSources {
2020
unknown: string[];
2121
}
2222

23-
export const stackblitzPrefillConfig = async (
24-
code: any,
25-
// components: Array<string>, // Add all required components to be imported from @carbon/react
26-
// icons: Array<string> // Add all required icons to be imported from @carbon/icons-react
27-
customImport: string
28-
) => {
29-
const { args } = code;
23+
interface previewerObject {
24+
story: any;
25+
customImports: string[];
26+
customFunctionDefs: string[];
27+
styles: string;
28+
}
29+
export const stackblitzPrefillConfig = async ({
30+
story,
31+
customImports = [],
32+
customFunctionDefs = [],
33+
styles,
34+
}: previewerObject) => {
35+
const { args } = story;
3036
const productComponents = await import('../src/index');
3137
componentNames = Object.keys(productComponents);
3238
const storyCode = filterStoryCode(
33-
code.parameters.docs.source.originalSource,
39+
story.parameters.docs.source.originalSource,
3440
args
3541
);
36-
const app = appGenerator(storyCode, customImport, args);
37-
42+
const app = appGenerator(storyCode, customImports, customFunctionDefs, args);
43+
let styleImport = style;
44+
if (styles) {
45+
// This regex matches multi-line comments that start with /* and end with */
46+
// It specifically looks for comments containing common license keywords
47+
const licenseCommentRegex =
48+
/\/\*\*?\s*\n?(?:\s*\*[^\n]*\n)*\s*\*?\s*(?:copyright|license|licensed|apache|mit|ibm corp|found in the|root directory)[^*]*\*\//gi;
49+
styleImport += styles.replace(licenseCommentRegex, '');
50+
}
3851
const stackblitzFileConfig: Project = {
3952
title: 'Carbon demo',
4053
description:
@@ -46,7 +59,7 @@ export const stackblitzPrefillConfig = async (
4659
'vite.config.js': viteConfig,
4760
'src/main.jsx': main,
4861
'src/App.jsx': app,
49-
'src/index.scss': style,
62+
'src/index.scss': styleImport,
5063
},
5164
};
5265

@@ -58,9 +71,17 @@ export const stackblitzPrefillConfig = async (
5871

5972
const filterStoryCode = (storyCode, args) => {
6073
let storyCodeUpdated = storyCode
61-
.replace(/^\s*args\s*=>\s*{\s*|}\s*;?\s*$/g, '')
74+
// Remove arrow functions
75+
.replace(/^\s*(\([^)]*\)|[\w]+)\s*=>\s*{\s*|}\s*;?\s*$/g, '')
76+
// Remove empty arrow wrappers
6277
.replace(/^\s*\(\)\s*=>\s*{/g, '')
78+
// Replace `args =>` with `return`
6379
.replace(/^\s*args\s*=>/g, 'return')
80+
// Remove ALL action() calls (including template literals and invocations)
81+
.replace(/action\(([^)]*)\)(\(\))?/g, '')
82+
// Replace ONLY `context.viewMode !== 'docs'` with `false` (your specific case)
83+
.replace(/context\.viewMode\s*!==\s*'docs'/g, 'false')
84+
// Remove quotes/action handlers (unchanged)
6485
.replace(/^"|"$/g, '')
6586
.replace(/onChange=\{(args\.onChange|action\('onChange'\))\}\s*/g, '')
6687
.replace(/onClick=\{(args\.onClick|action\('onClick'\))\}\s*/g, '');
@@ -94,11 +115,15 @@ const filterStoryCode = (storyCode, args) => {
94115
storyCodeUpdated = storyCodeUpdated.replace(regex, valueStr);
95116
}
96117
});
97-
storyCodeUpdated = storyCodeUpdated.replace(/`([^`]+)`/g, '"$1"');
98118
return storyCodeUpdated;
99119
};
100120

101-
const appGenerator = (storyCode: string, customImport: string, args: any) => {
121+
const appGenerator = (
122+
storyCode: string,
123+
customImports: Array<string>,
124+
customFunctionDefs: Array<string>,
125+
args: any
126+
) => {
102127
const {
103128
carbon: matchedCarbonComponents,
104129
ibmProducts: matchedComponents,
@@ -108,17 +133,24 @@ const appGenerator = (storyCode: string, customImport: string, args: any) => {
108133
if (unknownComponents.length > 0) {
109134
storyCode = removeUnknownComponents(storyCode, unknownComponents);
110135
}
136+
const foundHooks = detectReactHooks(storyCode);
137+
const hooksString = foundHooks.join(', ');
138+
const regex = /(\.\.\.\s*args)|(\{\s*[^}]*\.\.\.[^}]*\}\s*=\s*args)/;
139+
const hasArgs = regex.test(storyCode);
111140
// Generate App.jsx code
112141
const formattedArgs = `const args = ${JSON.stringify(args, null, 2)};`;
113142
const app = `
114-
import React, { useState, useEffect } from 'react';
115-
${customImport ? customImport : ''}
143+
import React ${hooksString != '' ? `, { ${hooksString} }` : ''} from 'react';
144+
${customImports?.length > 0 ? customImports?.map((customImport) => customImport) : ''}
116145
${matchedComponents.length > 0 ? `import { ${matchedComponents.join(', ')} } from "@carbon/ibm-products";` : ''}
117146
${matchedCarbonComponents.length > 0 ? `import { ${matchedCarbonComponents.join(', ')} } from "@carbon/react";` : ''}
118147
${matchedIcons.length > 0 ? `import { ${matchedIcons.join(', ')} } from "@carbon/icons-react";` : ''}
119-
const storyClass = 'example'
120148
export default function App() {
121-
${formattedArgs}
149+
${hasArgs ? formattedArgs : ''}
150+
const pkg = {
151+
prefix: 'c4p'
152+
}
153+
${customFunctionDefs?.map((customFunction) => customFunction)}
122154
${storyCode}
123155
}
124156
`;
@@ -167,3 +199,22 @@ const removeUnknownComponents = (storyCode, unknownComponents) => {
167199
cleanedCode = cleanedCode.replace(closingTagPattern, '');
168200
return cleanedCode;
169201
};
202+
203+
function detectReactHooks(sanitizedCode: string): string[] {
204+
const hooksToCheck = [
205+
'useState',
206+
'useEffect',
207+
'useContext',
208+
'useRef',
209+
] as const;
210+
const foundHooks: string[] = [];
211+
212+
hooksToCheck.forEach((hook) => {
213+
const regex = new RegExp(`\\b${hook}\\s*\\(`);
214+
if (regex.test(sanitizedCode)) {
215+
foundHooks.push(hook);
216+
}
217+
});
218+
219+
return foundHooks;
220+
}

packages/ibm-products/previewer/configFiles.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ root.render(
7272
`;
7373

7474
export const style: string = `
75+
/**
76+
* Copyright IBM Corp. 2024
77+
*
78+
* This source code is licensed under the Apache-2.0 license found in the
79+
* LICENSE file in the root directory of this source tree.
80+
*/
81+
7582
@use '@carbon/styles';
7683
@use '@carbon/ibm-products/css/index';
7784
`;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { Story, Controls, Source, Canvas } from '@storybook/addon-docs';
2+
import { APIKeyModal } from '.';
3+
import * as stories from './APIKeyModal.stories';
4+
import { stackblitzPrefillConfig } from '../../../previewer/codePreviewer';
5+
6+
export const customFunctionArr = [
7+
`function wait(ms) {
8+
return new Promise((resolve) => setTimeout(resolve, ms));
9+
}`,
10+
];
11+
export const customImportsArr = [
12+
`import {InlineLoading} from '@carbon/react';`,
13+
];
14+
15+
# APIKeyModal
16+
17+
## Table of Contents
18+
19+
- [Overview](#overview)
20+
- [Component API](#component-api)
21+
22+
## Overview
23+
[API key modal guidelines](https://pages.github.ibm.com/carbon/ibm-products/components/api-key-modal/usage/)
24+
(IBM internal)
25+
26+
## Generate
27+
<Canvas
28+
of={stories.Generate}
29+
additionalActions={[
30+
{
31+
title: 'Open in Stackblitz',
32+
onClick: () => stackblitzPrefillConfig({
33+
'story': stories.Generate,
34+
'customFunctionDefs': customFunctionArr,
35+
'styles': stories.default.parameters.styles
36+
}),
37+
},
38+
]}>
39+
</Canvas>
40+
41+
## Generate With Error
42+
43+
<Canvas
44+
of={stories.GenerateWithError}
45+
additionalActions={[
46+
{
47+
title: 'Open in Stackblitz',
48+
onClick: () => stackblitzPrefillConfig({
49+
'story': stories.GenerateWithError,
50+
'customFunctionDefs': customFunctionArr,
51+
'styles': stories.default.parameters.styles
52+
}),
53+
},
54+
]}>
55+
</Canvas>
56+
57+
## Instant Generate
58+
59+
<Canvas
60+
of={stories.InstantGenerate}
61+
additionalActions={[
62+
{
63+
title: 'Open in Stackblitz',
64+
onClick: () => stackblitzPrefillConfig({
65+
'story': stories.InstantGenerate,
66+
'customImports': customImportsArr,
67+
'customFunctionDefs': customFunctionArr,
68+
'styles': stories.default.parameters.styles
69+
}),
70+
},
71+
]}>
72+
</Canvas>
73+
74+
## Custom Generate
75+
76+
<Canvas
77+
of={stories.CustomGenerate}
78+
additionalActions={[
79+
{
80+
title: 'Open in Stackblitz',
81+
onClick: () => stackblitzPrefillConfig({
82+
'story': stories.CustomGenerate,
83+
'customFunctionDefs': customFunctionArr,
84+
'styles': stories.default.parameters.styles
85+
}),
86+
},
87+
]}>
88+
</Canvas>
89+
90+
## Edit
91+
92+
<Canvas
93+
of={stories.Edit}
94+
additionalActions={[
95+
{
96+
title: 'Open in Stackblitz',
97+
onClick: () => stackblitzPrefillConfig({
98+
'story': stories.Edit,
99+
'customFunctionDefs': customFunctionArr,
100+
'styles': stories.default.parameters.styles
101+
}),
102+
},
103+
]}>
104+
</Canvas>
105+
106+
## Edit With Error
107+
108+
<Canvas
109+
of={stories.EditWithError}
110+
additionalActions={[
111+
{
112+
title: 'Open in Stackblitz',
113+
onClick: () => stackblitzPrefillConfig({
114+
'story': stories.EditWithError,
115+
'customFunctionDefs': customFunctionArr,
116+
'styles': stories.default.parameters.styles
117+
}),
118+
},
119+
]}>
120+
</Canvas>
121+
122+
## Custom Edit
123+
124+
<Canvas
125+
of={stories.CustomEdit}
126+
additionalActions={[
127+
{
128+
title: 'Open in Stackblitz',
129+
onClick: () => stackblitzPrefillConfig({
130+
'story': stories.CustomEdit,
131+
'customFunctionDefs': customFunctionArr,
132+
'styles': stories.default.parameters.styles
133+
}),
134+
},
135+
]}>
136+
</Canvas>
137+
138+
## Component API
139+
140+
<Controls />

packages/ibm-products/src/components/APIKeyModal/APIKeyModal.stories.jsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ import { pkg } from '../../settings';
2121
import { APIKeyModal } from '.';
2222
import wait from '../../global/js/utils/wait';
2323
import styles from './_storybook-styles.scss?inline'; // import index in case more files are added later.
24-
import DocsPage from './APIKeyModal.docs-page';
24+
import mdx from './APIKeyModal.mdx';
2525

2626
export default {
2727
title: 'IBM Products/Components/Generating an API key/APIKeyModal',
2828
component: APIKeyModal,
2929
tags: ['autodocs'],
3030
parameters: {
3131
styles,
32-
docs: { page: DocsPage },
32+
docs: {
33+
page: mdx,
34+
},
3335
},
3436
argTypes: {
3537
generateSuccessBody: {
@@ -80,13 +82,11 @@ const defaultProps = {
8082
modalLabel: 'An example of Generate API key',
8183
};
8284

83-
const blockClass = `${pkg.prefix}--apikey-modal`;
84-
8585
const InstantTemplate = (args, context) => {
8686
const [open, setOpen] = useState(context.viewMode !== 'docs');
8787
const [loading, setLoading] = useState(false);
8888
const buttonRef = useRef(undefined);
89-
89+
const blockClass = `${pkg.prefix}--apikey-modal`;
9090
const generateKey = async () => {
9191
setLoading(true);
9292
await wait(1000);
@@ -126,6 +126,7 @@ const TemplateWithState = (args, context) => {
126126
const [loading, setLoading] = useState(false);
127127
const [fetchError, setFetchError] = useState(false);
128128
const buttonRef = useRef(undefined);
129+
const blockClass = `${pkg.prefix}--apikey-modal`;
129130

130131
// eslint-disable-next-line
131132
const submitHandler = async (apiKeyName) => {
@@ -179,6 +180,7 @@ const MultiStepTemplate = (args, context) => {
179180
const [apiKey, setApiKey] = useState('');
180181
const [loading, setLoading] = useState(false);
181182
const buttonRef = useRef(undefined);
183+
const blockClass = `${pkg.prefix}--apikey-modal`;
182184

183185
// multi step options
184186
const [name, setName] = useState(savedName);
@@ -339,6 +341,7 @@ const EditTemplate = (args, context) => {
339341
const [fetchError, setFetchError] = useState(false);
340342
const [fetchSuccess, setFetchSuccess] = useState(false);
341343
const buttonRef = useRef(undefined);
344+
const blockClass = `${pkg.prefix}--apikey-modal`;
342345

343346
const submitHandler = async () => {
344347
action(`submitted ${apiKeyName}`)();

packages/ibm-products/src/components/FullPageError/FullPageError.mdx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ errors, and not caused by server connectivity issues.
3333
additionalActions={[
3434
{
3535
title: 'Open in Stackblitz',
36-
onClick: () => stackblitzPrefillConfig(stories.fullPageError),
36+
onClick: () => stackblitzPrefillConfig({
37+
'story': stories.fullPageError,
38+
'styles': stories.default.parameters.styles
39+
}),
3740
},
3841
]}
3942
/>
@@ -48,7 +51,10 @@ access the page or data, or when login is required.
4851
additionalActions={[
4952
{
5053
title: 'Open in Stackblitz',
51-
onClick: () => stackblitzPrefillConfig(stories.fullPageError403),
54+
onClick: () => stackblitzPrefillConfig({
55+
'story': stories.fullPageError403,
56+
'styles': stories.default.parameters.styles
57+
}),
5258
},
5359
]}
5460
/>
@@ -64,7 +70,10 @@ broken links.
6470
additionalActions={[
6571
{
6672
title: 'Open in Stackblitz',
67-
onClick: () => stackblitzPrefillConfig(stories.fullPageError404),
73+
onClick: () => stackblitzPrefillConfig({
74+
'story': stories.fullPageError404,
75+
'styles': stories.default.parameters.styles
76+
}),
6877
},
6978
]}
7079
/>

0 commit comments

Comments
 (0)