Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from 'storybook'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Storybook 10 export Meta and StoryObj types from the bare 'storybook' package, or only from framework packages like @storybook/react?

💡 Result:

In Storybook 10, the Meta and StoryObj types are exported from your specific framework package (e.g., @storybook/react, @storybook/vue3-vite, @storybook/tanstack-react), not from the bare storybook package [1][2][3]. Official documentation and migration guidance consistently instruct developers to import these utility types from the framework-specific package they are using [4][5][6]. This is necessary because these types are generics that require the framework-aware parameter types (such as React, Vue, or TanStack-specific annotations) to provide correct type safety, autocompletion, and inference for your component's props and arguments [4][5][3]. Using imports like import { Meta, StoryObj } from '@storybook/react' (or your respective framework package) ensures that Storybook can correctly map your component's metadata and args to the framework's internal type definitions [3][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files of interest:\n'
git ls-files | rg '(^|/)(package.json|tsconfig.*|\.storybook/|storybook|OnboardingNextSteps\.stories\.tsx)$'

printf '\nPackage manifests mentioning Storybook:\n'
rg -n '"storybook"|`@storybook`' package.json frontend/package.json .storybook package.json 2>/dev/null || true

printf '\nStorybook-related config files (first 200 lines each if present):\n'
for f in .storybook/main.* .storybook/preview.* frontend/.storybook/main.* frontend/.storybook/preview.*; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,200p' "$f"
  fi
done

printf '\nTarget file:\n'
sed -n '1,80p' frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx

Repository: Flagsmith/flagsmith

Length of output: 2002


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo 'package.json'
sed -n '1,220p' package.json 2>/dev/null || true

echo
echo 'frontend/package.json'
sed -n '1,220p' frontend/package.json 2>/dev/null || true

echo
echo 'Storybook config files'
find . -maxdepth 3 \( -path '*/.storybook/*' -o -name 'main.ts' -o -name 'main.js' -o -name 'preview.ts' -o -name 'preview.js' \) | sort | sed -n '1,50p'

echo
echo 'Search for storybook type imports'
rg -n "from 'storybook'|from \"storybook\"|from '`@storybook/`" frontend --glob '*.stories.*' --glob '*.ts' --glob '*.tsx'

Repository: Flagsmith/flagsmith

Length of output: 2002


🌐 Web query:

site:storybook.js.org/docs StoryObj import from 'storybook' Meta StoryObj framework package

💡 Result:

In Storybook, Meta and StoryObj are TypeScript utility types used to type your story files and ensure type safety [1][2]. You should import them from your specific framework's Storybook package, not from a generic 'storybook' package [3][4][5]. Replace 'your-framework' in the import statement with the specific Storybook framework package you are using (e.g., @storybook/react-vite, @storybook/nextjs, @storybook/vue3-vite) [6][1][2]. Example usage in a TypeScript story file: import type { Meta, StoryObj } from '@storybook/your-framework'; import { Button } from './Button'; const meta = { component: Button, } satisfies Meta; export default meta; type Story = StoryObj; export const Primary: Story = { args: { primary: true, }, }; Using these types allows Storybook to infer component metadata and argument types, providing autocompletion and error checking for your stories [1][2]. The satisfies operator is typically used with Meta to associate the component definition with the meta object, which then allows StoryObj to correctly infer the argument types from that meta definition [1][2].

Citations:


Import Meta/StoryObj from the configured Storybook framework package

storybook does not export these types; use the framework package for this project (for example @storybook/react or @storybook/react-vite) so this story type-checks.


import OnboardingNextSteps from 'components/pages/onboarding/OnboardingNextSteps'

const meta: Meta<typeof OnboardingNextSteps> = {
args: {
locked: false,
onSelect: () => {},
},
component: OnboardingNextSteps,
parameters: {
docs: {
description: {
component:
'The "Choose your next quest" section at the bottom of the onboarding flow: the three ways the demo flag can level up (gradual rollout, experiment, remote config), each linking to its real config. Locked and dimmed until the app connects.',
},
},
layout: 'padded',
},
title: 'Pages/Onboarding/OnboardingNextSteps',
}
export default meta

type Story = StoryObj<typeof OnboardingNextSteps>

export const Connected: Story = {}

export const Locked: Story = {
args: { locked: true },
}
27 changes: 27 additions & 0 deletions frontend/e2e/tests/onboarding-tests.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ test.describe('Onboarding', () => {
await expect(page.getByText('LISTENING')).toBeVisible();
await expect(page.getByText('Copy install command')).not.toContainText('✓');

// Before the first evaluation, the next-quest cards are locked.
await expect(
page.getByText('Unlocks after your first evaluation'),
).toBeVisible();

log('Copy snippets, checklist ticks');
await page.getByRole('button', { name: 'Copy install command' }).click();
await expect(page.getByText('Copy install command')).toContainText('✓');
Expand Down Expand Up @@ -101,5 +106,27 @@ test.describe('Onboarding', () => {
.getByText('Onboarding', { exact: true }),
).toBeVisible();
await visualSnapshot(page, 'onboarding-renamed', testInfo);

// The next-quest cards unlock once connected, each deep-linking to the
// flag's real config (nothing faked).
log('Next-quest cards link to the real config');
await expect(
page.getByRole('heading', { name: 'Choose your next quest' }),
).toBeVisible();

await page.getByRole('button', { name: /Gradual rollout/ }).click();
await expect(page).toHaveURL(
/\/features\?feature=\d+&tab=segment-overrides/,
);

await page.goto('/getting-started?connected');
await flowReady();
await page.getByRole('button', { name: /Remote config/ }).click();
await expect(page).toHaveURL(/\/features\?feature=\d+&tab=value/);

await page.goto('/getting-started?connected');
await flowReady();
await page.getByRole('button', { name: /Experiment/ }).click();
await expect(page).toHaveURL(/\/experiments$/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-width: 0;
}

&__icon {
Expand Down
22 changes: 18 additions & 4 deletions frontend/web/components/base/SelectableCard/SelectableCard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import React, { FC, ReactNode } from 'react'
import cn from 'classnames'
import BareButton from 'components/base/forms/BareButton'
import './SelectableCard.scss'

type BadgeVariant = 'primary' | 'secondary'

type SelectableCardProps = {
selected: boolean
// Selection state. Omit for a card that acts on click without a chosen state
// (e.g. a card that navigates).
selected?: boolean
onClick: () => void
icon?: ReactNode
title: string
description: string
badge?: { label: string; variant: BadgeVariant }
tags?: string[]
disabled?: boolean
className?: string
// Extra content below the description (e.g. an illustrative preview).
children?: ReactNode
}

const SelectableCard: FC<SelectableCardProps> = ({
badge,
children,
className,
description,
disabled,
icon,
Expand All @@ -27,9 +35,14 @@ const SelectableCard: FC<SelectableCardProps> = ({
}) => {
return (
<BareButton
className={`selectable-card${
selected ? ' selectable-card--selected' : ''
}${disabled ? ' selectable-card--disabled' : ''}`}
className={cn(
'selectable-card',
{
'selectable-card--disabled': disabled,
'selectable-card--selected': selected,
},
className,
)}
onClick={onClick}
disabled={disabled}
>
Expand All @@ -46,6 +59,7 @@ const SelectableCard: FC<SelectableCardProps> = ({
))}
</div>
)}
{children}
</div>
{badge && (
<div className='selectable-card__aside'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
font-size: 16px;
}

// Lock affordance beside the title while the toggle is waiting to connect.
&__hint {
font-size: 13px;
color: var(--color-text-tertiary);
}

&__table {
width: 100%;
max-width: 760px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tag as TTag } from 'common/types/responses'
import FeatureName from 'components/feature-summary/FeatureName'
import Tag from 'components/tags/Tag'
import Switch from 'components/Switch'
import Icon from 'components/icons/Icon'
import './OnboardingFlagsTable.scss'

export type OnboardingFlagsTableStatus = 'waiting' | 'connected'
Expand Down Expand Up @@ -41,12 +42,20 @@ const OnboardingFlagsTable: FC<OnboardingFlagsTableProps> = ({
className='onboarding-flags d-flex flex-column align-items-center'
aria-labelledby='onboarding-flags-title'
>
<h3
className='onboarding-flags__title m-0 fw-bold'
id='onboarding-flags-title'
>
Your flags
</h3>
<div className='onboarding-flags__heading d-flex align-items-center gap-2'>
<h3
className='onboarding-flags__title m-0 fw-bold'
id='onboarding-flags-title'
>
Your flags
</h3>
{waiting && (
<span className='onboarding-flags__hint d-flex align-items-center gap-1'>
<Icon name='lock' width={13} />
Flip it once your app connects
</span>
)}
</div>
Comment on lines +45 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the hint aligned with every disabled state.

The switch is disabled when togglesReady is false, but this hint only renders while waiting is true. If the app connects before the feature-state query resolves, users see a disabled toggle without an explanation. Use the same lock condition or provide a loading-specific message.

<div
className={classNames(
'onboarding-flags__table bg-surface-default rounded-xl',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import ThemeToggle from 'components/pages/onboarding/ThemeToggle'
import OnboardingConnectPanel from 'components/pages/onboarding/OnboardingConnectPanel'
import OnboardingTerminal from 'components/pages/onboarding/OnboardingTerminal'
import OnboardingFlagsTable from 'components/pages/onboarding/OnboardingFlagsTable'
import OnboardingNextSteps, {
OnboardingNextStep,
} from 'components/pages/onboarding/OnboardingNextSteps'
import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources'
import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename'
import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag'
Expand Down Expand Up @@ -64,6 +67,7 @@ const OnboardingFlow: FC = () => {
const [snippetCopied, setSnippetCopied] = useState(false)
const {
enabled: flagEnabled,
flagId,
isToggling,
ready: flagStateReady,
tags: flagTags,
Expand Down Expand Up @@ -117,6 +121,24 @@ const OnboardingFlow: FC = () => {
}
}

// Each next-step card deep-links to the flag's real config; nothing faked.
const goToNextStep = (step: OnboardingNextStep) => {
if (projectId === null) {
return
}
const base = `/project/${projectId}/environment/${environmentKey}`
if (step === 'experiment') {
history.push(`${base}/experiments`)
return
}
if (flagId === null) {
return
}
// Tab param is the slugified tab label (see TabMenu/Tabs urlParam).
const tab = step === 'rollout' ? 'segment-overrides' : 'value'
history.push(`${base}/features?feature=${flagId}&tab=${tab}`)
}
Comment on lines +124 to +140

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not unlock dependent cards before flagId is ready.

The handler returns without navigating when flagId === null, but Lines [203-205] make the cards interactive as soon as the connection is established. During the flag query, rollout and remote-config cards can therefore look clickable while doing nothing. Include flag readiness in the lock condition, or add per-step loading/disabled handling instead of silently returning.

Also applies to: 203-205


if (status === 'creating') {
return (
<div className='onboarding-flow mx-auto text-center'>
Expand Down Expand Up @@ -178,6 +200,10 @@ const OnboardingFlow: FC = () => {
togglingFlag={isToggling ? featureName : null}
togglesReady={flagStateReady}
/>
<OnboardingNextSteps
locked={connection !== 'connected'}
onSelect={goToNextStep}
/>
<div className='d-flex justify-content-end'>
<Button theme='text' onClick={skipToApp}>
<span className='d-inline-flex align-items-center gap-1'>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// "Choose your next quest" section. Layout comes from utilities in the markup;
// this file holds the type sizes, secondary text colour, the dimmed locked
// state, and the small per-card preview shapes (rollout track, A/B segmented,
// value pill). Secondary text isn't a utility: Bootstrap's .text-secondary
// (gold, !important) would win over our token utility, so it's set here.
.onboarding-next-steps {
&__title {
font-size: 16px;
}

&__subtitle {
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary);
}

// Locked affordance: intentionally faint (disabled look). The lock icon
// inherits this via currentColor.
&__lock {
font-size: 12px;
color: var(--color-text-tertiary);
}

&__caption {
font-size: 12px;
color: var(--color-text-secondary);
}

// Dimmed until the app connects (the design's 0.4 locked state). The `inert`
// attribute on the same element blocks pointer + keyboard interaction.
&__body--locked {
opacity: 0.4;
}

// Equal-width cards regardless of their copy length.
&__row > * {
flex: 1 1 0;
min-width: 0;
}

// Illustrative preview sits a little below the card's description.
&__preview {
margin-top: 6px;
}

// Rollout: a part-filled pill track.
&__track {
display: block;
width: 100%;
height: 8px;
border-radius: var(--radius-full);
background: var(--color-border-default);
}

&__track-fill {
display: block;
width: 25%;
height: 100%;
border-radius: var(--radius-full);
background: var(--color-surface-action);
}

// Experiment: an A/B segmented control.
&__segmented {
align-self: flex-start;
overflow: hidden;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);
}

&__segment {
padding: 5px 16px;
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-surface-default);

&--active {
color: var(--color-text-action);
background: var(--color-surface-action-subtle);
}
}

// Remote config: a value chip.
&__value-pill {
align-self: flex-start;
padding: 5px 10px;
border-radius: var(--radius-md);
font-size: 12px;
}
}
Loading
Loading