mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'main' into authorities_and_controlled_vocabularies
This commit is contained in:
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -1,7 +1,7 @@
|
||||
## References
|
||||
_Add references/links to any related tickets or PRs. These may include:_
|
||||
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
|
||||
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
|
||||
_Add references/links to any related issues or PRs. These may include:_
|
||||
* Fixes [GitHub issue](https://github.com/DSpace/dspace-angular/issues), if any
|
||||
* Requires [REST API PR](https://github.com/DSpace/DSpace/pulls), if any
|
||||
|
||||
## Description
|
||||
Short summary of changes (1-2 sentences).
|
||||
|
@@ -0,0 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{'admin.curation-tasks.header' |translate }}</h2>
|
||||
<ds-curation-form></ds-curation-form>
|
||||
</div>
|
@@ -0,0 +1,28 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('AdminCurationTasksComponent', () => {
|
||||
let comp: AdminCurationTasksComponent;
|
||||
let fixture: ComponentFixture<AdminCurationTasksComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [AdminCurationTasksComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminCurationTasksComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
describe('init', () => {
|
||||
it('should initialise the comp', () => {
|
||||
expect(comp).toBeDefined();
|
||||
expect(fixture.debugElement.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Component responsible for rendering the system wide Curation Task UI
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-admin-curation-task',
|
||||
templateUrl: './admin-curation-tasks.component.html',
|
||||
})
|
||||
export class AdminCurationTasksComponent {
|
||||
|
||||
}
|
@@ -6,6 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
|
||||
const REGISTRIES_MODULE_PATH = 'registries';
|
||||
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
@@ -41,6 +42,12 @@ export function getAccessControlModulePath() {
|
||||
component: AdminWorkflowPageComponent,
|
||||
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
||||
},
|
||||
{
|
||||
path: 'curation-tasks',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AdminCurationTasksComponent,
|
||||
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -469,7 +469,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: ''
|
||||
link: 'admin/curation-tasks'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
|
@@ -16,6 +16,7 @@ import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './adm
|
||||
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -35,6 +36,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
|
||||
CommunityAdminSearchResultGridElementComponent,
|
||||
CollectionAdminSearchResultGridElementComponent,
|
||||
ItemAdminSearchResultActionsComponent,
|
||||
AdminCurationTasksComponent,
|
||||
|
||||
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<div class="container">
|
||||
<h3>{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}</h3>
|
||||
<ds-curation-form
|
||||
[dsoHandle]="(dsoRD$|async)?.payload.handle"
|
||||
></ds-curation-form>
|
||||
</div>
|
||||
|
@@ -0,0 +1,69 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { CollectionCurateComponent } from './collection-curate.component';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
describe('CollectionCurateComponent', () => {
|
||||
let comp: CollectionCurateComponent;
|
||||
let fixture: ComponentFixture<CollectionCurateComponent>;
|
||||
let debugEl: DebugElement;
|
||||
|
||||
let routeStub;
|
||||
let dsoNameService;
|
||||
|
||||
const collection = Object.assign(new Collection(), {
|
||||
handle: '123456789/1', metadata: {'dc.title': ['Collection Name']}
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
routeStub = {
|
||||
parent: {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(collection)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
||||
getName: 'Collection Name'
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [CollectionCurateComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: DSONameService, useValue: dsoNameService}
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CollectionCurateComponent);
|
||||
comp = fixture.componentInstance;
|
||||
debugEl = fixture.debugElement;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('init', () => {
|
||||
it('should initialise the comp', () => {
|
||||
expect(comp).toBeDefined();
|
||||
expect(debugEl.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||
});
|
||||
it('should contain the collection information provided in the route', () => {
|
||||
comp.dsoRD$.subscribe((value) => {
|
||||
expect(value.payload.handle
|
||||
).toEqual('123456789/1');
|
||||
});
|
||||
comp.collectionName$.subscribe((value) => {
|
||||
expect(value).toEqual('Collection Name');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,4 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Component for managing a collection's curation tasks
|
||||
@@ -8,5 +15,26 @@ import { Component } from '@angular/core';
|
||||
templateUrl: './collection-curate.component.html',
|
||||
})
|
||||
export class CollectionCurateComponent {
|
||||
/* TODO: Implement Collection Edit - Curate */
|
||||
dsoRD$: Observable<RemoteData<Collection>>;
|
||||
collectionName$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dsoRD$ = this.route.parent.data.pipe(
|
||||
take(1),
|
||||
map((data) => data.dso),
|
||||
);
|
||||
|
||||
this.collectionName$ = this.dsoRD$.pipe(
|
||||
filter((rd: RemoteData<Collection>) => hasValue(rd)),
|
||||
map((rd: RemoteData<Collection>) => {
|
||||
return this.dsoNameService.getName(rd.payload);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<div class="container">
|
||||
<h3>{{'community.curate.header' |translate:{community: (communityName$ |async)} }}</h3>
|
||||
<ds-curation-form
|
||||
[dsoHandle]="(dsoRD$|async)?.payload.handle"
|
||||
></ds-curation-form>
|
||||
</div>
|
||||
|
@@ -0,0 +1,69 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { CommunityCurateComponent } from './community-curate.component';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
|
||||
describe('CommunityCurateComponent', () => {
|
||||
let comp: CommunityCurateComponent;
|
||||
let fixture: ComponentFixture<CommunityCurateComponent>;
|
||||
let debugEl: DebugElement;
|
||||
|
||||
let routeStub;
|
||||
let dsoNameService;
|
||||
|
||||
const community = Object.assign(new Community(), {
|
||||
handle: '123456789/1', metadata: {'dc.title': ['Community Name']}
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
routeStub = {
|
||||
parent: {
|
||||
data: observableOf({
|
||||
dso: createSuccessfulRemoteDataObject(community)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
||||
getName: 'Community Name'
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [CommunityCurateComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: DSONameService, useValue: dsoNameService}
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommunityCurateComponent);
|
||||
comp = fixture.componentInstance;
|
||||
debugEl = fixture.debugElement;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('init', () => {
|
||||
it('should initialise the comp', () => {
|
||||
expect(comp).toBeDefined();
|
||||
expect(debugEl.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||
});
|
||||
it('should contain the community information provided in the route', () => {
|
||||
comp.dsoRD$.subscribe((value) => {
|
||||
expect(value.payload.handle
|
||||
).toEqual('123456789/1');
|
||||
});
|
||||
comp.communityName$.subscribe((value) => {
|
||||
expect(value).toEqual('Community Name');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,4 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { filter } from 'rxjs/internal/operators/filter';
|
||||
|
||||
/**
|
||||
* Component for managing a community's curation tasks
|
||||
@@ -7,6 +15,29 @@ import { Component } from '@angular/core';
|
||||
selector: 'ds-community-curate',
|
||||
templateUrl: './community-curate.component.html',
|
||||
})
|
||||
export class CommunityCurateComponent {
|
||||
/* TODO: Implement Community Edit - Curate */
|
||||
export class CommunityCurateComponent implements OnInit {
|
||||
|
||||
dsoRD$: Observable<RemoteData<Community>>;
|
||||
communityName$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dsoRD$ = this.route.parent.data.pipe(
|
||||
take(1),
|
||||
map((data) => data.dso),
|
||||
);
|
||||
|
||||
this.communityName$ = this.dsoRD$.pipe(
|
||||
filter((rd: RemoteData<Community>) => hasValue(rd)),
|
||||
map((rd: RemoteData<Community>) => {
|
||||
return this.dsoNameService.getName(rd.payload);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -164,6 +164,8 @@ import { VocabularyEntriesResponseParsingService } from './submission/vocabulari
|
||||
import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { VocabularyService } from './submission/vocabularies/vocabulary.service';
|
||||
import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service';
|
||||
import { ConfigurationDataService } from './data/configuration-data.service';
|
||||
import { ConfigurationProperty } from './shared/configuration-property.model';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -247,6 +249,7 @@ const PROVIDERS = [
|
||||
UploaderService,
|
||||
FileService,
|
||||
DSpaceObjectDataService,
|
||||
ConfigurationDataService,
|
||||
DSOChangeAnalyzer,
|
||||
DefaultChangeAnalyzer,
|
||||
ArrayMoveChangeAnalyzer,
|
||||
@@ -357,7 +360,8 @@ export const models =
|
||||
Registration,
|
||||
Vocabulary,
|
||||
VocabularyEntry,
|
||||
VocabularyEntryDetail
|
||||
VocabularyEntryDetail,
|
||||
ConfigurationProperty
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
87
src/app/core/data/configuration-data.service.spec.ts
Normal file
87
src/app/core/data/configuration-data.service.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { FindByIDRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ConfigurationDataService } from './configuration-data.service';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
|
||||
describe('ConfigurationDataService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: ConfigurationDataService;
|
||||
let halService: HALEndpointService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
const testObject = {
|
||||
uuid: 'test-property',
|
||||
name: 'test-property',
|
||||
values: ['value-1', 'value-2']
|
||||
} as ConfigurationProperty;
|
||||
const configLink = 'https://rest.api/rest/api/config/properties';
|
||||
const requestURL = `https://rest.api/rest/api/config/properties/${testObject.name}`;
|
||||
const requestUUID = 'test-property';
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', {a: configLink})
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: cold('a', {
|
||||
a: {
|
||||
payload: testObject
|
||||
}
|
||||
})
|
||||
});
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
|
||||
service = new ConfigurationDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator
|
||||
);
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should call HALEndpointService with the path to the properties endpoint', () => {
|
||||
scheduler.schedule(() => service.findByPropertyName(testObject.name));
|
||||
scheduler.flush();
|
||||
|
||||
expect(halService.getEndpoint).toHaveBeenCalledWith('properties');
|
||||
});
|
||||
|
||||
it('should configure the proper FindByIDRequest', () => {
|
||||
scheduler.schedule(() => service.findByPropertyName(testObject.name));
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.name));
|
||||
});
|
||||
|
||||
it('should return a RemoteData<ConfigurationProperty> for the object with the given name', () => {
|
||||
const result = service.findByPropertyName(testObject.name);
|
||||
const expected = cold('a', {
|
||||
a: {
|
||||
payload: testObject
|
||||
}
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
62
src/app/core/data/configuration-data.service.ts
Normal file
62
src/app/core/data/configuration-data.service.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { DataService } from './data.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestService } from './request.service';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { CONFIG_PROPERTY } from '../shared/config-property.resource-type';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class DataServiceImpl extends DataService<ConfigurationProperty> {
|
||||
protected linkPath = 'properties';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<ConfigurationProperty>) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@dataService(CONFIG_PROPERTY)
|
||||
/**
|
||||
* Data Service responsible for retrieving Configuration properties
|
||||
*/
|
||||
export class ConfigurationDataService {
|
||||
protected linkPath = 'properties';
|
||||
private dataService: DataServiceImpl;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<ConfigurationProperty>) {
|
||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a configuration property by name
|
||||
* @param name
|
||||
*/
|
||||
findByPropertyName(name: string): Observable<RemoteData<ConfigurationProperty>> {
|
||||
return this.dataService.findById(name);
|
||||
}
|
||||
}
|
9
src/app/core/shared/config-property.resource-type.ts
Normal file
9
src/app/core/shared/config-property.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for ConfigurationProperty
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const CONFIG_PROPERTY = new ResourceType('property');
|
48
src/app/core/shared/configuration-property.model.ts
Normal file
48
src/app/core/shared/configuration-property.model.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../cache/builders/build-decorators';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { CONFIG_PROPERTY } from './config-property.resource-type';
|
||||
|
||||
/**
|
||||
* Model class for a Configuration Property
|
||||
*/
|
||||
@typedObject
|
||||
export class ConfigurationProperty implements CacheableObject {
|
||||
static type = CONFIG_PROPERTY;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* The uuid of the configuration property
|
||||
* The name is used as id for configuration properties
|
||||
*/
|
||||
@autoserializeAs(String, 'name')
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* The name of the configuration property
|
||||
*/
|
||||
@autoserialize
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The values of the configuration property
|
||||
*/
|
||||
@autoserialize
|
||||
values: string[];
|
||||
|
||||
/**
|
||||
* The links of the configuration property
|
||||
*/
|
||||
@deserialize
|
||||
_links: { self: HALLink };
|
||||
|
||||
}
|
20
src/app/curation-form/curation-form.component.html
Normal file
20
src/app/curation-form/curation-form.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<div class="form-group">
|
||||
<div class="row mb-2">
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="font-weight-bold" for="task">{{'curation.form.task-select.label' |translate }}</label>
|
||||
<select id="task" formControlName="task" class="form-control">
|
||||
<option *ngFor="let task of tasks" [value]="task">
|
||||
{{ 'curation-task.task.' + task + '.label' | translate }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="!hasHandleValue()" class="col-12 col-sm-6">
|
||||
<label class="font-weight-bold" for="handle">{{'curation.form.handle.label' |translate }}</label>
|
||||
<input id="handle" class="form-control" formControlName="handle">
|
||||
<small class="text-muted">{{'curation.form.handle.hint' |translate }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-default btn-primary" type="submit">{{'curation.form.submit' |translate }}</button>
|
||||
</div>
|
||||
</form>
|
165
src/app/curation-form/curation-form.component.spec.ts
Normal file
165
src/app/curation-form/curation-form.component.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CurationFormComponent } from './curation-form.component';
|
||||
import { ScriptDataService } from '../core/data/processes/script-data.service';
|
||||
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RequestEntry } from '../core/data/request.reducer';
|
||||
import { DSOSuccessResponse, RestResponse } from '../core/cache/response.models';
|
||||
import { Process } from '../process-page/processes/process.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||
import { RouterStub } from '../shared/testing/router.stub';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||
|
||||
describe('CurationFormComponent', () => {
|
||||
let comp: CurationFormComponent;
|
||||
let fixture: ComponentFixture<CurationFormComponent>;
|
||||
|
||||
let scriptDataService: ScriptDataService;
|
||||
let processDataService: ProcessDataService;
|
||||
let configurationDataService: ConfigurationDataService;
|
||||
let authService: AuthService;
|
||||
let notificationsService;
|
||||
let router;
|
||||
|
||||
const requestEntry = Object.assign(new RequestEntry(),
|
||||
{response: new DSOSuccessResponse(['process-link'], 200, 'success')});
|
||||
const failedRequestEntry = Object.assign(new RequestEntry(),
|
||||
{response: new RestResponse(false, 400, 'Bad Request')});
|
||||
|
||||
const process = Object.assign(new Process(), {processId: 'process-id'});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
||||
scriptDataService = jasmine.createSpyObj('scriptDataService', {
|
||||
invoke: observableOf(requestEntry)
|
||||
});
|
||||
|
||||
processDataService = jasmine.createSpyObj('processDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(process)
|
||||
});
|
||||
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
getAuthenticatedUserFromStore: observableOf(Object.assign(new EPerson(), {email: 'test@mail'}))
|
||||
});
|
||||
|
||||
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
name: 'plugin.named.org.dspace.curate.CurationTask',
|
||||
values: [
|
||||
'org.dspace.ctask.general.ProfileFormats = profileformats',
|
||||
'',
|
||||
'org.dspace.ctask.general.RequiredMetadata = requiredmetadata',
|
||||
'org.dspace.ctask.general.MetadataValueLinkChecker = checklinks',
|
||||
'value-to-be-skipped'
|
||||
]
|
||||
}))
|
||||
});
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
router = new RouterStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
|
||||
declarations: [CurationFormComponent],
|
||||
providers: [
|
||||
{provide: ScriptDataService, useValue: scriptDataService},
|
||||
{provide: ProcessDataService, useValue: processDataService},
|
||||
{provide: AuthService, useValue: authService},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: Router, useValue: router},
|
||||
{provide: ConfigurationDataService, useValue: configurationDataService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CurationFormComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('init', () => {
|
||||
it('should initialise the comp and contain the different tasks', () => {
|
||||
expect(comp).toBeDefined();
|
||||
|
||||
const elements = fixture.debugElement.queryAll(By.css('option'));
|
||||
expect(elements.length).toEqual(3);
|
||||
expect(elements[0].nativeElement.innerHTML).toContain('curation-task.task.profileformats.label');
|
||||
expect(elements[1].nativeElement.innerHTML).toContain('curation-task.task.requiredmetadata.label');
|
||||
expect(elements[2].nativeElement.innerHTML).toContain('curation-task.task.checklinks.label');
|
||||
});
|
||||
});
|
||||
describe('hasHandleValue', () => {
|
||||
it('should return true when a dsoHandle value was provided', () => {
|
||||
comp.dsoHandle = 'some-handle';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.hasHandleValue()).toBeTrue();
|
||||
});
|
||||
it('should return false when no dsoHandle value was provided', () => {
|
||||
expect(comp.hasHandleValue()).toBeFalse();
|
||||
});
|
||||
});
|
||||
describe('submit', () => {
|
||||
it('should submit the selected process and handle to the scriptservice and navigate to the corresponding process page', () => {
|
||||
comp.dsoHandle = 'test-handle';
|
||||
comp.submit();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||
{name: '-t', value: 'profileformats'},
|
||||
{name: '-i', value: 'test-handle'},
|
||||
{name: '-e', value: 'test@mail'},
|
||||
], []);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(processDataService.findByHref).toHaveBeenCalledWith('process-link');
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/processes', 'process-id']);
|
||||
});
|
||||
it('should the selected process and handle to the scriptservice and stay on the page on error', () => {
|
||||
(scriptDataService.invoke as jasmine.Spy).and.returnValue(observableOf(failedRequestEntry));
|
||||
|
||||
comp.dsoHandle = 'test-handle';
|
||||
comp.submit();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||
{name: '-t', value: 'profileformats'},
|
||||
{name: '-i', value: 'test-handle'},
|
||||
{name: '-e', value: 'test@mail'},
|
||||
], []);
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(processDataService.findByHref).not.toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should use the handle provided by the form when no dsoHandle is provided', () => {
|
||||
comp.form.get('handle').patchValue('form-handle');
|
||||
|
||||
comp.submit();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||
{name: '-t', value: 'profileformats'},
|
||||
{name: '-i', value: 'form-handle'},
|
||||
{name: '-e', value: 'test@mail'},
|
||||
], []);
|
||||
});
|
||||
it('should use "all" when the handle provided by the form is empty and when no dsoHandle is provided', () => {
|
||||
|
||||
comp.submit();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||
{name: '-t', value: 'profileformats'},
|
||||
{name: '-i', value: 'all'},
|
||||
{name: '-e', value: 'test@mail'},
|
||||
], []);
|
||||
});
|
||||
});
|
118
src/app/curation-form/curation-form.component.ts
Normal file
118
src/app/curation-form/curation-form.component.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ScriptDataService } from '../core/data/processes/script-data.service';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { getResponseFromEntry } from '../core/shared/operators';
|
||||
import { DSOSuccessResponse } from '../core/cache/response.models';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||
import { Process } from '../process-page/processes/process.model';
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { find } from 'rxjs/internal/operators/find';
|
||||
|
||||
export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
|
||||
|
||||
/**
|
||||
* Component responsible for rendering the Curation Task form
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-curation-form',
|
||||
templateUrl: './curation-form.component.html'
|
||||
})
|
||||
export class CurationFormComponent implements OnInit {
|
||||
|
||||
config: Observable<RemoteData<ConfigurationProperty>>;
|
||||
tasks: string[];
|
||||
form: FormGroup;
|
||||
|
||||
@Input()
|
||||
dsoHandle: string;
|
||||
|
||||
constructor(
|
||||
private scriptDataService: ScriptDataService,
|
||||
private configurationDataService: ConfigurationDataService,
|
||||
private processDataService: ProcessDataService,
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = new FormGroup({
|
||||
task: new FormControl(''),
|
||||
handle: new FormControl('')
|
||||
});
|
||||
|
||||
this.config = this.configurationDataService.findByPropertyName(CURATION_CFG);
|
||||
this.config.pipe(
|
||||
find((rd: RemoteData<ConfigurationProperty>) => rd.hasSucceeded),
|
||||
map((rd: RemoteData<ConfigurationProperty>) => rd.payload)
|
||||
).subscribe((configProperties) => {
|
||||
this.tasks = configProperties.values
|
||||
.filter((value) => isNotEmpty(value) && value.includes('='))
|
||||
.map((value) => value.split('=')[1].trim());
|
||||
this.form.get('task').patchValue(this.tasks[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the inputted dsoHandle has a value
|
||||
*/
|
||||
hasHandleValue() {
|
||||
if (hasValue(this.dsoHandle)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the selected taskName and handle to the script data service to run the corresponding curation script
|
||||
* Navigate to the process page on success
|
||||
*/
|
||||
submit() {
|
||||
const taskName = this.form.get('task').value;
|
||||
let handle;
|
||||
if (this.hasHandleValue()) {
|
||||
handle = this.dsoHandle;
|
||||
} else {
|
||||
handle = this.form.get('handle').value;
|
||||
if (isEmpty(handle)) {
|
||||
handle = 'all';
|
||||
}
|
||||
}
|
||||
this.authService.getAuthenticatedUserFromStore().pipe(
|
||||
take(1),
|
||||
switchMap((eperson: EPerson) => {
|
||||
return this.scriptDataService.invoke('curate', [
|
||||
{name: '-t', value: taskName},
|
||||
{name: '-i', value: handle},
|
||||
{name: '-e', value: eperson.email},
|
||||
], []).pipe(getResponseFromEntry());
|
||||
})
|
||||
).subscribe((response: DSOSuccessResponse) => {
|
||||
if (response.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
|
||||
this.translateService.get('curation.form.submit.success.content'));
|
||||
this.processDataService.findByHref(response.resourceSelfLinks[0]).pipe(
|
||||
filter((processRD: RemoteData<Process>) => hasValue(processRD) && hasValue(processRD.payload)),
|
||||
take(1))
|
||||
.subscribe((processRD: RemoteData<Process>) => {
|
||||
this.router.navigate(['/processes', processRD.payload.processId]);
|
||||
});
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
||||
this.translateService.get('curation.form.submit.error.content'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -16,7 +16,9 @@
|
||||
<div class="mb-4">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<a *ngIf="!hideReturnButton" [routerLink]="getPageUrl((dsoRD$ | async)?.payload)" class="btn btn-outline-secondary">{{ type + '.edit.return' | translate }}</a>
|
||||
<div class="col-12">
|
||||
<a *ngIf="!hideReturnButton" [routerLink]="getPageUrl((dsoRD$ | async)?.payload)" class="btn btn-outline-secondary">{{ type + '.edit.return' | translate }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -205,6 +205,7 @@ import { FileDownloadLinkComponent } from './file-download-link/file-download-li
|
||||
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
|
||||
import { DsSelectComponent } from './ds-select/ds-select.component';
|
||||
import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
|
||||
import { CurationFormComponent } from '../curation-form/curation-form.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -471,7 +472,8 @@ const ENTRY_COMPONENTS = [
|
||||
ClaimedTaskActionsReturnToPoolComponent,
|
||||
ClaimedTaskActionsEditMetadataComponent,
|
||||
FileDownloadLinkComponent,
|
||||
VocabularyTreeviewComponent
|
||||
VocabularyTreeviewComponent,
|
||||
CurationFormComponent
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
@@ -529,7 +531,8 @@ const DIRECTIVES = [
|
||||
...PIPES,
|
||||
...COMPONENTS,
|
||||
...SHARED_ITEM_PAGE_COMPONENTS,
|
||||
...DIRECTIVES
|
||||
...DIRECTIVES,
|
||||
CurationFormComponent
|
||||
],
|
||||
entryComponents: [
|
||||
...ENTRY_COMPONENTS
|
||||
|
@@ -14,7 +14,11 @@
|
||||
|
||||
"404.page-not-found": "page not found",
|
||||
|
||||
"admin.curation-tasks.breadcrumbs": "System curation tasks",
|
||||
|
||||
"admin.curation-tasks.title": "System curation tasks",
|
||||
|
||||
"admin.curation-tasks.header": "System curation tasks",
|
||||
|
||||
"admin.registries.bitstream-formats.breadcrumbs": "Format registry",
|
||||
|
||||
@@ -560,6 +564,8 @@
|
||||
|
||||
"collection.create.sub-head": "Create a Collection for Community {{ parent }}",
|
||||
|
||||
"collection.curate.header": "Curate Collection: {{collection}}",
|
||||
|
||||
"collection.delete.cancel": "Cancel",
|
||||
|
||||
"collection.delete.confirm": "Confirm",
|
||||
@@ -770,6 +776,8 @@
|
||||
|
||||
"community.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
|
||||
|
||||
"community.curate.header": "Curate Community: {{community}}",
|
||||
|
||||
"community.delete.cancel": "Cancel",
|
||||
|
||||
"community.delete.confirm": "Confirm",
|
||||
@@ -909,6 +917,38 @@
|
||||
|
||||
|
||||
|
||||
"curation-task.task.checklinks.label": "Check Links in Metadata",
|
||||
|
||||
"curation-task.task.noop.label": "NOOP",
|
||||
|
||||
"curation-task.task.profileformats.label": "Profile Bitstream Formats",
|
||||
|
||||
"curation-task.task.requiredmetadata.label": "Check for Required Metadata",
|
||||
|
||||
"curation-task.task.translate.label": "Microsoft Translator",
|
||||
|
||||
"curation-task.task.vscan.label": "Virus Scan",
|
||||
|
||||
|
||||
|
||||
"curation.form.task-select.label": "Task:",
|
||||
|
||||
"curation.form.submit": "Start",
|
||||
|
||||
"curation.form.submit.success.head": "The curation task has been started successfully",
|
||||
|
||||
"curation.form.submit.success.content": "You will be redirected to the corresponding process page.",
|
||||
|
||||
"curation.form.submit.error.head": "Running the curation task failed",
|
||||
|
||||
"curation.form.submit.error.content": "An error occured when trying to start the curation task.",
|
||||
|
||||
"curation.form.handle.label": "Handle:",
|
||||
|
||||
"curation.form.handle.hint": "Hint: Enter [your-handle-prefix]/0 to run a task across entire site (not all tasks may support this capability)",
|
||||
|
||||
|
||||
|
||||
"dso-selector.create.collection.head": "New collection",
|
||||
|
||||
"dso-selector.create.community.head": "New community",
|
||||
|
@@ -6,6 +6,7 @@ import { RestRequestMethod } from '../app/core/data/rest-request-method';
|
||||
export const environment: GlobalConfig = {
|
||||
production: true,
|
||||
// Angular Universal server settings.
|
||||
// NOTE: these must be "synced" with the 'dspace.ui.url' setting in your backend's local.cfg.
|
||||
ui: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
@@ -14,6 +15,8 @@ export const environment: GlobalConfig = {
|
||||
nameSpace: '/',
|
||||
},
|
||||
// The REST API server settings.
|
||||
// NOTE: these must be "synced" with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||
// The 'nameSpace' must always end in "/api" as that's the subpath of the REST API in the backend.
|
||||
rest: {
|
||||
ssl: true,
|
||||
host: 'dspace7.4science.cloud',
|
||||
@@ -212,5 +215,5 @@ export const environment: GlobalConfig = {
|
||||
},
|
||||
theme: {
|
||||
name: 'default',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -195,5 +195,5 @@ export const environment: Partial<GlobalConfig> = {
|
||||
},
|
||||
theme: {
|
||||
name: 'default',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
Reference in New Issue
Block a user