diff --git a/frontend/.claude/commands/api.md b/frontend/.claude/commands/api.md new file mode 100644 index 000000000000..a786ca166a1a --- /dev/null +++ b/frontend/.claude/commands/api.md @@ -0,0 +1,12 @@ +--- +description: Generate a new RTK Query API service +--- + + + +Generate a new API service. Follow these steps: + +1. Go through the process mentioned in `.claude/context/api-integration.md` +2. 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..547e3a1edf6c --- /dev/null +++ b/frontend/.claude/commands/backend.md @@ -0,0 +1,14 @@ +--- +description: Search the backend codebase for endpoint details +--- + +Search the `../api` codebase for the requested endpoint. + +Look for: +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 + +If the endpoint isn't found, check swagger docs: https://staging.flagsmith.com/api/v1/docs/ diff --git a/frontend/.claude/commands/check-staged.md b/frontend/.claude/commands/check-staged.md new file mode 100644 index 000000000000..4da239a8b1b5 --- /dev/null +++ b/frontend/.claude/commands/check-staged.md @@ -0,0 +1,8 @@ +--- +description: Run type checking and linting on staged files +--- + +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/context.md b/frontend/.claude/commands/context.md new file mode 100644 index 000000000000..c67309252944 --- /dev/null +++ b/frontend/.claude/commands/context.md @@ -0,0 +1,12 @@ +--- +description: Load detailed context files for specific topics +--- + +Available context files in `.claude/context/`: + +1. **api-integration.md** - API integration workflow, Redux setup, cross-platform patterns +2. **architecture.md** - Environment config, tech stack, additional rules +3. **feature-flags/** - Flagsmith feature flags (usage + MCP workflows) +4. **patterns/** - Code patterns (API, mobile) + +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..42372209279d --- /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/` diff --git a/frontend/.claude/commands/form.md.draft b/frontend/.claude/commands/form.md.draft new file mode 100644 index 000000000000..401f0d744d6f --- /dev/null +++ b/frontend/.claude/commands/form.md.draft @@ -0,0 +1,15 @@ +--- +description: Create a new form using Yup + Formik pattern +--- + +Create a form following the standard pattern: + +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 + +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..7abce1f34fa0 --- /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 `../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 new file mode 100644 index 000000000000..3ba3fd95997b --- /dev/null +++ b/frontend/.claude/context/api-integration.md @@ -0,0 +1,220 @@ +# API Integration Guide + +## 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. + +**Finding Backend Endpoints**: See `.claude/context/backend-integration.md` for strategies to locate and understand backend API endpoints. + +## 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: Find Backend Endpoint + +Use strategies from `backend-integration.md` to locate the endpoint. + +**Backend endpoint found:** `GET /organisations/{organisation_id}/invoices` + +### Step 2: Add Request Type + +**File:** `common/types/requests.ts` + +```typescript +export type Req = { + // ... existing types + getCompanyInvoices: { + organisation_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: `organisations/${organisation_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( + { organisation_id: `${companyId}` }, + { skip: !organisation_id } // 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. + +## 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 `../api/apps/*/serializers.py`, `../api/apps/*/views.py`, `apps/*/urls.py` + - Or swagger docs: https://api.flagsmith.com/api/v1/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 + ``` + +### 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 + +```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 +``` + +See `common/services/useAuditLog.ts` for a complete example. + +## State Management + +- **Redux Toolkit + RTK Query** for all API calls +- Store: `common/store.ts` with redux-persist +- Base service: `common/service.ts` + 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 + +### RTK Query Mutations + +```typescript +const [createMail, { isLoading, error }] = useCreateMailMutation() + +const handleSubmit = async () => { + try { + const result = await createThing(data).unwrap() + toast('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(err.message || 'An error occurred', 'danger') + toast(err.message || 'An error occurred') + } + } +} +``` + +### RTK Query Queries + +```typescript +const { data, error, isLoading, refetch } = useThing({ id: '123' }) + +// Display error in UI, it won't render if error is undefined +return {error} + +// Retry on error +const handleRetry = () => refetch() +``` 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/backend-integration.md b/frontend/.claude/context/backend-integration.md new file mode 100644 index 000000000000..37015a63a10a --- /dev/null +++ b/frontend/.claude/context/backend-integration.md @@ -0,0 +1,277 @@ +# Backend Integration Guide + +## Backend Repository Structure + +The backend API is located at `../api` (sibling directory to this frontend repo). + +### Key Backend Directories + +``` +../api/ +├── apps/ +│ ├── projects/ # Projects and features +│ ├── environments/ # Environment management +│ ├── features/ # Feature flags +│ ├── segments/ # User segments +│ ├── organisations/ # Organization management +│ ├── users/ # User management +│ ├── permissions/ # Access control +│ └── audit/ # Audit logs +└── tests/ # Test files mirror apps/ structure +``` + +## Finding Backend Endpoints + +### Method 1: Search by Feature Branch + +When a feature is on a specific branch: + +```bash +cd ../api +git fetch +git log --oneline origin/feat/your-branch -n 10 +``` + +Look for the relevant commit, then examine the changes: + +```bash +# See what files were changed +git show COMMIT_HASH --stat + +# View specific file at that commit +git show COMMIT_HASH:apps/customers/urls.py + +# Search for specific pattern in file +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" +``` + +### Method 2: Search Current Codebase + +If the feature is already merged or on main: + +```bash +cd ../api + +# Search for URL patterns +grep -r "path.*features" apps/*/urls.py + +# Search for view functions +grep -r "def get.*feature" apps/*/views.py + +# Search for serializers +grep -r "class.*FeatureSerializer" apps/*/serializers.py +``` + +### Method 3: Use `/backend` Command + +From the frontend, use the `/backend` slash command: + +``` +/backend feature endpoint +``` + +This searches the backend codebase for relevant code. + +## Understanding Backend Code + +### 1. URL Patterns (`urls.py`) + +Django URL patterns define the API routes: + +```python +# apps/features/urls.py +path( + "", + get_feature, + name="feature-detail", +), +``` + +**Maps to:** `GET /api/v1/features/{feature_id}` + +### 2. View Functions (`views.py`) + +Views handle the HTTP request/response: + +```python +# apps/features/views.py +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def get_feature(request, feature_id, *args, **kwargs): + feature = Feature.objects.get(id=feature_id) + serializer = FeatureSerializer(feature) + return Response(status=status.HTTP_200_OK, data=serializer.data) +``` + +Key things to note: +- HTTP method: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` +- Authentication required: Usually JWT or Token based +- Permissions: `@permission_classes` +- Path parameters: `feature_id` from URL +- Return format: Usually `Response(data=...)` + +### 3. Service Layer (`services.py`) + +Business logic is in service classes: + +```python +# apps/features/services.py (if exists) +# Note: Flagsmith backend may not always use a service layer +# Business logic often lives directly in views or models +``` + +### 4. Serializers (`serializers.py`) + +Serializers define the data structure: + +```python +# apps/features/serializers.py +class FeatureSerializer(serializers.ModelSerializer): + id = serializers.IntegerField() + name = serializers.CharField() + description = serializers.CharField() + type = serializers.CharField() + default_enabled = serializers.BooleanField() + created_date = serializers.DateTimeField() +``` + +## API Response Patterns + +### List Endpoints + +``` +GET /api/v3/resource/ +Response: [{ id: 1, ... }, { id: 2, ... }] +``` + +### Detail Endpoints + +``` +GET /api/v3/resource/{id}/ +Response: { id: 1, name: "...", ... } +``` + +### Nested Resources + +``` +GET /api/v3/admin/partners/{partner_id}/customers +Response: [{ id: 1, name: "...", ... }, { id: 2, name: "...", ... }] +``` + +### Action Endpoints + +``` +POST /api/v3/admin/partners/{id}/activate +Body: { commission_rate: 10.5 } +Response: { success: true, partner: {...} } +``` + +## Common Partner Portal Endpoints + +| Resource | Endpoint | Method | Notes | +|----------|----------|--------|-------| +| **Partners** | `/admin/partners` | GET | List all partners | +| | `/admin/partners/{id}` | GET | Partner detail | +| | `/admin/partners/{id}` | PUT/PATCH | Update partner | +| | `/admin/partners/{id}/activate` | POST | Activate partner | +| | `/admin/partners/{id}/deactivate` | POST | Deactivate partner | +| **Customers** | `/admin/partners/{id}/customers` | GET | List partner's customers | +| | `/admin/customers/{id}` | GET | Customer detail | +| **Commissions** | `/admin/partners/{id}/commissions` | GET | Partner commission history | +| | `/admin/commissions/{id}` | GET | Commission detail | +| **Payouts** | `/admin/partners/{id}/payouts` | GET | Partner payout history | +| | `/admin/payouts/{id}` | GET | Payout detail | +| | `/admin/payouts` | POST | Create new payout | +| **Offers** | `/offers` | GET | List available offers | +| | `/offers/{id}` | GET | Offer detail | +| **Dashboard** | `/admin/partners/{id}/stats` | GET | Partner statistics/metrics | + +## Testing Backend Changes Locally + +### 1. Run Backend Locally + +```bash +cd ../api +# Follow backend README for setup +python manage.py runserver +``` + +### 2. Point Frontend to Local Backend + +```bash +# In frontend repo +API_URL=http://localhost:8000/api/v3/ npm run dev +``` + +### 3. Check API Response + +```bash +# Test endpoint directly +curl -H "Authorization: Bearer YOUR_TOKEN" \ + http://localhost:8000/api/v3/admin/partners/123 +``` + +## Common Patterns + +### Permission Checks + +Backend often checks: +- `@permission_classes([IsAuthenticated])` - Must be logged in +- `@permission_classes([IsAdminUser])` - Admin user required +- `@has_partner_permission([PartnerPermissionType.VIEW_CUSTOMERS])` - Specific permission required + +Frontend should mirror these checks using: +```typescript +const { user } = useAuth() +const isAdmin = user?.is_admin +const canViewCustomers = user?.permissions?.includes('VIEW_CUSTOMERS') +``` + +### Pagination + +Backend may return paginated responses: +```json +{ + "count": 100, + "next": "https://api.../resource/?page=2", + "previous": null, + "results": [...] +} +``` + +Frontend should handle with: +```typescript +query: (req) => ({ + url: `resource`, + params: { page: req.page, page_size: 20 } +}) +``` + +### Error Responses + +Backend typically returns: +```json +{ + "detail": "Error message", + "code": "ERROR_CODE" +} +``` + +Frontend should extract `detail` for user-facing error messages. + +## Checklist for New Endpoints + +- [ ] Find endpoint in backend code (views.py, urls.py) +- [ ] Note HTTP method (GET, POST, etc.) +- [ ] Note path parameters (`{id}`, `{company_id}`) +- [ ] Note query parameters (`?page=1`) +- [ ] Check authentication requirements +- [ ] Check permission requirements +- [ ] Find or create matching serializer for response type +- [ ] Check if response needs transformation (e.g., timestamps) +- [ ] Test endpoint directly with curl or Postman +- [ ] Add request type to `common/types/requests.ts` +- [ ] Add/extend RTK Query service in `common/services/` +- [ ] Use hook in component with loading/error handling +- [ ] Run linter on modified files diff --git a/frontend/.claude/context/feature-flags/feature-flags.md b/frontend/.claude/context/feature-flags/feature-flags.md new file mode 100644 index 000000000000..ec7c5030d92a --- /dev/null +++ b/frontend/.claude/context/feature-flags/feature-flags.md @@ -0,0 +1,190 @@ +# Feature Flags (Flagsmith) + +## Overview + +The project uses Flagsmith for feature flag management. Flags allow you to control feature visibility without deploying code changes. + +**IMPORTANT:** Only implement feature flags when explicitly requested by the user. By default, implement features directly without flags. Feature flags add complexity and should only be used when there's a specific need for: +- Progressive rollouts to specific users +- A/B testing different implementations +- Ability to quickly disable a feature in production +- Gradual feature adoption across user segments + +If the user doesn't mention feature flags, implement features directly. + +## Project Configuration + +Configuration files: +- **Staging**: `common/project.js` (look for `flagsmith` property) +- **Production**: `common/project_prod_*.js` (look for `flagsmith` property) + +### Environment-Specific Configuration + +See `project-config.local.md` for API keys, project IDs, and environment IDs. + +If the file doesn't exist, copy from `project-config.local.md.example` and fill in your values. + +## 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 + +### Standard Pattern (Recommended) + +**This project uses `Utils.getFlagsmithHasFeature()` for all feature flag checks.** + +```typescript +// In any component (functional or class) +const MyComponent = () => { + const isFeatureEnabled = Utils.getFlagsmithHasFeature('feature_name') + + return ( + <> + {isFeatureEnabled && ( +
Feature content here
+ )} + + ) +} +``` + +```typescript +// In class components (use in render method) +class MyClassComponent extends Component { + render() { + const isFeatureEnabled = Utils.getFlagsmithHasFeature('feature_name') + + return ( + <> + {isFeatureEnabled &&
Feature content here
} + + ) + } +} +``` + +### Alternative: Direct flagsmith Hook (Not Used in This Project) + +For reference, the project also supports direct `useFlags` hook, but **Utils.getFlagsmithHasFeature is preferred**: + +```typescript +import { useFlags } from 'flagsmith/react' + +const MyComponent = () => { + const flags = useFlags(['feature_name']) + const isFeatureEnabled = flags.feature_name?.enabled + + return ( + <> + {isFeatureEnabled &&
Feature content here
} + + ) +} +``` + +## Best Practices + +1. **Use Utils.getFlagsmithHasFeature**: This is the standard pattern used throughout the codebase +2. **Declare flags early in render**: Define flag variables at the top of your render method or component +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`) +7. **For feature values**: Use `Utils.getFlagsmithJSONValue('flag_name', defaultValue)` to get JSON values + +## Examples + +### Simple Feature Toggle +```typescript +const isNewDashboard = Utils.getFlagsmithHasFeature('new_dashboard') +if (isNewDashboard) { + return +} +return +``` + +### Progressive Feature Rollout Pattern + +Common pattern: Add new features behind flags while keeping existing functionality intact. + +```typescript +import { useState } from 'react' +import { Tabs } from 'components/base/forms/Tabs' + +const MyPage = () => { + const newFeatureEnabled = Utils.getFlagsmithHasFeature('new_feature') + const [activeTab, setActiveTab] = useState(0) + + return ( +
+

Section Title

+ {newFeatureEnabled ? ( + +
+
+
+ ) : ( + + )} +
+ ) +} +``` + +### Table Column with Flag +```typescript +const canShowActions = Utils.getFlagsmithHasFeature('show_actions') + +return ( + + + + + {canShowActions && } + + + + {data.map(item => ( + + + {canShowActions && ( + + )} + + ))} + +
NameActions
{item.name}
+) +``` + +### 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) +``` + +## Managing Feature Flags via MCP + +For MCP tools and workflows, see [mcp-workflows.md](mcp-workflows.md). + +## 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/feature-flags/mcp-workflows.md b/frontend/.claude/context/feature-flags/mcp-workflows.md new file mode 100644 index 000000000000..7eebc605eeaf --- /dev/null +++ b/frontend/.claude/context/feature-flags/mcp-workflows.md @@ -0,0 +1,179 @@ +# Flagsmith MCP Workflows + +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. + +## ⚠️ CRITICAL: Project Verification Before Creating Flags + +**STOP! Before creating ANY feature flag, follow this mandatory checklist:** + +1. ✅ **Verify the correct project**: Check `project-config.local.md` for the correct project ID +2. ✅ **Read configuration**: Check `common/project.js` to see the Flagsmith API key +3. ✅ **Confirm with MCP**: Run `mcp__flagsmith__list_project_environments` with the project_id from config to verify +4. ✅ **Create ONCE**: Only call `mcp__flagsmith__create_feature` ONCE with the correct project_id + +**NEVER:** +- ❌ Create flags in multiple projects to "try" which one is correct +- ❌ Guess the project ID without verification +- ❌ Create duplicate flags + +**Why this matters:** Creating flags in the wrong project pollutes other Flagsmith projects with incorrect flags that don't belong there. This is a critical error. + +## Known Limitations + +**IMPORTANT:** Published feature versions cannot be modified via the Flagsmith API. + +- After creating a flag with MCP (`mcp__flagsmith__create_feature`), the flag is created but disabled by default +- To enable/disable the flag in specific environments, you must use the Flagsmith web UI at https://app.flagsmith.com +- This is a Flagsmith API limitation, not a tooling issue +- The MCP can create flags, but enabling/disabling must be done manually via the UI + +**Workflow:** +1. Create flag via MCP → ✅ Automated +2. Implement code with `useFlags()` → ✅ Automated +3. Enable flag in staging/production → ❌ Manual (via Flagsmith UI) + +When documenting completion, always inform the user that step 3 requires manual action via the web UI. + +## CRITICAL: When User Says "Create a Feature Flag" + +**When the user requests to create a feature flag, you MUST:** + +1. ✅ **Actually create the flag in Flagsmith** using `mcp__flagsmith__create_feature` +2. ✅ **Implement the frontend code** that uses the flag with `useFlags()` +3. ✅ **Return the flag details** (ID, name, project) to confirm creation + +**DO NOT:** +- ❌ Only implement the code without creating the flag +- ❌ Assume the flag already exists +- ❌ Assume the user will create it manually + +**This is a two-part task:** +- **Backend (Flagsmith)**: Create the flag entity in Flagsmith +- **Frontend (Code)**: Write code that checks the flag with `useFlags()` + +Both parts are required when "create a feature flag" is requested. + +## Standard Workflow Example + +``` +User: "Add a download button, create this under a feature flag download_invoices" + +Step 1: Create flag in Flagsmith + - Use mcp__flagsmith__list_organizations (if needed) + - Use mcp__flagsmith__list_projects_in_organization (find project) + - Use mcp__flagsmith__create_feature with name "download_invoices" + - Confirm flag ID and status to user + +Step 2: Implement code + - Add useFlags(['download_invoices']) to component + - Wrap button with flag check: {flags.download_invoices?.enabled && } + - Test that code compiles + +Step 3: Report completion + - Confirm flag created in Flagsmith (with ID) + - Confirm code implementation complete +``` + +## Available MCP Tools + +The MCP provides tools prefixed with `mcp__flagsmith-admin-api__` for managing feature flags. + +### 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_request`** - 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 diff --git a/frontend/.claude/context/feature-flags/project-config.local.md.example b/frontend/.claude/context/feature-flags/project-config.local.md.example new file mode 100644 index 000000000000..cc1436e67556 --- /dev/null +++ b/frontend/.claude/context/feature-flags/project-config.local.md.example @@ -0,0 +1,25 @@ +# Flagsmith Project Configuration + +Copy this file to `project-config.local.md` and fill in your values. + +**Flagsmith Organization:** (ID: ) + +**Project: (ID: )** +- **Production Environment** + - ID: + - API Key: `` + - Requires approval for changes (minimum_change_request_approvals: 1) +- **Staging Environment** + - ID: + - API Key: `` + - No approval required for changes + +**CRITICAL: ALWAYS USE PROJECT ID WHEN CREATING FEATURE FLAGS FOR THIS FRONTEND.** + +**BEFORE CREATING ANY FEATURE FLAG, YOU MUST:** +1. Read `common/project.js` and verify the `flagsmith` API key +2. Use `mcp__flagsmith__list_project_environments` with project_id: to confirm the API key matches +3. **ONLY** create the flag in Project ID () +4. **NEVER** create flags in multiple projects or guess which project to use + +**Creating flags in the wrong project is a critical error that pollutes other projects with incorrect flags.** diff --git a/frontend/.claude/context/forms.md.draft b/frontend/.claude/context/forms.md.draft new file mode 100644 index 000000000000..416d229cb983 --- /dev/null +++ b/frontend/.claude/context/forms.md.draft @@ -0,0 +1,130 @@ +# Form Patterns (Yup + Formik) + +## Standard Pattern for ALL Forms + +```typescript +import { useFormik } from 'formik' +import * as yup from 'yup' +import { validateForm } from 'project/utils/forms/validateForm' + +const schema = yup.object().shape({ + name: yup.string().required('Name is required'), +}) + +const MyForm = () => { + const { errors, touched, values, handleChange, handleBlur, handleSubmit, setTouched } = useFormik({ + initialValues: { name: '' }, + onSubmit: async (values) => { /* API call */ }, + validationSchema: schema, + validateOnMount: true, + }) + + const onSubmit = async (e) => { + e.preventDefault() + const isValid = await validateForm(errors, setTouched) + if (isValid) handleSubmit() + } + + return ( +
+ + + + ) +} +``` + +## Form Components (in `components/base/forms/`) + +- **InputGroup**: Standard wrapper - pass `touched` and `error` props +- **DatePicker**, **PhoneInput**, **Select**: Use with `component` prop on InputGroup +- **Radio**, **Checkbox**, **Switch**: Boolean/choice inputs + +**Reference**: See `/examples/forms/ComprehensiveFormExample.tsx` + +## Form Spacing & Layout + +### Standard Form Layout Structure + +```tsx +
+
+

Form Title

+ +
+ + + +
+
+
+``` + +### Spacing Classes + +Use Bootstrap's spacing scale consistently: + +- **Between InputGroups**: `d-flex flex-column gap-4` (24px vertical gap) +- **Section header margin**: `h3 className='mb-4 pb-3 border-bottom'` (24px bottom) +- **Two-column rows**: `d-flex gap-4 mb-4` with `flex-1` per column +- **Button rows**: `d-flex justify-content-end gap-2 mt-3` (8px between buttons, 16px top margin) +- **Error messages**: `mb-5` when standalone (48px) + +### Multi-Section Forms + +For forms with multiple sections: + +```tsx +
+
{/* 48px between sections */} + + {/* Section 1 */} +
+

Section Title

+
+ + +
+
+ + {/* Section 2 */} +
+

Another Section

+
+ +
+
+ + {/* Actions */} +
+ + +
+
+
+``` + +### Bootstrap Gap/Margin Scale + +- `gap-2` = 0.5rem (8px) +- `gap-3` = 1rem (16px) +- `gap-4` = 1.5rem (24px) ← Use for InputGroup spacing +- `gap-5` = 3rem (48px) ← Use for section separation + +- `mb-3` = 1rem (16px) +- `mb-4` = 1.5rem (24px) ← Use for section headers +- `mb-5` = 3rem (48px) ← Use for standalone errors/content + +### Key Files with Examples + +- `/components/examples/forms/ComprehensiveFormExample.tsx` - Full pattern with sections +- `/components/ChangeAccountInformation.tsx` - Account form spacing +- `/components/ChangeContact.tsx` - Contact form spacing +- `/components/whatsapp/CreateEditNumber.tsx` - Modal form pattern 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/api.md b/frontend/.claude/context/patterns/api.md new file mode 100644 index 000000000000..b0f7a3e6da93 --- /dev/null +++ b/frontend/.claude/context/patterns/api.md @@ -0,0 +1,207 @@ +# 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/hooks/useInfiniteScroll' +import { useGetMailQuery } from 'common/services/useMail' + +const MailList = ({ subscription_id }: Props) => { + const { + data, + isLoading, + isFetching, + loadMore, + refresh, + searchItems, + } = useInfiniteScroll( + useGetMailQuery, + { subscription_id, page_size: 20 }, + ) + + return ( + } + refreshFunction={refresh} + pullDownToRefresh + > + {data?.results.map(item => )} + + ) +} +``` + +## 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 { mailItemService } from 'common/services/useMailItem' + +export const clearMailCache = () => { + getStore().dispatch( + mailItemService.util.invalidateTags([{ type: 'MailItem', id: 'LIST' }]) + ) +} +``` + +### Automatic Invalidation + +Cache invalidation is handled automatically through RTK Query tags: + +```typescript +// Mutation invalidates the list +createMail: builder.mutation({ + invalidatesTags: [{ type: 'Mail', 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 = { + getMail: PagedRequest<{ + subscription_id: string + q?: string + }> + createMail: { + id: string + content: string + } + // END OF TYPES +} + +// common/types/responses.ts +export type Res = { + mail: PagedResponse + mailItem: MailItem + // 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) diff --git a/frontend/.claude/context/patterns/index.md b/frontend/.claude/context/patterns/index.md new file mode 100644 index 000000000000..f47d84d40664 --- /dev/null +++ b/frontend/.claude/context/patterns/index.md @@ -0,0 +1,167 @@ +# Common Code Patterns + +> For API patterns, see [api.md](api.md). For mobile patterns, see [mobile.md](mobile.md). + +## Complete Feature Implementation Example + +This end-to-end example shows how to add tabs with a new API endpoint (real implementation from the codebase). + +**Requirements:** Add a "Top-Up" invoices tab to the account-billing page, pulling from a new backend endpoint. + +### Step 1: Check Backend API + +```bash +cd ../api +git fetch +git show COMMIT_HASH:apps/customers/urls.py | grep "invoice" +# Found: path("companies//invoices", get_company_invoices) +``` + +### Step 2: Add Request Type + +**File:** `common/types/requests.ts` + +```typescript +export type Req = { + // ... existing types + getCompanyInvoices: { + company_id: string + } +} +``` + +### Step 3: Extend RTK Query Service + +**File:** `common/services/useInvoice.ts` + +```typescript +export const invoiceService = service + .enhanceEndpoints({ addTagTypes: ['Invoice'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + 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, + // END OF EXPORTS +} = invoiceService +``` + +### Step 4: Create Table Component + +**File:** `components/project/tables/CompanyInvoiceTable.tsx` + +```typescript +import { useGetCompanyInvoicesQuery } from 'common/services/useInvoice' +import { useDefaultSubscription } from 'common/services/useDefaultSubscription' + +const CompanyInvoiceTable: FC = () => { + const { subscriptionDetail } = useDefaultSubscription() + const companyId = subscriptionDetail?.company_id + + const { data: invoices, error, isLoading } = useGetCompanyInvoicesQuery( + { company_id: `${companyId}` }, + { skip: !companyId } + ) + + if (isLoading) return + if (error) return {error} + + return ( + + + {/* table structure */} +
+
+ ) +} +``` + +### Step 5: Add Tabs to Page + +**File:** `pages/account-billing.tsx` + +```typescript +import { useState } from 'react' +import { Tabs } from 'components/base/forms/Tabs' +import InvoiceTable from 'components/project/tables/InvoiceTable' +import CompanyInvoiceTable from 'components/project/tables/CompanyInvoiceTable' + +const AccountAndBilling = () => { + const [activeTab, setActiveTab] = useState(0) + + return ( +
+

Invoices

+ +
+
+
+
+ ) +} +``` + +### Step 6: Run Linter + +```bash +npx eslint --fix common/types/requests.ts common/services/useInvoice.ts \ + components/project/tables/CompanyInvoiceTable.tsx pages/account-billing.tsx +``` + +**Done!** The feature is now live with tabs and proper error handling. + +### Optional: Add Feature Flag + +If you need to gate this feature behind a feature flag (only when explicitly requested), see `feature-flags/` for the pattern. + +## 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' +``` + +## 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/context/patterns/mobile.md.draft b/frontend/.claude/context/patterns/mobile.md.draft new file mode 100644 index 000000000000..9938eef138e4 --- /dev/null +++ b/frontend/.claude/context/patterns/mobile.md.draft @@ -0,0 +1,169 @@ +# Mobile-Specific Patterns + +## Toast Notifications + +Mobile uses `react-native-toast-message`: + +```typescript +// ✅ Correct for mobile +import Toast from 'react-native-toast-message' + +Toast.show({ text1: 'Success message' }) +Toast.show({ text1: 'Error message', type: 'error' }) + +// ❌ Wrong - this is web only +import { toast } from 'components/base/Toast' +toast('Success', 'Message') +``` + +## Icons + +Mobile uses specific SVG component imports, not a generic Icon component: + +```typescript +// ✅ Correct for mobile +import EditIcon from 'components/svgs/EditIcon' +import RemoveIcon from 'components/svgs/RemoveIcon' +import IconButton from 'components/IconButton' + +} onPress={handleEdit} /> +} onPress={handleDelete} /> + +// ❌ Wrong - this is web only +import Icon from 'project/Icon' + +``` + +## Confirm Dialogs + +Mobile uses Alert or the utility function: + +```typescript +// ✅ Correct for mobile +import openConfirm from 'components/utility-components/openConfirm' + +openConfirm( + 'Delete Item', + 'Are you sure you want to delete this item?', + () => handleDelete(), +) + +// ❌ Wrong - this is web only +import { openConfirm } from 'components/base/Modal' +openConfirm('Title', , callback) +``` + +## Component Naming + +Follow existing mobile patterns: + +```typescript +// Mobile components in mobile/app/components/ +- MailTable.tsx (not MailList) +- TeamTable.tsx +- WhatsAppTable.tsx +- CreateEditNumber.tsx (modal for creating/editing) +- Custom modals use CustomModal component +``` + +## Modal-Based CRUD Pattern + +For list components with create/edit capabilities, use a single modal for both operations: + +```typescript +import { useState } from 'react' +import CreateEditModal from './CreateEditModal' + +const MyTable: FC = () => { + const [modalOpen, setModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const handleEdit = (item: ItemType) => { + setEditing(item) + setModalOpen(true) + } + + const handleCreate = () => { + setEditing(null) // null = create mode + setModalOpen(true) + } + + return ( + <> + + + {items?.map((item) => ( + + {!item.cancelled_on && ( + handleEdit(item)} + icon={} + /> + )} + + ))} + + setModalOpen(false)} + initial={editing} // null for create, data for edit + /> + + ) +} +``` + +**Modal component pattern:** +```typescript +type Props = { + isOpen: boolean + onClose: () => void + initial?: ItemType | null // null/undefined = create, data = edit + onSuccess?: () => void +} + +const CreateEditModal: FC = ({ isOpen, onClose, initial }) => { + const [createItem] = useCreateItemMutation() + const [updateItem] = useUpdateItemMutation() + + const [form, setForm] = useState({ + field1: initial?.field1 || '', + field2: initial?.field2 || '', + }) + + // Reset form when modal opens + useEffect(() => { + if (isOpen && initial) { + setForm({ field1: initial.field1, field2: initial.field2 }) + } else if (isOpen && !initial) { + setForm({ field1: '', field2: '' }) + } + }, [isOpen, initial]) + + const handleSubmit = async () => { + if (initial) { + await updateItem({ id: initial.id, ...form }) + } else { + await createItem(form) + } + onClose() + } + + return ( + + {/* form fields */} + + ) +} +``` + +**Key principles:** +- Single modal handles both create and edit +- `initial` prop determines mode (null = create, data = edit) +- Reset form state when modal opens +- Different mutation based on mode +- Button text changes based on mode diff --git a/frontend/.claude/context/quick-reference.md b/frontend/.claude/context/quick-reference.md new file mode 100644 index 000000000000..a0bb051f4d9a --- /dev/null +++ b/frontend/.claude/context/quick-reference.md @@ -0,0 +1,269 @@ +# Quick Reference Guide + +## Common Tasks Checklist + +### Finding a Page Component + +**All page components are in `web/components/pages/`** - no need to search extensively. + +Examples: +- Features page: `web/components/pages/FeaturesPage.js` +- Project settings: `web/components/pages/ProjectSettingsPage.js` +- Environment settings: `web/components/pages/EnvironmentSettingsPage.tsx` +- Users: `web/components/pages/UsersPage.tsx` + +To find a page: +```bash +ls web/components/pages/ | grep -i +``` + +### Adding a New API Endpoint + +- [ ] Check backend for endpoint (see `backend-integration.md`) +- [ ] Add request type to `common/types/requests.ts` +- [ ] Add/extend RTK Query service in `common/services/use*.ts` +- [ ] Export hook from service +- [ ] Use hook in component with proper loading/error handling +- [ ] Run linter: `npx eslint --fix ` + +### Creating a New Table Component + +- [ ] Create in `components/project/tables/` +- [ ] Fetch data with RTK Query hook +- [ ] Handle loading state with `` +- [ ] Handle error state with `` +- [ ] Wrap table in `` +- [ ] Use responsive classes: `d-none d-md-table-cell` + +### Adding Tabs to a Page + +- [ ] Import `{ Tabs }` from `components/base/forms/Tabs` +- [ ] Add `useState` for active tab +- [ ] Pass `value`, `onChange`, and `tabLabels` props +- [ ] Wrap each tab content in `
` + +### Implementing Feature Flags (When Requested) + +Only use feature flags when explicitly requested by the user. + +- [ ] Create flag in Flagsmith (use `/feature-flag` command or MCP tools) +- [ ] In render method or component, call `Utils.getFlagsmithHasFeature('flag_name')` +- [ ] Store result in a variable (e.g., `const isEnabled = Utils.getFlagsmithHasFeature('flag_name')`) +- [ ] Use conditional rendering: `{isEnabled && }` +- [ ] Provide fallback when flag is disabled +- [ ] Enable flag manually via Flagsmith UI at https://app.flagsmith.com + +## File Locations + +| Purpose | Location | Example | +|---------|----------|---------| +| API Services | `common/services/use*.ts` | `useEnvironment.ts`, `useFeature.ts` | +| Request Types | `common/types/requests.ts` | API request interfaces | +| Response Types | `common/types/responses.ts` | API response interfaces | +| Table Components | `components/project/tables/` | User tables, data grids | +| **Page Components** | **`web/components/pages/`** | **`FeaturesPage.js`, `ProjectSettingsPage.js`** | +| Card Components | `components/project/cards/` | Summary cards, info cards | +| Base UI Components | `components/base/` | Buttons, forms, inputs | +| Feature Flags Context | `.claude/context/feature-flags/` | Flagsmith integration guide | +| Backend API | `../api/` | Flagsmith backend API | + +## Common Components for Messages + +| Component | Location | Usage | +|-----------|----------|-------| +| InfoMessage | `components/InfoMessage` | Info alerts/messages | +| ErrorMessage | `components/base/Messages` | Error states | +| SuccessMessage | `components/base/Messages` | Success notifications | +| Loader | `components/base/Loader` | Loading states | +| Tooltip | `components/Tooltip` | Hover tooltips | +| Button | `components/base/forms/Button` | Standard buttons | + +## Common Imports + +### RTK Query +```typescript +import { service } from 'common/service' +import { Req } from 'common/types/requests' +import { Res } from 'common/types/responses' +``` + +### UI Components +```typescript +import { Tabs } from 'components/base/forms/Tabs' +import Loader from 'components/base/Loader' +import { ErrorMessage } from 'components/base/Messages' +``` + +### Hooks +```typescript +import { useDefaultSubscription } from 'common/services/useDefaultSubscription' +import { useState } from 'react' +``` + +### Utils +```typescript +import { Format } from 'common/utils/format' +import dayjs from 'dayjs' +``` + +## Backend API Structure + +``` +../api/apps/ +├── projects/ # Projects and features +├── environments/ # Environment management +├── features/ # Feature flags +├── segments/ # User segments +├── users/ # User management +├── organisations/ # Organization management +├── permissions/ # Access control +└── audit/ # Audit logs +``` + +## Common Backend Endpoints + +See `/backend` slash command to search the backend codebase for specific endpoints. + +## RTK Query Patterns + +> For comprehensive RTK Query patterns, examples, and service structure, see [api-integration.md](api-integration.md). + +**Quick reference:** +- `builder.query` for GET requests with `providesTags` +- `builder.mutation` for POST/PUT/DELETE with `invalidatesTags` +- Use `{ skip: !id }` to conditionally skip queries + +## Component Patterns + +### Loading State +```typescript +if (isLoading) { + return ( +
+ +
+ ) +} +``` + +### Error State +```typescript +if (error) return {error} +``` + +### Feature Flag Check +```typescript +const isFeatureEnabled = Utils.getFlagsmithHasFeature('flag_name') +if (isFeatureEnabled) { + // Show new feature +} +``` + +## Slash Commands + +| Command | Purpose | +|---------|---------| +| `/api` | Generate new API service | +| `/backend ` | Search backend codebase | +| `/feature-flag` | Create feature flag | +| `/form` | Generate form with Yup + Formik | +| `/check` | Run type checking and linting | +| `/context` | Load specific context files | + +## Common Utilities + +### Date Formatting +```typescript +import dayjs from 'dayjs' +dayjs(timestamp).format('DD MMM YY') +``` + +### Money Formatting +```typescript +import { Format } from 'common/utils/format' +Format.money(amountInCents) // e.g., "$12.34" +Format.camelCase('pending_payment') // e.g., "Pending Payment" +``` + +### Subscription Info +```typescript +const { defaultSubscriptionId, subscriptionDetail, hasPermission } = + useDefaultSubscription() + +const canManageBilling = hasPermission('MANAGE_BILLING') +const companyId = subscriptionDetail?.company_id +``` + +## Bootstrap Classes Reference + +### Responsive Display +- `d-none d-md-block` - Hide on mobile, show on desktop +- `d-block d-md-none` - Show on mobile, hide on desktop +- `d-none d-md-table-cell` - For table cells + +### Spacing +- `mt-4` - Margin top +- `mb-4` - Margin bottom +- `pb-4` - Padding bottom +- `mb-24` - Large margin bottom + +### Layout +- `container-fluid` - Full-width container +- `row` - Bootstrap row +- `col-lg-6 col-md-12` - Responsive columns + +### Flexbox +- `d-flex` - Display flex +- `justify-content-center` - Center horizontally +- `align-items-center` - Center vertically + +## Linting + +Always run linter after changes: +```bash +npx eslint --fix common/types/requests.ts +npx eslint --fix common/services/useInvoice.ts +npx eslint --fix components/project/tables/MyTable.tsx +npx eslint --fix pages/my-page.tsx +``` + +Or use the check command: +```bash +/check +``` + +## Git Workflow + +Check backend branches: +```bash +cd ../api +git fetch +git log --oneline origin/feat/branch-name -n 10 +git show COMMIT_HASH:path/to/file.py +``` + +## Debugging Tips + +### Check if query is running +```typescript +const { data, error, isLoading, isFetching } = useGetEntityQuery(...) +console.log({ data, error, isLoading, isFetching }) +``` + +### Check feature flag value +```typescript +const isEnabled = Utils.getFlagsmithHasFeature('my_flag') +console.log('Flag enabled:', isEnabled) +``` + +### Inspect Redux state +```typescript +import { getStore } from 'common/store' +console.log(getStore().getState()) +``` + +### Force refetch +```typescript +const { refetch } = useGetEntityQuery(...) +refetch() +``` diff --git a/frontend/.claude/context/ui-patterns.md b/frontend/.claude/context/ui-patterns.md new file mode 100644 index 000000000000..13b8d70c575a --- /dev/null +++ b/frontend/.claude/context/ui-patterns.md @@ -0,0 +1,171 @@ +# UI Patterns & Best Practices + +## Table Components + +### Pattern: Reusable Table Components + +**Location:** `components/project/tables/` + +Tables should be self-contained components that fetch their own data and handle loading/error states. + +**Example:** `InvoiceTable.tsx` + +```typescript +import { useGetInvoicesQuery } from 'common/services/useInvoice' +import { useDefaultSubscription } from 'common/services/useDefaultSubscription' +import Loader from 'components/base/Loader' +import { ErrorMessage } from 'components/base/Messages' +import ContentContainer from './ContentContainer' + +const InvoiceTable: FC = () => { + const { defaultSubscriptionId } = useDefaultSubscription() + const { data: invoices, error, isLoading } = useGetInvoicesQuery({ + subscription_id: `${defaultSubscriptionId}`, + }) + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (error) return {error} + + return ( + + + + + + + + + + + {invoices?.map((invoice) => ( + + + + + + ))} + +
Invoice No.DescriptionTotal
{invoice.id}{invoice.description}{invoice.total}
+
+ ) +} +``` + +### Responsive Tables + +Use Bootstrap classes for responsive behavior: +- `d-none d-md-table-cell` - Hide column on mobile +- `d-block d-md-none` - Show on mobile only + +## Tabs Component + +**Location:** `components/base/forms/Tabs.tsx` + +### Basic Usage + +```typescript +import { useState } from 'react' +import { Tabs } from 'components/base/forms/Tabs' + +const MyPage = () => { + const [activeTab, setActiveTab] = useState(0) + + return ( + +
Tab 1 content
+
Tab 2 content
+
Tab 3 content
+
+ ) +} +``` + +### Tabs with Feature Flag (Optional) + +**Note:** Only use feature flags when explicitly requested. By default, implement features directly without flags. + +When specifically requested, this pattern shows tabs only when feature flag is enabled: + +```typescript +import { useFlags } from 'flagsmith/react' +import { Tabs } from 'components/base/forms/Tabs' +import Utils from 'common/utils/utils' +const MyPage = () => { + const my_feature_flag = Utils.getFlagsmithHasFeature('my_feature_flag') + const [activeTab, setActiveTab] = useState(0) + + return ( +
+

My Section

+ {my_feature_flag? ( + +
+
+
+ ) : ( + + )} +
+ ) +} +``` + +See `feature-flags/` for more details on when and how to use feature flags. + +### Uncontrolled Tabs + +For simple cases without parent state management: + +```typescript + +
Tab 1 content
+
Tab 2 content
+
+``` + +## 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({ + body: 'Closing this will discard your unsaved changes.', + noText: 'Cancel', + onNo: () => resolve(false), + onYes: () => resolve(true), + title: 'Discard changes', + yesText: 'Ok', +}) +``` + +### 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) => { ... }` 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/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 000000000000..a43d3624f4e8 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1 @@ +Read @CLAUDE.md diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 000000000000..49d43e6ea0d7 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,39 @@ +# CLAUDE.md + +## Commands +- `npm run dev` - Start dev server +- `npm run typecheck` - Type checking + +## Structure +- `/common` - Shared Redux/API (no web/mobile code) +- `/web/components` - React components (includes `/web/components/pages/` for all page components) +- `/web/components/pages/` - **All page components** (e.g., `FeaturesPage.js`, `ProjectSettingsPage.js`) +- `/common/types/` - `requests.ts` and `responses.ts` for API types +- Ignore: `ios/`, `android/`, `.net/`, `*.md.draft` + +## Rules +1. **API Integration**: Use `npx ssg` CLI + check `../api` backend +2. **Imports**: Use `common/`, `components/`, `project/` (NO relative imports) +3. **State**: Redux Toolkit + RTK Query, store in `common/store.ts` +4. **Feature Flags**: When user says "create a feature flag", you MUST: (1) Create it in Flagsmith using MCP tools (`mcp__flagsmith__create_feature`), (2) Implement code with `useFlags` hook. See `.claude/context/feature-flags/` for details +5. **Linting**: ALWAYS run `npx eslint --fix ` on any files you modify +6. **Type Enums**: Extract inline union types to named types (e.g., `type Status = 'A' | 'B'` instead of inline) +7. **NO FETCH**: NEVER use `fetch()` directly - ALWAYS use RTK Query mutations/queries (inject endpoints into services in `common/services/`), see api-integration context + +## Key Files +- Store: `common/store.ts` +- Base service: `common/service.ts` + +## Context Files + +The `.claude/context/` directory contains **required patterns and standards** for this codebase. These are not optional suggestions - they document how things must be done in this project. + +For detailed guidance on specific topics: +- **Quick Start**: `.claude/context/quick-reference.md` - Common tasks, commands, patterns +- **API Integration**: `.claude/context/api-integration.md` - Adding endpoints, RTK Query (required reading for API work) +- **Backend**: `.claude/context/backend-integration.md` - Finding endpoints, backend structure +- **UI Patterns**: `.claude/context/ui-patterns.md` - Tables, tabs, modals, confirmations (required reading for UI work) +- **Feature Flags**: `.claude/context/feature-flags/` - Using Flagsmith flags (optional, only when requested) +- **Code Patterns**: `.claude/context/patterns/` - Complete examples, best practices + +**Tip:** Start with `quick-reference.md` for common tasks and checklists. diff --git a/frontend/README.md b/frontend/README.md index 8d23cbd8c842..69e9c308d3a6 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,3 +1,73 @@ ## Flagsmith Frontend -TODO +### Docker-based development + +To bring up the API and database via Docker Compose: + +```bash +curl -o docker-compose.yml https://raw.githubusercontent.com/Flagsmith/flagsmith/main/docker-compose.yml +docker-compose -f docker-compose.yml up +``` + +The application will bootstrap an admin user, organisation, and project for you. You'll find a link to set your password in your Compose logs: + +```txt +Superuser "admin@example.com" created successfully. +Please go to the following page and choose a password: http://localhost:8000/password-reset/confirm/.../... +``` + +### Local development + +The project assumes the following tools installed: +- [Node.js](https://nodejs.org/) version 22.x +- [npm](https://www.npmjs.com/) version 10.x + +To install dependencies, run `npm install`. + +The API must be running on localhost:8000 (either via Docker or `make serve` in `../api`). + +To bring up a dev server, run `ENV=local npm run dev`. + +To run linters, run `npm run lint` (or `npm run lint:fix` to auto-fix). + +To run type checking, run `npm run typecheck`. + +### Environment configuration + +Environment configuration is defined in `project_*.js` files (`common/project.js` for defaults, `env/project_*.js` for staging/prod/selfhosted), selected at build time based on the target environment. All configs support runtime overrides via `globalThis.projectOverrides`, allowing deployment-time customisation without rebuilding. + +The `bin/env.js` script copies the appropriate `env/project_${ENV}.js` to `common/project.js`: +- `npm run dev` → copies `project_dev.js` (staging API) +- `ENV=local npm run dev` → copies `project_local.js` (localhost) +- `ENV=prod npm run bundle` → copies `project_prod.js` (production) + +For a full list of frontend environment variables, see the [Flagsmith documentation](https://docs.flagsmith.com/deployment/hosting/locally-frontend#environment-variables). + +### Code guidelines + +#### Testing + +This codebase uses TestCafe for end-to-end testing. Tests are located in the `e2e/` directory. + +To run E2E tests (requires the API running on localhost:8000), run `npm run test`. + +#### Typing + +This codebase uses TypeScript. Run `npm run typecheck` to check for type errors. + +We encourage adding types to new code and improving types in existing code when working nearby. + +#### Design and architecture + +The frontend is organised into: +- `common/` - Shared code (Redux store, RTK Query services, types, utilities) +- `web/components/` - React components +- `web/components/pages/` - Page-level components + +State management uses Redux Toolkit with RTK Query for API calls. Services are defined in `common/services/`. + +API types are centralised in: +- `common/types/requests.ts` - Request types +- `common/types/responses.ts` - Response types + +For AI-assisted development, see [CLAUDE.md](https://github.com/Flagsmith/flagsmith/blob/main/frontend/CLAUDE.md). diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index c0371f35abd7..2dc4770e8c91 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -190,9 +190,9 @@ export type Req = { projectId?: number environmentId?: string billing_period?: - | 'current_billing_period' - | 'previous_billing_period' - | '90_day_period' + | 'current_billing_period' + | 'previous_billing_period' + | '90_day_period' } getWebhooks: { environmentId: string diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index de5a656f4504..7f3f5236c767 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -348,13 +348,13 @@ export type AuditLogItem = { related_object_uuid?: number related_feature_id?: number related_object_type: - | 'FEATURE' - | 'FEATURE_STATE' - | 'ENVIRONMENT' - | 'CHANGE_REQUEST' - | 'SEGMENT' - | 'EF_VERSION' - | 'EDGE_IDENTITY' + | 'FEATURE' + | 'FEATURE_STATE' + | 'ENVIRONMENT' + | 'CHANGE_REQUEST' + | 'SEGMENT' + | 'EF_VERSION' + | 'EDGE_IDENTITY' is_system_event: boolean } diff --git a/frontend/web/components/SegmentOverrides.js b/frontend/web/components/SegmentOverrides.js index 64ea7f53fdfb..d681d59e13a3 100644 --- a/frontend/web/components/SegmentOverrides.js +++ b/frontend/web/components/SegmentOverrides.js @@ -454,7 +454,9 @@ class TheComponent extends Component { const { highlightSegmentId, projectId, value } = this.props if (!highlightSegmentId) return - const existingOverride = value?.find((v) => v.segment === highlightSegmentId) + const existingOverride = value?.find( + (v) => v.segment === highlightSegmentId, + ) if (existingOverride) { return } @@ -613,7 +615,7 @@ class TheComponent extends Component {
= ({ segmentId={`${segment.id}`} /> )} - {isEdit && !condensed ? ( + {isEdit && !condensed && ( = ({ )} - ) : metadataEnable && segmentContentType?.id ? ( + )} + {!(isEdit && !condensed) && metadataEnable && segmentContentType?.id && ( setTab(tab)}> = ({
{MetadataTab}
- ) : ( -
- -
)} + {!(isEdit && !condensed) && + !(metadataEnable && segmentContentType?.id) && ( +
+ +
+ )} ) } diff --git a/frontend/web/components/modals/FlagValueFooter.tsx b/frontend/web/components/modals/FlagValueFooter.tsx index 71d16c1e990a..77b6323a12e0 100644 --- a/frontend/web/components/modals/FlagValueFooter.tsx +++ b/frontend/web/components/modals/FlagValueFooter.tsx @@ -138,13 +138,16 @@ const FlagValueFooter = ({ !savePermission } > - {isSaving - ? existingChangeRequest - ? 'Updating Change Request' - : 'Scheduling Update' - : existingChangeRequest - ? 'Update Change Request' - : 'Schedule Update'} + {(() => { + if (isSaving) { + return existingChangeRequest + ? 'Updating Change Request' + : 'Scheduling Update' + } + return existingChangeRequest + ? 'Update Change Request' + : 'Schedule Update' + })()} )} - {isSaving - ? is4Eyes - ? existingChangeRequest - ? 'Updating Change Request' - : 'Creating Change Request' - : 'Updating' - : is4Eyes - ? existingChangeRequest - ? 'Update Change Request' - : 'Create Change Request' - : 'Update Feature Value'} + {(() => { + if (isSaving) { + if (is4Eyes) { + return existingChangeRequest + ? 'Updating Change Request' + : 'Creating Change Request' + } + return 'Updating' + } + if (is4Eyes) { + return existingChangeRequest + ? 'Update Change Request' + : 'Create Change Request' + } + return 'Update Feature Value' + })()} , ) diff --git a/frontend/web/components/mv/VariationOptions.tsx b/frontend/web/components/mv/VariationOptions.tsx index f36ce21088b5..1a962c194c18 100644 --- a/frontend/web/components/mv/VariationOptions.tsx +++ b/frontend/web/components/mv/VariationOptions.tsx @@ -99,19 +99,26 @@ export const VariationOptions: React.FC = ({
)} {multivariateOptions.map((theValue, i) => { - const override = select - ? variationOverrides && - variationOverrides[0] && + let override: (typeof variationOverrides)[number] | false | undefined + if (select) { + const hasIndex = + variationOverrides?.[0] && typeof variationOverrides[0].multivariate_feature_option_index === 'number' - ? i === variationOverrides[0].multivariate_feature_option_index && + if (hasIndex) { + override = + i === variationOverrides[0].multivariate_feature_option_index && variationOverrides[0] - : variationOverrides && - variationOverrides.find( - (v) => v.multivariate_feature_option === theValue.id, - ) - : variationOverrides && - variationOverrides.find((v) => v.percentage_allocation === 100) + } else { + override = variationOverrides?.find( + (v) => v.multivariate_feature_option === theValue.id, + ) + } + } else { + override = variationOverrides?.find( + (v) => v.percentage_allocation === 100, + ) + } return select ? (
@@ -150,7 +157,7 @@ export const VariationOptions: React.FC = ({ key={i} index={i} canCreateFeature={canCreateFeature} - readOnly={readOnly} + readOnly={readOnly ?? false} value={theValue} onChange={(e) => { updateVariation(i, e, variationOverrides)