diff --git a/eslint.config.js b/eslint.config.js index 31c8a9a1a..1b4ca309c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -82,6 +82,7 @@ module.exports = tseslint.config( ], 'simple-import-sort/exports': 'error', 'unused-imports/no-unused-imports': 'error', + 'no-console': 'error', }, }, { diff --git a/src/app/features/project/addons/addons.component.spec.ts b/src/app/features/project/addons/addons.component.spec.ts index 8afcb774e..4a0671c36 100644 --- a/src/app/features/project/addons/addons.component.spec.ts +++ b/src/app/features/project/addons/addons.component.spec.ts @@ -10,7 +10,7 @@ import { AddonsComponent } from './addons.component'; import { OSFTestingModule } from '@testing/osf.testing.module'; -describe('AddonsComponent', () => { +describe('Component: Addons', () => { let component: AddonsComponent; let fixture: ComponentFixture; @@ -33,10 +33,6 @@ describe('AddonsComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should render the connected description paragraph', () => { component['selectedTab'].set(component['AddonTabValue'].ALL_ADDONS); fixture.detectChanges(); diff --git a/src/app/features/project/addons/components/configure-addon/configure-addon.component.html b/src/app/features/project/addons/components/configure-addon/configure-addon.component.html index ab72be55c..9e2993dd6 100644 --- a/src/app/features/project/addons/components/configure-addon/configure-addon.component.html +++ b/src/app/features/project/addons/components/configure-addon/configure-addon.component.html @@ -65,6 +65,7 @@

{{ 'settings.addons.connectAddon.chooseExistingAccount' | trans

{{ 'settings.addons.connectAddon.configure' | translate }} {{ addon()?.displayName }}

({} as AuthorizedAccountModel); + public readonly isGoogleDrive = computed(() => { + return this.selectedAccount()?.externalServiceName === 'googledrive'; + }); protected addonsUserReference = select(AddonsSelectors.getAddonsUserReference); protected createdAuthorizedAddon = select(AddonsSelectors.getCreatedOrUpdatedAuthorizedAddon); @@ -137,14 +141,15 @@ export class ConnectConfiguredAddonComponent { protected handleCreateConfiguredAddon() { const addon = this.addon(); - const selectedAccount = this.currentAuthorizedAddonAccounts().find( - (account) => account.id === this.chosenAccountId() + this.selectedAccount.set( + this.currentAuthorizedAddonAccounts().find((account) => account.id === this.chosenAccountId()) || + ({} as AuthorizedAccountModel) ); - if (!addon || !selectedAccount) return; + if (!addon || !this.selectedAccount()) return; const payload = this.addonFormService.generateConfiguredAddonCreatePayload( addon, - selectedAccount, + this.selectedAccount(), this.userReferenceId(), this.resourceUri(), this.accountNameControl.value || '', @@ -181,19 +186,20 @@ export class ConnectConfiguredAddonComponent { } protected handleConfirmAccountConnection(): void { - const selectedAccount = this.currentAuthorizedAddonAccounts().find( - (account) => account.id === this.chosenAccountId() + this.selectedAccount.set( + this.currentAuthorizedAddonAccounts().find((account) => account.id === this.chosenAccountId()) || + ({} as AuthorizedAccountModel) ); - if (!selectedAccount) return; + if (!this.selectedAccount()) return; - const dialogRef = this.addonDialogService.openConfirmAccountConnectionDialog(selectedAccount); + const dialogRef = this.addonDialogService.openConfirmAccountConnectionDialog(this.selectedAccount()); dialogRef.subscribe((result) => { if (result?.success) { this.stepper()?.value.set(ProjectAddonsStepperValue.CONFIGURE_ROOT_FOLDER); - this.chosenAccountName.set(selectedAccount.displayName); - this.accountNameControl.setValue(selectedAccount.displayName); + this.chosenAccountName.set(this.selectedAccount().displayName); + this.accountNameControl.setValue(this.selectedAccount().displayName); } }); } diff --git a/src/app/shared/components/addons/folder-selector/folder-selector.component.html b/src/app/shared/components/addons/folder-selector/folder-selector.component.html index 86350ac4a..dfe944a76 100644 --- a/src/app/shared/components/addons/folder-selector/folder-selector.component.html +++ b/src/app/shared/components/addons/folder-selector/folder-selector.component.html @@ -34,7 +34,12 @@

@if (isOperationInvocationSubmitting()) { } @else if (isGoogleFilePicker()) { - + } @else {
diff --git a/src/app/shared/components/addons/folder-selector/folder-selector.component.spec.ts b/src/app/shared/components/addons/folder-selector/folder-selector.component.spec.ts index 0507b7aa4..f5783c2fd 100644 --- a/src/app/shared/components/addons/folder-selector/folder-selector.component.spec.ts +++ b/src/app/shared/components/addons/folder-selector/folder-selector.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { OperationNames } from '@osf/features/project/addons/enums'; import { FolderSelectorComponent } from '@shared/components/addons'; import { MOCK_STORE, TranslateServiceMock } from '@shared/mocks'; -import { StorageItem } from '@shared/models'; +import { StorageItemModel } from '@shared/models'; describe('FolderSelectorComponent', () => { let component: FolderSelectorComponent; @@ -65,11 +65,11 @@ describe('FolderSelectorComponent', () => { }); it('should set selectedRootFolderId', () => { - const mockFolder: StorageItem = { + const mockFolder: StorageItemModel = { itemId: 'test-folder-id', itemName: 'Test Folder', itemType: 'folder', - } as StorageItem; + } as StorageItemModel; (component as any).selectedRootFolder.set(mockFolder); (component as any).handleSave(); diff --git a/src/app/shared/components/addons/folder-selector/folder-selector.component.ts b/src/app/shared/components/addons/folder-selector/folder-selector.component.ts index 92d2f6896..115ac1739 100644 --- a/src/app/shared/components/addons/folder-selector/folder-selector.component.ts +++ b/src/app/shared/components/addons/folder-selector/folder-selector.component.ts @@ -27,7 +27,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { OperationNames } from '@osf/features/project/addons/enums'; -import { OperationInvokeData, StorageItem } from '@shared/models'; +import { OperationInvokeData, StorageItemModel } from '@shared/models'; import { AddonsSelectors } from '@shared/stores/addons'; import { GoogleFilePickerComponent } from './google-file-picker/google-file-picker.component'; @@ -56,7 +56,8 @@ export class FolderSelectorComponent implements OnInit { isGoogleFilePicker = input.required(); accountName = input.required(); - operationInvocationResult = input.required(); + accountId = input.required(); + operationInvocationResult = input.required(); accountNameControl = input(new FormControl()); isCreateMode = input(false); @@ -67,7 +68,7 @@ export class FolderSelectorComponent implements OnInit { protected readonly OperationNames = OperationNames; protected hasInputChanged = signal(false); protected hasFolderChanged = signal(false); - protected selectedRootFolder = signal(null); + public selectedRootFolder = signal(null); protected breadcrumbItems = signal([]); protected initiallySelectedFolder = select(AddonsSelectors.getSelectedFolder); protected isOperationInvocationSubmitting = select(AddonsSelectors.getOperationInvocationSubmitting); @@ -126,10 +127,10 @@ export class FolderSelectorComponent implements OnInit { this.cancelSelection.emit(); } - protected handleFolderSelection(folder: StorageItem): void { + public handleFolderSelection = (folder: StorageItemModel): void => { this.selectedRootFolder.set(folder); this.hasFolderChanged.set(folder?.itemId !== this.initiallySelectedFolder()?.itemId); - } + }; private updateBreadcrumbs( operationName: OperationNames, diff --git a/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.spec.ts b/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.spec.ts index 6a2f01652..8cd5480b6 100644 --- a/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.spec.ts +++ b/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.spec.ts @@ -17,6 +17,7 @@ describe('Component: Google File Picker', () => { loadGapiModules: jest.fn().mockReturnValue(of(void 0)), }; + const handleFolderSelection = jest.fn(); const setDeveloperKey = jest.fn().mockReturnThis(); const setAppId = jest.fn().mockReturnThis(); const addView = jest.fn().mockReturnThis(); @@ -39,6 +40,18 @@ describe('Component: Google File Picker', () => { selectSnapshot: jest.fn().mockReturnValue('mock-token'), }; + beforeAll(() => { + window.google = { + picker: { + Action: null, + }, + }; + }); + + afterAll(() => { + delete (window as any).google; + }); + describe('isFolderPicker - true', () => { beforeEach(async () => { jest.clearAllMocks(); @@ -84,8 +97,10 @@ describe('Component: Google File Picker', () => { fixture = TestBed.createComponent(GoogleFilePickerComponent); component = fixture.componentInstance; fixture.componentRef.setInput('isFolderPicker', true); - fixture.componentRef.setInput('rootFolderId', 'root-folder-id'); - fixture.componentRef.setInput('selectedFolderName', 'selected-folder-name'); + fixture.componentRef.setInput('rootFolder', { + itemId: 'root-folder-id', + }); + fixture.componentRef.setInput('handleFolderSelection', handleFolderSelection); fixture.componentRef.setInput('accountId', 'account-id'); fixture.detectChanges(); }); @@ -118,6 +133,41 @@ describe('Component: Google File Picker', () => { expect(build).toHaveBeenCalledWith(); expect(setVisible).toHaveBeenCalledWith(true); }); + + describe('pickerCallback', () => { + it('should handle a folder selection `PICKED` action', () => { + window.google.picker.Action = { + PICKED: 'PICKED', + }; + component.pickerCallback( + Object({ + action: 'PICKED', + docs: [ + Object({ + itemId: 'item id', + itemName: 'item name', + }), + ], + }) + ); + + expect(handleFolderSelection).toHaveBeenCalledWith(Object({})); + }); + + it('should handle a folder selection not `PICKED` action', () => { + window.google.picker.Action = { + PICKED: 'not picked', + }; + + component.pickerCallback( + Object({ + action: 'Loading', + }) + ); + + expect(handleFolderSelection).not.toHaveBeenCalled(); + }); + }); }); describe('isFolderPicker - false', () => { @@ -164,8 +214,10 @@ describe('Component: Google File Picker', () => { fixture = TestBed.createComponent(GoogleFilePickerComponent); component = fixture.componentInstance; fixture.componentRef.setInput('isFolderPicker', false); - fixture.componentRef.setInput('rootFolderId', 'root-folder-id'); - fixture.componentRef.setInput('selectedFolderName', 'selected-folder-name'); + fixture.componentRef.setInput('rootFolder', { + itemId: 'root-folder-id', + }); + fixture.componentRef.setInput('handleFolderSelection', jest.fn()); fixture.detectChanges(); }); diff --git a/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.ts b/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.ts index 7f27a2184..6decdbcda 100644 --- a/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.ts +++ b/src/app/shared/components/addons/folder-selector/google-file-picker/google-file-picker.component.ts @@ -7,6 +7,9 @@ import { Button } from 'primeng/button'; import { ChangeDetectionStrategy, Component, inject, input, OnInit, signal } from '@angular/core'; import { ENVIRONMENT } from '@core/constants/environment.token'; +import { StorageItemModel } from '@osf/shared/models'; +import { GoogleFileDataModel } from '@osf/shared/models/files/google-file.data.model'; +import { GoogleFilePickerModel } from '@osf/shared/models/files/google-file.picker.model'; import { AddonsSelectors, GetAuthorizedStorageOauthToken } from '@osf/shared/stores'; import { GoogleFilePickerDownloadService } from './service/google-file-picker.download.service'; @@ -25,18 +28,11 @@ export class GoogleFilePickerComponent implements OnInit { readonly #environment = inject(ENVIRONMENT); public isFolderPicker = input.required(); - public selectedFolderName = input(''); - public rootFolderId = input(''); + public rootFolder = input(null); public accountId = input(''); + public handleFolderSelection = input.required<(folder: StorageItemModel) => void>(); - // selectFolder?: (a: Partial) => void; - // onRegisterChild?: (a: GoogleFilePickerWidget) => void; - // manager: StorageManager; - // @tracked openGoogleFilePicker = false; - private folderName = signal(''); - selectFolder = undefined; - accessToken = signal(null); - + public accessToken = signal(null); public visible = signal(false); public isGFPDisabled = signal(true); private readonly apiKey = this.#environment.google.GOOGLE_FILE_PICKER_API_KEY; @@ -47,20 +43,17 @@ export class GoogleFilePickerComponent implements OnInit { private title!: string; ngOnInit(): void { - // window.GoogleFilePickerWidget = this; - // this.selectFolder = this.selectFolder(); - this.parentId = this.isFolderPicker() ? '' : this.rootFolderId(); + this.parentId = this.isFolderPicker() ? '' : this.rootFolder()?.itemId || ''; this.title = this.isFolderPicker() ? this.#translateService.instant('settings.addons.configureAddon.google-file-picker.root-folder-title') : this.#translateService.instant('settings.addons.configureAddon.google-file-picker.file-folder-title'); this.isMultipleSelect = !this.isFolderPicker(); - this.folderName.set(this.selectedFolderName()); this.#googlePicker.loadScript().subscribe({ next: () => { this.#googlePicker.loadGapiModules().subscribe({ next: () => { - this.initializePicker(); + this.#initializePicker(); this.#loadOauthToken(); }, // TODO add this error when the Sentry service is working @@ -72,13 +65,13 @@ export class GoogleFilePickerComponent implements OnInit { }); } - public initializePicker() { + #initializePicker() { if (this.isFolderPicker()) { this.visible.set(true); } } - createPicker(): void { + public createPicker(): void { const google = window.google; const googlePickerView = new google.picker.DocsView(google.picker.ViewId.DOCS); @@ -118,62 +111,18 @@ export class GoogleFilePickerComponent implements OnInit { } } - // /** - // * Displays the file details of the user's selection. - // * @param {object} data - Containers the user selection from the picker - // */ - // eslint-disable-next-line - async pickerCallback(data: any) { - // async pickerCallback(data: any) { - // if (data.action === window.google.picker.Action.PICKED) { - // this.filePickerCallback(data.docs[0]); - // } - // } - console.log('data'); + #filePickerCallback(data: GoogleFileDataModel) { + this.handleFolderSelection()( + Object({ + itemName: data.name, + itemId: data.id, + }) + ); } -} -// /** -// * 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(data: any) { -// if (this.selectFolder !== undefined) { -// this.folderName = data.name; -// this.selectFolder({ -// itemName: data.name, -// itemId: data.id, -// }); -// } else { -// this.args.manager.reload(); -// } -// } - -// @action -// registerComponent() { -// if (this.args.onRegisterChild) { -// this.args.onRegisterChild(this); // Pass the child's instance to the parent -// } -// } - -// willDestroy() { -// super.willDestroy(); -// this.pickerInited = false; -// } - -// /** -// * 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) { -// this.filePickerCallback(data.docs[0]); -// } -// } -// } + pickerCallback(data: GoogleFilePickerModel) { + if (data.action === window.google.picker.Action.PICKED) { + this.#filePickerCallback(data.docs[0]); + } + } +} diff --git a/src/app/shared/models/addons/index.ts b/src/app/shared/models/addons/index.ts index 9c22ecdad..0c82ab2fb 100644 --- a/src/app/shared/models/addons/index.ts +++ b/src/app/shared/models/addons/index.ts @@ -5,4 +5,5 @@ export * from './authorized-account.model'; export * from './configured-storage-addon.model'; export * from './operation-invocation.models'; export * from './operation-invoke-data.model'; +export * from './strorage-item.model'; export * from './term.model'; diff --git a/src/app/shared/models/addons/operation-invocation.models.ts b/src/app/shared/models/addons/operation-invocation.models.ts index 107aa00d9..2dfc69763 100644 --- a/src/app/shared/models/addons/operation-invocation.models.ts +++ b/src/app/shared/models/addons/operation-invocation.models.ts @@ -1,3 +1,5 @@ +import { StorageItemModel } from './strorage-item.model'; + export interface StorageItemResponseJsonApi { item_id?: string; item_name?: string; @@ -55,14 +57,6 @@ export interface OperationInvocationResponseJsonApi { }; } -export interface StorageItem { - itemId?: string; - itemName?: string; - itemType?: string; - canBeRoot?: boolean; - mayContainRootCandidates?: boolean; -} - export interface OperationInvocation { id: string; type: string; @@ -72,6 +66,6 @@ export interface OperationInvocation { itemId?: string; itemType?: string; }; - operationResult: StorageItem[]; + operationResult: StorageItemModel[]; itemCount: number; } diff --git a/src/app/shared/models/addons/strorage-item.model.ts b/src/app/shared/models/addons/strorage-item.model.ts new file mode 100644 index 000000000..7b8b79542 --- /dev/null +++ b/src/app/shared/models/addons/strorage-item.model.ts @@ -0,0 +1,7 @@ +export interface StorageItemModel { + itemId?: string; + itemName?: string; + itemType?: string; + canBeRoot?: boolean; + mayContainRootCandidates?: boolean; +} diff --git a/src/app/shared/models/files/google-file.data.model.ts b/src/app/shared/models/files/google-file.data.model.ts new file mode 100644 index 000000000..7de592dae --- /dev/null +++ b/src/app/shared/models/files/google-file.data.model.ts @@ -0,0 +1,16 @@ +/** + * Represents a simplified file object returned from the Google File Picker. + * + * This model is used to extract and store essential metadata for a selected file. + */ +export interface GoogleFileDataModel { + /** + * The display name of the selected file. + */ + name: string; + + /** + * The unique identifier assigned to the file. + */ + id: number; +} diff --git a/src/app/shared/models/files/google-file.picker.model.ts b/src/app/shared/models/files/google-file.picker.model.ts new file mode 100644 index 000000000..4618e0354 --- /dev/null +++ b/src/app/shared/models/files/google-file.picker.model.ts @@ -0,0 +1,18 @@ +import { GoogleFileDataModel } from './google-file.data.model'; + +/** + * Represents the data returned by the Google File Picker integration. + */ +export interface GoogleFilePickerModel { + /** + * The type of action performed by the user in the file picker. + * For example: 'picked', 'cancelled'. + */ + action: string; + + /** + * The list of documents selected by the user. + * Each document is represented as a GoogleFileDataModel. + */ + docs: GoogleFileDataModel[]; +} diff --git a/src/app/shared/services/addons/addons.service.spec.ts b/src/app/shared/services/addons/addons.service.spec.ts index d5804fcd8..854e92d51 100644 --- a/src/app/shared/services/addons/addons.service.spec.ts +++ b/src/app/shared/services/addons/addons.service.spec.ts @@ -126,7 +126,13 @@ describe('Service: Addons', () => { expect(request.request.method).toBe('PATCH'); expect(request.request.body).toEqual( Object({ - serializeOauthToken: true, + data: Object({ + attributes: Object({ + serialize_oauth_token: 'true', + }), + id: 'account-id', + type: 'authorized-storage-accounts', + }), }) ); request.flush(getAddonsAuthorizedStorageData(0)); diff --git a/src/app/shared/services/addons/addons.service.ts b/src/app/shared/services/addons/addons.service.ts index 3c5634766..bb05de2d8 100644 --- a/src/app/shared/services/addons/addons.service.ts +++ b/src/app/shared/services/addons/addons.service.ts @@ -122,7 +122,13 @@ export class AddonsService { .patch( `${environment.addonsApiUrl}/authorized-storage-accounts/${accountId}`, { - serializeOauthToken: true, + data: { + id: accountId, + type: 'authorized-storage-accounts', + attributes: { + serialize_oauth_token: 'true', + }, + }, } ) .pipe( diff --git a/src/app/shared/stores/addons/addons.selectors.ts b/src/app/shared/stores/addons/addons.selectors.ts index b200e0b2d..24b78702f 100644 --- a/src/app/shared/stores/addons/addons.selectors.ts +++ b/src/app/shared/stores/addons/addons.selectors.ts @@ -8,7 +8,7 @@ import { ConfiguredStorageAddonModel, OperationInvocation, ResourceReferenceJsonApi, - StorageItem, + StorageItemModel, UserReferenceJsonApi, } from '@shared/models'; @@ -195,7 +195,7 @@ export class AddonsSelectors { } @Selector([AddonsState]) - static getSelectedFolder(state: AddonsStateModel): StorageItem | null { + static getSelectedFolder(state: AddonsStateModel): StorageItemModel | null { return state.selectedFolderOperationInvocation.data?.operationResult[0] || null; }