Skip to content

Commit 454d869

Browse files
Merge branch 'main' of https://bitbucket.org/4Science/dspace-angular into DURACOM-151
2 parents b1aa2f3 + 5af9793 commit 454d869

60 files changed

Lines changed: 1232 additions & 288 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/browse-by/browse-by-guard.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { first } from 'rxjs/operators';
22
import { BrowseByGuard } from './browse-by-guard';
33
import { of as observableOf } from 'rxjs';
44
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
5-
import { BrowseDefinition } from '../core/shared/browse-definition.model';
65
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
6+
import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model';
77
import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock';
88
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
99

@@ -20,7 +20,7 @@ describe('BrowseByGuard', () => {
2020
const id = 'author';
2121
const scope = '1234-65487-12354-1235';
2222
const value = 'Filter';
23-
const browseDefinition = Object.assign(new BrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
23+
const browseDefinition = Object.assign(new ValueListBrowseDefinition(), { type: BrowseByDataType.Metadata, metadataKeys: ['dc.contributor'] });
2424

2525
beforeEach(() => {
2626
dsoService = {

src/app/browse-by/browse-by-switcher/browse-by-decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const map = new Map();
2626
* @param browseByType The type of page
2727
* @param theme The optional theme for the component
2828
*/
29-
export function rendersBrowseBy(browseByType: BrowseByDataType, theme = DEFAULT_THEME) {
29+
export function rendersBrowseBy(browseByType: string, theme = DEFAULT_THEME) {
3030
return function decorator(component: any) {
3131
if (hasNoValue(map.get(browseByType))) {
3232
map.set(browseByType, new Map());

src/app/browse-by/browse-by-switcher/browse-by-switcher.component.spec.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,45 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
33
import { NO_ERRORS_SCHEMA } from '@angular/core';
44
import { ActivatedRoute } from '@angular/router';
55
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
6-
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
76
import { BehaviorSubject } from 'rxjs';
87
import { ThemeService } from '../../shared/theme-support/theme.service';
8+
import { FlatBrowseDefinition } from '../../core/shared/flat-browse-definition.model';
9+
import { ValueListBrowseDefinition } from '../../core/shared/value-list-browse-definition.model';
10+
import { NonHierarchicalBrowseDefinition } from '../../core/shared/non-hierarchical-browse-definition';
911

1012
describe('BrowseBySwitcherComponent', () => {
1113
let comp: BrowseBySwitcherComponent;
1214
let fixture: ComponentFixture<BrowseBySwitcherComponent>;
1315

1416
const types = [
1517
Object.assign(
16-
new BrowseDefinition(), {
18+
new FlatBrowseDefinition(), {
1719
id: 'title',
1820
dataType: BrowseByDataType.Title,
1921
}
2022
),
2123
Object.assign(
22-
new BrowseDefinition(), {
24+
new FlatBrowseDefinition(), {
2325
id: 'dateissued',
2426
dataType: BrowseByDataType.Date,
2527
metadataKeys: ['dc.date.issued']
2628
}
2729
),
2830
Object.assign(
29-
new BrowseDefinition(), {
31+
new ValueListBrowseDefinition(), {
3032
id: 'author',
3133
dataType: BrowseByDataType.Metadata,
3234
}
3335
),
3436
Object.assign(
35-
new BrowseDefinition(), {
37+
new ValueListBrowseDefinition(), {
3638
id: 'subject',
3739
dataType: BrowseByDataType.Metadata,
3840
}
3941
),
4042
];
4143

42-
const data = new BehaviorSubject(createDataWithBrowseDefinition(new BrowseDefinition()));
44+
const data = new BehaviorSubject(createDataWithBrowseDefinition(new FlatBrowseDefinition()));
4345

4446
const activatedRouteStub = {
4547
data
@@ -70,7 +72,7 @@ describe('BrowseBySwitcherComponent', () => {
7072
comp = fixture.componentInstance;
7173
}));
7274

73-
types.forEach((type: BrowseDefinition) => {
75+
types.forEach((type: NonHierarchicalBrowseDefinition) => {
7476
describe(`when switching to a browse-by page for "${type.id}"`, () => {
7577
beforeEach(() => {
7678
data.next(createDataWithBrowseDefinition(type));

src/app/browse-by/browse-by-switcher/browse-by-switcher.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class BrowseBySwitcherComponent implements OnInit {
3131
*/
3232
ngOnInit(): void {
3333
this.browseByComponent = this.route.data.pipe(
34-
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType, this.themeService.getThemeName()))
34+
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName()))
3535
);
3636
}
3737

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="container">
2+
<div class="mb-3">
3+
<ds-vocabulary-treeview [vocabularyOptions]=vocabularyOptions
4+
[multiSelect]="true"
5+
(select)="onSelect($event)"
6+
(deselect)="onDeselect($event)">
7+
</ds-vocabulary-treeview>
8+
</div>
9+
<a class="btn btn-primary" [routerLink]="['/search']" [queryParams]="queryParams">{{ 'browse.taxonomy.button' | translate }}</a>
10+
</div>

src/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component.scss

Whitespace-only changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
4+
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
5+
import { TranslateModule } from '@ngx-translate/core';
6+
import { NO_ERRORS_SCHEMA } from '@angular/core';
7+
import { ActivatedRoute } from '@angular/router';
8+
import { BehaviorSubject } from 'rxjs';
9+
import { createDataWithBrowseDefinition } from '../browse-by-switcher/browse-by-switcher.component.spec';
10+
import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
11+
import { ThemeService } from '../../shared/theme-support/theme.service';
12+
13+
describe('BrowseByTaxonomyPageComponent', () => {
14+
let component: BrowseByTaxonomyPageComponent;
15+
let fixture: ComponentFixture<BrowseByTaxonomyPageComponent>;
16+
let themeService: ThemeService;
17+
let detail1: VocabularyEntryDetail;
18+
let detail2: VocabularyEntryDetail;
19+
20+
const data = new BehaviorSubject(createDataWithBrowseDefinition(new HierarchicalBrowseDefinition()));
21+
const activatedRouteStub = {
22+
data
23+
};
24+
25+
beforeEach(async () => {
26+
themeService = jasmine.createSpyObj('themeService', {
27+
getThemeName: 'dspace',
28+
});
29+
30+
await TestBed.configureTestingModule({
31+
imports: [ TranslateModule.forRoot() ],
32+
declarations: [ BrowseByTaxonomyPageComponent ],
33+
providers: [
34+
{ provide: ActivatedRoute, useValue: activatedRouteStub },
35+
{ provide: ThemeService, useValue: themeService },
36+
],
37+
schemas: [NO_ERRORS_SCHEMA]
38+
})
39+
.compileComponents();
40+
});
41+
42+
beforeEach(() => {
43+
fixture = TestBed.createComponent(BrowseByTaxonomyPageComponent);
44+
component = fixture.componentInstance;
45+
fixture.detectChanges();
46+
detail1 = new VocabularyEntryDetail();
47+
detail2 = new VocabularyEntryDetail();
48+
detail1.value = 'HUMANITIES and RELIGION';
49+
detail2.value = 'TECHNOLOGY';
50+
detail1.id = 'id-1';
51+
detail2.id = 'id-2';
52+
});
53+
54+
it('should create', () => {
55+
expect(component).toBeTruthy();
56+
});
57+
58+
it('should handle select event', () => {
59+
component.onSelect(detail1);
60+
expect(component.selectedItems.length).toBe(1);
61+
expect(component.selectedItems).toContain(detail1);
62+
expect(component.selectedItems.length).toBe(1);
63+
expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals'] );
64+
});
65+
66+
it('should handle select event with multiple selected items', () => {
67+
component.onSelect(detail1);
68+
component.onSelect(detail2);
69+
expect(component.selectedItems.length).toBe(2);
70+
expect(component.selectedItems).toContain(detail1, detail2);
71+
expect(component.selectedItems.length).toBe(2);
72+
expect(component.filterValues).toEqual(['HUMANITIES and RELIGION,equals', 'TECHNOLOGY,equals'] );
73+
});
74+
75+
it('should handle deselect event', () => {
76+
component.onSelect(detail1);
77+
component.onSelect(detail2);
78+
expect(component.selectedItems.length).toBe(2);
79+
expect(component.selectedItems.length).toBe(2);
80+
component.onDeselect(detail1);
81+
expect(component.selectedItems.length).toBe(1);
82+
expect(component.selectedItems).toContain(detail2);
83+
expect(component.selectedItems.length).toBe(1);
84+
expect(component.filterValues).toEqual(['TECHNOLOGY,equals'] );
85+
});
86+
87+
afterEach(() => {
88+
fixture.destroy();
89+
component = null;
90+
});
91+
});
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Component, OnInit, Inject, OnDestroy } from '@angular/core';
2+
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
3+
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
4+
import { ActivatedRoute } from '@angular/router';
5+
import { Observable, Subscription } from 'rxjs';
6+
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
7+
import { GenericConstructor } from '../../core/shared/generic-constructor';
8+
import { BROWSE_BY_COMPONENT_FACTORY } from '../browse-by-switcher/browse-by-decorator';
9+
import { map } from 'rxjs/operators';
10+
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
11+
import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model';
12+
13+
@Component({
14+
selector: 'ds-browse-by-taxonomy-page',
15+
templateUrl: './browse-by-taxonomy-page.component.html',
16+
styleUrls: ['./browse-by-taxonomy-page.component.scss']
17+
})
18+
/**
19+
* Component for browsing items by metadata in a hierarchical controlled vocabulary
20+
*/
21+
export class BrowseByTaxonomyPageComponent implements OnInit, OnDestroy {
22+
23+
/**
24+
* The {@link VocabularyOptions} object
25+
*/
26+
vocabularyOptions: VocabularyOptions;
27+
28+
/**
29+
* The selected vocabulary entries
30+
*/
31+
selectedItems: VocabularyEntryDetail[] = [];
32+
33+
/**
34+
* The query parameters, contain the selected entries
35+
*/
36+
filterValues: string[];
37+
38+
/**
39+
* The facet the use when filtering
40+
*/
41+
facetType: string;
42+
43+
/**
44+
* The used vocabulary
45+
*/
46+
vocabularyName: string;
47+
48+
/**
49+
* The parameters used in the URL
50+
*/
51+
queryParams: any;
52+
53+
/**
54+
* Resolved browse-by component
55+
*/
56+
browseByComponent: Observable<any>;
57+
58+
/**
59+
* Subscriptions to track
60+
*/
61+
browseByComponentSubs: Subscription[] = [];
62+
63+
public constructor( protected route: ActivatedRoute,
64+
protected themeService: ThemeService,
65+
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
66+
}
67+
68+
ngOnInit(): void {
69+
this.browseByComponent = this.route.data.pipe(
70+
map((data: { browseDefinition: BrowseDefinition }) => {
71+
this.getComponentByBrowseByType(data.browseDefinition.getRenderType(), this.themeService.getThemeName());
72+
return data.browseDefinition;
73+
})
74+
);
75+
this.browseByComponentSubs.push(this.browseByComponent.subscribe((browseDefinition: HierarchicalBrowseDefinition) => {
76+
this.facetType = browseDefinition.facetType;
77+
this.vocabularyName = browseDefinition.vocabulary;
78+
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
79+
}));
80+
}
81+
82+
/**
83+
* Adds detail to selectedItems, transforms it to be used as query parameter
84+
* and adds that to filterValues.
85+
*
86+
* @param detail VocabularyEntryDetail to be added
87+
*/
88+
onSelect(detail: VocabularyEntryDetail): void {
89+
this.selectedItems.push(detail);
90+
this.filterValues = this.selectedItems
91+
.map((item: VocabularyEntryDetail) => `${item.value},equals`);
92+
this.updateQueryParams();
93+
}
94+
95+
/**
96+
* Removes detail from selectedItems and filterValues.
97+
*
98+
* @param detail VocabularyEntryDetail to be removed
99+
*/
100+
onDeselect(detail: VocabularyEntryDetail): void {
101+
this.selectedItems = this.selectedItems.filter((entry: VocabularyEntryDetail) => { return entry.id !== detail.id; });
102+
this.filterValues = this.filterValues.filter((value: string) => { return value !== `${detail.value},equals`; });
103+
this.updateQueryParams();
104+
}
105+
106+
/**
107+
* Updates queryParams based on the current facetType and filterValues.
108+
*/
109+
private updateQueryParams(): void {
110+
this.queryParams = {
111+
['f.' + this.facetType]: this.filterValues
112+
};
113+
}
114+
115+
ngOnDestroy(): void {
116+
this.browseByComponentSubs.forEach((sub: Subscription) => sub.unsubscribe());
117+
}
118+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Component } from '@angular/core';
2+
import { ThemedComponent } from '../../shared/theme-support/themed.component';
3+
import { rendersBrowseBy } from '../browse-by-switcher/browse-by-decorator';
4+
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page.component';
5+
6+
@Component({
7+
selector: 'ds-themed-browse-by-taxonomy-page',
8+
templateUrl: '../../shared/theme-support/themed.component.html',
9+
styleUrls: []
10+
})
11+
/**
12+
* Themed wrapper for BrowseByTaxonomyPageComponent
13+
*/
14+
@rendersBrowseBy('hierarchy')
15+
export class ThemedBrowseByTaxonomyPageComponent extends ThemedComponent<BrowseByTaxonomyPageComponent>{
16+
17+
protected getComponentName(): string {
18+
return 'BrowseByTaxonomyPageComponent';
19+
}
20+
21+
protected importThemedComponent(themeName: string): Promise<any> {
22+
return import(`../../../themes/${themeName}/app/browse-by/browse-by-taxonomy-page/browse-by-taxonomy-page.component`);
23+
}
24+
25+
protected importUnthemedComponent(): Promise<any> {
26+
return import(`./browse-by-taxonomy-page.component`);
27+
}
28+
}

src/app/browse-by/browse-by.module.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,37 @@ import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-tit
44
import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component';
55
import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component';
66
import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component';
7+
import { BrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/browse-by-taxonomy-page.component';
78
import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component';
89
import { ComcolModule } from '../shared/comcol/comcol.module';
910
import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component';
1011
import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component';
1112
import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component';
13+
import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/themed-browse-by-taxonomy-page.component';
1214
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
1315
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
16+
import { FormModule } from '../shared/form/form.module';
1417

1518
const ENTRY_COMPONENTS = [
1619
// put only entry components that use custom decorator
1720
BrowseByTitlePageComponent,
1821
BrowseByMetadataPageComponent,
1922
BrowseByDatePageComponent,
23+
BrowseByTaxonomyPageComponent,
2024

2125
ThemedBrowseByMetadataPageComponent,
2226
ThemedBrowseByDatePageComponent,
2327
ThemedBrowseByTitlePageComponent,
24-
28+
ThemedBrowseByTaxonomyPageComponent,
2529
];
2630

2731
@NgModule({
2832
imports: [
2933
SharedBrowseByModule,
3034
CommonModule,
3135
ComcolModule,
32-
DsoPageModule
36+
DsoPageModule,
37+
FormModule,
3338
],
3439
declarations: [
3540
BrowseBySwitcherComponent,

0 commit comments

Comments
 (0)