Skip to content

Commit bbc47e1

Browse files
Onboarding: header and connect panel (#7801)
1 parent 171d251 commit bbc47e1

75 files changed

Lines changed: 2732 additions & 116 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎frontend/CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
5. **Linting**: ALWAYS run `npx eslint --fix <file>` on any files you modify
2020
6. **Type Enums**: Extract inline union types to named types (e.g., `type Status = 'A' | 'B'` instead of inline)
2121
7. **NO FETCH**: NEVER use `fetch()` directly - ALWAYS use RTK Query mutations/queries (inject endpoints into services in `common/services/`), see api-integration context
22+
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.
2223

2324
## Key Files
2425
- Store: `common/store.ts`

‎frontend/common/services/useEnvironment.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ export const environmentService = service
66
.enhanceEndpoints({ addTagTypes: ['Environment'] })
77
.injectEndpoints({
88
endpoints: (builder) => ({
9+
createEnvironment: builder.mutation<
10+
Res['environment'],
11+
Req['createEnvironment']
12+
>({
13+
invalidatesTags: [{ id: 'LIST', type: 'Environment' }],
14+
query: (body: Req['createEnvironment']) => ({
15+
body,
16+
method: 'POST',
17+
url: `environments/`,
18+
}),
19+
}),
920
getEnvironment: builder.query<Res['environment'], Req['getEnvironment']>({
1021
providesTags: (res) => [{ id: res?.id, type: 'Environment' }],
1122
query: (query: Req['getEnvironment']) => ({
@@ -84,6 +95,7 @@ export async function updateEnvironment(
8495
// END OF FUNCTION_EXPORTS
8596

8697
export const {
98+
useCreateEnvironmentMutation,
8799
useGetEnvironmentMetricsQuery,
88100
useGetEnvironmentQuery,
89101
useGetEnvironmentsQuery,

‎frontend/common/services/useProject.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ export const projectService = service
77
.enhanceEndpoints({ addTagTypes: ['Project'] })
88
.injectEndpoints({
99
endpoints: (builder) => ({
10+
createProject: builder.mutation<Res['project'], Req['createProject']>({
11+
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
12+
query: (body: Req['createProject']) => ({
13+
body,
14+
method: 'POST',
15+
url: `projects/`,
16+
}),
17+
}),
1018
deleteProject: builder.mutation<void, Req['deleteProject']>({
1119
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
1220
query: ({ id }: Req['deleteProject']) => ({
@@ -99,6 +107,7 @@ export async function getProject(
99107
// END OF FUNCTION_EXPORTS
100108

101109
export const {
110+
useCreateProjectMutation,
102111
useDeleteProjectMutation,
103112
useGetProjectPermissionsQuery,
104113
useGetProjectQuery,

‎frontend/common/types/requests.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ export type Req = {
659659
id: string
660660
}
661661
getProject: { id: number }
662+
createProject: { name: string; organisation: number }
662663
updateProject: { id: number; body: UpdateProjectBody }
663664
deleteProject: { id: number }
664665
migrateProject: { id: number }
@@ -722,6 +723,7 @@ export type Req = {
722723
feature_id: number
723724
group_ids: number[]
724725
}
726+
createEnvironment: { name: string; project: number }
725727
updateEnvironment: { id: number; body: Environment }
726728
createCloneIdentityFeatureStates: {
727729
environment_id: string
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { sanitizeFeatureName } from 'common/utils/sanitizeFeatureName'
2+
3+
describe('sanitizeFeatureName', () => {
4+
it.each`
5+
raw | caseSensitive | expected
6+
${'my flag'} | ${false} | ${'my_flag'}
7+
${'My Flag'} | ${false} | ${'My_Flag'}
8+
${'My Flag'} | ${true} | ${'my_flag'}
9+
${'show demo button'} | ${true} | ${'show_demo_button'}
10+
${'already_ok'} | ${false} | ${'already_ok'}
11+
${'UPPER'} | ${true} | ${'upper'}
12+
${'UPPER'} | ${false} | ${'UPPER'}
13+
${'a b'} | ${false} | ${'a__b'}
14+
${''} | ${false} | ${''}
15+
`(
16+
'sanitizeFeatureName($raw, $caseSensitive) returns $expected',
17+
({ caseSensitive, expected, raw }) => {
18+
expect(sanitizeFeatureName(raw, caseSensitive)).toBe(expected)
19+
},
20+
)
21+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Normalise a feature/flag name the way the backend expects: spaces become
2+
// underscores, and the name is lower-cased when the project enforces lower-case
3+
// feature names (only_allow_lower_case_feature_names). The backend regex stays
4+
// the final word on validity.
5+
export const sanitizeFeatureName = (
6+
raw: string,
7+
caseSensitive: boolean,
8+
): string => {
9+
const next = raw.replace(/ /g, '_')
10+
return caseSensitive ? next.toLowerCase() : next
11+
}
12+
13+
export default sanitizeFeatureName
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react'
2+
import type { Meta, StoryObj } from 'storybook'
3+
4+
import Chip from 'components/base/Chip'
5+
6+
const meta: Meta<typeof Chip> = {
7+
args: { children: 'Production' },
8+
component: Chip,
9+
parameters: {
10+
docs: {
11+
description: {
12+
component:
13+
'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.',
14+
},
15+
},
16+
layout: 'centered',
17+
},
18+
title: 'Components/Data Display/Chip',
19+
}
20+
export default meta
21+
22+
type Story = StoryObj<typeof Chip>
23+
24+
export const Neutral: Story = {}
25+
26+
export const Accent: Story = {
27+
args: { children: '"hello"', variant: 'accent' },
28+
}
29+
30+
export const Sizes: Story = {
31+
render: () => (
32+
<div className='d-flex align-items-center gap-2'>
33+
<Chip size='default'>Default</Chip>
34+
<Chip size='sm'>Small</Chip>
35+
<Chip size='xs'>Extra small</Chip>
36+
</div>
37+
),
38+
}
39+
40+
export const Removable: Story = {
41+
args: { children: 'feature-flag', onRemove: () => undefined },
42+
}
43+
44+
export const Truncated: Story = {
45+
args: {
46+
children: '{ "test": "testvalue-that-keeps-going-and-going" }',
47+
truncate: true,
48+
variant: 'accent',
49+
},
50+
}
51+
52+
export const Group: Story = {
53+
render: () => (
54+
<div className='d-flex flex-wrap gap-2'>
55+
<Chip>Development</Chip>
56+
<Chip variant='accent'>Staging</Chip>
57+
<Chip onRemove={() => undefined}>Production</Chip>
58+
</div>
59+
),
60+
}

‎frontend/documentation/components/GhostInput.stories.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import type { Meta, StoryObj } from 'storybook'
33

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

6-
const meta: Meta = {
6+
const meta: Meta<typeof GhostInput> = {
7+
component: GhostInput,
78
parameters: { layout: 'centered' },
89
title: 'Components/Forms/GhostInput',
910
}
1011
export default meta
1112

12-
type Story = StoryObj
13+
type Story = StoryObj<typeof GhostInput>
1314

1415
const Interactive = () => {
1516
const [value, setValue] = useState('my-feature-flag')
@@ -31,3 +32,14 @@ export const Empty: Story = {
3132
<GhostInput value='' onChange={() => {}} placeholder='Enter a name...' />
3233
),
3334
}
35+
36+
// Guards the clipping regression: the whole value must render, not "show_demo_butto".
37+
export const LongValue: Story = {
38+
render: () => (
39+
<GhostInput
40+
value='show_demo_button'
41+
onChange={() => {}}
42+
placeholder='Enter a name...'
43+
/>
44+
),
45+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from 'react'
2+
import type { Meta, StoryObj } from 'storybook'
3+
4+
import CodeCard from 'components/pages/onboarding/OnboardingConnectPanel/CodeCard'
5+
// CodeCard's structural styles (radius, header, lang label) live in the connect
6+
// panel's stylesheet; import it so the card renders fully styled in isolation.
7+
import 'components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.scss'
8+
9+
const meta: Meta<typeof CodeCard> = {
10+
args: {
11+
code: 'npm install flagsmith',
12+
headerLeft: (
13+
<span className='onboarding-connect__codecard-lang'>Shell</span>
14+
),
15+
language: 'bash',
16+
},
17+
component: CodeCard,
18+
parameters: {
19+
docs: {
20+
description: {
21+
component:
22+
'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.',
23+
},
24+
},
25+
layout: 'padded',
26+
},
27+
title: 'Pages/Onboarding/CodeCard',
28+
}
29+
export default meta
30+
31+
type Story = StoryObj<typeof CodeCard>
32+
33+
export const Install: Story = {}
34+
35+
export const Wire: Story = {
36+
args: {
37+
code: "import flagsmith from 'flagsmith'\nflagsmith.init({ environmentID: 'ser.abc123EXAMPLEkey' })\nconst showDemo = flagsmith.hasFeature('show_demo_button')",
38+
headerLeft: (
39+
<span className='onboarding-connect__codecard-lang'>JavaScript</span>
40+
),
41+
language: 'javascript',
42+
},
43+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useState } from 'react'
2+
import type { Meta, StoryObj } from 'storybook'
3+
4+
import InlineInput from 'components/pages/onboarding/InlineInput'
5+
6+
const meta: Meta<typeof InlineInput> = {
7+
component: InlineInput,
8+
parameters: {
9+
docs: {
10+
description: {
11+
component:
12+
'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.',
13+
},
14+
},
15+
layout: 'centered',
16+
},
17+
title: 'Pages/Onboarding/InlineInput',
18+
}
19+
export default meta
20+
21+
type Story = StoryObj<typeof InlineInput>
22+
23+
// InlineInput is controlled; wrap it so the stories commit and re-render.
24+
const Controlled = ({
25+
initial,
26+
label,
27+
transform,
28+
}: {
29+
initial: string
30+
label: string
31+
transform?: (raw: string) => string
32+
}) => {
33+
const [value, setValue] = useState(initial)
34+
return (
35+
<InlineInput
36+
label={label}
37+
value={value}
38+
onCommit={setValue}
39+
transform={transform}
40+
/>
41+
)
42+
}
43+
44+
export const Default: Story = {
45+
render: () => <Controlled label='Organisation' initial='Acme Inc' />,
46+
}
47+
48+
export const Empty: Story = {
49+
render: () => <Controlled label='Project' initial='' />,
50+
}
51+
52+
// Normalises on commit (spaces → underscores, lower-cased) like the flag chip.
53+
export const WithTransform: Story = {
54+
render: () => (
55+
<Controlled
56+
label='Flag'
57+
initial='show_demo_button'
58+
transform={(raw) => raw.replace(/ /g, '_').toLowerCase()}
59+
/>
60+
),
61+
}

0 commit comments

Comments
 (0)