Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Save from controls #26827

Merged
merged 218 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
218 commits
Select commit Hold shift + click to select a range
0dbda33
Add SaveFromControls action bar
ghengeveld Apr 1, 2024
8cbd012
Add SaveFromControls bar to Controls panel
ghengeveld Apr 2, 2024
48c14e8
add events
ndelangen Apr 2, 2024
2a15187
move code for whatsnew to new file to not over-complicate the common …
ndelangen Apr 2, 2024
d5b573a
more scaffolding
ndelangen Apr 2, 2024
06d3ab8
Fix bar height
ghengeveld Apr 2, 2024
ebd07b4
Implement new story form
ghengeveld Apr 2, 2024
fd7fd6b
Fix bar position when there's extra vertical space
ghengeveld Apr 2, 2024
4b8f2dd
Add slideIn animation and improve short label display
ghengeveld Apr 2, 2024
ba76685
Avoid scrollbar while animating
ghengeveld Apr 2, 2024
69f7f26
Merge branch 'next' into ghengeveld/26661-project-sfc-create-a-toolba…
ghengeveld Apr 8, 2024
0346103
Merge branch 'next' into norbert/sfc-backend
ndelangen Apr 9, 2024
0414d50
Move Modal component from addon-onboarding to @storybook/components, …
ghengeveld Apr 9, 2024
68de9ef
No need for 'forwards' modifier
ghengeveld Apr 9, 2024
9fbf59b
find the AST node successfully
ndelangen Apr 10, 2024
9c8864e
cleanup
ndelangen Apr 10, 2024
7bd6ece
Fix error message in save-from-controls.ts
ndelangen Apr 10, 2024
154adca
Add notification icons for save story success and error
ndelangen Apr 10, 2024
22920d4
wip
ndelangen Apr 10, 2024
4515e78
add events
ndelangen Apr 2, 2024
3d23b0c
iteration on actually being able to add args
ndelangen Apr 10, 2024
3893f71
wip
ndelangen Apr 10, 2024
218b865
Hide info text on overflow
ghengeveld Apr 10, 2024
bcbca9b
Update story definition to use satisfies keyword
ghengeveld Apr 10, 2024
499864f
get the tests the way I want them, add support for existing prop over…
ndelangen Apr 10, 2024
5cadefc
add tests to verify the update works in a variation of scenarios
ndelangen Apr 11, 2024
6e9a493
improve diff
ndelangen Apr 11, 2024
8fe99de
support generating a new story cloned from an existing one
ndelangen Apr 11, 2024
163273b
dedupe
ndelangen Apr 11, 2024
e1c2bfe
Merge branch 'next' into norbert/sfc-backend
ndelangen Apr 11, 2024
9bbda19
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 11, 2024
4f42711
Merge branch 'next' into save-from-controls
ndelangen Apr 11, 2024
5aebec6
dedupe
ndelangen Apr 11, 2024
4e507b0
Merge branch 'save-from-controls' into norbert/sfc-backend
ndelangen Apr 11, 2024
e5f891d
Add initialArgs to useArgs array
ghengeveld Apr 11, 2024
f7c3d88
Only render SaveFromControls bar when there's changes made to the ini…
ghengeveld Apr 11, 2024
7c66b2e
Add Error subcomponent to Modal
ghengeveld Apr 11, 2024
6eafb5f
Add missing import
ghengeveld Apr 11, 2024
3f06428
Fix comparing objects with undefined values
ghengeveld Apr 11, 2024
d661faa
Highlight bar when it pops up
ghengeveld Apr 11, 2024
5de6a5d
Fix dark mode Modal design
ghengeveld Apr 12, 2024
12b2b55
Fix solid button focus style
ghengeveld Apr 12, 2024
162de70
Discard changes to code/frameworks/nextjs/package.json
ndelangen Apr 12, 2024
5a8fdd4
Discard changes to code/lib/core-server/src/utils/get-story-id.ts
ndelangen Apr 12, 2024
df1f0b9
Merge branch 'save-from-controls' into ghengeveld/26661-project-sfc-c…
ghengeveld Apr 12, 2024
af5877f
Merge pull request #26713 from storybookjs/norbert/sfc-backend
ndelangen Apr 12, 2024
6994504
Merge branch 'save-from-controls' into ghengeveld/26661-project-sfc-c…
ndelangen Apr 12, 2024
653c140
Docs: MDX minor fixes
jonniebigodes Apr 9, 2024
f5aaee2
CLI: Add --config-dir flag to add command
eric-blue Apr 8, 2024
c9f65de
add to cli-options docs
eric-blue Apr 9, 2024
9cfeca7
Add 'Create a new story' button to sidebar
valentinpalkovic Apr 10, 2024
951057f
Add stories to 'Create a new story' button
valentinpalkovic Apr 10, 2024
76539e9
Only show create new story button for react renderers
valentinpalkovic Apr 10, 2024
2e0e2ba
Disable TurboSnap
valentinpalkovic Apr 10, 2024
b43d2df
Apply mediumdark color only to IconButton
valentinpalkovic Apr 11, 2024
f762974
Move SaveFromControls from ArgsTable to Controls addon
ghengeveld Apr 12, 2024
869822f
Merge pull request #26710 from storybookjs/ghengeveld/26661-project-s…
ndelangen Apr 12, 2024
393e968
Merge branch 'next' into save-from-controls
ndelangen Apr 12, 2024
5531259
link the front-end and backend, remove dummy addon for testing
ndelangen Apr 12, 2024
463ea88
Fix cloning issue in duplicateStoryWithNewName function
ndelangen Apr 12, 2024
4091421
Restore original focus style for solid buttons
ghengeveld Apr 12, 2024
6de99c0
Adjust top position of code in search field
valentinpalkovic Apr 15, 2024
83a2c1a
Update exports
valentinpalkovic Apr 15, 2024
3b8be57
Merge branch 'valentin/create-story-file' into valentin/add-new-story…
valentinpalkovic Apr 15, 2024
6d8af9e
Merge branch 'next' into valentin/add-new-story-icon-to-sidebar
valentinpalkovic Apr 15, 2024
90a779e
Merge remote-tracking branch 'origin/next' into save-from-controls
valentinpalkovic Apr 15, 2024
6208d65
Merge branch 'save-from-controls' into valentin/add-new-story-icon-to…
valentinpalkovic Apr 15, 2024
c8f1002
Fix makeTitle fallback value in save-from-controls.ts
ndelangen Apr 15, 2024
8580a11
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 15, 2024
71ff85b
fix bad ts
ndelangen Apr 15, 2024
37dbae3
Update directory path for controls addon in .storybook/main.ts
ndelangen Apr 15, 2024
51e2294
Don't hide reset button in addon panel
ghengeveld Apr 15, 2024
46c8cc0
Only show SaveFromControls in dev mode
ghengeveld Apr 15, 2024
8d21fb2
Add typings.d.ts file for controls addon
ndelangen Apr 15, 2024
47807aa
Merge pull request #26793 from storybookjs/valentin/add-new-story-ico…
valentinpalkovic Apr 15, 2024
6778ee7
Allow numbers in story name
ghengeveld Apr 15, 2024
b42e1e2
Improve error handling and fix edge cases
ghengeveld Apr 15, 2024
9a8d83e
Fix Storybook structure
ghengeveld Apr 15, 2024
6651442
Merge branch 'next' into save-from-controls
ghengeveld Apr 15, 2024
3e7059b
Move error display to modal and refactor channel request/response mes…
ghengeveld Apr 16, 2024
b97b1e1
Fix type error
ghengeveld Apr 16, 2024
07e705b
Fix initialization of SaveFromControls in common-preset.ts
ndelangen Apr 16, 2024
d464012
Merge branch 'next' into save-from-controls
ndelangen Apr 16, 2024
69324cf
Also reset args when updating existing story
ghengeveld Apr 16, 2024
ebe1fe8
Rename SaveFromControls to SaveStory and add telemetry
ghengeveld Apr 16, 2024
cd3ccac
Improve error display in modal
ghengeveld Apr 16, 2024
0be9649
Merge branch 'next' into save-from-controls
ndelangen Apr 17, 2024
897becd
cleanup
ndelangen Apr 17, 2024
e6cba31
Add 'Create a new story' button to sidebar
valentinpalkovic Apr 10, 2024
0675bb4
Add stories to 'Create a new story' button
valentinpalkovic Apr 10, 2024
92ba347
Only show create new story button for react renderers
valentinpalkovic Apr 10, 2024
2e6862a
Disable TurboSnap
valentinpalkovic Apr 10, 2024
b86eeb0
Apply mediumdark color only to IconButton
valentinpalkovic Apr 11, 2024
f1c6ae9
Adjust top position of code in search field
valentinpalkovic Apr 15, 2024
0757f18
fix CI
valentinpalkovic Apr 15, 2024
5247e9a
Avoid nested button
ghengeveld Apr 17, 2024
88556bf
Remove unused ref
ghengeveld Apr 17, 2024
762a784
Wait for story to rerender after HMR before responding to save-story …
ghengeveld Apr 17, 2024
365a46d
No need to retry selectStory call anymore
ghengeveld Apr 17, 2024
3e66373
e2e tests for save-from-controls
ndelangen Apr 17, 2024
b5e083d
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 17, 2024
907fb3a
Fix import in ControlsPanel.tsx
ndelangen Apr 17, 2024
10b556c
Fix notification selector in save-from-controls.spec.ts
ndelangen Apr 17, 2024
80dd303
Fix notification selector in save-from-controls.spec.ts
ndelangen Apr 17, 2024
3816841
Fix submit button click in save-from-controls.spec.ts
ndelangen Apr 17, 2024
0222515
Fix input fill in save-from-controls.spec.ts
ndelangen Apr 17, 2024
3d10cd7
Refactor save-from-controls.spec.ts and ControlsPanel.tsx
ndelangen Apr 17, 2024
e89b365
Merge remote-tracking branch 'origin/save-from-controls' into valenti…
valentinpalkovic Apr 17, 2024
60fb5ba
Refactor save-from-controls.spec.ts and ControlsPanel.tsx
ndelangen Apr 17, 2024
139e8c9
Prevent color swatch from showing on top of SaveStory bar
ghengeveld Apr 17, 2024
8390ec5
Avoid transparent background to prevent controls from showing through
ghengeveld Apr 17, 2024
f34265b
Only send updated args when saving story, to prevent meta args from b…
ghengeveld Apr 17, 2024
fa3d3be
Fix typo in save-from-controls.spec.ts
ndelangen Apr 17, 2024
a07031a
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 17, 2024
6c08614
Refactor save-from-controls.spec.ts and ControlsPanel.tsx
ndelangen Apr 17, 2024
43be965
Use deepEqual rather than equality check
ghengeveld Apr 17, 2024
944ff5f
Introduce useDebounce hook
valentinpalkovic Apr 18, 2024
333329a
Export request and response interfaces for some channels
valentinpalkovic Apr 18, 2024
0c19a46
Change generic parser to use @babel/parser
valentinpalkovic Apr 18, 2024
6b81b0c
Use renderer package for typescript template
valentinpalkovic Apr 18, 2024
a449d8c
Return storyFilePath and exported name when creating a story in the r…
valentinpalkovic Apr 18, 2024
0f37b98
Enhance file search to exclude stories and to filter out unsupported …
valentinpalkovic Apr 18, 2024
b2ba678
Update error message
valentinpalkovic Apr 18, 2024
343081b
Implement argtypes channel in Preview
valentinpalkovic Apr 18, 2024
30eb66a
Implement FileSearchModal
valentinpalkovic Apr 18, 2024
fa49c94
Add unit tests to FileSearchModal.utils
valentinpalkovic Apr 18, 2024
19b6094
Rename helper function
valentinpalkovic Apr 18, 2024
aac320d
Remove unnecessary imports
valentinpalkovic Apr 18, 2024
f9d81b5
Animate modal opening
valentinpalkovic Apr 18, 2024
05ac43c
Try to absolute position modal
valentinpalkovic Apr 18, 2024
67c6785
Fix highlight color on dark theme
ghengeveld Apr 18, 2024
07c4fc3
Add types to stories
ghengeveld Apr 18, 2024
9077a3f
Remove stray story
ghengeveld Apr 18, 2024
b997f1e
Use getByLabel rather than getByText
ghengeveld Apr 18, 2024
f1808c8
Modal polishing
valentinpalkovic Apr 18, 2024
1d423a7
Merge remote-tracking branch 'origin/save-from-controls' into valenti…
valentinpalkovic Apr 18, 2024
03039b6
allow args to contain function etc.
ndelangen Apr 18, 2024
cfec679
Add stories and polishing
valentinpalkovic Apr 18, 2024
2a4c289
hopefully fix the e2e
ndelangen Apr 18, 2024
d904884
Cherry-pick 343081be5e47149b569a71b822fcc2ff3cc6850c
ghengeveld Apr 18, 2024
567f464
Move requestResponse util and types to core-events
ghengeveld Apr 18, 2024
8bfbaa8
Move requestResponse util to manager-api
ghengeveld Apr 18, 2024
963b8ef
Merge branch 'next' into save-from-controls
ghengeveld Apr 18, 2024
0df8706
Fix save-from-controls e2e test
ndelangen Apr 18, 2024
2cf3ea2
Polishing
valentinpalkovic Apr 19, 2024
e0af40b
Polish skeleton
valentinpalkovic Apr 19, 2024
c3f0177
Polish no results
valentinpalkovic Apr 19, 2024
d7039ef
Implement file formatting
ghengeveld Apr 19, 2024
fa134a9
Add FileSearchModal
valentinpalkovic Apr 19, 2024
ccf154f
pick https://github.com/storybookjs/storybook/pull/26882/files#diff-d…
ndelangen Apr 19, 2024
a8385b3
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 19, 2024
7c7134e
Merge branch 'next' into save-from-controls
ndelangen Apr 19, 2024
0596ca2
Separate CreateNewStoryFileModal from FileSearchModal
valentinpalkovic Apr 19, 2024
53ec3dd
dedupe
ndelangen Apr 19, 2024
551704b
Fix error handling for saveStory
ghengeveld Apr 19, 2024
20d0e7e
Fix babel traverse issue
ghengeveld Apr 19, 2024
5538e26
Wait a while
ghengeveld Apr 19, 2024
1c194d8
Cleanup
valentinpalkovic Apr 19, 2024
d16983e
Fix stories
ghengeveld Apr 19, 2024
9077f9e
Move icons to FileSearchList
valentinpalkovic Apr 19, 2024
f29966e
Fix test
valentinpalkovic Apr 19, 2024
1378113
Retrigger search after succefull story creation
valentinpalkovic Apr 19, 2024
2e5a457
Fix modal snapshot
ghengeveld Apr 19, 2024
56e6520
Merge branch 'next' into save-from-controls
ndelangen Apr 19, 2024
e7817b0
Merge remote-tracking branch 'origin/save-from-controls' into valenti…
valentinpalkovic Apr 19, 2024
a007256
WIP
valentinpalkovic Apr 19, 2024
3cf9846
Finalize Create New Story File feature
valentinpalkovic Apr 20, 2024
6a961aa
Fix tests
valentinpalkovic Apr 20, 2024
bda5336
Fix TypeScript errors
valentinpalkovic Apr 20, 2024
d310cb9
Fix tests
valentinpalkovic Apr 22, 2024
7702cb6
Fix Modals in onboarding and add stories to manager's Storybook
valentinpalkovic Apr 23, 2024
28f4bb7
Merge pull request #26924 from storybookjs/valentin/fix-onboarding-modal
valentinpalkovic Apr 23, 2024
466a774
Upgrade babel dependencies
valentinpalkovic Apr 23, 2024
9fc3a0f
Update kitchen-sink lock files
valentinpalkovic Apr 23, 2024
3625c2e
Merge remote-tracking branch 'origin/next' into valentin/add-file-picker
valentinpalkovic Apr 25, 2024
7c89f0f
Update snapshot
valentinpalkovic Apr 25, 2024
397ab28
Fix play function
valentinpalkovic Apr 25, 2024
bb832e5
Merge pull request #26875 from storybookjs/valentin/add-file-picker
valentinpalkovic Apr 25, 2024
e9f4264
Merge branch 'next' into save-from-controls
ghengeveld Apr 28, 2024
5ccf741
dedupe
ndelangen Apr 29, 2024
1b1af0b
throw error when asked to convert CSF2
ndelangen Apr 29, 2024
8841ef7
only explicitly support empty function when save-from-controls is used
ndelangen Apr 29, 2024
b336534
improve mobile layout
ndelangen Apr 29, 2024
2a46827
Add arrow navigation to the Save From Controls Modal search results
valentinpalkovic Apr 29, 2024
88fdc09
Enhance error messaging for the user when updating CSF2 stories
valentinpalkovic Apr 29, 2024
f163e6f
Maintain cursor position in FileSearchModal input field while typing
valentinpalkovic Apr 29, 2024
d9851f7
Improve required args extraction by taking account controls type
valentinpalkovic Apr 29, 2024
7cb3ecf
Enhance extractSeededRequiredArgs function
valentinpalkovic Apr 29, 2024
bc129cb
Fix stringification of function for saving a new story
valentinpalkovic Apr 29, 2024
eeabdb6
redesign error screen
ndelangen Apr 29, 2024
f1616eb
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 29, 2024
da73dd6
Merge branch 'next' into save-from-controls
ndelangen Apr 29, 2024
13a9f37
fix
ndelangen Apr 29, 2024
d510c20
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 29, 2024
15901b9
thrown when missing
ndelangen Apr 29, 2024
41f8819
fix chromatic
ndelangen Apr 29, 2024
77595aa
Merge branch 'next' into save-from-controls
ndelangen Apr 29, 2024
9bd0766
make stacktrace deterministic
ndelangen Apr 30, 2024
5e63c2f
Fix issue with getProjectRoot function in paths.ts when user does not…
valentinpalkovic Apr 30, 2024
3844765
Fix issues with disabled checkboxes and radios
valentinpalkovic Apr 30, 2024
313c50d
Improve required args extraction by considering enums
valentinpalkovic Apr 30, 2024
9b836fe
Fix paths util
valentinpalkovic Apr 30, 2024
abf5d99
Fix existing story evaluation
valentinpalkovic Apr 30, 2024
b1063c8
Enhance handleFileSearch
valentinpalkovic Apr 30, 2024
aee76c7
fix layout of error screen in preview
ndelangen Apr 30, 2024
5b24137
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen Apr 30, 2024
8c1740f
When creating a story, navigate to it if it exists
valentinpalkovic Apr 30, 2024
50fbf50
Fix tests
valentinpalkovic Apr 30, 2024
ad1b0b0
Implement telemetry for create-new-story-file flow
valentinpalkovic Apr 30, 2024
1298227
Fix sorting for file search
valentinpalkovic Apr 30, 2024
7664e2c
Merge branch 'next' into save-from-controls
ndelangen Apr 30, 2024
f0d81b1
change placeholder, to indicate you can use globs
ndelangen Apr 30, 2024
d1dbc73
improve UI in dark mode
ndelangen Apr 30, 2024
9b2b60f
Fix selection of story if it exists
valentinpalkovic Apr 30, 2024
9386252
disable chromatic for failing interaction test story
ndelangen May 1, 2024
dbb3718
Merge branch 'save-from-controls' of https://github.com/storybookjs/s…
ndelangen May 1, 2024
85ec7ff
Merge branch 'next' into save-from-controls
ndelangen May 1, 2024
5aa7cfc
Merge branch 'next' into save-from-controls
ndelangen May 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions code/addons/controls/package.json
Expand Up @@ -52,13 +52,15 @@
},
"dependencies": {
"@storybook/blocks": "workspace:*",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"@storybook/client-logger": "workspace:*",
"@storybook/components": "workspace:*",
"@storybook/core-common": "workspace:*",
"@storybook/icons": "^1.2.5",
"@storybook/manager-api": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/preview-api": "workspace:*",
Expand Down
67 changes: 50 additions & 17 deletions code/addons/controls/src/ControlsPanel.tsx
@@ -1,5 +1,6 @@
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import { dequal as deepEqual } from 'dequal';
import React, { useEffect, useMemo, useState } from 'react';
import { global } from '@storybook/global';
import {
useArgs,
useGlobals,
Expand All @@ -8,19 +9,41 @@ import {
useStorybookState,
} from '@storybook/manager-api';
import { PureArgsTable as ArgsTable, type PresetColor, type SortType } from '@storybook/blocks';

import { styled } from '@storybook/theming';
import type { ArgTypes } from '@storybook/types';

import { PARAM_KEY } from './constants';
import { SaveStory } from './SaveStory';

// Remove undefined values (top-level only)
const clean = (obj: { [key: string]: any }) =>
Object.entries(obj).reduce(
(acc, [key, value]) => (value !== undefined ? Object.assign(acc, { [key]: value }) : acc),
{} as typeof obj
);

const AddonWrapper = styled.div({
display: 'grid',
gridTemplateRows: '1fr 39px',
height: '100%',
maxHeight: '100vh',
overflowY: 'auto',
});

interface ControlsParameters {
sort?: SortType;
expanded?: boolean;
presetColors?: PresetColor[];
}

export const ControlsPanel: FC = () => {
interface ControlsPanelProps {
saveStory: () => Promise<unknown>;
createStory: (storyName: string) => Promise<unknown>;
}

export const ControlsPanel = ({ saveStory, createStory }: ControlsPanelProps) => {
const [isLoading, setIsLoading] = useState(true);
const [args, updateArgs, resetArgs] = useArgs();
const [args, updateArgs, resetArgs, initialArgs] = useArgs();
const [globals] = useGlobals();
const rows = useArgTypes();
const { expanded, sort, presetColors } = useParameter<ControlsParameters>(PARAM_KEY, {});
Expand All @@ -42,18 +65,28 @@ export const ControlsPanel: FC = () => {
return acc;
}, {} as ArgTypes);

const hasUpdatedArgs = useMemo(
() => !!args && !!initialArgs && !deepEqual(clean(args), clean(initialArgs)),
[args, initialArgs]
);

return (
<ArgsTable
key={path} // resets state when switching stories
compact={!expanded && hasControls}
rows={withPresetColors}
args={args}
globals={globals}
updateArgs={updateArgs}
resetArgs={resetArgs}
inAddonPanel
sort={sort}
isLoading={isLoading}
/>
<AddonWrapper>
<ArgsTable
key={path} // resets state when switching stories
compact={!expanded && hasControls}
rows={withPresetColors}
args={args}
globals={globals}
updateArgs={updateArgs}
resetArgs={resetArgs}
inAddonPanel
sort={sort}
isLoading={isLoading}
/>
{hasControls && hasUpdatedArgs && global.CONFIG_TYPE === 'DEVELOPMENT' && (
<SaveStory {...{ resetArgs, saveStory, createStory }} />
)}
</AddonWrapper>
);
};
59 changes: 59 additions & 0 deletions code/addons/controls/src/SaveStory.stories.tsx
@@ -0,0 +1,59 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';

import { SaveStory } from './SaveStory';
import { expect, fireEvent, fn, userEvent, within } from '@storybook/test';

const meta = {
component: SaveStory,
args: {
saveStory: fn((...args) => Promise.resolve(action('saveStory')(...args))),
createStory: fn((...args) => Promise.resolve(action('createStory')(...args))),
resetArgs: fn(action('resetArgs')),
},
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<div style={{ minHeight: '100vh' }}>
<Story />
</div>
),
],
} satisfies Meta<typeof SaveStory>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Creating = {
play: async ({ canvasElement }) => {
const createButton = await within(canvasElement).findByRole('button', { name: /Create/i });
await fireEvent.click(createButton);
await new Promise((resolve) => setTimeout(resolve, 300));
},
} satisfies Story;

