# Legacy AttachmentStore Implementation - Reference Documentation

**Source:** `web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee`
**Purpose:** Reference documentation for porting file upload functionality to React/Redux
**Date:** 2026-02-02

---

## Overview

The legacy AttachmentStore is a **Reflux store** written in CoffeeScript that handles file attachment uploads for both lesson chat and session chat. It provides client-side validation, FormData construction, AJAX upload, and error handling. The store integrates with the legacy ChatStore to trigger chat message creation after successful uploads.

**Key characteristics:**
- Reflux store pattern (event-based state management)
- Supports two attachment types: `notation` and `audio`
- Client-side validation (10 MB limit)
- jQuery AJAX for multipart/form-data uploads
- Hidden file input trigger pattern
- Separate upload flows for lesson vs session context

---

## 1. File Structure

**Location:** `web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee`

**Store Pattern:** Reflux.createStore with listenables

**Public Methods (Actions):**
- `onStartAttachRecording(lessonId, sessionId)` - Triggers recording selector dialog
- `onStartAttachNotation(lessonId, sessionId)` - Triggers notation file input
- `onStartAttachAudio(lessonId, sessionId)` - Triggers audio file input
- `onUploadNotations(notations, doneCallback, failCallback)` - Uploads notation files
- `onUploadAudios(notations, doneCallback, failCallback)` - Uploads audio files

**Internal State:**
- `uploading: boolean` - Prevents concurrent uploads
- `lessonId: string | null` - Current lesson context
- `sessionId: string | null` - Current session context

**Integration:**
- Listens to `AttachmentActions` (Reflux action dispatcher)
- Listens to `AppStore` for app initialization
- Uses `context.JK.Rest()` for API calls
- Uses `@app.layout.showDialog()` for upload progress dialog

---

## 2. Upload Flow

### Step-by-Step Breakdown (Notation Upload)

**1. User Initiates Upload**
```javascript
// Translated from CoffeeScript
onStartAttachNotation(lessonId, sessionId = null) {
  if (this.uploading) {
    logger.warn("rejecting onStartAttachNotation attempt as currently busy");
    return;
  }
  this.lessonId = lessonId;
  this.sessionId = sessionId;

  logger.debug("notation upload started for lesson: " + lessonId);
  this.triggerNotation();  // Clicks hidden file input
  this.changed();           // Triggers state update
}
```

**2. Hidden File Input Triggered**
```javascript
triggerNotation() {
  if (!this.attachNotationBtn) {
    this.attachNotationBtn = $('input.attachment-notation').eq(0);
  }
  console.log("@attachNotationBtn", this.attachNotationBtn);
  this.attachNotationBtn.trigger('click');
}
```

**3. Files Selected → onUploadNotations Called**
```javascript
onUploadNotations(notations, doneCallback, failCallback) {
  logger.debug("beginning upload of notations", notations);
  this.uploading = true;
  this.changed();

  // Client-side validation
  const formData = new FormData();
  let maxExceeded = false;

  $.each(notations, (i, file) => {
    const max = 10 * 1024 * 1024;  // 10 MB
    if (file.size > max) {
      maxExceeded = true;
      return false;  // Break loop
    }
    formData.append('files[]', file);
  });

  if (maxExceeded) {
    this.app.notify({
      title: "Maximum Music Notation Size Exceeded",
      text: "You can only upload files up to 10 megabytes in size."
    });
    failCallback();
    this.uploading = false;
    this.changed();
    return;
  }

  // Add context parameters
  if (this.lessonId) {
    formData.append('lesson_session_id', this.lessonId);
  } else if (this.sessionId) {
    formData.append('session_id', this.sessionId);
  }

  formData.append('attachment_type', 'notation');

  // Show progress dialog
  this.app.layout.showDialog('music-notation-upload-dialog');

  // Upload via REST API
  rest.uploadMusicNotations(formData)
    .done((response) => this.doneUploadingNotations(notations, response, doneCallback, failCallback))
    .fail((jqXHR) => this.failUploadingNotations(jqXHR, failCallback));
}
```

