From 6aa1a2de88f3dbd76c10a02ec0215178478da64b Mon Sep 17 00:00:00 2001 From: abram axel booth Date: Tue, 6 May 2025 16:06:33 -0400 Subject: [PATCH 01/12] fix: use compact IRIs to match the api (yet todo: json-ld parsing that would handle either) --- app/models/index-card-search.ts | 2 +- app/models/search-result.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/index-card-search.ts b/app/models/index-card-search.ts index fc5250db51..5421da16dc 100644 --- a/app/models/index-card-search.ts +++ b/app/models/index-card-search.ts @@ -10,7 +10,7 @@ export interface SearchFilter { filterType?: string; } -export const ShareMoreThanTenThousand = 'https://share.osf.io/vocab/2023/trove/ten-thousands-and-more'; +export const ShareMoreThanTenThousand = 'trove:ten-thousands-and-more'; export default class IndexCardSearchModel extends Model { @attr('string') cardSearchText!: string; diff --git a/app/models/search-result.ts b/app/models/search-result.ts index f4c7dae903..f7279cb0a6 100644 --- a/app/models/search-result.ts +++ b/app/models/search-result.ts @@ -314,7 +314,7 @@ export default class SearchResultModel extends Model { } get isWithdrawn() { - return this.resourceMetadata.dateWithdrawn || this.resourceMetadata['https://osf.io/vocab/2022/withdrawal']; + return this.resourceMetadata.dateWithdrawn || this.resourceMetadata['osf:withdrawal']; } get configuredAddonNames() { From 75a686df8688c993b0e181144f5b0b4d524d915b Mon Sep 17 00:00:00 2001 From: abram axel booth Date: Wed, 7 May 2025 14:05:17 -0400 Subject: [PATCH 02/12] fix: IRIs represented as {"@id": "..."} --- lib/osf-components/addon/components/search-page/component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/osf-components/addon/components/search-page/component.ts b/lib/osf-components/addon/components/search-page/component.ts index c2a6803ae3..9b20948809 100644 --- a/lib/osf-components/addon/components/search-page/component.ts +++ b/lib/osf-components/addon/components/search-page/component.ts @@ -263,7 +263,7 @@ export default class SearchPage extends Component { this.nextPageCursor = searchResult.nextPageCursor; this.prevPageCursor = searchResult.prevPageCursor; this.searchResults = searchResult.searchResultPage.toArray(); - this.totalResultCount = searchResult.totalResultCount === ShareMoreThanTenThousand ? '10,000+' : + this.totalResultCount = searchResult.totalResultCount['@id'] === ShareMoreThanTenThousand ? '10,000+' : searchResult.totalResultCount; } catch (e) { this.toast.error(e); From f2f3d738942dee6f0ef798c8a0d89f4b88949c06 Mon Sep 17 00:00:00 2001 From: abram axel booth Date: Thu, 8 May 2025 08:43:24 -0400 Subject: [PATCH 03/12] (partly) fix types --- lib/osf-components/addon/components/search-page/component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/osf-components/addon/components/search-page/component.ts b/lib/osf-components/addon/components/search-page/component.ts index 9b20948809..7217d24958 100644 --- a/lib/osf-components/addon/components/search-page/component.ts +++ b/lib/osf-components/addon/components/search-page/component.ts @@ -83,7 +83,7 @@ export default class SearchPage extends Component { @tracked relatedProperties?: RelatedPropertyPathModel[] = []; @tracked booleanFilters?: RelatedPropertyPathModel[] = []; @tracked page?: string = ''; - @tracked totalResultCount?: string | number; + @tracked totalResultCount?: number | {'@id': string}; @tracked firstPageCursor?: string | null; @tracked prevPageCursor?: string | null; @tracked nextPageCursor?: string | null; @@ -263,7 +263,7 @@ export default class SearchPage extends Component { this.nextPageCursor = searchResult.nextPageCursor; this.prevPageCursor = searchResult.prevPageCursor; this.searchResults = searchResult.searchResultPage.toArray(); - this.totalResultCount = searchResult.totalResultCount['@id'] === ShareMoreThanTenThousand ? '10,000+' : + this.totalResultCount = searchResult.totalResultCount?.['@id'] === ShareMoreThanTenThousand ? '10,000+' : searchResult.totalResultCount; } catch (e) { this.toast.error(e); From 73ab56c85b69d535be12252c98c9c9d18595a31a Mon Sep 17 00:00:00 2001 From: Lord Business <113387478+bp-cos@users.noreply.github.com> Date: Thu, 29 May 2025 08:14:52 -0500 Subject: [PATCH 04/12] [ENG-7876] Feature/file picker select root (#2556) * Added the files * Removed tokens * Updates to display the selected root folder * Added GFP to the files page * Updates to add the root folder * Added the manager to reload the wb once a new file or folder is selected * Enabled the disabled button featuree again * Fixed a test * Updates to the flow for a token from the BE * Added the final touches and removed pruned instructions * Updates for the PR * Updates to move all the google file picker code into the component * Cleaned up comments and models * Update for PR request --- app/config/environment.d.ts | 6 + app/guid-node/files/provider/template.hbs | 1 + app/models/authorized-account.ts | 3 +- app/models/authorized-storage-account.ts | 7 +- config/environment.js | 13 + .../configured-addon-edit/component.ts | 40 ++- .../configured-addon-edit/styles.scss | 5 + .../configured-addon-edit/template.hbs | 9 +- .../addons-service/manager/component.ts | 3 +- .../file-browser/add-new/component.ts | 49 ++- .../file-browser/add-new/template.hbs | 19 ++ .../components/file-browser/template.hbs | 6 +- .../google-file-picker-widget/component.ts | 278 ++++++++++++++++++ .../google-file-picker-widget/styles.scss | 24 ++ .../google-file-picker-widget/template.hbs | 27 ++ .../google-file-picker-widget/component.js | 1 + .../google-file-picker-widget/template.js | 1 + translations/en-us.yml | 6 + 18 files changed, 488 insertions(+), 10 deletions(-) create mode 100644 lib/osf-components/addon/components/google-file-picker-widget/component.ts create mode 100644 lib/osf-components/addon/components/google-file-picker-widget/styles.scss create mode 100644 lib/osf-components/addon/components/google-file-picker-widget/template.hbs create mode 100644 lib/osf-components/app/components/google-file-picker-widget/component.js create mode 100644 lib/osf-components/app/components/google-file-picker-widget/template.js diff --git a/app/config/environment.d.ts b/app/config/environment.d.ts index d29e42e6b8..863cd9add0 100644 --- a/app/config/environment.d.ts +++ b/app/config/environment.d.ts @@ -141,6 +141,12 @@ declare const config: { doiUrlPrefix: string; dataciteTrackerRepoId: string; dataCiteTrackerUrl: string; + googleFilePicker: { + GOOGLE_FILE_PICKER_SCOPES: string; + GOOGLE_FILE_PICKER_CLIENT_ID: string; + GOOGLE_FILE_PICKER_API_KEY: string; + GOOGLE_FILE_PICKER_APP_ID: number; + } }; social: { twitter: { diff --git a/app/guid-node/files/provider/template.hbs b/app/guid-node/files/provider/template.hbs index 8f1e23912c..0deaedcd1d 100644 --- a/app/guid-node/files/provider/template.hbs +++ b/app/guid-node/files/provider/template.hbs @@ -7,6 +7,7 @@ >
{ // To be implemented in child classes return; diff --git a/app/models/authorized-storage-account.ts b/app/models/authorized-storage-account.ts index 45ea1d0668..f23a509b2d 100644 --- a/app/models/authorized-storage-account.ts +++ b/app/models/authorized-storage-account.ts @@ -1,13 +1,16 @@ -import { AsyncBelongsTo, belongsTo } from '@ember-data/model'; +import { AsyncBelongsTo, attr, belongsTo } from '@ember-data/model'; import { waitFor } from '@ember/test-waiters'; import { task } from 'ember-concurrency'; import { ConnectedStorageOperationNames, OperationKwargs } from 'ember-osf-web/models/addon-operation-invocation'; +import ExternalStorageServiceModel from 'ember-osf-web/models/external-storage-service'; -import ExternalStorageServiceModel from './external-storage-service'; import AuthorizedAccountModel from './authorized-account'; import UserReferenceModel from './user-reference'; export default class AuthorizedStorageAccountModel extends AuthorizedAccountModel { + @attr('fixstring') serializeOauthToken!: string; + @attr('fixstring') oauthToken!: string; + @belongsTo('user-reference', { inverse: 'authorizedStorageAccounts' }) readonly accountOwner!: AsyncBelongsTo & UserReferenceModel; diff --git a/config/environment.js b/config/environment.js index 61bc5d0ec3..158d19b093 100644 --- a/config/environment.js +++ b/config/environment.js @@ -102,6 +102,13 @@ const { SHARE_SEARCH_URL: shareSearchUrl = 'http://localhost:8003/api/v2/search/creativeworks/_search', SOURCEMAPS_ENABLED: sourcemapsEnabled = true, SHOW_DEV_BANNER = false, + + GOOGLE_FILE_PICKER_SCOPES, + /* eslint-disable-next-line max-len */ + GOOGLE_FILE_PICKER_CLIENT_ID, + GOOGLE_FILE_PICKER_API_KEY, + GOOGLE_FILE_PICKER_APP_ID, + } = { ...process.env, ...localConfig }; module.exports = function(environment) { @@ -224,6 +231,12 @@ module.exports = function(environment) { doiUrlPrefix: 'https://doi.org/', dataciteTrackerRepoId, dataCiteTrackerUrl, + googleFilePicker: { + GOOGLE_FILE_PICKER_SCOPES, + GOOGLE_FILE_PICKER_CLIENT_ID, + GOOGLE_FILE_PICKER_API_KEY, + GOOGLE_FILE_PICKER_APP_ID, + }, }, social: { twitter: { diff --git a/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts b/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts index 6b55e01e76..28bec3d385 100644 --- a/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts +++ b/lib/osf-components/addon/components/addons-service/configured-addon-edit/component.ts @@ -1,7 +1,10 @@ import { action } from '@ember/object'; +import { waitFor } from '@ember/test-waiters'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { TaskInstance } from 'ember-concurrency'; +import { task, TaskInstance } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; + import { Item, ItemType } from 'ember-osf-web/models/addon-operation-invocation'; import AuthorizedAccountModel from 'ember-osf-web/models/authorized-account'; @@ -12,6 +15,7 @@ import ConfiguredAddonModel from 'ember-osf-web/models/configured-addon'; import ConfiguredCitationAddonModel from 'ember-osf-web/models/configured-citation-addon'; import ConfiguredComputingAddonModel from 'ember-osf-web/models/configured-computing-addon'; import ConfiguredStorageAddonModel from 'ember-osf-web/models/configured-storage-addon'; +import ExternalStorageServiceModel from 'ember-osf-web/models/external-storage-service'; interface Args { @@ -25,6 +29,8 @@ export default class ConfiguredAddonEdit extends Component { @tracked selectedFolder = this.args.configuredAddon?.rootFolder; @tracked selectedFolderDisplayName = this.args.configuredAddon?.rootFolderName; @tracked currentItems: Item[] = []; + @tracked isWBGoogleDrive = false; + @tracked accountId!: string; originalName = this.displayName; originalRootFolder = this.selectedFolder; @@ -34,6 +40,7 @@ export default class ConfiguredAddonEdit extends Component { super(owner, args); if (this.args.configuredAddon) { if (this.args.configuredAddon instanceof ConfiguredStorageAddonModel) { + taskFor(this.loadExternalStorageService).perform(); this.defaultKwargs['itemType'] = ItemType.Folder; } if (this.args.configuredAddon instanceof ConfiguredCitationAddonModel) { @@ -42,6 +49,7 @@ export default class ConfiguredAddonEdit extends Component { } if (this.args.authorizedAccount) { if (this.args.authorizedAccount instanceof AuthorizedStorageAccountModel) { + taskFor(this.loadExternalStorageService).perform(); this.defaultKwargs['itemType'] = ItemType.Folder; } if (this.args.authorizedAccount instanceof AuthorizedCitationAccountModel) { @@ -50,6 +58,28 @@ export default class ConfiguredAddonEdit extends Component { } } + /** + * This is called only to authorize because the current implementation will throw an + * error because the "root folder" is not yet set. + */ + @task + @waitFor + async loadExternalStorageService() { + let external!: ExternalStorageServiceModel; + if (this.args.configuredAddon && this.args.configuredAddon instanceof ConfiguredStorageAddonModel) { + const baseAccount = await this.args.configuredAddon.baseAccount; + this.accountId = baseAccount?.id; + external = await this.args.configuredAddon.externalStorageService; + } + if (this.args.authorizedAccount && this.args.authorizedAccount instanceof AuthorizedStorageAccountModel) { + external = await this.args.authorizedAccount.externalStorageService; + + this.accountId = this.args.authorizedAccount.id; + } + + this.isWBGoogleDrive = external?.wbKey === 'googledrive'; + } + get requiresRootFolder() { return !( this.args.authorizedAccount instanceof AuthorizedComputingAccountModel @@ -58,6 +88,14 @@ export default class ConfiguredAddonEdit extends Component { ); } + get isGoogleDrive(): boolean { + return this.isWBGoogleDrive; + } + + get displayFileManager(): boolean { + return this.requiresRootFolder && !this.isGoogleDrive; + } + get invalidDisplayName() { return !this.displayName || this.displayName?.trim().length === 0; } diff --git a/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss b/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss index 3dc6c476d5..f362d65404 100644 --- a/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss +++ b/lib/osf-components/addon/components/addons-service/configured-addon-edit/styles.scss @@ -62,3 +62,8 @@ .item-name { white-space: normal; } + +.picker-style { + white-space: pre-wrap; + +} diff --git a/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs b/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs index 4d40bc76e4..44737b94b7 100644 --- a/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs +++ b/lib/osf-components/addon/components/addons-service/configured-addon-edit/template.hbs @@ -21,7 +21,7 @@ {{/if}}
- {{#if this.requiresRootFolder }} + {{#if this.displayFileManager}}
{{t 'addons.configure.selected-folder'}} @@ -150,6 +150,13 @@ + {{else}} + {{/if}}
+ {{#if this.isGoogleDrive}} + + {{/if}} + {{#if this.isGoogleDrive}} + + {{/if}} {{/let}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/file-browser/template.hbs b/lib/osf-components/addon/components/file-browser/template.hbs index 924bf455b8..7e76424f15 100644 --- a/lib/osf-components/addon/components/file-browser/template.hbs +++ b/lib/osf-components/addon/components/file-browser/template.hbs @@ -160,7 +160,11 @@ @isRegistration={{@manager.targetNode.isRegistration}} /> {{#if (and @manager.currentFolder.userCanUploadToHere @enableUpload)}} - + {{/if}} {{/if}}
diff --git a/lib/osf-components/addon/components/google-file-picker-widget/component.ts b/lib/osf-components/addon/components/google-file-picker-widget/component.ts new file mode 100644 index 0000000000..49d42c2203 --- /dev/null +++ b/lib/osf-components/addon/components/google-file-picker-widget/component.ts @@ -0,0 +1,278 @@ +import Store from '@ember-data/store'; +import { action } from '@ember/object'; +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import config from 'ember-osf-web/config/environment'; +import { Item } from 'ember-osf-web/models/addon-operation-invocation'; +import StorageManager from 'osf-components/components/storage-provider-manager/storage-manager/component'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +const { + GOOGLE_FILE_PICKER_SCOPES, + GOOGLE_FILE_PICKER_CLIENT_ID, + GOOGLE_FILE_PICKER_API_KEY, + GOOGLE_FILE_PICKER_APP_ID, +} = config.OSF.googleFilePicker; + +// +// 📚 Interface for Expected Arguments +// +interface Args { + /** + * selectFolder + * + * @description + * A callback function passed into the component + * that accepts a partial Item object and handles it (e.g., selects a file). + */ + selectFolder?: (a: Partial) => void; + onRegisterChild?: (a: GoogleFilePickerWidget) => void; + selectedFolderName?: string; + isFolderPicker: boolean; + rootFolderId: string; + manager: StorageManager; + accountId: string; +} + +// +// 📚 Extend Global Window Type +// +// Declares that `window` can optionally have a GoogleFilePickerWidget instance. +// This allows safe typing when accessing it elsewhere. +// +declare global { + interface Window { + GoogleFilePickerWidget?: GoogleFilePickerWidget; + gapi?: any; + google?: any; + } +} + +// +// GoogleFilePickerWidget Component +// +// @description +// An Ember Glimmer component that exposes itself to the global `window` +// so that external JavaScript (like Google Picker API callbacks) +// can interact with it directly. +// +export default class GoogleFilePickerWidget extends Component { + @service intl!: Intl; + @service store!: Store; + @tracked folderName!: string | undefined; + @tracked isFolderPicker = false; + @tracked openGoogleFilePicker = false; + @tracked visible = false; + pickerInited = false; + gisInited = false; + selectFolder: any = undefined; + tokenClient: any = undefined; + accessToken!: string; + scopes = GOOGLE_FILE_PICKER_SCOPES; + clientId = GOOGLE_FILE_PICKER_CLIENT_ID; + apiKey = GOOGLE_FILE_PICKER_API_KEY; + appId = GOOGLE_FILE_PICKER_APP_ID; + mimeTypes = ''; + parentId = ''; + isMultipleSelect: boolean; + title!: string; + + /** + * Constructor + * + * @description + * Initializes the GoogleFilePickerWidget component and exposes its key methods to the global `window` object + * for integration with external JavaScript (e.g., Google Picker API). + * + * - Sets `window.GoogleFilePickerWidget` to the current component instance (`this`), + * allowing external scripts to call methods like `filePickerCallback()`. + * - Captures the closure action `selectFolder` from `this.args` and assigns it directly to `window.selectFolder`, + * preserving the correct closure reference even outside of Ember's internal context. + * + * @param owner - The owner/context passed by Ember at component instantiation. + * @param args - The arguments passed to the component, including closure actions like `selectFolder`. + */ + constructor(owner: unknown, args: Args) { + super(owner, args); + + window.GoogleFilePickerWidget = this; + this.selectFolder = this.args.selectFolder; + this.mimeTypes = this.args.isFolderPicker ? 'application/vnd.google-apps.folder' : ''; + this.parentId = this.args.isFolderPicker ? '': this.args.rootFolderId; + this.title = this.args.isFolderPicker ? + this.intl.t('addons.configure.google-file-picker.root-folder-title') : + this.intl.t('addons.configure.google-file-picker.file-folder-title'); + this.isMultipleSelect = !this.args.isFolderPicker; + this.isFolderPicker = this.args.isFolderPicker; + + + this.folderName = this.args.selectedFolderName; + + taskFor(this.loadOauthToken).perform(); + } + + @task + @waitFor + private async loadOauthToken(): Promise{ + if (this.args.accountId) { + const authorizedStorageAccount = await this.store. + findRecord('authorized-storage-account', this.args.accountId); + authorizedStorageAccount.serializeOauthToken = true; + const token = await authorizedStorageAccount.save(); + this.accessToken = token.oauthToken; + } + } + + /** + * filePickerCallback + * + * @description + * Action triggered when a file is selected via an external picker. + * Logs the file data and notifies the parent system by calling `selectFolder`. + * + * @param file - The file object selected (format determined by external API) + */ + @action + filePickerCallback(file: any) { + if (this.selectFolder !== undefined) { + this.folderName = file.name; + this.selectFolder({ + itemName: file.name, + itemId: file.id, + }); + } else { + this.args.manager.reload(); + } + } + + @action + openPicker() { + // Logic for opening Google File Picker here + if (this.handleAuthClick) { + this.handleAuthClick(); + } + } + + @action + registerComponent() { + if (this.args.onRegisterChild) { + this.args.onRegisterChild(this); // Pass the child's instance to the parent + } + } + + willDestroy() { + super.willDestroy(); + delete this.tokenClient; + this.pickerInited = false; + this.gisInited = false; + } + + + /** + * Callback after api.js is loaded. + */ + gapiLoaded() { + window.gapi.load('client:picker', this.initializePicker.bind(this)); + } + + /** + * Callback after the API client is loaded. Loads the + * discovery doc to initialize the API. + */ + async initializePicker() { + await window.gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'); + this.pickerInited = true; + this.maybeEnableButtons(); + } + + /** + * Callback after Google Identity Services are loaded. + */ + gisLoaded() { + if (!this.tokenClient) { + this.tokenClient = window.google?.accounts?.oauth2.initTokenClient({ + client_id: this.clientId, + scope: this.scopes, + callback: '', // defined later + }); + } + this.gisInited = true; + this.maybeEnableButtons(); + } + + /** + * Enables user interaction after all libraries are loaded. + */ + maybeEnableButtons() { + if (this.pickerInited && this.gisInited && this.isFolderPicker) { + this.visible = true; + } + } + + /** + * Sign in the user upon button click. + */ + @action + handleAuthClick() { + this.tokenClient.callback = async (response: any) => { + if (response.error !== undefined) { + throw (response); + } + await this.createPicker(); + }; + + if (this.accessToken === null) { + // Prompt the user to select a Google Account and ask for consent to share their data + // when establishing a new session. + this.tokenClient?.requestAccessToken({prompt: 'consent'}); + } else { + // Skip display of account chooser and consent dialog for an existing session. + this.tokenClient?.requestAccessToken({prompt: ''}); + } + } + + /** + * Create and render a Picker object for searching images. + */ + createPicker() { + const googlePickerView = new window.google.picker.DocsView(window.google.picker.ViewId.DOCS); + googlePickerView.setSelectFolderEnabled(true); + googlePickerView.setMimeTypes(this.mimeTypes); + googlePickerView.setIncludeFolders(true); + googlePickerView.setParent(this.parentId); + + const picker = new window.google.picker.PickerBuilder() + .enableFeature(this.isMultipleSelect ? window.google.picker.Feature.MULTISELECT_ENABLED : '') + .setDeveloperKey(this.apiKey) + .setAppId(this.appId) + .addView(googlePickerView) + .setTitle(this.title) + .setOAuthToken(this.accessToken) + .setCallback(this.pickerCallback.bind(this)) + .build(); + picker.setVisible(true); + } + + /** + * Displays the file details of the user's selection. + * @param {object} data - Containers the user selection from the picker + */ + async pickerCallback(data: any) { + if (data.action === window.google.picker.Action.PICKED) { + const document = data[window.google.picker.Response.DOCUMENTS][0]; + const fileId = document[window.google.picker.Document.ID]; + const res = await window.gapi.client.drive.files.get({ + fileId, + fields: '*', + }); + // Correctly call the Ember method + this.filePickerCallback(res.result); + } + } +} + + diff --git a/lib/osf-components/addon/components/google-file-picker-widget/styles.scss b/lib/osf-components/addon/components/google-file-picker-widget/styles.scss new file mode 100644 index 0000000000..1a6698f232 --- /dev/null +++ b/lib/osf-components/addon/components/google-file-picker-widget/styles.scss @@ -0,0 +1,24 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors, selector-no-qualifying-type + +.google-file-picker-container { + width: 100%; + border: 1px solid blacl; + + .instruction-container { + width: 100%; + border: 1px solid blacl; + } + + .action-container { + width: 100%; + border: 1px solid blacl; + + .authorize-button { + visibility: hidden; + + &.visible { + visibility: visible; + } + } + } +} diff --git a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs new file mode 100644 index 0000000000..d8652569c2 --- /dev/null +++ b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs @@ -0,0 +1,27 @@ +
+ {{#if this.isFolderPicker}} +
+ {{#if this.folderName}} +

+ + {{~t 'addons.configure.google-file-picker.selected-folder'~}}: + + + {{this.folderName}} + +

+ {{/if}} +
+
+ +
+ {{/if}} + + +
\ No newline at end of file diff --git a/lib/osf-components/app/components/google-file-picker-widget/component.js b/lib/osf-components/app/components/google-file-picker-widget/component.js new file mode 100644 index 0000000000..ad0f5c2048 --- /dev/null +++ b/lib/osf-components/app/components/google-file-picker-widget/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/google-file-picker-widget/component'; diff --git a/lib/osf-components/app/components/google-file-picker-widget/template.js b/lib/osf-components/app/components/google-file-picker-widget/template.js new file mode 100644 index 0000000000..b1eb6c3fcd --- /dev/null +++ b/lib/osf-components/app/components/google-file-picker-widget/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/google-file-picker-widget/template'; diff --git a/translations/en-us.yml b/translations/en-us.yml index 78b043bdc6..c49da356d4 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -358,6 +358,11 @@ addons: verify: 'Connect the following account:' authorizing: 'Connecting…' configure: + google-file-picker: + select-root-folder: 'Select Root Folder' + selected-folder: 'Selected Folder' + root-folder-title: 'Select a root folder' + file-folder-title: 'Select a file or folder to add' heading: 'Configure {providerName}' display-name: 'Display name' selected-folder: 'Selected folder:' @@ -3038,6 +3043,7 @@ osf-components: error_ends_with_dot: 'File name cannot end with period' error_forbidden_chars: 'Please remove special characters from the folder name.' add_button_aria: 'Add files or folders here' + add-from-drive: 'Add from Drive' upload_file: 'Upload file' uploading_file: 'Uploading {fileCount, plural, one {# file} other {# files}}' upload_failed: '{fileCount, plural, one {# file} other {# files}} failed, click to try again' From 8c360671ff6e231be67ed86666a14a596f55fd34 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 5 Jun 2025 06:40:57 -0400 Subject: [PATCH 05/12] do not send initiate_oauth after oauth flow is successful --- app/packages/addons-service/provider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/packages/addons-service/provider.ts b/app/packages/addons-service/provider.ts index 602aa683fb..edb62595a3 100644 --- a/app/packages/addons-service/provider.ts +++ b/app/packages/addons-service/provider.ts @@ -237,6 +237,7 @@ export default class Provider { accountOwner: this.userReference, }); await newAccount.save(); + newAccount.initiateOauth = null; return newAccount; } From cd58445e4a52c495e1fbc46eacac9cc35b27c804 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Thu, 5 Jun 2025 15:33:49 -0400 Subject: [PATCH 06/12] fix --- .../google-file-picker-widget/component.ts | 57 +++---------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/lib/osf-components/addon/components/google-file-picker-widget/component.ts b/lib/osf-components/addon/components/google-file-picker-widget/component.ts index 49d42c2203..65f3cf95e9 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/component.ts +++ b/lib/osf-components/addon/components/google-file-picker-widget/component.ts @@ -13,7 +13,6 @@ import Intl from 'ember-intl/services/intl'; const { GOOGLE_FILE_PICKER_SCOPES, - GOOGLE_FILE_PICKER_CLIENT_ID, GOOGLE_FILE_PICKER_API_KEY, GOOGLE_FILE_PICKER_APP_ID, } = config.OSF.googleFilePicker; @@ -68,12 +67,9 @@ export default class GoogleFilePickerWidget extends Component { @tracked openGoogleFilePicker = false; @tracked visible = false; pickerInited = false; - gisInited = false; selectFolder: any = undefined; - tokenClient: any = undefined; accessToken!: string; scopes = GOOGLE_FILE_PICKER_SCOPES; - clientId = GOOGLE_FILE_PICKER_CLIENT_ID; apiKey = GOOGLE_FILE_PICKER_API_KEY; appId = GOOGLE_FILE_PICKER_APP_ID; mimeTypes = ''; @@ -137,12 +133,12 @@ export default class GoogleFilePickerWidget extends Component { * @param file - The file object selected (format determined by external API) */ @action - filePickerCallback(file: any) { + filePickerCallback(data: any) { if (this.selectFolder !== undefined) { - this.folderName = file.name; + this.folderName = data.name; this.selectFolder({ - itemName: file.name, - itemId: file.id, + itemName: data.name, + itemId: data.id, }); } else { this.args.manager.reload(); @@ -166,9 +162,7 @@ export default class GoogleFilePickerWidget extends Component { willDestroy() { super.willDestroy(); - delete this.tokenClient; this.pickerInited = false; - this.gisInited = false; } @@ -184,31 +178,15 @@ export default class GoogleFilePickerWidget extends Component { * discovery doc to initialize the API. */ async initializePicker() { - await window.gapi.client.load('https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'); this.pickerInited = true; this.maybeEnableButtons(); } - /** - * Callback after Google Identity Services are loaded. - */ - gisLoaded() { - if (!this.tokenClient) { - this.tokenClient = window.google?.accounts?.oauth2.initTokenClient({ - client_id: this.clientId, - scope: this.scopes, - callback: '', // defined later - }); - } - this.gisInited = true; - this.maybeEnableButtons(); - } - /** * Enables user interaction after all libraries are loaded. */ maybeEnableButtons() { - if (this.pickerInited && this.gisInited && this.isFolderPicker) { + if (this.pickerInited && this.isFolderPicker) { this.visible = true; } } @@ -218,21 +196,7 @@ export default class GoogleFilePickerWidget extends Component { */ @action handleAuthClick() { - this.tokenClient.callback = async (response: any) => { - if (response.error !== undefined) { - throw (response); - } - await this.createPicker(); - }; - - if (this.accessToken === null) { - // Prompt the user to select a Google Account and ask for consent to share their data - // when establishing a new session. - this.tokenClient?.requestAccessToken({prompt: 'consent'}); - } else { - // Skip display of account chooser and consent dialog for an existing session. - this.tokenClient?.requestAccessToken({prompt: ''}); - } + this.createPicker(); } /** @@ -263,14 +227,7 @@ export default class GoogleFilePickerWidget extends Component { */ async pickerCallback(data: any) { if (data.action === window.google.picker.Action.PICKED) { - const document = data[window.google.picker.Response.DOCUMENTS][0]; - const fileId = document[window.google.picker.Document.ID]; - const res = await window.gapi.client.drive.files.get({ - fileId, - fields: '*', - }); - // Correctly call the Ember method - this.filePickerCallback(res.result); + this.filePickerCallback(data.docs[0]); } } } From 16373524c45c87c3b678ab17388b0a879e29eedd Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Thu, 5 Jun 2025 18:01:02 -0500 Subject: [PATCH 07/12] Removed a call to a function --- .../addon/components/google-file-picker-widget/template.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs index d8652569c2..b5ecf732cd 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs +++ b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs @@ -23,5 +23,4 @@ local-class='google-file-picker-container {{if this.isMobile 'mobile'}}'
{{/if}} - \ No newline at end of file From e8a910868d7dc2e4c7dbddab541ee048e1324287 Mon Sep 17 00:00:00 2001 From: Lord Business <113387478+bp-cos@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:53:18 -0500 Subject: [PATCH 08/12] Added functionality so the GD menu item and select folder are not enabled until oauth token is loaded (#2571) * Removed a call to a function * Updates to disable the menu and select folder button until the token is loaded --- .../file-browser/add-new/component.ts | 6 +++- .../file-browser/add-new/template.hbs | 1 + .../google-file-picker-widget/component.ts | 28 +++---------------- .../google-file-picker-widget/template.hbs | 3 +- 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/lib/osf-components/addon/components/file-browser/add-new/component.ts b/lib/osf-components/addon/components/file-browser/add-new/component.ts index 28348eb6b6..5aaaef6ce3 100644 --- a/lib/osf-components/addon/components/file-browser/add-new/component.ts +++ b/lib/osf-components/addon/components/file-browser/add-new/component.ts @@ -46,6 +46,10 @@ export default class FileBrowser extends Component { return this.isWBGoogleDrive; } + get isGoogleAuthorized(): boolean { + return this.googlePickerComponent?.isGFPDisabled || false; + } + @action registerChild(child: GoogleFilePickerWidget) { this.googlePickerComponent = child; // Store the child's instance @@ -55,7 +59,7 @@ export default class FileBrowser extends Component { openGoogleFilePicker(dropdown: any) { dropdown.close(); if (this.googlePickerComponent) { - this.googlePickerComponent.openPicker(); + this.googlePickerComponent.createPicker(); } } } diff --git a/lib/osf-components/addon/components/file-browser/add-new/template.hbs b/lib/osf-components/addon/components/file-browser/add-new/template.hbs index 0503aab30e..a998ad9deb 100644 --- a/lib/osf-components/addon/components/file-browser/add-new/template.hbs +++ b/lib/osf-components/addon/components/file-browser/add-new/template.hbs @@ -40,6 +40,7 @@ data-test-add-google-drive @layout='fake-link' {{on 'click' (fn this.openGoogleFilePicker dropdown)}} + disabled={{this.isGoogleAuthorized}} > {{t 'osf-components.file-browser.add-from-drive'}} diff --git a/lib/osf-components/addon/components/google-file-picker-widget/component.ts b/lib/osf-components/addon/components/google-file-picker-widget/component.ts index 65f3cf95e9..8f104d89dc 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/component.ts +++ b/lib/osf-components/addon/components/google-file-picker-widget/component.ts @@ -66,6 +66,7 @@ export default class GoogleFilePickerWidget extends Component { @tracked isFolderPicker = false; @tracked openGoogleFilePicker = false; @tracked visible = false; + @tracked isGFPDisabled = true; pickerInited = false; selectFolder: any = undefined; accessToken!: string; @@ -120,6 +121,7 @@ export default class GoogleFilePickerWidget extends Component { authorizedStorageAccount.serializeOauthToken = true; const token = await authorizedStorageAccount.save(); this.accessToken = token.oauthToken; + this.isGFPDisabled = this.accessToken ? false : true; } } @@ -145,14 +147,6 @@ export default class GoogleFilePickerWidget extends Component { } } - @action - openPicker() { - // Logic for opening Google File Picker here - if (this.handleAuthClick) { - this.handleAuthClick(); - } - } - @action registerComponent() { if (this.args.onRegisterChild) { @@ -179,29 +173,15 @@ export default class GoogleFilePickerWidget extends Component { */ async initializePicker() { this.pickerInited = true; - this.maybeEnableButtons(); - } - - /** - * Enables user interaction after all libraries are loaded. - */ - maybeEnableButtons() { - if (this.pickerInited && this.isFolderPicker) { + if (this.isFolderPicker) { this.visible = true; } } - /** - * Sign in the user upon button click. - */ - @action - handleAuthClick() { - this.createPicker(); - } - /** * Create and render a Picker object for searching images. */ + @action createPicker() { const googlePickerView = new window.google.picker.DocsView(window.google.picker.ViewId.DOCS); googlePickerView.setSelectFolderEnabled(true); diff --git a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs index b5ecf732cd..73113fbb47 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs +++ b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs @@ -16,7 +16,8 @@ local-class='google-file-picker-container {{if this.isMobile 'mobile'}}'
From a893273f5f5e556d2e8a847c3e91f4512ac942a6 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Mon, 9 Jun 2025 14:20:37 -0400 Subject: [PATCH 09/12] remove unnecessary group embed --- lib/osf-components/addon/components/activity-log/component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/osf-components/addon/components/activity-log/component.ts b/lib/osf-components/addon/components/activity-log/component.ts index 1f6e9318c0..dc3b9614a7 100644 --- a/lib/osf-components/addon/components/activity-log/component.ts +++ b/lib/osf-components/addon/components/activity-log/component.ts @@ -4,7 +4,7 @@ export default class ActivityLogComponent extends Component { public loadEmbeds = { embed: [ - 'group', 'linked_node', 'linked_registration', 'original_node', + 'linked_node', 'linked_registration', 'original_node', 'template_node', 'user', ], }; From 7b1a228f14a9bc685da75803f64951dfdf2f0245 Mon Sep 17 00:00:00 2001 From: Lord Business <113387478+bp-cos@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:31:18 -0500 Subject: [PATCH 10/12] Fixed a css issue (#2577) --- .../addon/components/google-file-picker-widget/styles.scss | 7 ++++--- .../components/google-file-picker-widget/template.hbs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/osf-components/addon/components/google-file-picker-widget/styles.scss b/lib/osf-components/addon/components/google-file-picker-widget/styles.scss index 1a6698f232..bce6451396 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/styles.scss +++ b/lib/osf-components/addon/components/google-file-picker-widget/styles.scss @@ -2,16 +2,17 @@ .google-file-picker-container { width: 100%; - border: 1px solid blacl; + + &.file-picker { + width: 0; + } .instruction-container { width: 100%; - border: 1px solid blacl; } .action-container { width: 100%; - border: 1px solid blacl; .authorize-button { visibility: hidden; diff --git a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs index 73113fbb47..f00ea98b7e 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/template.hbs +++ b/lib/osf-components/addon/components/google-file-picker-widget/template.hbs @@ -1,5 +1,5 @@
{{#if this.isFolderPicker}}
From 392d58714716be8def4317f627e591bf86d001e8 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Fri, 13 Jun 2025 09:59:29 -0500 Subject: [PATCH 11/12] Fixed the logic --- .../addon/components/google-file-picker-widget/styles.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/osf-components/addon/components/google-file-picker-widget/styles.scss b/lib/osf-components/addon/components/google-file-picker-widget/styles.scss index bce6451396..a1e4c242a9 100644 --- a/lib/osf-components/addon/components/google-file-picker-widget/styles.scss +++ b/lib/osf-components/addon/components/google-file-picker-widget/styles.scss @@ -1,10 +1,10 @@ // stylelint-disable max-nesting-depth, selector-max-compound-selectors, selector-no-qualifying-type .google-file-picker-container { - width: 100%; + width: 0; &.file-picker { - width: 0; + width: 100%; } .instruction-container { From 069e04f228b15d7ab00b77287ee5b3284d4ddc2e Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Fri, 13 Jun 2025 13:14:56 -0400 Subject: [PATCH 12/12] Bump version no. Add CHANGELOG --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 652291afdc..b8ca3a95b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [25.12.0] - 2025-06-13 +### Added +- Google File Picker workflow +- Misc. improvements + ## [25.11.0] - 2025-06-11 ### Added - Manual GUID and DOI assignment during Preprint and Registration Creation diff --git a/package.json b/package.json index a420d92cc0..b7b3331d81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "25.11.0", + "version": "25.12.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme",