Skip to content

Conversation

@takaokouji
Copy link

Summary

Implements the ability to save Scratch 3.0 projects (.sb3) to Google Drive with a custom dialog for specifying the filename and save location, addressing issue #428.

Key Features

  • Menu Integration: Added "Save to Google Drive" menu item in the File menu
  • Custom Save Dialog: Dialog with filename input and save location selection
  • Folder Selection: Users can save to My Drive root or select a specific folder using Google Picker
  • File Upload: Multipart upload using Google Drive Files API v3
  • Seamless Integration: Works with existing project saving mechanism

Implementation Details

New Files

  1. src/containers/google-drive-saver-hoc.jsx

    • Higher Order Component (HOC) for Google Drive save functionality
    • Manages save dialog state and upload process
    • Converts Ruby code to blocks before saving (using RubyToBlocksConverterHOC)
    • Provides error handling and user feedback
  2. src/components/google-drive-save-dialog/google-drive-save-dialog.jsx

    • Custom dialog component for save operation
    • Filename input with .sb3 extension validation
    • Save location dropdown with two options:
      • "Google Drive - My Drive" (save to root)
      • "Google Drive - Select folder..." (opens folder picker)
    • Action buttons: Cancel, Reset, Save
    • Follows app.diagrams.net UI/UX design patterns
  3. src/components/google-drive-save-dialog/google-drive-save-dialog.css

    • Styling for save dialog
    • Responsive design
    • Consistent with existing Smalruby UI

Modified Files

  1. src/lib/google-drive-api.js

    • Added uploadFile() method for multipart upload to Google Drive
    • Added showFolderPicker() method for folder selection
    • Added handleFolderPickerResponse() for handling folder picker callbacks
    • Uses base64 encoding for binary file data
  2. src/components/menu-bar/menu-bar.jsx

    • Added "Save to Google Drive" menu item after "Save to your computer"
    • Integrated GoogleDriveSaverHOC into component composition
    • Added GoogleDriveSaveDialog component to render method
    • Added PropTypes for new props
  3. src/locales/ja.js

    • Added Japanese translations for all new UI elements
    • Includes messages for menu item, dialog labels, buttons, and error messages

Technical Approach

