-
-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Summary
Add the ability to save Scratch 3.0 projects (.sb3) to Google Drive with a custom dialog for specifying the filename and save location. This complements the existing "Load from Google Drive" functionality (PR #427).
Background
Currently, the application supports two methods for saving projects:
- Save to your computer (
SB3Downloader): Downloads the .sb3 file to the user's local computer using browser download functionality - Save / Save as a copy: Server-side save operations (for logged-in users)
There is no direct file upload functionality to cloud storage services. This issue proposes adding Google Drive upload capability to enable seamless cloud-based workflows.
Existing Implementation Reference
Current Save Flow (SB3Downloader)
- Component:
src/containers/sb3-downloader.jsx - Menu:
src/components/menu-bar/menu-bar.jsx(line 502-513) - Process:
- Convert Ruby code to blocks using
RubyToBlocksConverterHOC - Call
vm.saveProjectSb3()to generate project data (Blob) - Use
downloadBlob()to trigger browser download - Filename: Generated from project title +
.sb3extension
- Convert Ruby code to blocks using
Current Load Flow (GoogleDriveLoaderHOC)
- Component:
src/containers/google-drive-loader-hoc.jsx - API:
src/lib/google-drive-api.js - Menu:
src/components/menu-bar/menu-bar.jsx(line 523-531) - Process:
- Click "Load from Google Drive" menu item
- Call
googleDriveAPI.showPicker()to display Google Picker - User selects .sb3 file from Google Drive
- Download file using Google Drive API (
files.getwithalt=media) - Call
vm.loadProject()to load the project
Requirements
Functional Requirements
-
Menu Integration
- Add "Save to Google Drive" menu item in the File menu
- Position it after "Save to your computer" menu item
- Should be available at all times (no login required)
-
Save Dialog
- Display a custom dialog when user selects "Save to Google Drive"
- Dialog components:
- Filename input field: Pre-filled with current project title +
.sb3 - Save location dropdown: Choose where to save the file
- Action buttons: Cancel, Reset, Save
- Filename input field: Pre-filled with current project title +
-
Save Location Options
- "Google Drive - My Drive": Save to root of user's Google Drive
- "Google Drive - Select folder...": Open Google Picker to choose a specific folder
-
Upload Process
- Convert Ruby code to blocks (same as SB3Downloader)
- Generate project data using
vm.saveProjectSb3() - Upload to Google Drive using Files API v3
- Show loading indicator during upload
- Display success/error feedback
UI/UX Requirements
Based on app.diagrams.net's save dialog:
┌─────────────────────────────────────────────┐
│ Save to Google Drive │
├─────────────────────────────────────────────┤
│ │
│ 名前を付けて保存: [project-name.sb3 ] │
│ │
│ タイプ: [XMLファイル (.drawio) ▾] │
│ │
│ Where: [Google ドライブ - My D▾] │
│ ┌─────────────────────┐ │
│ │☑ Google ドライブ - │ │
│ │ My Drive │ │
│ │ Google ドライブ - │ │
│ │ フォルダを選択する…│ │
│ └─────────────────────┘ │
│ │
│ [?] [キャンセル] [リセット] [保存] │
└─────────────────────────────────────────────┘
Key UI Elements:
- Clear section labels in Japanese (internationalization support)
- Dropdown for save location with checkmark for selected option
- Pre-filled filename input
- Standard action buttons (Cancel, Reset, Save)
Technical Approach
1. Reuse Existing Infrastructure
- Authentication:
src/lib/google-drive-api.js(OAuth 2.0 flow) - Script Loading:
src/lib/google-script-loader.js(dynamic loading) - OAuth Scope:
https://www.googleapis.com/auth/drive.file(already configured)- This scope allows both reading and writing files created by the app
2. New Components to Create
A. src/containers/google-drive-saver-hoc.jsx
Higher Order Component (HOC) following the same pattern as GoogleDriveLoaderHOC:
const GoogleDriveSaverHOC = function (WrappedComponent) {
class GoogleDriveSaverComponent extends React.Component {
constructor(props) {
super(props);
bindAll(this, [
'handleStartSavingToGoogleDrive',
'handleSaveDialogSubmit',
'handleFolderSelected'
]);
}
async handleStartSavingToGoogleDrive() {
// 1. Check if Google Drive is configured
// 2. Close file menu
// 3. Show custom save dialog
}
async handleSaveDialogSubmit(filename, saveLocation) {
// 1. Convert Ruby code to blocks
// 2. Generate project data (vm.saveProjectSb3)
// 3. Upload to Google Drive
// 4. Show success/error message
}
// ... render wrapped component with onStartSavingToGoogleDrive prop
}
}B. src/components/google-drive-save-dialog/google-drive-save-dialog.jsx
Custom dialog component:
class GoogleDriveSaveDialog extends React.Component {
// State: filename, saveLocation, isUploading
// Methods: handleFilenameChange, handleLocationChange, handleSubmit
// Render: Modal with form fields and buttons
}C. src/lib/google-drive-api.js (modifications)
Add upload functionality:
class GoogleDriveAPI {
// ... existing methods ...
/**
* Upload file to Google Drive
* @param {string} filename - File name
* @param {Blob} fileData - File content
* @param {string} folderId - Optional folder ID (null for My Drive root)
* @returns {Promise<object>} Upload result with file ID
*/
async uploadFile(filename, fileData, folderId = null) {
// Use multipart upload with Files API v3
// POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart
}
/**
* Show folder picker
* @param {Function} callback - Called when user selects a folder
* @param {string} locale - Locale code
* @returns {Promise<void>}
*/
async showFolderPicker(callback, locale = 'en') {
// Similar to showPicker but folder-only view
// Use DocsView with setSelectFolderEnabled(true)
}
}3. Menu Integration
Modify src/components/menu-bar/menu-bar.jsx:
// Add after "Save to your computer" menu item (around line 513)
<MenuItem
onClick={this.props.onStartSavingToGoogleDrive}
>
<FormattedMessage
defaultMessage="Save to Google Drive"
description="Menu bar item for saving to Google Drive"
id="gui.menuBar.saveToGoogleDrive"
/>
</MenuItem>4. Google Drive Files API v3 Upload
Multipart Upload Format:
POST /upload/drive/v3/files?uploadType=multipart HTTP/1.1
Host: www.googleapis.com
Authorization: Bearer [ACCESS_TOKEN]
Content-Type: multipart/related; boundary=foo_bar_baz
--foo_bar_baz
Content-Type: application/json; charset=UTF-8
{
"name": "project-name.sb3",
"mimeType": "application/x.scratch.sb3",
"parents": ["folder-id-or-root"]
}
--foo_bar_baz
Content-Type: application/x.scratch.sb3
[binary .sb3 file content]
--foo_bar_baz--
Implementation Details
File Upload Flow
-
User clicks "Save to Google Drive"
- Call
handleStartSavingToGoogleDrive() - Check Google Drive API configuration
- Show save dialog
- Call
-
User fills in dialog
- Filename: Pre-filled with
${projectTitle}.sb3 - Save location: Dropdown with two options
- "Google Drive - My Drive" (default)
- "Google Drive - Select folder..."
- Filename: Pre-filled with
-
User selects location
- If "My Drive": Use
parents: ["root"] - If "Select folder...": Show Google Picker with folder view
- Get folder ID from picker result
- Use
parents: [folderId]
- If "My Drive": Use
-
User clicks "Save"
- Validate filename (not empty, ends with .sb3)
- Convert Ruby code to blocks
- Call
vm.saveProjectSb3()to get project blob - Show loading modal
- Call
googleDriveAPI.uploadFile(filename, blob, folderId) - Hide loading modal
- Show success alert with link to file (optional)
-
Error handling
- Network errors
- Authentication failures
- Permission denied
- Quota exceeded
- Invalid filename
Dialog State Management
this.state = {
filename: `${this.props.projectTitle}.sb3`,
saveLocation: 'my-drive', // 'my-drive' | 'select-folder'
selectedFolderId: null,
selectedFolderName: null,
isUploading: false,
showDialog: false
}Internationalization
Add messages to src/lib/shared-messages.js or create new message file:
const messages = defineMessages({
saveToGoogleDrive: {
id: 'gui.menuBar.saveToGoogleDrive',
defaultMessage: 'Save to Google Drive',
description: 'Menu bar item for saving to Google Drive'
},
saveDialogTitle: {
id: 'gui.googleDriveSaver.dialogTitle',
defaultMessage: 'Save to Google Drive',
description: 'Title for save dialog'
},
filenameLabel: {
id: 'gui.googleDriveSaver.filenameLabel',
defaultMessage: '名前を付けて保存:',
description: 'Label for filename input'
},
// ... more messages
});Testing Considerations
Manual Testing
-
Basic upload flow
- Click "Save to Google Drive"
- Keep default filename and "My Drive" location
- Click "Save"
- Verify file appears in Google Drive root
-
Folder selection
- Click "Save to Google Drive"
- Select "Select folder..." from dropdown
- Choose a folder from picker
- Click "Save"
- Verify file appears in selected folder
-
Filename validation
- Empty filename (should show error)
- Filename without .sb3 extension (should auto-append)
- Very long filename (should truncate or warn)
- Special characters in filename (should sanitize or warn)
-
Error scenarios
- Cancel dialog (should close without error)
- Network disconnected during upload (should show error)
- Not authenticated (should prompt for auth)
- Cancel folder picker (should return to dialog)
-
UI/UX testing
- Dialog appearance matches design
- Loading indicator during upload
- Success/error messages display correctly
- Dialog is responsive (mobile/tablet)
Automated Testing
- Lint:
npm run test:lintshould pass - Build:
npm run buildshould succeed - Unit tests (optional): Test dialog component in isolation
- Integration tests (optional): Mock Google Drive API responses
Related Work
-
PR Add Google Drive file loading functionality #427: Google Drive file loading functionality
- Reuse authentication flow
- Reuse script loading mechanism
- Follow similar HOC pattern
-
SB3Downloader: Local file download
- Reuse Ruby-to-blocks conversion
- Reuse
vm.saveProjectSb3()method
Breaking Changes
None. This is a new feature that doesn't affect existing functionality.
Future Enhancements
-
Auto-save to Google Drive
- Periodic auto-save to last used location
- Conflict resolution (version history)
-
File overwrite detection
- Check if file with same name exists
- Ask user: overwrite, create new, or cancel
-
Shared drives support
- Allow saving to shared/team drives
- Require additional OAuth scope
-
Recent locations
- Remember recently used folders
- Quick access to favorite locations
-
Upload progress indicator
- Show percentage during upload
- Cancel upload in progress
Documentation Updates
- Update
docs/google-drive-setup.mdto mention save functionality - Add screenshots of save dialog
- Document required OAuth scopes (already configured)
Dependencies
- Google Drive API v3 (already used)
- Google Identity Services (already used)
- Google Picker API (already used)
- No new dependencies required
Implementation checklist:
- Create
google-drive-saver-hoc.jsx - Create
google-drive-save-dialog.jsxcomponent - Add
uploadFile()method togoogle-drive-api.js - Add
showFolderPicker()method togoogle-drive-api.js - Add menu item to
menu-bar.jsx - Add internationalization messages
- Add CSS styles for dialog
- Implement error handling
- Manual testing
- Lint and build verification
- Update documentation
🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]