export const Created: Story = {
play: async (context) => {
await Creating.play(context);

const dialog = await within(document.body).findByRole('dialog');
const input = await within(dialog).findByRole('textbox');
await userEvent.type(input, 'MyNewStory');

fireEvent.submit(dialog.getElementsByTagName('form')[0]);
await expect(context.args.createStory).toHaveBeenCalledWith('MyNewStory');
},
};

export const CreatingFailed: Story = {
args: {
// eslint-disable-next-line local-rules/no-uncategorized-errors
createStory: fn((...args) => Promise.reject<any>(new Error('Story already exists.'))),
},
play: Created.play,
};
219 changes: 219 additions & 0 deletions code/addons/controls/src/SaveStory.tsx
@@ -0,0 +1,219 @@
import {
Bar as BaseBar,
Button,
Form,
IconButton,
Modal,
TooltipNote,
WithTooltip,
} from '@storybook/components';
import { AddIcon, CheckIcon, UndoIcon } from '@storybook/icons';
import { keyframes, styled } from '@storybook/theming';
import React from 'react';

const slideIn = keyframes({
from: { transform: 'translateY(40px)' },
to: { transform: 'translateY(0)' },
});

const highlight = keyframes({
from: { background: 'var(--highlight-bg-color)' },
to: {},
});

