Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion __tests__/google-picker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@ describe("pickDriveFolder", () => {
const fakePicker = { setVisible: jest.fn() };
const fakeGoogle = {
picker: {
ViewId: { FOLDERS: "folders" },
ViewId: { FOLDERS: "folders", DOCS: "docs" },
Action: { PICKED: "picked", CANCEL: "cancel" },
Feature: { SUPPORT_DRIVES: "supportDrives" },
DocsView: jest.fn().mockImplementation(() => ({
setParent: jest.fn().mockReturnThis(),
setMimeTypes: jest.fn().mockReturnThis(),
setSelectFolderEnabled: jest.fn().mockReturnThis(),
setIncludeFolders: jest.fn().mockReturnThis(),
setEnableDrives: jest.fn().mockReturnThis(),
})),
PickerBuilder: jest.fn().mockImplementation(() => ({
addView: jest.fn().mockReturnThis(),
enableFeature: jest.fn().mockReturnThis(),
setOAuthToken: jest.fn().mockReturnThis(),
setDeveloperKey: jest.fn().mockReturnThis(),
setAppId: jest.fn().mockReturnThis(),
Expand Down
8 changes: 4 additions & 4 deletions functions/src/__tests__/providers-gdrive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("1. writeSessionFile success", () => {
expect(mockFetch).toHaveBeenCalledTimes(1);
const { url, options } = callArgs(0);

expect(url).toBe(`${API_BASE}/upload/drive/v3/files?uploadType=multipart`);
expect(url).toBe(`${API_BASE}/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true`);
expect(options.method).toBe("POST");
expect(header(options.headers, "Authorization")).toBe("Bearer test-token");

Expand Down Expand Up @@ -217,7 +217,7 @@ describe("2. writeSessionFile subfolder", () => {
});

const uploadCall = callArgs(2);
expect(uploadCall.url).toBe(`${API_BASE}/upload/drive/v3/files?uploadType=multipart`);
expect(uploadCall.url).toBe(`${API_BASE}/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true`);
const uploadBody = uploadCall.options.body.toString();
expect(uploadBody).toContain('"name":"file.csv"');
expect(uploadBody).toContain('"parents":["sub-folder-id"]');
Expand Down Expand Up @@ -510,7 +510,7 @@ describe("5. updateFile", () => {

expect(mockFetch).toHaveBeenCalledTimes(1);
const { url, options } = callArgs(0);
expect(url).toBe(`${API_BASE}/upload/drive/v3/files/gdrive-existing-1?uploadType=media`);
expect(url).toBe(`${API_BASE}/upload/drive/v3/files/gdrive-existing-1?uploadType=media&supportsAllDrives=true`);
expect(options.method).toBe("PATCH");
expect(header(options.headers, "Authorization")).toBe("Bearer test-token");
expect(options.body).toBe("updated-data");
Expand Down Expand Up @@ -616,7 +616,7 @@ describe("7. downloadFile", () => {

expect(mockFetch).toHaveBeenCalledTimes(1);
const { url, options } = callArgs(0);
expect(url).toBe(`${API_BASE}/drive/v3/files/gdrive-file-9?alt=media`);
expect(url).toBe(`${API_BASE}/drive/v3/files/gdrive-file-9?alt=media&supportsAllDrives=true`);
expect(options.method).toBe("GET");
expect(header(options.headers, "Authorization")).toBe("Bearer test-token");

Expand Down
16 changes: 12 additions & 4 deletions functions/src/providers/gdrive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ async function findFolder(
const url = new URL(`${getApiBase()}/drive/v3/files`);
const q = `name='${escapeQueryValue(name)}' and '${parentId}' in parents and mimeType='${FOLDER_MIME}' and trashed=false`;
url.searchParams.set("q", q);
// Shared Drives require these on every list/query call, or folders that
// live in a shared drive are invisible to the query.
url.searchParams.set("supportsAllDrives", "true");
url.searchParams.set("includeItemsFromAllDrives", "true");

const response = await fetch(url.toString(), {
method: "GET",
Expand All @@ -136,7 +140,7 @@ async function findFolder(
}

async function createFolder(auth: ResolvedAuth, name: string, parentId: string): Promise<string> {
const response = await fetch(`${getApiBase()}/drive/v3/files`, {
const response = await fetch(`${getApiBase()}/drive/v3/files?supportsAllDrives=true`, {
method: "POST",
headers: {
...authHeaders(auth),
Expand Down Expand Up @@ -252,7 +256,7 @@ export const gdriveProvider: StorageProvider = {
meta.contentType
);

const response = await fetch(`${getApiBase()}/upload/drive/v3/files?uploadType=multipart`, {
const response = await fetch(`${getApiBase()}/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true`, {
method: "POST",
headers: {
...authHeaders(auth),
Expand Down Expand Up @@ -288,7 +292,7 @@ export const gdriveProvider: StorageProvider = {
meta: FileMeta
): Promise<WriteResult> {
const response = await fetch(
`${getApiBase()}/upload/drive/v3/files/${existingFileRef.id}?uploadType=media`,
`${getApiBase()}/upload/drive/v3/files/${existingFileRef.id}?uploadType=media&supportsAllDrives=true`,
{
method: "PATCH",
headers: {
Expand Down Expand Up @@ -337,6 +341,10 @@ export const gdriveProvider: StorageProvider = {
url.searchParams.set("q", q);
url.searchParams.set("fields", "nextPageToken,files(id,name,mimeType)");
url.searchParams.set("pageSize", "1000");
// Shared Drives require these on every list/query call, or folders
// that live in a shared drive are invisible to the query.
url.searchParams.set("supportsAllDrives", "true");
url.searchParams.set("includeItemsFromAllDrives", "true");
if (pageToken) {
url.searchParams.set("pageToken", pageToken);
}
Expand Down Expand Up @@ -378,7 +386,7 @@ export const gdriveProvider: StorageProvider = {
_container: ContainerRef,
fileRef: FileRef
): Promise<DownloadResult> {
const response = await fetch(`${getApiBase()}/drive/v3/files/${fileRef.id}?alt=media`, {
const response = await fetch(`${getApiBase()}/drive/v3/files/${fileRef.id}?alt=media&supportsAllDrives=true`, {
method: "GET",
headers: authHeaders(auth),
});
Expand Down
24 changes: 20 additions & 4 deletions lib/google-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,29 @@ export async function pickDriveFolder({ accessToken, apiKey, appId }) {

return new Promise((resolve, reject) => {
try {
const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
// A DOCS view rooted at My Drive (setParent "root") gives a navigable
// folder tree with a breadcrumb path -- unlike ViewId.FOLDERS, which
// renders every folder in one flat list with no hierarchy. Restricting
// mimeTypes to folders keeps files out, so the user only sees and picks
// folders while still drilling down through the tree.
const myDriveView = new google.picker.DocsView(google.picker.ViewId.DOCS)
.setParent("root")
.setMimeTypes("application/vnd.google-apps.folder")
.setSelectFolderEnabled(true)
.setIncludeFolders(true);
.setIncludeFolders(true)
.setSelectFolderEnabled(true);

// A second DOCS view (setEnableDrives) surfaces Shared Drives as their
// own navigable, folder-only tree alongside My Drive above.
const sharedDrivesView = new google.picker.DocsView(google.picker.ViewId.DOCS)
.setEnableDrives(true)
.setMimeTypes("application/vnd.google-apps.folder")
.setIncludeFolders(true)
.setSelectFolderEnabled(true);

const picker = new google.picker.PickerBuilder()
.addView(view)
.addView(myDriveView)
.addView(sharedDrivesView)
.enableFeature(google.picker.Feature.SUPPORT_DRIVES)
.setOAuthToken(accessToken)
.setDeveloperKey(apiKey)
.setAppId(appId)
Expand Down
1 change: 1 addition & 0 deletions pages/admin/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ function NewExperimentForm() {
<HStack gap={3}>
<Button
variant="outline"
colorPalette="brandTeal"
size="md"
loading={folderPickerLoading}
onClick={handleChooseFolder}
Expand Down