mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-31 14:00:43 +00:00
refactor and fix review feedback
This commit is contained in:
parent
d69fff10f5
commit
2601279ecb
25 changed files with 585 additions and 630 deletions
|
|
@ -11,6 +11,8 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/google-drive-picker": "workspace:*",
|
||||
"@uppy/google-photos-picker": "workspace:*",
|
||||
"@uppy/react": "workspace:*",
|
||||
"@uppy/remote-sources": "workspace:*",
|
||||
"@uppy/screen-capture": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/** biome-ignore-all lint/nursery/useUniqueElementIds: it's fine */
|
||||
import Uppy from '@uppy/core'
|
||||
import GoogleDrivePicker from '@uppy/google-drive-picker'
|
||||
import {
|
||||
Dropzone,
|
||||
FilesGrid,
|
||||
|
|
@ -19,16 +20,36 @@ import Webcam from './Webcam'
|
|||
|
||||
import './app.css'
|
||||
import '@uppy/react/css/style.css'
|
||||
import GooglePhotosPicker from '@uppy/google-photos-picker'
|
||||
|
||||
const companionUrl = 'http://localhost:3020'
|
||||
const googlePickerClientId = '' // see GOOGLE_PICKER_CLIENT_ID in dev Dashboard
|
||||
const googlePickerApiKey = '' // see GOOGLE_PICKER_API_KEY in dev Dashboard
|
||||
const googlePickerAppId = '' // see GOOGLE_PICKER_APP_ID in dev Dashboard
|
||||
|
||||
function App() {
|
||||
const [uppy] = useState(() =>
|
||||
new Uppy()
|
||||
new Uppy({
|
||||
restrictions: {
|
||||
maxNumberOfFiles: 1,
|
||||
},
|
||||
})
|
||||
.use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
})
|
||||
.use(GoogleDrivePicker, {
|
||||
clientId: googlePickerClientId,
|
||||
companionUrl,
|
||||
apiKey: googlePickerApiKey,
|
||||
appId: googlePickerAppId,
|
||||
})
|
||||
.use(GooglePhotosPicker, {
|
||||
clientId: googlePickerClientId,
|
||||
companionUrl,
|
||||
})
|
||||
.use(UppyWebcam)
|
||||
.use(UppyScreenCapture)
|
||||
.use(UppyRemoteSources, { companionUrl: 'http://localhost:3020' }),
|
||||
.use(UppyRemoteSources, { companionUrl }),
|
||||
)
|
||||
|
||||
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||
|
|
|
|||
|
|
@ -11,26 +11,15 @@ export interface CustomDropzoneProps {
|
|||
openModal: (plugin: CustomDropzonePlugin) => void
|
||||
}
|
||||
|
||||
const companionUrl = 'http://localhost:3020'
|
||||
const googlePickerClientId = '' // see GOOGLE_PICKER_CLIENT_ID in dev Dashboard
|
||||
const googlePickerApiKey = '' // see GOOGLE_PICKER_API_KEY in dev Dashboard
|
||||
const googlePickerAppId = '' // see GOOGLE_PICKER_APP_ID in dev Dashboard
|
||||
|
||||
export function CustomDropzone({ openModal }: CustomDropzoneProps) {
|
||||
const { getRootProps, getInputProps } = useDropzone({ noClick: true })
|
||||
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||
|
||||
const googleDrivePicker = useGooglePicker({
|
||||
clientId: googlePickerClientId,
|
||||
companionUrl: companionUrl,
|
||||
apiKey: googlePickerApiKey,
|
||||
appId: googlePickerAppId,
|
||||
pickerType: 'drive',
|
||||
})
|
||||
|
||||
const googlePhotosPicker = useGooglePicker({
|
||||
clientId: googlePickerClientId,
|
||||
companionUrl: companionUrl,
|
||||
pickerType: 'photos',
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@
|
|||
"p-retry": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/google.accounts": "^0.0.14",
|
||||
"@types/google.picker": "^0.0.42",
|
||||
"jsdom": "^26.1.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
import type Uppy from '@uppy/core'
|
||||
|
||||
export type GooglePickerType = 'drive' | 'photos'
|
||||
|
||||
import type { UppyEventMap } from '@uppy/core'
|
||||
import type { Meta } from '@uppy/utils'
|
||||
import {
|
||||
type CompanionPluginOptions,
|
||||
type RequestClientOptions,
|
||||
tokenStorage,
|
||||
} from './index.js'
|
||||
|
||||
// https://developers.google.com/photos/picker/reference/rest/v1/mediaItems
|
||||
// Note that the google api doc is not correct, hence some things are optional here but not in their docs
|
||||
|
|
@ -112,9 +122,7 @@ async function injectScript(src: string) {
|
|||
injectedScripts.add(src)
|
||||
}
|
||||
|
||||
export async function ensureScriptsInjected(
|
||||
pickerType: PickerType,
|
||||
): Promise<void> {
|
||||
async function ensureScriptsInjected(pickerType: PickerType): Promise<void> {
|
||||
await Promise.all([
|
||||
injectScript('https://accounts.google.com/gsi/client'), // Google Identity Services
|
||||
(async () => {
|
||||
|
|
@ -149,7 +157,7 @@ async function isTokenValid(
|
|||
return false
|
||||
}
|
||||
|
||||
export async function authorize({
|
||||
async function authorize({
|
||||
pickerType,
|
||||
clientId,
|
||||
accessToken,
|
||||
|
|
@ -190,7 +198,7 @@ export async function authorize({
|
|||
return response.access_token
|
||||
}
|
||||
|
||||
export async function logout(accessToken: string): Promise<void> {
|
||||
async function doLogout(accessToken: string): Promise<void> {
|
||||
await new Promise<void>((resolve) =>
|
||||
google.accounts.oauth2.revoke(accessToken, resolve),
|
||||
)
|
||||
|
|
@ -285,7 +293,7 @@ async function handleDocObjectRecursively({
|
|||
return items
|
||||
}
|
||||
|
||||
export async function showDrivePicker({
|
||||
async function showDrivePicker({
|
||||
token,
|
||||
apiKey,
|
||||
appId,
|
||||
|
|
@ -352,7 +360,7 @@ export async function showDrivePicker({
|
|||
signal?.addEventListener('abort', () => picker.dispose())
|
||||
}
|
||||
|
||||
export async function showPhotosPicker({
|
||||
async function showPhotosPicker({
|
||||
token,
|
||||
pickingSession,
|
||||
onPickingSessionChange,
|
||||
|
|
@ -486,7 +494,7 @@ async function resolvePickedPhotos({
|
|||
})
|
||||
}
|
||||
|
||||
export async function pollPickingSession({
|
||||
async function pollPickingSession({
|
||||
getPickingSession,
|
||||
getAccessToken,
|
||||
onPickingSessionClear,
|
||||
|
|
@ -594,3 +602,238 @@ export const mapPickerFile = (
|
|||
meta: rest.metadata,
|
||||
}) as Meta), // dunno how to type this
|
||||
})
|
||||
|
||||
export function createGooglePickerStoreAdapter({
|
||||
uppy,
|
||||
getPluginState,
|
||||
setPluginState,
|
||||
}: {
|
||||
uppy: Uppy<any, any>
|
||||
getPluginState: () => GooglePickerState
|
||||
setPluginState: (state: Partial<GooglePickerState>) => void
|
||||
}) {
|
||||
return {
|
||||
getSnapshot: () => getPluginState(),
|
||||
setState: (
|
||||
updater:
|
||||
| GooglePickerState
|
||||
| ((prevState: GooglePickerState) => GooglePickerState),
|
||||
) => {
|
||||
const newState =
|
||||
typeof updater === 'function' ? updater(getPluginState()) : updater
|
||||
|
||||
setPluginState(newState)
|
||||
},
|
||||
subscribe: (listener: (state: GooglePickerState) => void) => {
|
||||
const onStateUpdate: UppyEventMap<any, any>['state-update'] = (
|
||||
_prev,
|
||||
_next,
|
||||
patch,
|
||||
): void => {
|
||||
const screenCapturePatch = (patch?.plugins?.GoogleDrivePicker ??
|
||||
patch?.plugins?.GooglePhotosPicker) as GooglePickerState | undefined
|
||||
|
||||
if (screenCapturePatch) listener(screenCapturePatch)
|
||||
}
|
||||
|
||||
uppy.on('state-update', onStateUpdate)
|
||||
return () => uppy.off('state-update', onStateUpdate)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type GooglePickerState = {
|
||||
loading: boolean
|
||||
accessToken: string | null | undefined
|
||||
}
|
||||
|
||||
export type GooglePickerStore = ReturnType<
|
||||
typeof createGooglePickerStoreAdapter
|
||||
>
|
||||
|
||||
export interface GooglePickerOptions
|
||||
extends Pick<RequestClientOptions, 'companionUrl'>,
|
||||
Pick<CompanionPluginOptions, 'storage'> {
|
||||
pickerType: GooglePickerType
|
||||
clientId: string
|
||||
requestClientId: string
|
||||
apiKey?: string
|
||||
appId?: string
|
||||
store: GooglePickerStore
|
||||
}
|
||||
|
||||
export function createGooglePickerController({
|
||||
uppy,
|
||||
store,
|
||||
storage: persistentStore = tokenStorage,
|
||||
pickerType,
|
||||
companionUrl,
|
||||
requestClientId = pickerType === 'drive'
|
||||
? 'GoogleDrivePicker'
|
||||
: 'GooglePhotosPicker',
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
}: {
|
||||
uppy: Uppy
|
||||
} & GooglePickerOptions) {
|
||||
const storageKey = `uppy:google-${pickerType}-picker:accessToken`
|
||||
|
||||
let abortController = new AbortController()
|
||||
let initPromise: Promise<void> | undefined
|
||||
let pickingSession: PickingSession | undefined
|
||||
|
||||
const handleFilesPicked = async (
|
||||
files: PickedItem[],
|
||||
accessToken: string,
|
||||
) => {
|
||||
uppy.addFiles(
|
||||
files.map((file) =>
|
||||
mapPickerFile({ requestClientId, accessToken, companionUrl }, file),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function setAccessToken(newAccessToken: string | null) {
|
||||
store.setState((s) => ({ ...s, accessToken: newAccessToken }))
|
||||
if (newAccessToken == null) {
|
||||
return persistentStore.removeItem(storageKey)
|
||||
}
|
||||
return persistentStore.setItem(storageKey, newAccessToken)
|
||||
}
|
||||
|
||||
function setLoading(v: boolean) {
|
||||
store.setState((s) => ({ ...s, loading: v }))
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (initPromise == null) {
|
||||
initPromise = (async () => {
|
||||
abortController = new AbortController()
|
||||
|
||||
setAccessToken(await persistentStore.getItem(storageKey))
|
||||
|
||||
abortController.signal.throwIfAborted()
|
||||
|
||||
// For google photos we need to continuously poll the current picking session
|
||||
if (pickerType === 'photos') {
|
||||
pollPickingSession({
|
||||
getPickingSession: () => pickingSession,
|
||||
getAccessToken: () => store.getSnapshot().accessToken,
|
||||
onPickingSessionClear: () => {
|
||||
pickingSession = undefined
|
||||
},
|
||||
signal: abortController.signal,
|
||||
onFilesPicked: handleFilesPicked,
|
||||
onError: (err) => uppy.log(err),
|
||||
})
|
||||
}
|
||||
})()
|
||||
}
|
||||
await initPromise
|
||||
}
|
||||
|
||||
async function showPicker() {
|
||||
await init()
|
||||
|
||||
let newAccessToken = store.getSnapshot().accessToken
|
||||
|
||||
const doShowPicker = async (token: string) => {
|
||||
if (pickerType === 'drive') {
|
||||
if (apiKey == null || appId == null)
|
||||
throw new TypeError(
|
||||
'apiKey and appId are required for Google Drive picker',
|
||||
)
|
||||
await showDrivePicker({
|
||||
token,
|
||||
apiKey,
|
||||
appId,
|
||||
onFilesPicked: handleFilesPicked,
|
||||
signal: abortController.signal,
|
||||
onLoadingChange: (isLoading: boolean) => setLoading(isLoading),
|
||||
onError: (err: unknown) => {
|
||||
uppy.log(err)
|
||||
uppy.info(uppy.i18n('failedToAddFiles'), 'error')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// photos
|
||||
const onPickingSessionChange = (newPickingSession: PickingSession) => {
|
||||
pickingSession = newPickingSession
|
||||
}
|
||||
await showPhotosPicker({
|
||||
token,
|
||||
pickingSession,
|
||||
onPickingSessionChange,
|
||||
signal: abortController.signal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
try {
|
||||
await ensureScriptsInjected(pickerType)
|
||||
|
||||
if (newAccessToken == null) {
|
||||
newAccessToken = await authorize({ clientId, pickerType })
|
||||
}
|
||||
if (newAccessToken == null) throw new Error()
|
||||
|
||||
await doShowPicker(newAccessToken)
|
||||
setAccessToken(newAccessToken)
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidTokenError) {
|
||||
uppy.log('Token is invalid or expired, reauthenticating')
|
||||
newAccessToken = await authorize({
|
||||
pickerType,
|
||||
accessToken: newAccessToken,
|
||||
clientId,
|
||||
})
|
||||
// now try again:
|
||||
await doShowPicker(newAccessToken)
|
||||
setAccessToken(newAccessToken)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
'type' in err &&
|
||||
err.type === 'popup_closed'
|
||||
) {
|
||||
// user closed the auth popup, ignore
|
||||
} else {
|
||||
setAccessToken(null)
|
||||
uppy.log(err)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const { accessToken } = store.getSnapshot()
|
||||
if (accessToken) {
|
||||
await doLogout(accessToken)
|
||||
setAccessToken(null)
|
||||
pickingSession = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
abortController.abort()
|
||||
|
||||
pickingSession = undefined
|
||||
initPromise = undefined
|
||||
store.setState((s) => ({ ...s, accessToken: undefined, loading: false }))
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
show: showPicker,
|
||||
reset,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,25 @@
|
|||
|
||||
export type { CompanionPluginOptions } from './CompanionPluginOptions.js'
|
||||
export { default as getAllowedHosts } from './getAllowedHosts.js'
|
||||
export type {
|
||||
GooglePickerOptions,
|
||||
GooglePickerState,
|
||||
GooglePickerType,
|
||||
MediaItem,
|
||||
MediaItemBase,
|
||||
PhotoMediaItem,
|
||||
PickedDriveItem,
|
||||
PickedItemBase,
|
||||
PickedPhotosItem,
|
||||
PickingSession,
|
||||
UnspecifiedMediaItem,
|
||||
VideoMediaItem,
|
||||
} from './googlePicker.js'
|
||||
export {
|
||||
createGooglePickerController,
|
||||
createGooglePickerStoreAdapter,
|
||||
InvalidTokenError,
|
||||
} from './googlePicker.js'
|
||||
export { default as Provider } from './Provider.js'
|
||||
export {
|
||||
default as RequestClient,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
"types": ["google.accounts", "google.picker", "gapi"]
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"types": ["google.accounts", "google.picker", "gapi"]
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@
|
|||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/companion-client": "workspace:^",
|
||||
"@uppy/core": "workspace:^",
|
||||
"@uppy/google-drive-picker": "workspace:^",
|
||||
"@uppy/google-photos-picker": "workspace:^",
|
||||
"@uppy/image-editor": "workspace:^",
|
||||
"@uppy/screen-capture": "workspace:^",
|
||||
"@uppy/webcam": "workspace:^"
|
||||
|
|
|
|||
53
packages/@uppy/components/src/googlePicker.ts
Normal file
53
packages/@uppy/components/src/googlePicker.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
createGooglePickerStoreAdapter,
|
||||
type GooglePickerType,
|
||||
} from '@uppy/companion-client'
|
||||
import type Uppy from '@uppy/core'
|
||||
import type GoogleDrivePicker from '@uppy/google-drive-picker'
|
||||
import type GooglePhotosPicker from '@uppy/google-photos-picker'
|
||||
|
||||
export function createGooglePickerPluginAdapter(
|
||||
uppy: Uppy<any, any>,
|
||||
pickerType: GooglePickerType,
|
||||
) {
|
||||
const pluginName =
|
||||
pickerType === 'drive' ? 'GoogleDrivePicker' : 'GooglePhotosPicker'
|
||||
const plugin = uppy.getPlugin<
|
||||
GoogleDrivePicker<any, any> | GooglePhotosPicker<any, any>
|
||||
>(pluginName)
|
||||
if (!plugin) {
|
||||
throw new Error(`${pluginName} plugin is not registered in Uppy instance`)
|
||||
}
|
||||
|
||||
const { getPluginState, setPluginState, opts } = plugin
|
||||
|
||||
const {
|
||||
companionUrl,
|
||||
companionCookiesRule,
|
||||
companionHeaders,
|
||||
companionKeysParams,
|
||||
clientId,
|
||||
} = opts
|
||||
|
||||
const apiKey = 'apiKey' in opts ? opts.apiKey : undefined
|
||||
const appId = 'appId' in opts ? opts.appId : undefined
|
||||
const { requestClientId } = plugin
|
||||
|
||||
return {
|
||||
store: createGooglePickerStoreAdapter({
|
||||
uppy,
|
||||
getPluginState,
|
||||
setPluginState,
|
||||
}),
|
||||
opts: {
|
||||
companionUrl,
|
||||
companionCookiesRule,
|
||||
companionHeaders,
|
||||
companionKeysParams,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
requestClientId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
import type { RequestClientOptions } from '@uppy/companion-client'
|
||||
import {
|
||||
type CompanionPluginOptions,
|
||||
RequestClient,
|
||||
tokenStorage,
|
||||
} from '@uppy/companion-client'
|
||||
import type Uppy from '@uppy/core'
|
||||
import {
|
||||
authorize,
|
||||
logout as doLogout,
|
||||
ensureScriptsInjected,
|
||||
InvalidTokenError,
|
||||
mapPickerFile,
|
||||
type PickedItem,
|
||||
type PickingSession,
|
||||
pollPickingSession,
|
||||
showDrivePicker,
|
||||
showPhotosPicker,
|
||||
} from '@uppy/provider-views'
|
||||
|
||||
export type GooglePickerType = 'drive' | 'photos'
|
||||
|
||||
/**
|
||||
* A simple react-like immutable store that can be used with useSyncExternalStore and the like
|
||||
*/
|
||||
function createStore<T extends object>(initialState: T) {
|
||||
let state = initialState
|
||||
const listeners = new Set<(state: T) => void>()
|
||||
|
||||
function getSnapshot() {
|
||||
return state
|
||||
}
|
||||
|
||||
function setState(updater: T | ((prevState: T) => T)) {
|
||||
state = typeof updater === 'function' ? updater(state) : updater
|
||||
|
||||
listeners.forEach((listener) => listener(state))
|
||||
}
|
||||
|
||||
function subscribe(listener: (state: T) => void) {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
}
|
||||
|
||||
return {
|
||||
getSnapshot,
|
||||
setState,
|
||||
subscribe,
|
||||
}
|
||||
}
|
||||
|
||||
export interface GooglePickerOptions
|
||||
extends RequestClientOptions,
|
||||
Pick<CompanionPluginOptions, 'storage'> {
|
||||
pickerType: GooglePickerType
|
||||
clientId: string
|
||||
requestClientId?: string
|
||||
apiKey?: string
|
||||
appId?: string
|
||||
}
|
||||
|
||||
export function createGooglePickerController({
|
||||
uppy,
|
||||
storage: persistentStore = tokenStorage,
|
||||
pickerType,
|
||||
companionUrl,
|
||||
companionHeaders,
|
||||
companionCookiesRule,
|
||||
requestClientId = pickerType === 'drive'
|
||||
? 'GoogleDrivePicker'
|
||||
: 'GooglePhotosPicker',
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
}: {
|
||||
uppy: Uppy
|
||||
} & GooglePickerOptions) {
|
||||
const storageKey = `uppy:google-${pickerType}-picker:accessToken`
|
||||
|
||||
const store = createStore({
|
||||
loading: false,
|
||||
accessToken: undefined as string | null | undefined,
|
||||
})
|
||||
|
||||
let abortController = new AbortController()
|
||||
let initPromise: Promise<void> | undefined
|
||||
let pickingSession: PickingSession | undefined
|
||||
|
||||
const handleFilesPicked = async (
|
||||
files: PickedItem[],
|
||||
accessToken: string,
|
||||
) => {
|
||||
uppy.addFiles(
|
||||
files.map((file) =>
|
||||
mapPickerFile({ requestClientId, accessToken, companionUrl }, file),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function setAccessToken(newAccessToken: string | null) {
|
||||
store.setState((s) => ({ ...s, accessToken: newAccessToken }))
|
||||
if (newAccessToken == null) {
|
||||
return persistentStore.removeItem(storageKey)
|
||||
}
|
||||
return persistentStore.setItem(storageKey, newAccessToken)
|
||||
}
|
||||
|
||||
function setLoading(v: boolean) {
|
||||
store.setState((s) => ({ ...s, loading: v }))
|
||||
}
|
||||
|
||||
async function init() {
|
||||
abortController = new AbortController()
|
||||
|
||||
setAccessToken(await persistentStore.getItem(storageKey))
|
||||
|
||||
abortController.signal.throwIfAborted()
|
||||
|
||||
// For google photos we need to continuously poll the current picking session
|
||||
if (pickerType === 'photos') {
|
||||
pollPickingSession({
|
||||
getPickingSession: () => pickingSession,
|
||||
getAccessToken: () => store.getSnapshot().accessToken,
|
||||
onPickingSessionClear: () => {
|
||||
pickingSession = undefined
|
||||
},
|
||||
signal: abortController.signal,
|
||||
onFilesPicked: handleFilesPicked,
|
||||
onError: (err) => uppy.log(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function showPicker() {
|
||||
if (initPromise == null) initPromise = init()
|
||||
await initPromise
|
||||
|
||||
let newAccessToken = store.getSnapshot().accessToken
|
||||
|
||||
const doShowPicker = async (token: string) => {
|
||||
if (pickerType === 'drive') {
|
||||
if (apiKey == null || appId == null)
|
||||
throw new TypeError(
|
||||
'apiKey and appId are required for Google Drive picker',
|
||||
)
|
||||
await showDrivePicker({
|
||||
token,
|
||||
apiKey,
|
||||
appId,
|
||||
onFilesPicked: handleFilesPicked,
|
||||
signal: abortController.signal,
|
||||
onLoadingChange: (isLoading: boolean) => setLoading(isLoading),
|
||||
onError: (err: unknown) => {
|
||||
uppy.log(err)
|
||||
uppy.info(uppy.i18n('failedToAddFiles'), 'error')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// photos
|
||||
const onPickingSessionChange = (newPickingSession: PickingSession) => {
|
||||
pickingSession = newPickingSession
|
||||
}
|
||||
await showPhotosPicker({
|
||||
token,
|
||||
pickingSession,
|
||||
onPickingSessionChange,
|
||||
signal: abortController.signal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
try {
|
||||
const client = new RequestClient(uppy, {
|
||||
companionUrl,
|
||||
companionHeaders,
|
||||
companionCookiesRule,
|
||||
})
|
||||
|
||||
uppy.registerRequestClient(requestClientId, client)
|
||||
|
||||
await ensureScriptsInjected(pickerType)
|
||||
|
||||
if (newAccessToken == null) {
|
||||
newAccessToken = await authorize({ clientId, pickerType })
|
||||
}
|
||||
if (newAccessToken == null) throw new Error()
|
||||
|
||||
await doShowPicker(newAccessToken)
|
||||
setAccessToken(newAccessToken)
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidTokenError) {
|
||||
uppy.log('Token is invalid or expired, reauthenticating')
|
||||
newAccessToken = await authorize({
|
||||
pickerType,
|
||||
accessToken: newAccessToken,
|
||||
clientId,
|
||||
})
|
||||
// now try again:
|
||||
await doShowPicker(newAccessToken)
|
||||
setAccessToken(newAccessToken)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
'type' in err &&
|
||||
err.type === 'popup_closed'
|
||||
) {
|
||||
// user closed the auth popup, ignore
|
||||
} else {
|
||||
setAccessToken(null)
|
||||
uppy.log(err)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const { accessToken } = store.getSnapshot()
|
||||
if (accessToken) {
|
||||
await doLogout(accessToken)
|
||||
setAccessToken(null)
|
||||
pickingSession = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
abortController.abort()
|
||||
|
||||
pickingSession = undefined
|
||||
initPromise = undefined
|
||||
store.setState((s) => ({ ...s, accessToken: undefined, loading: false }))
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
show: showPicker,
|
||||
reset,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
// Headless components
|
||||
|
||||
export {
|
||||
createGooglePickerController,
|
||||
type GooglePickerOptions,
|
||||
type GooglePickerType,
|
||||
} from '@uppy/companion-client'
|
||||
|
||||
export { type DropzoneProps, default as Dropzone } from './Dropzone.js'
|
||||
export { default as FilesGrid, type FilesGridProps } from './FilesGrid.js'
|
||||
export { default as FilesList, type FilesListProps } from './FilesList.js'
|
||||
export { createGooglePickerPluginAdapter } from './googlePicker.js'
|
||||
// Headless hooks
|
||||
export {
|
||||
createDropzone,
|
||||
|
|
@ -14,11 +21,6 @@ export {
|
|||
type FileInputFunctions,
|
||||
type FileInputProps,
|
||||
} from './hooks/file-input.js'
|
||||
export {
|
||||
createGooglePickerController,
|
||||
type GooglePickerOptions,
|
||||
type GooglePickerType,
|
||||
} from './hooks/googlePicker.js'
|
||||
export {
|
||||
createRemoteSourceController,
|
||||
type RemoteSourceKeys,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@
|
|||
{
|
||||
"path": "../google-drive-picker/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../google-photos-picker/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../companion-client/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../image-editor/tsconfig.build.json"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
{
|
||||
"path": "../google-drive-picker/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../google-photos-picker/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../companion-client/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../image-editor/tsconfig.build.json"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import {
|
||||
type CompanionPluginOptions,
|
||||
type GooglePickerState,
|
||||
RequestClient,
|
||||
tokenStorage,
|
||||
} from '@uppy/companion-client'
|
||||
import type { AsyncStore, BaseProviderPlugin, Body, Meta } from '@uppy/core'
|
||||
import { UIPlugin, type Uppy } from '@uppy/core'
|
||||
import {
|
||||
GoogleDriveIcon,
|
||||
GooglePickerView,
|
||||
mapPickerFile,
|
||||
type PickedItem,
|
||||
} from '@uppy/provider-views'
|
||||
import { GoogleDriveIcon, GooglePickerView } from '@uppy/provider-views'
|
||||
import type { LocaleStrings } from '@uppy/utils'
|
||||
|
||||
import packageJson from '../package.json' with { type: 'json' }
|
||||
|
|
@ -30,7 +26,7 @@ export type GoogleDrivePickerOptions = CompanionPluginOptions & {
|
|||
}
|
||||
|
||||
export default class GoogleDrivePicker<M extends Meta, B extends Body>
|
||||
extends UIPlugin<GoogleDrivePickerOptions, M, B>
|
||||
extends UIPlugin<GoogleDrivePickerOptions, M, B, GooglePickerState>
|
||||
implements BaseProviderPlugin
|
||||
{
|
||||
static VERSION = packageJson.version
|
||||
|
|
@ -45,6 +41,8 @@ export default class GoogleDrivePicker<M extends Meta, B extends Body>
|
|||
|
||||
defaultLocale = locale
|
||||
|
||||
requestClientId = GoogleDrivePicker.requestClientId
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts: GoogleDrivePickerOptions) {
|
||||
super(uppy, opts)
|
||||
this.id = this.opts.id || 'GoogleDrivePicker'
|
||||
|
|
@ -54,13 +52,25 @@ export default class GoogleDrivePicker<M extends Meta, B extends Body>
|
|||
this.i18nInit()
|
||||
this.title = this.i18n('pluginNameGoogleDrivePicker')
|
||||
|
||||
const client = new RequestClient(uppy, {
|
||||
this.setPluginState({
|
||||
loading: false,
|
||||
accessToken: undefined,
|
||||
})
|
||||
|
||||
this.getPluginState = this.getPluginState.bind(this)
|
||||
this.setPluginState = this.setPluginState.bind(this)
|
||||
|
||||
const requestClient = new RequestClient(uppy, {
|
||||
companionUrl: this.opts.companionUrl,
|
||||
companionHeaders: this.opts.companionHeaders,
|
||||
companionCookiesRule: this.opts.companionCookiesRule,
|
||||
companionKeysParams: this.opts.companionKeysParams,
|
||||
})
|
||||
|
||||
this.uppy.registerRequestClient(GoogleDrivePicker.requestClientId, client)
|
||||
this.uppy.registerRequestClient(
|
||||
GoogleDrivePicker.requestClientId,
|
||||
requestClient,
|
||||
)
|
||||
}
|
||||
|
||||
install(): void {
|
||||
|
|
@ -74,26 +84,10 @@ export default class GoogleDrivePicker<M extends Meta, B extends Body>
|
|||
this.unmount()
|
||||
}
|
||||
|
||||
private handleFilesPicked = async (
|
||||
files: PickedItem[],
|
||||
accessToken: string,
|
||||
) => {
|
||||
this.uppy.addFiles(
|
||||
files.map((file) =>
|
||||
mapPickerFile(
|
||||
{
|
||||
requestClientId: GoogleDrivePicker.requestClientId,
|
||||
accessToken,
|
||||
companionUrl: this.opts.companionUrl,
|
||||
},
|
||||
file,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<GooglePickerView
|
||||
getPluginState={this.getPluginState}
|
||||
setPluginState={this.setPluginState}
|
||||
storage={this.storage}
|
||||
pickerType="drive"
|
||||
uppy={this.uppy}
|
||||
|
|
@ -101,7 +95,8 @@ export default class GoogleDrivePicker<M extends Meta, B extends Body>
|
|||
clientId={this.opts.clientId}
|
||||
apiKey={this.opts.apiKey}
|
||||
appId={this.opts.appId}
|
||||
onFilesPicked={this.handleFilesPicked}
|
||||
requestClientId={GoogleDrivePicker.requestClientId}
|
||||
companionUrl={this.opts.companionUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import {
|
||||
type CompanionPluginOptions,
|
||||
type GooglePickerState,
|
||||
RequestClient,
|
||||
tokenStorage,
|
||||
} from '@uppy/companion-client'
|
||||
import type { AsyncStore, BaseProviderPlugin, Body, Meta } from '@uppy/core'
|
||||
import { UIPlugin, type Uppy } from '@uppy/core'
|
||||
import {
|
||||
GooglePhotosIcon,
|
||||
GooglePickerView,
|
||||
mapPickerFile,
|
||||
type PickedItem,
|
||||
} from '@uppy/provider-views'
|
||||
import { GooglePhotosIcon, GooglePickerView } from '@uppy/provider-views'
|
||||
import type { LocaleStrings } from '@uppy/utils'
|
||||
|
||||
import packageJson from '../package.json' with { type: 'json' }
|
||||
|
|
@ -28,7 +24,7 @@ export type GooglePhotosPickerOptions = CompanionPluginOptions & {
|
|||
}
|
||||
|
||||
export default class GooglePhotosPicker<M extends Meta, B extends Body>
|
||||
extends UIPlugin<GooglePhotosPickerOptions, M, B>
|
||||
extends UIPlugin<GooglePhotosPickerOptions, M, B, GooglePickerState>
|
||||
implements BaseProviderPlugin
|
||||
{
|
||||
static VERSION = packageJson.version
|
||||
|
|
@ -43,6 +39,8 @@ export default class GooglePhotosPicker<M extends Meta, B extends Body>
|
|||
|
||||
defaultLocale = locale
|
||||
|
||||
requestClientId = GooglePhotosPicker.requestClientId
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts: GooglePhotosPickerOptions) {
|
||||
super(uppy, opts)
|
||||
this.id = this.opts.id || 'GooglePhotosPicker'
|
||||
|
|
@ -52,13 +50,25 @@ export default class GooglePhotosPicker<M extends Meta, B extends Body>
|
|||
this.i18nInit()
|
||||
this.title = this.i18n('pluginNameGooglePhotosPicker')
|
||||
|
||||
const client = new RequestClient(uppy, {
|
||||
this.setPluginState({
|
||||
loading: false,
|
||||
accessToken: undefined,
|
||||
})
|
||||
|
||||
this.getPluginState = this.getPluginState.bind(this)
|
||||
this.setPluginState = this.setPluginState.bind(this)
|
||||
|
||||
const requestClient = new RequestClient(uppy, {
|
||||
companionUrl: this.opts.companionUrl,
|
||||
companionHeaders: this.opts.companionHeaders,
|
||||
companionCookiesRule: this.opts.companionCookiesRule,
|
||||
companionKeysParams: this.opts.companionKeysParams,
|
||||
})
|
||||
|
||||
this.uppy.registerRequestClient(GooglePhotosPicker.requestClientId, client)
|
||||
this.uppy.registerRequestClient(
|
||||
GooglePhotosPicker.requestClientId,
|
||||
requestClient,
|
||||
)
|
||||
}
|
||||
|
||||
install(): void {
|
||||
|
|
@ -72,32 +82,17 @@ export default class GooglePhotosPicker<M extends Meta, B extends Body>
|
|||
this.unmount()
|
||||
}
|
||||
|
||||
private handleFilesPicked = async (
|
||||
files: PickedItem[],
|
||||
accessToken: string,
|
||||
) => {
|
||||
this.uppy.addFiles(
|
||||
files.map((file) =>
|
||||
mapPickerFile(
|
||||
{
|
||||
requestClientId: GooglePhotosPicker.requestClientId,
|
||||
accessToken,
|
||||
companionUrl: this.opts.companionUrl,
|
||||
},
|
||||
file,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<GooglePickerView
|
||||
getPluginState={this.getPluginState}
|
||||
setPluginState={this.setPluginState}
|
||||
storage={this.storage}
|
||||
pickerType="photos"
|
||||
uppy={this.uppy}
|
||||
i18n={this.i18n}
|
||||
clientId={this.opts.clientId}
|
||||
onFilesPicked={this.handleFilesPicked}
|
||||
requestClientId={GooglePhotosPicker.requestClientId}
|
||||
companionUrl={this.opts.companionUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,6 @@
|
|||
"preact": "^10.26.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/google.accounts": "^0.0.14",
|
||||
"@types/google.picker": "^0.0.42",
|
||||
"cssnano": "^7.0.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
|
|
|
|||
|
|
@ -1,50 +1,77 @@
|
|||
import {
|
||||
type CompanionPluginOptions,
|
||||
createGooglePickerController,
|
||||
createGooglePickerStoreAdapter,
|
||||
type GooglePickerOptions,
|
||||
type GooglePickerState,
|
||||
} from '@uppy/companion-client'
|
||||
import type { AsyncStore, Uppy } from '@uppy/core'
|
||||
import type { I18n } from '@uppy/utils'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useSyncExternalStore } from 'preact/compat'
|
||||
import { useEffect, useMemo, useRef } from 'preact/hooks'
|
||||
import AuthView from '../ProviderView/AuthView.js'
|
||||
import {
|
||||
authorize,
|
||||
ensureScriptsInjected,
|
||||
InvalidTokenError,
|
||||
logout,
|
||||
type PickedItem,
|
||||
type PickingSession,
|
||||
pollPickingSession,
|
||||
showDrivePicker,
|
||||
showPhotosPicker,
|
||||
} from './googlePicker.js'
|
||||
import { GoogleDriveIcon, GooglePhotosIcon } from './icons.js'
|
||||
|
||||
function useStore(
|
||||
store: AsyncStore,
|
||||
key: string,
|
||||
): [string | undefined | null, (v: string | null) => Promise<void>] {
|
||||
const [value, setValueState] = useState<string | null | undefined>()
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
setValueState(await store.getItem(key))
|
||||
})()
|
||||
}, [key, store])
|
||||
export function useGooglePicker({
|
||||
uppy,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
storage,
|
||||
store,
|
||||
}: GooglePickerOptions & { uppy: Uppy<any, any> } & Pick<
|
||||
CompanionPluginOptions,
|
||||
'companionUrl'
|
||||
>) {
|
||||
const { subscribe, getSnapshot } = store
|
||||
|
||||
const setValue = useCallback(
|
||||
async (v: string | null) => {
|
||||
setValueState(v)
|
||||
if (v == null) {
|
||||
return store.removeItem(key)
|
||||
}
|
||||
return store.setItem(key, v)
|
||||
},
|
||||
[key, store],
|
||||
const { reset, init, ...rest } = useMemo(
|
||||
() =>
|
||||
createGooglePickerController({
|
||||
uppy,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
storage,
|
||||
store,
|
||||
}),
|
||||
[
|
||||
uppy,
|
||||
requestClientId,
|
||||
clientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
apiKey,
|
||||
appId,
|
||||
storage,
|
||||
store,
|
||||
],
|
||||
)
|
||||
|
||||
return [value, setValue]
|
||||
useEffect(() => {
|
||||
init()
|
||||
return () => {
|
||||
reset()
|
||||
}
|
||||
}, [reset, init])
|
||||
|
||||
return { ...useSyncExternalStore(subscribe, getSnapshot), ...rest }
|
||||
}
|
||||
|
||||
export type GooglePickerViewProps = {
|
||||
setPluginState: (state: Partial<GooglePickerState>) => void
|
||||
getPluginState: () => GooglePickerState
|
||||
uppy: Uppy<any, any>
|
||||
i18n: I18n
|
||||
clientId: string
|
||||
onFilesPicked: (files: PickedItem[], accessToken: string) => void
|
||||
requestClientId: string
|
||||
companionUrl: CompanionPluginOptions['companionUrl']
|
||||
storage: AsyncStore
|
||||
} & (
|
||||
| {
|
||||
|
|
@ -59,179 +86,54 @@ export type GooglePickerViewProps = {
|
|||
}
|
||||
)
|
||||
|
||||
// todo rewrite to use useGooglePicker hook internally
|
||||
export default function GooglePickerView({
|
||||
getPluginState,
|
||||
setPluginState,
|
||||
uppy,
|
||||
i18n,
|
||||
clientId,
|
||||
onFilesPicked,
|
||||
pickerType,
|
||||
apiKey,
|
||||
appId,
|
||||
storage,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
}: GooglePickerViewProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [accessToken, setAccessTokenStored] = useStore(
|
||||
const store = useMemo(
|
||||
() =>
|
||||
createGooglePickerStoreAdapter({ uppy, getPluginState, setPluginState }),
|
||||
[uppy, getPluginState, setPluginState],
|
||||
)
|
||||
|
||||
const googlePicker = useGooglePicker({
|
||||
uppy,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
storage,
|
||||
`uppy:google-${pickerType}-picker:accessToken`,
|
||||
)
|
||||
store,
|
||||
})
|
||||
|
||||
const pickingSessionRef = useRef<PickingSession>()
|
||||
const accessTokenRef = useRef(accessToken)
|
||||
const shownPickerRef = useRef(false)
|
||||
|
||||
const setAccessToken = useCallback(
|
||||
(t: string | null) => {
|
||||
uppy.log('Access token updated')
|
||||
setAccessTokenStored(t)
|
||||
accessTokenRef.current = t
|
||||
},
|
||||
[setAccessTokenStored, uppy],
|
||||
)
|
||||
|
||||
// keep access token in sync with the ref
|
||||
useEffect(() => {
|
||||
accessTokenRef.current = accessToken
|
||||
}, [accessToken])
|
||||
|
||||
const showPicker = useCallback(
|
||||
async (signal?: AbortSignal) => {
|
||||
let newAccessToken = accessToken
|
||||
|
||||
const doShowPicker = async (token: string) => {
|
||||
if (pickerType === 'drive') {
|
||||
await showDrivePicker({
|
||||
token,
|
||||
apiKey,
|
||||
appId,
|
||||
onFilesPicked,
|
||||
signal,
|
||||
onLoadingChange: (isLoading: boolean) => setLoading(isLoading),
|
||||
onError: (err: unknown) => {
|
||||
uppy.log(err)
|
||||
uppy.info(i18n('failedToAddFiles'), 'error')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// photos
|
||||
const onPickingSessionChange = (
|
||||
newPickingSession: PickingSession,
|
||||
) => {
|
||||
pickingSessionRef.current = newPickingSession
|
||||
}
|
||||
await showPhotosPicker({
|
||||
token,
|
||||
pickingSession: pickingSessionRef.current,
|
||||
onPickingSessionChange,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
try {
|
||||
await ensureScriptsInjected(pickerType)
|
||||
|
||||
if (newAccessToken == null) {
|
||||
newAccessToken = await authorize({ clientId, pickerType })
|
||||
}
|
||||
if (newAccessToken == null) throw new Error()
|
||||
|
||||
await doShowPicker(newAccessToken)
|
||||
shownPickerRef.current = true
|
||||
setAccessToken(newAccessToken)
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidTokenError) {
|
||||
uppy.log('Token is invalid or expired, reauthenticating')
|
||||
newAccessToken = await authorize({
|
||||
pickerType,
|
||||
accessToken: newAccessToken,
|
||||
clientId,
|
||||
})
|
||||
// now try again:
|
||||
await doShowPicker(newAccessToken)
|
||||
shownPickerRef.current = true
|
||||
setAccessToken(newAccessToken)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
'type' in err &&
|
||||
err.type === 'popup_closed'
|
||||
) {
|
||||
// user closed the auth popup, ignore
|
||||
} else {
|
||||
setAccessToken(null)
|
||||
uppy.log(err)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[
|
||||
accessToken,
|
||||
apiKey,
|
||||
appId,
|
||||
clientId,
|
||||
onFilesPicked,
|
||||
pickerType,
|
||||
setAccessToken,
|
||||
uppy,
|
||||
i18n,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController()
|
||||
|
||||
pollPickingSession({
|
||||
getPickingSession: () => pickingSessionRef.current,
|
||||
getAccessToken: () => accessTokenRef.current,
|
||||
onPickingSessionClear: () => {
|
||||
pickingSessionRef.current = undefined
|
||||
},
|
||||
signal: abortController.signal,
|
||||
onFilesPicked,
|
||||
onError: (err) => uppy.log(err),
|
||||
})
|
||||
|
||||
return () => abortController.abort()
|
||||
}, [onFilesPicked, uppy])
|
||||
|
||||
useEffect(() => {
|
||||
// when mounting, once we have a token, be nice to the user and automatically show the picker
|
||||
// accessToken === undefined means not yet loaded from storage, so wait for that first
|
||||
if (accessToken === undefined || shownPickerRef.current) {
|
||||
return undefined
|
||||
// googlePicker.accessToken === undefined means not yet loaded from storage, so wait for that first
|
||||
if (googlePicker.accessToken === undefined || shownPickerRef.current) {
|
||||
return
|
||||
}
|
||||
shownPickerRef.current = true
|
||||
googlePicker.show()
|
||||
}, [googlePicker.show, googlePicker.accessToken])
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
showPicker(abortController.signal)
|
||||
|
||||
return () => {
|
||||
// only abort the picker if it's not yet shown
|
||||
if (!shownPickerRef.current) abortController.abort()
|
||||
}
|
||||
}, [accessToken, showPicker])
|
||||
|
||||
const handleLogoutClick = useCallback(async () => {
|
||||
if (accessToken) {
|
||||
await logout(accessToken)
|
||||
setAccessToken(null)
|
||||
pickingSessionRef.current = undefined
|
||||
}
|
||||
}, [accessToken, setAccessToken])
|
||||
|
||||
if (loading) {
|
||||
if (googlePicker.loading) {
|
||||
return <div>{i18n('pleaseWait')}...</div>
|
||||
}
|
||||
|
||||
if (accessToken == null) {
|
||||
if (googlePicker.accessToken == null) {
|
||||
return (
|
||||
<AuthView
|
||||
pluginName={
|
||||
|
|
@ -240,9 +142,9 @@ export default function GooglePickerView({
|
|||
: i18n('pluginNameGooglePhotosPicker')
|
||||
}
|
||||
pluginIcon={pickerType === 'drive' ? GoogleDriveIcon : GooglePhotosIcon}
|
||||
handleAuth={showPicker}
|
||||
handleAuth={googlePicker.show}
|
||||
i18n={i18n}
|
||||
loading={loading}
|
||||
loading={googlePicker.loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -253,16 +155,16 @@ export default function GooglePickerView({
|
|||
type="button"
|
||||
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary"
|
||||
style={{ display: 'block', marginBottom: '1em' }}
|
||||
disabled={loading}
|
||||
onClick={() => showPicker()}
|
||||
disabled={googlePicker.loading}
|
||||
onClick={googlePicker.show}
|
||||
>
|
||||
{pickerType === 'drive' ? i18n('pickFiles') : i18n('pickPhotos')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="uppy-u-reset uppy-c-btn"
|
||||
disabled={loading}
|
||||
onClick={handleLogoutClick}
|
||||
disabled={googlePicker.loading}
|
||||
onClick={googlePicker.logout}
|
||||
>
|
||||
{i18n('logOut')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,5 @@
|
|||
export { default as FilterInput } from './FilterInput.js'
|
||||
export { default as GooglePickerView } from './GooglePicker/GooglePickerView.js'
|
||||
export type {
|
||||
MediaItem,
|
||||
MediaItemBase,
|
||||
PhotoMediaItem,
|
||||
PickedDriveItem,
|
||||
PickedItem,
|
||||
PickedItemBase,
|
||||
PickedPhotosItem,
|
||||
PickingSession,
|
||||
UnspecifiedMediaItem,
|
||||
VideoMediaItem,
|
||||
} from './GooglePicker/googlePicker.js'
|
||||
export {
|
||||
authorize,
|
||||
ensureScriptsInjected,
|
||||
InvalidTokenError,
|
||||
logout,
|
||||
mapPickerFile,
|
||||
pollPickingSession,
|
||||
showDrivePicker,
|
||||
showPhotosPicker,
|
||||
} from './GooglePicker/googlePicker.js'
|
||||
export { GoogleDriveIcon, GooglePhotosIcon } from './GooglePicker/icons.js'
|
||||
export {
|
||||
default as ProviderViews,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"types": ["google.accounts", "google.picker", "gapi"]
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"types": ["google.accounts", "google.picker", "gapi"]
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
createGooglePickerController,
|
||||
createGooglePickerPluginAdapter,
|
||||
type GooglePickerOptions,
|
||||
} from '@uppy/components'
|
||||
|
||||
|
|
@ -8,56 +9,40 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'
|
|||
import { useUppyContext } from './headless/UppyContextProvider.js'
|
||||
|
||||
export function useGooglePicker({
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
companionCookiesRule,
|
||||
companionHeaders,
|
||||
companionKeysParams,
|
||||
storage,
|
||||
}: GooglePickerOptions) {
|
||||
}: Pick<GooglePickerOptions, 'pickerType' | 'storage'>) {
|
||||
const { uppy } = useUppyContext()
|
||||
|
||||
const {
|
||||
store: { subscribe, getSnapshot },
|
||||
reset,
|
||||
...rest
|
||||
} = useMemo(
|
||||
const { store, opts } = useMemo(
|
||||
() => createGooglePickerPluginAdapter(uppy, pickerType),
|
||||
[uppy, pickerType],
|
||||
)
|
||||
|
||||
const { subscribe, getSnapshot } = store
|
||||
|
||||
const { reset, init, ...rest } = useMemo(
|
||||
() =>
|
||||
createGooglePickerController({
|
||||
uppy,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
companionCookiesRule,
|
||||
companionHeaders,
|
||||
companionKeysParams,
|
||||
storage,
|
||||
store,
|
||||
...opts,
|
||||
}),
|
||||
[
|
||||
uppy,
|
||||
requestClientId,
|
||||
clientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
apiKey,
|
||||
appId,
|
||||
companionCookiesRule,
|
||||
companionHeaders,
|
||||
companionKeysParams,
|
||||
storage,
|
||||
],
|
||||
[uppy, pickerType, storage, store, opts],
|
||||
)
|
||||
|
||||
const store = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
||||
useEffect(() => {
|
||||
init()
|
||||
|
||||
useEffect(() => () => reset(), [reset])
|
||||
return () => {
|
||||
reset()
|
||||
}
|
||||
}, [reset, init])
|
||||
|
||||
return { ...store, ...rest }
|
||||
return {
|
||||
...useSyncExternalStore(subscribe, getSnapshot, getSnapshot),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import {
|
||||
createGooglePickerController,
|
||||
type GooglePickerOptions,
|
||||
} from "@uppy/components";
|
||||
import { onDestroy } from "svelte";
|
||||
createGooglePickerController,
|
||||
createGooglePickerPluginAdapter,
|
||||
type GooglePickerOptions,
|
||||
} from '@uppy/components';
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getUppyContext } from "./components/headless/uppyContext.js";
|
||||
import { useExternalStore } from "./useSyncExternalStore.svelte.js";
|
||||
|
||||
|
|
@ -17,34 +18,34 @@ type SvelteGooglePickerSnapshot = GooglePickerSnapshot & {
|
|||
};
|
||||
|
||||
export function useGooglePicker({
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
...restOptions
|
||||
}: GooglePickerOptions): SvelteGooglePickerSnapshot {
|
||||
storage,
|
||||
}: Pick<GooglePickerOptions, 'pickerType' | 'storage'>): SvelteGooglePickerSnapshot {
|
||||
const ctx = getUppyContext();
|
||||
|
||||
const { store, opts } = createGooglePickerPluginAdapter(ctx.uppy, pickerType)
|
||||
|
||||
const { subscribe, getSnapshot } = store;
|
||||
|
||||
const {
|
||||
store: { subscribe, getSnapshot },
|
||||
reset,
|
||||
init,
|
||||
show,
|
||||
logout,
|
||||
} = createGooglePickerController({
|
||||
uppy: ctx.uppy,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
...restOptions,
|
||||
store,
|
||||
storage,
|
||||
...opts,
|
||||
});
|
||||
|
||||
const state = useExternalStore<GooglePickerSnapshot>(getSnapshot, subscribe);
|
||||
|
||||
onMount(() => {
|
||||
init()
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
reset();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import {
|
||||
createGooglePickerController,
|
||||
createGooglePickerPluginAdapter,
|
||||
type GooglePickerOptions,
|
||||
} from '@uppy/components'
|
||||
import { computed, onUnmounted, type ShallowRef } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, type ShallowRef } from 'vue'
|
||||
import { injectUppyContext } from './headless/context-provider.js'
|
||||
import { useExternalStore } from './useSyncExternalStore.js'
|
||||
|
||||
|
|
@ -17,36 +18,31 @@ type GooglePickerController = {
|
|||
}
|
||||
|
||||
export function useGooglePicker({
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
...restOptions
|
||||
}: GooglePickerOptions): ShallowRef<
|
||||
storage,
|
||||
}: Pick<GooglePickerOptions, 'pickerType' | 'storage'>): ShallowRef<
|
||||
GooglePickerSnapshot & GooglePickerController
|
||||
> {
|
||||
const ctx = injectUppyContext()
|
||||
|
||||
const {
|
||||
store: { subscribe, getSnapshot },
|
||||
reset,
|
||||
show,
|
||||
logout,
|
||||
} = createGooglePickerController({
|
||||
const { store, opts } = createGooglePickerPluginAdapter(ctx.uppy, pickerType)
|
||||
|
||||
const { subscribe, getSnapshot } = store
|
||||
|
||||
const { reset, init, show, logout } = createGooglePickerController({
|
||||
uppy: ctx.uppy,
|
||||
requestClientId,
|
||||
companionUrl,
|
||||
pickerType,
|
||||
clientId,
|
||||
apiKey,
|
||||
appId,
|
||||
...restOptions,
|
||||
store,
|
||||
storage,
|
||||
...opts,
|
||||
})
|
||||
|
||||
const state = useExternalStore<GooglePickerSnapshot>(getSnapshot, subscribe)
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
reset()
|
||||
})
|
||||
|
|
|
|||
11
yarn.lock
11
yarn.lock
|
|
@ -8787,6 +8787,9 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@uppy/companion-client@workspace:packages/@uppy/companion-client"
|
||||
dependencies:
|
||||
"@types/gapi": "npm:^0.0.47"
|
||||
"@types/google.accounts": "npm:^0.0.14"
|
||||
"@types/google.picker": "npm:^0.0.42"
|
||||
"@uppy/utils": "workspace:^"
|
||||
jsdom: "npm:^26.1.0"
|
||||
namespace-emitter: "npm:^2.0.1"
|
||||
|
|
@ -8881,7 +8884,10 @@ __metadata:
|
|||
tailwindcss: "npm:^4.0.6"
|
||||
typescript: "npm:^5.8.3"
|
||||
peerDependencies:
|
||||
"@uppy/companion-client": "workspace:^"
|
||||
"@uppy/core": "workspace:^"
|
||||
"@uppy/google-drive-picker": "workspace:^"
|
||||
"@uppy/google-photos-picker": "workspace:^"
|
||||
"@uppy/image-editor": "workspace:^"
|
||||
"@uppy/screen-capture": "workspace:^"
|
||||
"@uppy/webcam": "workspace:^"
|
||||
|
|
@ -9185,9 +9191,6 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@uppy/provider-views@workspace:packages/@uppy/provider-views"
|
||||
dependencies:
|
||||
"@types/gapi": "npm:^0.0.47"
|
||||
"@types/google.accounts": "npm:^0.0.14"
|
||||
"@types/google.picker": "npm:^0.0.42"
|
||||
"@uppy/utils": "workspace:^"
|
||||
classnames: "npm:^2.2.6"
|
||||
cssnano: "npm:^7.0.7"
|
||||
|
|
@ -13257,6 +13260,8 @@ __metadata:
|
|||
"@types/react": "npm:^19.0.10"
|
||||
"@types/react-dom": "npm:^19.0.4"
|
||||
"@uppy/core": "workspace:*"
|
||||
"@uppy/google-drive-picker": "workspace:*"
|
||||
"@uppy/google-photos-picker": "workspace:*"
|
||||
"@uppy/react": "workspace:*"
|
||||
"@uppy/remote-sources": "workspace:*"
|
||||
"@uppy/screen-capture": "workspace:*"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue