Skip to content

Commit d6a83b6

Browse files
committed
Deleting a keyword will also remove it from the current record
When removing a keyword from the archive settings, the currently open file would not be updated and stale keywords would appear. The current record subscribes to tags changes and then compares the record keywords with the ones from the archive and makes sure it only shows the ones that are still present in the archive. Issue: PER-10447
1 parent 537347d commit d6a83b6

File tree

4 files changed

+401
-72
lines changed

4 files changed

+401
-72
lines changed

src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
22
import { ComponentFixture, TestBed } from '@angular/core/testing';
3-
import { of } from 'rxjs';
3+
import { of, Subject } from 'rxjs';
44
import { By } from '@angular/platform-browser';
55
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
66
import { FormsModule } from '@angular/forms';
7+
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
78

89
import { ItemVO, TagVOData, RecordVO } from '@models';
910
import { ApiService } from '@shared/services/api/api.service';
@@ -291,4 +292,264 @@ describe('EditTagsComponent', () => {
291292

292293
expect(dialogCdkServiceSpy.open).toHaveBeenCalled();
293294
});
295+
296+
describe('checkItemTags allTags filtering', () => {
297+
it('should exclude item tags that do not exist in allTags', () => {
298+
const item = new RecordVO({
299+
TagVOs: [
300+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
301+
{ tagId: 99, name: 'deletedTag', type: 'type.generic.placeholder' },
302+
],
303+
});
304+
setupComponent(item, 'keyword');
305+
306+
expect(component.itemTags.find((t) => t.name === 'tagOne')).toBeTruthy();
307+
308+
expect(
309+
component.itemTags.find((t) => t.name === 'deletedTag'),
310+
).toBeFalsy();
311+
});
312+
313+
it('should include item tags that exist in allTags', () => {
314+
const item = new RecordVO({
315+
TagVOs: [
316+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
317+
{ tagId: 2, name: 'tagTwo', type: 'type.generic.placeholder' },
318+
],
319+
});
320+
setupComponent(item, 'keyword');
321+
322+
expect(component.itemTags.length).toBe(2);
323+
expect(component.itemTags.find((t) => t.name === 'tagOne')).toBeTruthy();
324+
325+
expect(component.itemTags.find((t) => t.name === 'tagTwo')).toBeTruthy();
326+
});
327+
328+
it('should return no item tags when allTags is empty', () => {
329+
TestBed.resetTestingModule();
330+
TestBed.configureTestingModule({
331+
declarations: [EditTagsComponent],
332+
imports: [NoopAnimationsModule, FormsModule],
333+
providers: [
334+
{
335+
provide: SearchService,
336+
useValue: { getTagResults: () => [] },
337+
},
338+
{
339+
provide: TagsService,
340+
useValue: {
341+
getTags: () => [],
342+
getTags$: () => of([]),
343+
setItemTags: () => {},
344+
getItemTags$: () => of([]),
345+
},
346+
},
347+
{
348+
provide: MessageService,
349+
useValue: { showError: () => {} },
350+
},
351+
{
352+
provide: ApiService,
353+
useValue: {
354+
tag: {
355+
deleteTagLink: async () =>
356+
await Promise.resolve(new TagResponse()),
357+
create: async () => await Promise.resolve(new TagResponse()),
358+
},
359+
},
360+
},
361+
{
362+
provide: DataService,
363+
useValue: {
364+
currentFolderChange: of(null),
365+
fetchFullItems: async () => await Promise.resolve([]),
366+
},
367+
},
368+
{
369+
provide: DialogCdkService,
370+
useValue: dialogCdkServiceSpy,
371+
},
372+
],
373+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
374+
}).compileComponents();
375+
376+
fixture = TestBed.createComponent(EditTagsComponent);
377+
component = fixture.componentInstance;
378+
379+
const item = new RecordVO({
380+
TagVOs: [
381+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
382+
],
383+
});
384+
component.item = item;
385+
component.tagType = 'keyword';
386+
fixture.detectChanges();
387+
388+
expect(component.itemTags.length).toBe(0);
389+
});
390+
391+
it('should exclude deleted custom metadata tags not in allTags', () => {
392+
const item = new RecordVO({
393+
TagVOs: [
394+
{
395+
tagId: 3,
396+
name: 'customField:customValueOne',
397+
type: 'type.tag.metadata.customField',
398+
},
399+
{
400+
tagId: 99,
401+
name: 'deletedField:deletedValue',
402+
type: 'type.tag.metadata.deletedField',
403+
},
404+
],
405+
});
406+
setupComponent(item, 'customMetadata');
407+
408+
expect(
409+
component.itemTags.find((t) => t.name === 'customField:customValueOne'),
410+
).toBeTruthy();
411+
412+
expect(
413+
component.itemTags.find((t) => t.name === 'deletedField:deletedValue'),
414+
).toBeFalsy();
415+
});
416+
417+
it('should only add filtered item tags to itemTagsById', () => {
418+
const item = new RecordVO({
419+
TagVOs: [
420+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
421+
{ tagId: 99, name: 'deletedTag', type: 'type.generic.placeholder' },
422+
],
423+
});
424+
setupComponent(item, 'keyword');
425+
426+
expect(component.itemTagsById.has(1)).toBeTrue();
427+
expect(component.itemTagsById.has(99)).toBeFalse();
428+
});
429+
});
430+
431+
describe('dialog keyword filtering against allTags', () => {
432+
let tagsSubject: Subject<TagVOData[]>;
433+
let itemTagsSubject: Subject<TagVOData[]>;
434+
435+
function setupDialogComponent() {
436+
tagsSubject = new Subject<TagVOData[]>();
437+
itemTagsSubject = new Subject<TagVOData[]>();
438+
439+
TestBed.resetTestingModule();
440+
TestBed.configureTestingModule({
441+
declarations: [EditTagsComponent],
442+
imports: [NoopAnimationsModule, FormsModule],
443+
providers: [
444+
{
445+
provide: SearchService,
446+
useValue: { getTagResults: () => defaultTagList },
447+
},
448+
{
449+
provide: TagsService,
450+
useValue: {
451+
getTags: () => defaultTagList,
452+
getTags$: () => tagsSubject.asObservable(),
453+
setItemTags: () => {},
454+
getItemTags$: () => itemTagsSubject.asObservable(),
455+
},
456+
},
457+
{
458+
provide: MessageService,
459+
useValue: { showError: () => {} },
460+
},
461+
{
462+
provide: ApiService,
463+
useValue: {
464+
tag: {
465+
deleteTagLink: async () =>
466+
await Promise.resolve(new TagResponse()),
467+
create: async () => await Promise.resolve(new TagResponse()),
468+
},
469+
},
470+
},
471+
{
472+
provide: DataService,
473+
useValue: {
474+
currentFolderChange: of(null),
475+
fetchFullItems: async () => await Promise.resolve([]),
476+
},
477+
},
478+
{
479+
provide: DialogCdkService,
480+
useValue: dialogCdkServiceSpy,
481+
},
482+
{
483+
provide: DIALOG_DATA,
484+
useValue: { item: defaultItem, type: 'keyword' },
485+
},
486+
{
487+
provide: DialogRef,
488+
useValue: { close: () => {} },
489+
},
490+
],
491+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
492+
}).compileComponents();
493+
494+
fixture = TestBed.createComponent(EditTagsComponent);
495+
component = fixture.componentInstance;
496+
fixture.detectChanges();
497+
}
498+
499+
it('should only include keywords that exist in allTags in dialogTags', () => {
500+
setupDialogComponent();
501+
502+
itemTagsSubject.next([
503+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
504+
{ tagId: 99, name: 'orphanTag', type: 'type.generic.placeholder' },
505+
]);
506+
507+
expect(component.dialogTags.length).toBe(1);
508+
expect(component.dialogTags[0].name).toBe('tagOne');
509+
});
510+
511+
it('should exclude metadata tags from keyword dialogTags', () => {
512+
setupDialogComponent();
513+
514+
itemTagsSubject.next([
515+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
516+
{
517+
tagId: 3,
518+
name: 'customField:customValueOne',
519+
type: 'type.tag.metadata.customField',
520+
},
521+
]);
522+
523+
expect(component.dialogTags.length).toBe(1);
524+
expect(component.dialogTags[0].name).toBe('tagOne');
525+
});
526+
527+
it('should show no keyword dialogTags when allTags is empty', () => {
528+
setupDialogComponent();
529+
530+
component.allTags = [];
531+
532+
itemTagsSubject.next([
533+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
534+
]);
535+
536+
expect(component.dialogTags.length).toBe(0);
537+
});
538+
539+
it('should update dialogTags when allTags changes via getTags$', () => {
540+
setupDialogComponent();
541+
542+
tagsSubject.next([
543+
{ tagId: 10, name: 'newTag', type: 'type.generic.placeholder' },
544+
]);
545+
546+
itemTagsSubject.next([
547+
{ tagId: 10, name: 'newTag', type: 'type.generic.placeholder' },
548+
{ tagId: 1, name: 'tagOne', type: 'type.generic.placeholder' },
549+
]);
550+
551+
expect(component.dialogTags.length).toBe(1);
552+
expect(component.dialogTags[0].name).toBe('newTag');
553+
});
554+
});
294555
});

src/app/file-browser/components/edit-tags/edit-tags.component.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ export class EditTagsComponent
108108
.subscribe((tags) => {
109109
if (this.tagType === 'keyword') {
110110
this.dialogTags = tags?.filter(
111-
(tag) => !tag.type.includes('type.tag.metadata'),
111+
(tag) =>
112+
!tag.type.includes('type.tag.metadata') &&
113+
this.allTags.find(
114+
(generalTag) => generalTag?.name === tag?.name,
115+
),
112116
);
113117
} else {
114118
this.dialogTags = tags?.filter((tag) =>
@@ -233,7 +237,8 @@ export class EditTagsComponent
233237
this.itemTags = this.filterTagsByType(
234238
(this.item?.TagVOs || []).filter(
235239
// Filter out tags that are now null from deletion
236-
(tag) => tag?.name,
240+
(tag) =>
241+
!!this.allTags?.find((genericTag) => genericTag?.name === tag?.name),
237242
),
238243
);
239244

@@ -244,7 +249,7 @@ export class EditTagsComponent
244249
for (const tag of this.itemTags) {
245250
this.itemTagsById.add(tag.tagId);
246251
}
247-
this.tagsService.setItemTags(this.item.TagVOs);
252+
this.tagsService.setItemTags(this.item?.TagVOs);
248253
}
249254

250255
onManageTagsClick() {

0 commit comments

Comments
 (0)