diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index df2424fc03..3e727e2bdf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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). diff --git a/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.html b/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.html new file mode 100644 index 0000000000..a702a7e6b0 --- /dev/null +++ b/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.html @@ -0,0 +1,4 @@ +
+

{{'admin.curation-tasks.header' |translate }}

+ +
diff --git a/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts b/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts new file mode 100644 index 0000000000..b84f619ff1 --- /dev/null +++ b/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts @@ -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; + + 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'); + }); + }); +}); diff --git a/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.ts b/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.ts new file mode 100644 index 0000000000..9a80f341b9 --- /dev/null +++ b/src/app/+admin/admin-curation-tasks/admin-curation-tasks.component.ts @@ -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 { + +} diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index b199129c4e..43b3a4ab34 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -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: [ diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index ade3e33aed..eb86de5f3c 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -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 diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index 25b8bd4648..85749afe03 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -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, diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html index e69de29bb2..38c9d22f4e 100644 --- a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html @@ -0,0 +1,6 @@ +
+

{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}

+ +
diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts new file mode 100644 index 0000000000..91c264cd0f --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts @@ -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; + 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'); + }); + }); + }); +}); diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts index d7deaea982..e20f229cd6 100644 --- a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts @@ -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>; + collectionName$: Observable; + + 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) => hasValue(rd)), + map((rd: RemoteData) => { + return this.dsoNameService.getName(rd.payload); + }) + ); + } } diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts index 8384385572..b5e03b7983 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -30,24 +30,27 @@ describe('CollectionRolesComponent', () => { undefined, Object.assign(new Collection(), { _links: { - 'irrelevant': { + irrelevant: { href: 'irrelevant link', }, - 'adminGroup': { + adminGroup: { href: 'adminGroup link', }, - 'submittersGroup': { + submittersGroup: { href: 'submittersGroup link', }, - 'itemReadGroup': { + itemReadGroup: { href: 'itemReadGroup link', }, - 'bitstreamReadGroup': { + bitstreamReadGroup: { href: 'bitstreamReadGroup link', }, - 'workflowGroups/test': { - href: 'test workflow group link', - }, + workflowGroups: [ + { + name: 'test', + href: 'test workflow group link', + }, + ], }, }), ), diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts index 45f2f37b9b..996933e43d 100644 --- a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts @@ -5,7 +5,7 @@ import { first, map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; -import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; +import { HALLink } from '../../../core/shared/hal-link.model'; /** * Component for managing a collection's roles @@ -31,19 +31,27 @@ export class CollectionRolesComponent implements OnInit { /** * The different roles for the collection, as an observable. */ - getComcolRoles(): Observable { + getComcolRoles(): Observable { return this.collection$.pipe( - map((collection) => - [ - ComcolRole.COLLECTION_ADMIN, - ComcolRole.SUBMITTERS, - ComcolRole.ITEM_READ, - ComcolRole.BITSTREAM_READ, - ...Object.keys(collection._links) - .filter((link) => link.startsWith('workflowGroups/')) - .map((link) => new ComcolRole(link.substr('workflowGroups/'.length), link)), - ] - ), + map((collection) => [ + { + name: 'collection-admin', + href: collection._links.adminGroup.href, + }, + { + name: 'submitters', + href: collection._links.submittersGroup.href, + }, + { + name: 'item_read', + href: collection._links.itemReadGroup.href, + }, + { + name: 'bitstream_read', + href: collection._links.bitstreamReadGroup.href, + }, + ...collection._links.workflowGroups, + ]), ); } diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html index e69de29bb2..6c041d1725 100644 --- a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html +++ b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html @@ -0,0 +1,6 @@ +
+

{{'community.curate.header' |translate:{community: (communityName$ |async)} }}

+ +
diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.spec.ts b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.spec.ts new file mode 100644 index 0000000000..42dc0f08a9 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.spec.ts @@ -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; + 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'); + }); + }); + }); +}); diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts index 6151d3fe9a..5954b3e95e 100644 --- a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts +++ b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts @@ -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>; + communityName$: Observable; + + 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) => hasValue(rd)), + map((rd: RemoteData) => { + return this.dsoNameService.getName(rd.payload); + }) + ); + } + } diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html index 231645a6a5..07d5d96fcd 100644 --- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html @@ -1,5 +1,5 @@ diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts index 336a56a584..62e1f73bad 100644 --- a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts @@ -4,8 +4,8 @@ import { Observable } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { Community } from '../../../core/shared/community.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; -import { ComcolRole } from '../../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; import { RemoteData } from '../../../core/data/remote-data'; +import { HALLink } from '../../../core/shared/hal-link.model'; /** * Component for managing a community's roles @@ -31,10 +31,15 @@ export class CommunityRolesComponent implements OnInit { /** * The different roles for the community. */ - getComcolRoles(): ComcolRole[] { - return [ - ComcolRole.COMMUNITY_ADMIN, - ]; + getComcolRoles$(): Observable { + return this.community$.pipe( + map((community) => [ + { + name: 'community-admin', + href: community._links.adminGroup.href, + }, + ]), + ); } constructor( diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.html b/src/app/+my-dspace-page/collection-selector/collection-selector.component.html new file mode 100644 index 0000000000..83cc4151a3 --- /dev/null +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.scss b/src/app/+my-dspace-page/collection-selector/collection-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts b/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts new file mode 100644 index 0000000000..982d06aa75 --- /dev/null +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts @@ -0,0 +1,164 @@ +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { CollectionSelectorComponent } from './collection-selector.component'; +import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component'; +import { Collection } from 'src/app/core/shared/collection.model'; +import { of, Observable } from 'rxjs'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { Community } from 'src/app/core/shared/community.model'; +import { FindListOptions } from 'src/app/core/data/request.models'; +import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; +import { PaginatedList } from 'src/app/core/data/paginated-list'; +import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; +import { PageInfo } from 'src/app/core/shared/page-info.model'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute } from '@angular/router'; +import { hot } from 'jasmine-marbles'; +import { By } from '@angular/platform-browser'; + +describe('CollectionSelectorComponent', () => { + let component: CollectionSelectorComponent; + let fixture: ComponentFixture; + const modal = jasmine.createSpyObj('modal', ['close', 'dismiss']); + + const community: Community = Object.assign(new Community(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Community 1' + }); + + const collections: Collection[] = [ + Object.assign(new Collection(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Collection 1', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 1' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + name: 'Collection 2', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 2' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + name: 'Collection 3', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 3' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', + name: 'Collection 4', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 4' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: 'a5159760-f362-4659-9e81-e3253ad91ede', + name: 'Collection 5', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 5' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }) + ]; + + // tslint:disable-next-line: max-classes-per-file + const collectionDataServiceMock = { + getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return hot( 'a|', { + a: createSuccessfulRemoteDataObject( + new PaginatedList(new PageInfo(), collections) + ) + }); + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ CollectionSelectorComponent, CollectionDropdownComponent ], + providers: [ + {provide: CollectionDataService, useValue: collectionDataServiceMock}, + {provide: ChangeDetectorRef, useValue: {}}, + {provide: ElementRef, userValue: {}}, + {provide: NgbActiveModal, useValue: modal}, + {provide: ActivatedRoute, useValue: {}} + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call selectObject', fakeAsync(() => { + spyOn(component, 'selectObject'); + fixture.detectChanges(); + tick(); + fixture.whenStable().then(() => { + const collectionItem = fixture.debugElement.query(By.css('.collection-item:nth-child(2)')); + collectionItem.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + expect(component.selectObject).toHaveBeenCalled(); + }); + })); + + it('should close the dialog', () => { + component.close(); + expect((component as any).activeModal.close).toHaveBeenCalled(); + }); +}); diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.ts b/src/app/+my-dspace-page/collection-selector/collection-selector.component.ts new file mode 100644 index 0000000000..f930fc3f54 --- /dev/null +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +/** + * This component displays the dialog that shows the list of selectable collections + * on the MyDSpace page + */ +@Component({ + selector: 'ds-collection-selector', + templateUrl: './collection-selector.component.html', + styleUrls: ['./collection-selector.component.scss'] +}) +export class CollectionSelectorComponent { + + constructor(protected activeModal: NgbActiveModal) {} + + /** + * Method called when an element has been selected from collection list. + * Its close the active modal and send selected value to the component container + * @param dso The selected DSpaceObject + */ + selectObject(dso: DSpaceObject) { + this.activeModal.close(dso); + } + + /** + * Close the modal + */ + close() { + this.activeModal.close(); + } +} diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html index 4cb58012bc..9ae38a2205 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html @@ -3,7 +3,8 @@ + (onUploadError)="onUploadError($event)" + (onFileSelected)="afterFileLoaded($event)">
diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts index f2687e452c..bbcb7f65bf 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, inject, TestBed, tick, fakeAsync } from '@angular/core/testing'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Store } from '@ngrx/store'; @@ -15,7 +15,6 @@ import { createTestComponent } from '../../shared/testing/utils.test'; import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component'; import { AppState } from '../../app.reducer'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { SharedModule } from '../../shared/shared.module'; @@ -25,10 +24,25 @@ import { By } from '@angular/platform-browser'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; +import { UploaderComponent } from 'src/app/shared/uploader/uploader.component'; -describe('MyDSpaceNewSubmissionComponent test', () => { +fdescribe('MyDSpaceNewSubmissionComponent test', () => { + + const translateService: TranslateService = jasmine.createSpyObj('translateService', { + get: (key: string): any => { observableOf(key) }, + instant: jasmine.createSpy('instant') + }); + + const uploader: any = jasmine.createSpyObj('uploader', { + clearQueue: jasmine.createSpy('clearQueue') + }); + + const modalService = { + open: () => { + return { result: new Promise((res, rej) => {/****/}) }; + } + }; - const translateService: any = getMockTranslateService(); const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ dispatch: {}, @@ -58,11 +72,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => { { provide: ScrollToService, useValue: getMockScrollToService() }, { provide: Store, useValue: store }, { provide: TranslateService, useValue: translateService }, - { - provide: NgbModal, useValue: { - open: () => {/*comment*/} - } - }, + { provide: NgbModal, useValue: modalService }, ChangeDetectorRef, MyDSpaceNewSubmissionComponent, UploaderService, @@ -103,6 +113,10 @@ describe('MyDSpaceNewSubmissionComponent test', () => { beforeEach(() => { fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent); comp = fixture.componentInstance; + comp.uploadFilesOptions.authToken = 'user-auth-token'; + comp.uploadFilesOptions.url = 'https://fake.upload-api.url'; + comp.uploaderComponent = TestBed.createComponent(UploaderComponent).componentInstance; + comp.uploaderComponent.uploader = uploader; }); it('should call app.openDialog', () => { @@ -114,6 +128,12 @@ describe('MyDSpaceNewSubmissionComponent test', () => { }); expect(comp.openDialog).toHaveBeenCalled(); }); + + it('should show a collection selector if only one file are uploaded', () => { + spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) }); + comp.afterFileLoaded(['']); + expect((comp as any).modalService.open).toHaveBeenCalled(); + }); }); }); diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts index 5659c442ad..d14fe46afd 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts @@ -1,13 +1,10 @@ -import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; -import { Router } from '@angular/router'; +import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { SubmissionState } from '../../submission/submission.reducers'; import { AuthService } from '../../core/auth/auth.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -18,6 +15,9 @@ import { NotificationType } from '../../shared/notifications/models/notification import { hasValue } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component'; +import { UploaderComponent } from '../../shared/uploader/uploader.component'; +import { UploaderError } from '../../shared/uploader/uploader-error.model'; /** * This component represents the whole mydspace page header @@ -43,6 +43,11 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { */ private sub: Subscription; + /** + * Reference to uploaderComponent + */ + @ViewChild(UploaderComponent, { static: false }) uploaderComponent: UploaderComponent; + /** * Initialize instance variables * @@ -59,9 +64,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private halService: HALEndpointService, private notificationsService: NotificationsService, - private store: Store, private translate: TranslateService, - private router: Router, private modalService: NgbModal) { } @@ -69,6 +72,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { * Initialize url and Bearer token */ ngOnInit() { + this.uploadFilesOptions.autoUpload = false; this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => { this.uploadFilesOptions.url = url; this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); @@ -108,8 +112,12 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { /** * Method called on file upload error */ - public onUploadError() { - this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed')); + public onUploadError(error: UploaderError) { + let errorMessageKey = 'mydspace.upload.upload-failed'; + if (hasValue(error.status) && error.status === 422) { + errorMessageKey = 'mydspace.upload.upload-failed-manyentries'; + } + this.notificationsService.error(null, this.translate.get(errorMessageKey)); } /** @@ -120,6 +128,28 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { this.modalService.open(CreateItemParentSelectorComponent); } + /** + * Method invoked after all file are loaded from upload plugin + */ + afterFileLoaded(items) { + const uploader = this.uploaderComponent.uploader; + if (hasValue(items) && items.length > 1) { + this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed-moreonefile')); + uploader.clearQueue(); + this.changeDetectorRef.detectChanges(); + } else { + const modalRef = this.modalService.open(CollectionSelectorComponent); + // When the dialog are closes its takes the collection selected and + // uploads choosed file after adds owningCollection parameter + modalRef.result.then( (result) => { + uploader.onBuildItemForm = (fileItem: any, form: any) => { + form.append('owningCollection', result.uuid); + }; + uploader.uploadAll(); + }); + } + } + /** * Unsubscribe from the subscription */ diff --git a/src/app/+my-dspace-page/my-dspace-page.module.ts b/src/app/+my-dspace-page/my-dspace-page.module.ts index 1cf30c4ec9..49570fec6d 100644 --- a/src/app/+my-dspace-page/my-dspace-page.module.ts +++ b/src/app/+my-dspace-page/my-dspace-page.module.ts @@ -20,6 +20,7 @@ import { SearchResultListElementComponent } from '../shared/object-list/search-r import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component'; import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component'; import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component'; +import { CollectionSelectorComponent } from './collection-selector/collection-selector.component'; @NgModule({ imports: [ @@ -40,7 +41,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/ ClaimedTaskSearchResultDetailElementComponent, PoolSearchResultDetailElementComponent, MyDSpaceNewSubmissionComponent, - ItemSearchResultListElementSubmissionComponent + ItemSearchResultListElementSubmissionComponent, + CollectionSelectorComponent ], providers: [ MyDSpaceGuard, @@ -57,7 +59,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/ WorkflowItemSearchResultDetailElementComponent, ClaimedTaskSearchResultDetailElementComponent, PoolSearchResultDetailElementComponent, - ItemSearchResultListElementSubmissionComponent + ItemSearchResultListElementSubmissionComponent, + CollectionSelectorComponent ] }) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 835a7f6ba7..5aa462d5e0 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -160,6 +160,8 @@ import { SubmissionCcLicenseDataService } from './submission/submission-cc-licen import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.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 @@ -245,6 +247,7 @@ const PROVIDERS = [ UploaderService, FileService, DSpaceObjectDataService, + ConfigurationDataService, DSOChangeAnalyzer, DefaultChangeAnalyzer, ArrayMoveChangeAnalyzer, @@ -350,7 +353,8 @@ export const models = TemplateItem, Feature, Authorization, - Registration + Registration, + ConfigurationProperty ]; @NgModule({ diff --git a/src/app/core/data/configuration-data.service.spec.ts b/src/app/core/data/configuration-data.service.spec.ts new file mode 100644 index 0000000000..fde55070e1 --- /dev/null +++ b/src/app/core/data/configuration-data.service.spec.ts @@ -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 for the object with the given name', () => { + const result = service.findByPropertyName(testObject.name); + const expected = cold('a', { + a: { + payload: testObject + } + }); + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/data/configuration-data.service.ts b/src/app/core/data/configuration-data.service.ts new file mode 100644 index 0000000000..ec8221ebec --- /dev/null +++ b/src/app/core/data/configuration-data.service.ts @@ -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 { + protected linkPath = 'properties'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + 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) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Finds a configuration property by name + * @param name + */ + findByPropertyName(name: string): Observable> { + return this.dataService.findById(name); + } +} diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index a10b46c3d0..c186bc8dcd 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -40,7 +40,6 @@ import { GROUP } from './models/group.resource-type'; import { DSONameService } from '../breadcrumbs/dso-name.service'; import { Community } from '../shared/community.model'; import { Collection } from '../shared/collection.model'; -import { ComcolRole } from '../../shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role'; const groupRegistryStateSelector = (state: AppState) => state.groupRegistry; const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup); @@ -324,16 +323,17 @@ export class GroupDataService extends DataService { * Create a group for a given role for a given community or collection. * * @param dso The community or collection for which to create a group + * @param role The name of the role for which to create a group * @param link The REST endpoint to create the group */ - createComcolGroup(dso: Community|Collection, link: string): Observable { + createComcolGroup(dso: Community|Collection, role: string, link: string): Observable { const requestId = this.requestService.generateRequestId(); const group = Object.assign(new Group(), { metadata: { 'dc.description': [ { - value: `${this.nameService.getName(dso)} admin group`, + value: `${this.nameService.getName(dso)} ${role} group`, } ], }, diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index b65ac252ef..c1464d7d39 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -15,8 +15,6 @@ import { RESOURCE_POLICY } from '../resource-policy/models/resource-policy.resou import { COMMUNITY } from './community.resource-type'; import { Community } from './community.model'; import { ChildHALResource } from './child-hal-resource.model'; -import { GROUP } from '../eperson/models/group.resource-type'; -import { Group } from '../eperson/models/group.model'; @typedObject @inheritSerialization(DSpaceObject) @@ -41,6 +39,11 @@ export class Collection extends DSpaceObject implements ChildHALResource { defaultAccessConditions: HALLink; logo: HALLink; parentCommunity: HALLink; + workflowGroups: HALLink[]; + adminGroup: HALLink; + submittersGroup: HALLink; + itemReadGroup: HALLink; + bitstreamReadGroup: HALLink; self: HALLink; }; @@ -72,12 +75,6 @@ export class Collection extends DSpaceObject implements ChildHALResource { @link(COMMUNITY, false) parentCommunity?: Observable>; - /** - * The administrators group of this community. - */ - @link(GROUP) - adminGroup?: Observable>; - /** * The introductory text of this Collection * Corresponds to the metadata field dc.description diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index bdcda70e9b..796aaa8ece 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -3,8 +3,6 @@ import { Observable } from 'rxjs'; import { link, typedObject } from '../cache/builders/build-decorators'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { Group } from '../eperson/models/group.model'; -import { GROUP } from '../eperson/models/group.resource-type'; import { Bitstream } from './bitstream.model'; import { BITSTREAM } from './bitstream.resource-type'; import { Collection } from './collection.model'; @@ -66,12 +64,6 @@ export class Community extends DSpaceObject implements ChildHALResource { @link(COMMUNITY, false) parentCommunity?: Observable>; - /** - * The administrators group of this community. - */ - @link(GROUP) - adminGroup?: Observable>; - /** * The introductory text of this Community * Corresponds to the metadata field dc.description diff --git a/src/app/core/shared/config-property.resource-type.ts b/src/app/core/shared/config-property.resource-type.ts new file mode 100644 index 0000000000..b93c29dd66 --- /dev/null +++ b/src/app/core/shared/config-property.resource-type.ts @@ -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'); diff --git a/src/app/core/shared/configuration-property.model.ts b/src/app/core/shared/configuration-property.model.ts new file mode 100644 index 0000000000..465523c29f --- /dev/null +++ b/src/app/core/shared/configuration-property.model.ts @@ -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 }; + +} diff --git a/src/app/core/shared/hal-resource.model.ts b/src/app/core/shared/hal-resource.model.ts index 334509007b..6e4fe1f502 100644 --- a/src/app/core/shared/hal-resource.model.ts +++ b/src/app/core/shared/hal-resource.model.ts @@ -22,6 +22,6 @@ export class HALResource { /** * {@link HALLink}s to related {@link HALResource}s */ - [k: string]: HALLink; + [k: string]: HALLink | HALLink[]; }; } diff --git a/src/app/curation-form/curation-form.component.html b/src/app/curation-form/curation-form.component.html new file mode 100644 index 0000000000..c940494016 --- /dev/null +++ b/src/app/curation-form/curation-form.component.html @@ -0,0 +1,20 @@ +
+
+
+
+ + +
+
+ + + {{'curation.form.handle.hint' |translate }} +
+
+ +
+
diff --git a/src/app/curation-form/curation-form.component.spec.ts b/src/app/curation-form/curation-form.component.spec.ts new file mode 100644 index 0000000000..93bad4fef2 --- /dev/null +++ b/src/app/curation-form/curation-form.component.spec.ts @@ -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; + + 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'}, + ], []); + }); +}); diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts new file mode 100644 index 0000000000..95512d1129 --- /dev/null +++ b/src/app/curation-form/curation-form.component.ts @@ -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>; + 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) => rd.hasSucceeded), + map((rd: RemoteData) => 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) => hasValue(processRD) && hasValue(processRD.payload)), + take(1)) + .subscribe((processRD: RemoteData) => { + 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')); + } + }); + } +} diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts index 4694c13603..6604667a98 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts @@ -4,13 +4,11 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { By } from '@angular/platform-browser'; import { SharedModule } from '../../../shared.module'; import { TranslateModule } from '@ngx-translate/core'; -import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { RequestService } from '../../../../core/data/request.service'; -import { ComcolRole } from './comcol-role'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { RemoteData } from '../../../../core/data/remote-data'; import { RouterTestingModule } from '@angular/router/testing'; -import { Collection } from '../../../../core/shared/collection.model'; describe('ComcolRoleComponent', () => { @@ -65,20 +63,10 @@ describe('ComcolRoleComponent', () => { comp = fixture.componentInstance; de = fixture.debugElement; - comp.comcolRole = new ComcolRole( - 'test role name', - 'test role endpoint', - ); - - comp.dso = Object.assign( - new Collection(), { - _links: { - 'test role endpoint': { - href: 'test role link', - } - } - } - ); + comp.comcolRole = { + name: 'test role name', + href: 'test role link', + }; fixture.detectChanges(); }); @@ -138,7 +126,7 @@ describe('ComcolRoleComponent', () => { }); it('should call the groupService create method', () => { - expect(groupService.createComcolGroup).toHaveBeenCalledWith(comp.dso, 'test role link'); + expect(groupService.createComcolGroup).toHaveBeenCalledWith(comp.dso, 'test role name', 'test role link'); }); }); }); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts index 41cb7e7cd2..986d616c7d 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts @@ -7,9 +7,9 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Collection } from '../../../../core/shared/collection.model'; import { filter, map } from 'rxjs/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; -import { ComcolRole } from './comcol-role'; import { RequestService } from '../../../../core/data/request.service'; import { RemoteData } from '../../../../core/data/remote-data'; +import { HALLink } from '../../../../core/shared/hal-link.model'; /** * Component for managing a community or collection role. @@ -31,7 +31,7 @@ export class ComcolRoleComponent implements OnInit { * The role to manage */ @Input() - comcolRole: ComcolRole; + comcolRole: HALLink; constructor( protected requestService: RequestService, @@ -43,7 +43,7 @@ export class ComcolRoleComponent implements OnInit { * The link to the related group. */ get groupLink(): string { - return this.dso._links[this.comcolRole.linkName].href; + return this.comcolRole.href; } /** @@ -106,7 +106,7 @@ export class ComcolRoleComponent implements OnInit { * Create a group for this community or collection role. */ create() { - this.groupService.createComcolGroup(this.dso, this.groupLink).subscribe(); + this.groupService.createComcolGroup(this.dso, this.comcolRole.name, this.groupLink).subscribe(); } /** diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts deleted file mode 100644 index 2ac74fe67b..0000000000 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Community } from '../../../../core/shared/community.model'; -import { Collection } from '../../../../core/shared/collection.model'; - -/** - * Class representing a community or collection role. - */ -export class ComcolRole { - - /** - * The community admin role. - */ - public static COMMUNITY_ADMIN = new ComcolRole( - 'community-admin', - 'adminGroup', - ); - - /** - * The collection admin role. - */ - public static COLLECTION_ADMIN = new ComcolRole( - 'collection-admin', - 'adminGroup', - ); - - /** - * The submitters role. - */ - public static SUBMITTERS = new ComcolRole( - 'submitters', - 'submittersGroup', - ); - - /** - * The default item read role. - */ - public static ITEM_READ = new ComcolRole( - 'item_read', - 'itemReadGroup', - ); - - /** - * The default bitstream read role. - */ - public static BITSTREAM_READ = new ComcolRole( - 'bitstream_read', - 'bitstreamReadGroup', - ); - - /** - * @param name The name for this community or collection role. - * @param linkName The path linking to this community or collection role. - */ - constructor( - public name, - public linkName, - ) { - } - - /** - * Get the REST endpoint for managing this role for a given community or collection. - * @param dso - */ - public getEndpoint(dso: Community | Collection) { - - let linkPath; - switch (dso.type + '') { - case 'community': - linkPath = 'communities'; - break; - case 'collection': - linkPath = 'collections'; - break; - } - - return `${linkPath}/${dso.uuid}/${this.linkName}`; - } -} diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html index aa6290ea9f..ad270aab80 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html @@ -16,7 +16,9 @@
- {{ type + '.edit.return' | translate }} +
diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index dee06c29b2..9886eb28d2 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -318,6 +318,12 @@ export class FormComponent implements OnDestroy, OnInit { const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any; if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN) { model.value = Object.values(value)[0]; + const ctrl = formArrayControl.controls[formArrayControl.length - 1]; + const ctrlValue = ctrl.value; + const ctrlValueKey = Object.keys(ctrlValue)[0]; + ctrl.setValue({ + [ctrlValueKey]: model.value + }); } else if (this.formBuilderService.isQualdropGroup(model)) { const ctrl = formArrayControl.controls[formArrayControl.length - 1]; const ctrlKey = Object.keys(ctrl.value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX))); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index d1c7ed7efe..e766881f59 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -208,6 +208,7 @@ import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group- import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component'; import { DsSelectComponent } from './ds-select/ds-select.component'; +import { CurationFormComponent } from '../curation-form/curation-form.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -472,7 +473,8 @@ const ENTRY_COMPONENTS = [ ClaimedTaskActionsReturnToPoolComponent, ClaimedTaskActionsEditMetadataComponent, CollectionDropdownComponent, - FileDownloadLinkComponent + FileDownloadLinkComponent, + CurationFormComponent, ]; const SHARED_ITEM_PAGE_COMPONENTS = [ @@ -530,7 +532,8 @@ const DIRECTIVES = [ ...PIPES, ...COMPONENTS, ...SHARED_ITEM_PAGE_COMPONENTS, - ...DIRECTIVES + ...DIRECTIVES, + CurationFormComponent ], entryComponents: [ ...ENTRY_COMPONENTS diff --git a/src/app/shared/uploader/uploader-error.model.ts b/src/app/shared/uploader/uploader-error.model.ts new file mode 100644 index 0000000000..9238a0df36 --- /dev/null +++ b/src/app/shared/uploader/uploader-error.model.ts @@ -0,0 +1,9 @@ +/** + * An interface that represents the upload error values + */ +export interface UploaderError { + item?: any; + response?: any; + status?: any; + headers?: any; +} diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/uploader/uploader-options.model.ts index f195b0930e..959e5c3295 100644 --- a/src/app/shared/uploader/uploader-options.model.ts +++ b/src/app/shared/uploader/uploader-options.model.ts @@ -17,6 +17,11 @@ export class UploaderOptions { */ autoUpload = true; + /** + * Set the max number of files that can be loaded + */ + maxFileNumber: number; + /** * The request method to use for the file upload request */ diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts index 72a38d1eb1..07f4245954 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/uploader/uploader.component.ts @@ -69,6 +69,11 @@ export class UploaderComponent { */ @Output() onUploadError: EventEmitter = new EventEmitter(); + /** + * The function to call when a file is selected + */ + @Output() onFileSelected: EventEmitter = new EventEmitter(); + public uploader: FileUploader; public uploaderId: string; public isOverBaseDropZone = observableOf(false); @@ -102,7 +107,8 @@ export class UploaderComponent { itemAlias: this.uploadFilesOptions.itemAlias, removeAfterUpload: true, autoUpload: this.uploadFilesOptions.autoUpload, - method: this.uploadFilesOptions.method + method: this.uploadFilesOptions.method, + queueLimit: this.uploadFilesOptions.maxFileNumber }); if (isUndefined(this.enableDragOverDocument)) { @@ -121,6 +127,9 @@ export class UploaderComponent { this.uploader.onAfterAddingFile = ((item) => { item.withCredentials = false; }); + this.uploader.onAfterAddingAll = ((items) => { + this.onFileSelected.emit(items); + }); if (isUndefined(this.onBeforeUpload)) { this.onBeforeUpload = () => {return}; } @@ -149,7 +158,7 @@ export class UploaderComponent { } }; this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => { - this.onUploadError.emit(null); + this.onUploadError.emit({ item: item, response: response, status: status, headers: headers }); this.uploader.cancelAll(); }; this.uploader.onProgressAll = () => this.onProgress(); diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index 6e7a35fe26..28bf71b210 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -28,6 +28,7 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; +import { deepClone } from 'fast-json-patch'; /** * The service handling all form section operations @@ -310,7 +311,7 @@ export class SectionFormOperationsService { event: DynamicFormControlEvent ): void { const path = this.getFieldPathSegmentedFromChangeEvent(event); - const value = this.getFieldValueFromChangeEvent(event); + const value = deepClone(this.getFieldValueFromChangeEvent(event)); if (isNotEmpty(value)) { value.place = this.getArrayIndexFromEvent(event); if (hasValue(event.group) && hasValue(event.group.value)) { diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ee07ef3553..7cb87125c8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -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", @@ -919,6 +959,8 @@ "dso-selector.create.item.head": "New item", + "dso-selector.create.submission.head": "New submission", + "dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.community.head": "Edit community", @@ -1941,6 +1983,10 @@ "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", + "mydspace.upload.upload-failed-manyentries": "Unprocessable file. Detected too many entries but allowed only one for file.", + + "mydspace.upload.upload-failed-moreonefile": "Unprocessable request. Only one file is allowed.", + "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts index 3a10fc4b82..32ae2f54b0 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -215,5 +215,5 @@ export const environment: GlobalConfig = { }, theme: { name: 'default', - } + }, }; diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts index 7bf6d6c846..6e4d60e268 100644 --- a/src/environments/mock-environment.ts +++ b/src/environments/mock-environment.ts @@ -195,5 +195,5 @@ export const environment: Partial = { }, theme: { name: 'default', - } + }, };