From b82bfdfd72f5c0aed8ffe84f80351197183bdb2f Mon Sep 17 00:00:00 2001 From: scatwang Date: Thu, 26 Sep 2024 08:32:46 -0700 Subject: [PATCH 01/12] just icons and upload GoogleDriveUploadDialog --- package.json | 1 + src/explorer/Explorer.tsx | 22 ++++- src/explorer/actions.ts | 11 ++- .../GoogleDriveUploadDialog.tsx | 91 +++++++++++++++++++ .../googleDriveUploadDialog/actions.ts | 27 ++++++ src/explorer/googleDriveUploadDialog/i18n.ts | 12 +++ .../googleDriveUploadDialog/reducers.ts | 36 ++++++++ .../translations/en.json | 7 ++ src/explorer/reducers.ts | 4 +- src/explorer/sagas.ts | 25 ++++- yarn.lock | 71 +++++++++++++++ 11 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx create mode 100644 src/explorer/googleDriveUploadDialog/actions.ts create mode 100644 src/explorer/googleDriveUploadDialog/i18n.ts create mode 100644 src/explorer/googleDriveUploadDialog/reducers.ts create mode 100644 src/explorer/googleDriveUploadDialog/translations/en.json diff --git a/package.json b/package.json index 4b3e29f32..4d1879761 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "fake-indexeddb": "^5.0.2", "file-loader": "^6.2.0", "fs-extra": "^11.2.0", + "google-drive-picker": "^1.1.26", "html-webpack-plugin": "^5.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", diff --git a/src/explorer/Explorer.tsx b/src/explorer/Explorer.tsx index ae595e2fa..2b894c28e 100644 --- a/src/explorer/Explorer.tsx +++ b/src/explorer/Explorer.tsx @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors +// Copyright (c) 2022-2024 The Pybricks Authors // A file explorer control. @@ -14,6 +14,8 @@ import { } from '@blueprintjs/core'; import { Archive, + CloudDownload, + CloudUpload, Document, Duplicate, Edit, @@ -58,12 +60,15 @@ import { explorerDeleteFile, explorerDuplicateFile, explorerExportFile, + explorerImportFileFromGoogleDrive, explorerImportFiles, explorerRenameFile, + explorerUploadFileToGoogleDrive, explorerUserActivateFile, } from './actions'; import DeleteFileAlert from './deleteFileAlert/DeleteFileAlert'; import DuplicateFileDialog from './duplicateFileDialog/DuplicateFileDialog'; +import GoogleDriveUploadDialog from './googleDriveUploadDialog/GoogleDriveUploadDialog'; import { useI18n } from './i18n'; import NewFileWizard from './newFileWizard/NewFileWizard'; import RenameFileDialog from './renameFileDialog/RenameFileDialog'; @@ -132,6 +137,7 @@ const FileActionButtonGroup: React.FunctionComponent = ( const duplicateButtonId = useId(); const exportButtonId = useId(); const deleteButtonId = useId(); + const uploadToGoogleDriveButtonId = useId(); return ( = ( tooltip={i18n.translate('treeItem.exportTooltip', { fileName })} onClick={() => dispatch(explorerExportFile(fileName))} /> + } + tooltip="Upload file to Google Cloud" + onClick={() => dispatch(explorerUploadFileToGoogleDrive(fileName))} + /> } @@ -183,6 +195,7 @@ const FileActionButtonGroup: React.FunctionComponent = ( // matches ID in tour component const archiveButtonId = 'pb-explorer-archive-button'; const newButtonId = 'pb-explorer-add-button'; +const downloadFromGoogleDriveButtonId = 'pb-download-from-google-drive-button'; const Header: React.FunctionComponent = () => { const exportButtonId = useId(); @@ -196,6 +209,12 @@ const Header: React.FunctionComponent = () => { firstFocusableItemId={archiveButtonId} > + } + tooltip="Import file from Google Cloud" + onClick={() => dispatch(explorerImportFileFromGoogleDrive())} + /> } @@ -469,6 +488,7 @@ const Explorer: React.FunctionComponent = () => { + ); }; diff --git a/src/explorer/actions.ts b/src/explorer/actions.ts index b4fc76dd4..410d233f2 100644 --- a/src/explorer/actions.ts +++ b/src/explorer/actions.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors +// Copyright (c) 2022-2024 The Pybricks Authors import { createAction } from '../actions'; import { UUID } from '../fileStorage'; @@ -26,6 +26,15 @@ export const explorerDidFailToArchiveAllFiles = createAction((error: Error) => ( error, })); +export const explorerUploadFileToGoogleDrive = createAction((fileName: string) => ({ + type: 'explorer.action.uploadFileToGoogleDrive', + fileName, +})); + +export const explorerImportFileFromGoogleDrive = createAction(() => ({ + type: 'explorer.action.importFileFromGoogleDrive', +})); + /** * Action that requests to import (upload) files into the app. */ diff --git a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx new file mode 100644 index 000000000..5125649fd --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 The Pybricks Authors + +import { Button, Classes, Dialog } from '@blueprintjs/core'; +//import GoogleDrivePicker from 'google-drive-picker'; +import React, { useCallback, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { useSelector } from '../../reducers'; +import { + googleDriveUploadDialogDidAccept, + googleDriveUploadDialogDidCancel, +} from './actions'; +import { useI18n } from './i18n'; + +const GoogleDriveUploadDialog: React.FunctionComponent = () => { + const i18n = useI18n(); + const dispatch = useDispatch(); + const isOpen = useSelector((s) => s.explorer.googleDriveUploadDialog.isOpen); + const fileName = useSelector((s) => s.explorer.googleDriveUploadDialog.fileName); + + const inputRef = useRef(null); + + //const [authToken, setAuthToken] = useState(''); + //const [openPicker, authResponse] = GoogleDrivePicker(); + + const handlePickerOpen = () => { + // openPicker({ + // clientId: + // '1034337501504-of3um354h2dsdm200bhjnfpk6cg0m0n6.apps.googleusercontent.com', + // developerKey: 'AIzaSyBMKnuqNI3N0r95XNns1tT7TYJHGkM5juU', + // viewId: 'FOLDERS', + // //token: authToken, + // setSelectFolderEnabled: true, + // supportDrives: true, + // callbackFunction: (data) => { + // if (data.action === 'cancel') { + // console.log('User clicked cancel/close button'); + // } else if (data.docs && data.docs.length > 0) { + // console.log(data); + // } + // }, + // }); + }; + + // useEffect(() => { + // if (authResponse) { + // //setAuthToken(authResponse.access_token); + // } + // }, [authResponse]); + + const handleSubmit = useCallback( + (e) => { + e.preventDefault(); + dispatch(googleDriveUploadDialogDidAccept()); + }, + [dispatch], + ); + + const handleClose = useCallback(() => { + dispatch(googleDriveUploadDialogDidCancel()); + }, [dispatch]); + + return ( + { + inputRef.current?.select(); + inputRef.current?.focus(); + }} + onClose={handleClose} + > +
+
+ Upload to: {} + +
body
+
+
+
+ +
+
+
+
+ ); +}; + +export default GoogleDriveUploadDialog; diff --git a/src/explorer/googleDriveUploadDialog/actions.ts b/src/explorer/googleDriveUploadDialog/actions.ts new file mode 100644 index 000000000..fec40bc37 --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/actions.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 The Pybricks Authors + +import { createAction } from '../../actions'; + +/** + * Action that requests to show the rename file dialog. + * @param oldName The old file name. + */ +export const googleDriveUploadDialogShow = createAction((fileName: string) => ({ + type: 'explorer.googleDriveUploadDialog.action.show', + fileName, +})); + +/** + * Action that indicates the Google Drive upload dialog was accepted. + */ +export const googleDriveUploadDialogDidAccept = createAction(() => ({ + type: 'explorer.googleDriveUploadDialog.action.didAccept', +})); + +/** + * Action that indicates the Google Drive upload dialog was canceled. + */ +export const googleDriveUploadDialogDidCancel = createAction(() => ({ + type: 'explorer.googleDriveUploadDialog.action.didCancel', +})); diff --git a/src/explorer/googleDriveUploadDialog/i18n.ts b/src/explorer/googleDriveUploadDialog/i18n.ts new file mode 100644 index 000000000..f358aafba --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/i18n.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 The Pybricks Authors + +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; + +export function useI18n(): TypedI18n { + // istanbul ignore next: babel-loader rewrites this line + const [i18n] = useShopifyI18n(); + return i18n; +} diff --git a/src/explorer/googleDriveUploadDialog/reducers.ts b/src/explorer/googleDriveUploadDialog/reducers.ts new file mode 100644 index 000000000..c0db3f409 --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/reducers.ts @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 The Pybricks Authors + +import { Reducer, combineReducers } from 'redux'; +import { + googleDriveUploadDialogDidAccept, + googleDriveUploadDialogShow, +} from './actions'; + +const initialDialogFileName = ''; + +/** Controls the Google Drive upload file dialog isOpen state. */ +const isOpen: Reducer = (state = false, action) => { + if (googleDriveUploadDialogShow.matches(action)) { + return true; + } + + if ( + googleDriveUploadDialogDidAccept.matches(action) || + googleDriveUploadDialogDidAccept.matches(action) + ) { + return false; + } + + return state; +}; + +const fileName: Reducer = (state = initialDialogFileName, action) => { + if (googleDriveUploadDialogShow.matches(action)) { + return action.fileName; + } + + return state; +}; + +export default combineReducers({ isOpen, fileName }); diff --git a/src/explorer/googleDriveUploadDialog/translations/en.json b/src/explorer/googleDriveUploadDialog/translations/en.json new file mode 100644 index 000000000..05a195dc8 --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/translations/en.json @@ -0,0 +1,7 @@ +{ + "title": "Upload '{fileName}' to Google Drive", + + "action": { + "upload": "Upload" + } +} diff --git a/src/explorer/reducers.ts b/src/explorer/reducers.ts index ebfea2dc9..d9dd07e5a 100644 --- a/src/explorer/reducers.ts +++ b/src/explorer/reducers.ts @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors +// Copyright (c) 2022-2024 The Pybricks Authors import { combineReducers } from 'redux'; import deleteFileAlert from './deleteFileAlert/reducers'; import duplicateFileDialog from './duplicateFileDialog/reducers'; +import googleDriveUploadDialog from './googleDriveUploadDialog/reducers'; import newFileWizard from './newFileWizard/reducers'; import renameFileDialog from './renameFileDialog/reducers'; import renameImportDialog from './renameImportDialog/reducers'; @@ -17,4 +18,5 @@ export default combineReducers({ renameFileDialog, renameImportDialog, replaceImportDialog, + googleDriveUploadDialog, }); diff --git a/src/explorer/sagas.ts b/src/explorer/sagas.ts index 84ced80ea..6d97b8d21 100644 --- a/src/explorer/sagas.ts +++ b/src/explorer/sagas.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022-2023 The Pybricks Authors +// Copyright (c) 2022-2024 The Pybricks Authors import { fileOpen, fileSave } from 'browser-fs-access'; import JSZip from 'jszip'; @@ -74,8 +74,10 @@ import { explorerDidRenameFile, explorerDuplicateFile, explorerExportFile, + explorerImportFileFromGoogleDrive, explorerImportFiles, explorerRenameFile, + explorerUploadFileToGoogleDrive, explorerUserActivateFile, explorerUserDidActivateFile, } from './actions'; @@ -90,6 +92,7 @@ import { duplicateFileDialogShow, } from './duplicateFileDialog/actions'; import { ExplorerError } from './error'; +import { googleDriveUploadDialogShow } from './googleDriveUploadDialog/actions'; import { newFileWizardDidAccept, newFileWizardDidCancel, @@ -171,6 +174,21 @@ function* handleExplorerArchiveAllFiles(): Generator { } } +function* handleUploadFileToGoogleDrive( + action: ReturnType, +): Generator { + console.info('hello'); + + yield* put(googleDriveUploadDialogShow(action.fileName)); +} + +function* handleImportFileFromGoogleDrive(): Generator { + console.info('hello'); + + //TBD + yield* put(explorerDidArchiveAllFiles()); +} + type ImportContext = { rememberedAction?: ReplaceImportDialogAction; }; @@ -592,4 +610,9 @@ export default function* (): Generator { yield* takeEvery(explorerDuplicateFile, handleExplorerDuplicateFile); yield* takeEvery(explorerExportFile, handleExplorerExportFile); yield* takeEvery(explorerDeleteFile, handleExplorerDeleteFile); + yield* takeEvery(explorerUploadFileToGoogleDrive, handleUploadFileToGoogleDrive); + yield* takeEvery( + explorerImportFileFromGoogleDrive, + handleImportFileFromGoogleDrive, + ); } diff --git a/yarn.lock b/yarn.lock index 7108c80de..987251ad3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3039,6 +3039,7 @@ __metadata: fake-indexeddb: ^5.0.2 file-loader: ^6.2.0 fs-extra: ^11.2.0 + google-drive-picker: ^1.1.26 html-webpack-plugin: ^5.6.0 identity-obj-proxy: ^3.0.0 jest: ^29.7.0 @@ -5317,6 +5318,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15.7.12": + version: 15.7.13 + resolution: "@types/prop-types@npm:15.7.13" + checksum: 8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" @@ -5340,6 +5348,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18.3.0": + version: 18.3.0 + resolution: "@types/react-dom@npm:18.3.0" + dependencies: + "@types/react": "*" + checksum: a0cd9b1b815a6abd2a367a9eabdd8df8dd8f13f95897b2f9e1359ea3ac6619f957c1432ece004af7d95e2a7caddbba19faa045f831f32d6263483fc5404a7596 + languageName: node + linkType: hard + "@types/react-splitter-layout@npm:^3.0.5": version: 3.0.5 resolution: "@types/react-splitter-layout@npm:3.0.5" @@ -5369,6 +5386,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.3.4": + version: 18.3.9 + resolution: "@types/react@npm:18.3.9" + dependencies: + "@types/prop-types": "*" + csstype: ^3.0.2 + checksum: ac8c7cf6f9c5c2ecef790f3db6f98c672fdec0884f4e4730777fd18823207231cdd1d2e4673f473ef17a096c73c87bff309e83b026fb2602ba068371f0dc54fe + languageName: node + linkType: hard + "@types/redux-logger@npm:^3.0.12": version: 3.0.12 resolution: "@types/redux-logger@npm:3.0.12" @@ -10169,6 +10196,20 @@ __metadata: languageName: node linkType: hard +"google-drive-picker@npm:^1.1.26": + version: 1.1.29 + resolution: "google-drive-picker@npm:1.1.29" + dependencies: + "@types/prop-types": ^15.7.12 + "@types/react": ^18.3.4 + "@types/react-dom": ^18.3.0 + prop-types: ^15.8.1 + react: ^18.3.1 + react-dom: ^18.3.1 + checksum: 37cd7c1ebda4ea1e5495bdeeae6b157c0a9183fd386eefe2282e1a8325fcd1cdb53bbc1513859437a299dfec1a3b29803d88bf51bc0c647466f24eafa12efe61 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -14620,6 +14661,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^18.3.1": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.2 + peerDependencies: + react: ^18.3.1 + checksum: 298954ecd8f78288dcaece05e88b570014d8f6dce5db6f66e6ee91448debeb59dcd31561dddb354eee47e6c1bb234669459060deb238ed0213497146e555a0b9 + languageName: node + linkType: hard + "react-dropzone@npm:^14.2.3": version: 14.2.3 resolution: "react-dropzone@npm:14.2.3" @@ -14804,6 +14857,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: ^1.1.0 + checksum: a27bcfa8ff7c15a1e50244ad0d0c1cb2ad4375eeffefd266a64889beea6f6b64c4966c9b37d14ee32d6c9fcd5aa6ba183b6988167ab4d127d13e7cb5b386a376 + languageName: node + linkType: hard + "read-cache@npm:^1.0.0": version: 1.0.0 resolution: "read-cache@npm:1.0.0" @@ -15339,6 +15401,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.2": + version: 0.23.2 + resolution: "scheduler@npm:0.23.2" + dependencies: + loose-envify: ^1.1.0 + checksum: 3e82d1f419e240ef6219d794ff29c7ee415fbdc19e038f680a10c067108e06284f1847450a210b29bbaf97b9d8a97ced5f624c31c681248ac84c80d56ad5a2c4 + languageName: node + linkType: hard + "schema-utils@npm:2.7.0": version: 2.7.0 resolution: "schema-utils@npm:2.7.0" From 9d4960d44bf3abf90277cf9414c81f7f86a57839 Mon Sep 17 00:00:00 2001 From: scatwang Date: Fri, 27 Sep 2024 00:03:53 -0700 Subject: [PATCH 02/12] Enable drive picker --- config/webpackDevServer.config.js | 4 +- package.json | 2 +- scripts/serve.py | 4 +- .../GoogleDriveUploadDialog.tsx | 108 ++++++--- yarn.lock | 229 +++++++++++++++--- 5 files changed, 277 insertions(+), 70 deletions(-) diff --git a/config/webpackDevServer.config.js b/config/webpackDevServer.config.js index d14d820d7..7c66c1aed 100644 --- a/config/webpackDevServer.config.js +++ b/config/webpackDevServer.config.js @@ -40,8 +40,8 @@ module.exports = function (proxy, allowedHost) { 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': '*', // items below required by Pybricks Code app - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', + // 'Cross-Origin-Opener-Policy': 'same-origin', + // 'Cross-Origin-Embedder-Policy': 'require-corp', }, // Enable gzip compression of generated files. compress: true, diff --git a/package.json b/package.json index 4d1879761..474172235 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "fake-indexeddb": "^5.0.2", "file-loader": "^6.2.0", "fs-extra": "^11.2.0", - "google-drive-picker": "^1.1.26", + "google-drive-picker": "^1.1.29", "html-webpack-plugin": "^5.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", diff --git a/scripts/serve.py b/scripts/serve.py index 79f49f725..fc5811b0f 100755 --- a/scripts/serve.py +++ b/scripts/serve.py @@ -24,8 +24,8 @@ def __init__( def end_headers(self) -> None: # custom headers needed for some web API features - self.send_header("Cross-Origin-Opener-Policy", "same-origin") - self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + # self.send_header("Cross-Origin-Opener-Policy", "same-origin") + # self.send_header("Cross-Origin-Embedder-Policy", "require-corp") return super().end_headers() diff --git a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx index 5125649fd..735368795 100644 --- a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx +++ b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx @@ -2,8 +2,8 @@ // Copyright (c) 2024 The Pybricks Authors import { Button, Classes, Dialog } from '@blueprintjs/core'; -//import GoogleDrivePicker from 'google-drive-picker'; -import React, { useCallback, useRef } from 'react'; +import GoogleDrivePicker from 'google-drive-picker'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useSelector } from '../../reducers'; import { @@ -12,6 +12,31 @@ import { } from './actions'; import { useI18n } from './i18n'; +export interface DriveDocument { + description: string; + downloadUrl?: string; + driveSuccess: boolean; + embedUrl: string; + iconUrl: string; + id: string; + isShared: boolean; + lastEditedUtc: number; + mimeType: string; + name: string; + rotation: number; + rotationDegree: number; + serviceId: string; + sizeBytes: number; + type: string; + uploadState?: string; + url: string; +} + +export interface PickerResponse { + action: string; + docs: DriveDocument[]; +} + const GoogleDriveUploadDialog: React.FunctionComponent = () => { const i18n = useI18n(); const dispatch = useDispatch(); @@ -20,40 +45,54 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { const inputRef = useRef(null); - //const [authToken, setAuthToken] = useState(''); - //const [openPicker, authResponse] = GoogleDrivePicker(); + const [authToken, setAuthToken] = useState(''); + const [openPicker, authRes] = GoogleDrivePicker(); + const [dest, setDest] = useState(); const handlePickerOpen = () => { - // openPicker({ - // clientId: - // '1034337501504-of3um354h2dsdm200bhjnfpk6cg0m0n6.apps.googleusercontent.com', - // developerKey: 'AIzaSyBMKnuqNI3N0r95XNns1tT7TYJHGkM5juU', - // viewId: 'FOLDERS', - // //token: authToken, - // setSelectFolderEnabled: true, - // supportDrives: true, - // callbackFunction: (data) => { - // if (data.action === 'cancel') { - // console.log('User clicked cancel/close button'); - // } else if (data.docs && data.docs.length > 0) { - // console.log(data); - // } - // }, - // }); + console.log('token:', authToken); + openPicker({ + clientId: + '1034337501504-of3um354h2dsdm200bhjnfpk6cg0m0n6.apps.googleusercontent.com', + developerKey: 'AIzaSyBMKnuqNI3N0r95XNns1tT7TYJHGkM5juU', + viewId: 'FOLDERS', + token: authToken, + customScopes: ['https://www.googleapis.com/auth/drive'], + setSelectFolderEnabled: true, + supportDrives: true, + callbackFunction: (data: PickerResponse) => { + if (data.action === 'cancel') { + console.log('User clicked cancel/close button'); + } + console.log(data); + if (data && data.docs) { + setDest(data.docs[0]); + } + }, + }); + console.log('token:', authToken); }; - // useEffect(() => { - // if (authResponse) { - // //setAuthToken(authResponse.access_token); - // } - // }, [authResponse]); + useEffect(() => { + if (authRes) { + setAuthToken(authRes.access_token); + } + }, [authRes]); const handleSubmit = useCallback( (e) => { e.preventDefault(); + console.log('upload: %s, %s', fileName, authToken); + + // const drive = new TsGoogleDrive({ + // oAuthCredentials: { access_token: authToken }, + // }); + // console.log(drive); + // drive.upload(fileName); + dispatch(googleDriveUploadDialogDidAccept()); }, - [dispatch], + [dispatch, authToken, fileName], ); const handleClose = useCallback(() => { @@ -72,9 +111,20 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { >
- Upload to: {} - -
body
+
+ Upload to: + {dest && ( + + )} + +
diff --git a/yarn.lock b/yarn.lock index 987251ad3..8df7db079 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2378,6 +2378,15 @@ __metadata: languageName: node linkType: hard +"@googleapis/drive@npm:^8.14.0": + version: 8.14.0 + resolution: "@googleapis/drive@npm:8.14.0" + dependencies: + googleapis-common: ^7.0.0 + checksum: f415fee41a4903dbdb98109760b64e7ce769ca6ab7e0648dfebb831e65d6ca156901d8e32cba3d79b42f1b635b1723b2c0727b0c5c7c9b0cf65ece98b5bd56dd + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.13": version: 0.11.13 resolution: "@humanwhocodes/config-array@npm:0.11.13" @@ -2973,6 +2982,7 @@ __metadata: dependencies: "@babel/core": ^7.23.9 "@blueprintjs/core": ^5.8.2 + "@googleapis/drive": ^8.14.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.11 "@pybricks/firmware": 7.10.0 "@pybricks/ide-docs": 2.14.0 @@ -3039,7 +3049,7 @@ __metadata: fake-indexeddb: ^5.0.2 file-loader: ^6.2.0 fs-extra: ^11.2.0 - google-drive-picker: ^1.1.26 + google-drive-picker: ^1.1.29 html-webpack-plugin: ^5.6.0 identity-obj-proxy: ^3.0.0 jest: ^29.7.0 @@ -6206,6 +6216,15 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.0.2": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: ^4.3.4 + checksum: 51c158769c5c051482f9ca2e6e1ec085ac72b5a418a9b31b4e82fe6c0a6699adb94c1c42d246699a587b3335215037091c79e0de512c516f73b6ea844202f037 + languageName: node + linkType: hard + "agentkeepalive@npm:^4.2.1": version: 4.2.1 resolution: "agentkeepalive@npm:4.2.1" @@ -6868,6 +6887,13 @@ __metadata: languageName: node linkType: hard +"base64-js@npm:^1.3.0": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + "batch@npm:0.6.1": version: 0.6.1 resolution: "batch@npm:0.6.1" @@ -6902,6 +6928,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -7038,6 +7071,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -8584,6 +8624,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: ^5.0.1 + checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -9556,6 +9605,13 @@ __metadata: languageName: node linkType: hard +"extend@npm:^3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 + languageName: node + linkType: hard + "fake-indexeddb@npm:^5.0.2": version: 5.0.2 resolution: "fake-indexeddb@npm:5.0.2" @@ -9987,6 +10043,29 @@ __metadata: languageName: node linkType: hard +"gaxios@npm:^6.0.0, gaxios@npm:^6.0.3, gaxios@npm:^6.1.1": + version: 6.7.1 + resolution: "gaxios@npm:6.7.1" + dependencies: + extend: ^3.0.2 + https-proxy-agent: ^7.0.1 + is-stream: ^2.0.0 + node-fetch: ^2.6.9 + uuid: ^9.0.1 + checksum: ed5952655339918e0868c6f4e079d6e9e55b20a242ddb1be25076cdfb0dd1ca5a2cb233da7352baa972c19f898a78fa6ba67e7d848717c9ca9877c269b5b02f7 + languageName: node + linkType: hard + +"gcp-metadata@npm:^6.1.0": + version: 6.1.0 + resolution: "gcp-metadata@npm:6.1.0" + dependencies: + gaxios: ^6.0.0 + json-bigint: ^1.0.0 + checksum: 55de8ae4a6b7664379a093abf7e758ae06e82f244d41bd58d881a470bf34db94c4067ce9e1b425d9455b7705636d5f8baad844e49bb73879c338753ba7785b2b + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -10196,7 +10275,21 @@ __metadata: languageName: node linkType: hard -"google-drive-picker@npm:^1.1.26": +"google-auth-library@npm:^9.7.0": + version: 9.14.1 + resolution: "google-auth-library@npm:9.14.1" + dependencies: + base64-js: ^1.3.0 + ecdsa-sig-formatter: ^1.0.11 + gaxios: ^6.1.1 + gcp-metadata: ^6.1.0 + gtoken: ^7.0.0 + jws: ^4.0.0 + checksum: 98c7ffb6ef8d811a54d728a94c31aa60c777f035306f0ded70654ce0aa1f4dcf393bb505b262aa48438f5ead8941248f3759f24f0e22b4465b8537b1d90415ac + languageName: node + linkType: hard + +"google-drive-picker@npm:^1.1.29": version: 1.1.29 resolution: "google-drive-picker@npm:1.1.29" dependencies: @@ -10210,6 +10303,20 @@ __metadata: languageName: node linkType: hard +"googleapis-common@npm:^7.0.0": + version: 7.2.0 + resolution: "googleapis-common@npm:7.2.0" + dependencies: + extend: ^3.0.2 + gaxios: ^6.0.3 + google-auth-library: ^9.7.0 + qs: ^6.7.0 + url-template: ^2.0.8 + uuid: ^9.0.0 + checksum: 58f520134c9d6f439ef81919471689f0278ef0ffdbd50c693a59282d95141be74df3a5614c25347c140fc44201e0ef998300389f4cbf51502f2351e67c758ab6 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -10233,6 +10340,16 @@ __metadata: languageName: node linkType: hard +"gtoken@npm:^7.0.0": + version: 7.1.0 + resolution: "gtoken@npm:7.1.0" + dependencies: + gaxios: ^6.0.0 + jws: ^4.0.0 + checksum: 1f338dced78f9d895ea03cd507454eb5a7b77e841ecd1d45e44483b08c1e64d16a9b0342358d37586d87462ffc2d5f5bff5dfe77ed8d4f0aafc3b5b0347d5d16 + languageName: node + linkType: hard + "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -10559,6 +10676,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^7.0.1": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: ^7.0.2 + debug: 4 + checksum: 2e1a28960f13b041a50702ee74f240add8e75146a5c37fc98f1960f0496710f6918b3a9fe1e5aba41e50f58e6df48d107edd9c405c5f0d73ac260dabf2210857 + languageName: node + linkType: hard + "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -11912,6 +12039,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: ^9.0.0 + checksum: c67bb93ccb3c291e60eb4b62931403e378906aab113ec1c2a8dd0f9a7f065ad6fd9713d627b732abefae2e244ac9ce1721c7a3142b2979532f12b258634ce6f6 + languageName: node + linkType: hard + "json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -12020,6 +12156,27 @@ __metadata: languageName: node linkType: hard +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: ^5.0.1 + checksum: 8f00b71ad5fe94cb55006d0d19202f8f56889109caada2f7eeb63ca81755769ce87f4f48101967f398462e3b8ae4faebfbd5a0269cb755dead5d63c77ba4d2f1 + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: ^2.0.0 + safe-buffer: ^5.0.1 + checksum: d68d07aa6d1b8cb35c363a9bd2b48f15064d342a5d9dc18a250dbbce8dc06bd7e4792516c50baa16b8d14f61167c19e851fd7f66b59ecc68b7f6a013759765f7 + languageName: node + linkType: hard + "kind-of@npm:^6.0.2": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -12803,6 +12960,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.9": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 + languageName: node + linkType: hard + "node-forge@npm:^1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -14454,7 +14625,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.0": +"qs@npm:6.13.0, qs@npm:^6.7.0": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -14649,19 +14820,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.2.0": - version: 18.2.0 - resolution: "react-dom@npm:18.2.0" - dependencies: - loose-envify: ^1.1.0 - scheduler: ^0.23.0 - peerDependencies: - react: ^18.2.0 - checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc - languageName: node - linkType: hard - -"react-dom@npm:^18.3.1": +"react-dom@npm:^18.2.0, react-dom@npm:^18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" dependencies: @@ -14848,16 +15007,7 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.2.0": - version: 18.2.0 - resolution: "react@npm:18.2.0" - dependencies: - loose-envify: ^1.1.0 - checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b - languageName: node - linkType: hard - -"react@npm:^18.3.1": +"react@npm:^18.2.0, react@npm:^18.3.1": version: 18.3.1 resolution: "react@npm:18.3.1" dependencies: @@ -15312,7 +15462,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -15392,15 +15542,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0": - version: 0.23.0 - resolution: "scheduler@npm:0.23.0" - dependencies: - loose-envify: ^1.1.0 - checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a - languageName: node - linkType: hard - "scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" @@ -17051,6 +17192,13 @@ __metadata: languageName: node linkType: hard +"url-template@npm:^2.0.8": + version: 2.0.8 + resolution: "url-template@npm:2.0.8" + checksum: 4183fccd74e3591e4154134d4443dccecba9c455c15c7df774f1f1e3fa340fd9bffb903b5beec347196d15ce49c34edf6dec0634a95d170ad6e78c0467d6e13e + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.0.0": version: 1.1.0 resolution: "use-sync-external-store@npm:1.1.0" @@ -17107,6 +17255,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.0.1 resolution: "v8-to-istanbul@npm:9.0.1" From 282f39ab9a3eeab5006cee9935bd61c327476204 Mon Sep 17 00:00:00 2001 From: scatwang Date: Fri, 27 Sep 2024 12:06:40 -0700 Subject: [PATCH 03/12] Adding upload support. --- .../GoogleDriveUploadDialog.tsx | 68 +++---- .../googleDriveUploadDialog/actions.ts | 42 +++- .../googleDriveUploadDialog/protocol.ts | 29 +++ .../googleDriveUploadDialog/reducers.ts | 7 +- src/explorer/googleDriveUploadDialog/sagas.ts | 84 ++++++++ .../translations/en.json | 5 +- src/explorer/sagas.ts | 8 +- src/sagas.ts | 2 +- yarn.lock | 191 +----------------- 9 files changed, 186 insertions(+), 250 deletions(-) create mode 100644 src/explorer/googleDriveUploadDialog/protocol.ts create mode 100644 src/explorer/googleDriveUploadDialog/sagas.ts diff --git a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx index 735368795..964b1850c 100644 --- a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx +++ b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx @@ -1,41 +1,17 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2024 The Pybricks Authors -import { Button, Classes, Dialog } from '@blueprintjs/core'; +import { Button, Classes, Dialog, Spinner } from '@blueprintjs/core'; import GoogleDrivePicker from 'google-drive-picker'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useSelector } from '../../reducers'; import { - googleDriveUploadDialogDidAccept, googleDriveUploadDialogDidCancel, + googleDriveUploadDialogUpload, } from './actions'; import { useI18n } from './i18n'; - -export interface DriveDocument { - description: string; - downloadUrl?: string; - driveSuccess: boolean; - embedUrl: string; - iconUrl: string; - id: string; - isShared: boolean; - lastEditedUtc: number; - mimeType: string; - name: string; - rotation: number; - rotationDegree: number; - serviceId: string; - sizeBytes: number; - type: string; - uploadState?: string; - url: string; -} - -export interface PickerResponse { - action: string; - docs: DriveDocument[]; -} +import { DriveDocument, PickerResponse } from './protocol'; const GoogleDriveUploadDialog: React.FunctionComponent = () => { const i18n = useI18n(); @@ -48,6 +24,7 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { const [authToken, setAuthToken] = useState(''); const [openPicker, authRes] = GoogleDrivePicker(); const [dest, setDest] = useState(); + const [uploading, setUploading] = useState(false); const handlePickerOpen = () => { console.log('token:', authToken); @@ -79,21 +56,15 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { } }, [authRes]); - const handleSubmit = useCallback( - (e) => { - e.preventDefault(); - console.log('upload: %s, %s', fileName, authToken); - - // const drive = new TsGoogleDrive({ - // oAuthCredentials: { access_token: authToken }, - // }); - // console.log(drive); - // drive.upload(fileName); + const handleUpload = () => { + setUploading(true); + dispatch(googleDriveUploadDialogUpload(fileName, authToken, dest?.id || '')); - dispatch(googleDriveUploadDialogDidAccept()); - }, - [dispatch, authToken, fileName], - ); + setTimeout(() => { + setUploading(false); + dispatch(googleDriveUploadDialogDidCancel()); + }, 500); + }; const handleClose = useCallback(() => { dispatch(googleDriveUploadDialogDidCancel()); @@ -109,7 +80,7 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { }} onClose={handleClose} > - +
Upload to: @@ -122,15 +93,24 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => {
)}
- +
diff --git a/src/explorer/googleDriveUploadDialog/actions.ts b/src/explorer/googleDriveUploadDialog/actions.ts index fec40bc37..7bd960442 100644 --- a/src/explorer/googleDriveUploadDialog/actions.ts +++ b/src/explorer/googleDriveUploadDialog/actions.ts @@ -4,8 +4,8 @@ import { createAction } from '../../actions'; /** - * Action that requests to show the rename file dialog. - * @param oldName The old file name. + * Action that requests to show the Google Drive upload dialog. + * @param fileName The name of the local file to be uploaded. */ export const googleDriveUploadDialogShow = createAction((fileName: string) => ({ type: 'explorer.googleDriveUploadDialog.action.show', @@ -13,11 +13,41 @@ export const googleDriveUploadDialogShow = createAction((fileName: string) => ({ })); /** - * Action that indicates the Google Drive upload dialog was accepted. + * Action that requests to start uploading. + * @param fileName The name of the local file to be uploaded. + * @param authToken The Google API auth token. + * @param targetFolderId The target Google Drive folder id. */ -export const googleDriveUploadDialogDidAccept = createAction(() => ({ - type: 'explorer.googleDriveUploadDialog.action.didAccept', -})); +export const googleDriveUploadDialogUpload = createAction( + (fileName: string, authToken: string, targetFolderId: string) => ({ + type: 'explorer.googleDriveUploadDialog.action.didAccept', + fileName, + authToken, + targetFolderId, + }), +); + +/** + * Action that indicates the upload was finished. + * @param googleDriveDocId The Google Drive doc id of the uploaded file. + */ +export const googleDriveUploadDialogDidUploadFile = createAction( + (googleDriveDocId: string) => ({ + type: 'explorer.googleDriveUploadDialog.action.didUploadFile', + googleDriveDocId, + }), +); + +/** + * Action that indicates the upload was failed. + * @param error The error from Google Drive API. + */ +export const googleDriveUploadDialogFailedToUploadFile = createAction( + (error: Error) => ({ + type: 'explorer.googleDriveUploadDialog.action.failedToUploadFile', + error, + }), +); /** * Action that indicates the Google Drive upload dialog was canceled. diff --git a/src/explorer/googleDriveUploadDialog/protocol.ts b/src/explorer/googleDriveUploadDialog/protocol.ts new file mode 100644 index 000000000..3f5deedb4 --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/protocol.ts @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 The Pybricks Authors + +/** A individual doc returned from Google Picker API. */ +export interface DriveDocument { + description: string; + downloadUrl?: string; + driveSuccess: boolean; + embedUrl: string; + iconUrl: string; + id: string; + isShared: boolean; + lastEditedUtc: number; + mimeType: string; + name: string; + rotation: number; + rotationDegree: number; + serviceId: string; + sizeBytes: number; + type: string; + uploadState?: string; + url: string; +} + +/** Response from Google Picker API. */ +export interface PickerResponse { + action: string; + docs: DriveDocument[]; +} diff --git a/src/explorer/googleDriveUploadDialog/reducers.ts b/src/explorer/googleDriveUploadDialog/reducers.ts index c0db3f409..073875968 100644 --- a/src/explorer/googleDriveUploadDialog/reducers.ts +++ b/src/explorer/googleDriveUploadDialog/reducers.ts @@ -3,7 +3,7 @@ import { Reducer, combineReducers } from 'redux'; import { - googleDriveUploadDialogDidAccept, + googleDriveUploadDialogDidCancel, googleDriveUploadDialogShow, } from './actions'; @@ -15,10 +15,7 @@ const isOpen: Reducer = (state = false, action) => { return true; } - if ( - googleDriveUploadDialogDidAccept.matches(action) || - googleDriveUploadDialogDidAccept.matches(action) - ) { + if (googleDriveUploadDialogDidCancel.matches(action)) { return false; } diff --git a/src/explorer/googleDriveUploadDialog/sagas.ts b/src/explorer/googleDriveUploadDialog/sagas.ts new file mode 100644 index 000000000..1db59e761 --- /dev/null +++ b/src/explorer/googleDriveUploadDialog/sagas.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 The Pybricks Authors + +import { put, race, take, takeEvery } from 'typed-redux-saga/macro'; +import { + fileStorageDidFailToReadFile, + fileStorageDidReadFile, + fileStorageReadFile, +} from '../../fileStorage/actions'; +import { defined, ensureError } from '../../utils'; + +import { + googleDriveUploadDialogDidUploadFile, + googleDriveUploadDialogFailedToUploadFile, + googleDriveUploadDialogUpload, +} from './actions'; + +function* handleGoogleDriveUploadDialogUploadFile( + action: ReturnType, +): Generator { + try { + yield* put(fileStorageReadFile(action.fileName)); + console.log(action); + + const { didRead, didFailToRead } = yield* race({ + didRead: take( + fileStorageDidReadFile.when((a) => a.path === action.fileName), + ), + didFailToRead: take( + fileStorageDidFailToReadFile.when((a) => a.path === action.fileName), + ), + }); + + if (didFailToRead) { + throw didFailToRead.error; + } + + defined(didRead); + + const form = new FormData(); + form.append( + 'metadata', + new Blob( + [ + JSON.stringify({ + name: action.fileName, + mimeType: 'text/plain', + parents: [action.targetFolderId], + }), + ], + { type: 'application/json' }, + ), + ); + form.append('file', new Blob([didRead.contents], { type: 'text/plain' })); + + const xhr = new XMLHttpRequest(); + xhr.open( + 'post', + 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', + ); + xhr.setRequestHeader('Authorization', 'Bearer ' + action.authToken); + xhr.responseType = 'json'; + xhr.onload = () => { + console.log('Google drive file id:', xhr.response.id); + }; + xhr.onerror = (event) => { + console.log('Failed to upload file to Google Drive:', event); + }; + xhr.send(form); + + // TBD + yield* put(googleDriveUploadDialogDidUploadFile(xhr.response.id)); + } catch (err) { + console.log('Failed to upload file to Google Drive:', err); + yield* put(googleDriveUploadDialogFailedToUploadFile(ensureError(err))); + } +} + +export default function* (): Generator { + yield* takeEvery( + googleDriveUploadDialogUpload, + handleGoogleDriveUploadDialogUploadFile, + ); +} diff --git a/src/explorer/googleDriveUploadDialog/translations/en.json b/src/explorer/googleDriveUploadDialog/translations/en.json index 05a195dc8..309c0960b 100644 --- a/src/explorer/googleDriveUploadDialog/translations/en.json +++ b/src/explorer/googleDriveUploadDialog/translations/en.json @@ -2,6 +2,9 @@ "title": "Upload '{fileName}' to Google Drive", "action": { - "upload": "Upload" + "change_destination": "Change Destination", + "choose_destination": "Choose Destination", + "upload": "Upload", + "cancel": "Cancel" } } diff --git a/src/explorer/sagas.ts b/src/explorer/sagas.ts index 6d97b8d21..3857e6b50 100644 --- a/src/explorer/sagas.ts +++ b/src/explorer/sagas.ts @@ -4,6 +4,7 @@ import { fileOpen, fileSave } from 'browser-fs-access'; import JSZip from 'jszip'; import { + all, call, getContext, put, @@ -93,6 +94,7 @@ import { } from './duplicateFileDialog/actions'; import { ExplorerError } from './error'; import { googleDriveUploadDialogShow } from './googleDriveUploadDialog/actions'; +import googleDriveUploadDialog from './googleDriveUploadDialog/sagas'; import { newFileWizardDidAccept, newFileWizardDidCancel, @@ -177,15 +179,12 @@ function* handleExplorerArchiveAllFiles(): Generator { function* handleUploadFileToGoogleDrive( action: ReturnType, ): Generator { - console.info('hello'); - yield* put(googleDriveUploadDialogShow(action.fileName)); } function* handleImportFileFromGoogleDrive(): Generator { - console.info('hello'); - //TBD + console.info('TBD: show download button'); yield* put(explorerDidArchiveAllFiles()); } @@ -615,4 +614,5 @@ export default function* (): Generator { explorerImportFileFromGoogleDrive, handleImportFileFromGoogleDrive, ); + yield* all([googleDriveUploadDialog()]); } diff --git a/src/sagas.ts b/src/sagas.ts index a30080117..7420aee7d 100644 --- a/src/sagas.ts +++ b/src/sagas.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2022 The Pybricks Authors +// Copyright (c) 2020-2024 The Pybricks Authors import { eventChannel } from 'redux-saga'; import { all, spawn, take } from 'typed-redux-saga/macro'; diff --git a/yarn.lock b/yarn.lock index 8df7db079..ea1ce5f2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2378,15 +2378,6 @@ __metadata: languageName: node linkType: hard -"@googleapis/drive@npm:^8.14.0": - version: 8.14.0 - resolution: "@googleapis/drive@npm:8.14.0" - dependencies: - googleapis-common: ^7.0.0 - checksum: f415fee41a4903dbdb98109760b64e7ce769ca6ab7e0648dfebb831e65d6ca156901d8e32cba3d79b42f1b635b1723b2c0727b0c5c7c9b0cf65ece98b5bd56dd - languageName: node - linkType: hard - "@humanwhocodes/config-array@npm:^0.11.13": version: 0.11.13 resolution: "@humanwhocodes/config-array@npm:0.11.13" @@ -2982,7 +2973,6 @@ __metadata: dependencies: "@babel/core": ^7.23.9 "@blueprintjs/core": ^5.8.2 - "@googleapis/drive": ^8.14.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.11 "@pybricks/firmware": 7.10.0 "@pybricks/ide-docs": 2.14.0 @@ -6216,15 +6206,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2": - version: 7.1.1 - resolution: "agent-base@npm:7.1.1" - dependencies: - debug: ^4.3.4 - checksum: 51c158769c5c051482f9ca2e6e1ec085ac72b5a418a9b31b4e82fe6c0a6699adb94c1c42d246699a587b3335215037091c79e0de512c516f73b6ea844202f037 - languageName: node - linkType: hard - "agentkeepalive@npm:^4.2.1": version: 4.2.1 resolution: "agentkeepalive@npm:4.2.1" @@ -6887,13 +6868,6 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.3.0": - version: 1.5.1 - resolution: "base64-js@npm:1.5.1" - checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 - languageName: node - linkType: hard - "batch@npm:0.6.1": version: 0.6.1 resolution: "batch@npm:0.6.1" @@ -6928,13 +6902,6 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.0.0": - version: 9.1.2 - resolution: "bignumber.js@npm:9.1.2" - checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -7071,13 +7038,6 @@ __metadata: languageName: node linkType: hard -"buffer-equal-constant-time@npm:1.0.1": - version: 1.0.1 - resolution: "buffer-equal-constant-time@npm:1.0.1" - checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab - languageName: node - linkType: hard - "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -8624,15 +8584,6 @@ __metadata: languageName: node linkType: hard -"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": - version: 1.0.11 - resolution: "ecdsa-sig-formatter@npm:1.0.11" - dependencies: - safe-buffer: ^5.0.1 - checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 - languageName: node - linkType: hard - "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -9605,13 +9556,6 @@ __metadata: languageName: node linkType: hard -"extend@npm:^3.0.2": - version: 3.0.2 - resolution: "extend@npm:3.0.2" - checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 - languageName: node - linkType: hard - "fake-indexeddb@npm:^5.0.2": version: 5.0.2 resolution: "fake-indexeddb@npm:5.0.2" @@ -10043,29 +9987,6 @@ __metadata: languageName: node linkType: hard -"gaxios@npm:^6.0.0, gaxios@npm:^6.0.3, gaxios@npm:^6.1.1": - version: 6.7.1 - resolution: "gaxios@npm:6.7.1" - dependencies: - extend: ^3.0.2 - https-proxy-agent: ^7.0.1 - is-stream: ^2.0.0 - node-fetch: ^2.6.9 - uuid: ^9.0.1 - checksum: ed5952655339918e0868c6f4e079d6e9e55b20a242ddb1be25076cdfb0dd1ca5a2cb233da7352baa972c19f898a78fa6ba67e7d848717c9ca9877c269b5b02f7 - languageName: node - linkType: hard - -"gcp-metadata@npm:^6.1.0": - version: 6.1.0 - resolution: "gcp-metadata@npm:6.1.0" - dependencies: - gaxios: ^6.0.0 - json-bigint: ^1.0.0 - checksum: 55de8ae4a6b7664379a093abf7e758ae06e82f244d41bd58d881a470bf34db94c4067ce9e1b425d9455b7705636d5f8baad844e49bb73879c338753ba7785b2b - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -10275,20 +10196,6 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^9.7.0": - version: 9.14.1 - resolution: "google-auth-library@npm:9.14.1" - dependencies: - base64-js: ^1.3.0 - ecdsa-sig-formatter: ^1.0.11 - gaxios: ^6.1.1 - gcp-metadata: ^6.1.0 - gtoken: ^7.0.0 - jws: ^4.0.0 - checksum: 98c7ffb6ef8d811a54d728a94c31aa60c777f035306f0ded70654ce0aa1f4dcf393bb505b262aa48438f5ead8941248f3759f24f0e22b4465b8537b1d90415ac - languageName: node - linkType: hard - "google-drive-picker@npm:^1.1.29": version: 1.1.29 resolution: "google-drive-picker@npm:1.1.29" @@ -10303,20 +10210,6 @@ __metadata: languageName: node linkType: hard -"googleapis-common@npm:^7.0.0": - version: 7.2.0 - resolution: "googleapis-common@npm:7.2.0" - dependencies: - extend: ^3.0.2 - gaxios: ^6.0.3 - google-auth-library: ^9.7.0 - qs: ^6.7.0 - url-template: ^2.0.8 - uuid: ^9.0.0 - checksum: 58f520134c9d6f439ef81919471689f0278ef0ffdbd50c693a59282d95141be74df3a5614c25347c140fc44201e0ef998300389f4cbf51502f2351e67c758ab6 - languageName: node - linkType: hard - "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -10340,16 +10233,6 @@ __metadata: languageName: node linkType: hard -"gtoken@npm:^7.0.0": - version: 7.1.0 - resolution: "gtoken@npm:7.1.0" - dependencies: - gaxios: ^6.0.0 - jws: ^4.0.0 - checksum: 1f338dced78f9d895ea03cd507454eb5a7b77e841ecd1d45e44483b08c1e64d16a9b0342358d37586d87462ffc2d5f5bff5dfe77ed8d4f0aafc3b5b0347d5d16 - languageName: node - linkType: hard - "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -10676,16 +10559,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": - version: 7.0.5 - resolution: "https-proxy-agent@npm:7.0.5" - dependencies: - agent-base: ^7.0.2 - debug: 4 - checksum: 2e1a28960f13b041a50702ee74f240add8e75146a5c37fc98f1960f0496710f6918b3a9fe1e5aba41e50f58e6df48d107edd9c405c5f0d73ac260dabf2210857 - languageName: node - linkType: hard - "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -12039,15 +11912,6 @@ __metadata: languageName: node linkType: hard -"json-bigint@npm:^1.0.0": - version: 1.0.0 - resolution: "json-bigint@npm:1.0.0" - dependencies: - bignumber.js: ^9.0.0 - checksum: c67bb93ccb3c291e60eb4b62931403e378906aab113ec1c2a8dd0f9a7f065ad6fd9713d627b732abefae2e244ac9ce1721c7a3142b2979532f12b258634ce6f6 - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -12156,27 +12020,6 @@ __metadata: languageName: node linkType: hard -"jwa@npm:^2.0.0": - version: 2.0.0 - resolution: "jwa@npm:2.0.0" - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: ^5.0.1 - checksum: 8f00b71ad5fe94cb55006d0d19202f8f56889109caada2f7eeb63ca81755769ce87f4f48101967f398462e3b8ae4faebfbd5a0269cb755dead5d63c77ba4d2f1 - languageName: node - linkType: hard - -"jws@npm:^4.0.0": - version: 4.0.0 - resolution: "jws@npm:4.0.0" - dependencies: - jwa: ^2.0.0 - safe-buffer: ^5.0.1 - checksum: d68d07aa6d1b8cb35c363a9bd2b48f15064d342a5d9dc18a250dbbce8dc06bd7e4792516c50baa16b8d14f61167c19e851fd7f66b59ecc68b7f6a013759765f7 - languageName: node - linkType: hard - "kind-of@npm:^6.0.2": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -12960,20 +12803,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.9": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: ^5.0.0 - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 - languageName: node - linkType: hard - "node-forge@npm:^1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -14625,7 +14454,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.13.0, qs@npm:^6.7.0": +"qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -15462,7 +15291,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 @@ -17192,13 +17021,6 @@ __metadata: languageName: node linkType: hard -"url-template@npm:^2.0.8": - version: 2.0.8 - resolution: "url-template@npm:2.0.8" - checksum: 4183fccd74e3591e4154134d4443dccecba9c455c15c7df774f1f1e3fa340fd9bffb903b5beec347196d15ce49c34edf6dec0634a95d170ad6e78c0467d6e13e - languageName: node - linkType: hard - "use-sync-external-store@npm:^1.0.0": version: 1.1.0 resolution: "use-sync-external-store@npm:1.1.0" @@ -17255,15 +17077,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0, uuid@npm:^9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" - bin: - uuid: dist/bin/uuid - checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 - languageName: node - linkType: hard - "v8-to-istanbul@npm:^9.0.1": version: 9.0.1 resolution: "v8-to-istanbul@npm:9.0.1" From 99951e4ce2af1600f5dd5578005db4a48f9c4a54 Mon Sep 17 00:00:00 2001 From: scatwang Date: Wed, 2 Oct 2024 00:05:28 -0700 Subject: [PATCH 04/12] logic for upload success and failed --- .vscode/settings.json | 3 +- package.json | 2 + .../GoogleDriveUploadDialog.tsx | 150 ++++++++++++------ .../googleDriveUploadDialog/reducers.ts | 22 ++- src/explorer/googleDriveUploadDialog/sagas.ts | 44 ++--- .../translations/en.json | 2 + yarn.lock | 104 ++++++++++++ 7 files changed, 256 insertions(+), 71 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 789111723..7346f0a3f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,5 +57,6 @@ "enforceHeader": true, "include": ["scss", "typescript", "typescriptreact"], "replace": ["Copyright"] - } + }, + "cSpell.words": ["blueprintjs", "Pybricks"] } diff --git a/package.json b/package.json index 474172235..cc1c15aa5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "file-loader": "^6.2.0", "fs-extra": "^11.2.0", "google-drive-picker": "^1.1.29", + "googleapi": "^1.0.2", "html-webpack-plugin": "^5.6.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", @@ -102,6 +103,7 @@ "react-splitter-layout": "^4.0.0", "redux": "^4.2.1", "redux-logger": "^3.0.6", + "redux-resource-xhr": "^4.0.2", "redux-saga": "^1.3.0", "resolve": "^1.22.8", "resolve-url-loader": "^5.0.0", diff --git a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx index 964b1850c..29e120aeb 100644 --- a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx +++ b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx @@ -1,7 +1,15 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2024 The Pybricks Authors -import { Button, Classes, Dialog, Spinner } from '@blueprintjs/core'; +import { + Button, + Classes, + Dialog, + FormGroup, + Icon, + InputGroup, + Spinner, +} from '@blueprintjs/core'; import GoogleDrivePicker from 'google-drive-picker'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -18,16 +26,21 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { const dispatch = useDispatch(); const isOpen = useSelector((s) => s.explorer.googleDriveUploadDialog.isOpen); const fileName = useSelector((s) => s.explorer.googleDriveUploadDialog.fileName); + const driveDocId = useSelector( + (s) => s.explorer.googleDriveUploadDialog.driveDocId, + ); + const isUploadFailed = useSelector( + (s) => s.explorer.googleDriveUploadDialog.isUploadFailed, + ); const inputRef = useRef(null); const [authToken, setAuthToken] = useState(''); - const [openPicker, authRes] = GoogleDrivePicker(); - const [dest, setDest] = useState(); - const [uploading, setUploading] = useState(false); + const [openPicker, authResponse] = GoogleDrivePicker(); + const [destFolder, setDestFolder] = useState(); + const [uploadStarted, setUploadStarted] = useState(false); - const handlePickerOpen = () => { - console.log('token:', authToken); + const handleOpenPicker = () => { openPicker({ clientId: '1034337501504-of3um354h2dsdm200bhjnfpk6cg0m0n6.apps.googleusercontent.com', @@ -38,32 +51,25 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { setSelectFolderEnabled: true, supportDrives: true, callbackFunction: (data: PickerResponse) => { - if (data.action === 'cancel') { - console.log('User clicked cancel/close button'); - } - console.log(data); - if (data && data.docs) { - setDest(data.docs[0]); + if (data.action === 'picked' && data.docs) { + setDestFolder(data.docs[0]); } }, }); - console.log('token:', authToken); }; useEffect(() => { - if (authRes) { - setAuthToken(authRes.access_token); + if (authResponse) { + setAuthToken(authResponse.access_token); } - }, [authRes]); + }, [authResponse]); const handleUpload = () => { - setUploading(true); - dispatch(googleDriveUploadDialogUpload(fileName, authToken, dest?.id || '')); - - setTimeout(() => { - setUploading(false); - dispatch(googleDriveUploadDialogDidCancel()); - }, 500); + setUploadStarted(true); + if (!destFolder) { + return; + } + dispatch(googleDriveUploadDialogUpload(fileName, authToken, destFolder.id)); }; const handleClose = useCallback(() => { @@ -83,34 +89,78 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => {
- Upload to: - {dest && ( - - )} - -
-
-
-
- {uploading && } - - + + } + rightElement={ +
+ {destFolder && ( + + + + )} + +
+ } + onMouseDown={(e) => e.stopPropagation()} + /> + +
+ {driveDocId && destFolder && ( +
+ Uploaded to: {destFolder.name}/ + + {fileName} + +
+ )} + {isUploadFailed &&
Upload failed.
} +
+
+ {uploadStarted && driveDocId === '' && !isUploadFailed && ( + + )} + + +
diff --git a/src/explorer/googleDriveUploadDialog/reducers.ts b/src/explorer/googleDriveUploadDialog/reducers.ts index 073875968..66c3239b5 100644 --- a/src/explorer/googleDriveUploadDialog/reducers.ts +++ b/src/explorer/googleDriveUploadDialog/reducers.ts @@ -4,6 +4,8 @@ import { Reducer, combineReducers } from 'redux'; import { googleDriveUploadDialogDidCancel, + googleDriveUploadDialogDidUploadFile, + googleDriveUploadDialogFailedToUploadFile, googleDriveUploadDialogShow, } from './actions'; @@ -30,4 +32,22 @@ const fileName: Reducer = (state = initialDialogFileName, action) => { return state; }; -export default combineReducers({ isOpen, fileName }); +const driveDocId: Reducer = (state = '', action) => { + if (googleDriveUploadDialogDidUploadFile.matches(action)) { + return action.googleDriveDocId; + } + return state; +}; + +const isUploadFailed: Reducer = (state = false, action) => { + if (googleDriveUploadDialogFailedToUploadFile.matches(action)) { + return false; + } + return state; +}; +export default combineReducers({ + isOpen, + fileName, + driveDocId, + isUploadFailed, +}); diff --git a/src/explorer/googleDriveUploadDialog/sagas.ts b/src/explorer/googleDriveUploadDialog/sagas.ts index 1db59e761..603462130 100644 --- a/src/explorer/googleDriveUploadDialog/sagas.ts +++ b/src/explorer/googleDriveUploadDialog/sagas.ts @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2024 The Pybricks Authors -import { put, race, take, takeEvery } from 'typed-redux-saga/macro'; +import { call, put, race, take, takeEvery } from 'typed-redux-saga/macro'; import { fileStorageDidFailToReadFile, fileStorageDidReadFile, fileStorageReadFile, } from '../../fileStorage/actions'; +import { pythonFileMimeType } from '../../pybricksMicropython/lib'; import { defined, ensureError } from '../../utils'; import { @@ -44,32 +45,37 @@ function* handleGoogleDriveUploadDialogUploadFile( [ JSON.stringify({ name: action.fileName, - mimeType: 'text/plain', + mimeType: pythonFileMimeType, parents: [action.targetFolderId], }), ], { type: 'application/json' }, ), ); - form.append('file', new Blob([didRead.contents], { type: 'text/plain' })); + form.append('file', new Blob([didRead.contents], { type: pythonFileMimeType })); - const xhr = new XMLHttpRequest(); - xhr.open( - 'post', - 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', - ); - xhr.setRequestHeader('Authorization', 'Bearer ' + action.authToken); - xhr.responseType = 'json'; - xhr.onload = () => { - console.log('Google drive file id:', xhr.response.id); - }; - xhr.onerror = (event) => { - console.log('Failed to upload file to Google Drive:', event); - }; - xhr.send(form); + const uploadFile = new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open( + 'post', + 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', + ); + xhr.setRequestHeader('Authorization', 'Bearer ' + action.authToken); + xhr.responseType = 'json'; + xhr.onload = () => { + console.log('Google drive file id:', xhr.response.id); + resolve(xhr.response.id); + }; + xhr.onerror = (event) => { + console.log('Failed to upload file to Google Drive:', event); + reject(event); + }; + xhr.send(form); + }); + + const fileId = yield* call(() => uploadFile); - // TBD - yield* put(googleDriveUploadDialogDidUploadFile(xhr.response.id)); + yield* put(googleDriveUploadDialogDidUploadFile(fileId)); } catch (err) { console.log('Failed to upload file to Google Drive:', err); yield* put(googleDriveUploadDialogFailedToUploadFile(ensureError(err))); diff --git a/src/explorer/googleDriveUploadDialog/translations/en.json b/src/explorer/googleDriveUploadDialog/translations/en.json index 309c0960b..4125cdb39 100644 --- a/src/explorer/googleDriveUploadDialog/translations/en.json +++ b/src/explorer/googleDriveUploadDialog/translations/en.json @@ -1,5 +1,7 @@ { "title": "Upload '{fileName}' to Google Drive", + "upload_to": "Upload To", + "upload_to_sub_label": "Choose a destination folder on your Google Drive", "action": { "change_destination": "Change Destination", diff --git a/yarn.lock b/yarn.lock index ea1ce5f2d..201b7eb3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3040,6 +3040,7 @@ __metadata: file-loader: ^6.2.0 fs-extra: ^11.2.0 google-drive-picker: ^1.1.29 + googleapi: ^1.0.2 html-webpack-plugin: ^5.6.0 identity-obj-proxy: ^3.0.0 jest: ^29.7.0 @@ -3078,6 +3079,7 @@ __metadata: react-splitter-layout: ^4.0.0 redux: ^4.2.1 redux-logger: ^3.0.6 + redux-resource-xhr: ^4.0.2 redux-saga: ^1.3.0 resolve: ^1.22.8 resolve-url-loader: ^5.0.0 @@ -8497,6 +8499,13 @@ __metadata: languageName: node linkType: hard +"dom-walk@npm:^0.1.0": + version: 0.1.2 + resolution: "dom-walk@npm:0.1.2" + checksum: 19eb0ce9c6de39d5e231530685248545d9cd2bd97b2cb3486e0bfc0f2a393a9addddfd5557463a932b52fdfcf68ad2a619020cd2c74a5fe46fbecaa8e80872f3 + languageName: node + linkType: hard + "domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" @@ -10130,6 +10139,16 @@ __metadata: languageName: node linkType: hard +"global@npm:~4.4.0": + version: 4.4.0 + resolution: "global@npm:4.4.0" + dependencies: + min-document: ^2.19.0 + process: ^0.11.10 + checksum: 9c057557c8f5a5bcfbeb9378ba4fe2255d04679452be504608dd5f13b54edf79f7be1db1031ea06a4ec6edd3b9f5f17d2d172fb47e6c69dae57fd84b7e72b77f + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -10210,6 +10229,13 @@ __metadata: languageName: node linkType: hard +"googleapi@npm:^1.0.2": + version: 1.0.2 + resolution: "googleapi@npm:1.0.2" + checksum: f3d7eb9e80e236a68c92bb58578d941ba260cea9cb54351c03fb41a249f04212f040d63a72b7c529466788d02e8a9dc70b355ca1b4dea3c035c035521d28a53c + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -10902,6 +10928,13 @@ __metadata: languageName: node linkType: hard +"is-function@npm:^1.0.1": + version: 1.0.2 + resolution: "is-function@npm:1.0.2" + checksum: 7d564562e07b4b51359547d3ccc10fb93bb392fd1b8177ae2601ee4982a0ece86d952323fc172a9000743a3971f09689495ab78a1d49a9b14fc97a7e28521dc0 + languageName: node + linkType: hard + "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -12468,6 +12501,15 @@ __metadata: languageName: node linkType: hard +"min-document@npm:^2.19.0": + version: 2.19.0 + resolution: "min-document@npm:2.19.0" + dependencies: + dom-walk: ^0.1.0 + checksum: da6437562ea2228041542a2384528e74e22d1daa1a4ec439c165abf0b9d8a63e17e3b8a6dc6e0c731845e85301198730426932a0e813d23f932ca668340c9623 + languageName: node + linkType: hard + "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -13265,6 +13307,13 @@ __metadata: languageName: node linkType: hard +"parse-headers@npm:^2.0.0": + version: 2.0.5 + resolution: "parse-headers@npm:2.0.5" + checksum: 3e97f01e4c7f960bfbfd0ee489f0bd8d3c72b6c814f1f79b66abec2cca8eaf8e4ecd89deba0b6e61266469aed87350bc932001181c01ff8c29a59e696abe251f + languageName: node + linkType: hard + "parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -14366,6 +14415,13 @@ __metadata: languageName: node linkType: hard +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: bfcce49814f7d172a6e6a14d5fa3ac92cc3d0c3b9feb1279774708a719e19acd673995226351a082a9ae99978254e320ccda4240ddc474ba31a76c79491ca7c3 + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -14463,6 +14519,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^1.0.0": + version: 1.0.0 + resolution: "querystringify@npm:1.0.0" + checksum: 1d6ca0a6f3af658fe3a3365a8c31bba19c6b7607d0a0be77a76638a60371316e6e5a26957748425c7f7a2e27fcb5e0ccbc429222931f4cbddea2621e8c04209b + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -14917,6 +14980,28 @@ __metadata: languageName: node linkType: hard +"redux-resource-xhr@npm:^4.0.2": + version: 4.0.2 + resolution: "redux-resource-xhr@npm:4.0.2" + dependencies: + querystringify: ^1.0.0 + redux-resource: ^3.0.0 + xhr: ^2.4.0 + peerDependencies: + redux: 3.x || 4.x + checksum: 9b79d4b7c587fc30bc148c49c31c5483ca623a0ca7575591dad6e3d1747fdb9e86a486cf0d6ef8d336d3789bba8a9ac66215df7ef9ef1d7302085425d6015c3f + languageName: node + linkType: hard + +"redux-resource@npm:^3.0.0": + version: 3.1.1 + resolution: "redux-resource@npm:3.1.1" + peerDependencies: + redux: 3.x || 4.x + checksum: acff25d7086702faea6903d63d089b74a443f65fe52b85db45bd1303ed45d5b09e133898f86deb75a15623d4bda386e643825718670b2b89a1cc7ca583862edc + languageName: node + linkType: hard + "redux-saga@npm:^1.3.0": version: 1.3.0 resolution: "redux-saga@npm:1.3.0" @@ -17732,6 +17817,18 @@ __metadata: languageName: node linkType: hard +"xhr@npm:^2.4.0": + version: 2.6.0 + resolution: "xhr@npm:2.6.0" + dependencies: + global: ~4.4.0 + is-function: ^1.0.1 + parse-headers: ^2.0.0 + xtend: ^4.0.0 + checksum: a1db277e37737caf3ed363d2a33ce4b4ea5b5fc190b663a6f70bc252799185b840ccaa166eaeeea4841c9c60b87741f0a24e29cbcf6708dd425986d4df186d2f + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0" @@ -17746,6 +17843,13 @@ __metadata: languageName: node linkType: hard +"xtend@npm:^4.0.0": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a + languageName: node + linkType: hard + "xterm-addon-fit@npm:^0.8.0": version: 0.8.0 resolution: "xterm-addon-fit@npm:0.8.0" From 1cbb56276bf51058f733cdb5ddfa748b05e90ab9 Mon Sep 17 00:00:00 2001 From: scatwang Date: Wed, 2 Oct 2024 00:17:59 -0700 Subject: [PATCH 05/12] reset uploading after close --- .../googleDriveUploadDialog/GoogleDriveUploadDialog.tsx | 1 + src/explorer/googleDriveUploadDialog/reducers.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx index 29e120aeb..75985291b 100644 --- a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx +++ b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx @@ -73,6 +73,7 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { }; const handleClose = useCallback(() => { + setUploadStarted(false); dispatch(googleDriveUploadDialogDidCancel()); }, [dispatch]); diff --git a/src/explorer/googleDriveUploadDialog/reducers.ts b/src/explorer/googleDriveUploadDialog/reducers.ts index 66c3239b5..163ddb25d 100644 --- a/src/explorer/googleDriveUploadDialog/reducers.ts +++ b/src/explorer/googleDriveUploadDialog/reducers.ts @@ -33,6 +33,9 @@ const fileName: Reducer = (state = initialDialogFileName, action) => { }; const driveDocId: Reducer = (state = '', action) => { + if (googleDriveUploadDialogShow.matches(action)) { + return ''; + } if (googleDriveUploadDialogDidUploadFile.matches(action)) { return action.googleDriveDocId; } @@ -40,6 +43,9 @@ const driveDocId: Reducer = (state = '', action) => { }; const isUploadFailed: Reducer = (state = false, action) => { + if (googleDriveUploadDialogShow.matches(action)) { + return false; + } if (googleDriveUploadDialogFailedToUploadFile.matches(action)) { return false; } From 8de154e58644965109669332efdf52206962e5bd Mon Sep 17 00:00:00 2001 From: scatwang Date: Sat, 5 Oct 2024 22:54:40 -0700 Subject: [PATCH 06/12] refactor --- src/app/constants.ts | 9 +- .../protocol.ts => components/GoogleApi.ts} | 0 src/explorer/Explorer.tsx | 11 ++- src/explorer/actions.ts | 7 +- .../GoogleDriveUploadDialog.tsx | 76 ++++++--------- .../googleDriveUploadDialog/actions.ts | 37 ------- .../googleDriveUploadDialog/reducers.ts | 30 ++++-- src/explorer/sagas.ts | 53 +++++++--- src/explorer/translations/en.json | 3 +- src/fileStorage/sagas.ts | 2 +- src/googleDrive/GoogleDrive.tsx | 96 +++++++++++++++++++ src/googleDrive/actions.ts | 66 +++++++++++++ src/googleDrive/protocol.ts | 29 ++++++ .../sagas.ts | 67 +++++++++---- src/googleDrive/utils.ts | 24 +++++ src/sagas.ts | 2 + 16 files changed, 382 insertions(+), 130 deletions(-) rename src/{explorer/googleDriveUploadDialog/protocol.ts => components/GoogleApi.ts} (100%) create mode 100644 src/googleDrive/GoogleDrive.tsx create mode 100644 src/googleDrive/actions.ts create mode 100644 src/googleDrive/protocol.ts rename src/{explorer/googleDriveUploadDialog => googleDrive}/sagas.ts (53%) create mode 100644 src/googleDrive/utils.ts diff --git a/src/app/constants.ts b/src/app/constants.ts index 90d4addcc..7c9e4cded 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2023 The Pybricks Authors +// Copyright (c) 2021-2024 The Pybricks Authors import docsPackage from '@pybricks/ide-docs/package.json'; // Definitions for compile-time UI settings. @@ -87,3 +87,10 @@ export const zipFileExtension = '.zip'; /** The ZIP file MIME type ('application/zip') */ export const zipFileMimeType = 'application/zip'; + +/** Google Cloud API key */ +export const googleApiKey = 'AIzaSyBMKnuqNI3N0r95XNns1tT7TYJHGkM5juU'; + +/** Google OAuth 2.0 Client ID */ +export const googleClientId = + '1034337501504-of3um354h2dsdm200bhjnfpk6cg0m0n6.apps.googleusercontent.com'; diff --git a/src/explorer/googleDriveUploadDialog/protocol.ts b/src/components/GoogleApi.ts similarity index 100% rename from src/explorer/googleDriveUploadDialog/protocol.ts rename to src/components/GoogleApi.ts diff --git a/src/explorer/Explorer.tsx b/src/explorer/Explorer.tsx index 2b894c28e..124d0183c 100644 --- a/src/explorer/Explorer.tsx +++ b/src/explorer/Explorer.tsx @@ -47,6 +47,7 @@ import { Toolbar } from '../components/toolbar/Toolbar'; import { useToolbarItemFocus } from '../components/toolbar/aria'; import { UUID } from '../fileStorage'; import { useFileStorageMetadata } from '../fileStorage/hooks'; +import DownloadPicker from '../googleDrive/GoogleDrive'; import { isMacOS } from '../utils/os'; import { RenderProps, @@ -60,7 +61,6 @@ import { explorerDeleteFile, explorerDuplicateFile, explorerExportFile, - explorerImportFileFromGoogleDrive, explorerImportFiles, explorerRenameFile, explorerUploadFileToGoogleDrive, @@ -195,12 +195,13 @@ const FileActionButtonGroup: React.FunctionComponent = ( // matches ID in tour component const archiveButtonId = 'pb-explorer-archive-button'; const newButtonId = 'pb-explorer-add-button'; -const downloadFromGoogleDriveButtonId = 'pb-download-from-google-drive-button'; +const importFromGoogleDriveButtonId = 'pb-download-from-google-drive-button'; const Header: React.FunctionComponent = () => { const exportButtonId = useId(); const dispatch = useDispatch(); const i18n = useI18n(); + const openDownloadPicker = DownloadPicker(); return ( { > } - tooltip="Import file from Google Cloud" - onClick={() => dispatch(explorerImportFileFromGoogleDrive())} + tooltip={i18n.translate('header.toolbar.importFromGoogleDrive')} + onClick={() => openDownloadPicker()} /> ( error, })); +/** + * Action that requests to upload files to Google Drive. + */ export const explorerUploadFileToGoogleDrive = createAction((fileName: string) => ({ type: 'explorer.action.uploadFileToGoogleDrive', fileName, })); -export const explorerImportFileFromGoogleDrive = createAction(() => ({ - type: 'explorer.action.importFileFromGoogleDrive', -})); - /** * Action that requests to import (upload) files into the app. */ diff --git a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx index 75985291b..9e9ec58dd 100644 --- a/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx +++ b/src/explorer/googleDriveUploadDialog/GoogleDriveUploadDialog.tsx @@ -10,24 +10,24 @@ import { InputGroup, Spinner, } from '@blueprintjs/core'; -import GoogleDrivePicker from 'google-drive-picker'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { FolderPicker } from '../../googleDrive/GoogleDrive'; +import { googleDriveUploadFile } from '../../googleDrive/actions'; import { useSelector } from '../../reducers'; -import { - googleDriveUploadDialogDidCancel, - googleDriveUploadDialogUpload, -} from './actions'; +import { googleDriveUploadDialogDidCancel } from './actions'; import { useI18n } from './i18n'; -import { DriveDocument, PickerResponse } from './protocol'; const GoogleDriveUploadDialog: React.FunctionComponent = () => { const i18n = useI18n(); const dispatch = useDispatch(); const isOpen = useSelector((s) => s.explorer.googleDriveUploadDialog.isOpen); const fileName = useSelector((s) => s.explorer.googleDriveUploadDialog.fileName); - const driveDocId = useSelector( - (s) => s.explorer.googleDriveUploadDialog.driveDocId, + const destFolder = useSelector( + (s) => s.explorer.googleDriveUploadDialog.descFolder, + ); + const uploadedDocId = useSelector( + (s) => s.explorer.googleDriveUploadDialog.uploadedDocId, ); const isUploadFailed = useSelector( (s) => s.explorer.googleDriveUploadDialog.isUploadFailed, @@ -35,41 +35,19 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { const inputRef = useRef(null); - const [authToken, setAuthToken] = useState(''); - const [openPicker, authResponse] = GoogleDrivePicker(); - const [destFolder, setDestFolder] = useState(); const [uploadStarted, setUploadStarted] = useState(false); + const openFolderPicker = FolderPicker(); const handleOpenPicker = () => { - openPicker({ - clientId: - '1034337501504-of3um354h2dsdm200bhjnfpk6cg0m0n6.apps.googleusercontent.com', - developerKey: 'AIzaSyBMKnuqNI3N0r95XNns1tT7TYJHGkM5juU', - viewId: 'FOLDERS', - token: authToken, - customScopes: ['https://www.googleapis.com/auth/drive'], - setSelectFolderEnabled: true, - supportDrives: true, - callbackFunction: (data: PickerResponse) => { - if (data.action === 'picked' && data.docs) { - setDestFolder(data.docs[0]); - } - }, - }); + openFolderPicker(); }; - useEffect(() => { - if (authResponse) { - setAuthToken(authResponse.access_token); - } - }, [authResponse]); - const handleUpload = () => { setUploadStarted(true); - if (!destFolder) { + if (!destFolder.folder) { return; } - dispatch(googleDriveUploadDialogUpload(fileName, authToken, destFolder.id)); + dispatch(googleDriveUploadFile(fileName, destFolder.folder.id)); }; const handleClose = useCallback(() => { @@ -96,18 +74,20 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { > + destFolder.folder && ( + + ) } rightElement={ - {driveDocId && destFolder && ( + {uploadedDocId && destFolder.folder && (
- Uploaded to: {destFolder.name}/ + Uploaded to: {destFolder.folder.name}/ @@ -143,15 +127,15 @@ const GoogleDriveUploadDialog: React.FunctionComponent = () => { {isUploadFailed &&
Upload failed.
}
- {uploadStarted && driveDocId === '' && !isUploadFailed && ( - - )} + {uploadStarted && + uploadedDocId === '' && + !isUploadFailed && }
)} {isUploadFailed &&
Upload failed.
} diff --git a/src/explorer/googleDriveUploadDialog/reducers.ts b/src/explorer/googleDriveUploadDialog/reducers.ts index c4e222159..f2b348cfd 100644 --- a/src/explorer/googleDriveUploadDialog/reducers.ts +++ b/src/explorer/googleDriveUploadDialog/reducers.ts @@ -57,6 +57,16 @@ const uploadedDocId: Reducer = (state = '', action) => { return state; }; +const overwroteExistingFile: Reducer = (state = false, action) => { + if (googleDriveUploadDialogShow.matches(action)) { + return false; + } + if (googleDriveDidUploadFile.matches(action)) { + return action.overwroteExistingFile; + } + return state; +}; + const isUploadFailed: Reducer = (state = false, action) => { if (googleDriveUploadDialogShow.matches(action)) { return false; @@ -66,10 +76,12 @@ const isUploadFailed: Reducer = (state = false, action) => { } return state; }; + export default combineReducers({ isOpen, fileName, descFolder, uploadedDocId, isUploadFailed, + overwroteExistingFile, }); diff --git a/src/googleDrive/actions.ts b/src/googleDrive/actions.ts index e1695bfad..c32cfe1f2 100644 --- a/src/googleDrive/actions.ts +++ b/src/googleDrive/actions.ts @@ -17,10 +17,13 @@ export const googleDriveUploadFile = createAction( }), ); -export const googleDriveDidUploadFile = createAction((uploadedFileId: string) => ({ - type: 'googleDrive.action.didUploadFile', - uploadedFileId, -})); +export const googleDriveDidUploadFile = createAction( + (uploadedFileId: string, overwroteExistingFile: boolean) => ({ + type: 'googleDrive.action.didUploadFile', + uploadedFileId, + overwroteExistingFile, + }), +); export const googleDriveFailToUploadFile = createAction((err: Error) => ({ type: 'googleDrive.action.failToUploadFile', diff --git a/src/googleDrive/sagas.ts b/src/googleDrive/sagas.ts index 6564d3da2..678ba903f 100644 --- a/src/googleDrive/sagas.ts +++ b/src/googleDrive/sagas.ts @@ -151,7 +151,7 @@ function* handleUploadFile( const fileId = yield* call(() => uploadFile); - yield* put(googleDriveDidUploadFile(fileId)); + yield* put(googleDriveDidUploadFile(fileId, existingFile !== undefined)); } catch (err) { console.log('Failed to upload file to Google Drive:', err); yield* put(googleDriveFailToUploadFile(ensureError(err)));