Skip to content

Add Google Drive file upload functionality with custom dialog #428

@takaokouji

Description

@takaokouji

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:

  1. Save to your computer (SB3Downloader): Downloads the .sb3 file to the user's local computer using browser download functionality
  2. 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:
    1. Convert Ruby code to blocks using RubyToBlocksConverterHOC
    2. Call vm.saveProjectSb3() to generate project data (Blob)
    3. Use downloadBlob() to trigger browser download
    4. Filename: Generated from project title + .sb3 extension

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:
    1. Click "Load from Google Drive" menu item
    2. Call googleDriveAPI.showPicker() to display Google Picker
    3. User selects .sb3 file from Google Drive
    4. Download file using Google Drive API (files.get with alt=media)
    5. Call vm.loadProject() to load the project

Requirements

Functional Requirements

  1. 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)
  2. 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
  3. 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
  4. 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

  1. User clicks "Save to Google Drive"

    • Call handleStartSavingToGoogleDrive()
    • Check Google Drive API configuration
    • Show save dialog
  2. 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..."
  3. 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]
  4. 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)
  5. 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

  1. Basic upload flow

    • Click "Save to Google Drive"
    • Keep default filename and "My Drive" location
    • Click "Save"
    • Verify file appears in Google Drive root
  2. 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
  3. 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)
  4. 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)
  5. 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:lint should pass
  • Build: npm run build should 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

  1. Auto-save to Google Drive

    • Periodic auto-save to last used location
    • Conflict resolution (version history)
  2. File overwrite detection

    • Check if file with same name exists
    • Ask user: overwrite, create new, or cancel
  3. Shared drives support

    • Allow saving to shared/team drives
    • Require additional OAuth scope
  4. Recent locations

    • Remember recently used folders
    • Quick access to favorite locations
  5. Upload progress indicator

    • Show percentage during upload
    • Cancel upload in progress

Documentation Updates

  • Update docs/google-drive-setup.md to 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.jsx component
  • Add uploadFile() method to google-drive-api.js
  • Add showFolderPicker() method to google-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]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions