diff --git a/src/app/core/components/folder-picker/folder-picker.component.spec.ts b/src/app/core/components/folder-picker/folder-picker.component.spec.ts index 93316a970..8b0a18a0d 100644 --- a/src/app/core/components/folder-picker/folder-picker.component.spec.ts +++ b/src/app/core/components/folder-picker/folder-picker.component.spec.ts @@ -4,15 +4,45 @@ import { cloneDeep, some } from 'lodash'; import { DataService } from '@shared/services/data/data.service'; import { ApiService } from '@shared/services/api/api.service'; +import { MessageService } from '@shared/services/message/message.service'; import { FolderResponse } from '@shared/services/api/index.repo'; import { SharedModule } from '@shared/shared.module'; import { FolderVO } from '@root/app/models'; import { HttpTestingController } from '@angular/common/http/testing'; import { FolderPickerService } from '@core/services/folder-picker/folder-picker.service'; import { DataStatus } from '@models/data-status.enum'; -import { of } from 'rxjs'; import { FolderPickerComponent } from './folder-picker.component'; +const buildRootFolderResponse = () => + new FolderResponse({ + isSuccessful: true, + Results: [ + { + data: [ + { + FolderVO: { + folderId: 1, + folder_linkId: 10, + type: 'type.folder.root.root', + displayName: 'Root', + ChildItemVOs: [ + { folder_linkId: 11, type: 'type.folder.root.private' }, + { folder_linkId: 12, type: 'type.folder.root.app' }, + { folder_linkId: 13, type: 'type.folder.root.vault' }, + ], + }, + }, + ], + }, + ], + }); + +const buildErrorFolderResponse = (errorMessage: string) => { + const errorResponse = new FolderResponse(); + errorResponse.Results = [{ message: [errorMessage] }]; + return errorResponse; +}; + describe('FolderPickerComponent', () => { let component: FolderPickerComponent; let fixture: ComponentFixture; @@ -49,13 +79,16 @@ describe('FolderPickerComponent', () => { const navigateMinExpected = require('@root/test/responses/folder.navigateMin.myFiles.success.json'); const myFiles = new FolderResponse(navigateMinExpected).getFolderVO(); - spyOn(api.folder, 'navigate').and.returnValue( - of(new FolderResponse(navigateMinExpected)), - ); + // myFiles is type.folder.root.private (not root.root), so setFolder + // takes the getWithChildren branch. + const getWithChildrenSpy = spyOn( + api.folder, + 'getWithChildren', + ).and.resolveTo(new FolderResponse(navigateMinExpected)); await component.setFolder(myFiles); - expect(api.folder.navigate).toHaveBeenCalledTimes(1); + expect(getWithChildrenSpy).toHaveBeenCalledTimes(1); expect(component.currentFolder).toBeTruthy(); expect(component.currentFolder.folder_linkId).toEqual( myFiles.folder_linkId, @@ -64,9 +97,7 @@ describe('FolderPickerComponent', () => { expect(some(component.currentFolder.ChildItemVOs, 'isRecord')).toBeFalsy(); const getLeanItemsExpected = require('@root/test/responses/folder.getLeanItems.folderPicker.myFiles.success.json'); - spyOn(api.folder, 'getWithChildren').and.returnValue( - Promise.resolve(new FolderResponse(getLeanItemsExpected)), - ); + getWithChildrenSpy.and.resolveTo(new FolderResponse(getLeanItemsExpected)); await component.loadCurrentFolderChildData(); @@ -80,4 +111,50 @@ describe('FolderPickerComponent', () => { ), ).toBeFalsy(); }); + + it('should load the root folder via getRoot and strip app and vault folders', async () => { + const api = TestBed.inject(ApiService) as ApiService; + const getRootSpy = spyOn(api.folder, 'getRoot').and.resolveTo( + buildRootFolderResponse(), + ); + const getWithChildrenSpy = spyOn(api.folder, 'getWithChildren'); + const rootFolder = buildRootFolderResponse().getFolderVO(); + + await component.setFolder(rootFolder); + + expect(getRootSpy).toHaveBeenCalledTimes(1); + expect(getWithChildrenSpy).not.toHaveBeenCalled(); + expect(component.isRootFolder).toBeTrue(); + expect( + some(component.currentFolder.ChildItemVOs, (item) => + item.type.includes('type.folder.root.app'), + ), + ).toBeFalse(); + + expect( + some(component.currentFolder.ChildItemVOs, (item) => + item.type.includes('type.folder.root.vault'), + ), + ).toBeFalse(); + }); + + it('should show an error and leave the current folder unchanged when the response is unsuccessful', async () => { + const api = TestBed.inject(ApiService) as ApiService; + const message = TestBed.inject(MessageService) as MessageService; + spyOn(api.folder, 'getWithChildren').and.resolveTo( + buildErrorFolderResponse('Folder not found'), + ); + const showErrorSpy = spyOn(message, 'showError'); + + await component.setFolder( + new FolderVO({ folderId: 42, type: 'type.folder.private' }), + ); + + expect(showErrorSpy).toHaveBeenCalledWith({ + message: 'Folder not found', + translate: true, + }); + + expect(component.currentFolder).toBeFalsy(); + }); }); diff --git a/src/app/core/components/folder-picker/folder-picker.component.ts b/src/app/core/components/folder-picker/folder-picker.component.ts index 3451f3c45..ce4d0bb20 100644 --- a/src/app/core/components/folder-picker/folder-picker.component.ts +++ b/src/app/core/components/folder-picker/folder-picker.component.ts @@ -7,6 +7,7 @@ import { FolderResponse } from '@shared/services/api/index.repo'; import { FolderPickerService } from '@core/services/folder-picker/folder-picker.service'; import { MessageService } from '@shared/services/message/message.service'; import { PromptService } from '@shared/services/prompt/prompt.service'; +import { AccountService } from '@shared/services/account/account.service'; export enum FolderPickerOperations { Move = 1, @@ -46,6 +47,7 @@ export class FolderPickerComponent implements OnDestroy { private message: MessageService, private folderPickerService: FolderPickerService, private prompt: PromptService, + private accountService: AccountService, ) { this.folderPickerService.registerComponent(this); } @@ -116,15 +118,33 @@ export class FolderPickerComponent implements OnDestroy { async setFolder(folder: FolderVO) { this.waiting = true; try { - const folderResponse = await this.api.folder - .navigate( - new FolderVO({ - folder_linkId: folder.folder_linkId, - folderId: folder.folderId, - archiveNbr: folder.archiveNbr, - }), - ) - .toPromise(); + const rootFolder = this.accountService.getRootFolder(); + // goToParentFolder reconstructs the root folder with only its ids + // (no type), so detect root by type or by identity against the cache. + const isRoot = + folder.type?.includes('type.folder.root.root') || + (rootFolder && + (folder.folderId === rootFolder.folderId || + folder.folder_linkId === rootFolder.folder_linkId)); + + // The root.root folder is a virtual folder that Stela's + // getWithChildren does not serve, so keep loading it through getRoot. + const folderResponse: FolderResponse = isRoot + ? await this.api.folder.getRoot() + : await this.api.folder.getWithChildren([ + new FolderVO({ + folder_linkId: folder.folder_linkId, + folderId: folder.folderId, + archiveNbr: folder.archiveNbr, + }), + ]); + + // getWithChildren resolves an error-shaped response instead of + // rejecting, so surface failures explicitly for the catch below. + if (!folderResponse.isSuccessful) { + throw folderResponse; + } + this.currentFolder = folderResponse.getFolderVO(true); this.isRootFolder = this.currentFolder.type.includes( 'type.folder.root.root', diff --git a/src/app/shared/services/api/folder.repo.ts b/src/app/shared/services/api/folder.repo.ts index b09df3e83..78685d336 100644 --- a/src/app/shared/services/api/folder.repo.ts +++ b/src/app/shared/services/api/folder.repo.ts @@ -1,6 +1,6 @@ import { FolderVO, FolderVOData, ItemVO } from '@root/app/models'; import { BaseResponse, BaseRepo } from '@shared/services/api/base'; -import { firstValueFrom, Observable } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { DataStatus } from '@models/data-status.enum'; import { ShareLink } from '@root/app/share-links/models/share-link'; import { @@ -388,25 +388,6 @@ export class FolderRepo extends BaseRepo { } } - public navigate(folderVO: FolderVO): Observable { - const response = { - ...folderVO, - }; - if (folderVO.type === 'type.folder.root.private') { - response.displayName = 'Private'; - } - - const data = [ - { - FolderVO: new FolderVO(response), - }, - ]; - - return this.http.sendRequest('/folder/navigateMin', data, { - ResponseClass: FolderResponse, - }); - } - public async post(folderVOs: FolderVO[]): Promise { const data = folderVOs.map((folderVO) => ({ FolderVO: new FolderVO(folderVO),