docs(12-01): document legacy AttachmentStore implementation

- Comprehensive analysis of CoffeeScript Reflux store patterns
- Upload flow breakdown with FormData construction
- Client-side validation logic (10 MB limit)
- Hidden file input trigger pattern
- Error handling for 413/422 responses
- Integration points with ChatStore and dialog system
- React port patterns and implementation checklist
- 538 lines, 42 code examples

ATTACHMENT_LEGACY.md: .planning/phases/12-attachment-research-&-backend-validation/docs/ATTACHMENT_LEGACY.md
This commit is contained in:
Nuwan 2026-02-02 18:54:43 +05:30
parent ec0607a1d4
commit 48ff1dfbb1
1 changed files with 538 additions and 0 deletions

View File

@ -0,0 +1,538 @@
# 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.