Reuses Existing Infrastructure

  • Authentication: Uses existing OAuth 2.0 flow from GoogleDriveLoaderHOC
  • Script Loading: Leverages google-script-loader.js for dynamic loading
  • OAuth Scope: drive.file scope (already configured in PR Add Google Drive file loading functionality #427)
    • This scope allows both reading and writing files created by the app

Upload Flow

  1. User clicks "Save to Google Drive" from File menu
  2. Custom dialog displays with:
    • Pre-filled filename (project title + .sb3)
    • Save location dropdown
  3. User selects save location:
    • "My Drive": Saves to root (uses parents: ["root"])
    • "Select folder...": Opens Google Picker in folder-only mode
  4. User clicks "Save" button
  5. Ruby code is converted to blocks
  6. Project data is generated using vm.saveProjectSb3()
  7. File is uploaded to Google Drive using multipart upload
  8. Success/error message is displayed

Multipart Upload

Uses Google Drive Files API v3 with multipart upload format:

  • Metadata (JSON): filename, MIME type, parent folder
  • File content (base64): .sb3 binary data
  • Boundary delimiter for multipart sections

Error Handling

  • Configuration validation (Google API credentials)
  • User-friendly error messages
  • Graceful handling of authentication failures
  • Proper cleanup on dialog cancellation

UI/UX Reference

The dialog design follows app.diagrams.net's save dialog:

  • Clear Japanese labels (名前を付けて保存, タイプ, Where)
  • Dropdown for save location selection
  • Standard action buttons (キャンセル, リセット, 保存)

See Issue #428 for reference screenshots.

Testing

Manual Testing

Tested the following scenarios:

  • ✅ Menu item displays correctly
  • ✅ Dialog opens with pre-filled filename
  • ✅ Save to My Drive root works
  • ✅ Folder selection opens Google Picker
  • ✅ Cancel button closes dialog without errors
  • ✅ Reset button resets form fields

Lint and Build

  • ✅ All lint checks pass: npm run test:lint
  • ⏳ Build test pending (requires ~300 seconds)

Breaking Changes

None. This is a new feature that doesn't affect existing functionality.

Configuration

Uses the same environment variables as the load functionality:

  • GOOGLE_CLIENT_ID: OAuth 2.0 Client ID
  • GOOGLE_API_KEY: API Key for Google Picker API

See docs/google-drive-setup.md for setup instructions.

Related Work

Future Enhancements

Possible improvements for future PRs:

  • Auto-save to Google Drive
  • File overwrite detection and conflict resolution
  • Upload progress indicator
  • Support for saving to shared drives
  • Recent folders quick access

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

takaokouji and others added 6 commits December 14, 2025 10:31
This commit implements the ability to save Scratch 3.0 projects (.sb3)
to Google Drive with a custom dialog for specifying the filename and
save location, complementing the existing "Load from Google Drive"
functionality.

## New Features

- **Menu Integration**: Added "Save to Google Drive" menu item in File menu
- **Save Dialog**: Custom dialog with filename input and save location selection
- **Folder Selection**: Users can save to My Drive root or select a specific folder
- **File Upload**: Multipart upload using Google Drive Files API v3

## Implementation

### New Components

1. **google-drive-saver-hoc.jsx**: HOC for Google Drive save functionality
   - Manages save dialog state and upload process
   - Converts Ruby code to blocks before saving
   - Provides error handling and user feedback

2. **google-drive-save-dialog.jsx**: Custom save dialog component
   - Filename input with .sb3 extension validation
   - Save location dropdown (My Drive or folder selection)
   - Cancel, Reset, and Save buttons

3. **google-drive-save-dialog.css**: Dialog styling

### Modified Files

1. **google-drive-api.js**: Added upload functionality
   - uploadFile(): Multipart upload to Google Drive
   - showFolderPicker(): Folder selection via Google Picker
   - handleFolderPickerResponse(): Handle folder picker callback

2. **menu-bar.jsx**: Integrated save functionality
   - Added "Save to Google Drive" menu item
   - Integrated GoogleDriveSaverHOC
   - Added GoogleDriveSaveDialog component

3. **ja.js**: Added Japanese translations for all new UI elements

## Technical Details

- Uses existing OAuth 2.0 authentication from GoogleDriveLoaderHOC
- Leverages drive.file scope (already configured)
- Multipart upload with metadata and file content
- Base64 encoding for binary file data
- Folder picker using Google Picker API

## Testing

- ✅ Lint checks pass: npm run test:lint
- Dialog UI follows app.diagrams.net design patterns
- Supports both My Drive root and folder selection

## Related

- Implements feature request from Issue #428
- Built on top of PR #427 (Google Drive load functionality)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed from undefined $motion-primary-transparent to $motion-tertiary
to match existing button patterns in the codebase.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Moved 'Save to Google Drive' menu item to appear after
'Load from Google Drive' as requested, making the menu
order more logical with related operations grouped together.

Menu order:
- Load from your computer
- Save to your computer
- Load from URL
- Load from Google Drive
- Save to Google Drive (moved here)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fixed three issues based on user feedback:

1. **Removed file type field**: Removed unnecessary 'タイプ:' field
   showing 'Scratch 3.0 Project (.sb3)' as it's redundant

2. **Changed 'Where:' label to 'フォルダ:'**: More intuitive Japanese
   label for folder selection

3. **Fixed select dropdown display**: Changed from FormattedMessage
   to intl.formatMessage in option tags to properly display folder
   options instead of '[object Object]'

4. **Added white background**: Set dialog body background to white
   (was transparent before)

These changes improve the dialog's usability and visual clarity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Removed 'Google ドライブ –' prefix from folder selection options
for cleaner, more concise UI:

Before:
- Google ドライブ – My Drive
- Google ドライブ – フォルダを選択する...

After:
- My Drive
- フォルダを選択する...

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed 'My Drive' to 'マイドライブ' in Japanese locale
for better user experience with native speakers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@takaokouji takaokouji force-pushed the feature/google-drive-upload branch from aba5b78 to 4638cbc Compare December 14, 2025 08:38
takaokouji and others added 9 commits December 14, 2025 17:43
Added .setParent('root') to Google Picker DocsView to enable
hierarchical folder navigation instead of showing all folders
in a flat list.

Now the folder picker:
- Starts from root (top-level folders only)
- Allows users to navigate into subfolders by double-clicking
- Provides a cleaner, more intuitive folder selection experience

This matches the expected behavior shown in the reference screenshot.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Added .setParent('root') to Google Picker DocsView in showPicker
method to enable hierarchical folder navigation when loading files
from Google Drive.

Now the file picker:
- Starts from root (top-level folders and files only)
- Allows users to navigate into subfolders by double-clicking
- Provides consistent UX with the folder picker for saving

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Changed menu label from 'Google ドライブに保存' to
'Googleドライブに名前をつけて保存...' to better indicate
that a save dialog will appear.

Changes:
- Japanese: 'Googleドライブに名前をつけて保存...'
- English: 'Save to Google Drive...'

The ellipsis (...) indicates that user interaction is required
before the action completes, following standard UI conventions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Changed menu label from "Save to Google Drive..." to "Save as to Google Drive..."
- Aligns with Japanese label "Googleドライブに名前をつけて保存..."

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Updated dialog title and header to "Save as to Google Drive"
- Internationalized all labels with proper English defaults:
  - Filename label: "Save as:" (ja: "名前を付けて保存:")
  - Folder label: "Folder:" (ja: "フォルダ:")
- Internationalized all buttons with English defaults:
  - Cancel button: "Cancel" (ja: "キャンセル")
  - Reset button: "Reset" (ja: "リセット")
  - Save button: "Save" (ja: "保存")
- Internationalized folder selection option:
  - "Select folder..." (ja: "フォルダを選択する...")
- Updated Japanese translations for title/header to "Googleドライブに名前をつけて保存"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…Drive"

- Updated menu label from "Save as to Google Drive..." to "Save a copy to Google Drive..."
- Updated dialog title and header to match new label
- Updated Japanese translations to "Googleドライブにコピーを保存"
- Aligns with scratch.mit.edu UI conventions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Removed redundant header content at top of dialog body
- Dialog title in modal is sufficient, no need for duplicate header
- Removed unused 'gui.googleDriveSaveDialog.header' translation from ja.js

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Replaced alert dialog with inline save status display in menu bar
- Added loading spinner during save with "Saving project..." message
- Show "Project saved." message for 3 seconds after successful save
- Changed saveStatus state management from boolean to string enum ('idle' | 'saving' | 'saved')
- Added Spinner component to menu bar with proper styling
- Matches scratch.mit.edu UI/UX patterns

English messages:
- Saving: "Saving project..."
- Saved: "Project saved."

Japanese messages:
- Saving: "プロジェクトを保存中..."
- Saved: "プロジェクトが保存されました。"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
The MenuBar component now uses GoogleDriveSaverHOC, which wraps it with
RubyToBlocksConverterHOC. This HOC requires `targets` and `rubyCode`
state slices in the Redux store.

The test's mock store was missing these state slices, causing the tests
to fail with:
  TypeError: Cannot read properties of undefined (reading 'editingTarget')

Added the missing state slices with their initial values from the
respective reducers:
- targets: sprites, stage, editingTarget, highlightedTargetId, highlightedTargetTime
- rubyCode: target, code, modified, errors, markers

This fixes the CI failures in PR #429.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@takaokouji takaokouji merged commit 24d6fdb into develop Dec 14, 2025
2 checks passed
@takaokouji takaokouji deleted the feature/google-drive-upload branch December 14, 2025 10:35
github-actions bot pushed a commit that referenced this pull request Dec 14, 2025
…e-drive-upload

Add Google Drive file upload functionality with custom dialog
takaokouji added a commit that referenced this pull request Dec 14, 2025
Add GOOGLE_CLIENT_ID and GOOGLE_API_KEY environment variables to all
production build steps in the GitHub Actions workflow. This enables the
Google Drive integration features (file loading and saving) in deployed
environments.

Changes:
- Added environment variables to "Run Build" step (line 66-67)
- Added environment variables to "Rebuild for smalruby3-gui GitHub Pages" step (line 127-128)
- Added environment variables to "Rebuild for branch GitHub Pages" step (line 155-156)

These variables will be sourced from GitHub Secrets and injected into
the webpack build process via webpack.DefinePlugin.

Deployment targets:
- smalruby.app (main production site)
- smalruby.github.io/smalruby3-gui/ (GitHub Pages)
- Branch-specific deployments

Related: #428, #429

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants