diff --git a/frontend/.claude/commands/api-types-sync.md b/frontend/.claude/commands/api-types-sync.md new file mode 100644 index 000000000000..5316bab76d8f --- /dev/null +++ b/frontend/.claude/commands/api-types-sync.md @@ -0,0 +1,437 @@ +--- +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: + +```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) +``` + +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) + +**Goal:** Validate only types affected by recent backend changes. + +**Steps:** + +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 (Phase 3 only) + +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. + +```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:** + ```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) + +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 +{ + "lastSync": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "lastBackendCommit": "CURRENT_COMMIT_HASH" +} +EOF +``` + +### 3. Report Summary + +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 + +**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 + +**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 + +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"] + }, + "response_types": { + "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": { + "createProject": { + "serializer": "projects/serializers.py:ProjectSerializer", + "endpoint": "projects/", + "method": "POST" + }, + "updateProject": { + "serializer": "projects/serializers.py:ProjectSerializer", + "endpoint": "projects/:id/", + "method": "PUT" + } + } +} +``` + +## 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 + +## Enum Change Detection + +Enum changes require checking beyond serializers: + +**When enum definitions change (`*/enums.py`):** + +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:** +```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) +``` + +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 diff --git a/frontend/.claude/commands/api.md b/frontend/.claude/commands/api.md new file mode 100644 index 000000000000..8d55451ec1e9 --- /dev/null +++ b/frontend/.claude/commands/api.md @@ -0,0 +1,13 @@ +--- +description: Generate a new RTK Query API service +--- + +IMPORTANT: Before starting, always run `/api-types-sync` to ensure frontend types are in sync with the backend. + +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/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..a24a0482dfca --- /dev/null +++ b/frontend/.claude/commands/check-staged.md @@ -0,0 +1,9 @@ +--- +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/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/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 new file mode 100644 index 000000000000..c1f8841630b7 --- /dev/null +++ b/frontend/.claude/commands/feature-flag.md @@ -0,0 +1,5 @@ +--- +description: Create a feature flag +--- + +1. Create a feature flag using the context defined in `.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..d7e41a8295e1 --- /dev/null +++ b/frontend/.claude/context/api-integration.md @@ -0,0 +1,185 @@ +# 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 + +### Example Service Structure + +```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 + +### 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` +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 + +### File Download Pattern + +**Use the reusable utility function:** + +```typescript +import { handleFileDownload } from 'common/utils/fileDownload' + +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` +- **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 + +```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`: +- Check the service.ts file for specific 401 handling logic +- Typically debounced to prevent multiple logout calls + +### 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) +} +``` + +## 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 new file mode 100644 index 000000000000..cc1aad9e47fc --- /dev/null +++ b/frontend/.claude/context/architecture.md @@ -0,0 +1,28 @@ +# Architecture & Configuration + +## Environment Configuration + +- 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 +- 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/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 + +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 new file mode 100644 index 000000000000..e5743668ba34 --- /dev/null +++ b/frontend/.claude/context/feature-flags.md @@ -0,0 +1,235 @@ +# Feature Flags (Flagsmith) + +## Overview + +The project uses Flagsmith for feature flag management. Flags allow you to control feature visibility without deploying code changes. + +## Project Configuration + +Configuration files: +- **Staging**: `common/project.js` (look for `flagsmith` property) +- **Production**: `common/project_prod_*.js` (look for `flagsmith` property) + +To find your organization and project IDs, use the MCP tools (see "Managing Feature Flags" section below). + +## Setup + +- **Provider**: `components/FeatureFlagProvider.tsx` wraps the app +- **Configuration**: Flagsmith environment ID in `common/project.ts` +- **User Context**: Flags are user-specific, identified by email + +## Usage Pattern + +```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
+ )} + + ) +} +``` + +## Best Practices + +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`) + +## Examples + +### Simple Feature Toggle +```typescript +const flags = useFlags(['new_dashboard']) +if (flags.new_dashboard?.enabled) { + return +} +return +``` + +### 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 +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 + +Step 2: List projects in your organization +Tool: mcp__flagsmith-admin-api__flagsmith_admin_api_list_projects_in_organization +Parameters: {"org_id": } + +Step 3: Find project by matching repository name to project name +``` + +#### 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} +``` + +#### 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 +``` + +#### 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} +``` + +### Best Practices + +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 + +### Environment-Specific Configuration + +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 +// Traits are user-specific preferences, not feature toggles +const flags = useFlags([], ['dark_mode']) +const isDarkMode = flags.dark_mode // Returns boolean/string/number directly + +// Setting a trait +const flagsmith = useFlagsmith() +flagsmith.setTrait('dark_mode', true) +``` + +## Reference Implementation + +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 + +See `components/DarkModeHandler.tsx` for an example of trait usage. diff --git a/frontend/.claude/context/forms.md b/frontend/.claude/context/forms.md new file mode 100644 index 000000000000..67ae20cd7521 --- /dev/null +++ b/frontend/.claude/context/forms.md @@ -0,0 +1,103 @@ +# Form Patterns (Custom Components) + +**IMPORTANT**: This codebase does NOT use Formik or Yup. Forms are built with custom form components. + +## Standard Pattern for Forms + +```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 +} + +const MyForm: FC = () => { + const [formData, setFormData] = useState({ + name: '', + email: '', + }) + const [error, setError] = useState(null) + + const setFieldValue = (key: keyof FormData, value: any) => { + setFormData((prev) => ({ ...prev, [key]: value })) + } + + const isValid = !!formData.name && Utils.isValidEmail(formData.email) + + const handleSubmit = (e: FormEvent) => { + e.preventDefault() + if (isValid) { + // Make API call + } + } + + return ( +
+ setFieldValue('name', e)} + /> + setFieldValue('email', e)} + /> + + + ) +} +``` + +## Form Components (in `web/components/base/forms/`) + +- **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 + +## Validation Patterns + +- **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 + +## Example with RTK Query Mutation + +```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) + } +} +``` + +**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 new file mode 100644 index 000000000000..f4ef15634f00 --- /dev/null +++ b/frontend/.claude/context/git-workflow.md @@ -0,0 +1,37 @@ +# Git Workflow + +## Pre-Commit Checking Strategy + +Before creating commits, always lint staged files to catch errors early: + +```bash +npx lint-staged --allow-empty +``` + +Or use the slash command: +``` +/check +``` + +This runs ESLint with auto-fix on staged files only, mimicking pre-commit hooks. + +## Available Scripts + +- `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 + +- 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 new file mode 100644 index 000000000000..29f526452f68 --- /dev/null +++ b/frontend/.claude/context/patterns.md @@ -0,0 +1,223 @@ +# 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' +``` + +## 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 + +- **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 + +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 [createMail, { isLoading, error }] = useCreateMailMutation() + +const handleSubmit = async () => { + try { + const result = await createMail(data).unwrap() + // Success - result contains the response + 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.error(errMsg) + } else { + // SerializedError + toast.error(err.message || 'An error occurred') + } + } +} +``` + +### Query Refetching + +```typescript +const { data, refetch } = useGetMailQuery({ 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 { entityService } from 'common/services/useEntity' + +export const clearEntityCache = () => { + getStore().dispatch( + entityService.util.invalidateTags([{ type: 'Entity', id: 'LIST' }]) + ) +} +``` + +### Automatic Invalidation + +Cache invalidation is handled automatically through RTK Query tags: + +```typescript +// Mutation invalidates the list +createEntity: builder.mutation({ + invalidatesTags: [{ type: 'Entity', 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 = { + getEntity: { + id: string + } + createEntity: { + name: string + } + // END OF TYPES +} + +// common/types/responses.ts +export type Res = { + entity: Entity + entities: Entity[] + // END OF TYPES +} +``` + +### Shared Types + +**Shared types:** + +Types used across multiple request/response types should be defined separately and imported. + +## SSG CLI Usage (Optional) + +You can 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) + +**Note**: Manual service creation is also acceptable - follow patterns from existing services. + +## Linting + +Always run ESLint on files you modify: + +```bash +npx eslint --fix +``` + +Or run it on all files: + +```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..c9e6794fb35d --- /dev/null +++ b/frontend/.claude/scripts/sync-types-helper.py @@ -0,0 +1,231 @@ +#!/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/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..63b49ce3f778 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,137 @@ +# CLAUDE.md + +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 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 (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 +