diff --git a/src/app/core/resolves/folder-resolve.service.spec.ts b/src/app/core/resolves/folder-resolve.service.spec.ts index abd82f723..db16447e7 100644 --- a/src/app/core/resolves/folder-resolve.service.spec.ts +++ b/src/app/core/resolves/folder-resolve.service.spec.ts @@ -1,20 +1,301 @@ import { TestBed } from '@angular/core/testing'; import * as Testing from '@root/test/testbedConfig'; import { cloneDeep } from 'lodash'; +import { Router } from '@angular/router'; import { FolderResolveService } from '@core/resolves/folder-resolve.service'; +import { AccountService } from '@shared/services/account/account.service'; +import { FilesystemService } from '@root/app/filesystem/filesystem.service'; +import { FolderResponse } from '@shared/services/api/folder.repo'; +import { FolderVO } from '@models/index'; +import { + MessageDisplayOptions, + MessageService, +} from '@shared/services/message/message.service'; + +const buildFolder = (folderData: Record) => + new FolderVO({ + ChildItemVOs: [], + type: 'type.folder.private', + view: 'folder.view.grid', + ...folderData, + }); describe('FolderResolveService', () => { let service: FolderResolveService; + let accountService: AccountService; + let filesystem: FilesystemService; + let message: MessageService; + let router: Router; beforeEach(() => { const config = cloneDeep(Testing.BASE_TEST_CONFIG); config.providers.push(FolderResolveService); TestBed.configureTestingModule(config); + service = TestBed.inject(FolderResolveService); + accountService = TestBed.inject(AccountService); + filesystem = TestBed.inject(FilesystemService); + message = TestBed.inject(MessageService); + router = TestBed.inject(Router); + + spyOn(accountService, 'getRootFolder').and.returnValue( + new FolderVO({ + ChildItemVOs: [ + new FolderVO({ + folderId: '11', + type: 'type.folder.root.private', + archiveNbr: '0001-0001', + }), + new FolderVO({ + folderId: '22', + type: 'type.folder.root.app', + archiveNbr: '0001-0002', + }), + new FolderVO({ + folderId: '33', + type: 'type.folder.root.public', + archiveNbr: '0001-0003', + }), + ], + }), + ); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should load My Files by default', async () => { + const getFolderSpy = spyOn(filesystem, 'getFolder').and.resolveTo( + buildFolder({ + displayName: 'My Files', + type: 'type.folder.root.private', + }), + ); + + const result = await service.resolve( + { params: {}, data: {} } as any, + { url: '/private' } as any, + ); + + const requestedFolder = getFolderSpy.calls.mostRecent().args[0] as FolderVO; + + expect(getFolderSpy).toHaveBeenCalled(); + expect(requestedFolder.folderId).toBe('11'); + expect(result.displayName).toBe('My Files'); + }); + + it('should load the apps folder on /apps', async () => { + const getFolderSpy = spyOn(filesystem, 'getFolder').and.resolveTo( + buildFolder({ displayName: 'Apps', type: 'type.folder.root.app' }), + ); + + await service.resolve( + { params: {}, data: {} } as any, + { url: '/apps' } as any, + ); + + expect((getFolderSpy.calls.mostRecent().args[0] as FolderVO).folderId).toBe( + '22', + ); + }); + + it('should load the public root on /public', async () => { + const getFolderSpy = spyOn(filesystem, 'getFolder').and.resolveTo( + buildFolder({ displayName: 'Public', type: 'type.folder.root.public' }), + ); + + await service.resolve( + { params: {}, data: {} } as any, + { url: '/public' } as any, + ); + + expect((getFolderSpy.calls.mostRecent().args[0] as FolderVO).folderId).toBe( + '33', + ); + }); + + it('should pass the route identifiers through for a deep link', async () => { + const getFolderSpy = spyOn(filesystem, 'getFolder').and.resolveTo( + buildFolder({ displayName: 'Deep Linked' }), + ); + + const result = await service.resolve( + { + params: { archiveNbr: '0001-0005', folderLinkId: '99' }, + data: {}, + } as any, + { url: '/private/0001-0005/99' } as any, + ); + + const requestedFolder = getFolderSpy.calls.mostRecent().args[0] as FolderVO; + + expect(requestedFolder.archiveNbr).toBe('0001-0005'); + expect(requestedFolder.folder_linkId).toBe('99' as any); + expect(requestedFolder.folderId).toBeUndefined(); + expect(result.displayName).toBe('Deep Linked'); + }); + + it('should splice share crumbs onto a shared record without loading a folder', async () => { + const getFolderSpy = spyOn(filesystem, 'getFolder'); + const sharedRecord = { displayName: 'A shared photo' }; + + const result = await service.resolve( + { + params: {}, + data: {}, + parent: { + data: { + sharePreviewVO: { FolderVO: null, RecordVO: sharedRecord }, + currentFolder: new FolderVO({ + pathAsText: ['My Files'], + pathAsArchiveNbr: ['0001-0001'], + pathAsFolder_linkId: [11], + }), + }, + }, + } as any, + { url: '/share/abc123' } as any, + ); + + expect(getFolderSpy).not.toHaveBeenCalled(); + expect(result.pathAsText).toEqual(['Shares', 'Record', 'My Files']); + expect(result.pathAsArchiveNbr).toEqual([ + '0000-0000', + '0000-0000', + '0001-0001', + ]); + + expect(result.pathAsFolder_linkId).toEqual([0, 0, 11]); + expect(result.ChildItemVOs).toEqual([sharedRecord] as any); + }); + + it('should redirect a timeline folder to the public timeline route', async () => { + spyOn(filesystem, 'getFolder').and.resolveTo( + buildFolder({ displayName: 'Trip', view: 'folder.view.timeline' }), + ); + const navigateSpy = spyOn(router, 'navigate'); + + await service.resolve( + { + params: { + archiveNbr: '0001-0005', + folderLinkId: '99', + publicArchiveNbr: '0002-0000', + }, + data: {}, + } as any, + { url: '/p/archive/0002-0000/0001-0005/99' } as any, + ); + + expect(navigateSpy).toHaveBeenCalledWith([ + 'p', + 'archive', + '0002-0000', + 'view', + 'timeline', + '0001-0005', + '99', + ]); + }); + + it('should not redirect when the route already declares a folder view', async () => { + spyOn(filesystem, 'getFolder').and.resolveTo( + buildFolder({ displayName: 'Trip', view: 'folder.view.timeline' }), + ); + const navigateSpy = spyOn(router, 'navigate'); + + const result = await service.resolve( + { + params: { + archiveNbr: '0001-0005', + folderLinkId: '99', + publicArchiveNbr: '0002-0000', + }, + data: { folderView: 'folder.view.timeline' }, + } as any, + { url: '/p/archive/0002-0000/view/timeline/0001-0005/99' } as any, + ); + + expect(navigateSpy).not.toHaveBeenCalled(); + expect(result.displayName).toBe('Trip'); + }); + + it('should surface the server message when the load fails', async () => { + spyOn(filesystem, 'getFolder').and.rejectWith( + new FolderResponse({ + isSuccessful: false, + Results: [{ message: ['Test Error'] }], + }), + ); + spyOn(accountService, 'logOut').and.resolveTo(null); + spyOn(router, 'navigate'); + let displayedErrorMessage: string; + spyOn(message, 'showError').and.callFake((data: MessageDisplayOptions) => { + displayedErrorMessage = data.message; + }); + + await expectAsync( + service.resolve( + { params: {}, data: {} } as any, + { url: '/private' } as any, + ), + ).toBeRejected(); + + expect(displayedErrorMessage).toBe('Test Error'); + }); + + it('should fall back to a generic message for a raw error', async () => { + spyOn(filesystem, 'getFolder').and.rejectWith(new Error('Network down')); + spyOn(accountService, 'logOut').and.resolveTo(null); + spyOn(router, 'navigate'); + let displayedErrorMessage: string; + spyOn(message, 'showError').and.callFake((data: MessageDisplayOptions) => { + displayedErrorMessage = data.message; + }); + + await expectAsync( + service.resolve( + { params: {}, data: {} } as any, + { url: '/private' } as any, + ), + ).toBeRejected(); + + expect(displayedErrorMessage).toBe('error.generic.internal'); + }); + + it('should log out when a root folder fails to load', async () => { + spyOn(filesystem, 'getFolder').and.rejectWith(new Error('Network down')); + const logOutSpy = spyOn(accountService, 'logOut').and.resolveTo(null); + spyOn(router, 'navigate'); + spyOn(message, 'showError'); + + await expectAsync( + service.resolve( + { params: {}, data: {} } as any, + { url: '/private' } as any, + ), + ).toBeRejected(); + + expect(logOutSpy).toHaveBeenCalled(); + }); + + it('should redirect rather than throw when a deep link fails', async () => { + spyOn(filesystem, 'getFolder').and.rejectWith(new Error('Network down')); + const navigateSpy = spyOn(router, 'navigate'); + spyOn(message, 'showError'); + + await expectAsync( + service.resolve( + { + params: { archiveNbr: '0001-0005', folderLinkId: '99' }, + data: {}, + } as any, + { url: '/private/0001-0005/99' } as any, + ), + ).toBeRejectedWith(false); + + expect(navigateSpy).toHaveBeenCalledWith(['/private']); + }); }); diff --git a/src/app/core/resolves/folder-resolve.service.ts b/src/app/core/resolves/folder-resolve.service.ts index ab54c126b..cfd314a52 100644 --- a/src/app/core/resolves/folder-resolve.service.ts +++ b/src/app/core/resolves/folder-resolve.service.ts @@ -9,11 +9,10 @@ import { find, cloneDeep } from 'lodash'; import { AccountService } from '@shared/services/account/account.service'; import { MessageService } from '@shared/services/message/message.service'; -import { FolderResponse } from '@shared/services/api/index.repo'; - import { FolderVO } from '@root/app/models'; import { FolderView } from '@shared/services/folder-view/folder-view.enum'; import { findRouteData } from '@shared/utilities/router'; +import { getFolderErrorMessage } from '@shared/utilities/folder-error-message'; import { FilesystemService } from '@root/app/filesystem/filesystem.service'; @Injectable() @@ -92,12 +91,12 @@ export class FolderResolveService { } return folder; }) - .catch(async (response: FolderResponse) => { + .catch(async (error: unknown) => { this.message.showError({ - message: response.getMessage(), + message: getFolderErrorMessage(error), translate: true, }); - if (targetFolder.type.includes('root')) { + if (targetFolder.type?.includes('root')) { this.accountService .logOut() .then(() => { diff --git a/src/app/core/resolves/lean-folder-resolve.service.spec.ts b/src/app/core/resolves/lean-folder-resolve.service.spec.ts deleted file mode 100644 index 726a795a9..000000000 --- a/src/app/core/resolves/lean-folder-resolve.service.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import * as Testing from '@root/test/testbedConfig'; -import { cloneDeep } from 'lodash'; -import { Router } from '@angular/router'; - -import { LeanFolderResolveService } from '@core/resolves/lean-folder-resolve.service'; -import { ApiService } from '@shared/services/api/api.service'; -import { AccountService } from '@shared/services/account/account.service'; -import { FolderResponse } from '@shared/services/api/folder.repo'; -import { FolderVO } from '@models/index'; -import { - MessageDisplayOptions, - MessageService, -} from '@shared/services/message/message.service'; - -const buildFolderResponse = (folderData: Record) => - new FolderResponse({ - isSuccessful: true, - Results: [{ data: [{ FolderVO: { ChildItemVOs: [], ...folderData } }] }], - }); - -describe('LeanFolderResolveService', () => { - let service: LeanFolderResolveService; - let api: ApiService; - let accountService: AccountService; - let message: MessageService; - let router: Router; - - beforeEach(() => { - const config = cloneDeep(Testing.BASE_TEST_CONFIG); - config.providers.push(LeanFolderResolveService); - TestBed.configureTestingModule(config); - - service = TestBed.inject(LeanFolderResolveService); - api = TestBed.inject(ApiService); - accountService = TestBed.inject(AccountService); - message = TestBed.inject(MessageService); - router = TestBed.inject(Router); - - spyOn(accountService, 'getRootFolder').and.returnValue( - new FolderVO({ - ChildItemVOs: [ - new FolderVO({ - folderId: '11', - type: 'type.folder.root.private', - archiveNbr: '0001-0001', - }), - new FolderVO({ - folderId: '22', - type: 'type.folder.root.app', - archiveNbr: '0001-0002', - }), - ], - }), - ); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should load My Files by default', async () => { - const getSpy = spyOn( - api.folder, - 'getWithChildrenByIdentifier', - ).and.resolveTo(buildFolderResponse({ displayName: 'My Files' })); - - const result = await service.resolve( - { params: {} } as any, - { url: '/private' } as any, - ); - - expect(getSpy).toHaveBeenCalled(); - expect(getSpy.calls.mostRecent().args[0].folderId).toBe('11'); - expect(result.displayName).toBe('My Files'); - }); - - it('should load the apps folder on /apps', async () => { - const getSpy = spyOn( - api.folder, - 'getWithChildrenByIdentifier', - ).and.resolveTo(buildFolderResponse({ displayName: 'Apps' })); - - await service.resolve({ params: {} } as any, { url: '/apps' } as any); - - expect(getSpy.calls.mostRecent().args[0].folderId).toBe('22'); - }); - - it('should pass the route identifiers through for a deep link', async () => { - const getSpy = spyOn( - api.folder, - 'getWithChildrenByIdentifier', - ).and.resolveTo(buildFolderResponse({ displayName: 'Deep Linked' })); - - const result = await service.resolve( - { params: { archiveNbr: '0001-0005', folderLinkId: '99' } } as any, - { url: '/view/timeline/0001-0005/99' } as any, - ); - - const requestedFolder = getSpy.calls.mostRecent().args[0]; - - expect(requestedFolder.archiveNbr).toBe('0001-0005'); - expect(requestedFolder.folder_linkId).toBe('99' as any); - expect(requestedFolder.folderId).toBeUndefined(); - expect(result.displayName).toBe('Deep Linked'); - }); - - it('should splice share crumbs onto a shared record without calling the API', async () => { - const getSpy = spyOn(api.folder, 'getWithChildrenByIdentifier'); - const sharedRecord = { displayName: 'A shared photo' }; - - const result = await service.resolve( - { - params: {}, - parent: { - data: { - sharePreviewVO: { FolderVO: null, RecordVO: sharedRecord }, - currentFolder: new FolderVO({ - pathAsText: ['My Files'], - pathAsArchiveNbr: ['0001-0001'], - pathAsFolder_linkId: [11], - }), - }, - }, - } as any, - { url: '/share/abc123/view/timeline' } as any, - ); - - expect(getSpy).not.toHaveBeenCalled(); - expect(result.pathAsText).toEqual(['Shares', 'Record', 'My Files']); - expect(result.pathAsArchiveNbr).toEqual([ - '0000-0000', - '0000-0000', - '0001-0001', - ]); - - expect(result.pathAsFolder_linkId).toEqual([0, 0, 11]); - expect(result.ChildItemVOs).toEqual([sharedRecord] as any); - }); - - it('should surface the server message when the load fails', async () => { - spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith( - new FolderResponse({ - isSuccessful: false, - Results: [{ message: ['Test Error'] }], - }), - ); - spyOn(accountService, 'logOut').and.resolveTo(null); - spyOn(router, 'navigate'); - let displayedErrorMessage: string; - spyOn(message, 'showError').and.callFake((data: MessageDisplayOptions) => { - displayedErrorMessage = data.message; - }); - - await expectAsync( - service.resolve({ params: {} } as any, { url: '/private' } as any), - ).toBeRejected(); - - expect(displayedErrorMessage).toBe('Test Error'); - }); - - it('should log out when a root folder fails to load', async () => { - spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith( - new Error('Network down'), - ); - const logOutSpy = spyOn(accountService, 'logOut').and.resolveTo(null); - spyOn(router, 'navigate'); - spyOn(message, 'showError'); - - await expectAsync( - service.resolve({ params: {} } as any, { url: '/private' } as any), - ).toBeRejected(); - - expect(logOutSpy).toHaveBeenCalled(); - }); - - it('should fall back to a generic message for a raw error', async () => { - spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith( - new Error('Network down'), - ); - spyOn(accountService, 'logOut').and.resolveTo(null); - spyOn(router, 'navigate'); - let displayedErrorMessage: string; - spyOn(message, 'showError').and.callFake((data: MessageDisplayOptions) => { - displayedErrorMessage = data.message; - }); - - await expectAsync( - service.resolve({ params: {} } as any, { url: '/private' } as any), - ).toBeRejected(); - - expect(displayedErrorMessage).toBe('error.generic.internal'); - }); - - it('should redirect rather than throw when a deep link fails', async () => { - spyOn(api.folder, 'getWithChildrenByIdentifier').and.rejectWith( - new Error('Network down'), - ); - const navigateSpy = spyOn(router, 'navigate'); - spyOn(message, 'showError'); - - await expectAsync( - service.resolve( - { params: { archiveNbr: '0001-0005', folderLinkId: '99' } } as any, - { url: '/view/timeline/0001-0005/99' } as any, - ), - ).toBeRejectedWith(false); - - expect(navigateSpy).toHaveBeenCalledWith(['/private']); - }); -}); diff --git a/src/app/core/resolves/lean-folder-resolve.service.ts b/src/app/core/resolves/lean-folder-resolve.service.ts deleted file mode 100644 index 38c5e0f68..000000000 --- a/src/app/core/resolves/lean-folder-resolve.service.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - RouterStateSnapshot, - Router, -} from '@angular/router'; -import { find, cloneDeep } from 'lodash'; -import { ApiService } from '@shared/services/api/api.service'; -import { AccountService } from '@shared/services/account/account.service'; -import { MessageService } from '@shared/services/message/message.service'; - -import { getFolderErrorMessage } from '@shared/utilities/folder-error-message'; - -import { FolderVO } from '@root/app/models'; - -@Injectable() -export class LeanFolderResolveService { - constructor( - private api: ApiService, - private accountService: AccountService, - private message: MessageService, - private router: Router, - ) {} - - async resolve( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Promise { - let targetFolder; - - if (route.params.archiveNbr && route.params.folderLinkId) { - targetFolder = new FolderVO({ - archiveNbr: route.params.archiveNbr, - folder_linkId: route.params.folderLinkId, - }); - } else if (state.url === '/apps') { - const apps = find(this.accountService.getRootFolder().ChildItemVOs, { - type: 'type.folder.root.app', - }); - targetFolder = new FolderVO(apps); - } else if (state.url.includes('/share/')) { - const sharedFolder = route.parent.data.sharePreviewVO.FolderVO; - const sharedRecord = route.parent.data.sharePreviewVO.RecordVO; - if (sharedFolder) { - targetFolder = new FolderVO(sharedFolder); - } else { - const folder = new FolderVO(cloneDeep(route.parent.data.currentFolder)); - folder.pathAsArchiveNbr.unshift('0000-0000', '0000-0000'); - folder.pathAsText.unshift('Shares', 'Record'); - folder.pathAsFolder_linkId.unshift(0, 0); - folder.ChildItemVOs = [sharedRecord]; - return folder; - } - } else { - const myFiles = find(this.accountService.getRootFolder().ChildItemVOs, { - type: 'type.folder.root.private', - }); - targetFolder = new FolderVO(myFiles); - } - - try { - const folderResponse = - await this.api.folder.getWithChildrenByIdentifier(targetFolder); - - if (!folderResponse.isSuccessful) { - throw folderResponse; - } - - return folderResponse.getFolderVO(true); - } catch (error) { - this.message.showError({ - message: getFolderErrorMessage(error), - translate: true, - }); - if (targetFolder.type?.includes('root')) { - this.accountService - .logOut() - .then(() => { - this.router.navigate(['/login']); - }) - .catch(() => { - this.router.navigate(['/login']); - }); - } else if (state.url.includes('apps')) { - this.router.navigate(['/apps']); - } else { - this.router.navigate(['/private']); - } - return await Promise.reject(false); - } - } -} diff --git a/src/app/filesystem/filesystem-api.service.spec.ts b/src/app/filesystem/filesystem-api.service.spec.ts index 1716643bf..24181419d 100644 --- a/src/app/filesystem/filesystem-api.service.spec.ts +++ b/src/app/filesystem/filesystem-api.service.spec.ts @@ -3,7 +3,6 @@ import { FolderResponse } from '@shared/services/api/folder.repo'; import { FolderVO } from '@models/index'; import { DataStatus } from '@models/data-status.enum'; import { ApiService } from '@shared/services/api/api.service'; -import { of } from 'rxjs'; import { ShareLinksService } from '../share-links/services/share-links.service'; import { FilesystemApiService } from './filesystem-api.service'; @@ -46,9 +45,9 @@ describe('FilesystemApiService', () => { getWithChildren: jasmine .createSpy('getWithChildren') .and.returnValue(Promise.resolve(mockSuccessResponse)), - navigateLean: jasmine - .createSpy('navigateLean') - .and.returnValue(of(mockSuccessResponse)), + getWithChildrenByIdentifier: jasmine + .createSpy('getWithChildrenByIdentifier') + .and.returnValue(Promise.resolve(mockSuccessResponse)), }, }; @@ -72,20 +71,32 @@ describe('FilesystemApiService', () => { expect(service).toBeTruthy(); }); - it('should navigate using navigateLean', async () => { + it('should navigate using getWithChildrenByIdentifier', async () => { shareLinksServiceSpy.isUnlistedShare.and.resolveTo(false); const folder = await service.navigate({ folderId }); - expect(mockApiService.folder.navigateLean).toHaveBeenCalledWith( - jasmine.any(FolderVO), - ); + expect( + mockApiService.folder.getWithChildrenByIdentifier, + ).toHaveBeenCalledWith(jasmine.any(FolderVO)); expect(folder.folderId).toBe(folderId); expect(folder.displayName).toBe('Unlisted Folder'); expect(folder.dataStatus).toBe(DataStatus.Lean); }); + it('should navigate by archiveNbr and folder_linkId without a folder id', async () => { + shareLinksServiceSpy.isUnlistedShare.and.resolveTo(false); + + await service.navigate({ archiveNbr: '0001-0000' }); + + const [requestedFolder] = + mockApiService.folder.getWithChildrenByIdentifier.calls.mostRecent().args; + + expect(requestedFolder.archiveNbr).toBe('0001-0000'); + expect(requestedFolder.folderId).toBeUndefined(); + }); + it('should navigate using getWithChildren when in unlisted share', async () => { shareLinksServiceSpy.isUnlistedShare.and.resolveTo(true); shareLinksServiceSpy.currentShareToken = 'mock-token'; @@ -104,8 +115,8 @@ describe('FilesystemApiService', () => { it('should throw FolderResponse error if response is unsuccessful', async () => { shareLinksServiceSpy.isUnlistedShare.and.resolveTo(false); - mockApiService.folder.navigateLean.and.returnValue( - of(mockUnsuccessfulResponse), + mockApiService.folder.getWithChildrenByIdentifier.and.resolveTo( + mockUnsuccessfulResponse, ); try { @@ -115,4 +126,13 @@ describe('FilesystemApiService', () => { expect(error).toBeDefined(); } }); + + it('should surface a rejection from getWithChildrenByIdentifier', async () => { + shareLinksServiceSpy.isUnlistedShare.and.resolveTo(false); + mockApiService.folder.getWithChildrenByIdentifier.and.rejectWith( + new Error('500 Internal Server Error'), + ); + + await expectAsync(service.navigate({ folderId })).toBeRejected(); + }); }); diff --git a/src/app/filesystem/filesystem-api.service.ts b/src/app/filesystem/filesystem-api.service.ts index 2026554df..8e1ff5fed 100644 --- a/src/app/filesystem/filesystem-api.service.ts +++ b/src/app/filesystem/filesystem-api.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { firstValueFrom } from 'rxjs'; import { FolderVO, RecordVO } from '@models/index'; import { ApiService } from '@shared/services/api/api.service'; @@ -26,13 +25,16 @@ export class FilesystemApiService implements FilesystemApi { const isUnlistedShare = await this.shareLinksService.isUnlistedShare(); let response: FolderResponse = null; if (isUnlistedShare) { + // A share-token visitor has no auth token, so the folder id cannot be + // resolved through the v1 endpoint the way getWithChildrenByIdentifier + // does -- these routes always carry a real folder id already. response = await this.api.folder.getWithChildren( [new FolderVO(folder)], this.shareLinksService.currentShareToken, ); } else { - response = await firstValueFrom( - this.api.folder.navigateLean(new FolderVO(folder)), + response = await this.api.folder.getWithChildrenByIdentifier( + new FolderVO(folder), ); } if (!response.isSuccessful) { diff --git a/src/app/shared/services/api/folder.repo.spec.ts b/src/app/shared/services/api/folder.repo.spec.ts index 1fc8a8045..d7156e6b2 100644 --- a/src/app/shared/services/api/folder.repo.spec.ts +++ b/src/app/shared/services/api/folder.repo.spec.ts @@ -504,6 +504,63 @@ describe('Folder repo', () => { expect(child.thumbnail256).toBe('thumb256'); }); + // Stela's children endpoint ranks folders and records together by the + // parent folder's sort setting, so the response order is the render order. + it('should keep children in the order the response sent them', async () => { + httpV2Spy.get.and.returnValues( + of([{ items: [mockStelaFolder] }]), + of([ + { + items: [ + { recordId: '77', displayName: 'Apple', folderLinkId: '900' }, + { folderId: '78', displayName: 'Banana', folderLinkId: '901' }, + { recordId: '79', displayName: 'Cherry', folderLinkId: '902' }, + { folderId: '80', displayName: 'Date', folderLinkId: '903' }, + ], + }, + ]), + ); + + const result = await folderRepo.getWithChildren([ + new FolderVO({ folderId: 123 }), + ]); + const childNames = result + .getFolderVO(true) + .ChildItemVOs.map((child) => child.displayName); + + expect(childNames).toEqual(['Apple', 'Banana', 'Cherry', 'Date']); + }); + + it('should still expose children split by kind, each in response order', async () => { + httpV2Spy.get.and.returnValues( + of([{ items: [mockStelaFolder] }]), + of([ + { + items: [ + { recordId: '77', displayName: 'Apple', folderLinkId: '900' }, + { folderId: '78', displayName: 'Banana', folderLinkId: '901' }, + { recordId: '79', displayName: 'Cherry', folderLinkId: '902' }, + { folderId: '80', displayName: 'Date', folderLinkId: '903' }, + ], + }, + ]), + ); + + const folder = ( + await folderRepo.getWithChildren([new FolderVO({ folderId: 123 })]) + ).getFolderVO(); + + expect(folder.ChildFolderVOs.map((child) => child.displayName)).toEqual([ + 'Banana', + 'Date', + ]); + + expect(folder.RecordVOs.map((child) => child.displayName)).toEqual([ + 'Apple', + 'Cherry', + ]); + }); + it('should leave link ids undefined rather than NaN when absent', async () => { const folder = await convertFolder({ folderLinkId: undefined }); diff --git a/src/app/shared/services/api/folder.repo.ts b/src/app/shared/services/api/folder.repo.ts index 667eff963..6b7054230 100644 --- a/src/app/shared/services/api/folder.repo.ts +++ b/src/app/shared/services/api/folder.repo.ts @@ -1,4 +1,4 @@ -import { FolderVO, FolderVOData, ItemVO } from '@root/app/models'; +import { FolderVO, FolderVOData, ItemVO, RecordVO } from '@root/app/models'; import { BaseResponse, BaseRepo } from '@shared/services/api/base'; import { firstValueFrom, Observable } from 'rxjs'; import { DataStatus } from '@models/data-status.enum'; @@ -124,12 +124,21 @@ const toFolderLinkId = ( const convertStelaFolderToFolderVO = (stelaFolder: StelaFolder): FolderVO => { stelaFolder.children ??= []; - const childFolderVOs = stelaFolder.children - .filter((child): child is StelaFolder => !isStelaRecord(child)) - .map(convertStelaFolderToFolderVO); - const childRecordVOs = stelaFolder.children - .filter(isStelaRecord) - .map(convertStelaRecordToRecordVO); + // Stela's children endpoint already ranks folders and records together by the + // parent folder's sort setting, so the incoming order is the order to render. + // Splitting the children by kind and concatenating them would discard it. + const childFolderVOs: FolderVO[] = []; + const childRecordVOs: RecordVO[] = []; + const childItemVOs = stelaFolder.children.map((child) => { + if (isStelaRecord(child)) { + const childRecordVO = convertStelaRecordToRecordVO(child); + childRecordVOs.push(childRecordVO); + return childRecordVO; + } + const childFolderVO = convertStelaFolderToFolderVO(child); + childFolderVOs.push(childFolderVO); + return childFolderVO; + }); const { accessRole: stelaAccessRole, ...stelaFolderWithoutAccessRole } = stelaFolder; return new FolderVO({ @@ -185,7 +194,7 @@ const convertStelaFolderToFolderVO = (stelaFolder: StelaFolder): FolderVO => { TagVOs: (stelaFolder.tags ?? []).map((stelaTag) => convertStelaTagToTagVO(stelaTag, stelaFolder.archive?.id), ), - ChildItemVOs: [...childRecordVOs, ...childFolderVOs], + ChildItemVOs: childItemVOs, ShareVOs: (stelaFolder.shares ?? []).map(convertStelaSharetoShareVO), isFolder: true, }); diff --git a/src/app/views/views.routes.ts b/src/app/views/views.routes.ts index e935a98bc..aed86bafb 100644 --- a/src/app/views/views.routes.ts +++ b/src/app/views/views.routes.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { FileViewerComponent } from '@fileBrowser/components/file-viewer/file-viewer.component'; -import { LeanFolderResolveService } from '@core/resolves/lean-folder-resolve.service'; import { RecordResolveService } from '@core/resolves/record-resolve.service'; import { FileBrowserComponentsModule } from '@fileBrowser/file-browser-components.module'; import { fileListChildRoutes } from '@fileBrowser/file-browser.routes'; @@ -16,10 +15,6 @@ const folderResolve = { currentFolder: FolderResolveService, }; -const leanFolderResolve = { - currentFolder: LeanFolderResolveService, -}; - const recordResolve = { currentRecord: RecordResolveService, }; @@ -34,13 +29,13 @@ export const routes: RoutesWithData = [ { path: '', component: TimelineViewComponent, - resolve: leanFolderResolve, + resolve: folderResolve, children: fileListChildRoutes, }, { path: ':archiveNbr/:folderLinkId', component: TimelineViewComponent, - resolve: leanFolderResolve, + resolve: folderResolve, children: [ { path: 'record/:recArchiveNbr', @@ -79,11 +74,7 @@ export const routes: RoutesWithData = [ FileBrowserComponentsModule, ], exports: [], - providers: [ - LeanFolderResolveService, - RecordResolveService, - FolderResolveService, - ], + providers: [RecordResolveService, FolderResolveService], declarations: [], }) export class ViewsRoutingModule {}