**4. REST API Call (from jam_rest.js)**
```javascript
function uploadMusicNotations(formData) {
  return $.ajax({
    type: "POST",
    processData: false,      // Don't convert FormData to string
    contentType: false,       // Don't set Content-Type (browser adds multipart boundary)
    dataType: "json",
    cache: false,
    url: "/api/music_notations",
    data: formData
  });
}
```

**5. Success Handler**
```javascript
doneUploadingNotations(notations, response, doneCallback, failCallback) {
  this.uploading = false;
  this.changed();

  const error_files = [];
  $.each(response, (i, music_notation) => {
    if (music_notation.errors) {
      error_files.push(notations[i].name);
    }
  });

  if (error_files.length > 0) {
    failCallback();
    this.app.notifyAlert("Failed to upload notations.", error_files.join(', '));
  } else {
    doneCallback();
  }
}
```

**6. Error Handler**
```javascript
failUploadingNotations(jqXHR, failCallback) {
  this.uploading = false;
  this.changed();

  if (jqXHR.status == 413) {
    // File too large (server-side check)
    this.app.notify({
      title: "Maximum Music Notation Size Exceeded",
      text: "You can only upload files up to 10 megabytes in size."
    });
  } else {
    this.app.notifyServerError(jqXHR, "Unable to upload music notations");
  }
}
```

---

## 3. Client-Side Validation

### File Size Check

**Limit:** 10 MB (10 × 1024 × 1024 bytes)

**Implementation:**
```javascript
const max = 10 * 1024 * 1024;
if (file.size > max) {
  maxExceeded = true;
  return false;
}
```

**Error Message:**
```
Title: "Maximum Music Notation Size Exceeded"
Text: "You can only upload files up to 10 megabytes in size."
```

**When shown:**
- Immediately after file selection, before upload starts
- If server returns 413 (Payload Too Large)

### File Type Validation

**Note:** The legacy AttachmentStore does NOT perform file type validation on the client side. File type filtering happens at the file input level via HTML `accept` attribute, and server-side validation happens in the `MusicNotationUploader` whitelist.

**Server-side whitelist (for reference):**
```ruby
def extension_white_list
  %w(pdf png jpg jpeg gif xml mxl txt wav flac ogg aiff aifc au)
end
```

---

## 4. Attachment Type Detection

The legacy implementation uses **explicit attachment type** passed as a parameter, not automatic detection based on file extension.

**Two Upload Methods:**
1. `onUploadNotations()` → `attachment_type: 'notation'`
2. `onUploadAudios()` → `attachment_type: 'audio'`

**How it works:**
```javascript
// For notation files
formData.append('attachment_type', 'notation');

// For audio files
formData.append('attachment_type', 'audio');
```

**File Type to Attachment Type Mapping:**

| File Extensions | Attachment Type | Used For |
|----------------|----------------|----------|
| `.pdf, .png, .jpg, .jpeg, .gif, .xml, .mxl, .txt` | `notation` | Music notation, sheet music, images, text |
| `.wav, .flac, .ogg, .aiff, .aifc, .au` | `audio` | Audio files |

**Note:** The mapping is implicit based on which button the user clicks (Attach Notation vs Attach Audio), not programmatically determined from file extension.

---

## 5. Integration Points

### Integration with ChatStore

The AttachmentStore does NOT directly trigger chat messages. Instead:

1. **Upload completes** → Returns array of created `MusicNotation` objects
2. **Backend automatically creates ChatMessage** with attachment reference
3. **Backend broadcasts via WebSocket** → ChatMessage with attachment metadata
4. **ChatStore receives WebSocket message** → Displays in chat UI

**Key insight:** The attachment upload is decoupled from chat message creation. The backend handles the integration.

### Event Emission Pattern

**Reflux trigger pattern:**
```javascript
changed() {
  this.trigger({
    lessonId: this.lessonId,
    uploading: this.uploading
  });
}
```

**When triggered:**
- On `onStartAttachNotation()` / `onStartAttachAudio()` - Sets `uploading: true`
- On `doneUploadingNotations()` / `failUploadingNotations()` - Sets `uploading: false`

**UI components consume via:**
```javascript
// In React component
mixins: [Reflux.connect(AttachmentStore, 'attachmentState')]

// Component can access this.state.attachmentState.uploading
```

### Dialog System Integration

**Upload Progress Dialog:**
```javascript
this.app.layout.showDialog('music-notation-upload-dialog');
```

**Notification System:**
```javascript
// Success/Info notification
this.app.notify({
  title: "Title",
  text: "Message"
});

// Error notification
this.app.notifyAlert("Error title", "Error details");

// Server error handler
this.app.notifyServerError(jqXHR, "Context message");
```

---

## 6. FormData Construction

**Critical Pattern for React Port:**

```javascript
// Create FormData instance
const formData = new FormData();

// Append files (supports multiple)
formData.append('files[]', file);  // Array notation for multiple files

// Append session context (one of these)
formData.append('lesson_session_id', lessonId);  // For lesson chat
formData.append('session_id', sessionId);        // For session chat

// Append attachment type
formData.append('attachment_type', 'notation');  // or 'audio'

// AJAX configuration
$.ajax({
  type: "POST",
  processData: false,   // CRITICAL: Don't convert FormData to string
  contentType: false,   // CRITICAL: Don't set Content-Type (browser adds multipart boundary)
  dataType: "json",
  url: "/api/music_notations",
  data: formData
});
```

**Why this matters for React port:**
- `processData: false` → When using fetch(), don't stringify FormData
- `contentType: false` → Don't manually set `Content-Type` header (browser auto-adds boundary)
- File array notation `files[]` → Required by Rails params parsing
- Session context → Only ONE of `lesson_session_id` or `session_id` should be set

---

## 7. Audio Upload Flow

The audio upload flow is nearly identical to notation upload, with these differences:

**Method:** `onUploadAudios()` instead of `onUploadNotations()`

**attachment_type:** `'audio'` instead of `'notation'`

**Error messages:**
- Title: "Maximum Music Audio Size Exceeded"
- Failure: "Failed to upload audio files."

**Code Example:**
```javascript
onUploadAudios(notations, doneCallback, failCallback) {
  // ... same validation logic ...

  formData.append('attachment_type', 'audio');  // Only difference

  rest.uploadMusicNotations(formData)  // Same endpoint!
    .done((response) => this.doneUploadingAudios(notations, response, doneCallback, failCallback))
    .fail((jqXHR) => this.failUploadingAudios(jqXHR, failCallback));
}
```

**Note:** Both notation and audio uploads use the same REST endpoint (`/api/music_notations`), differentiated only by the `attachment_type` parameter.

---

## 8. Recording Attachment Flow

**Different from file uploads:** Recording attachments link existing recordings to lessons/sessions, they don't upload files.

```javascript
recordingsSelected(recordings) {
  if (this.lessonId) {
    const options = { id: this.lessonId, recordings: recordings };
    rest.attachRecordingToLesson(options)
      .done((response) => this.attachedRecordingsToLesson(response))
      .fail((jqXHR) => this.attachedRecordingsFail(jqXHR));
  } else if (this.sessionId) {
    const options = { id: this.sessionId, recordings: recordings };
    rest.attachRecordingToSession(options)
      .done((response) => this.attachedRecordingsToSession(response))
      .fail((jqXHR) => this.attachedRecordingsFail(jqXHR));
  }
}
```

**Success messages:**
- Lesson: "Your recording has been associated with this lesson, and can be accessed from the Messages window for this lesson."
- Session: "Your recording has been associated with this session."

**Note:** This flow is NOT in scope for v1.2 Session Attachments milestone. Recording attachments are handled separately from file uploads.

---

## 9. Key Patterns for React Port

