Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
97caa23
feat(design-system): add Chip component
talissoncosta Jun 17, 2026
7381361
fix(forms): GhostInput clipping/flicker, React 19 ref, own folder
talissoncosta Jun 17, 2026
79f018b
refactor(feature-name): extract shared sanitizeFeatureName util
talissoncosta Jun 17, 2026
06a3555
feat(services): add createProject and createEnvironment endpoints
talissoncosta Jun 17, 2026
baa5583
feat(onboarding): single-page header with editable resource chips
talissoncosta Jun 17, 2026
28b7de3
feat(onboarding): a11y OnboardingTabs (list, button, panel)
talissoncosta Jun 17, 2026
07d402b
feat(onboarding): connect panel (AI prompt + SDK radiogroup)
talissoncosta Jun 17, 2026
7faf921
feat(onboarding): bootstrap, gate and chromeless flow with skip
talissoncosta Jun 17, 2026
c22f931
test(onboarding): unit tests for the pure logic
talissoncosta Jun 17, 2026
6ae0074
docs: component-folder convention and onboarding stories
talissoncosta Jun 17, 2026
b9332c6
fix(onboarding): use currentColor for the copy icon, not hardcoded white
talissoncosta Jun 17, 2026
9f78cea
fix(onboarding): drop the redundant copy-icon fill
talissoncosta Jun 17, 2026
fdf0634
refactor(onboarding): name the code prop 'language', not 'hljs'
talissoncosta Jun 17, 2026
5906244
feat(onboarding): pin the SDK More toggle and open the tail in place
talissoncosta Jun 19, 2026
ec3cdec
style(onboarding): tier surfaces and elevate the connect panel
talissoncosta Jun 19, 2026
3d04510
feat(onboarding): inline editable values in the header sentence
talissoncosta Jun 19, 2026
914287f
feat(onboarding): light/dark theme toggle above the header
talissoncosta Jun 19, 2026
a71a806
style(onboarding): right-align the skip link via flex
talissoncosta Jun 19, 2026
406c0bb
style(onboarding): drop em dashes and update the skip CTA copy
talissoncosta Jun 19, 2026
798b232
style(onboarding): make the inline-edit underline obvious (purple)
talissoncosta Jun 19, 2026
eda6b09
fix(onboarding): look up SDK snippets by codeHelp key, not display label
talissoncosta Jun 19, 2026
7c3b019
fix(onboarding): reuse existing flag on revisit; extract bootstrapOnb…
talissoncosta Jun 19, 2026
9908775
feat(onboarding): accent the flag name and match its input to the cre…
talissoncosta Jun 19, 2026
ea9b312
feat(onboarding): toast and revert on org/project/flag rename
talissoncosta Jun 19, 2026
6a25877
style(onboarding): add minimal top padding to the flow
talissoncosta Jun 19, 2026
d9d7210
style(onboarding): trim redundant comments
talissoncosta Jun 19, 2026
b90829f
refactor(onboarding): name the bootstrap default strings as consts
talissoncosta Jun 19, 2026
678e6ee
test(e2e): skip legacy signup test when onboarding_quickstart_flow is on
talissoncosta Jun 22, 2026
9337457
fix(onboarding): resolve Next.js wire snippet
talissoncosta Jun 23, 2026
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
1 change: 1 addition & 0 deletions frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
5. **Linting**: ALWAYS run `npx eslint --fix <file>` on any files you modify
6. **Type Enums**: Extract inline union types to named types (e.g., `type Status = 'A' | 'B'` instead of inline)
7. **NO FETCH**: NEVER use `fetch()` directly - ALWAYS use RTK Query mutations/queries (inject endpoints into services in `common/services/`), see api-integration context
8. **Component structure**: Each new component lives in its own folder with an `index.ts` barrel - `ComponentName/ComponentName.tsx`, co-located `ComponentName.scss`, any sub-components, and an `index.ts` that re-exports the default (and public types). Import via the folder (`components/.../ComponentName`), never the inner file. Keep files focused (~100 lines as a target); split by concern, not to hit a number. Data tables/constant maps are exempt.

