diff --git a/src/app/models/access-role.spec.ts b/src/app/models/access-role.spec.ts new file mode 100644 index 000000000..107a7311c --- /dev/null +++ b/src/app/models/access-role.spec.ts @@ -0,0 +1,69 @@ +import { + ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE, + getAccessRoleFromArchiveMembershipRole, + getAccessAsEnum, + type ArchiveMembershipRoleType, +} from './access-role'; + +describe('getAccessRoleFromArchiveMembershipRole', () => { + const expectedTranslations: Array<[ArchiveMembershipRoleType, string]> = [ + ['contributor', 'access.role.contributor'], + ['curator', 'access.role.curator'], + ['editor', 'access.role.editor'], + ['manager', 'access.role.manager'], + ['owner', 'access.role.owner'], + ['viewer', 'access.role.viewer'], + ]; + + expectedTranslations.forEach( + ([archiveMembershipRole, expectedAccessRole]) => { + it(`should translate ${archiveMembershipRole}`, () => { + expect( + getAccessRoleFromArchiveMembershipRole(archiveMembershipRole), + ).toBe(expectedAccessRole); + }); + }, + ); + + it('should cover every role Stela can send', () => { + expect(Object.keys(ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE).length).toBe( + expectedTranslations.length, + ); + }); + + it('should produce roles that getAccessAsEnum understands', () => { + expectedTranslations.forEach(([archiveMembershipRole]) => { + const accessRole = getAccessRoleFromArchiveMembershipRole( + archiveMembershipRole, + ); + + expect(getAccessAsEnum(accessRole)).toBeDefined(); + }); + }); + + it('should translate manager to manager, not the curator that PERMISSIONS_LEVEL_TO_ACCESS_ROLE maps it to', () => { + expect(getAccessRoleFromArchiveMembershipRole('manager')).toBe( + 'access.role.manager', + ); + }); + + it('should return undefined rather than guess at an unknown role', () => { + expect( + getAccessRoleFromArchiveMembershipRole( + 'wizard' as ArchiveMembershipRoleType, + ), + ).toBeUndefined(); + }); + + it('should return undefined when no role is given', () => { + expect(getAccessRoleFromArchiveMembershipRole(undefined)).toBeUndefined(); + }); + + it('should return undefined for an inherited object property name', () => { + expect( + getAccessRoleFromArchiveMembershipRole( + 'toString' as ArchiveMembershipRoleType, + ), + ).toBeUndefined(); + }); +}); diff --git a/src/app/models/access-role.ts b/src/app/models/access-role.ts index 4f06f63b0..04c00a45f 100644 --- a/src/app/models/access-role.ts +++ b/src/app/models/access-role.ts @@ -22,6 +22,46 @@ export type PermissionsLevel = | 'owner' | 'viewer'; +/** Mirrors Stela's ArchiveMembershipRole enum */ +export type ArchiveMembershipRoleType = + | 'contributor' + | 'curator' + | 'editor' + | 'manager' + | 'owner' + | 'viewer'; + +export const ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE: Record< + ArchiveMembershipRoleType, + AccessRoleType +> = { + contributor: 'access.role.contributor', + curator: 'access.role.curator', + editor: 'access.role.editor', + manager: 'access.role.manager', + owner: 'access.role.owner', + viewer: 'access.role.viewer', +}; + +function isArchiveMembershipRole( + value: unknown, +): value is ArchiveMembershipRoleType { + return ( + typeof value === 'string' && + Object.hasOwn(ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE, value) + ); +} + +export function getAccessRoleFromArchiveMembershipRole( + archiveMembershipRole: ArchiveMembershipRoleType | undefined, +): AccessRoleType | undefined { + if (!isArchiveMembershipRole(archiveMembershipRole)) { + return undefined; + } + + return ARCHIVE_MEMBERSHIP_ROLE_TO_ACCESS_ROLE[archiveMembershipRole]; +} + // Mapping for share link permissions. Note the stela share link API // mistakenly returns "manager" where it should use "curator" -- see // https://github.com/PermanentOrg/stela/issues/540 diff --git a/src/app/shared/services/api/folder.repo.spec.ts b/src/app/shared/services/api/folder.repo.spec.ts index 436163dc4..872140044 100644 --- a/src/app/shared/services/api/folder.repo.spec.ts +++ b/src/app/shared/services/api/folder.repo.spec.ts @@ -30,6 +30,7 @@ const mockStelaFolder = { downloadName: 'test-folder', imageRatio: 1.5, paths: { names: ['path1', 'path2'] }, + accessRole: 'owner', publicAt: null, sort: 'name', thumbnailUrls: { @@ -395,4 +396,65 @@ describe('Folder repo', () => { }); }); }); + + describe('access role translation', () => { + const convertFolder = async (overrides: Record) => { + httpV2Spy.get.and.returnValue( + of([{ items: [{ ...mockStelaFolder, ...overrides }] }]), + ); + const result = await folderRepo.getStelaFolderVOs([ + new FolderVO({ folderId: 123 }), + ]); + return result.getFolderVOs()[0]; + }; + + it("should translate Stela's role into ours", async () => { + const folder = await convertFolder({ accessRole: 'owner' }); + + expect(folder.accessRole).toBe('access.role.owner'); + }); + + it('should translate manager to manager, not curator', async () => { + const folder = await convertFolder({ accessRole: 'manager' }); + + expect(folder.accessRole).toBe('access.role.manager'); + }); + + it('should leave the role undefined when Stela sends nothing', async () => { + const folder = await convertFolder({ accessRole: undefined }); + + expect(folder.accessRole).toBeUndefined(); + }); + + it('should merge onto an existing folder without breaking its role', async () => { + const existingFolder = new FolderVO({ + folderId: '123', + accessRole: 'access.role.owner', + }); + + existingFolder.update(await convertFolder({ accessRole: 'owner' })); + + expect(existingFolder.accessRole).toBe('access.role.owner'); + }); + + it('should translate the role on child folders too', async () => { + httpV2Spy.get.and.returnValues( + of([{ items: [mockStelaFolder] }]), + of([ + { + items: [ + { ...mockStelaFolder, folderId: '999', accessRole: 'viewer' }, + ], + }, + ]), + ); + + const result = await folderRepo.getWithChildren([ + new FolderVO({ folderId: 123 }), + ]); + const child = result.getFolderVO(true).ChildItemVOs[0]; + + expect(child.accessRole).toBe('access.role.viewer'); + }); + }); }); diff --git a/src/app/shared/services/api/folder.repo.ts b/src/app/shared/services/api/folder.repo.ts index 188d18012..fee99b0a5 100644 --- a/src/app/shared/services/api/folder.repo.ts +++ b/src/app/shared/services/api/folder.repo.ts @@ -2,6 +2,10 @@ import { FolderVO, FolderVOData, ItemVO } from '@root/app/models'; import { BaseResponse, BaseRepo } from '@shared/services/api/base'; import { firstValueFrom, Observable } from 'rxjs'; import { DataStatus } from '@models/data-status.enum'; +import { + getAccessRoleFromArchiveMembershipRole, + type ArchiveMembershipRoleType, +} from '@models/access-role'; import { ShareLink } from '@root/app/share-links/models/share-link'; import { convertStelaLocationToLocnVOData, @@ -67,6 +71,7 @@ interface StelaFolder { paths: { names: string[]; }; + accessRole: ArchiveMembershipRoleType; publicAt: string; sort: string; thumbnailUrls?: { @@ -117,6 +122,7 @@ const convertStelaFolderToFolderVO = (stelaFolder: StelaFolder): FolderVO => { view: stelaFolder.view, imageRatio: stelaFolder.imageRatio, type: stelaFolder.type, + accessRole: getAccessRoleFromArchiveMembershipRole(stelaFolder.accessRole), thumbStatus: stelaFolder.status, thumbURL200: stelaFolder.thumbnailUrls?.['200'], thumbURL500: stelaFolder.thumbnailUrls?.['500'], diff --git a/src/app/shared/services/api/record.repo.spec.ts b/src/app/shared/services/api/record.repo.spec.ts index c0403ec8e..4cacd2974 100644 --- a/src/app/shared/services/api/record.repo.spec.ts +++ b/src/app/shared/services/api/record.repo.spec.ts @@ -466,5 +466,47 @@ describe('RecordRepo', () => { expect(record.thumbURL2000).toBe('https://example.com/2000'); expect(record.thumbnail256).toBe('https://example.com/256'); }); + + it("should translate Stela's role into ours", () => { + const record = convertStelaRecordToRecordVO({ + ...baseStelaRecord, + accessRole: 'viewer', + } as any); + + expect(record.accessRole).toBe('access.role.viewer'); + }); + + it('should translate manager to manager, not curator', () => { + const record = convertStelaRecordToRecordVO({ + ...baseStelaRecord, + accessRole: 'manager', + } as any); + + expect(record.accessRole).toBe('access.role.manager'); + }); + + it('should leave the role undefined when Stela sends nothing', () => { + const record = convertStelaRecordToRecordVO({ + ...baseStelaRecord, + } as any); + + expect(record.accessRole).toBeUndefined(); + }); + + it('should merge onto an existing record without breaking its role', () => { + const existingRecord = new RecordVO({ + recordId: 42, + accessRole: 'access.role.curator', + }); + + existingRecord.update( + convertStelaRecordToRecordVO({ + ...baseStelaRecord, + accessRole: 'owner', + } as any), + ); + + expect(existingRecord.accessRole).toBe('access.role.owner'); + }); }); }); diff --git a/src/app/shared/services/api/record.repo.ts b/src/app/shared/services/api/record.repo.ts index b49354431..581eac12e 100644 --- a/src/app/shared/services/api/record.repo.ts +++ b/src/app/shared/services/api/record.repo.ts @@ -18,7 +18,11 @@ import { ThumbnailCache } from '@shared/utilities/thumbnail-cache/thumbnail-cach import { firstValueFrom } from 'rxjs'; import { FileFormat, PermanentFile } from '@models/file-vo'; import { ShareStatus } from '@models/share-vo'; -import { AccessRoleType } from '@models/access-role'; +import { + AccessRoleType, + getAccessRoleFromArchiveMembershipRole, + type ArchiveMembershipRoleType, +} from '@models/access-role'; import { ShareLink } from '@root/app/share-links/models/share-link'; import { getFirst } from '../http-v2/http-v2.service'; import { CENTRAL_TIMEZONE_VO } from './folder.repo'; @@ -106,8 +110,9 @@ export interface StelaShare { thumbURL200: string; }; } -export type StelaRecord = Omit & { +export type StelaRecord = Omit & { tags: Array | null; + accessRole: ArchiveMembershipRoleType; archiveNumber: string; displayDate: string; displayTime?: string; @@ -214,6 +219,7 @@ export const convertStelaRecordToRecordVO = ( convertStelaTagToTagVO(stelaTag, stelaRecord.archiveId), ), archiveNbr: stelaRecord.archiveNumber, + accessRole: getAccessRoleFromArchiveMembershipRole(stelaRecord.accessRole), displayDT: stelaRecord.displayDate, displayTime: stelaRecord.displayTime, folder_linkId: Number.parseInt(stelaRecord.folderLinkId, 10),