-
-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Summary
Add "Save directly to Google Drive" (Googleドライブに直ちに保存) menu item that allows users to quickly save changes to the currently opened Google Drive file without showing a save dialog.
Background
Currently, the application supports:
- Load from Google Drive: Opens a file picker to load .sb3 files
- Save a copy to Google Drive: Shows a dialog to save the current project as a new file or to a specific folder
However, there is no quick-save functionality to update the currently opened file. Users must use "Save a copy" each time, which requires re-entering the filename and selecting the folder.
Requirements
Functional Requirements
-
Menu Item
- Add "Save directly to Google Drive" (Googleドライブに直ちに保存) menu item in the File menu
- Position it after "Save a copy to Google Drive..." menu item
- Initially disabled (grayed out)
-
Enabling Conditions
- Enable when a file is loaded from Google Drive
- Enable when a file is saved to Google Drive using "Save a copy"
- Disable when creating a new project or loading from local file
-
Save Behavior
- Click "Save directly to Google Drive" to overwrite the current file in Google Drive
- No dialog shown - save immediately
- Use the same project data generation process as "Save a copy"
- Display progress indicator in menu bar (same as "Save a copy")
-
OAuth Token Management
- OAuth tokens expire after 1 hour
- When expired, automatically re-authenticate before saving
- Show authentication dialog if needed
- Resume save operation after successful re-authentication
UI/UX Requirements
Menu Structure:
File
├── New
├── Load from your computer
├── Save to your computer
├── Load from Google Drive
├── Save a copy to Google Drive...
├── Save directly to Google Drive <-- NEW (disabled by default)
└── ...
Progress Display:
- Show "Saving project..." in menu bar status area (same location as "Save a copy")
- Show "Project saved." message after successful save
- Show error message if save fails
Technical Implementation
Code Investigation Results
Based on current implementation analysis:
Current Components:
-
GoogleDriveLoaderHOC (
src/containers/google-drive-loader-hoc.jsx)- Handles file loading from Google Drive
- Uses
googleDriveAPI.showPicker()to select files - Receives file metadata:
fileId,fileName - Currently does NOT store file metadata for later use
-
GoogleDriveSaverHOC (
src/containers/google-drive-saver-hoc.jsx)- Handles "Save a copy" functionality
- State:
saveStatus: 'idle' | 'saving' | 'saved' - Process:
- Convert Ruby code to blocks:
this.props.targetCodeToBlocks() - Generate project data:
this.props.saveProjectSb3() - Upload to Google Drive:
googleDriveAPI.uploadFile(filename, content, folderId)
- Convert Ruby code to blocks:
- Shows progress in menu bar via
googleDriveSaveStatusprop
-
GoogleDriveAPI (
src/lib/google-drive-api.js)- OAuth authentication:
requestAccessToken() - Token validation:
this.accessToken && window.gapi.client.getToken() - File download:
downloadFile(fileId, fileName) - File upload (create new):
uploadFile(filename, fileData, folderId)- uses POST to/upload/drive/v3/files - Token client:
this.tokenClient(Google Identity Services)
- OAuth authentication:
New Implementation Requirements
-
Redux State Management
- Create new reducer:
google-drive-fileto store current file metadata - State structure:
{ fileId: string | null, // Google Drive file ID fileName: string | null, // File name with .sb3 extension folderId: string | null, // Parent folder ID (null for My Drive root) isGoogleDriveFile: boolean // True if current project is from Google Drive }
- Actions:
setGoogleDriveFile(fileId, fileName, folderId)- Set current file infoclearGoogleDriveFile()- Clear when creating new project or loading local file
- Create new reducer:
-
Update GoogleDriveLoaderHOC
- After successful file load, dispatch
setGoogleDriveFile(fileId, fileName, null) - Store file metadata from picker callback
- After successful file load, dispatch
-
Update GoogleDriveSaverHOC
- After successful "Save a copy", dispatch
setGoogleDriveFile(fileId, fileName, folderId) - Get file ID from upload response:
response.result.id
- After successful "Save a copy", dispatch
-
New Component: DirectGoogleDriveSaverHOC (or extend GoogleDriveSaverHOC)
- Method:
handleSaveDirectlyToGoogleDrive() - Process:
- Check if Google Drive file info exists in state
- Check OAuth token validity
- If token expired, re-authenticate:
googleDriveAPI.requestAccessToken() - Convert Ruby code to blocks
- Generate project data
- Update file:
googleDriveAPI.updateFile(fileId, filename, content) - Show progress in menu bar
- No dialog shown - direct save operation
- Method:
-
New Method in GoogleDriveAPI:
updateFile()/** * Update existing file in Google Drive * @param {string} fileId - Google Drive file ID * @param {string} filename - File name * @param {Blob} fileData - File content as Blob * @returns {Promise<object>} Update result */ async updateFile(fileId, filename, fileData) { // Use PATCH instead of POST // Endpoint: /upload/drive/v3/files/{fileId} // Similar multipart format as uploadFile() }
-
Menu Bar Integration
- Add menu item in
src/components/menu-bar/menu-bar.jsx(after line 543) - Props:
isGoogleDriveFile: boolean- Enable/disable menu itemonSaveDirectlyToGoogleDrive: function- Click handlergoogleDriveSaveDirectStatus: string- Progress display ('idle' | 'saving' | 'saved')
- Menu item:
<MenuItem disabled={!this.props.isGoogleDriveFile} onClick={this.props.onSaveDirectlyToGoogleDrive} > <FormattedMessage defaultMessage="Save directly to Google Drive" description="Menu bar item for saving directly to currently opened Google Drive file" id="gui.menuBar.saveDirectlyToGoogleDrive" /> </MenuItem>
- Add menu item in
-
Internationalization
- Add to
src/locales/ja.js:'gui.menuBar.saveDirectlyToGoogleDrive': 'Googleドライブに直ちに保存', 'gui.googleDriveSaver.savingDirectly': 'プロジェクトを保存中...', 'gui.googleDriveSaver.savedDirectly': 'プロジェクトを保存しました。', 'gui.googleDriveSaver.saveDirectError': 'Googleドライブへの保存に失敗しました。'
- Add to
OAuth Token Expiration Handling
OAuth tokens expire after 1 hour. Implementation approach:
-
Before Save Operation:
async handleSaveDirectlyToGoogleDrive() { try { // Check token validity const hasValidToken = googleDriveAPI.accessToken && window.gapi.client.getToken(); if (!hasValidToken) { // Token expired - re-authenticate await googleDriveAPI.requestAccessToken(); } // Proceed with save... } catch (error) { // Handle authentication error } }
-
Token Client Behavior:
requestAccessToken()automatically shows Google OAuth consent screen if token is invalid- If user cancels authentication, operation should be aborted
- If authentication succeeds, save operation resumes
-
Error Handling:
- Network errors during save
- Authentication failures
- Permission denied (file deleted or access revoked)
- File not found (file deleted from Google Drive)
Google Drive API Reference
Update File Endpoint:
PATCH /upload/drive/v3/files/{fileId}?uploadType=multipart
Request Format:
Same multipart format as uploadFile(), but:
- Use PATCH method instead of POST
- Include fileId in URL path
- Metadata can optionally update name, parents, etc.
Response:
{
"id": "file-id",
"name": "project-name.sb3",
"webViewLink": "https://drive.google.com/file/d/..."
}Implementation Checklist
Phase 1: State Management
- Create
google-drive-filereducer with actions - Add state to Redux store configuration
- Create action creators and selectors
Phase 2: API Layer
- Implement
updateFile()method ingoogle-drive-api.js - Add token expiration check before API calls
- Test with expired tokens (wait 1 hour or manually invalidate)
Phase 3: Component Updates
- Update
GoogleDriveLoaderHOCto store file metadata - Update
GoogleDriveSaverHOCto store file metadata after save-as - Create or extend HOC for direct save functionality
- Add OAuth re-authentication flow
Phase 4: UI Integration
- Add menu item to
menu-bar.jsx - Connect menu item to Redux state (enable/disable)
- Add progress indicator (reuse existing save status display)
- Add internationalization messages
Phase 5: Testing
- Manual testing: Load from Google Drive → Save directly
- Manual testing: Save a copy → Save directly
- Manual testing: New project → Menu item should be disabled
- Manual testing: Wait 1 hour → Save directly → Should re-authenticate
- Manual testing: Cancel authentication → Should not save
- Manual testing: Delete file in Google Drive → Save directly → Should show error
- Lint:
npm run test:lint - Build:
npm run build
Edge Cases and Error Handling
-
File Deleted from Google Drive
- Detect 404 error from update API
- Show error message
- Disable "Save directly" menu item
- Clear Google Drive file state
-
Permission Revoked
- Detect 403 error from update API
- Show error message with re-authentication option
-
Network Disconnected
- Detect network error
- Show appropriate error message
- Allow retry
-
Multiple Browser Tabs
- Each tab maintains its own file state
- No synchronization needed (Google Drive handles conflicts)
-
File Renamed in Google Drive
- Continue using same fileId (names are for display only)
- Update operation will succeed even if name changed externally
Related Work
- PR Add Google Drive file loading functionality #427: Google Drive file loading functionality (base implementation)
- PR Add Google Drive file upload functionality with custom dialog #429: Google Drive file saving functionality (provides save-as foundation)
- Issue Add Google Drive file upload functionality with custom dialog #428: Feature request for Google Drive upload
Future Enhancements
-
Auto-save
- Periodic auto-save to Google Drive (every 2 minutes)
- Save before closing browser tab
- Conflict resolution
-
File History
- Show revision history from Google Drive
- Revert to previous versions
-
Keyboard Shortcut
- Cmd+S / Ctrl+S for quick save
- Currently saves to local computer
-
Visual Indicator
- Show Google Drive icon when editing a Google Drive file
- Show "unsaved changes" indicator
Technical Reference
OAuth 2.0 Token Lifecycle
Token Duration: 1 hour (3600 seconds)
Token Structure (from Google Identity Services):
{
access_token: "ya29.xxx...",
expires_in: 3599,
scope: "https://www.googleapis.com/auth/drive.file",
token_type: "Bearer"
}Checking Token Validity:
// Method 1: Check if token exists in gapi client
const hasToken = window.gapi.client.getToken();
// Method 2: Check stored token
const hasStoredToken = googleDriveAPI.accessToken;
// Best practice: Check both
const isTokenValid = hasStoredToken && hasToken;Re-authentication Flow:
- User clicks "Save directly to Google Drive"
- Check token validity
- If invalid, call
googleDriveAPI.requestAccessToken() - Google shows OAuth consent screen (popup or redirect)
- User grants permission
- Token callback receives new access_token
- Resume save operation
Google Drive Files API v3
Update File Documentation:
- https://developers.google.com/drive/api/v3/reference/files/update
- https://developers.google.com/drive/api/v3/manage-uploads#multipart
Multipart Upload for Update:
Same format as create, but with PATCH method:
PATCH https://www.googleapis.com/upload/drive/v3/files/{fileId}?uploadType=multipart
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 [email protected]