### Pattern 1: Hidden File Input with Button Trigger
```javascript
// Legacy uses jQuery trigger
this.attachNotationBtn = $('input.attachment-notation').eq(0);
this.attachNotationBtn.trigger('click');

// React equivalent
const fileInputRef = useRef(null);
const handleAttachClick = () => {
  fileInputRef.current?.click();
};

return (
  <>
    <button onClick={handleAttachClick}>Attach File</button>
    <input
      ref={fileInputRef}
      type="file"
      style={{ display: 'none' }}
      accept=".pdf,.xml,.mxl,.txt,.png,.jpg,.jpeg,.gif,.wav"
      onChange={(e) => {
        if (e.target.files?.[0]) {
          handleFileSelect(e.target.files[0]);
          e.target.value = ''; // Reset for re-selection
        }
      }}
    />
  </>
);
```

### Pattern 2: Upload State Management
```javascript
// Legacy Reflux store
this.uploading = true;
this.changed();

// React Redux equivalent
dispatch(setUploadingState({ uploading: true, error: null }));
```

### Pattern 3: Client-Side Validation Before Upload
```javascript
// Always validate BEFORE creating FormData
const MAX_SIZE = 10 * 1024 * 1024;
if (file.size > MAX_SIZE) {
  dispatch(setUploadError('File exceeds 10 MB limit'));
  return;
}

// Then proceed with FormData construction
```

### Pattern 4: FormData with fetch()
```javascript
// Modern fetch equivalent
const formData = new FormData();
formData.append('files[]', file);
formData.append('session_id', sessionId);
formData.append('attachment_type', 'notation');

const response = await fetch('/api/music_notations', {
  method: 'POST',
  credentials: 'include',  // Important for session cookies
  body: formData           // Don't set Content-Type!
});

if (!response.ok) {
  if (response.status === 413) {
    throw new Error('File too large - maximum 10 MB');
  }
  throw new Error('Upload failed');
}

return response.json();
```

---

## 10. Differences Between Legacy and Modern Implementation

| Aspect | Legacy (CoffeeScript) | Modern (React/Redux) |
|--------|----------------------|---------------------|
| Store pattern | Reflux with events | Redux Toolkit with slices |
| AJAX library | jQuery $.ajax | fetch() API |
| State updates | `this.changed()` triggers listeners | Redux actions with reducers |
| File input | jQuery trigger on hidden input | useRef + ref.current.click() |
| Context passing | Store instance variables | Redux state or component props |
| Callbacks | done/fail callbacks | async/await with try/catch |
| Error handling | jqXHR status checks | response.ok and response.status |
| Multiple files | Supported (files[] array) | Implement single file first, extend later |
| Dialog system | `@app.layout.showDialog()` | Custom React component or state flag |

---

## 11. Implementation Checklist for React Port

Based on the legacy implementation, the React port should include:

- [ ] Redux state for upload status (`uploading`, `progress`, `error`)
- [ ] Hidden file input with ref-based trigger
- [ ] Client-side file size validation (10 MB)
- [ ] Client-side file type validation (match requirements list)
- [ ] FormData construction with `files[]`, `session_id`, `attachment_type`
- [ ] fetch() upload with `credentials: include`, no Content-Type header
- [ ] Error handling for 413 (file too large)
- [ ] Error handling for 422 (validation errors)
- [ ] Success/error user feedback (toast notifications)
- [ ] Disable UI during upload (prevent concurrent uploads)
- [ ] Reset file input after selection (allow re-selection)
- [ ] Integration with sessionChatSlice for attachment state
- [ ] WebSocket message handling for attachment metadata

---

## Summary

The legacy AttachmentStore provides a proven reference implementation for file uploads. Key takeaways:

1. **FormData is critical:** `processData: false` and `contentType: false` equivalent in fetch()
2. **Client-side validation prevents wasted uploads:** Check size before FormData construction
3. **Single upload state flag:** Prevents concurrent uploads with simple boolean
4. **Backend creates chat messages:** Don't manually create chat entries after upload
5. **Error handling by status code:** 413 gets special user-friendly message
6. **Hidden file input pattern:** Standard approach, works reliably
7. **Same endpoint for both types:** Differentiated by `attachment_type` parameter

**Next steps:** Refer to ATTACHMENT_API.md for backend contract details and response formats.