const Container = styled.div({
containerType: 'size',
position: 'sticky',
bottom: 0,
height: 39,
overflow: 'hidden',
zIndex: 1,
});

const Bar = styled(BaseBar)(({ theme }) => ({
'--highlight-bg-color': theme.base === 'dark' ? '#153B5B' : '#E0F0FF',
display: 'flex',
flexDirection: 'row-reverse', // hide Info rather than Actions on overflow
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 6,
padding: '6px 10px',
animation: `${slideIn} 300ms, ${highlight} 2s`,
background: theme.background.bar,
borderTop: `1px solid ${theme.appBorderColor}`,
fontSize: theme.typography.size.s2,

'@container (max-width: 799px)': {
flexDirection: 'row',
justifyContent: 'flex-end',
},
}));

const Info = styled.div({
display: 'flex',
flex: '99 0 auto',
alignItems: 'center',
marginLeft: 10,
gap: 6,
});

const Actions = styled.div(({ theme }) => ({
display: 'flex',
flex: '1 0 0',
alignItems: 'center',
gap: 2,
color: theme.color.mediumdark,
fontSize: theme.typography.size.s2,
}));

const Label = styled.div({
'@container (max-width: 799px)': {
lineHeight: 0,
textIndent: '-9999px',
'&::after': {
content: 'attr(data-short-label)',
display: 'block',
lineHeight: 'initial',
textIndent: '0',
},
},
});

const ModalInput = styled(Form.Input)(({ theme }) => ({
'::placeholder': {
color: theme.color.mediumdark,
},
'&:invalid:not(:placeholder-shown)': {
boxShadow: `${theme.color.negative} 0 0 0 1px inset`,
},
}));

type SaveStoryProps = {
saveStory: () => Promise<unknown>;
createStory: (storyName: string) => Promise<unknown>;
resetArgs: () => void;
};

export const SaveStory = ({ saveStory, createStory, resetArgs }: SaveStoryProps) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const [saving, setSaving] = React.useState(false);
const [creating, setCreating] = React.useState(false);
const [storyName, setStoryName] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState(null);

const onSaveStory = async () => {
if (saving) return;
setSaving(true);
await saveStory().catch(() => {});
setSaving(false);
};

const onShowForm = () => {
setCreating(true);
setStoryName('');
setTimeout(() => inputRef.current?.focus(), 0);
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
.replace(/^[^a-z]/i, '')
.replace(/[^a-z0-9-_ ]/gi, '')
.replaceAll(/([-_ ]+[a-z0-9])/gi, (match) => match.toUpperCase().replace(/[-_ ]/g, ''));
setStoryName(value.charAt(0).toUpperCase() + value.slice(1));
};
const onSubmitForm = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (saving) return;
try {
setErrorMessage(null);
setSaving(true);
await createStory(storyName.replace(/^[^a-z]/i, '').replaceAll(/[^a-z0-9]/gi, ''));
setCreating(false);
setSaving(false);
} catch (e: any) {
setErrorMessage(e.message);
setSaving(false);
}
};

return (
<Container>
<Bar>
<Actions>
<WithTooltip
as="div"
hasChrome={false}
trigger="hover"
tooltip={<TooltipNote note="Save changes to story" />}
>
<IconButton aria-label="Save changes to story" disabled={saving} onClick={onSaveStory}>
<CheckIcon />
<Label data-short-label="Save">Update story</Label>
</IconButton>
</WithTooltip>

<WithTooltip
as="div"
hasChrome={false}
trigger="hover"
tooltip={<TooltipNote note="Create new story with these settings" />}
>
<IconButton aria-label="Create new story with these settings" onClick={onShowForm}>
<AddIcon />
<Label data-short-label="New">Create new story</Label>
</IconButton>
</WithTooltip>

<WithTooltip
as="div"
hasChrome={false}
trigger="hover"
tooltip={<TooltipNote note="Reset changes" />}
>
<IconButton aria-label="Reset changes" onClick={() => resetArgs()}>
<UndoIcon />
<span>Reset</span>
</IconButton>
</WithTooltip>
</Actions>

<Info>
<Label data-short-label="Unsaved changes">
You modified this story. Do you want to save your changes?
</Label>
</Info>

<Modal width={350} open={creating} onOpenChange={setCreating}>
<Form onSubmit={onSubmitForm}>
<Modal.Content>
<Modal.Header>
<Modal.Title>Create new story</Modal.Title>
<Modal.Description>
This will add a new story to your existing stories file.
</Modal.Description>
</Modal.Header>
<ModalInput
onChange={onChange}
placeholder="Story export name"
readOnly={saving}
ref={inputRef}
value={storyName}
/>
<Modal.Actions>
<Button disabled={saving || !storyName} size="medium" type="submit" variant="solid">
Create
</Button>
<Modal.Dialog.Close asChild>
<Button disabled={saving} size="medium" type="reset">
Cancel
</Button>
</Modal.Dialog.Close>
</Modal.Actions>
</Modal.Content>
</Form>
{errorMessage && <Modal.Error>{errorMessage}</Modal.Error>}
</Modal>
</Bar>
</Container>
);
};