## Key Files
- Store: `common/store.ts`
Expand Down
12 changes: 12 additions & 0 deletions frontend/common/services/useEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export const environmentService = service
.enhanceEndpoints({ addTagTypes: ['Environment'] })
.injectEndpoints({
endpoints: (builder) => ({
createEnvironment: builder.mutation<
Res['environment'],
Req['createEnvironment']
>({
invalidatesTags: [{ id: 'LIST', type: 'Environment' }],
query: (body: Req['createEnvironment']) => ({
body,
method: 'POST',
url: `environments/`,
}),
}),
getEnvironment: builder.query<Res['environment'], Req['getEnvironment']>({
providesTags: (res) => [{ id: res?.id, type: 'Environment' }],
query: (query: Req['getEnvironment']) => ({
Expand Down Expand Up @@ -84,6 +95,7 @@ export async function updateEnvironment(
// END OF FUNCTION_EXPORTS

export const {
useCreateEnvironmentMutation,
useGetEnvironmentMetricsQuery,
useGetEnvironmentQuery,
useGetEnvironmentsQuery,
Expand Down
9 changes: 9 additions & 0 deletions frontend/common/services/useProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export const projectService = service
.enhanceEndpoints({ addTagTypes: ['Project'] })
.injectEndpoints({
endpoints: (builder) => ({
createProject: builder.mutation<Res['project'], Req['createProject']>({
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
query: (body: Req['createProject']) => ({
body,
method: 'POST',
url: `projects/`,
}),
}),
deleteProject: builder.mutation<void, Req['deleteProject']>({
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
query: ({ id }: Req['deleteProject']) => ({
Expand Down Expand Up @@ -99,6 +107,7 @@ export async function getProject(
// END OF FUNCTION_EXPORTS

export const {
useCreateProjectMutation,
useDeleteProjectMutation,
useGetProjectPermissionsQuery,
useGetProjectQuery,
Expand Down
2 changes: 2 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ export type Req = {
id: string
}
getProject: { id: number }
createProject: { name: string; organisation: number }
updateProject: { id: number; body: UpdateProjectBody }
deleteProject: { id: number }
migrateProject: { id: number }
Expand Down Expand Up @@ -722,6 +723,7 @@ export type Req = {
feature_id: number
group_ids: number[]
}
createEnvironment: { name: string; project: number }
updateEnvironment: { id: number; body: Environment }
createCloneIdentityFeatureStates: {
environment_id: string
Expand Down
21 changes: 21 additions & 0 deletions frontend/common/utils/__tests__/sanitizeFeatureName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { sanitizeFeatureName } from 'common/utils/sanitizeFeatureName'

describe('sanitizeFeatureName', () => {
it.each`
raw | caseSensitive | expected
${'my flag'} | ${false} | ${'my_flag'}
${'My Flag'} | ${false} | ${'My_Flag'}
${'My Flag'} | ${true} | ${'my_flag'}
${'show demo button'} | ${true} | ${'show_demo_button'}
${'already_ok'} | ${false} | ${'already_ok'}
${'UPPER'} | ${true} | ${'upper'}
${'UPPER'} | ${false} | ${'UPPER'}
${'a b'} | ${false} | ${'a__b'}
${''} | ${false} | ${''}
`(
'sanitizeFeatureName($raw, $caseSensitive) returns $expected',
({ caseSensitive, expected, raw }) => {
expect(sanitizeFeatureName(raw, caseSensitive)).toBe(expected)
},
)
})
13 changes: 13 additions & 0 deletions frontend/common/utils/sanitizeFeatureName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Normalise a feature/flag name the way the backend expects: spaces become
// underscores, and the name is lower-cased when the project enforces lower-case
// feature names (only_allow_lower_case_feature_names). The backend regex stays
// the final word on validity.
export const sanitizeFeatureName = (
raw: string,
caseSensitive: boolean,
): string => {
const next = raw.replace(/ /g, '_')
return caseSensitive ? next.toLowerCase() : next
}

export default sanitizeFeatureName
60 changes: 60 additions & 0 deletions frontend/documentation/components/Chip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import Chip from 'components/base/Chip'

const meta: Meta<typeof Chip> = {
args: { children: 'Production' },
component: Chip,
parameters: {
docs: {
description: {
component:
'Canonical token-based chip primitive: a small labelled pill token. Layout via Bootstrap utilities, colour/radius via token utilities, padding/sizes/border/truncation in SCSS. Leading/trailing icons go in as children. Selection lives in ToggleChip and count badges are a separate Badge concern. The legacy `.chip` (old SCSS vars + manual dark-mode block, ~35×) migrates onto this under #6606.',
},
},
layout: 'centered',
},
title: 'Components/Data Display/Chip',
}
export default meta

type Story = StoryObj<typeof Chip>

export const Neutral: Story = {}

export const Accent: Story = {
args: { children: '"hello"', variant: 'accent' },
}

export const Sizes: Story = {
render: () => (
<div className='d-flex align-items-center gap-2'>
<Chip size='default'>Default</Chip>
<Chip size='sm'>Small</Chip>
<Chip size='xs'>Extra small</Chip>
</div>
),
}

export const Removable: Story = {
args: { children: 'feature-flag', onRemove: () => undefined },
}

export const Truncated: Story = {
args: {
children: '{ "test": "testvalue-that-keeps-going-and-going" }',
truncate: true,
variant: 'accent',
},
}

export const Group: Story = {
render: () => (
<div className='d-flex flex-wrap gap-2'>
<Chip>Development</Chip>
<Chip variant='accent'>Staging</Chip>
<Chip onRemove={() => undefined}>Production</Chip>
</div>
),
}
16 changes: 14 additions & 2 deletions frontend/documentation/components/GhostInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import type { Meta, StoryObj } from 'storybook'

import GhostInput from 'components/base/forms/GhostInput'

const meta: Meta = {
const meta: Meta<typeof GhostInput> = {
component: GhostInput,
parameters: { layout: 'centered' },
title: 'Components/Forms/GhostInput',
}
export default meta

type Story = StoryObj
type Story = StoryObj<typeof GhostInput>

const Interactive = () => {
const [value, setValue] = useState('my-feature-flag')
Expand All @@ -31,3 +32,14 @@ export const Empty: Story = {
<GhostInput value='' onChange={() => {}} placeholder='Enter a name...' />
),
}

// Guards the clipping regression: the whole value must render, not "show_demo_butto".
export const LongValue: Story = {
render: () => (
<GhostInput
value='show_demo_button'
onChange={() => {}}
placeholder='Enter a name...'
/>
),
}
43 changes: 43 additions & 0 deletions frontend/documentation/pages/onboarding/CodeCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import CodeCard from 'components/pages/onboarding/OnboardingConnectPanel/CodeCard'
// CodeCard's structural styles (radius, header, lang label) live in the connect
// panel's stylesheet; import it so the card renders fully styled in isolation.
import 'components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.scss'

const meta: Meta<typeof CodeCard> = {
args: {
code: 'npm install flagsmith',
headerLeft: (
<span className='onboarding-connect__codecard-lang'>Shell</span>
),
language: 'bash',
},
component: CodeCard,
parameters: {
docs: {
description: {
component:
'A copyable, syntax-highlighted code block with a header strip. Owns its own "Copied" feedback (announced via aria-live) and is theme-adaptive via semantic tokens - a light editor in light mode, dark in dark mode.',
},
},
layout: 'padded',
},
title: 'Pages/Onboarding/CodeCard',
}
export default meta

type Story = StoryObj<typeof CodeCard>

export const Install: Story = {}

export const Wire: Story = {
args: {
code: "import flagsmith from 'flagsmith'\nflagsmith.init({ environmentID: 'ser.abc123EXAMPLEkey' })\nconst showDemo = flagsmith.hasFeature('show_demo_button')",
headerLeft: (
<span className='onboarding-connect__codecard-lang'>JavaScript</span>
),
language: 'javascript',
},
}
61 changes: 61 additions & 0 deletions frontend/documentation/pages/onboarding/InlineInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import InlineInput from 'components/pages/onboarding/InlineInput'

const meta: Meta<typeof InlineInput> = {
component: InlineInput,
parameters: {
docs: {
description: {
component:
'An onboarding-local inline editable value (GhostInput + pencil) used in the welcome sentence. Reads as part of the prose - a dashed underline hints it’s editable - rather than a pill. Commits on blur / Enter; an empty value reverts; an optional `transform` normalises on commit (e.g. flag-name rules). Feature-local - not a shared inline-edit primitive.',
},
},
layout: 'centered',
},
title: 'Pages/Onboarding/InlineInput',
}
export default meta

type Story = StoryObj<typeof InlineInput>

// InlineInput is controlled; wrap it so the stories commit and re-render.
const Controlled = ({
initial,
label,
transform,
}: {
initial: string
label: string
transform?: (raw: string) => string
}) => {
const [value, setValue] = useState(initial)
return (
<InlineInput
label={label}
value={value}
onCommit={setValue}
transform={transform}
/>
)
}

export const Default: Story = {
render: () => <Controlled label='Organisation' initial='Acme Inc' />,
}

export const Empty: Story = {
render: () => <Controlled label='Project' initial='' />,
}

// Normalises on commit (spaces → underscores, lower-cased) like the flag chip.
export const WithTransform: Story = {
render: () => (
<Controlled
label='Flag'
initial='show_demo_button'
transform={(raw) => raw.replace(/ /g, '_').toLowerCase()}
/>
),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from 'storybook'

import OnboardingConnectPanel from 'components/pages/onboarding/OnboardingConnectPanel'

const meta: Meta<typeof OnboardingConnectPanel> = {
args: {
environmentKey: 'ser.abc123EXAMPLEkey',
featureName: 'show_demo_button',
},
component: OnboardingConnectPanel,
parameters: {
docs: {
description: {
component:
'Two ways to connect an app to the pre-created flag: a "Connect with AI" tab (an agent-agnostic, zero-auth prompt carrying the real env key + flag) and a "Connect your code" tab (an SDK selector built on Chip, with install + wire snippets per language). Nothing is faked - the prompt and snippets carry the user\'s real env key + flag.',
},
},
layout: 'padded',
},
title: 'Pages/Onboarding/OnboardingConnectPanel',
}
export default meta

type Story = StoryObj<typeof OnboardingConnectPanel>

export const Default: Story = {}

export const CustomFlagName: Story = {
args: { featureName: 'enable_new_checkout' },
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from 'storybook'

import OnboardingHeader from 'components/pages/onboarding/OnboardingHeader'

const meta: Meta<typeof OnboardingHeader> = {
args: {
caseSensitive: true,
featureName: 'show_demo_button',
organisationName: 'Acme Inc',
projectName: 'Web App',
},
component: OnboardingHeader,
parameters: {
docs: {
description: {
component:
'The top of the single-page onboarding flow: a welcome title and a sentence naming the resources we pre-created, with organisation / project / flag inline-editable in place via the shared GhostInput. Presentational - the rename handlers are owned by the page that assembles the flow.',
},
},
layout: 'padded',
},
title: 'Pages/Onboarding/OnboardingHeader',
}
export default meta

type Story = StoryObj<typeof OnboardingHeader>

export const Default: Story = {}

export const LongNames: Story = {
args: {
featureName: 'enable_the_brand_new_checkout_experience_v2',
organisationName: 'A Very Long Organisation Name That Wraps',
projectName: 'Customer-Facing Marketing Website Project',
},
}

export const ShortNames: Story = {
args: {
featureName: 'beta',
organisationName: 'Co',
projectName: 'App',
},
}
Loading
Loading