From ba3b42e781366e7bc254700a7420e503da369335 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 14 Oct 2025 14:13:25 +0100 Subject: [PATCH 01/31] Claude context --- frontend/.claude/commands/api.md | 19 ++ frontend/.claude/commands/backend.md | 23 ++ frontend/.claude/commands/check-staged.md | 4 + frontend/.claude/commands/check.md | 9 + frontend/.claude/commands/feature-flag.md | 32 +++ frontend/.claude/commands/form.md | 22 ++ frontend/.claude/context/api-integration.md | 128 ++++++++++ frontend/.claude/context/architecture.md | 58 +++++ frontend/.claude/context/feature-flags.md | 99 ++++++++ frontend/.claude/context/forms.md | 168 ++++++++++++++ frontend/.claude/context/git-workflow.md | 29 +++ frontend/.claude/context/patterns.md | 244 ++++++++++++++++++++ frontend/.claude/settings.json | 71 ++++++ frontend/.env-example | 5 + frontend/CLAUDE.md | 57 +++++ 15 files changed, 968 insertions(+) create mode 100644 frontend/.claude/commands/api.md create mode 100644 frontend/.claude/commands/backend.md create mode 100644 frontend/.claude/commands/check-staged.md create mode 100644 frontend/.claude/commands/check.md create mode 100644 frontend/.claude/commands/feature-flag.md create mode 100644 frontend/.claude/commands/form.md create mode 100644 frontend/.claude/context/api-integration.md create mode 100644 frontend/.claude/context/architecture.md create mode 100644 frontend/.claude/context/feature-flags.md create mode 100644 frontend/.claude/context/forms.md create mode 100644 frontend/.claude/context/git-workflow.md create mode 100644 frontend/.claude/context/patterns.md create mode 100644 frontend/.claude/settings.json create mode 100644 frontend/.env-example create mode 100644 frontend/CLAUDE.md diff --git a/frontend/.claude/commands/api.md b/frontend/.claude/commands/api.md new file mode 100644 index 000000000000..1a0fcaed7913 --- /dev/null +++ b/frontend/.claude/commands/api.md @@ -0,0 +1,19 @@ +--- +description: Generate a new RTK Query API service using the SSG CLI +--- + +Use `npx ssg` to generate a new API service. Follow these steps: + +1. Check backend code in `../api/` Django backend for endpoint details + - Use `/backend ` to search for the endpoint + - Look for URL patterns, views, and serializers +2. Run `npx ssg` and follow the interactive prompts + - Choose operation type (get, create, update, delete, crud) + - Enter the resource name (e.g., "Feature", "Environment") +3. Define request/response types in `common/types/requests.ts` and `responses.ts` + - Add to the `Req` type for requests + - Add to the `Res` type for responses +4. Verify the generated service URL matches the backend endpoint (usually `/api/v1/...`) +5. Use the generated hooks in components: `useGetXQuery()`, `useCreateXMutation()` + +Context file: `.claude/context/api-integration.md` diff --git a/frontend/.claude/commands/backend.md b/frontend/.claude/commands/backend.md new file mode 100644 index 000000000000..fc43ed73ae87 --- /dev/null +++ b/frontend/.claude/commands/backend.md @@ -0,0 +1,23 @@ +--- +description: Search the backend codebase for endpoint details +--- + +Search the `../api/` Django backend codebase for the requested endpoint. + +Look for: +1. **URLs**: `../api//urls.py` - Route definitions and URL patterns +2. **Views**: `../api//views.py` - ViewSets and API logic +3. **Serializers**: `../api//serializers.py` - Request/response schemas +4. **Models**: `../api//models.py` - Data models +5. **Permissions**: Check for permission classes and authentication requirements + +Common Django apps in Flagsmith: +- `organisations/` - Organization management +- `projects/` - Project management +- `environments/` - Environment configuration +- `features/` - Feature flags +- `segments/` - User segments +- `users/` - User management +- `audit/` - Audit logging + +API base URL: `/api/v1/` diff --git a/frontend/.claude/commands/check-staged.md b/frontend/.claude/commands/check-staged.md new file mode 100644 index 000000000000..0ddcb6f9bd8f --- /dev/null +++ b/frontend/.claude/commands/check-staged.md @@ -0,0 +1,4 @@ +Run TypeScript checking and linting on all currently staged files, similar to pre-commit hooks. Steps: +1. Run `npm run check:staged` to typecheck and lint only staged files +2. Report any type errors or linting issues found +3. If errors exist, offer to fix them diff --git a/frontend/.claude/commands/check.md b/frontend/.claude/commands/check.md new file mode 100644 index 000000000000..60aaded6a3c1 --- /dev/null +++ b/frontend/.claude/commands/check.md @@ -0,0 +1,9 @@ +--- +description: Run type checking and linting +--- + +Run the following checks on the codebase: + +1. `npx lint-staged --allow-empty` - Fix linting issues on staged files only (same as git hook) + +Report any errors found and offer to fix them. diff --git a/frontend/.claude/commands/feature-flag.md b/frontend/.claude/commands/feature-flag.md new file mode 100644 index 000000000000..fab85f33d0a7 --- /dev/null +++ b/frontend/.claude/commands/feature-flag.md @@ -0,0 +1,32 @@ +--- +description: Create a new feature flag UI component or integration +--- + +**Note**: This codebase IS Flagsmith - the feature flag platform itself. + +When working with feature flag components: + +1. **Understand the data model**: + - Features belong to Projects + - Feature States are per Environment (enabled/disabled, value) + - Segment Overrides target specific user groups + - Identity Overrides are for specific users + +2. **Check existing components**: + - Search for similar components: `find web/components -name "*Feature*"` + - Look at Environment management patterns + - Review Segment override implementations + +3. **API patterns**: + - Features: `/api/v1/projects/{id}/features/` + - Feature States: `/api/v1/environments/{id}/featurestates/` + - Check existing services in `common/services/useFeature*.ts` + +4. **Common operations**: + - Toggle feature on/off in environment + - Update feature state value + - Add segment overrides + - Add identity overrides + - View audit history + +Context file: `.claude/context/feature-flags.md` diff --git a/frontend/.claude/commands/form.md b/frontend/.claude/commands/form.md new file mode 100644 index 000000000000..8879a75f6693 --- /dev/null +++ b/frontend/.claude/commands/form.md @@ -0,0 +1,22 @@ +--- +description: Create a new form component +--- + +**Note**: This codebase does NOT use Formik or Yup. + +Create a form following the standard pattern: + +1. Use React class component or functional component with `useState` +2. Use `InputGroup` component from global scope with `title`, `value`, `onChange` +3. For RTK Query mutations, use `useCreateXMutation()` hooks +4. Handle loading and error states +5. Use `Utils.preventDefault(e)` in submit handler +6. Use `toast()` for success/error messages +7. Use `closeModal()` to dismiss modal forms + +Examples to reference: +- `web/components/SamlForm.js` - Class component form +- `web/components/modals/CreateSegmentRulesTabForm.tsx` - Functional component form +- Search for `InputGroup` usage in `/web/components/` for more examples + +Context file: `.claude/context/forms.md` diff --git a/frontend/.claude/context/api-integration.md b/frontend/.claude/context/api-integration.md new file mode 100644 index 000000000000..772157b81839 --- /dev/null +++ b/frontend/.claude/context/api-integration.md @@ -0,0 +1,128 @@ +# API Integration Guide + +## Workflow + +**ALWAYS use the `npx ssg` CLI for API integration** + +1. Check backend code in `../api/` Django backend for endpoint details + - Use the `/backend` slash command to search backend: `/backend ` + - Common Django apps: `organisations/`, `projects/`, `environments/`, `features/`, `users/`, `segments/` + - Check: `/views.py`, `/urls.py`, `/serializers.py` +2. Run `npx ssg` and follow prompts to generate RTK Query service +3. Define types in `common/types/requests.ts` and `responses.ts` +4. Match URLs with backend endpoints in RTK Query config +5. Use generated hooks: `useGetXQuery()`, `useCreateXMutation()` + +## Finding Backend Endpoints + +### Quick Reference - Common Flagsmith API Patterns + +**Base URL Pattern**: `/api/v1/` (Flagsmith API v1) + +| Resource | List | Detail | Common Actions | +|----------|------|--------|----------------| +| **Projects** | `GET /projects/` | `GET /projects/{id}/` | `POST /projects/`, `PUT /projects/{id}/` | +| **Environments** | `GET /environments/` | `GET /environments/{api_key}/` | `POST /environments/`, `PUT /environments/{id}/` | +| **Features** | `GET /features/` | `GET /features/{id}/` | `POST /features/`, `PUT /features/{id}/`, `DELETE /features/{id}/` | +| **Feature States** | `GET /features/{id}/featurestates/` | `GET /featurestates/{id}/` | `POST /featurestates/`, `PUT /featurestates/{id}/` | +| **Identities** | `GET /identities/` | `GET /identities/{id}/` | `POST /identities/`, `DELETE /identities/{id}/` | +| **Segments** | `GET /segments/` | `GET /segments/{id}/` | `POST /segments/`, `PUT /segments/{id}/`, `DELETE /segments/{id}/` | +| **Users** | `GET /users/` | `GET /users/{id}/` | `POST /users/`, `PUT /users/{id}/` | +| **Organisations** | `GET /organisations/` | `GET /organisations/{id}/` | `POST /organisations/`, `PUT /organisations/{id}/` | + +### Search Strategy + +1. **Use `/backend` slash command**: `/backend ` searches Django codebase +2. **Check URL patterns**: Look in `../api//urls.py` + - Main API router: `../api/app/urls.py` +3. **Check ViewSets/Views**: Look in `../api//views.py` +4. **Check Serializers**: Look in `../api//serializers.py` for request/response schemas + +### Pagination Pattern + +Flagsmith API uses cursor-based pagination: + +```typescript +// Backend returns: +{ + results: [...], + next: "cursor_string", + previous: "cursor_string", + count: 100 +} + +// Use with useInfiniteScroll hook +import useInfiniteScroll from 'common/useInfiniteScroll' + +const { data, loadMore, isLoading } = useInfiniteScroll( + useGetFeaturesQuery, + { projectId: '123', page_size: 20 } +) +``` + +## State Management + +- **Redux Toolkit + RTK Query** for all API calls +- Store: `common/store.ts` with redux-persist +- Base service: `common/service.ts` +- **ALWAYS use `npx ssg` CLI to generate new services** + +### RTK Query Mutations + +```typescript +const [createMail, { isLoading, error }] = useCreateMailMutation() + +const handleSubmit = async () => { + try { + const result = await createMail(data).unwrap() + toast.success('Success!') + } catch (err) { + if ('status' in err) { + // FetchBaseQueryError - has status, data, error + const errMsg = 'error' in err ? err.error : JSON.stringify(err.data) + toast.error(errMsg) + } else { + // SerializedError - has message, code, name + toast.error(err.message || 'An error occurred') + } + } +} +``` + +### RTK Query Queries + +```typescript +const { data, error, isLoading, refetch } = useGetMailQuery({ id: '123' }) + +// Display error in UI +if (error) { + return +} + +// Retry on error +const handleRetry = () => refetch() +``` + +### 401 Unauthorized Handling + +**Automatic logout on 401** is handled in `common/service.ts`: +- All 401 responses (except email confirmation) trigger logout +- Debounced to prevent multiple logout calls +- Uses the logout endpoint from the service + +### Backend Error Response Format + +Backend typically returns: +```json +{ + "detail": "Error message here", + "code": "ERROR_CODE" +} +``` + +Access in error handling: +```typescript +if ('data' in err && err.data?.detail) { + toast.error(err.data.detail) +} +``` diff --git a/frontend/.claude/context/architecture.md b/frontend/.claude/context/architecture.md new file mode 100644 index 000000000000..a487da6164c5 --- /dev/null +++ b/frontend/.claude/context/architecture.md @@ -0,0 +1,58 @@ +# Architecture & Configuration + +## Monorepo Structure + +Flagsmith is a monorepo with: +- `../api/` - Django REST API backend +- `/frontend/` - React frontend (current directory) +- Other packages in parent directory + +## Environment Configuration + +- Config file: `common/project.js` (base environment config) +- Contains API URL, environment settings, feature flag config +- Override with `.env` file in frontend directory +- Dev server: `npm run dev` (default) or `npm run dev:local` (local API) + +## Key Technologies + +- **React 16.14** + TypeScript + Bootstrap 5.2.2 +- **Webpack 5** + Express dev server +- **Redux Toolkit + RTK Query** (API state management) +- **Flux stores** (legacy state management - being migrated to RTK) +- **Flagsmith SDK** (dogfooding - using own feature flag platform) +- **Sentry** (error tracking) +- **TestCafe** (E2E testing) + +## State Management + +### Modern (RTK Query) +- Base service: `common/service.ts` +- Store: `common/store.ts` +- Services: `common/services/use*.ts` +- Use `npx ssg` CLI to generate new services + +### Legacy (Flux) +- Stores: `common/stores/*-store.js` +- Actions: `common/dispatcher/app-actions.js` +- Being gradually migrated to RTK Query + +## Development Server + +The frontend uses an Express middleware server (`/api/index.js`) that: +- Serves the Webpack dev bundle +- Provides server-side rendering for handlebars template +- Proxies API requests to Django backend + +## Additional Rules + +- **TypeScript**: Strict type checking enabled +- **ESLint**: Enforces import aliases (no relative imports) +- **Web-specific code**: Goes in `/web/` (not `/common/`) +- **Common code**: Shared utilities/types go in `/common/` +- **Redux Persist**: User data whitelist in `common/store.ts` + +## Documentation + +- Main docs: https://docs.flagsmith.com +- GitHub: https://github.com/flagsmith/flagsmith diff --git a/frontend/.claude/context/feature-flags.md b/frontend/.claude/context/feature-flags.md new file mode 100644 index 000000000000..8657548e85dd --- /dev/null +++ b/frontend/.claude/context/feature-flags.md @@ -0,0 +1,99 @@ +# Feature Flags (Dogfooding Flagsmith) + +## Overview + +**Important**: This codebase IS Flagsmith itself - the feature flag platform. The frontend uses the Flagsmith JavaScript SDK internally for "dogfooding" (using our own product to control feature releases). + +## Configuration + +- **Flagsmith SDK**: Imported as `flagsmith` npm package +- **Environment ID**: Configured in `common/project.js` +- **Self-hosted**: Points to own API backend in `../api/` +- Uses Flagsmith to control its own feature rollouts + +## Usage Pattern + +**NOTE**: This codebase does NOT use `useFlags` hook. Check the actual implementation in the codebase for the correct pattern. + +The Flagsmith frontend likely uses one of these patterns: +1. Global `flagsmith` instance accessed directly +2. Custom provider/context for feature flags +3. Direct API calls to feature states + +To find the correct pattern, search for: +```bash +grep -r "flagsmith" common/ web/ --include="*.ts" --include="*.tsx" --include="*.js" +``` + +## Common Patterns in Feature Flag Platforms + +When building feature flag UI components, you'll typically work with: + +### Feature States +- Features have states per environment +- Each state has: `enabled` (boolean), `feature_state_value` (string/number/json) + +### Segments +- Target specific user groups with feature variations +- Segment overrides apply before default feature states + +### Identities +- Individual user overrides +- Highest priority in evaluation + +### Example API Structures + +```typescript +// Feature +{ + id: number + name: string + type: 'FLAG' | 'MULTIVARIATE' | 'CONFIG' + project: number + default_enabled: boolean + description: string +} + +// Feature State +{ + id: number + enabled: boolean + feature: number + environment: number + feature_state_value: string | null +} + +// Segment +{ + id: number + name: string + rules: SegmentRule[] + project: number +} +``` + +## Working with Feature Flag Components + +When building UI for feature management: + +1. **Feature List**: Display all features for a project +2. **Environment Toggle**: Enable/disable features per environment +3. **Value Editor**: Set configuration values for features +4. **Segment Overrides**: Target specific user segments +5. **Identity Overrides**: Override for specific users +6. **Audit Log**: Track all feature flag changes + +## Reference Examples + +Look at these existing components for patterns: +- Search for components with "Feature" in the name: `find web/components -name "*Feature*"` +- Environment management: `find web/components -name "*Environment*"` +- Segment components: `find web/components -name "*Segment*"` + +## Best Practices + +1. **Environment-specific**: Features are scoped to environments +2. **Audit everything**: Track all feature flag changes for compliance +3. **Gradual rollouts**: Use segments for percentage-based rollouts +4. **Identity targeting**: Test features with specific users first +5. **Change requests**: Require approval for production flag changes (if enabled) diff --git a/frontend/.claude/context/forms.md b/frontend/.claude/context/forms.md new file mode 100644 index 000000000000..29a8ab3cb506 --- /dev/null +++ b/frontend/.claude/context/forms.md @@ -0,0 +1,168 @@ +# Form Patterns + +## Standard Pattern for ALL Forms + +**NOTE**: This codebase does NOT use Formik or Yup. Use custom form components from global scope. + +```javascript +import React, { Component } from 'react' + +class MyForm extends Component { + constructor() { + super() + this.state = { + name: '', + isLoading: false, + error: null, + } + } + + handleSubmit = (e) => { + Utils.preventDefault(e) + if (this.state.isLoading) return + + // Basic validation + if (!this.state.name) { + this.setState({ error: 'Name is required' }) + return + } + + this.setState({ isLoading: true, error: null }) + + // Make API call + data.post(`${Project.api}endpoint/`, { name: this.state.name }) + .then(() => { + toast('Success!') + closeModal() + }) + .catch((error) => { + this.setState({ error: error.message, isLoading: false }) + }) + } + + render() { + return ( +
+ this.setState({ name: Utils.safeParseEventValue(e) })} + /> + {this.state.error && } + + + ) + } +} +``` + +## Functional Component Pattern (Modern) + +```javascript +import { FC, useState } from 'react' + +const MyForm: FC = () => { + const [name, setName] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = (e: React.FormEvent) => { + Utils.preventDefault(e) + if (isLoading) return + + if (!name) { + setError('Name is required') + return + } + + setIsLoading(true) + setError(null) + + data.post(`${Project.api}endpoint/`, { name }) + .then(() => { + toast('Success!') + closeModal() + }) + .catch((err) => { + setError(err.message) + setIsLoading(false) + }) + } + + return ( +
+ setName(Utils.safeParseEventValue(e))} + /> + {error && } + + + ) +} +``` + +## Form Components (Global Scope) + +These components are available globally (defined in global.d.ts): + +- **InputGroup**: Standard input wrapper with label + - `title` - Label text + - `value` - Input value + - `onChange` - Change handler + - `inputProps` - Additional input attributes +- **Input**: Basic input element +- **Select**: Dropdown select (uses react-select) +- **Switch**: Toggle switch component +- **Button**: Standard button with theme support + +## RTK Query Mutations Pattern + +```typescript +import { useCreateFeatureMutation } from 'common/services/useFeature' + +const MyForm: FC = () => { + const [name, setName] = useState('') + const [createFeature, { isLoading, error }] = useCreateFeatureMutation() + + const handleSubmit = async (e: React.FormEvent) => { + Utils.preventDefault(e) + + try { + await createFeature({ name, project_id: projectId }).unwrap() + toast('Feature created!') + closeModal() + } catch (err) { + toast('Error creating feature') + } + } + + return ( +
+ setName(Utils.safeParseEventValue(e))} + /> + {error && } + + + ) +} +``` + +## Examples + +Reference existing forms in the codebase: +- `web/components/SamlForm.js` - Class component form +- `web/components/modals/CreateSegmentRulesTabForm.tsx` - Complex form with state +- Search for `InputGroup` usage in `/web/components/` for more examples diff --git a/frontend/.claude/context/git-workflow.md b/frontend/.claude/context/git-workflow.md new file mode 100644 index 000000000000..7d26427c6e23 --- /dev/null +++ b/frontend/.claude/context/git-workflow.md @@ -0,0 +1,29 @@ +# Git Workflow + +## Pre-Commit Checking Strategy + +Before creating commits, always check and lint staged files to catch errors early: + +```bash +npm run check:staged +``` + +Or use the slash command: +``` +/check-staged +``` + +This runs both typechecking and linting on staged files only, mimicking pre-commit hooks. + +## Available Scripts + +- `npm run check:staged` - Typecheck + lint staged files (use this!) +- `npm run typecheck:staged` - Typecheck staged files only +- `npm run lint:staged` - Lint staged files only (with --fix) + +## Important Notes + +- Never run `npm run typecheck` (full project) or `npm run lint` on all files unless explicitly requested +- Always focus on staged files only to keep checks fast and relevant +- The lint:staged script auto-fixes issues where possible +- Fix any remaining type errors or lint issues before committing diff --git a/frontend/.claude/context/patterns.md b/frontend/.claude/context/patterns.md new file mode 100644 index 000000000000..84fa7944f24a --- /dev/null +++ b/frontend/.claude/context/patterns.md @@ -0,0 +1,244 @@ +# Common Code Patterns + +## Import Rules + +**ALWAYS use path aliases - NEVER use relative imports** + +```typescript +// ✅ Correct +import { service } from 'common/service' +import { Button } from 'components/base/forms/Button' +import { validateForm } from 'project/utils/forms/validateForm' + +// ❌ Wrong +import { service } from '../service' +import { Button } from '../../base/forms/Button' +import { validateForm } from '../../../utils/forms/validateForm' +``` + +## API Service Patterns + +### Query vs Mutation Rule + +- **GET requests** → `builder.query` +- **POST/PUT/PATCH/DELETE requests** → `builder.mutation` + +```typescript +// ✅ Correct: GET endpoint +getMailItem: builder.query({ + providesTags: (res, _, req) => [{ id: req?.id, type: 'MailItem' }], + query: (query: Req['getMailItem']) => ({ + url: `mailbox/mails/${query.id}`, + }), +}), + +// ✅ Correct: POST endpoint +createScanMail: builder.mutation({ + invalidatesTags: [{ id: 'LIST', type: 'ScanMail' }], + query: (query: Req['createScanMail']) => ({ + body: query, + method: 'POST', + url: `mailbox/mails/${query.id}/actions/scan`, + }), +}), +``` + +### File Download Pattern + +Use the reusable `handleFileDownload` utility for endpoints that return files: + +```typescript +import { handleFileDownload } from 'common/utils/fileDownload' + +getInvoiceDownload: builder.query({ + query: (query: Req['getInvoiceDownload']) => ({ + url: `customers/invoices/${query.id}/download`, + responseHandler: (response) => handleFileDownload(response, 'invoice.pdf'), + }), +}), +``` + +## Pagination Pattern + +Use `useInfiniteScroll` hook for paginated lists: + +```typescript +import useInfiniteScroll from 'common/useInfiniteScroll' +import { useGetFeaturesQuery } from 'common/services/useFeature' + +const FeatureList = ({ projectId }: Props) => { + const { + data, + isLoading, + isFetching, + loadMore, + refresh, + searchItems, + } = useInfiniteScroll( + useGetFeaturesQuery, + { projectId, page_size: 20 }, + ) + + return ( +
+ {data?.results?.map(feature => ( + + ))} + {data?.next && ( + + )} +
+ ) +} +``` + +## Error Handling + +### RTK Query Error Pattern + +```typescript +const [createFeature, { isLoading, error }] = useCreateFeatureMutation() + +const handleSubmit = async () => { + try { + const result = await createFeature(data).unwrap() + // Success - result contains the response + toast('Feature created successfully') + } catch (err) { + // Error handling + if ('status' in err) { + // FetchBaseQueryError + const errMsg = 'error' in err ? err.error : JSON.stringify(err.data) + toast(errMsg, 'danger') + } else { + // SerializedError + toast(err.message || 'An error occurred', 'danger') + } + } +} +``` + +### Query Refetching + +```typescript +const { data, refetch } = useGetFeatureQuery({ id: '123' }) + +// Refetch on demand +const handleRefresh = () => { + refetch() +} + +// Automatic refetch on focus/reconnect is enabled by default in common/service.ts +``` + +## Cache Invalidation + +### Manual Cache Clearing + +```typescript +import { getStore } from 'common/store' +import { featureService } from 'common/services/useFeature' + +export const clearFeatureCache = () => { + getStore().dispatch( + featureService.util.invalidateTags([{ type: 'Feature', id: 'LIST' }]) + ) +} +``` + +### Automatic Invalidation + +Cache invalidation is handled automatically through RTK Query tags: + +```typescript +// Mutation invalidates the list +createFeature: builder.mutation({ + invalidatesTags: [{ type: 'Feature', id: 'LIST' }], + // This will automatically refetch any active queries with matching tags +}), +``` + +## Type Organization + +### Request and Response Types + +All API types go in `common/types/`: + +```typescript +// common/types/requests.ts +export type Req = { + getFeatures: PagedRequest<{ + project: number + q?: string + }> + createFeature: { + project: number + name: string + type: 'FLAG' | 'CONFIG' + } + // END OF TYPES +} + +// common/types/responses.ts +export type Res = { + features: PagedResponse + feature: Feature + // END OF TYPES +} +``` + +### Shared Types + +For types used across requests AND responses, keep them in their respective files but document the shared usage: + +```typescript +// common/types/requests.ts +export type Address = { + address_line_1: string + address_line_2: string | null + postal_code: string + city: string + country: string +} +``` + +## SSG CLI Usage + +Always use `npx ssg` to generate new API services: + +```bash +# Interactive mode +npx ssg + +# Follow prompts to: +# 1. Choose action type (get/create/update/delete) +# 2. Enter resource name +# 3. Enter API endpoint URL +# 4. Configure cache invalidation +``` + +The CLI will: +- Create/update service file in `common/services/` +- Add types to `common/types/requests.ts` and `responses.ts` +- Generate appropriate hooks (Query or Mutation) +- Use correct import paths (no relative imports) + +## Pre-commit Checks + +Before committing, run: + +```bash +npm run check:staged +``` + +This runs: +1. TypeScript type checking on staged files +2. ESLint with auto-fix on staged files + +Or use the slash command: + +``` +/check-staged +``` diff --git a/frontend/.claude/settings.json b/frontend/.claude/settings.json new file mode 100644 index 000000000000..9367039aa0cc --- /dev/null +++ b/frontend/.claude/settings.json @@ -0,0 +1,71 @@ +{ + "autoApprovalSettings": { + "enabled": true, + "rules": [ + { + "tool": "Read", + "pattern": "**/*" + }, + { + "tool": "Bash", + "pattern": "npm run typecheck:*" + }, + { + "tool": "Bash", + "pattern": "npm run typecheck:staged" + }, + { + "tool": "Bash", + "pattern": "node:*" + }, + { + "tool": "Bash", + "pattern": "npm run lint:fix:*" + }, + { + "tool": "Bash", + "pattern": "npm run build:*" + }, + { + "tool": "Bash", + "pattern": "npm run lint*" + }, + { + "tool": "Bash", + "pattern": "npm run check:staged" + }, + { + "tool": "Bash", + "pattern": "npm run test*" + }, + { + "tool": "Bash", + "pattern": "git diff*" + }, + { + "tool": "Bash", + "pattern": "git log*" + }, + { + "tool": "Bash", + "pattern": "git status*" + }, + { + "tool": "Bash", + "pattern": "npx ssg*" + }, + { + "tool": "WebSearch", + "pattern": "*" + }, + { + "tool": "Glob", + "pattern": "*" + }, + { + "tool": "Grep", + "pattern": "*" + } + ] + } +} diff --git a/frontend/.env-example b/frontend/.env-example new file mode 100644 index 000000000000..3c0d775e8bd0 --- /dev/null +++ b/frontend/.env-example @@ -0,0 +1,5 @@ +E2E_TEST_TOKEN_DEV= +E2E_TEST_TOKEN_LOCAL= +E2E_TEST_TOKEN_PROD= +E2E_TEST_TOKEN_STAGING= +MCP_RELEASE_MANAGER_ADMIN_API_API_KEY_AUTH= diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 000000000000..6e92f4e47a53 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +## Commands +- `npm run dev` - Start dev server (frontend + API middleware) +- `npm run dev:local` - Start dev server with local environment +- `npx ssg help` - Generate Redux/API hooks (REQUIRED for API) +- `npm run typecheck` - Type checking (all files) +- `npm run lint` - Run ESLint +- `npm run lint:fix` - Auto-fix ESLint issues +- `npm run bundle` - Build production bundle +- `/check` - Slash command to typecheck and lint + +## Monorepo Structure +- `../api/` - Django REST API backend +- `/frontend/` - React frontend (current directory) + - `/common/` - Shared code (services, types, utils, hooks) + - `/web/` - Web-specific code (components, pages, styles) + - `/e2e/` - TestCafe E2E tests + - `/api/` - Express middleware server + +## Key Directories +- `/common/services/` - RTK Query services (use*.ts files) +- `/common/types/` - `requests.ts` and `responses.ts` for API types +- `/web/components/` - React components +- `/web/routes.js` - Application routing + +## Rules +1. **API Integration**: Use `npx ssg` CLI to generate RTK Query services + - Check `../api/` Django backend for endpoint details + - Use `/backend ` slash command to search backend +2. **Forms**: Custom form components (NO Formik/Yup in this codebase) + - Use InputGroup, Input, Select components from global scope + - See existing forms in `/web/components/` for patterns +3. **Imports**: Use `common/*`, `components/*`, `project/*` (NO relative imports - enforced by ESLint) +4. **State**: Redux Toolkit + RTK Query + Flux stores (legacy) + - Store: `common/store.ts` (RTK) + - Legacy stores: `common/stores/` (Flux) +5. **Feature Flags**: This IS Flagsmith - the feature flag platform itself + - Uses own Flagsmith SDK internally for dogfooding + - SDK imported as `flagsmith` package +6. **Patterns**: See `.claude/context/patterns.md` for common code patterns + +## Key Files +- Store: `common/store.ts` (RTK + redux-persist) +- Base service: `common/service.ts` (RTK Query base) +- Project config: `common/project.js` (environment config) +- Routes: `web/routes.js` + +## Tech Stack +- React 16.14 + TypeScript +- Redux Toolkit + RTK Query (API state) +- Flux stores (legacy state management) +- Bootstrap 5.2.2 + SCSS +- Webpack 5 + Express dev server +- TestCafe (E2E tests) + +For detailed guidance, see `.claude/context/` files. From 9746d1313cf83e1b409b017ed0ca6a8f759cb03f Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 14 Oct 2025 17:00:54 +0100 Subject: [PATCH 02/31] Claude context --- frontend/.claude/context/feature-flags.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/.claude/context/feature-flags.md b/frontend/.claude/context/feature-flags.md index 8657548e85dd..6056ae30b934 100644 --- a/frontend/.claude/context/feature-flags.md +++ b/frontend/.claude/context/feature-flags.md @@ -4,6 +4,14 @@ **Important**: This codebase IS Flagsmith itself - the feature flag platform. The frontend uses the Flagsmith JavaScript SDK internally for "dogfooding" (using our own product to control feature releases). +## Default Project Context + +**ALWAYS use the Flagsmith Website project unless the user explicitly specifies a different project.** + +When working with feature flags, releases, or any feature flag operations: +- Default project: "Flagsmith Website" +- Only use a different project if the user explicitly mentions it by name or ID + ## Configuration - **Flagsmith SDK**: Imported as `flagsmith` npm package From 5955f25aabd5f0b96ad7e4b4521b0f1a66781a4e Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 14 Oct 2025 17:15:48 +0100 Subject: [PATCH 03/31] Claude context --- frontend/.claude/context/feature-flags.md | 57 +++++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/frontend/.claude/context/feature-flags.md b/frontend/.claude/context/feature-flags.md index 6056ae30b934..4cd5f74660c9 100644 --- a/frontend/.claude/context/feature-flags.md +++ b/frontend/.claude/context/feature-flags.md @@ -15,24 +15,63 @@ When working with feature flags, releases, or any feature flag operations: ## Configuration - **Flagsmith SDK**: Imported as `flagsmith` npm package -- **Environment ID**: Configured in `common/project.js` +- **Environment ID**: `4vfqhypYjcPoGGu8ByrBaj` (configured in `common/project.js`) +- **API Endpoint**: `https://edge.api.flagsmith.com/api/v1/` - **Self-hosted**: Points to own API backend in `../api/` - Uses Flagsmith to control its own feature rollouts -## Usage Pattern +### Flagsmith Organization Details +- **Organization ID**: 13 (Flagsmith) +- **Main Project ID**: 12 (Flagsmith Website) +- **Main Project Name**: "Flagsmith Website" + +### Environments -**NOTE**: This codebase does NOT use `useFlags` hook. Check the actual implementation in the codebase for the correct pattern. +| Environment | ID | API Key | Description | +|-------------|-----|---------|-------------| +| **Production** | 22 | `4vfqhypYjcPoGGu8ByrBaj` | Live production environment (default in `common/project.js`) | +| **Staging** | 1848 | `ENktaJnfLVbLifybz34JmX` | Staging/testing environment | +| **Demo** | 20524 | `Ueo6zkrS8kt4LzuaJF9NFJ` | Demo environment | +| **Self hosted defaults** | 21938 | `MXSepNNQEacBBzxAU7RagJ` | Defaults for self-hosted instances | +| **Demo2** | 59277 | `DarXioFcqTNy53CeyvsqP4` | Second demo environment | -The Flagsmith frontend likely uses one of these patterns: -1. Global `flagsmith` instance accessed directly -2. Custom provider/context for feature flags -3. Direct API calls to feature states +## Querying Feature Flags by Environment + +To quickly check feature flag values for any environment: -To find the correct pattern, search for: ```bash -grep -r "flagsmith" common/ web/ --include="*.ts" --include="*.tsx" --include="*.js" +# Production flags +curl -H "X-Environment-Key: 4vfqhypYjcPoGGu8ByrBaj" \ + "https://edge.api.flagsmith.com/api/v1/flags/" | grep -A 5 "flag_name" + +# Staging flags +curl -H "X-Environment-Key: ENktaJnfLVbLifybz34JmX" \ + "https://edge.api.flagsmith.com/api/v1/flags/" | grep -A 5 "flag_name" + +# Get all flags (no filter) +curl -H "X-Environment-Key: 4vfqhypYjcPoGGu8ByrBaj" \ + "https://edge.api.flagsmith.com/api/v1/flags/" ``` +**Note**: The frontend in `common/project.js` is configured to use **Production** (`4vfqhypYjcPoGGu8ByrBaj`) by default. + +## Usage Pattern + +The Flagsmith frontend uses utility methods to access feature flags: + +```typescript +// Check if feature is enabled +Utils.getFlagsmithHasFeature('feature_name') + +// Get feature value +Utils.getFlagsmithValue('feature_name') + +// Get JSON feature value with default +Utils.getFlagsmithJSONValue('feature_name', defaultValue) +``` + +See `common/utils/utils.ts` for implementation details. + ## Common Patterns in Feature Flag Platforms When building feature flag UI components, you'll typically work with: From efedbdc003ec6d5b802e272c9ad1ee5c1e37d3c6 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 21 Oct 2025 09:37:41 +0100 Subject: [PATCH 04/31] Add api-sync / optmimise contexts --- frontend/.claude/commands/api-types-sync.md | 175 ++++++++++ frontend/.claude/commands/api.md | 20 +- frontend/.claude/commands/check-staged.md | 11 +- frontend/.claude/commands/context.md | 16 + frontend/.claude/commands/feature-flag.md | 31 +- frontend/.claude/context/api-integration.md | 155 ++++++--- frontend/.claude/context/api-types-sync.md | 217 ++++++++++++ frontend/.claude/context/architecture.md | 60 +--- frontend/.claude/context/feature-flags.md | 311 +++++++++++------- frontend/.claude/context/forms.md | 213 +++++------- frontend/.claude/context/git-workflow.md | 30 +- frontend/.claude/context/patterns.md | 131 ++++---- frontend/.claude/context/ui-patterns.md | 124 +++++++ frontend/.claude/scripts/sync-types-helper.py | 215 ++++++++++++ frontend/CLAUDE.md | 184 ++++++++--- 15 files changed, 1365 insertions(+), 528 deletions(-) create mode 100644 frontend/.claude/commands/api-types-sync.md create mode 100644 frontend/.claude/commands/context.md create mode 100644 frontend/.claude/context/api-types-sync.md create mode 100644 frontend/.claude/context/ui-patterns.md create mode 100755 frontend/.claude/scripts/sync-types-helper.py diff --git a/frontend/.claude/commands/api-types-sync.md b/frontend/.claude/commands/api-types-sync.md new file mode 100644 index 000000000000..58ba5a7361e2 --- /dev/null +++ b/frontend/.claude/commands/api-types-sync.md @@ -0,0 +1,175 @@ +--- +description: Sync frontend TypeScript types with backend Django serializers +--- + +Synchronizes types in `common/types/responses.ts` and `common/types/requests.ts` with backend serializers in `../api`. + +## Process + +### 1. Detect Backend Changes + +**IMPORTANT**: This is a monorepo. To get the latest backend changes, merge `main` into your current branch: + +```bash +git merge origin/main +``` + +Get last synced commit and current commit: + +```bash +LAST_COMMIT=$(python3 .claude/scripts/sync-types-helper.py get-last-commit 2>/dev/null || echo "") +CURRENT_COMMIT=$(cd ../api && git rev-parse HEAD) +``` + +**First-Time Sync (No LAST_COMMIT):** + +If `LAST_COMMIT` is empty, this is a first-time sync. You must: +1. Build the complete api-type-map.json by scanning ALL API endpoints +2. Use the Task tool with subagent_type=Explore to find all API service files in `common/services/` +3. For each endpoint, extract the serializer mappings and build the complete cache +4. Compare ALL frontend types with their backend serializers +5. Update any mismatches found +6. Save the complete cache with all type mappings + +**Incremental Sync (Has LAST_COMMIT):** + +If commits match, report "No changes" and exit. + +If commits differ, find changed files: + +```bash +cd ../api && git diff ${LAST_COMMIT}..HEAD --name-only | grep -E "(serializers\.py|models\.py|enums\.py)" +``` + +**File types to check:** + +1 **Serializers**: `../api//serializers.py` - Request/response schemas +2 **Models**: `../api//models.py` - Data models +3 **Enums**: `../api//enums.py` - Enum definitions + +If no relevant files changed, update cache metadata with new commit and exit. + +### 2. Identify & Update Affected Types + +For each changed serializer file: + +**A. Find affected types:** + +```bash +python3 .claude/scripts/sync-types-helper.py types-to-sync response FILE ../api +python3 .claude/scripts/sync-types-helper.py types-to-sync request FILE ../api +``` + +**B. For each affected type:** + +1. Read backend serializer fields: `cd ../api && grep -A 30 "class SerializerName" FILE` +2. Read frontend type definition: + - Response: `grep -A 15 "export type TypeName" common/types/responses.ts` + - Request: `grep -A 15 "TypeName:" common/types/requests.ts` +3. Compare fields (names, types, required/optional) +4. If mismatch found, use Edit tool to fix frontend type + +**C. Update cache:** + +```bash +cat << 'EOF' | python3 .claude/scripts/sync-types-helper.py update-metadata +{ + "lastSync": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "lastBackendCommit": "CURRENT_COMMIT_HASH" +} +EOF +``` + +### 3. Report Summary + +Display: + +- Changed serializer files (list) +- Updated response types (count + details) +- Updated request types (count + details) +- Total types synced + +## Type Comparison Rules + +**Field Matching:** + +- Ignore URL path parameters (e.g., `company_id`, `id` in path) +- String types → `string` +- Integer/Float types → `number` +- Boolean types → `boolean` +- Optional fields (`required=False`) → append `?` to field name +- Array fields → use `[]` suffix + +**Enum Handling:** + +- Django CharField with `choices` → TypeScript string union type +- Django enum fields → TypeScript string union type (e.g., `'ACTIVE' | 'CANCELLED' | 'PENDING'`) +- Model `@property` that returns `EnumName.VARIANT.name` → TypeScript string union type +- Example: Backend `status` property returns `SubscriptionStatus.ACTIVE.name` → Frontend should be `status: 'ACTIVE' | 'CANCELLED' | 'NO_PAY' | ...` +- **Note:** Computed properties returning enums require checking the model property definition to extract enum values + +**Common Mismatches:** + +- Frontend has `string` but backend expects `IntegerField` → change to `number` +- Frontend has required but backend has `required=False` → make optional with `?` +- Frontend includes URL params → remove from type definition +- Frontend includes read-only fields → remove from request types +- Frontend has generic `string` but backend has enum/choices → change to specific union type + +## Cache Structure + +```json +{ + "_metadata": { + "lastSync": "ISO timestamp", + "lastBackendCommit": "git hash" + }, + "response_types": { + "key": { + "type": "TypeName", + "serializer": "api_keys/serializers.py:SerializerName", + "endpoint": "api/endpoint/", + "method": "GET" + } + }, + "request_types": { + "key": { + "type": "TypeName", + "serializer": "api_keys/serializers.py:SerializerName", + "endpoint": "api/endpoint/", + "method": "POST|PUT|PATCH" + } + } +} +``` + +## Notes + +- Only sync types with actual Django serializers +- Request types exclude GET/DELETE endpoints (no body validation) +- File uploads (MULTIPART_FORM) need manual verification + +## Enum Change Detection + +Enum changes require checking beyond serializers: + +**When enum definitions change (`*/enums.py`):** +1. Identify which enums changed (e.g., `SubscriptionStatus`) +2. Search for TypeScript union types that should match +3. Update the union type with new/removed values + +**When model @property methods change (`*/models.py`):** +1. If a `@property` that returns `EnumType.VALUE.name` is modified +2. Check if it now returns a different enum type +3. Update corresponding frontend type's field + +**Example:** +```bash +# Detect enum file changes +git diff ${LAST_COMMIT}..HEAD ../subscriptions/enums.py + +# If SubscriptionStatus enum changed: +# 1. Check new enum values +# 2. Update frontend: export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' | ... +# 3. Find all types using this enum (PartnerSubscription, CompanySummary, etc.) +``` diff --git a/frontend/.claude/commands/api.md b/frontend/.claude/commands/api.md index 1a0fcaed7913..8d55451ec1e9 100644 --- a/frontend/.claude/commands/api.md +++ b/frontend/.claude/commands/api.md @@ -1,19 +1,13 @@ --- -description: Generate a new RTK Query API service using the SSG CLI +description: Generate a new RTK Query API service --- -Use `npx ssg` to generate a new API service. Follow these steps: +IMPORTANT: Before starting, always run `/api-types-sync` to ensure frontend types are in sync with the backend. -1. Check backend code in `../api/` Django backend for endpoint details - - Use `/backend ` to search for the endpoint - - Look for URL patterns, views, and serializers -2. Run `npx ssg` and follow the interactive prompts - - Choose operation type (get, create, update, delete, crud) - - Enter the resource name (e.g., "Feature", "Environment") -3. Define request/response types in `common/types/requests.ts` and `responses.ts` - - Add to the `Req` type for requests - - Add to the `Res` type for responses -4. Verify the generated service URL matches the backend endpoint (usually `/api/v1/...`) -5. Use the generated hooks in components: `useGetXQuery()`, `useCreateXMutation()` +Generate a new API service. Follow these steps: + +1. Run `/api-types-sync` to sync types with the backend (compares with latest backend in main) +2. Go through the process mentioned in `.claude/context/api-integration.md` +3. If I haven't specified, attempt to find where I'd want to create this component in the frontend Context file: `.claude/context/api-integration.md` diff --git a/frontend/.claude/commands/check-staged.md b/frontend/.claude/commands/check-staged.md index 0ddcb6f9bd8f..a24a0482dfca 100644 --- a/frontend/.claude/commands/check-staged.md +++ b/frontend/.claude/commands/check-staged.md @@ -1,4 +1,9 @@ -Run TypeScript checking and linting on all currently staged files, similar to pre-commit hooks. Steps: -1. Run `npm run check:staged` to typecheck and lint only staged files -2. Report any type errors or linting issues found +--- +description: Run linting on staged files +--- + +Run linting on all currently staged files, similar to pre-commit hooks: + +1. Run `npx lint-staged --allow-empty` to lint only staged files +2. Report any linting issues found 3. If errors exist, offer to fix them diff --git a/frontend/.claude/commands/context.md b/frontend/.claude/commands/context.md new file mode 100644 index 000000000000..55ecb9086ebd --- /dev/null +++ b/frontend/.claude/commands/context.md @@ -0,0 +1,16 @@ +--- +description: Load detailed context files for specific topics +--- + +Available context files in `.claude/context/`: + +1. **api-integration.md** - API integration workflow, RTK Query patterns, service creation +2. **api-types-sync.md** - Type synchronization between Django backend and TypeScript frontend +3. **architecture.md** - Environment config, tech stack, project structure +4. **feature-flags.md** - Flagsmith feature flag usage patterns (dogfooding) +5. **forms.md** - Custom form patterns, InputGroup components, validation +6. **git-workflow.md** - Git workflow, branching, PR process +7. **patterns.md** - Common code patterns, API services, error handling, linting +8. **ui-patterns.md** - UI patterns, confirmation dialogs, modals + +Which context would you like to explore? diff --git a/frontend/.claude/commands/feature-flag.md b/frontend/.claude/commands/feature-flag.md index fab85f33d0a7..c1f8841630b7 100644 --- a/frontend/.claude/commands/feature-flag.md +++ b/frontend/.claude/commands/feature-flag.md @@ -1,32 +1,5 @@ --- -description: Create a new feature flag UI component or integration +description: Create a feature flag --- -**Note**: This codebase IS Flagsmith - the feature flag platform itself. - -When working with feature flag components: - -1. **Understand the data model**: - - Features belong to Projects - - Feature States are per Environment (enabled/disabled, value) - - Segment Overrides target specific user groups - - Identity Overrides are for specific users - -2. **Check existing components**: - - Search for similar components: `find web/components -name "*Feature*"` - - Look at Environment management patterns - - Review Segment override implementations - -3. **API patterns**: - - Features: `/api/v1/projects/{id}/features/` - - Feature States: `/api/v1/environments/{id}/featurestates/` - - Check existing services in `common/services/useFeature*.ts` - -4. **Common operations**: - - Toggle feature on/off in environment - - Update feature state value - - Add segment overrides - - Add identity overrides - - View audit history - -Context file: `.claude/context/feature-flags.md` +1. Create a feature flag using the context defined in `.claude/context/feature-flags.md` diff --git a/frontend/.claude/context/api-integration.md b/frontend/.claude/context/api-integration.md index 772157b81839..d7e41a8295e1 100644 --- a/frontend/.claude/context/api-integration.md +++ b/frontend/.claude/context/api-integration.md @@ -2,70 +2,123 @@ ## Workflow -**ALWAYS use the `npx ssg` CLI for API integration** +### Preferred: Manual Service Creation + +The `npx ssg` CLI requires interactive input that cannot be automated. Instead, **manually create RTK Query services** following the patterns in existing service files. + +1. **Check backend code** in `../api` for endpoint details + - Search backend directly using Grep or Glob tools + - Common locations: `*/views.py`, `*/urls.py`, `*/serializers.py` + - Check API documentation or Swagger UI if available in your environment + +2. **Define types** in `common/types/requests.ts` and `responses.ts` + - Add to `Req` type for request parameters + - Add to `Res` type for response data + - Match backend serializer field names and types + +3. **Create service file** in `common/services/use{Entity}.ts` + - Follow pattern from existing services (e.g., `usePartner.ts`, `useCompany.ts`) + - Use `service.enhanceEndpoints()` and `.injectEndpoints()` + - Define queries with `builder.query()` + - Define mutations with `builder.mutation()` + - Set appropriate `providesTags` and `invalidatesTags` for cache management + - Export hooks: `useGetEntityQuery`, `useCreateEntityMutation`, etc. + +4. **CRITICAL: Update `.claude/api-type-map.json`** to register the new endpoint + - Add entry in `request_types` section with: + - `type`: TypeScript type signature (e.g., `{id: string, name: string}`) + - `serializer`: Backend serializer path (e.g., `entities/serializers.py:EntitySerializer`) + - `endpoint`: API endpoint pattern (e.g., `/api/v1/entities/{id}/`) + - `method`: HTTP method (GET, POST, PUT, DELETE) + - `service`: Frontend service file path (e.g., `common/services/useEntity.ts`) + - Add entry in `response_types` section (if needed) + - This enables the `/api-types-sync` command to track type changes + +5. **Verify implementation** + - Check URL matches backend endpoint exactly + - Verify HTTP method (GET, POST, PUT, DELETE) + - Ensure request body structure matches backend serializer + - Test with actual API calls + +### Example Service Structure -1. Check backend code in `../api/` Django backend for endpoint details - - Use the `/backend` slash command to search backend: `/backend ` - - Common Django apps: `organisations/`, `projects/`, `environments/`, `features/`, `users/`, `segments/` - - Check: `/views.py`, `/urls.py`, `/serializers.py` -2. Run `npx ssg` and follow prompts to generate RTK Query service -3. Define types in `common/types/requests.ts` and `responses.ts` -4. Match URLs with backend endpoints in RTK Query config -5. Use generated hooks: `useGetXQuery()`, `useCreateXMutation()` +```typescript +import { service } from 'common/service' +import { Req } from 'common/types/requests' +import { Res } from 'common/types/responses' + +export const entityService = service + .enhanceEndpoints({ addTagTypes: ['Entity'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + getEntity: builder.query({ + providesTags: (res) => [{ id: res?.id, type: 'Entity' }], + query: (query) => ({ + url: `entities/${query.id}`, + }), + }), + updateEntity: builder.mutation({ + invalidatesTags: (res) => [ + { id: 'LIST', type: 'Entity' }, + { id: res?.id, type: 'Entity' }, + ], + query: (query) => ({ + body: query, + method: 'PUT', + url: `entities/${query.id}`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export const { + useGetEntityQuery, + useUpdateEntityMutation, + // END OF EXPORTS +} = entityService +``` ## Finding Backend Endpoints -### Quick Reference - Common Flagsmith API Patterns - -**Base URL Pattern**: `/api/v1/` (Flagsmith API v1) - -| Resource | List | Detail | Common Actions | -|----------|------|--------|----------------| -| **Projects** | `GET /projects/` | `GET /projects/{id}/` | `POST /projects/`, `PUT /projects/{id}/` | -| **Environments** | `GET /environments/` | `GET /environments/{api_key}/` | `POST /environments/`, `PUT /environments/{id}/` | -| **Features** | `GET /features/` | `GET /features/{id}/` | `POST /features/`, `PUT /features/{id}/`, `DELETE /features/{id}/` | -| **Feature States** | `GET /features/{id}/featurestates/` | `GET /featurestates/{id}/` | `POST /featurestates/`, `PUT /featurestates/{id}/` | -| **Identities** | `GET /identities/` | `GET /identities/{id}/` | `POST /identities/`, `DELETE /identities/{id}/` | -| **Segments** | `GET /segments/` | `GET /segments/{id}/` | `POST /segments/`, `PUT /segments/{id}/`, `DELETE /segments/{id}/` | -| **Users** | `GET /users/` | `GET /users/{id}/` | `POST /users/`, `PUT /users/{id}/` | -| **Organisations** | `GET /organisations/` | `GET /organisations/{id}/` | `POST /organisations/`, `PUT /organisations/{id}/` | - ### Search Strategy -1. **Use `/backend` slash command**: `/backend ` searches Django codebase -2. **Check URL patterns**: Look in `../api//urls.py` - - Main API router: `../api/app/urls.py` -3. **Check ViewSets/Views**: Look in `../api//views.py` -4. **Check Serializers**: Look in `../api//serializers.py` for request/response schemas +1. **Search backend directly**: Use Grep/Glob tools to search the `../api` directory +2. **Check URL patterns**: Look in `../api/*/urls.py` +3. **Check ViewSets**: Look in `../api/*/views.py` +4. **Common file download pattern**: + - Backend returns PDF/file with `Content-Disposition: attachment; filename=...` + - Use `responseHandler` in RTK Query to handle blob downloads + - Check existing service files for examples -### Pagination Pattern +### File Download Pattern -Flagsmith API uses cursor-based pagination: +**Use the reusable utility function:** ```typescript -// Backend returns: -{ - results: [...], - next: "cursor_string", - previous: "cursor_string", - count: 100 -} - -// Use with useInfiniteScroll hook -import useInfiniteScroll from 'common/useInfiniteScroll' +import { handleFileDownload } from 'common/utils/fileDownload' -const { data, loadMore, isLoading } = useInfiniteScroll( - useGetFeaturesQuery, - { projectId: '123', page_size: 20 } -) +query: (query) => ({ + url: `resource/${query.id}/pdf`, + responseHandler: (response) => handleFileDownload(response, 'invoice.pdf'), +}) ``` +The utility automatically: +- Extracts filename from `Content-Disposition` header +- Creates and triggers download +- Cleans up blob URLs +- Returns `{ data: { url } }` format + ## State Management - **Redux Toolkit + RTK Query** for all API calls - Store: `common/store.ts` with redux-persist - Base service: `common/service.ts` -- **ALWAYS use `npx ssg` CLI to generate new services** +- **Use `npx ssg` CLI to generate new services** (optional but helpful) +- **IMPORTANT**: When implementing API logic, prefer implementing it in the RTK Query service layer (using `transformResponse`, `transformErrorResponse`, etc.) rather than in components. This makes the logic reusable across the application. + +## Error Handling Patterns ### RTK Query Mutations @@ -106,9 +159,8 @@ const handleRetry = () => refetch() ### 401 Unauthorized Handling **Automatic logout on 401** is handled in `common/service.ts`: -- All 401 responses (except email confirmation) trigger logout -- Debounced to prevent multiple logout calls -- Uses the logout endpoint from the service +- Check the service.ts file for specific 401 handling logic +- Typically debounced to prevent multiple logout calls ### Backend Error Response Format @@ -126,3 +178,8 @@ if ('data' in err && err.data?.detail) { toast.error(err.data.detail) } ``` + +## Platform Patterns + +- Web and common code are separated in the directory structure +- Check existing patterns in the codebase for platform-specific implementations diff --git a/frontend/.claude/context/api-types-sync.md b/frontend/.claude/context/api-types-sync.md new file mode 100644 index 000000000000..36cce1a4ebea --- /dev/null +++ b/frontend/.claude/context/api-types-sync.md @@ -0,0 +1,217 @@ +# API Type Synchronization + +Frontend TypeScript types in `common/types/responses.ts` mirror backend Django serializers from `../api`. + +## Type Mapping (Django → TypeScript) + +| Django Serializer | TypeScript Type | +| -------------------------------------------- | ---------------------------------------------- | +| `CharField()` | `field: string` | +| `CharField(required=False)` | `field?: string` | +| `CharField(allow_null=True)` | `field: string \| null` | +| `CharField(required=False, allow_null=True)` | `field?: string \| null` | +| `CharField(choices=EnumType.choices)` | `field: 'VALUE1' \| 'VALUE2' \| 'VALUE3'` | +| `IntegerField()` / `FloatField()` | `field: number` | +| `BooleanField()` | `field: boolean` | +| `DateTimeField()` / `DateField()` | `field: string` (ISO format) | +| `ImageField()` / `MediaSerializer()` | `field: Image` where `Image = { url: string }` | +| `ListField` / `many=True` | `Type[]` | +| `JSONField` | `Record` or specific interface | +| `SerializerMethodField()` | Usually optional: `field?: type` | +| Nested serializer | Nested type/interface | + +### Enum Types + +Django enums serialize as strings and should map to TypeScript string union types: + +**Direct enum fields:** +```python +# Backend +class MySerializer(serializers.ModelSerializer): + status = serializers.CharField(choices=StatusType.choices) +``` +```typescript +// Frontend +status: 'ACTIVE' | 'PENDING' | 'CANCELLED' +``` + +**Computed properties returning enums:** +```python +# Backend Model +@property +def status(self) -> str: + return SubscriptionStatus.ACTIVE.name # Returns "ACTIVE" +``` +```typescript +// Frontend - check model to find enum values +status: 'ACTIVE' | 'CANCELLED' | 'NO_PAY' | 'NO_ID' | 'PENDING_CANCELLATION' +``` + +**Important:** When a serializer field comes from a model `@property`, you must: +1. Find the property definition in the model file +2. Identify which enum it returns (e.g., `SubscriptionStatus.VARIANT.name`) +3. Look up the enum definition to get all possible values +4. Map to TypeScript union type with all enum values + +## Frontend-Only Types + +Types marked with `//claude-ignore` in `responses.ts` are frontend-only and should not be synced: + +```typescript +export type Res = { + // Backend-synced types above this line + + //claude-ignore - Frontend-only state + redirect: Url + devSettings: DevSettings + userData: UserData +} +``` + +Frontend-computed fields should be preserved with comment: + +```typescript +expired: boolean //FE-computed +currentPage: number //FE-computed +``` + +## Cache System + +The `.claude/api-type-map.json` file maps frontend types to backend serializers and tracks enum dependencies: + +```json +{ + "_metadata": { + "description": "Maps frontend API endpoints to backend Django serializers for type synchronization", + "lastSync": "2025-10-17T13:38:50Z", + "lastBackendCommit": "af60d9e3eef4696ca04dfd8010b4e89aa70cbe89", + "enum_dependencies": { + "description": "Maps TypeScript union types to Django enums for change detection", + "mappings": { + "SubscriptionStatus": { + "typescript_type": "SubscriptionStatus", + "django_enum": "apps/subscriptions/enums.py:SubscriptionStatus", + "used_in_types": ["PartnerSubscription", "Subscription", "Company"] + } + } + } + }, + "response_types": { + "offer": { + "type": "Offer", + "service": "common/services/useOffers.ts", + "endpoint": "offers/{id}/", + "method": "GET", + "serializer": "apps/offers/serializers.py:OfferDetailsSerializer" + } + } +} +``` + +This cache enables: + +- **Instant lookup of serializer locations** - Find backend serializer for any frontend type +- **Enum dependency tracking** - Know which Django enums map to TypeScript union types +- **Cross-reference type usage** - See which response types use each enum +- **Change detection** - Identify which types need updates when enums change +- **Backend commit tracking** - Know which backend version was last synced + +## Common Patterns + +**Nested types:** + +- `Image` → `{ url: string }` +- `Address` → Import from `requests.ts` +- Paged responses → Generic wrapper with `count`, `next?`, `previous?`, `results[]` + +**Finding serializers:** + +1. Check cache first (`.claude/api-type-map.json`) +2. Search service files: `grep -r ": TypeName" common/services/` +3. Search backend: `grep -r "SerializerName" ../api/*/views.py` + +**Before syncing:** +Always merge main into the branch to update backend code to its latest version + +## Enum Dependency Tracking + +The `_metadata.enum_dependencies` section tracks the relationship between Django enums and TypeScript union types: + +**Structure:** +```json +{ + "SubscriptionStatus": { + "typescript_type": "SubscriptionStatus", + "django_enum": "apps/subscriptions/enums.py:SubscriptionStatus", + "used_in_types": ["PartnerSubscription", "Subscription", "Company"] + } +} +``` + +**What it tracks:** +- **typescript_type** - Name of the TypeScript union type in `responses.ts` +- **django_enum** - Backend file path and enum class name +- **used_in_types** - List of response types that use this enum + +**How to use it:** + +1. **Find all enums used in the project:** + ```bash + # Read the enum_dependencies section + cat .claude/api-type-map.json | jq '._metadata.enum_dependencies.mappings' + ``` + +2. **Check which types use a specific enum:** + ```typescript + // Example: Find types using SubscriptionStatus + // Look at used_in_types: ["PartnerSubscription", "Subscription", "Company"] + ``` + +3. **Locate the Django enum definition:** + ```bash + # Use the django_enum path + cat ../api/subscriptions/enums.py | grep -A 10 "class SubscriptionStatus" + ``` + +4. **Update all affected types when enum changes:** + ```bash + # If SubscriptionStatus changes, update all types in used_in_types array + # Each type listed needs to be checked and synced + ``` + +## Monitoring Enum Changes + +Enum changes require additional tracking: + +**Files to monitor:** +- `apps/*/enums.py` - Enum value changes +- `apps/*/models.py` - Property methods returning enums +- `apps/*/serializers.py` - Serializer field changes + +**When to update frontend:** +- New enum value added → Add to TypeScript union +- Enum value removed → Remove from TypeScript union (check usage first!) +- Model `@property` changes which enum it returns → Update field type + +**Example workflow using enum dependencies:** +```bash +# 1. Check what changed since last sync +git diff LAST_COMMIT..HEAD --name-only | grep -E "(enums|models|serializers)\.py" + +# 2. If subscriptions/enums.py changed, check the api-type-map.json: +cat .claude/api-type-map.json | jq '._metadata.enum_dependencies.mappings.SubscriptionStatus' + +# 3. Read the updated enum values: +cat ../api/subscriptions/enums.py | grep -A 10 "class SubscriptionStatus" + +# 4. Update the TypeScript union type in common/types/responses.ts +# 5. Check all types listed in used_in_types to ensure they're still correct +``` + +**Automated enum sync workflow:** +When the `/api-types-sync` command runs, it: +1. Reads all enum dependencies from the cache +2. Checks if Django enum files have changed since last sync +3. Re-reads enum values from backend +4. Updates TypeScript union types +5. Verifies all types in `used_in_types` still use the correct enum diff --git a/frontend/.claude/context/architecture.md b/frontend/.claude/context/architecture.md index a487da6164c5..cc1aad9e47fc 100644 --- a/frontend/.claude/context/architecture.md +++ b/frontend/.claude/context/architecture.md @@ -1,58 +1,28 @@ # Architecture & Configuration -## Monorepo Structure - -Flagsmith is a monorepo with: -- `../api/` - Django REST API backend -- `/frontend/` - React frontend (current directory) -- Other packages in parent directory - ## Environment Configuration -- Config file: `common/project.js` (base environment config) -- Contains API URL, environment settings, feature flag config -- Override with `.env` file in frontend directory -- Dev server: `npm run dev` (default) or `npm run dev:local` (local API) +- Config files: `env/project_.js` +- Available environments: `dev`, `prod`, `staging`, `local`, `selfhosted`, `e2e` +- Project config: `common/project.js` (imports from env files) +- Override: `ENV=local npm run dev` or `ENV=staging npm run dev` ## Key Technologies -- **React 16.14** + TypeScript + Bootstrap 5.2.2 -- **Webpack 5** + Express dev server -- **Redux Toolkit + RTK Query** (API state management) -- **Flux stores** (legacy state management - being migrated to RTK) -- **Flagsmith SDK** (dogfooding - using own feature flag platform) -- **Sentry** (error tracking) -- **TestCafe** (E2E testing) - -## State Management - -### Modern (RTK Query) -- Base service: `common/service.ts` -- Store: `common/store.ts` -- Services: `common/services/use*.ts` -- Use `npx ssg` CLI to generate new services - -### Legacy (Flux) -- Stores: `common/stores/*-store.js` -- Actions: `common/dispatcher/app-actions.js` -- Being gradually migrated to RTK Query - -## Development Server - -The frontend uses an Express middleware server (`/api/index.js`) that: -- Serves the Webpack dev bundle -- Provides server-side rendering for handlebars template -- Proxies API requests to Django backend +- React 16.14 + TypeScript + Bootstrap 5.2.2 +- Redux Toolkit + RTK Query (API state management) +- Flux stores (legacy state management) +- Webpack 5 + Express dev server +- Sentry (error tracking) +- Flagsmith (feature flags - this project IS Flagsmith, dogfooding its own platform) ## Additional Rules -- **TypeScript**: Strict type checking enabled -- **ESLint**: Enforces import aliases (no relative imports) -- **Web-specific code**: Goes in `/web/` (not `/common/`) -- **Common code**: Shared utilities/types go in `/common/` -- **Redux Persist**: User data whitelist in `common/store.ts` +- **TypeScript/ESLint**: Build may ignore some errors, but always run linting on modified files +- **Web-specific code**: Goes in `/web/` directory (not `/common`) +- **Redux Persist**: Whitelist in `common/store.ts` +- **Imports**: Always use path aliases (`common/*`, `components/*`, `project/*`) - NO relative imports ## Documentation -- Main docs: https://docs.flagsmith.com -- GitHub: https://github.com/flagsmith/flagsmith +Check the main repository README and docs for additional information diff --git a/frontend/.claude/context/feature-flags.md b/frontend/.claude/context/feature-flags.md index 4cd5f74660c9..e5743668ba34 100644 --- a/frontend/.claude/context/feature-flags.md +++ b/frontend/.claude/context/feature-flags.md @@ -1,146 +1,235 @@ -# Feature Flags (Dogfooding Flagsmith) +# Feature Flags (Flagsmith) ## Overview -**Important**: This codebase IS Flagsmith itself - the feature flag platform. The frontend uses the Flagsmith JavaScript SDK internally for "dogfooding" (using our own product to control feature releases). +The project uses Flagsmith for feature flag management. Flags allow you to control feature visibility without deploying code changes. -## Default Project Context +## Project Configuration -**ALWAYS use the Flagsmith Website project unless the user explicitly specifies a different project.** +Configuration files: +- **Staging**: `common/project.js` (look for `flagsmith` property) +- **Production**: `common/project_prod_*.js` (look for `flagsmith` property) -When working with feature flags, releases, or any feature flag operations: -- Default project: "Flagsmith Website" -- Only use a different project if the user explicitly mentions it by name or ID +To find your organization and project IDs, use the MCP tools (see "Managing Feature Flags" section below). -## Configuration +## Setup -- **Flagsmith SDK**: Imported as `flagsmith` npm package -- **Environment ID**: `4vfqhypYjcPoGGu8ByrBaj` (configured in `common/project.js`) -- **API Endpoint**: `https://edge.api.flagsmith.com/api/v1/` -- **Self-hosted**: Points to own API backend in `../api/` -- Uses Flagsmith to control its own feature rollouts +- **Provider**: `components/FeatureFlagProvider.tsx` wraps the app +- **Configuration**: Flagsmith environment ID in `common/project.ts` +- **User Context**: Flags are user-specific, identified by email -### Flagsmith Organization Details -- **Organization ID**: 13 (Flagsmith) -- **Main Project ID**: 12 (Flagsmith Website) -- **Main Project Name**: "Flagsmith Website" - -### Environments - -| Environment | ID | API Key | Description | -|-------------|-----|---------|-------------| -| **Production** | 22 | `4vfqhypYjcPoGGu8ByrBaj` | Live production environment (default in `common/project.js`) | -| **Staging** | 1848 | `ENktaJnfLVbLifybz34JmX` | Staging/testing environment | -| **Demo** | 20524 | `Ueo6zkrS8kt4LzuaJF9NFJ` | Demo environment | -| **Self hosted defaults** | 21938 | `MXSepNNQEacBBzxAU7RagJ` | Defaults for self-hosted instances | -| **Demo2** | 59277 | `DarXioFcqTNy53CeyvsqP4` | Second demo environment | +## Usage Pattern -## Querying Feature Flags by Environment +```typescript +import { useFlags } from 'flagsmith/react' + +const MyComponent = () => { + // Request specific FEATURES by name (first parameter) + const flags = useFlags(['feature_name']) + const isFeatureEnabled = flags.feature_name?.enabled + + // For TRAITS, use the second parameter + // const flags = useFlags([], ['trait_name']) + // const traitValue = flags.trait_name + + return ( + <> + {isFeatureEnabled && ( +
Feature content here
+ )} + + ) +} +``` -To quickly check feature flag values for any environment: +## Best Practices -```bash -# Production flags -curl -H "X-Environment-Key: 4vfqhypYjcPoGGu8ByrBaj" \ - "https://edge.api.flagsmith.com/api/v1/flags/" | grep -A 5 "flag_name" +1. **Features vs Traits**: + - **Features** (first parameter): `useFlags(['feature_name'])` - Returns `{ enabled: boolean, value: any }` + - **Traits** (second parameter): `useFlags([], ['trait_name'])` - Returns raw value (string/number/boolean) +2. **Always check `.enabled` for features**: Use `flags.flag_name?.enabled` to get boolean +3. **Conditional rendering**: Wrap new features in flag checks +4. **Table columns**: Hide entire columns when flag is disabled (header + cells) +5. **API calls**: Only make requests if feature flag is enabled +6. **Naming**: Use snake_case for flag names (e.g., `download_invoices`) -# Staging flags -curl -H "X-Environment-Key: ENktaJnfLVbLifybz34JmX" \ - "https://edge.api.flagsmith.com/api/v1/flags/" | grep -A 5 "flag_name" +## Examples -# Get all flags (no filter) -curl -H "X-Environment-Key: 4vfqhypYjcPoGGu8ByrBaj" \ - "https://edge.api.flagsmith.com/api/v1/flags/" +### Simple Feature Toggle +```typescript +const flags = useFlags(['new_dashboard']) +if (flags.new_dashboard?.enabled) { + return +} +return ``` -**Note**: The frontend in `common/project.js` is configured to use **Production** (`4vfqhypYjcPoGGu8ByrBaj`) by default. - -## Usage Pattern - -The Flagsmith frontend uses utility methods to access feature flags: +### Table Column with Flag +```typescript +const flags = useFlags(['show_actions']) +const canShowActions = flags.show_actions?.enabled + +return ( + + + + + {canShowActions && } + + + + {data.map(item => ( + + + {canShowActions && ( + + )} + + ))} + +
NameActions
{item.name}
+) +``` +### Button with Flag ```typescript -// Check if feature is enabled -Utils.getFlagsmithHasFeature('feature_name') +const flags = useFlags(['allow_delete']) +const canDelete = flags.allow_delete?.enabled + +return ( + <> + {canDelete && ( + + )} + +) +``` + +## Managing Feature Flags via MCP + +This project uses the **flagsmith-admin-api MCP** for feature flag management. All operations are performed through MCP tools instead of manual API calls or web console. + +### Available MCP Tools + +The MCP provides tools prefixed with `mcp__flagsmith-admin-api__` for managing feature flags. Key operations: + +#### Discovery & Listing +- **`list_organizations`** - List all organizations accessible with your API key +- **`list_projects_in_organization`** - List all projects in an organization +- **`list_project_features`** - List all feature flags in a project +- **`list_project_environments`** - List all environments (staging, production, etc.) +- **`list_project_segments`** - List user segments for targeting + +#### Feature Flag Operations +- **`create_feature`** - Create a new feature flag (defaults to disabled) +- **`get_feature`** - Get detailed information about a specific flag +- **`update_feature`** - Update flag name or description +- **`get_feature_evaluation_data`** - Get analytics/metrics for a flag +- **`get_feature_external_resources`** - Get linked resources (Jira, GitHub, etc.) +- **`get_feature_code_references`** - Get code usage information + +#### Feature State Management +- **`get_environment_feature_versions`** - Get version info for a flag in an environment +- **`get_environment_feature_version_states`** - Get state info for a specific version +- **`create_environment_feature_version_state`** - Create new state (enable/disable/set value) +- **`update_environment_feature_version_state`** - Update existing state +- **`patch_environment_feature_version_state`** - Partially update state + +#### Advanced Features +- **`create_multivariate_option`** - Create A/B test variants +- **`list_multivariate_options`** - List all variants for a flag +- **`update_multivariate_option`** / **`delete_multivariate_option`** - Manage variants +- **`create_project_segment`** - Create user targeting rules +- **`update_project_segment`** / **`get_project_segment`** - Manage segments +- **`list_project_change_requests`** - List change requests for approval workflows +- **`create_environment_change_reques...`** - Create controlled deployment requests +- **`list_project_release_pipelines`** - List automated deployment pipelines + +### Common Workflows + +#### 1. Find Your Project +``` +Step 1: List organizations +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_organizations -// Get feature value -Utils.getFlagsmithValue('feature_name') +Step 2: List projects in your organization +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_projects_in_organization +Parameters: {"org_id": } -// Get JSON feature value with default -Utils.getFlagsmithJSONValue('feature_name', defaultValue) +Step 3: Find project by matching repository name to project name ``` -See `common/utils/utils.ts` for implementation details. +#### 2. List Existing Feature Flags +``` +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_project_features +Parameters: {"project_id": } +Optional: Add query params for pagination: {"page": 1, "page_size": 50} +``` -## Common Patterns in Feature Flag Platforms +#### 3. Create a New Feature Flag +``` +Step 1: Create the flag (disabled by default) +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_create_feature +Parameters: + pathParameters: {"project_id": } + body: {"name": "flag_name", "description": "Description"} + +Step 2: Get environment IDs +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_project_environments +Parameters: {"project_id": } + +Step 3: Enable for staging/development +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_get_environment_feature_versions +Then use create/update_environment_feature_version_state to enable +``` -When building feature flag UI components, you'll typically work with: +#### 4. Enable/Disable a Flag in an Environment +``` +Step 1: Get feature versions +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_get_environment_feature_versions +Parameters: {"environment_id": , "feature_id": } + +Step 2: Update feature state +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_patch_environment_feature_version_state +Parameters: + pathParameters: {"environment_id": , "feature_id": , "version_id": } + body: {"enabled": true} +``` -### Feature States -- Features have states per environment -- Each state has: `enabled` (boolean), `feature_state_value` (string/number/json) +### Best Practices -### Segments -- Target specific user groups with feature variations -- Segment overrides apply before default feature states +1. **Always look up IDs dynamically** - Don't hardcode organization, project, or feature IDs +2. **Match repository to project** - Project names typically correspond to repository names +3. **Start disabled** - New flags are created disabled by default +4. **Enable in staging first** - Test in non-production environments before enabling in production +5. **Use descriptive names** - Follow snake_case naming: `download_invoices`, `new_dashboard` +6. **Document usage** - Note which components use each flag -### Identities -- Individual user overrides -- Highest priority in evaluation +### Environment-Specific Configuration -### Example API Structures +When creating a new feature flag: +1. **Create the flag** (disabled globally by default) +2. **Enable for staging/development** to allow testing +3. **Keep production disabled** until ready for release +4. **Use change requests** for production changes if approval workflows are configured +### Trait Example (User Preferences) ```typescript -// Feature -{ - id: number - name: string - type: 'FLAG' | 'MULTIVARIATE' | 'CONFIG' - project: number - default_enabled: boolean - description: string -} +// Traits are user-specific preferences, not feature toggles +const flags = useFlags([], ['dark_mode']) +const isDarkMode = flags.dark_mode // Returns boolean/string/number directly -// Feature State -{ - id: number - enabled: boolean - feature: number - environment: number - feature_state_value: string | null -} - -// Segment -{ - id: number - name: string - rules: SegmentRule[] - project: number -} +// Setting a trait +const flagsmith = useFlagsmith() +flagsmith.setTrait('dark_mode', true) ``` -## Working with Feature Flag Components - -When building UI for feature management: +## Reference Implementation -1. **Feature List**: Display all features for a project -2. **Environment Toggle**: Enable/disable features per environment -3. **Value Editor**: Set configuration values for features -4. **Segment Overrides**: Target specific user segments -5. **Identity Overrides**: Override for specific users -6. **Audit Log**: Track all feature flag changes - -## Reference Examples - -Look at these existing components for patterns: -- Search for components with "Feature" in the name: `find web/components -name "*Feature*"` -- Environment management: `find web/components -name "*Environment*"` -- Segment components: `find web/components -name "*Segment*"` - -## Best Practices +See `pages/dashboard.tsx` for a complete example of: +- Feature flag setup with `useFlags(['flag_name'])` +- Conditional component rendering +- Checking `.enabled` property +- Wrapping entire components with feature flags -1. **Environment-specific**: Features are scoped to environments -2. **Audit everything**: Track all feature flag changes for compliance -3. **Gradual rollouts**: Use segments for percentage-based rollouts -4. **Identity targeting**: Test features with specific users first -5. **Change requests**: Require approval for production flag changes (if enabled) +See `components/DarkModeHandler.tsx` for an example of trait usage. diff --git a/frontend/.claude/context/forms.md b/frontend/.claude/context/forms.md index 29a8ab3cb506..67ae20cd7521 100644 --- a/frontend/.claude/context/forms.md +++ b/frontend/.claude/context/forms.md @@ -1,168 +1,103 @@ -# Form Patterns +# Form Patterns (Custom Components) -## Standard Pattern for ALL Forms +**IMPORTANT**: This codebase does NOT use Formik or Yup. Forms are built with custom form components. -**NOTE**: This codebase does NOT use Formik or Yup. Use custom form components from global scope. +## Standard Pattern for Forms -```javascript -import React, { Component } from 'react' - -class MyForm extends Component { - constructor() { - super() - this.state = { - name: '', - isLoading: false, - error: null, - } - } - - handleSubmit = (e) => { - Utils.preventDefault(e) - if (this.state.isLoading) return - - // Basic validation - if (!this.state.name) { - this.setState({ error: 'Name is required' }) - return - } - - this.setState({ isLoading: true, error: null }) - - // Make API call - data.post(`${Project.api}endpoint/`, { name: this.state.name }) - .then(() => { - toast('Success!') - closeModal() - }) - .catch((error) => { - this.setState({ error: error.message, isLoading: false }) - }) - } - - render() { - return ( -
- this.setState({ name: Utils.safeParseEventValue(e) })} - /> - {this.state.error && } - - - ) - } +```typescript +import { FC, FormEvent, useState } from 'react' +import InputGroup from 'components/base/forms/InputGroup' +import Button from 'components/base/forms/Button' +import Utils from 'common/utils/utils' + +type FormData = { + name: string + email: string } -``` - -## Functional Component Pattern (Modern) - -```javascript -import { FC, useState } from 'react' const MyForm: FC = () => { - const [name, setName] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) + const [formData, setFormData] = useState({ + name: '', + email: '', + }) + const [error, setError] = useState(null) + + const setFieldValue = (key: keyof FormData, value: any) => { + setFormData((prev) => ({ ...prev, [key]: value })) + } - const handleSubmit = (e: React.FormEvent) => { - Utils.preventDefault(e) - if (isLoading) return + const isValid = !!formData.name && Utils.isValidEmail(formData.email) - if (!name) { - setError('Name is required') - return + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + if (isValid) { + // Make API call } - - setIsLoading(true) - setError(null) - - data.post(`${Project.api}endpoint/`, { name }) - .then(() => { - toast('Success!') - closeModal() - }) - .catch((err) => { - setError(err.message) - setIsLoading(false) - }) } return (
setName(Utils.safeParseEventValue(e))} + title='Name' + isValid={!!formData.name && !error?.name} + inputProps={{ + error: error?.name, + name: 'name', + }} + value={formData.name} + onChange={(e: FormEvent) => setFieldValue('name', e)} + /> + setFieldValue('email', e)} /> - {error && } - + ) } ``` -## Form Components (Global Scope) +## Form Components (in `web/components/base/forms/`) -These components are available globally (defined in global.d.ts): +- **InputGroup**: Main form field wrapper + - Props: `title`, `value`, `onChange`, `isValid`, `inputProps`, `type` + - `inputProps` can contain `error`, `name`, `className`, etc. +- **Button**: Standard button component +- **Select**: Dropdown select component +- **Switch**, **Toggle**: Boolean inputs -- **InputGroup**: Standard input wrapper with label - - `title` - Label text - - `value` - Input value - - `onChange` - Change handler - - `inputProps` - Additional input attributes -- **Input**: Basic input element -- **Select**: Dropdown select (uses react-select) -- **Switch**: Toggle switch component -- **Button**: Standard button with theme support +## Validation Patterns -## RTK Query Mutations Pattern +- **Custom validation**: Use inline checks with `Utils` helpers + - `Utils.isValidEmail(email)` + - Custom business logic +- **isValid prop**: Controls visual feedback (green checkmark, etc.) +- **Error handling**: Pass `error` via `inputProps` for field-level errors -```typescript -import { useCreateFeatureMutation } from 'common/services/useFeature' +## Example with RTK Query Mutation -const MyForm: FC = () => { - const [name, setName] = useState('') - const [createFeature, { isLoading, error }] = useCreateFeatureMutation() - - const handleSubmit = async (e: React.FormEvent) => { - Utils.preventDefault(e) - - try { - await createFeature({ name, project_id: projectId }).unwrap() - toast('Feature created!') - closeModal() - } catch (err) { - toast('Error creating feature') - } +```typescript +const [createEntity, { isLoading, error: apiError }] = useCreateEntityMutation() + +const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + if (!isValid) return + + try { + await createEntity(formData).unwrap() + // Success - show toast, redirect, etc. + } catch (err) { + // Handle error + setError(err) } - - return ( -
- setName(Utils.safeParseEventValue(e))} - /> - {error && } - - - ) } ``` -## Examples - -Reference existing forms in the codebase: -- `web/components/SamlForm.js` - Class component form -- `web/components/modals/CreateSegmentRulesTabForm.tsx` - Complex form with state -- Search for `InputGroup` usage in `/web/components/` for more examples +**Reference**: See actual forms in `web/components/onboarding/` for real examples diff --git a/frontend/.claude/context/git-workflow.md b/frontend/.claude/context/git-workflow.md index 7d26427c6e23..f4ef15634f00 100644 --- a/frontend/.claude/context/git-workflow.md +++ b/frontend/.claude/context/git-workflow.md @@ -2,28 +2,36 @@ ## Pre-Commit Checking Strategy -Before creating commits, always check and lint staged files to catch errors early: +Before creating commits, always lint staged files to catch errors early: ```bash -npm run check:staged +npx lint-staged --allow-empty ``` Or use the slash command: ``` -/check-staged +/check ``` -This runs both typechecking and linting on staged files only, mimicking pre-commit hooks. +This runs ESLint with auto-fix on staged files only, mimicking pre-commit hooks. ## Available Scripts -- `npm run check:staged` - Typecheck + lint staged files (use this!) -- `npm run typecheck:staged` - Typecheck staged files only -- `npm run lint:staged` - Lint staged files only (with --fix) +- `npm run lint` - Lint all files +- `npm run lint:fix` - Lint and auto-fix all files +- `npm run typecheck` - Run TypeScript type checking +- `npx lint-staged --allow-empty` - Lint staged files only (use before committing!) + +## Linting Configuration + +The project uses lint-staged with the following configuration: +- Runs on `*.{js,tsx,ts}` files +- Uses `suppress-exit-code eslint --fix` to auto-fix issues +- Configured in `package.json` under `lint-staged` key ## Important Notes -- Never run `npm run typecheck` (full project) or `npm run lint` on all files unless explicitly requested -- Always focus on staged files only to keep checks fast and relevant -- The lint:staged script auto-fixes issues where possible -- Fix any remaining type errors or lint issues before committing +- Always run linting on modified files: `npx eslint --fix ` +- The lint-staged script auto-fixes issues where possible +- Fix any remaining lint issues before committing +- Husky pre-commit hooks may run lint-staged automatically diff --git a/frontend/.claude/context/patterns.md b/frontend/.claude/context/patterns.md index 84fa7944f24a..29f526452f68 100644 --- a/frontend/.claude/context/patterns.md +++ b/frontend/.claude/context/patterns.md @@ -16,6 +16,31 @@ import { Button } from '../../base/forms/Button' import { validateForm } from '../../../utils/forms/validateForm' ``` +## Web Component Patterns + +This codebase is primarily web-focused (React + Webpack). + +### Modals + +Use the modal system from `components/base/Modal`: + +```typescript +import { openModal, openConfirm } from 'components/base/Modal' + +// Open a custom modal +openModal('Modal Title', ) + +// Open a confirmation dialog +openConfirm( + 'Confirm Action', + 'Are you sure?', + async (closeModal) => { + // Perform action + closeModal() + } +) +``` + ## API Service Patterns ### Query vs Mutation Rule @@ -60,61 +85,29 @@ getInvoiceDownload: builder.query { - const { - data, - isLoading, - isFetching, - loadMore, - refresh, - searchItems, - } = useInfiniteScroll( - useGetFeaturesQuery, - { projectId, page_size: 20 }, - ) - - return ( -
- {data?.results?.map(feature => ( - - ))} - {data?.next && ( - - )} -
- ) -} -``` +Check existing components for pagination patterns. The codebase may use custom pagination logic or libraries like react-virtualized. ## Error Handling ### RTK Query Error Pattern ```typescript -const [createFeature, { isLoading, error }] = useCreateFeatureMutation() +const [createMail, { isLoading, error }] = useCreateMailMutation() const handleSubmit = async () => { try { - const result = await createFeature(data).unwrap() + const result = await createMail(data).unwrap() // Success - result contains the response - toast('Feature created successfully') + toast.success('Mail created successfully') } catch (err) { // Error handling if ('status' in err) { // FetchBaseQueryError const errMsg = 'error' in err ? err.error : JSON.stringify(err.data) - toast(errMsg, 'danger') + toast.error(errMsg) } else { // SerializedError - toast(err.message || 'An error occurred', 'danger') + toast.error(err.message || 'An error occurred') } } } @@ -123,7 +116,7 @@ const handleSubmit = async () => { ### Query Refetching ```typescript -const { data, refetch } = useGetFeatureQuery({ id: '123' }) +const { data, refetch } = useGetMailQuery({ id: '123' }) // Refetch on demand const handleRefresh = () => { @@ -139,11 +132,11 @@ const handleRefresh = () => { ```typescript import { getStore } from 'common/store' -import { featureService } from 'common/services/useFeature' +import { entityService } from 'common/services/useEntity' -export const clearFeatureCache = () => { +export const clearEntityCache = () => { getStore().dispatch( - featureService.util.invalidateTags([{ type: 'Feature', id: 'LIST' }]) + entityService.util.invalidateTags([{ type: 'Entity', id: 'LIST' }]) ) } ``` @@ -154,8 +147,8 @@ Cache invalidation is handled automatically through RTK Query tags: ```typescript // Mutation invalidates the list -createFeature: builder.mutation({ - invalidatesTags: [{ type: 'Feature', id: 'LIST' }], +createEntity: builder.mutation({ + invalidatesTags: [{ type: 'Entity', id: 'LIST' }], // This will automatically refetch any active queries with matching tags }), ``` @@ -169,44 +162,32 @@ All API types go in `common/types/`: ```typescript // common/types/requests.ts export type Req = { - getFeatures: PagedRequest<{ - project: number - q?: string - }> - createFeature: { - project: number + getEntity: { + id: string + } + createEntity: { name: string - type: 'FLAG' | 'CONFIG' } // END OF TYPES } // common/types/responses.ts export type Res = { - features: PagedResponse - feature: Feature + entity: Entity + entities: Entity[] // END OF TYPES } ``` ### Shared Types -For types used across requests AND responses, keep them in their respective files but document the shared usage: +**Shared types:** -```typescript -// common/types/requests.ts -export type Address = { - address_line_1: string - address_line_2: string | null - postal_code: string - city: string - country: string -} -``` +Types used across multiple request/response types should be defined separately and imported. -## SSG CLI Usage +## SSG CLI Usage (Optional) -Always use `npx ssg` to generate new API services: +You can use `npx ssg` to generate new API services: ```bash # Interactive mode @@ -225,20 +206,18 @@ The CLI will: - Generate appropriate hooks (Query or Mutation) - Use correct import paths (no relative imports) -## Pre-commit Checks +**Note**: Manual service creation is also acceptable - follow patterns from existing services. + +## Linting -Before committing, run: +Always run ESLint on files you modify: ```bash -npm run check:staged +npx eslint --fix ``` -This runs: -1. TypeScript type checking on staged files -2. ESLint with auto-fix on staged files +Or run it on all files: -Or use the slash command: - -``` -/check-staged +```bash +npm run lint:fix ``` diff --git a/frontend/.claude/context/ui-patterns.md b/frontend/.claude/context/ui-patterns.md new file mode 100644 index 000000000000..d6c8cea9b699 --- /dev/null +++ b/frontend/.claude/context/ui-patterns.md @@ -0,0 +1,124 @@ +# UI Patterns & Best Practices + +## Confirmation Dialogs + +**NEVER use `window.confirm`** - Always use the `openConfirm` function from `components/base/Modal`. + +### Correct Usage + +```typescript +import { openConfirm } from 'components/base/Modal' + +// Basic confirmation +openConfirm({ + title: 'Delete Item', + body: 'Are you sure you want to delete this item?', + onYes: () => { + // Perform delete action + deleteItem() + }, +}) + +// With custom button text and destructive styling +openConfirm({ + title: 'Discard changes', + body: 'Closing this will discard your unsaved changes.', + destructive: true, + yesText: 'Discard', + noText: 'Cancel', + onYes: () => { + // Discard changes + closeWithoutSaving() + }, + onNo: () => { + // Optional: Handle cancel action + console.log('User cancelled') + }, +}) + +// With JSX body +openConfirm({ + title: 'Delete User', + body: ( +
+ {'Are you sure you want to delete '} + {userName} + {' from this organization?'} +
+ ), + destructive: true, + onYes: async () => { + // Can be async + await deleteUser({ id: userId }) + }, +}) +``` + +### Parameters + +- **title**: `ReactNode` (required) - Dialog title (can be string or JSX) +- **body**: `ReactNode` (required) - Dialog content (can be string or JSX) +- **onYes**: `() => void` (required) - Callback when user confirms (can be async) +- **onNo**: `() => void` (optional) - Callback when user cancels +- **destructive**: `boolean` (optional) - Makes the confirm button red/dangerous +- **yesText**: `string` (optional) - Custom text for confirm button (default: "Confirm") +- **noText**: `string` (optional) - Custom text for cancel button (default: "Cancel") + +### Key Points + +- The modal closes automatically after `onYes` or `onNo` is called +- You do NOT need to manually close the modal +- Use `destructive: true` for dangerous actions (delete, discard, etc.) +- Both `onYes` and `onNo` callbacks can be async +- The `body` can be a string or JSX element for rich content +- NEVER use `window.confirm` - always use this `openConfirm` function + +## Custom Modals + +Use `openModal` for displaying custom modal content: + +```typescript +import { openModal } from 'components/base/Modal' + +// Basic modal +openModal('Modal Title', ) + +// With custom class and close callback +openModal( + 'Settings', + , + 'large-modal', // Optional className + () => { + // Optional: Called when modal closes + console.log('Modal closed') + } +) +``` + +### Parameters + +- **title**: `ReactNode` (required) - Modal title +- **body**: `ReactNode` (optional) - Modal content +- **className**: `string` (optional) - CSS class for modal styling +- **onClose**: `() => void` (optional) - Callback when modal closes + +### Nested Modals + +For modals that need to open on top of other modals (avoid if possible): + +```typescript +import { openModal2 } from 'components/base/Modal' + +openModal2('Second Modal', ) +``` + +## Backend Integration + +### Always Run API Types Sync Before API Work + +When using `/api` to generate new API services, the command automatically runs `/api-types-sync` first to: +1. Compare latest backend changes in main +2. Sync frontend types with backend serializers +3. Ensure types are up-to-date before generating new services + +This prevents type mismatches and ensures consistency. diff --git a/frontend/.claude/scripts/sync-types-helper.py b/frontend/.claude/scripts/sync-types-helper.py new file mode 100755 index 000000000000..a306e1d1a5b7 --- /dev/null +++ b/frontend/.claude/scripts/sync-types-helper.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Helper script for API type syncing operations. +Minimizes token usage by batching cache operations. +""" + +import json +import sys +from pathlib import Path +from typing import Dict, List + +CACHE_FILE = Path(__file__).parent.parent / "api-type-map.json" + + +def load_cache() -> Dict: + """Load the type cache from JSON file.""" + if not CACHE_FILE.exists(): + return {"_metadata": {}, "response_types": {}, "request_types": {}} + with open(CACHE_FILE, "r") as f: + cache = json.load(f) + # Migrate old format to new format if needed + if "types" in cache and "response_types" not in cache: + cache["response_types"] = cache.pop("types") + cache["request_types"] = {} + return cache + + +def save_cache(cache: Dict) -> None: + """Save the type cache to JSON file.""" + with open(CACHE_FILE, "w") as f: + json.dump(cache, f, indent=2) + f.write("\n") + + + + +def get_changed_serializers(old_commit: str, new_commit: str, api_path: str) -> List[str]: + """Get list of serializer files changed between commits.""" + import subprocess + + result = subprocess.run( + ["git", "diff", f"{old_commit}..{new_commit}", "--name-only"], + cwd=api_path, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + return [] + + files = result.stdout.strip().split("\n") + return [f for f in files if "serializers.py" in f] + + +def find_types_using_serializer(cache: Dict, serializer_path: str, serializer_name: str) -> List[str]: + """Find all type keys that use a specific serializer.""" + search_string = f"{serializer_path}:{serializer_name}" + types = [] + + for key, value in cache.items(): + if key == "_metadata": + continue + if value.get("serializer", "").startswith(search_string.split(":")[0]): + if serializer_name in value.get("serializer", ""): + types.append(key) + + return types + + + + +def update_metadata(stats: Dict) -> None: + """Update cache metadata with sync statistics.""" + cache = load_cache() + + if "_metadata" not in cache: + cache["_metadata"] = {} + + cache["_metadata"].update(stats) + save_cache(cache) + + +def get_types_needing_sync(serializer_files: List[str], api_path: str, type_category: str = "response") -> List[Dict]: + """ + Get list of types that need syncing based on changed serializer files. + + Args: + serializer_files: List of changed serializer file paths + api_path: Path to backend API repository + type_category: Either "response" or "request" + + Returns: + List of dicts with type info: {key, serializer_file, serializer_class, type_name} + """ + cache = load_cache() + types_to_check = [] + + # Select the appropriate cache section + cache_key = f"{type_category}_types" + type_cache = cache.get(cache_key, {}) + + for file_path in serializer_files: + # Extract serializer classes from the file path in cache + for type_key, type_data in type_cache.items(): + if type_key == "_metadata": + continue + + serializer = type_data.get("serializer", "") + if file_path in serializer and ":" in serializer: + serializer_class = serializer.split(":")[-1].strip() + types_to_check.append({ + "key": type_key, + "serializer_file": file_path, + "serializer_class": serializer_class, + "type_name": type_data.get("type", ""), + }) + + return types_to_check + + +def filter_syncable_types(cache: Dict, type_category: str = "response") -> List[Dict]: + """ + Filter cache to only include types with Django serializers (exclude custom/ChargeBee/empty). + + Args: + cache: Full cache dict + type_category: Either "response" or "request" + + Returns: + List of type info dicts + """ + syncable = [] + cache_key = f"{type_category}_types" + type_cache = cache.get(cache_key, {}) + + for type_key, type_data in type_cache.items(): + if type_key == "_metadata": + continue + + serializer = type_data.get("serializer", "") + note = type_data.get("note", "") + + # Skip custom responses, ChargeBee, NOT_IMPLEMENTED, and view methods + if any(x in note.lower() for x in ["custom", "chargebee", "empty"]): + continue + if "NOT_IMPLEMENTED" in serializer: + continue + if "views.py:" in serializer and "(" in serializer: + continue + + # Only include Django serializers + if "serializers.py:" in serializer and ":" in serializer: + parts = serializer.split(":") + if len(parts) == 2: + syncable.append({ + "key": type_key, + "serializer_file": parts[0], + "serializer_class": parts[1].strip(), + "type_name": type_data.get("type", ""), + }) + + return syncable + + +def get_last_commit() -> str: + """Get the last backend commit hash from cache metadata.""" + cache = load_cache() + return cache.get("_metadata", {}).get("lastBackendCommit", "") + + +if __name__ == "__main__": + # Command-line interface + command = sys.argv[1] if len(sys.argv) > 1 else "help" + + if command == "changed-serializers": + # Usage: python sync-types-helper.py changed-serializers OLD_COMMIT NEW_COMMIT API_PATH + old_commit = sys.argv[2] + new_commit = sys.argv[3] + api_path = sys.argv[4] + changed = get_changed_serializers(old_commit, new_commit, api_path) + print("\n".join(changed)) + + elif command == "types-to-sync": + # Usage: python sync-types-helper.py types-to-sync [response|request] FILE1 FILE2 ... API_PATH + type_category = sys.argv[2] if len(sys.argv) > 2 else "response" + files = sys.argv[3:] + api_path = sys.argv[-1] if files else "" + types = get_types_needing_sync(files[:-1], api_path, type_category) + print(json.dumps(types, indent=2)) + + elif command == "update-metadata": + # Usage: echo '{"lastSync": "..."}' | python sync-types-helper.py update-metadata + stats = json.load(sys.stdin) + update_metadata(stats) + print("Metadata updated") + + elif command == "syncable-types": + # Usage: python sync-types-helper.py syncable-types [response|request] + type_category = sys.argv[2] if len(sys.argv) > 2 else "response" + cache = load_cache() + types = filter_syncable_types(cache, type_category) + print(json.dumps(types, indent=2)) + + elif command == "get-last-commit": + # Usage: python sync-types-helper.py get-last-commit + commit = get_last_commit() + print(commit) + + else: + print("Usage:") + print(" changed-serializers OLD NEW PATH - Get changed serializer files") + print(" types-to-sync [response|request] FILE... PATH - Get types needing sync") + print(" update-metadata - Update metadata (JSON via stdin)") + print(" syncable-types [response|request] - Get all syncable type info") + print(" get-last-commit - Get last backend commit from cache") diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 6e92f4e47a53..63b49ce3f778 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -1,57 +1,137 @@ # CLAUDE.md -## Commands +Flagsmith is a feature flag and remote config platform. This is the frontend React application. + +## Monorepo Workflow + +**IMPORTANT**: This is a monorepo. Backend and frontend are in the same repository. +- Get latest backend changes: `git merge origin/main` (NOT git pull in ../api) +- Backend: `../api/` (Django REST API) +- Frontend: `/frontend/` (React app - current directory) + +## Quick Commands + +**Development:** - `npm run dev` - Start dev server (frontend + API middleware) -- `npm run dev:local` - Start dev server with local environment -- `npx ssg help` - Generate Redux/API hooks (REQUIRED for API) -- `npm run typecheck` - Type checking (all files) -- `npm run lint` - Run ESLint -- `npm run lint:fix` - Auto-fix ESLint issues -- `npm run bundle` - Build production bundle -- `/check` - Slash command to typecheck and lint - -## Monorepo Structure -- `../api/` - Django REST API backend -- `/frontend/` - React frontend (current directory) - - `/common/` - Shared code (services, types, utils, hooks) - - `/web/` - Web-specific code (components, pages, styles) - - `/e2e/` - TestCafe E2E tests - - `/api/` - Express middleware server - -## Key Directories -- `/common/services/` - RTK Query services (use*.ts files) -- `/common/types/` - `requests.ts` and `responses.ts` for API types -- `/web/components/` - React components -- `/web/routes.js` - Application routing - -## Rules -1. **API Integration**: Use `npx ssg` CLI to generate RTK Query services - - Check `../api/` Django backend for endpoint details - - Use `/backend ` slash command to search backend -2. **Forms**: Custom form components (NO Formik/Yup in this codebase) - - Use InputGroup, Input, Select components from global scope - - See existing forms in `/web/components/` for patterns -3. **Imports**: Use `common/*`, `components/*`, `project/*` (NO relative imports - enforced by ESLint) -4. **State**: Redux Toolkit + RTK Query + Flux stores (legacy) - - Store: `common/store.ts` (RTK) - - Legacy stores: `common/stores/` (Flux) -5. **Feature Flags**: This IS Flagsmith - the feature flag platform itself - - Uses own Flagsmith SDK internally for dogfooding - - SDK imported as `flagsmith` package -6. **Patterns**: See `.claude/context/patterns.md` for common code patterns - -## Key Files -- Store: `common/store.ts` (RTK + redux-persist) -- Base service: `common/service.ts` (RTK Query base) -- Project config: `common/project.js` (environment config) -- Routes: `web/routes.js` +- `npm run dev:local` - Start with local environment config + +**Code Quality:** +- `npx eslint --fix ` - Lint and fix a file (ALWAYS run on modified files) +- `npm run typecheck` - TypeScript type checking +- `npx lint-staged --allow-empty` - Lint all staged files + +**Build:** +- `npm run bundle` - Production build + +**Tools:** +- `npx ssg` - Generate RTK Query API services (optional) + +## Slash Commands + +- `/api` - Generate new RTK Query API service +- `/api-types-sync` - Sync TypeScript types with Django backend +- `/check` - Lint staged files +- `/context` - View available context files +- `/feature-flag` - Create a feature flag + +## Directory Structure + +``` +/frontend/ + /common/ - Shared code (services, types, utils, hooks) + /services/ - RTK Query API services (use*.ts files) + /types/ - requests.ts & responses.ts (API types) + store.ts - Redux store + redux-persist + service.ts - RTK Query base config + /web/ - Web-specific code + /components/ - React components + routes.js - Application routing + /e2e/ - TestCafe E2E tests + /api/ - Express dev server middleware + /env/ - Environment configs (project_*.js) +``` + +## Critical Rules + +### 1. Imports - NO Relative Imports +```typescript +// ✅ Correct +import { service } from 'common/service' +import Button from 'components/base/forms/Button' + +// ❌ Wrong +import { service } from '../../service' +import Button from '../base/forms/Button' +``` + +### 2. Linting - ALWAYS Required +- Run `npx eslint --fix ` on ANY file you modify +- Pre-commit hooks use lint-staged + +### 3. Forms - NO Formik/Yup +This codebase uses **custom form components**, NOT Formik or Yup: +```typescript +import InputGroup from 'components/base/forms/InputGroup' +import Button from 'components/base/forms/Button' +// Use state + custom validation +``` + +### 4. Modals - NEVER use window.confirm +```typescript +// ✅ Correct +import { openConfirm } from 'components/base/Modal' +openConfirm({ title: 'Delete?', body: '...', onYes: () => {} }) + +// ❌ Wrong +window.confirm('Delete?') +``` + +### 5. API Integration +- Backend types sync: Run `/api-types-sync` before API work +- RTK Query services: `common/services/use*.ts` +- Manual service creation or use `npx ssg` (optional) +- Check Django backend in `../api/` for endpoint details + +### 6. State Management +- **RTK Query**: API calls & caching (`common/service.ts`) +- **Redux Toolkit**: Global state (`common/store.ts`) +- **Flux stores**: Legacy (in `common/stores/`) + +### 7. Feature Flags (Dogfooding!) +This **IS** Flagsmith - the feature flag platform itself. We dogfood our own SDK: +```typescript +import flagsmith from 'flagsmith' +// Check .claude/context/feature-flags.md +``` + +### 8. Type Organization +- Extract inline union types to named types: +```typescript +// ✅ Good +type Status = 'active' | 'inactive' +const status: Status = 'active' + +// ❌ Avoid +const status: 'active' | 'inactive' = 'active' +``` ## Tech Stack -- React 16.14 + TypeScript -- Redux Toolkit + RTK Query (API state) -- Flux stores (legacy state management) -- Bootstrap 5.2.2 + SCSS -- Webpack 5 + Express dev server -- TestCafe (E2E tests) - -For detailed guidance, see `.claude/context/` files. +- **React**: 16.14 (older version, not latest) +- **TypeScript**: 4.6.4 +- **State**: Redux Toolkit + RTK Query + Flux (legacy) +- **Styling**: Bootstrap 5.2.2 + SCSS +- **Build**: Webpack 5 + Express dev server +- **Testing**: TestCafe (E2E) + +## Context Files + +Detailed documentation in `.claude/context/`: +- `api-integration.md` - RTK Query patterns, service creation +- `api-types-sync.md` - Django ↔ TypeScript type syncing +- `architecture.md` - Environment config, project structure +- `feature-flags.md` - Flagsmith SDK usage (dogfooding) +- `forms.md` - Custom form patterns +- `git-workflow.md` - Git workflow, linting, pre-commit +- `patterns.md` - Code patterns, error handling +- `ui-patterns.md` - Modals, confirmations, UI helpers + From cae73d7cdba7b849cdd6e5b698635e27eccce197 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 08:38:04 +0000 Subject: [PATCH 05/31] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- frontend/.claude/scripts/sync-types-helper.py | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/frontend/.claude/scripts/sync-types-helper.py b/frontend/.claude/scripts/sync-types-helper.py index a306e1d1a5b7..c9e6794fb35d 100755 --- a/frontend/.claude/scripts/sync-types-helper.py +++ b/frontend/.claude/scripts/sync-types-helper.py @@ -32,9 +32,9 @@ def save_cache(cache: Dict) -> None: f.write("\n") - - -def get_changed_serializers(old_commit: str, new_commit: str, api_path: str) -> List[str]: +def get_changed_serializers( + old_commit: str, new_commit: str, api_path: str +) -> List[str]: """Get list of serializer files changed between commits.""" import subprocess @@ -52,7 +52,9 @@ def get_changed_serializers(old_commit: str, new_commit: str, api_path: str) -> return [f for f in files if "serializers.py" in f] -def find_types_using_serializer(cache: Dict, serializer_path: str, serializer_name: str) -> List[str]: +def find_types_using_serializer( + cache: Dict, serializer_path: str, serializer_name: str +) -> List[str]: """Find all type keys that use a specific serializer.""" search_string = f"{serializer_path}:{serializer_name}" types = [] @@ -67,8 +69,6 @@ def find_types_using_serializer(cache: Dict, serializer_path: str, serializer_na return types - - def update_metadata(stats: Dict) -> None: """Update cache metadata with sync statistics.""" cache = load_cache() @@ -80,7 +80,9 @@ def update_metadata(stats: Dict) -> None: save_cache(cache) -def get_types_needing_sync(serializer_files: List[str], api_path: str, type_category: str = "response") -> List[Dict]: +def get_types_needing_sync( + serializer_files: List[str], api_path: str, type_category: str = "response" +) -> List[Dict]: """ Get list of types that need syncing based on changed serializer files. @@ -108,12 +110,14 @@ def get_types_needing_sync(serializer_files: List[str], api_path: str, type_cate serializer = type_data.get("serializer", "") if file_path in serializer and ":" in serializer: serializer_class = serializer.split(":")[-1].strip() - types_to_check.append({ - "key": type_key, - "serializer_file": file_path, - "serializer_class": serializer_class, - "type_name": type_data.get("type", ""), - }) + types_to_check.append( + { + "key": type_key, + "serializer_file": file_path, + "serializer_class": serializer_class, + "type_name": type_data.get("type", ""), + } + ) return types_to_check @@ -152,12 +156,14 @@ def filter_syncable_types(cache: Dict, type_category: str = "response") -> List[ if "serializers.py:" in serializer and ":" in serializer: parts = serializer.split(":") if len(parts) == 2: - syncable.append({ - "key": type_key, - "serializer_file": parts[0], - "serializer_class": parts[1].strip(), - "type_name": type_data.get("type", ""), - }) + syncable.append( + { + "key": type_key, + "serializer_file": parts[0], + "serializer_class": parts[1].strip(), + "type_name": type_data.get("type", ""), + } + ) return syncable @@ -208,8 +214,18 @@ def get_last_commit() -> str: else: print("Usage:") - print(" changed-serializers OLD NEW PATH - Get changed serializer files") - print(" types-to-sync [response|request] FILE... PATH - Get types needing sync") - print(" update-metadata - Update metadata (JSON via stdin)") - print(" syncable-types [response|request] - Get all syncable type info") - print(" get-last-commit - Get last backend commit from cache") + print( + " changed-serializers OLD NEW PATH - Get changed serializer files" + ) + print( + " types-to-sync [response|request] FILE... PATH - Get types needing sync" + ) + print( + " update-metadata - Update metadata (JSON via stdin)" + ) + print( + " syncable-types [response|request] - Get all syncable type info" + ) + print( + " get-last-commit - Get last backend commit from cache" + ) From 23a649559fd3df440402fc8d42a66109e9fdf99c Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 21 Oct 2025 10:16:31 +0100 Subject: [PATCH 06/31] Add api-sync / optimise contexts --- frontend/.claude/commands/api-types-sync.md | 372 +++++++++++++++++--- 1 file changed, 317 insertions(+), 55 deletions(-) diff --git a/frontend/.claude/commands/api-types-sync.md b/frontend/.claude/commands/api-types-sync.md index 58ba5a7361e2..5316bab76d8f 100644 --- a/frontend/.claude/commands/api-types-sync.md +++ b/frontend/.claude/commands/api-types-sync.md @@ -4,9 +4,62 @@ description: Sync frontend TypeScript types with backend Django serializers Synchronizes types in `common/types/responses.ts` and `common/types/requests.ts` with backend serializers in `../api`. +## Overview + +This command runs in **three phases** depending on the current state: + +1. **Phase 1 (First-time sync):** Build cache + validate 5-10 critical types (~10k tokens, 2-3 min) +2. **Phase 2 (Full validation):** Validate all remaining types (~50k tokens, 15-20 min) +3. **Phase 3 (Incremental sync):** Only validate types affected by backend changes (~5k-20k tokens) + +**Typical workflow:** +```bash +# First run - builds cache and validates critical types +/api-types-sync +# Output: "Phase 1 complete. Run /api-types-sync again for full validation." + +# Second run - validates all remaining types for 100% accuracy +/api-types-sync +# Output: "Phase 2 complete. All types now synced with backend." + +# Future runs - only sync types affected by backend changes +/api-types-sync +# Output: "Updated 3 types based on backend changes" +``` + +## How It Works + +**Frontend → Backend Mapping:** + +Frontend service files (`common/services/use*.ts`) define API calls with type annotations: + +```typescript +// Example: common/services/useProject.ts +export const projectService = baseService.injectEndpoints({ + endpoints: (builder) => ({ + getProject: builder.query({ + query: ({ id }) => `projects/${id}/`, + }), + updateProject: builder.mutation({ + query: ({ id, data }) => ({ + url: `projects/${id}/`, + method: 'PUT', + body: data, + }), + }), + }), +}) +``` + +**Mapping process:** +1. Extract: `Res['project']` → endpoint `projects/:id/` → method `GET` +2. Find Django URL: `projects/:id/` → maps to `ProjectViewSet.retrieve` +3. Find serializer: `ProjectViewSet.retrieve` uses `ProjectRetrieveSerializer` +4. Cache mapping: `"response_types.project"` → `"projects/serializers.py:ProjectRetrieveSerializer"` + ## Process -### 1. Detect Backend Changes +### 1. Detect Sync Mode **IMPORTANT**: This is a monorepo. To get the latest backend changes, merge `main` into your current branch: @@ -21,17 +74,107 @@ LAST_COMMIT=$(python3 .claude/scripts/sync-types-helper.py get-last-commit 2>/de CURRENT_COMMIT=$(cd ../api && git rev-parse HEAD) ``` -**First-Time Sync (No LAST_COMMIT):** +Load cache metadata to detect sync mode: + +```bash +FULLY_VALIDATED=$(cat .claude/api-type-map.json 2>/dev/null | grep -o '"fullyValidated"[[:space:]]*:[[:space:]]*[a-z]*' | grep -o '[a-z]*$' || echo "false") +``` + +**Determine which mode:** + +- **Phase 1 (First-time sync):** `LAST_COMMIT` is empty → Build cache + validate critical types +- **Phase 2 (Full validation):** `LAST_COMMIT` exists but `FULLY_VALIDATED` is false → Validate all remaining types +- **Phase 3 (Incremental sync):** `LAST_COMMIT` exists and `FULLY_VALIDATED` is true → Check for changed serializers only + +--- + +### Phase 1: First-Time Sync (No LAST_COMMIT) + +**Goal:** Build cache and validate critical types to prove system works. + +**Steps:** + +1. **Scan frontend service files** (`common/services/use*.ts`): + - Extract all endpoint definitions with `Res['typeName']` and `Req['typeName']` + - Record endpoint URL and HTTP method for each type + +2. **Map types to backend serializers** (multi-step process): + - For each endpoint URL (e.g., `projects/:id/`): + - Find matching Django URL pattern in `../api/*/urls.py` + - Extract the ViewSet or View class name + - Read the ViewSet/View file to find the serializer class + - Record: `"serializer": "app/serializers.py:SerializerClassName"` + - If no serializer found, leave empty string `""` + +3. **Build cache structure** (`.claude/api-type-map.json`): +```json +{ + "_metadata": { + "lastSync": "2025-01-15T10:30:00Z", + "lastBackendCommit": "abc123...", + "fullyValidated": false, + "criticalTypesValidated": ["project", "environment", "organisation", "projectFlag", "identity"] + }, + "response_types": { + "project": { + "type": "project", + "serializer": "projects/serializers.py:ProjectRetrieveSerializer", + "endpoint": "projects/:id/", + "method": "GET" + } + }, + "request_types": { ... } +} +``` + +4. **Validate critical types only** (5-10 types to prove system works): + - Response types: `project`, `environment`, `organisation`, `projectFlags`/`projectFlag`, `identity` + - Request types: `updateProject`, `createEnvironment`, `updateOrganisation`, `createProjectFlag`, `createIdentities` + - For each critical type: + - Read backend serializer fields + - Read frontend type definition + - Compare and fix mismatches + - Record validated type names in `_metadata.criticalTypesValidated` + +5. **Save cache** with `fullyValidated: false` and current commit hash + +**Output:** "Phase 1 complete. Cache built with X response types and Y request types. Validated 5-10 critical types. Run `/api-types-sync` again to validate all remaining types." + +--- + +### Phase 2: Full Validation (LAST_COMMIT exists, FULLY_VALIDATED = false) + +**Goal:** Validate all remaining types to reach 100% accuracy. + +**Steps:** + +1. **Load existing cache** and get all types with valid serializers: +```bash +python3 .claude/scripts/sync-types-helper.py syncable-types response > /tmp/response_types.json +python3 .claude/scripts/sync-types-helper.py syncable-types request > /tmp/request_types.json +``` + +2. **For each syncable type:** + - Skip if already in `_metadata.criticalTypesValidated` (done in Phase 1) + - Read backend serializer fields + - Read frontend type definition + - Compare and fix mismatches + - Track progress (e.g., "Validating type 15/96: auditLogs") -If `LAST_COMMIT` is empty, this is a first-time sync. You must: -1. Build the complete api-type-map.json by scanning ALL API endpoints -2. Use the Task tool with subagent_type=Explore to find all API service files in `common/services/` -3. For each endpoint, extract the serializer mappings and build the complete cache -4. Compare ALL frontend types with their backend serializers -5. Update any mismatches found -6. Save the complete cache with all type mappings +3. **Update cache metadata:** + - Set `fullyValidated: true` + - Update `lastBackendCommit` to current commit + - Update `lastSync` timestamp + +**Output:** "Phase 2 complete. Validated X response types and Y request types. All types now synced with backend." + +--- -**Incremental Sync (Has LAST_COMMIT):** +### Phase 3: Incremental Sync (LAST_COMMIT exists, FULLY_VALIDATED = true) + +**Goal:** Validate only types affected by recent backend changes. + +**Steps:** If commits match, report "No changes" and exit. @@ -43,17 +186,19 @@ cd ../api && git diff ${LAST_COMMIT}..HEAD --name-only | grep -E "(serializers\. **File types to check:** -1 **Serializers**: `../api//serializers.py` - Request/response schemas -2 **Models**: `../api//models.py` - Data models -3 **Enums**: `../api//enums.py` - Enum definitions +1. **Serializers**: `../api//serializers.py` - Request/response schemas +2. **Models**: `../api//models.py` - Data models +3. **Enums**: `../api//enums.py` - Enum definitions If no relevant files changed, update cache metadata with new commit and exit. -### 2. Identify & Update Affected Types +### 2. Identify & Update Affected Types (Phase 3 only) For each changed serializer file: -**A. Find affected types:** +**A. Find affected types using helper script:** + +**NOTE:** The helper script only works when cache already exists. For Phase 1 (first-time sync), you must build the cache manually by scanning frontend service files. ```bash python3 .claude/scripts/sync-types-helper.py types-to-sync response FILE ../api @@ -62,14 +207,21 @@ python3 .claude/scripts/sync-types-helper.py types-to-sync request FILE ../api **B. For each affected type:** -1. Read backend serializer fields: `cd ../api && grep -A 30 "class SerializerName" FILE` -2. Read frontend type definition: - - Response: `grep -A 15 "export type TypeName" common/types/responses.ts` - - Request: `grep -A 15 "TypeName:" common/types/requests.ts` -3. Compare fields (names, types, required/optional) -4. If mismatch found, use Edit tool to fix frontend type +1. **Read backend serializer fields:** + ```bash + cd ../api && grep -A 30 "class SerializerName" FILE + ``` + Extract field names, types, and whether they're required/optional + +2. **Read frontend type definition:** + - Response types: `grep -A 15 "export type TypeName" common/types/responses.ts` + - Request types: `grep -A 15 "TypeName:" common/types/requests.ts` + +3. **Compare fields** (names, types, required/optional) -**C. Update cache:** +4. **If mismatch found:** Use Edit tool to fix frontend type + +**C. Update cache metadata:** ```bash cat << 'EOF' | python3 .claude/scripts/sync-types-helper.py update-metadata @@ -82,12 +234,23 @@ EOF ### 3. Report Summary -Display: +Display based on phase: + +**Phase 1:** +- Cache built: X response types, Y request types +- Critical types validated: [list] +- Next step: Run `/api-types-sync` again for full validation + +**Phase 2:** +- Validated: X response types, Y request types +- Types fixed: Z +- All types now synced with backend -- Changed serializer files (list) -- Updated response types (count + details) -- Updated request types (count + details) -- Total types synced +**Phase 3:** +- Changed serializer files: [list] +- Updated response types: X (with details) +- Updated request types: Y (with details) +- Total types synced: Z ## Type Comparison Rules @@ -118,26 +281,69 @@ Display: ## Cache Structure +The cache maps frontend types to their backend serializers for efficient incremental syncing. + +**Field Definitions:** + +- `"key"`: The type key from `Res['key']` or `Req['key']` in frontend service files + - Example: `Res['project']` → key is `"project"` + - Example: `Req['createProject']` → key is `"createProject"` + +- `"type"`: Usually matches the key, but can differ for nested types + - In most cases: `"type": "project"` matches `"key": "project"` + +- `"serializer"`: Path to backend serializer in format `"app/serializers.py:ClassName"` + - Example: `"projects/serializers.py:ProjectRetrieveSerializer"` + - Empty string `""` if no serializer found (custom responses, view methods) + +- `"endpoint"`: The API endpoint URL from frontend service file + - Example: `"projects/:id/"` (include path params like `:id`) + +- `"method"`: HTTP method + - Response types: `"GET"`, `"POST"`, `"DELETE"` (any method that returns data) + - Request types: `"POST"`, `"PUT"`, `"PATCH"` (only methods that send body data) + +**Example cache:** + ```json { "_metadata": { - "lastSync": "ISO timestamp", - "lastBackendCommit": "git hash" + "lastSync": "2025-01-15T10:30:00Z", + "lastBackendCommit": "abc123def456", + "fullyValidated": false, + "criticalTypesValidated": ["project", "environment"] }, "response_types": { - "key": { - "type": "TypeName", - "serializer": "api_keys/serializers.py:SerializerName", - "endpoint": "api/endpoint/", + "project": { + "type": "project", + "serializer": "projects/serializers.py:ProjectRetrieveSerializer", + "endpoint": "projects/:id/", "method": "GET" + }, + "projects": { + "type": "projects", + "serializer": "projects/serializers.py:ProjectListSerializer", + "endpoint": "projects/", + "method": "GET" + }, + "identityTraits": { + "type": "identityTraits", + "serializer": "", + "endpoint": "/environments/:environmentId/identities/:identity/traits/", + "method": "GET", + "note": "Custom response, no serializer" } }, "request_types": { - "key": { - "type": "TypeName", - "serializer": "api_keys/serializers.py:SerializerName", - "endpoint": "api/endpoint/", - "method": "POST|PUT|PATCH" + "createProject": { + "serializer": "projects/serializers.py:ProjectSerializer", + "endpoint": "projects/", + "method": "POST" + }, + "updateProject": { + "serializer": "projects/serializers.py:ProjectSerializer", + "endpoint": "projects/:id/", + "method": "PUT" } } } @@ -145,31 +351,87 @@ Display: ## Notes -- Only sync types with actual Django serializers -- Request types exclude GET/DELETE endpoints (no body validation) -- File uploads (MULTIPART_FORM) need manual verification +- **Only sync types with actual Django serializers** - Skip custom responses, ChargeBee types, view methods +- **Request types exclude GET/DELETE** - These methods don't send body data, so no request type needed +- **File uploads need manual verification** - MULTIPART_FORM endpoints may not have serializers +- **Helper script requires cache** - `sync-types-helper.py` functions only work after cache is built in Phase 1 + +## Common Pitfalls + +**Pitfall 1: Using helper script during Phase 1** +- ❌ Problem: Running `syncable-types` before cache exists +- ✅ Solution: Build cache manually by scanning frontend service files first + +**Pitfall 2: Forgetting path parameters** +- ❌ Problem: Including `:id`, `:projectId` in request/response types +- ✅ Solution: Path params go in the URL, not the request/response body + +**Pitfall 3: Assuming type = key** +- ❌ Problem: Using "type" field as cache lookup key +- ✅ Solution: The JSON key (e.g., `"project"`) is the lookup key, `"type"` field is metadata + +**Pitfall 4: Stopping after Phase 1** +- ❌ Problem: Only critical types validated, 80%+ of types unchecked +- ✅ Solution: Run `/api-types-sync` again for Phase 2 full validation + +**Pitfall 5: Skipping enum type updates** +- ❌ Problem: Frontend has `status: string`, backend changed to enum +- ✅ Solution: Check serializer for `choices` and model for `@property` methods ## Enum Change Detection Enum changes require checking beyond serializers: **When enum definitions change (`*/enums.py`):** -1. Identify which enums changed (e.g., `SubscriptionStatus`) -2. Search for TypeScript union types that should match -3. Update the union type with new/removed values -**When model @property methods change (`*/models.py`):** -1. If a `@property` that returns `EnumType.VALUE.name` is modified -2. Check if it now returns a different enum type -3. Update corresponding frontend type's field +Django enum changes often don't show up in serializer diffs. You must: + +1. **Detect enum file changes:** + ```bash + cd ../api && git diff ${LAST_COMMIT}..HEAD --name-only | grep enums.py + ``` + +2. **For each changed enum:** + - Read the new enum values + - Search for TypeScript types using this enum + - Update the union type **Example:** -```bash -# Detect enum file changes -git diff ${LAST_COMMIT}..HEAD ../subscriptions/enums.py +```python +# Backend: subscriptions/enums.py +class SubscriptionStatus(enum.Enum): + ACTIVE = "active" + CANCELLED = "cancelled" + PENDING = "pending" # NEW VALUE ADDED +``` + +```typescript +// Frontend: common/types/responses.ts (BEFORE) +export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' + +// Frontend: common/types/responses.ts (AFTER) +export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' | 'PENDING' +``` + +**When model @property methods change (`*/models.py`):** + +If a `@property` returns `EnumType.VALUE.name`, it serializes as a string union: -# If SubscriptionStatus enum changed: -# 1. Check new enum values -# 2. Update frontend: export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' | ... -# 3. Find all types using this enum (PartnerSubscription, CompanySummary, etc.) +```python +# Backend: subscriptions/models.py +class Subscription(models.Model): + @property + def status(self): + return SubscriptionStatus.ACTIVE.name # Returns 'ACTIVE' (string) ``` + +Frontend type should be: +```typescript +status: 'ACTIVE' | 'CANCELLED' | 'PENDING' // NOT string +``` + +**Detection process:** +1. Find changed model files with `@property` methods +2. Check if property returns `EnumType.VALUE.name` +3. Find all types with this field +4. Update to string union type From ac641db359c694f372a004a935db185592fdb522 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Tue, 4 Nov 2025 11:26:15 +0000 Subject: [PATCH 07/31] Update contexts --- frontend/.claude/commands/api-types-sync.md | 447 +++-------- frontend/.claude/commands/backend.md | 23 +- frontend/.claude/commands/check-staged.md | 11 +- frontend/.claude/commands/context.md | 11 +- frontend/.claude/commands/form.md | 21 +- frontend/.claude/commands/ui-patterns.md | 47 ++ frontend/.claude/context/api-integration.md | 261 ++++-- frontend/.claude/context/api-types-sync.md | 18 +- frontend/.claude/context/architecture.md | 27 +- .../.claude/context/backend-integration.md | 280 +++++++ frontend/.claude/context/feature-flags.md | 88 +++ frontend/.claude/context/forms.md | 187 +++-- frontend/.claude/context/git-workflow.md | 30 +- frontend/.claude/context/mobile.md | 748 ++++++++++++++++++ frontend/.claude/context/patterns.md | 396 +++++++++- frontend/.claude/context/quick-reference.md | 275 +++++++ frontend/.claude/context/ui-patterns.md | 239 +++--- frontend/.claude/scripts/sync-types-helper.py | 64 +- frontend/CLAUDE.md | 167 +--- 19 files changed, 2510 insertions(+), 830 deletions(-) create mode 100644 frontend/.claude/commands/ui-patterns.md create mode 100644 frontend/.claude/context/backend-integration.md create mode 100644 frontend/.claude/context/mobile.md create mode 100644 frontend/.claude/context/quick-reference.md diff --git a/frontend/.claude/commands/api-types-sync.md b/frontend/.claude/commands/api-types-sync.md index 5316bab76d8f..f52cf607ff1d 100644 --- a/frontend/.claude/commands/api-types-sync.md +++ b/frontend/.claude/commands/api-types-sync.md @@ -4,67 +4,12 @@ description: Sync frontend TypeScript types with backend Django serializers Synchronizes types in `common/types/responses.ts` and `common/types/requests.ts` with backend serializers in `../api`. -## Overview - -This command runs in **three phases** depending on the current state: - -1. **Phase 1 (First-time sync):** Build cache + validate 5-10 critical types (~10k tokens, 2-3 min) -2. **Phase 2 (Full validation):** Validate all remaining types (~50k tokens, 15-20 min) -3. **Phase 3 (Incremental sync):** Only validate types affected by backend changes (~5k-20k tokens) - -**Typical workflow:** -```bash -# First run - builds cache and validates critical types -/api-types-sync -# Output: "Phase 1 complete. Run /api-types-sync again for full validation." - -# Second run - validates all remaining types for 100% accuracy -/api-types-sync -# Output: "Phase 2 complete. All types now synced with backend." - -# Future runs - only sync types affected by backend changes -/api-types-sync -# Output: "Updated 3 types based on backend changes" -``` - -## How It Works - -**Frontend → Backend Mapping:** - -Frontend service files (`common/services/use*.ts`) define API calls with type annotations: - -```typescript -// Example: common/services/useProject.ts -export const projectService = baseService.injectEndpoints({ - endpoints: (builder) => ({ - getProject: builder.query({ - query: ({ id }) => `projects/${id}/`, - }), - updateProject: builder.mutation({ - query: ({ id, data }) => ({ - url: `projects/${id}/`, - method: 'PUT', - body: data, - }), - }), - }), -}) -``` - -**Mapping process:** -1. Extract: `Res['project']` → endpoint `projects/:id/` → method `GET` -2. Find Django URL: `projects/:id/` → maps to `ProjectViewSet.retrieve` -3. Find serializer: `ProjectViewSet.retrieve` uses `ProjectRetrieveSerializer` -4. Cache mapping: `"response_types.project"` → `"projects/serializers.py:ProjectRetrieveSerializer"` - ## Process -### 1. Detect Sync Mode - -**IMPORTANT**: This is a monorepo. To get the latest backend changes, merge `main` into your current branch: +### 1. Update Backend & Detect Changes ```bash -git merge origin/main +cd ../api && git checkout main && git pull origin main && cd - ``` Get last synced commit and current commit: @@ -74,107 +19,17 @@ LAST_COMMIT=$(python3 .claude/scripts/sync-types-helper.py get-last-commit 2>/de CURRENT_COMMIT=$(cd ../api && git rev-parse HEAD) ``` -Load cache metadata to detect sync mode: - -```bash -FULLY_VALIDATED=$(cat .claude/api-type-map.json 2>/dev/null | grep -o '"fullyValidated"[[:space:]]*:[[:space:]]*[a-z]*' | grep -o '[a-z]*$' || echo "false") -``` - -**Determine which mode:** - -- **Phase 1 (First-time sync):** `LAST_COMMIT` is empty → Build cache + validate critical types -- **Phase 2 (Full validation):** `LAST_COMMIT` exists but `FULLY_VALIDATED` is false → Validate all remaining types -- **Phase 3 (Incremental sync):** `LAST_COMMIT` exists and `FULLY_VALIDATED` is true → Check for changed serializers only - ---- - -### Phase 1: First-Time Sync (No LAST_COMMIT) - -**Goal:** Build cache and validate critical types to prove system works. - -**Steps:** - -1. **Scan frontend service files** (`common/services/use*.ts`): - - Extract all endpoint definitions with `Res['typeName']` and `Req['typeName']` - - Record endpoint URL and HTTP method for each type - -2. **Map types to backend serializers** (multi-step process): - - For each endpoint URL (e.g., `projects/:id/`): - - Find matching Django URL pattern in `../api/*/urls.py` - - Extract the ViewSet or View class name - - Read the ViewSet/View file to find the serializer class - - Record: `"serializer": "app/serializers.py:SerializerClassName"` - - If no serializer found, leave empty string `""` - -3. **Build cache structure** (`.claude/api-type-map.json`): -```json -{ - "_metadata": { - "lastSync": "2025-01-15T10:30:00Z", - "lastBackendCommit": "abc123...", - "fullyValidated": false, - "criticalTypesValidated": ["project", "environment", "organisation", "projectFlag", "identity"] - }, - "response_types": { - "project": { - "type": "project", - "serializer": "projects/serializers.py:ProjectRetrieveSerializer", - "endpoint": "projects/:id/", - "method": "GET" - } - }, - "request_types": { ... } -} -``` - -4. **Validate critical types only** (5-10 types to prove system works): - - Response types: `project`, `environment`, `organisation`, `projectFlags`/`projectFlag`, `identity` - - Request types: `updateProject`, `createEnvironment`, `updateOrganisation`, `createProjectFlag`, `createIdentities` - - For each critical type: - - Read backend serializer fields - - Read frontend type definition - - Compare and fix mismatches - - Record validated type names in `_metadata.criticalTypesValidated` - -5. **Save cache** with `fullyValidated: false` and current commit hash - -**Output:** "Phase 1 complete. Cache built with X response types and Y request types. Validated 5-10 critical types. Run `/api-types-sync` again to validate all remaining types." - ---- - -### Phase 2: Full Validation (LAST_COMMIT exists, FULLY_VALIDATED = false) - -**Goal:** Validate all remaining types to reach 100% accuracy. - -**Steps:** - -1. **Load existing cache** and get all types with valid serializers: -```bash -python3 .claude/scripts/sync-types-helper.py syncable-types response > /tmp/response_types.json -python3 .claude/scripts/sync-types-helper.py syncable-types request > /tmp/request_types.json -``` - -2. **For each syncable type:** - - Skip if already in `_metadata.criticalTypesValidated` (done in Phase 1) - - Read backend serializer fields - - Read frontend type definition - - Compare and fix mismatches - - Track progress (e.g., "Validating type 15/96: auditLogs") - -3. **Update cache metadata:** - - Set `fullyValidated: true` - - Update `lastBackendCommit` to current commit - - Update `lastSync` timestamp - -**Output:** "Phase 2 complete. Validated X response types and Y request types. All types now synced with backend." - ---- - -### Phase 3: Incremental Sync (LAST_COMMIT exists, FULLY_VALIDATED = true) +**First-Time Sync (No LAST_COMMIT):** -**Goal:** Validate only types affected by recent backend changes. +If `LAST_COMMIT` is empty, this is a first-time sync. You must: +1. Build the complete api-type-map.json by scanning ALL API endpoints +2. Use the Task tool with subagent_type=Explore to find all API service files in `common/api/` +3. For each endpoint, extract the serializer mappings and build the complete cache +4. Compare ALL frontend types with their backend serializers +5. Update any mismatches found +6. Save the complete cache with all type mappings -**Steps:** +**Incremental Sync (Has LAST_COMMIT):** If commits match, report "No changes" and exit. @@ -192,13 +47,11 @@ cd ../api && git diff ${LAST_COMMIT}..HEAD --name-only | grep -E "(serializers\. If no relevant files changed, update cache metadata with new commit and exit. -### 2. Identify & Update Affected Types (Phase 3 only) +### 2. Identify & Update Affected Types For each changed serializer file: -**A. Find affected types using helper script:** - -**NOTE:** The helper script only works when cache already exists. For Phase 1 (first-time sync), you must build the cache manually by scanning frontend service files. +**A. Find affected types:** ```bash python3 .claude/scripts/sync-types-helper.py types-to-sync response FILE ../api @@ -207,21 +60,14 @@ python3 .claude/scripts/sync-types-helper.py types-to-sync request FILE ../api **B. For each affected type:** -1. **Read backend serializer fields:** - ```bash - cd ../api && grep -A 30 "class SerializerName" FILE - ``` - Extract field names, types, and whether they're required/optional - -2. **Read frontend type definition:** - - Response types: `grep -A 15 "export type TypeName" common/types/responses.ts` - - Request types: `grep -A 15 "TypeName:" common/types/requests.ts` +1. Read backend serializer fields: `cd ../api && grep -A 30 "class SerializerName" FILE` +2. Read frontend type definition: + - Response: `grep -A 15 "export type TypeName" common/types/responses.ts` + - Request: `grep -A 15 "TypeName:" common/types/requests.ts` +3. Compare fields (names, types, required/optional) +4. If mismatch found, use Edit tool to fix frontend type -3. **Compare fields** (names, types, required/optional) - -4. **If mismatch found:** Use Edit tool to fix frontend type - -**C. Update cache metadata:** +**C. Update cache:** ```bash cat << 'EOF' | python3 .claude/scripts/sync-types-helper.py update-metadata @@ -232,25 +78,91 @@ cat << 'EOF' | python3 .claude/scripts/sync-types-helper.py update-metadata EOF ``` -### 3. Report Summary +### 3. Generate Services for New Serializers -Display based on phase: +**CRITICAL: After adding new types, immediately check if services need to be generated.** -**Phase 1:** -- Cache built: X response types, Y request types -- Critical types validated: [list] -- Next step: Run `/api-types-sync` again for full validation +For each NEW serializer (not just updated): -**Phase 2:** -- Validated: X response types, Y request types -- Types fixed: Z -- All types now synced with backend +**A. Detect new serializers:** +```bash +cd ../api && git diff ${LAST_COMMIT}..HEAD FILE | grep "^+class.*Serializer" +``` -**Phase 3:** -- Changed serializer files: [list] -- Updated response types: X (with details) -- Updated request types: Y (with details) -- Total types synced: Z +**B. Check if serializer is used in views:** +```bash +cd ../api && grep -r "SerializerName" apps/*/views.py apps/*/urls.py +``` + +**C. If serializer has an endpoint (found in views/urls):** + +1. Extract endpoint details from backend: + - URL pattern from `urls.py` + - HTTP method from view decorator/viewset + - Request/response types already added to `common/types/` + +2. **Generate service file immediately** using the same pattern as existing services: + - Create `common/services/use[TypeName].ts` + - Follow pattern from `common/services/useManageSubscription.ts` or similar + - Add proper imports, builder.query for GET, builder.mutation for POST/PUT/DELETE + - Add cache tags for invalidation + - Export hook (`useGet[TypeName]Query` or `useCreate[TypeName]Mutation`) + - Export helper function for non-component usage + +3. Update `.claude/api-type-map.json`: + - Add entry in `response_types` or `request_types` + - Include `type`, `service`, `endpoint`, `method`, `serializer` fields + - Update `totalTypes` count + +4. Run eslint --fix on new files: + ```bash + npx eslint --fix common/services/use[TypeName].ts common/types/responses.ts common/types/requests.ts + ``` + +**D. Service Generation Template:** +```typescript +import { Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' + +export const [name]Service = service + .enhanceEndpoints({ addTagTypes: ['[TagName]'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + get[Name]: builder.query({ + providesTags: ['[TagName]'], + query: (q) => ({ + url: `[endpoint-path]`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export async function get[Name]( + store: any, + data: Req['get[Name]'], + options?: Parameters[1], +) { + return store.dispatch([name]Service.endpoints.get[Name].initiate(data, options)) +} +// END OF FUNCTION_EXPORTS + +export const { + useGet[Name]Query, + // END OF EXPORTS +} = [name]Service +``` + +### 4. Report Summary + +Display: + +- Changed serializer files (list) +- Updated response types (count + details) +- Updated request types (count + details) +- **NEW services generated (count + file paths)** +- Total types synced ## Type Comparison Rules @@ -281,69 +193,26 @@ Display based on phase: ## Cache Structure -The cache maps frontend types to their backend serializers for efficient incremental syncing. - -**Field Definitions:** - -- `"key"`: The type key from `Res['key']` or `Req['key']` in frontend service files - - Example: `Res['project']` → key is `"project"` - - Example: `Req['createProject']` → key is `"createProject"` - -- `"type"`: Usually matches the key, but can differ for nested types - - In most cases: `"type": "project"` matches `"key": "project"` - -- `"serializer"`: Path to backend serializer in format `"app/serializers.py:ClassName"` - - Example: `"projects/serializers.py:ProjectRetrieveSerializer"` - - Empty string `""` if no serializer found (custom responses, view methods) - -- `"endpoint"`: The API endpoint URL from frontend service file - - Example: `"projects/:id/"` (include path params like `:id`) - -- `"method"`: HTTP method - - Response types: `"GET"`, `"POST"`, `"DELETE"` (any method that returns data) - - Request types: `"POST"`, `"PUT"`, `"PATCH"` (only methods that send body data) - -**Example cache:** - ```json { "_metadata": { - "lastSync": "2025-01-15T10:30:00Z", - "lastBackendCommit": "abc123def456", - "fullyValidated": false, - "criticalTypesValidated": ["project", "environment"] + "lastSync": "ISO timestamp", + "lastBackendCommit": "git hash" }, "response_types": { - "project": { - "type": "project", - "serializer": "projects/serializers.py:ProjectRetrieveSerializer", - "endpoint": "projects/:id/", + "key": { + "type": "TypeName", + "serializer": "apps/path/serializers.py:SerializerName", + "endpoint": "api/endpoint/", "method": "GET" - }, - "projects": { - "type": "projects", - "serializer": "projects/serializers.py:ProjectListSerializer", - "endpoint": "projects/", - "method": "GET" - }, - "identityTraits": { - "type": "identityTraits", - "serializer": "", - "endpoint": "/environments/:environmentId/identities/:identity/traits/", - "method": "GET", - "note": "Custom response, no serializer" } }, "request_types": { - "createProject": { - "serializer": "projects/serializers.py:ProjectSerializer", - "endpoint": "projects/", - "method": "POST" - }, - "updateProject": { - "serializer": "projects/serializers.py:ProjectSerializer", - "endpoint": "projects/:id/", - "method": "PUT" + "key": { + "type": "TypeName", + "serializer": "apps/path/serializers.py:SerializerName", + "endpoint": "api/endpoint/", + "method": "POST|PUT|PATCH" } } } @@ -351,87 +220,31 @@ The cache maps frontend types to their backend serializers for efficient increme ## Notes -- **Only sync types with actual Django serializers** - Skip custom responses, ChargeBee types, view methods -- **Request types exclude GET/DELETE** - These methods don't send body data, so no request type needed -- **File uploads need manual verification** - MULTIPART_FORM endpoints may not have serializers -- **Helper script requires cache** - `sync-types-helper.py` functions only work after cache is built in Phase 1 - -## Common Pitfalls - -**Pitfall 1: Using helper script during Phase 1** -- ❌ Problem: Running `syncable-types` before cache exists -- ✅ Solution: Build cache manually by scanning frontend service files first - -**Pitfall 2: Forgetting path parameters** -- ❌ Problem: Including `:id`, `:projectId` in request/response types -- ✅ Solution: Path params go in the URL, not the request/response body - -**Pitfall 3: Assuming type = key** -- ❌ Problem: Using "type" field as cache lookup key -- ✅ Solution: The JSON key (e.g., `"project"`) is the lookup key, `"type"` field is metadata - -**Pitfall 4: Stopping after Phase 1** -- ❌ Problem: Only critical types validated, 80%+ of types unchecked -- ✅ Solution: Run `/api-types-sync` again for Phase 2 full validation - -**Pitfall 5: Skipping enum type updates** -- ❌ Problem: Frontend has `status: string`, backend changed to enum -- ✅ Solution: Check serializer for `choices` and model for `@property` methods +- Only sync types with actual Django serializers +- Request types exclude GET/DELETE endpoints (no body validation) +- File uploads (MULTIPART_FORM) need manual verification ## Enum Change Detection Enum changes require checking beyond serializers: -**When enum definitions change (`*/enums.py`):** +**When enum definitions change (`apps/*/enums.py`):** +1. Identify which enums changed (e.g., `SubscriptionStatus`, `MailQueueStatus`) +2. Search for TypeScript union types that should match +3. Update the union type with new/removed values -Django enum changes often don't show up in serializer diffs. You must: - -1. **Detect enum file changes:** - ```bash - cd ../api && git diff ${LAST_COMMIT}..HEAD --name-only | grep enums.py - ``` - -2. **For each changed enum:** - - Read the new enum values - - Search for TypeScript types using this enum - - Update the union type +**When model @property methods change (`apps/*/models.py`):** +1. If a `@property` that returns `EnumType.VALUE.name` is modified +2. Check if it now returns a different enum type +3. Update corresponding frontend type's field **Example:** -```python -# Backend: subscriptions/enums.py -class SubscriptionStatus(enum.Enum): - ACTIVE = "active" - CANCELLED = "cancelled" - PENDING = "pending" # NEW VALUE ADDED -``` - -```typescript -// Frontend: common/types/responses.ts (BEFORE) -export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' - -// Frontend: common/types/responses.ts (AFTER) -export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' | 'PENDING' -``` - -**When model @property methods change (`*/models.py`):** - -If a `@property` returns `EnumType.VALUE.name`, it serializes as a string union: - -```python -# Backend: subscriptions/models.py -class Subscription(models.Model): - @property - def status(self): - return SubscriptionStatus.ACTIVE.name # Returns 'ACTIVE' (string) -``` +```bash +# Detect enum file changes +cd ../api && git diff ${LAST_COMMIT}..HEAD apps/subscriptions/enums.py -Frontend type should be: -```typescript -status: 'ACTIVE' | 'CANCELLED' | 'PENDING' // NOT string +# If SubscriptionStatus enum changed: +# 1. Check new enum values +# 2. Update frontend: export type SubscriptionStatus = 'ACTIVE' | 'CANCELLED' | ... +# 3. Find all types using this enum (PartnerSubscription, CompanySummary, etc.) ``` - -**Detection process:** -1. Find changed model files with `@property` methods -2. Check if property returns `EnumType.VALUE.name` -3. Find all types with this field -4. Update to string union type diff --git a/frontend/.claude/commands/backend.md b/frontend/.claude/commands/backend.md index fc43ed73ae87..0a5e2615c984 100644 --- a/frontend/.claude/commands/backend.md +++ b/frontend/.claude/commands/backend.md @@ -2,22 +2,13 @@ description: Search the backend codebase for endpoint details --- -Search the `../api/` Django backend codebase for the requested endpoint. +Search the `../hoxtonmix-api` codebase for the requested endpoint. Look for: -1. **URLs**: `../api//urls.py` - Route definitions and URL patterns -2. **Views**: `../api//views.py` - ViewSets and API logic -3. **Serializers**: `../api//serializers.py` - Request/response schemas -4. **Models**: `../api//models.py` - Data models -5. **Permissions**: Check for permission classes and authentication requirements +1. Route definitions (URL path) +2. HTTP method (GET, POST, PUT, DELETE) +3. Request validation schema (request body/params types) +4. Response structure (what data is returned) +5. Authentication/authorization requirements -Common Django apps in Flagsmith: -- `organisations/` - Organization management -- `projects/` - Project management -- `environments/` - Environment configuration -- `features/` - Feature flags -- `segments/` - User segments -- `users/` - User management -- `audit/` - Audit logging - -API base URL: `/api/v1/` +If the endpoint isn't found, check swagger docs: https://staging-api.hoxtonmix.com/api/v3/docs/ diff --git a/frontend/.claude/commands/check-staged.md b/frontend/.claude/commands/check-staged.md index a24a0482dfca..0ddcb6f9bd8f 100644 --- a/frontend/.claude/commands/check-staged.md +++ b/frontend/.claude/commands/check-staged.md @@ -1,9 +1,4 @@ ---- -description: Run linting on staged files ---- - -Run linting on all currently staged files, similar to pre-commit hooks: - -1. Run `npx lint-staged --allow-empty` to lint only staged files -2. Report any linting issues found +Run TypeScript checking and linting on all currently staged files, similar to pre-commit hooks. Steps: +1. Run `npm run check:staged` to typecheck and lint only staged files +2. Report any type errors or linting issues found 3. If errors exist, offer to fix them diff --git a/frontend/.claude/commands/context.md b/frontend/.claude/commands/context.md index 55ecb9086ebd..1fa71ec3b7d0 100644 --- a/frontend/.claude/commands/context.md +++ b/frontend/.claude/commands/context.md @@ -4,13 +4,8 @@ description: Load detailed context files for specific topics Available context files in `.claude/context/`: -1. **api-integration.md** - API integration workflow, RTK Query patterns, service creation -2. **api-types-sync.md** - Type synchronization between Django backend and TypeScript frontend -3. **architecture.md** - Environment config, tech stack, project structure -4. **feature-flags.md** - Flagsmith feature flag usage patterns (dogfooding) -5. **forms.md** - Custom form patterns, InputGroup components, validation -6. **git-workflow.md** - Git workflow, branching, PR process -7. **patterns.md** - Common code patterns, API services, error handling, linting -8. **ui-patterns.md** - UI patterns, confirmation dialogs, modals +1. **api-integration.md** - API integration workflow, Redux setup, cross-platform patterns +2. **forms.md** - Form patterns with Yup + Formik, form components reference +3. **architecture.md** - Environment config, tech stack, additional rules Which context would you like to explore? diff --git a/frontend/.claude/commands/form.md b/frontend/.claude/commands/form.md index 8879a75f6693..401f0d744d6f 100644 --- a/frontend/.claude/commands/form.md +++ b/frontend/.claude/commands/form.md @@ -1,22 +1,15 @@ --- -description: Create a new form component +description: Create a new form using Yup + Formik pattern --- -**Note**: This codebase does NOT use Formik or Yup. - Create a form following the standard pattern: -1. Use React class component or functional component with `useState` -2. Use `InputGroup` component from global scope with `title`, `value`, `onChange` -3. For RTK Query mutations, use `useCreateXMutation()` hooks -4. Handle loading and error states -5. Use `Utils.preventDefault(e)` in submit handler -6. Use `toast()` for success/error messages -7. Use `closeModal()` to dismiss modal forms +1. Use `useFormik` hook with `validationSchema` from Yup +2. Always include `validateOnMount: true` +3. Use `validateForm` utility from `project/utils/forms/validateForm` in submit handler +4. Use `InputGroup` component with `touched` and `error` props +5. For special inputs (date, phone, select), use `component` prop on InputGroup -Examples to reference: -- `web/components/SamlForm.js` - Class component form -- `web/components/modals/CreateSegmentRulesTabForm.tsx` - Functional component form -- Search for `InputGroup` usage in `/web/components/` for more examples +Reference: `/examples/forms/ComprehensiveFormExample.tsx` Context file: `.claude/context/forms.md` diff --git a/frontend/.claude/commands/ui-patterns.md b/frontend/.claude/commands/ui-patterns.md new file mode 100644 index 000000000000..9cc05e1fae51 --- /dev/null +++ b/frontend/.claude/commands/ui-patterns.md @@ -0,0 +1,47 @@ +# UI Patterns & Best Practices + +## Confirmation Dialogs + +**NEVER use `window.confirm`** - Always use the `openConfirm` function from `components/base/Modal`. + +### Correct Usage + +```typescript +import { openConfirm } from 'components/base/Modal' + +// Signature: openConfirm(title, body, onYes, onNo?, challenge?) +openConfirm( + 'Delete Partner', + 'Are you sure you want to delete this partner?', + async (closeModal) => { + const res = await deleteAction() + if (!res.error) { + toast(null, 'Partner deleted successfully') + closeModal() // Always call closeModal to dismiss the dialog + } + }, +) +``` + +### Parameters +- `title: string` - Dialog title +- `body: ReactNode` - Dialog content (can be JSX) +- `onYes: (closeModal: () => void) => void` - Callback when user confirms +- `onNo?: () => void` - Optional callback when user cancels +- `challenge?: string` - Optional challenge text user must type to confirm + +### Key Points +- The `onYes` callback receives a `closeModal` function +- Always call `closeModal()` when the action completes successfully +- Can be async - use `async (closeModal) => { ... }` + +## Backend Integration + +### Always Run API Types Sync Before API Work + +When using `/api` to generate new API services, the command automatically runs `/api-types-sync` first to: +1. Pull latest backend changes (`git pull` in `../hoxtonmix-api`) +2. Sync frontend types with backend serializers +3. Ensure types are up-to-date before generating new services + +This prevents type mismatches and ensures consistency. diff --git a/frontend/.claude/context/api-integration.md b/frontend/.claude/context/api-integration.md index d7e41a8295e1..ed716004a781 100644 --- a/frontend/.claude/context/api-integration.md +++ b/frontend/.claude/context/api-integration.md @@ -1,44 +1,191 @@ # API Integration Guide -## Workflow - -### Preferred: Manual Service Creation - -The `npx ssg` CLI requires interactive input that cannot be automated. Instead, **manually create RTK Query services** following the patterns in existing service files. - -1. **Check backend code** in `../api` for endpoint details - - Search backend directly using Grep or Glob tools - - Common locations: `*/views.py`, `*/urls.py`, `*/serializers.py` - - Check API documentation or Swagger UI if available in your environment - -2. **Define types** in `common/types/requests.ts` and `responses.ts` - - Add to `Req` type for request parameters - - Add to `Res` type for response data - - Match backend serializer field names and types - -3. **Create service file** in `common/services/use{Entity}.ts` - - Follow pattern from existing services (e.g., `usePartner.ts`, `useCompany.ts`) - - Use `service.enhanceEndpoints()` and `.injectEndpoints()` - - Define queries with `builder.query()` - - Define mutations with `builder.mutation()` - - Set appropriate `providesTags` and `invalidatesTags` for cache management - - Export hooks: `useGetEntityQuery`, `useCreateEntityMutation`, etc. - -4. **CRITICAL: Update `.claude/api-type-map.json`** to register the new endpoint - - Add entry in `request_types` section with: - - `type`: TypeScript type signature (e.g., `{id: string, name: string}`) - - `serializer`: Backend serializer path (e.g., `entities/serializers.py:EntitySerializer`) - - `endpoint`: API endpoint pattern (e.g., `/api/v1/entities/{id}/`) - - `method`: HTTP method (GET, POST, PUT, DELETE) - - `service`: Frontend service file path (e.g., `common/services/useEntity.ts`) - - Add entry in `response_types` section (if needed) - - This enables the `/api-types-sync` command to track type changes - -5. **Verify implementation** - - Check URL matches backend endpoint exactly - - Verify HTTP method (GET, POST, PUT, DELETE) - - Ensure request body structure matches backend serializer - - Test with actual API calls +## Overview + +This project uses **RTK Query** (Redux Toolkit Query) for all API calls. The workflow is optimized for type safety and automatic sync with backend Django serializers. + +## Quick Start: Adding a New Endpoint (Complete Example) + +This example shows how to add a new endpoint for fetching company invoices (a real implementation from the codebase). + +### Step 1: Check Backend API + +```bash +cd ../hoxtonmix-api +git fetch +git log --oneline origin/feat/your-branch -n 10 +git show COMMIT_HASH:apps/customers/urls.py | grep -A 5 "invoice" +git show COMMIT_HASH:apps/customers/views.py | grep -A 20 "def get_company_invoices" +``` + +**Backend endpoint found:** `GET /customers/companies/{company_id}/invoices` + +### Step 2: Add Request Type + +**File:** `common/types/requests.ts` + +```typescript +export type Req = { + // ... existing types + getCompanyInvoices: { + company_id: string + } +} +``` + +### Step 3: Add RTK Query Endpoint + +**File:** `common/services/useInvoice.ts` + +```typescript +export const invoiceService = service + .enhanceEndpoints({ addTagTypes: ['Invoice'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + // Add new endpoint + getCompanyInvoices: builder.query< + Res['invoices'], + Req['getCompanyInvoices'] + >({ + providesTags: [{ id: 'LIST', type: 'Invoice' }], + query: (req) => ({ + url: `customers/companies/${req.company_id}/invoices`, + }), + transformResponse(res: InvoiceSummary[]) { + return res?.map((v) => ({ ...v, date: v.date * 1000 })) + }, + }), + }), + }) + +export const { + useGetCompanyInvoicesQuery, // Export new hook + // END OF EXPORTS +} = invoiceService +``` + +### Step 4: Use in Component + +```typescript +import { useGetCompanyInvoicesQuery } from 'common/services/useInvoice' + +const MyComponent = () => { + const { subscriptionDetail } = useDefaultSubscription() + const companyId = subscriptionDetail?.company_id + + const { data: invoices, error, isLoading } = useGetCompanyInvoicesQuery( + { company_id: `${companyId}` }, + { skip: !companyId } // Skip if no company ID + ) + + if (isLoading) return + if (error) return {error} + + return ( +
+ {invoices?.map(inv =>
{inv.id}
)} +
+ ) +} +``` + +### Step 5: Run Linter + +```bash +npx eslint --fix common/types/requests.ts common/services/useInvoice.ts +``` + +**Done!** The endpoint is now integrated and ready to use. + +## Primary Workflow: Automatic via `/api-types-sync` + +**ALWAYS start with `/api-types-sync`** when working with APIs. This command: +1. Pulls latest backend changes from `../hoxtonmix-api` +2. Detects new/changed Django serializers +3. Updates TypeScript types in `common/types/` +4. **Automatically generates RTK Query services** for new endpoints +5. Updates `.claude/api-type-map.json` for tracking + +### When `/api-types-sync` Auto-Generates Services + +The command detects: +- New serializer classes in `apps/*/serializers.py` +- Associated views/endpoints in `apps/*/views.py` and `apps/*/urls.py` +- Creates complete service files with proper RTK Query patterns +- Registers everything in the type map + +### When to Manually Use `/api` + +Only use the `/api` command when: +- Creating a service for an existing backend endpoint that was missed +- `/api-types-sync` didn't auto-generate (rare edge case) +- Implementing a frontend-only service (non-backend endpoint) + +**In 95% of cases, `/api-types-sync` handles service generation automatically.** + +## Manual Service Creation (Rare Cases) + +If you need to manually create a service (follow template in `.claude/commands/api.md`): + +1. **Identify backend endpoint** + - Use `/backend ` to search backend codebase + - Check `apps/*/serializers.py`, `apps/*/views.py`, `apps/*/urls.py` + - Or swagger docs: https://staging-api.hoxtonmix.com/api/v3/docs/ + +2. **Define types** in `common/types/` + - Response: `export type EntityName = { field: type }` in `responses.ts` + - Add to `Res` type: `entityName: EntityName` + - Request: `getEntityName: { param: type }` in `requests.ts` + +3. **Create service** `common/services/use{Entity}.ts` + - Use `builder.query` for GET, `builder.mutation` for POST/PUT/DELETE + - Configure cache tags: `providesTags`, `invalidatesTags` + - Export hooks: `useGetEntityQuery`, `useCreateEntityMutation` + +4. **Register in type map** `.claude/api-type-map.json` + - Add to `response_types` or `request_types` with full metadata + - Increment `_metadata.totalTypes` + +5. **Run linter** + ```bash + npx eslint --fix common/services/use*.ts common/types/*.ts + ``` + +## Type Sync Architecture + +### `.claude/api-type-map.json` + +Cache file mapping frontend → backend for type sync: + +```json +{ + "_metadata": { + "lastBackendCommit": "a73427688...", + "totalTypes": 48 + }, + "response_types": { + "entityName": { + "type": "EntityType", + "service": "common/services/useEntity.ts", + "endpoint": "entities/{id}", + "method": "GET", + "serializer": "apps/entities/serializers.py:EntitySerializer" + } + } +} +``` + +### Backend → Frontend Type Mapping + +| Django Type | TypeScript | +|------------|------------| +| `CharField` | `string` | +| `IntegerField` | `number` | +| `BooleanField` | `boolean` | +| `DateTimeField` | `string` (ISO) | +| `required=False` | `field?: type` | +| `many=True` | `Type[]` | +| Enum/Choices | `'A' \| 'B'` | ### Example Service Structure @@ -81,15 +228,30 @@ export const { ## Finding Backend Endpoints +### Quick Reference - Common Customer Portal API Patterns + +**Base URL Pattern**: `/api/v3/` (varies by resource) + +| Resource | List | Detail | Actions | +|----------|------|--------|---------| +| **Mail** | `GET /mailbox/mails` | `GET /mailbox/mails/{id}` | `POST /mailbox/mails/{id}/scan`, `POST /mailbox/mails/{id}/forward` | +| **Offers** | `GET /offers/` | `GET /offers/{id}/` | N/A | +| **Account** | `GET /account` | N/A | N/A | +| **KYC** | `GET /kyc/steps` | `GET /kyc/status` | `POST /kyc/verify`, `GET /kyc/link` | +| **Subscriptions** | N/A | `GET /customers/companies/{id}/hosted-page` | N/A | +| **Addresses** | N/A | N/A | `POST /addresses`, `PATCH /addresses/{id}` | +| **Payment** | N/A | N/A | `POST /topup`, `POST /payment-sources` | + ### Search Strategy -1. **Search backend directly**: Use Grep/Glob tools to search the `../api` directory -2. **Check URL patterns**: Look in `../api/*/urls.py` -3. **Check ViewSets**: Look in `../api/*/views.py` +1. **Use `/backend` slash command**: `/backend ` searches backend codebase +2. **Check URL patterns**: Look in `../hoxtonmix-api/apps//urls.py` + - Common apps: `mailbox`, `customers`, `kyc`, `offers`, `subscriptions`, `checkout` +3. **Check ViewSets**: Look in `../hoxtonmix-api/apps//views.py` 4. **Common file download pattern**: - Backend returns PDF/file with `Content-Disposition: attachment; filename=...` - Use `responseHandler` in RTK Query to handle blob downloads - - Check existing service files for examples + - See `common/services/useMailDownload.ts` for example ### File Download Pattern @@ -115,7 +277,7 @@ The utility automatically: - **Redux Toolkit + RTK Query** for all API calls - Store: `common/store.ts` with redux-persist - Base service: `common/service.ts` -- **Use `npx ssg` CLI to generate new services** (optional but helpful) + The `npx ssg` CLI requires interactive input that cannot be automated. Instead, **manually create RTK Query services** following the patterns in existing service files. - **IMPORTANT**: When implementing API logic, prefer implementing it in the RTK Query service layer (using `transformResponse`, `transformErrorResponse`, etc.) rather than in components. This makes the logic reusable across the application. ## Error Handling Patterns @@ -159,8 +321,9 @@ const handleRetry = () => refetch() ### 401 Unauthorized Handling **Automatic logout on 401** is handled in `common/service.ts`: -- Check the service.ts file for specific 401 handling logic -- Typically debounced to prevent multiple logout calls +- All 401 responses (except email confirmation) trigger logout +- Debounced to prevent multiple logout calls +- Uses the logout endpoint from the service ### Backend Error Response Format @@ -179,7 +342,7 @@ if ('data' in err && err.data?.detail) { } ``` -## Platform Patterns +## Cross-Platform Pattern -- Web and common code are separated in the directory structure -- Check existing patterns in the codebase for platform-specific implementations +- Web (`/project/api.ts`) and Mobile (`/mobile/app/api.ts`) implement same interface from `common/api-common.ts` +- Example: `getApi().loginRedirect()` uses Next.js Router on web, React Native Navigation on mobile diff --git a/frontend/.claude/context/api-types-sync.md b/frontend/.claude/context/api-types-sync.md index 36cce1a4ebea..f146427da5fd 100644 --- a/frontend/.claude/context/api-types-sync.md +++ b/frontend/.claude/context/api-types-sync.md @@ -1,6 +1,6 @@ # API Type Synchronization -Frontend TypeScript types in `common/types/responses.ts` mirror backend Django serializers from `../api`. +Frontend TypeScript types in `common/types/responses.ts` mirror backend Django serializers from `../hoxtonmix-api`. ## Type Mapping (Django → TypeScript) @@ -128,10 +128,14 @@ This cache enables: 1. Check cache first (`.claude/api-type-map.json`) 2. Search service files: `grep -r ": TypeName" common/services/` -3. Search backend: `grep -r "SerializerName" ../api/*/views.py` +3. Search backend: `grep -r "SerializerName" ../hoxtonmix-api/apps/*/views.py` **Before syncing:** -Always merge main into the branch to update backend code to its latest version +Always update backend repository to latest: + +```bash +cd ../hoxtonmix-api && git checkout main && git pull origin main && cd - +``` ## Enum Dependency Tracking @@ -170,7 +174,7 @@ The `_metadata.enum_dependencies` section tracks the relationship between Django 3. **Locate the Django enum definition:** ```bash # Use the django_enum path - cat ../api/subscriptions/enums.py | grep -A 10 "class SubscriptionStatus" + cat ../hoxtonmix-api/apps/subscriptions/enums.py | grep -A 10 "class SubscriptionStatus" ``` 4. **Update all affected types when enum changes:** @@ -196,13 +200,13 @@ Enum changes require additional tracking: **Example workflow using enum dependencies:** ```bash # 1. Check what changed since last sync -git diff LAST_COMMIT..HEAD --name-only | grep -E "(enums|models|serializers)\.py" +cd ../hoxtonmix-api && git diff LAST_COMMIT..HEAD --name-only | grep -E "(enums|models)\.py" -# 2. If subscriptions/enums.py changed, check the api-type-map.json: +# 2. If apps/subscriptions/enums.py changed, check the api-type-map.json: cat .claude/api-type-map.json | jq '._metadata.enum_dependencies.mappings.SubscriptionStatus' # 3. Read the updated enum values: -cat ../api/subscriptions/enums.py | grep -A 10 "class SubscriptionStatus" +cat ../hoxtonmix-api/apps/subscriptions/enums.py | grep -A 10 "class SubscriptionStatus" # 4. Update the TypeScript union type in common/types/responses.ts # 5. Check all types listed in used_in_types to ensure they're still correct diff --git a/frontend/.claude/context/architecture.md b/frontend/.claude/context/architecture.md index cc1aad9e47fc..c9d7567915dc 100644 --- a/frontend/.claude/context/architecture.md +++ b/frontend/.claude/context/architecture.md @@ -1,28 +1,27 @@ # Architecture & Configuration -## Environment Configuration +## Environment & Whitelabelling -- Config files: `env/project_.js` -- Available environments: `dev`, `prod`, `staging`, `local`, `selfhosted`, `e2e` -- Project config: `common/project.js` (imports from env files) -- Override: `ENV=local npm run dev` or `ENV=staging npm run dev` +- Config files: `common/env/project__.js` +- `ENV`: `dev` or `prod` (default: `dev`) +- `NAME`: Brand name like `hoxtonmix-portal` (default: `hoxtonmix-portal`) +- Override: `API_URL=https://my-api.com/api/v1/ npm run dev` ## Key Technologies -- React 16.14 + TypeScript + Bootstrap 5.2.2 -- Redux Toolkit + RTK Query (API state management) -- Flux stores (legacy state management) -- Webpack 5 + Express dev server +- Next.js 13.1.5 + TypeScript + Bootstrap 5.3.3 +- Preact (aliased as React for smaller bundle) +- AWS Amplify (Cognito auth) - Sentry (error tracking) -- Flagsmith (feature flags - this project IS Flagsmith, dogfooding its own platform) +- Flagsmith (feature flags - see `feature-flags.md` for usage) ## Additional Rules -- **TypeScript/ESLint**: Build may ignore some errors, but always run linting on modified files -- **Web-specific code**: Goes in `/web/` directory (not `/common`) +- **TypeScript/ESLint**: Build ignores errors (`ignoreBuildErrors: true`) +- **Web-specific utilities**: Go in `/project/` (not `/common`) - **Redux Persist**: Whitelist in `common/store.ts` -- **Imports**: Always use path aliases (`common/*`, `components/*`, `project/*`) - NO relative imports +- **Layout**: Default in `pages/_app.tsx` (`