diff --git a/src/app/core/services/tags/tags.service.spec.ts b/src/app/core/services/tags/tags.service.spec.ts index 589b81dd0..6fa036b23 100644 --- a/src/app/core/services/tags/tags.service.spec.ts +++ b/src/app/core/services/tags/tags.service.spec.ts @@ -96,7 +96,7 @@ describe('TagsService', () => { it('should only cache tags from the current archive', () => { const item = new RecordVO({ - TagVOs: [ + tags: [ { tagId: 1, name: 'testOne', archiveId: 1 }, { tagId: 2, name: 'testTwo', archiveId: 2 }, ], diff --git a/src/app/core/services/tags/tags.service.ts b/src/app/core/services/tags/tags.service.ts index be6241f1f..ce7d28f1f 100644 --- a/src/app/core/services/tags/tags.service.ts +++ b/src/app/core/services/tags/tags.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { TagVOData } from '@models/tag-vo'; import debug from 'debug'; -import { ItemVO } from '@models'; +import { FolderVO, ItemVO, RecordVO } from '@models'; import { AccountService } from '@shared/services/account/account.service'; import { orderBy, find } from 'lodash'; import { Subject } from 'rxjs'; @@ -53,21 +53,41 @@ export class TagsService { } checkTagsOnItem(item: ItemVO) { - if (!item.TagVOs?.length) { + if ( + (item.isRecord && !item.tags?.length) || + (item.isFolder && !item.TagVOs?.length) + ) { return; } let hasNew = false; - for (const itemTag of item.TagVOs) { - if ( - !this.tags.has(itemTag.tagId) && - itemTag.name && - itemTag.archiveId === this.account.getArchive().archiveId - ) { - this.tags.set(itemTag.tagId, itemTag); - hasNew = true; - this.debug('new tag seen %o', itemTag); + if (item.isFolder) { + for (const itemTag of item.TagVOs) { + if ( + !this.tags.has(itemTag.tagId) && + itemTag.name && + itemTag.archiveId === this.account.getArchive().archiveId + ) { + this.tags.set(itemTag.tagId, itemTag); + hasNew = true; + this.debug('new tag seen %o', itemTag); + } + } + } + + if (item.isRecord) { + for (const itemTag of (item as RecordVO).tags) { + if ( + !this.tags.has(itemTag.id) && + itemTag.name && + itemTag.archiveId === this.account.getArchive().archiveId + ) { + const tagId = itemTag.tagId ?? itemTag.id; + this.tags.set(tagId, { ...itemTag, tagId }); + hasNew = true; + this.debug('new tag seen %o', itemTag); + } } } diff --git a/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts b/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts index bcf4758c7..6baa63a9f 100644 --- a/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts +++ b/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts @@ -3,7 +3,7 @@ import { async } from '@angular/core/testing'; import { Shallow } from 'shallow-render'; import { Observable, of } from 'rxjs'; -import { ItemVO, TagVOData, RecordVO } from '@models'; +import { ItemVO, TagVOData, RecordVO, FolderVO } from '@models'; import { ApiService } from '@shared/services/api/api.service'; import { DataService } from '@shared/services/data/data.service'; import { TagsService } from '@core/services/tags/tags.service'; @@ -38,12 +38,35 @@ const defaultTagList: TagVOData[] = [ type: 'type.tag.metadata.customField', }, ]; -const defaultItem: ItemVO = new RecordVO({ TagVOs: defaultTagList }); +const defaultRecord = new RecordVO({ + tags: defaultTagList, + isRecord: true, +}); + +const defaultFolder = new FolderVO({ + TagVOs: defaultTagList, + isFolder: true, +}); describe('EditTagsComponent', () => { let shallow: Shallow; - async function defaultRender( - item: ItemVO = defaultItem, + async function defaultRenderRecord( + item: ItemVO = defaultRecord, + tagType: TagType = 'keyword', + ) { + return await shallow.render( + ``, + { + bind: { + item: item, + tagType: tagType, + }, + }, + ); + } + + async function defaultRenderFolder( + item: ItemVO = defaultFolder, tagType: TagType = 'keyword', ) { return await shallow.render( @@ -81,14 +104,32 @@ describe('EditTagsComponent', () => { ); })); - it('should create', async () => { - const { element } = await defaultRender(); + it('should create record tags', async () => { + const { element } = await defaultRenderRecord(); expect(element).not.toBeNull(); }); - it('should only show keywords in keyword mode', async () => { - const { element } = await defaultRender(); + it('should create folder tags', async () => { + const { element } = await defaultRenderFolder(); + + expect(element).not.toBeNull(); + }); + + it('should only show keywords in keyword mode for records', async () => { + const { element, fixture } = await defaultRenderRecord(); + + element.componentInstance.itemTags = [ + { name: 'tagOne' }, + { name: 'tagTwo' }, + ]; + + element.componentInstance.matchingTags = [ + { name: 'tagOne' }, + { name: 'tagTwo' }, + ]; + + fixture.detectChanges(); expect( element.componentInstance.itemTags.find((tag) => tag.name === 'tagOne'), @@ -135,56 +176,8 @@ describe('EditTagsComponent', () => { ).not.toBeTruthy(); }); - it('should only show custom metadata in custom metadata mode', async () => { - const { element } = await defaultRender(defaultItem, 'customMetadata'); - - expect( - element.componentInstance.itemTags.find((tag) => tag.name === 'tagOne'), - ).not.toBeTruthy(); - - expect( - element.componentInstance.itemTags.find((tag) => tag.name === 'tagTwo'), - ).not.toBeTruthy(); - - expect( - element.componentInstance.itemTags.find( - (tag) => tag.name === 'customField:customValueOne', - ), - ).toBeTruthy(); - - expect( - element.componentInstance.itemTags.find( - (tag) => tag.name === 'customField:customValueTwo', - ), - ).toBeTruthy(); - - expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'tagOne', - ), - ).not.toBeTruthy(); - - expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'tagTwo', - ), - ).not.toBeTruthy(); - - expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'customField:customValueOne', - ), - ).toBeTruthy(); - - expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'customField:customValueTwo', - ), - ).toBeTruthy(); - }); - - it('should not create custom metadata in keyword mode', async () => { - const { element } = await defaultRender(); + it('should not create custom metadata in keyword mode for records', async () => { + const { element } = await defaultRenderRecord(); const tagCreateSpy = spyOn(element.componentInstance.api.tag, 'create'); await element.componentInstance.onInputEnter('key:value'); @@ -192,8 +185,11 @@ describe('EditTagsComponent', () => { expect(tagCreateSpy).not.toHaveBeenCalled(); }); - it('should not create keyword in custom metadata mode', async () => { - const { element } = await defaultRender(defaultItem, 'customMetadata'); + it('should not create keyword in custom metadata mode for records', async () => { + const { element } = await defaultRenderRecord( + defaultRecord, + 'customMetadata', + ); const tagCreateSpy = spyOn(element.componentInstance.api.tag, 'create'); await element.componentInstance.onInputEnter('keyword'); @@ -202,7 +198,25 @@ describe('EditTagsComponent', () => { }); it('should highlight the correct tag on key down', async () => { - const { fixture, element } = await defaultRender(); + const { fixture, element } = await defaultRenderFolder(); + + element.componentInstance.isEditing = true; + + fixture.detectChanges(); + const tags = fixture.debugElement.queryAll(By.css('.edit-tag')); + + const arrowKeyDown = new KeyboardEvent('keydown', { key: 'ArrowDown' }); + tags[0].nativeElement.dispatchEvent(arrowKeyDown); + + fixture.detectChanges(); + + const focusedElement = document.activeElement as HTMLElement; + + expect(focusedElement).toBe(tags[1].nativeElement); + }); + + it('should highlight the correct tag for folders on key down', async () => { + const { fixture, element } = await defaultRenderFolder(); element.componentInstance.isEditing = true; @@ -220,7 +234,7 @@ describe('EditTagsComponent', () => { }); it('should highlight the correct tag on key up', async () => { - const { fixture, element } = await defaultRender(); + const { fixture, element } = await defaultRenderRecord(); element.componentInstance.isEditing = true; @@ -238,7 +252,7 @@ describe('EditTagsComponent', () => { }); it('should highlight the input on key up', async () => { - const { fixture, element } = await defaultRender(); + const { fixture, element } = await defaultRenderRecord(); element.componentInstance.isEditing = true; @@ -258,7 +272,7 @@ describe('EditTagsComponent', () => { }); it('should open dialog when manage link is clicked', async () => { - const { element, find, inject, fixture } = await defaultRender(); + const { element, find, inject, fixture } = await defaultRenderRecord(); const dialogOpenSpy = inject(DialogCdkService); element.componentInstance.isEditing = true; diff --git a/src/app/file-browser/components/edit-tags/edit-tags.component.ts b/src/app/file-browser/components/edit-tags/edit-tags.component.ts index a546f73b4..cb7c4868a 100644 --- a/src/app/file-browser/components/edit-tags/edit-tags.component.ts +++ b/src/app/file-browser/components/edit-tags/edit-tags.component.ts @@ -12,7 +12,7 @@ import { Inject, } from '@angular/core'; import { TagsService } from '@core/services/tags/tags.service'; -import { ItemVO, TagVOData, TagLinkVOData, FolderVO } from '@models'; +import { ItemVO, TagVOData, TagLinkVOData, FolderVO, RecordVO } from '@models'; import { DataService } from '@shared/services/data/data.service'; import { Subject, Subscription } from 'rxjs'; import { @@ -81,9 +81,9 @@ export class EditTagsComponent private tagsService: TagsService, private message: MessageService, private api: ApiService, - private dataService: DataService, private elementRef: ElementRef, private dialog: DialogCdkService, + private dataService: DataService, ) { this.subscriptions.push( this.tagsService.getTags$().subscribe((tags) => { @@ -113,9 +113,15 @@ export class EditTagsComponent (tag) => !tag.type.includes('type.tag.metadata'), ); } else { - this.dialogTags = tags?.filter((tag) => - tag.type.includes('type.tag.metadata'), - ); + this.dialogTags = tags + ?.filter((tag) => tag.type.includes('type.tag.metadata')) + .map((tag) => ({ + id: tag.id, + name: `${tag.name}:${ + tag.type.split('.')[tag.type.split.length - 1] + }`, + type: tag.type, + })); } }); } @@ -162,7 +168,7 @@ export class EditTagsComponent this.onTagType(this.newTagName); } - async onTagClick(tag: TagVOData) { + async onTagClick(tag) { const tagLink: TagLinkVOData = {}; if (this.item instanceof FolderVO) { tagLink.refTable = 'folder'; @@ -174,10 +180,14 @@ export class EditTagsComponent this.waiting = true; try { - if (tag.tagId && this.itemTagsById.has(tag.tagId)) { + if ( + (tag.tagId && this.itemTagsById.has(tag.tagId)) || + (tag.id && this.itemTagsById.has(tag.id)) + ) { await this.api.tag.deleteTagLink(tag, tagLink); } else { await this.api.tag.create(tag, tagLink); + await this.tagsService.refreshTags(); } await this.dataService.fetchFullItems([this.item]); } catch (err) { @@ -232,23 +242,39 @@ export class EditTagsComponent this.itemTagsById.clear(); - this.itemTags = this.filterTagsByType( - (this.item?.TagVOs || []) - .map((tag) => this.allTags?.find((t) => t.tagId === tag.tagId)) - .filter( - // Filter out tags that are now null from deletion - (tag) => tag?.name, - ), - ); + if (this.item && this.item?.isFolder) { + this.itemTags = this.filterTagsByType( + (this.item?.TagVOs || []) + .map((tag) => this.allTags?.find((t) => t.tagId === tag.tagId)) + .filter( + // Filter out tags that are now null from deletion + (tag) => tag?.name, + ), + ); + } - if (!this.item?.TagVOs?.length) { - return; + if (this.item && this.item?.isRecord) { + this.itemTags = this.filterTagsByType( + (this.item?.tags || []) + .map((tag) => this.allTags?.find((t) => t.tagId === +tag.id)) + .filter( + // Filter out tags that are now null from deletion + (tag) => tag?.name, + ), + ); + } + + if (Array.isArray(this.itemTags)) { + for (const tag of this.itemTags) { + this.itemTagsById.add(tag.tagId); + } } - for (const tag of this.itemTags) { - this.itemTagsById.add(tag.tagId); + if (this.item) { + this.tagsService.setItemTags( + this.item.isFolder ? this.item.TagVOs : (this.item as RecordVO).tags, + ); } - this.tagsService.setItemTags(this.item.TagVOs); } onManageTagsClick() { diff --git a/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts b/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts index b8ef40ffd..ddc7e7771 100644 --- a/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts +++ b/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts @@ -38,13 +38,13 @@ const defaultTagList: TagVOData[] = [ ]; const defaultItem: ItemVO = new RecordVO({ displayName: 'Default Item', - TagVOs: defaultTagList, + tags: defaultTagList, type: 'document', folder_linkId: 0, }); const secondItem: ItemVO = new RecordVO({ displayName: 'Second Item', - TagVOs: [], + tags: [], type: 'image', folder_linkId: 1, }); @@ -157,7 +157,12 @@ describe('FileViewerComponent', () => { }); it('should correctly distinguish between keywords and custom metadata', async () => { - const { element } = await defaultRender(); + const { element, fixture } = await defaultRender(); + + // Emit tags + tagsService.itemTagsObservable.next(defaultTagList); + await fixture.whenStable(); + fixture.detectChanges(); expect( element.componentInstance.keywords.find((tag) => tag.name === 'tagOne'), diff --git a/src/app/file-browser/components/file-viewer/file-viewer.component.ts b/src/app/file-browser/components/file-viewer/file-viewer.component.ts index c8c0f89a5..a0dcf6dee 100644 --- a/src/app/file-browser/components/file-viewer/file-viewer.component.ts +++ b/src/app/file-browser/components/file-viewer/file-viewer.component.ts @@ -219,7 +219,7 @@ export class FileViewerComponent implements OnInit, OnDestroy { return false; } - const original = this.currentRecord.FileVOs.find( + const original = this.currentRecord.FileVOs?.find( (file) => file.format === FileFormat.Original, ); const access = GetAccessFile(this.currentRecord); @@ -407,11 +407,16 @@ export class FileViewerComponent implements OnInit, OnDestroy { } private setCurrentTags(): void { - this.keywords = this.currentRecord.TagVOs.filter( + this.keywords = this.currentRecord.tags?.filter( (tag) => !tag.type.includes('type.tag.metadata'), ); - this.customMetadata = this.currentRecord.TagVOs.filter((tag) => - tag.type.includes('type.tag.metadata'), - ); + + this.customMetadata = this.currentRecord.tags + ?.filter((tag) => tag.type.includes('type.tag.metadata')) + .map((tag) => ({ + id: tag.id, + name: `${tag.name}:${tag.type.split('.')[tag.type.split.length - 1]}`, + type: tag.type, + })); } } diff --git a/src/app/models/folder-vo.ts b/src/app/models/folder-vo.ts index 309ec717e..8f64f54ff 100644 --- a/src/app/models/folder-vo.ts +++ b/src/app/models/folder-vo.ts @@ -111,10 +111,10 @@ export class FolderVO public ShareArchiveVO: ArchiveVO; public AccessVO; public AccessVOs; - // For the UI public posStart; public posLimit; + public tags; constructor( voData: FolderVOData, diff --git a/src/app/models/record-vo.ts b/src/app/models/record-vo.ts index 4adc9c5af..f321aeb50 100644 --- a/src/app/models/record-vo.ts +++ b/src/app/models/record-vo.ts @@ -122,6 +122,7 @@ export class RecordVO public RecordExifVO; public ShareVOs: ShareVO[]; public AccessVO; + public tags; constructor(voData: RecordVOData, options?: Partial) { super(voData); @@ -236,4 +237,6 @@ export interface RecordVOData extends BaseVOData { ShareVOs?: any; AccessVO?: any; isFolder?: boolean; + tags?: any; + isRecord?: boolean; } diff --git a/src/app/models/tag-vo.ts b/src/app/models/tag-vo.ts index 2ae3e7a33..4bd88ea19 100644 --- a/src/app/models/tag-vo.ts +++ b/src/app/models/tag-vo.ts @@ -7,6 +7,7 @@ export interface TagVOData extends BaseVOData { status?: string; type?: string; archiveId?: number; + id?: number; } export interface TagLinkVOData extends BaseVOData { diff --git a/src/app/shared/components/tags/tags.component.ts b/src/app/shared/components/tags/tags.component.ts index 5c1a64406..70e983f86 100644 --- a/src/app/shared/components/tags/tags.component.ts +++ b/src/app/shared/components/tags/tags.component.ts @@ -20,8 +20,6 @@ export class TagsComponent implements OnChanges { orderedTags: TagVOData[] = []; - constructor() {} - ngOnChanges() { if (!this.tags?.length) { this.orderedTags = [];