# Component Patterns

## Upload Components (3 generations)

| Version | Location | Rendering | Use Case |
|---------|----------|-----------|----------|
| V1 | `inputs/upload-input/` | Legacy, basic file list | Simple single-file uploads |
| V2 | `inputs/upload-input-v2/` | Custom React UI | Multi-file with progress |
| V3 | `inputs/upload-input-v3/` | MUI-based UI | Chunked uploads, async processing |

**V1/V2 wrap `inputs/dropzone/index.js`** (DropzoneJS class); **V3 wraps `inputs/upload-input-v3/dropzone-v3.js`** (DropzoneV3 wrapper). The underlying Dropzone library is `dropzone@5.7.2`.

### Chunked Upload Architecture

- `DropzoneJS` manages a chunk queue with `maxConcurrentChunks` (default: 6)
- `setupChunkThrottle()` wraps Dropzone's `_uploadData` to enforce concurrency
- Progress tracking uses `_completedBytes` floor to prevent oscillation during parallel chunk uploads
- `options.uploadprogress` override in `getDjsConfig()` centralizes progress correction (guards against `NaN` bytesSent from queued chunks)
- HTTP 202 responses trigger async polling via `_asyncProcessing` flag

### Upload Gotchas

- **Progress oscillation:** Dropzone creates chunk entries with `progress:0` and `bytesSent:undefined` upfront. Raw progress average across chunks causes backsliding — always apply `_completedBytes` floor.
- **Cancel cleanup:** `xhr.abort()` fires `onabort`, not `onload`/`onerror`. Cancelled files must be filtered from `chunkQueue` and `chunksInFlight` decremented in `removedfile` handler.
- **NaN guard:** `Math.max(bytesSent || 0, ...)` — bytesSent can be `undefined` for queued chunks.

## MUI Components

- Located in `src/components/mui/`
- Follow pattern: component in `index.js`, styles in `.module.less` or `.module.scss`
- Formik integration components in `mui/formik-inputs/` — prefix `mui-formik-*`
- All MUI components use MUI v6 APIs

## Redux Pattern

```javascript
// Action creator factory
export const createAction = type => payload => ({ type, payload });

// Async thunk pattern
export const myAction = (params) => async (dispatch, getState) => {
    dispatch(startLoading());
    return getRequest(
        createAction(REQUEST_TYPE),
        createAction(RECEIVE_TYPE),
        buildAPIBaseUrl('/api/v1/endpoint'),
        authErrorHandler
    )(params)(dispatch, getState);
};
```

- Actions: constants + thunks in same file
- Reducers: switch/case on action types
- State shape: `loggedUserState` for auth, domain-specific reducers in consumers

## FormItemTable / Sponsor Forms Metafields

- `meta_fields[].values[].id` (used as the `value` for `CheckBoxList`/`RadioButtonList`/`DropdownList` options in `ItemTableField.js`) is a **string** on the wire from the Summit API, not a number. `String(v.id)` coercion in `ItemTableField.js` is intentional and matches the API contract — it is not a value-vs-option type mismatch. Test fixtures using numeric `id` (e.g. `{ id: 1, value: "Option 1" }`) are simplified test data, not representative of the real payload shape.
- Do not flag this coercion as a selection/matching bug in code review without first confirming the actual field type returned by Summit API for the metafield in question.

## Testing Conventions

- Test files: `__tests__/` directories adjacent to components
- Framework: Jest + @testing-library/react
- Mock pattern: `jest.mock('module')` at top of file
- Component tests render with `@testing-library/react`, assert on DOM
- Dropzone tests mock the Dropzone constructor to capture options and simulate events
