Merge branch 'main' into iiif-mirador

This commit is contained in:
Michael Spalti
2021-04-01 16:01:08 -07:00
114 changed files with 2190 additions and 795 deletions

View File

@@ -132,7 +132,8 @@
"sortablejs": "1.10.1", "sortablejs": "1.10.1",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "^0.10.3" "zone.js": "^0.10.3",
"@kolkov/ngx-gallery": "^1.2.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "10.0.1", "@angular-builders/custom-webpack": "10.0.1",

View File

@@ -2,12 +2,7 @@ import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAdminModuleRoute } from '../app-routing-paths'; import { getAdminModuleRoute } from '../app-routing-paths';
export const REGISTRIES_MODULE_PATH = 'registries'; export const REGISTRIES_MODULE_PATH = 'registries';
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
export function getRegistriesModuleRoute() { export function getRegistriesModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
} }
export function getAccessControlModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), ACCESS_CONTROL_MODULE_PATH).toString();
}

View File

@@ -6,7 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
import { ACCESS_CONTROL_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -16,11 +16,6 @@ import { ACCESS_CONTROL_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-rout
loadChildren: () => import('./admin-registries/admin-registries.module') loadChildren: () => import('./admin-registries/admin-registries.module')
.then((m) => m.AdminRegistriesModule), .then((m) => m.AdminRegistriesModule),
}, },
{
path: ACCESS_CONTROL_MODULE_PATH,
loadChildren: () => import('./admin-access-control/admin-access-control.module')
.then((m) => m.AdminAccessControlModule),
},
{ {
path: 'search', path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: I18nBreadcrumbResolver },

View File

@@ -16,6 +16,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import createSpy = jasmine.createSpy;
describe('AdminSidebarComponent', () => { describe('AdminSidebarComponent', () => {
let comp: AdminSidebarComponent; let comp: AdminSidebarComponent;
@@ -170,4 +172,150 @@ describe('AdminSidebarComponent', () => {
expect(menuService.collapseMenuPreview).toHaveBeenCalled(); expect(menuService.collapseMenuPreview).toHaveBeenCalled();
})); }));
}); });
describe('menu', () => {
beforeEach(() => {
spyOn(menuService, 'addSection');
});
describe('for regular user', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
return observableOf(false);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should not show site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'admin_search', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'registries', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'registries', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'curation_tasks', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: false,
}));
});
it('should not show edit_community', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_community', visible: false,
}));
});
it('should not show edit_collection', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_collection', visible: false,
}));
});
it('should not show access control section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'access_control', visible: false,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'access_control', visible: false,
}));
});
});
describe('for site admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.AdministratorOf);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should contain site admin section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'admin_search', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'registries', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'registries', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'curation_tasks', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'workflow', visible: true,
}));
});
});
describe('for community admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.IsCommunityAdmin);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should show edit_community', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_community', visible: true,
}));
});
});
describe('for collection admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.IsCollectionAdmin);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should show edit_collection', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'edit_collection', visible: true,
}));
});
});
describe('for group admin', () => {
beforeEach(() => {
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.CanManageGroups);
});
});
beforeEach(() => {
comp.createMenu();
});
it('should show access control section', () => {
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
id: 'access_control', visible: true,
}));
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
parentID: 'access_control', visible: true,
}));
});
});
});
}); });

View File

@@ -1,6 +1,6 @@
import { Component, Injector, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest, combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { first, map, take } from 'rxjs/operators'; import { first, map, take } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { ScriptDataService } from '../../core/data/processes/script-data.service'; import { ScriptDataService } from '../../core/data/processes/script-data.service';
@@ -76,9 +76,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.createMenu(); this.createMenu();
this.createSiteAdministratorMenuSections();
this.createExportMenuSections();
this.createImportMenuSections();
super.ngOnInit(); super.ngOnInit();
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth'); this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
this.authService.isAuthenticated() this.authService.isAuthenticated()
@@ -102,192 +99,210 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
* Initialize all menu sections and items for this menu * Initialize all menu sections and items for this menu
*/ */
createMenu() { createMenu() {
const menuList = [ this.createMainMenuSections();
/* News */ this.createSiteAdministratorMenuSections();
{ this.createExportMenuSections();
id: 'new', this.createImportMenuSections();
active: false, this.createAccessControlMenuSections();
visible: true, }
model: {
type: MenuItemType.TEXT,
text: 'menu.section.new'
} as TextMenuItemModel,
icon: 'plus-circle',
index: 0
},
{
id: 'new_community',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_community',
function: () => {
this.modalService.open(CreateCommunityParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_collection',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_collection',
function: () => {
this.modalService.open(CreateCollectionParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_item',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_item',
function: () => {
this.modalService.open(CreateItemParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_process',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.new_process',
link: '/processes/new'
} as LinkMenuItemModel,
},
{
id: 'new_item_version',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.new_item_version',
link: ''
} as LinkMenuItemModel,
},
/* Edit */ /**
{ * Initialize the main menu sections.
id: 'edit', * edit_community / edit_collection is only included if the current user is a Community or Collection admin
active: false, */
visible: true, createMainMenuSections() {
model: { combineLatest([
type: MenuItemType.TEXT, this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
text: 'menu.section.edit' this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
} as TextMenuItemModel, this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
icon: 'pencil-alt', ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
index: 1 const menuList = [
}, /* News */
{ {
id: 'edit_community', id: 'new',
parentID: 'edit', active: false,
active: false, visible: true,
visible: true, model: {
model: { type: MenuItemType.TEXT,
type: MenuItemType.ONCLICK, text: 'menu.section.new'
text: 'menu.section.edit_community', } as TextMenuItemModel,
function: () => { icon: 'plus-circle',
this.modalService.open(EditCommunitySelectorComponent); index: 0
} },
} as OnClickMenuItemModel, {
}, id: 'new_community',
{ parentID: 'new',
id: 'edit_collection', active: false,
parentID: 'edit', visible: isCommunityAdmin,
active: false, model: {
visible: true, type: MenuItemType.ONCLICK,
model: { text: 'menu.section.new_community',
type: MenuItemType.ONCLICK, function: () => {
text: 'menu.section.edit_collection', this.modalService.open(CreateCommunityParentSelectorComponent);
function: () => { }
this.modalService.open(EditCollectionSelectorComponent); } as OnClickMenuItemModel,
} },
} as OnClickMenuItemModel, {
}, id: 'new_collection',
{ parentID: 'new',
id: 'edit_item', active: false,
parentID: 'edit', visible: isCommunityAdmin,
active: false, model: {
visible: true, type: MenuItemType.ONCLICK,
model: { text: 'menu.section.new_collection',
type: MenuItemType.ONCLICK, function: () => {
text: 'menu.section.edit_item', this.modalService.open(CreateCollectionParentSelectorComponent);
function: () => { }
this.modalService.open(EditItemSelectorComponent); } as OnClickMenuItemModel,
} },
} as OnClickMenuItemModel, {
}, id: 'new_item',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.new_item',
function: () => {
this.modalService.open(CreateItemParentSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'new_process',
parentID: 'new',
active: false,
visible: isCollectionAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.new_process',
link: '/processes/new'
} as LinkMenuItemModel,
},
{
id: 'new_item_version',
parentID: 'new',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.new_item_version',
link: ''
} as LinkMenuItemModel,
},
/* Curation tasks */ /* Edit */
{ {
id: 'curation_tasks', id: 'edit',
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.TEXT,
text: 'menu.section.curation_task', text: 'menu.section.edit'
link: '' } as TextMenuItemModel,
} as LinkMenuItemModel, icon: 'pencil-alt',
icon: 'filter', index: 1
index: 7 },
}, {
id: 'edit_community',
parentID: 'edit',
active: false,
visible: isCommunityAdmin,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.edit_community',
function: () => {
this.modalService.open(EditCommunitySelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'edit_collection',
parentID: 'edit',
active: false,
visible: isCollectionAdmin,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.edit_collection',
function: () => {
this.modalService.open(EditCollectionSelectorComponent);
}
} as OnClickMenuItemModel,
},
{
id: 'edit_item',
parentID: 'edit',
active: false,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.edit_item',
function: () => {
this.modalService.open(EditItemSelectorComponent);
}
} as OnClickMenuItemModel,
},
/* Statistics */ /* Curation tasks */
{ {
id: 'statistics_task', id: 'curation_tasks',
active: false, active: false,
visible: true, visible: isCollectionAdmin,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics_task', text: 'menu.section.curation_task',
link: '' link: ''
} as LinkMenuItemModel, } as LinkMenuItemModel,
icon: 'chart-bar', icon: 'filter',
index: 8 index: 7
}, },
/* Control Panel */ /* Statistics */
{ {
id: 'control_panel', id: 'statistics_task',
active: false, active: false,
visible: true, visible: true,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.control_panel', text: 'menu.section.statistics_task',
link: '' link: ''
} as LinkMenuItemModel, } as LinkMenuItemModel,
icon: 'cogs', icon: 'chart-bar',
index: 9 index: 8
}, },
/* Processes */ /* Control Panel */
{ {
id: 'processes', id: 'control_panel',
active: false, active: false,
visible: true, visible: isSiteAdmin,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.processes', text: 'menu.section.control_panel',
link: '/processes' link: ''
} as LinkMenuItemModel, } as LinkMenuItemModel,
icon: 'terminal', icon: 'cogs',
index: 10 index: 9
}, },
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { /* Processes */
shouldPersistOnRouteChange: true {
}))); id: 'processes',
active: false,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.processes',
link: '/processes'
} as LinkMenuItemModel,
icon: 'terminal',
index: 10
},
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true
})));
});
} }
/** /**
@@ -436,51 +451,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
createSiteAdministratorMenuSections() { createSiteAdministratorMenuSections() {
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => { this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
const menuList = [ const menuList = [
/* Access Control */
{
id: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.access_control'
} as TextMenuItemModel,
icon: 'key',
index: 4
},
{
id: 'access_control_people',
parentID: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_people',
link: '/admin/access-control/epeople'
} as LinkMenuItemModel,
},
{
id: 'access_control_groups',
parentID: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_groups',
link: '/admin/access-control/groups'
} as LinkMenuItemModel,
},
{
id: 'access_control_authorizations',
parentID: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_authorizations',
link: ''
} as LinkMenuItemModel,
},
/* Admin Search */ /* Admin Search */
{ {
id: 'admin_search', id: 'admin_search',
@@ -564,6 +534,65 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
}); });
} }
/**
* Create menu sections dependent on whether or not the current user can manage access control groups
*/
createAccessControlMenuSections() {
this.authorizationService.isAuthorized(FeatureID.CanManageGroups).subscribe((authorized) => {
const menuList = [
/* Access Control */
{
id: 'access_control_people',
parentID: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_people',
link: '/access-control/epeople'
} as LinkMenuItemModel,
},
{
id: 'access_control_groups',
parentID: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_groups',
link: '/access-control/groups'
} as LinkMenuItemModel,
},
{
id: 'access_control_authorizations',
parentID: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_authorizations',
link: ''
} as LinkMenuItemModel,
},
{
id: 'access_control',
active: false,
visible: authorized,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.access_control'
} as TextMenuItemModel,
icon: 'key',
index: 4
},
];
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
shouldPersistOnRouteChange: true,
})));
});
}
/** /**
* Method to change this.collapsed to false when the slide animation ends and is sliding open * Method to change this.collapsed to false when the slide animation ends and is sliding open
* @param event The animation event * @param event The animation event

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module'; import { AccessControlModule } from '../access-control/access-control.module';
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
import { AdminRegistriesModule } from './admin-registries/admin-registries.module'; import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
import { AdminRoutingModule } from './admin-routing.module'; import { AdminRoutingModule } from './admin-routing.module';
@@ -21,7 +21,7 @@ const ENTRY_COMPONENTS = [
imports: [ imports: [
AdminRoutingModule, AdminRoutingModule,
AdminRegistriesModule, AdminRegistriesModule,
AdminAccessControlModule, AccessControlModule,
AdminSearchModule.withEntryComponents(), AdminSearchModule.withEntryComponents(),
AdminWorkflowModuleModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(),
SharedModule, SharedModule,

View File

@@ -35,7 +35,7 @@
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<div class="pl-2"> <div class="pl-2">
<ds-dso-page-edit-button [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button> <ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
</div> </div>
</div> </div>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">

View File

@@ -27,6 +27,8 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface'; import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCollectionPageRoute } from './collection-page-routing-paths'; import { getCollectionPageRoute } from './collection-page-routing-paths';
@Component({ @Component({
@@ -50,6 +52,11 @@ export class CollectionPageComponent implements OnInit {
sortConfig: SortOptions sortConfig: SortOptions
}>; }>;
/**
* Whether the current user is a Community admin
*/
isCollectionAdmin$: Observable<boolean>;
/** /**
* Route to the community page * Route to the community page
*/ */
@@ -62,6 +69,7 @@ export class CollectionPageComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private authorizationDataService: AuthorizationDataService,
) { ) {
this.paginationConfig = new PaginationComponentOptions(); this.paginationConfig = new PaginationComponentOptions();
this.paginationConfig.id = 'collection-page-pagination'; this.paginationConfig.id = 'collection-page-pagination';
@@ -81,6 +89,7 @@ export class CollectionPageComponent implements OnInit {
filter((collection: Collection) => hasValue(collection)), filter((collection: Collection) => hasValue(collection)),
mergeMap((collection: Collection) => collection.logo) mergeMap((collection: Collection) => collection.logo)
); );
this.isCollectionAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCollectionAdmin);
this.paginationChanges$ = new BehaviorSubject({ this.paginationChanges$ = new BehaviorSubject({
paginationConfig: this.paginationConfig, paginationConfig: this.paginationConfig,

View File

@@ -12,6 +12,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard';
/** /**
* Routing module that handles the routing for the Edit Collection page administrator functionality * Routing module that handles the routing for the Edit Collection page administrator functionality
@@ -26,6 +27,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
}, },
data: { breadcrumbKey: 'collection.edit' }, data: { breadcrumbKey: 'collection.edit' },
component: EditCollectionPageComponent, component: EditCollectionPageComponent,
canActivate: [CollectionAdministratorGuard],
children: [ children: [
{ {
path: '', path: '',

View File

@@ -21,7 +21,7 @@
</ds-comcol-page-content> </ds-comcol-page-content>
</header> </header>
<div class="pl-2"> <div class="pl-2">
<ds-dso-page-edit-button [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button> <ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
</div> </div>
</div> </div>
<section class="comcol-page-browse-section"> <section class="comcol-page-browse-section">

View File

@@ -15,6 +15,8 @@ import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators'; import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators';
import { AuthService } from '../core/auth/auth.service'; import { AuthService } from '../core/auth/auth.service';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCommunityPageRoute } from './community-page-routing-paths'; import { getCommunityPageRoute } from './community-page-routing-paths';
@Component({ @Component({
@@ -33,6 +35,11 @@ export class CommunityPageComponent implements OnInit {
*/ */
communityRD$: Observable<RemoteData<Community>>; communityRD$: Observable<RemoteData<Community>>;
/**
* Whether the current user is a Community admin
*/
isCommunityAdmin$: Observable<boolean>;
/** /**
* The logo of this community * The logo of this community
*/ */
@@ -49,6 +56,7 @@ export class CommunityPageComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
private authorizationDataService: AuthorizationDataService
) { ) {
} }
@@ -66,6 +74,6 @@ export class CommunityPageComponent implements OnInit {
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
map((community) => getCommunityPageRoute(community.id)) map((community) => getCommunityPageRoute(community.id))
); );
this.isCommunityAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCommunityAdmin);
} }
} }

View File

@@ -10,6 +10,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard';
/** /**
* Routing module that handles the routing for the Edit Community page administrator functionality * Routing module that handles the routing for the Edit Community page administrator functionality
@@ -24,6 +25,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
}, },
data: { breadcrumbKey: 'community.edit' }, data: { breadcrumbKey: 'community.edit' },
component: EditCommunityPageComponent, component: EditCommunityPageComponent,
canActivate: [CommunityAdministratorGuard],
children: [ children: [
{ {
path: '', path: '',

View File

@@ -173,6 +173,19 @@ describe('ItemDeleteComponent', () => {
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id)); .toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
expect(comp.notify).toHaveBeenCalled(); expect(comp.notify).toHaveBeenCalled();
}); });
it('should call delete function from the ItemDataService with empty types', () => {
spyOn(comp, 'notify');
jasmine.getEnv().allowRespy(true);
spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]);
comp.ngOnInit();
comp.performAction();
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []);
expect(comp.notify).toHaveBeenCalled();
});
}); });
describe('notify', () => { describe('notify', () => {
it('should navigate to the homepage on successful deletion of the item', () => { it('should navigate to the homepage on successful deletion of the item', () => {

View File

@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators'; import {defaultIfEmpty, filter, map, switchMap, take} from 'rxjs/operators';
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { import {
@@ -121,8 +121,11 @@ export class ItemDeleteComponent
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
map((relationshipTypes) => relationshipTypes.page), map((relationshipTypes) => relationshipTypes.page),
switchMap((types) => switchMap((types) => {
combineLatest(types.map((type) => this.getRelationships(type))).pipe( if (types.length === 0) {
return observableOf(types);
}
return combineLatest(types.map((type) => this.getRelationships(type))).pipe(
map((relationships) => map((relationships) =>
types.reduce<RelationshipType[]>((includedTypes, type, index) => { types.reduce<RelationshipType[]>((includedTypes, type, index) => {
if (!includedTypes.some((includedType) => includedType.id === type.id) if (!includedTypes.some((includedType) => includedType.id === type.id)
@@ -133,8 +136,8 @@ export class ItemDeleteComponent
} }
}, []) }, [])
), ),
) );
), })
); );
} else { } else {
this.types$ = observableOf([]); this.types$ = observableOf([]);

View File

@@ -27,6 +27,10 @@ import { ResearchEntitiesModule } from '../entity-groups/research-entities/resea
import { ThemedItemPageComponent } from './simple/themed-item-page.component'; import { ThemedItemPageComponent } from './simple/themed-item-page.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
import { IIIFEntitiesModule } from '../entity-groups/iiif-entities/iiif-entities.module'; import { IIIFEntitiesModule } from '../entity-groups/iiif-entities/iiif-entities.module';
import { MediaViewerComponent } from './media-viewer/media-viewer.component';
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -53,6 +57,9 @@ const DECLARATIONS = [
ItemComponent, ItemComponent,
UploadBitstreamComponent, UploadBitstreamComponent,
AbstractIncrementalListComponent, AbstractIncrementalListComponent,
MediaViewerComponent,
MediaViewerVideoComponent,
MediaViewerImageComponent
]; ];
@NgModule({ @NgModule({
@@ -65,6 +72,7 @@ const DECLARATIONS = [
JournalEntitiesModule.withEntryComponents(), JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents(),
IIIFEntitiesModule.withEntryComponents() IIIFEntitiesModule.withEntryComponents()
NgxGalleryModule,
], ],
declarations: [ declarations: [
...DECLARATIONS ...DECLARATIONS

View File

@@ -0,0 +1,7 @@
<div [class.change-gallery]="isAuthenticated$ | async">
<ngx-gallery
class="ngx-gallery"
[options]="galleryOptions"
[images]="galleryImages"
></ngx-gallery>
</div>

View File

@@ -0,0 +1,6 @@
.ngx-gallery {
display: inline-block;
margin-bottom: 20px;
width: 340px !important;
height: 279px !important;
}

View File

@@ -0,0 +1,89 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
import { MediaViewerImageComponent } from './media-viewer-image.component';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service';
describe('MediaViewerImageComponent', () => {
let component: MediaViewerImageComponent;
let fixture: ComponentFixture<MediaViewerImageComponent>;
const authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(false)
});
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201,
content:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: observableOf(MockBitstreamFormat1),
bundleName: 'ORIGINAL',
_links: {
self: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
},
content: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
},
},
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: {
'dc.title': [
{
language: null,
value: 'test_word.docx',
},
],
},
});
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
new Array<MediaViewerItem>(),
[
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
]
);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports:[],
declarations: [MediaViewerImageComponent],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{ provide: AuthService, useValue: authService },
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MediaViewerImageComponent);
component = fixture.componentInstance;
component.galleryOptions = [new NgxGalleryOptions({})];
component.galleryImages = component.convertToGalleryImage(
mockMediaViewerItems
);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should contain a gallery options', () => {
expect(component.galleryOptions.length).toBeGreaterThan(0);
});
it('should contain an image array', () => {
expect(component.galleryImages.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,88 @@
import { Component, Input, OnInit } from '@angular/core';
import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { NgxGalleryAnimation } from '@kolkov/ngx-gallery';
import { Observable } from 'rxjs';
import { AuthService } from '../../../core/auth/auth.service';
/**
* This componenet render an image gallery for the image viewer
*/
@Component({
selector: 'ds-media-viewer-image',
templateUrl: './media-viewer-image.component.html',
styleUrls: ['./media-viewer-image.component.scss'],
})
export class MediaViewerImageComponent implements OnInit {
@Input() images: MediaViewerItem[];
@Input() preview?: boolean;
@Input() image?: string;
loggedin: boolean;
galleryOptions: NgxGalleryOptions[];
galleryImages: NgxGalleryImage[];
/**
* Whether or not the current user is authenticated
*/
isAuthenticated$: Observable<boolean>;
constructor(private authService: AuthService) {}
/**
* Thi method sets up the gallery settings and data
*/
ngOnInit(): void {
this.isAuthenticated$ = this.authService.isAuthenticated();
this.galleryOptions = [
{
preview: this.preview !== undefined ? this.preview : true,
image: true,
imageSize: 'contain',
thumbnails: false,
imageArrows: false,
startIndex: 0,
imageAnimation: NgxGalleryAnimation.Slide,
previewCloseOnEsc: true,
previewZoom: true,
previewRotate: true,
previewFullscreen: true,
},
];
if (this.image) {
this.galleryImages = [
{
small: this.image,
medium: this.image,
big: this.image,
},
];
} else {
this.galleryImages = this.convertToGalleryImage(this.images);
}
}
/**
* This method convert an array of MediaViewerItem into NgxGalleryImage array
* @param medias input NgxGalleryImage array
*/
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
const mappadImages = [];
for (const image of medias) {
if (image.format === 'image') {
mappadImages.push({
small: image.thumbnail
? image.thumbnail
: './assets/images/replacement_image.svg',
medium: image.thumbnail
? image.thumbnail
: './assets/images/replacement_image.svg',
big: image.bitstream._links.content.href,
});
}
}
return mappadImages;
}
}

View File

@@ -0,0 +1,47 @@
<video
#media
[src]="filteredMedias[currentIndex].bitstream._links.content.href"
id="singleVideo"
[poster]="
filteredMedias[currentIndex].thumbnail ||
replacements[filteredMedias[currentIndex].format]
"
preload="none"
controls
></video>
<div class="buttons" *ngIf="filteredMedias?.length > 1">
<button
class="btn btn-primary previous"
[disabled]="currentIndex === 0"
(click)="prevMedia()"
>
{{ "media-viewer.previous" | translate }}
</button>
<button
class="btn btn-primary next"
[disabled]="currentIndex === filteredMedias.length - 1"
(click)="nextMedia()"
>
{{ "media-viewer.next" | translate }}
</button>
<div ngbDropdown class="d-inline-block">
<button
class="btn btn-outline-primary playlist"
id="dropdownBasic1"
ngbDropdownToggle
>
{{ "media-viewer.playlist" | translate }}
</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button
ngbDropdownItem
*ngFor="let item of filteredMedias; index as indexOfelement"
class="list-element"
(click)="selectedMedia(indexOfelement)"
>
{{ item.bitstream.name }}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
video {
width: 340px;
height: 279px;
}

View File

@@ -0,0 +1,145 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { FileSizePipe } from '../../../shared/utils/file-size-pipe';
import { VarDirective } from '../../../shared/utils/var.directive';
import { MetadataFieldWrapperComponent } from '../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
import { MediaViewerVideoComponent } from './media-viewer-video.component';
import { By } from '@angular/platform-browser';
describe('MediaViewerVideoComponent', () => {
let component: MediaViewerVideoComponent;
let fixture: ComponentFixture<MediaViewerVideoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
BrowserAnimationsModule,
],
declarations: [
MediaViewerVideoComponent,
VarDirective,
FileSizePipe,
MetadataFieldWrapperComponent,
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201,
content:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: observableOf(MockBitstreamFormat1),
bundleName: 'ORIGINAL',
_links: {
self: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
},
content: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
},
},
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: {
'dc.title': [
{
language: null,
value: 'test_word.docx',
},
],
},
});
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
new Array<MediaViewerItem>(),
[
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
]
);
const mockMediaViewerItem: MediaViewerItem[] = Object.assign(
new Array<MediaViewerItem>(),
[{ bitstream: mockBitstream, format: 'video', thumbnail: null }]
);
beforeEach(() => {
fixture = TestBed.createComponent(MediaViewerVideoComponent);
component = fixture.componentInstance;
component.medias = mockMediaViewerItem;
component.filteredMedias = mockMediaViewerItem;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('should show controller buttons when the having mode then one video', () => {
beforeEach(() => {
component.medias = mockMediaViewerItems;
component.filteredMedias = mockMediaViewerItems;
fixture.detectChanges();
});
it('should show buttons', () => {
const controllerButtons = fixture.debugElement.query(By.css('.buttons'));
expect(controllerButtons).toBeTruthy();
});
describe('when the "Next" button is clicked', () => {
beforeEach(() => {
component.currentIndex = 0;
fixture.detectChanges();
});
it('should increase the index', () => {
const viewMore = fixture.debugElement.query(By.css('.next'));
viewMore.triggerEventHandler('click', null);
expect(component.currentIndex).toBe(1);
});
});
describe('when the "Previous" button is clicked', () => {
beforeEach(() => {
component.currentIndex = 1;
fixture.detectChanges();
});
it('should decrease the index', () => {
const viewMore = fixture.debugElement.query(By.css('.previous'));
viewMore.triggerEventHandler('click', null);
expect(component.currentIndex).toBe(0);
});
});
describe('when the "Playlist element" button is clicked', () => {
beforeEach(() => {
component.isCollapsed = true;
fixture.detectChanges();
});
it('should set the the index with the selected one', () => {
const viewMore = fixture.debugElement.query(By.css('.list-element'));
viewMore.triggerEventHandler('click', null);
expect(component.currentIndex).toBe(0);
});
});
});
});

View File

@@ -0,0 +1,55 @@
import { Component, Input, OnInit } from '@angular/core';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
/**
* This componenet renders a video viewer and playlist for the media viewer
*/
@Component({
selector: 'ds-media-viewer-video',
templateUrl: './media-viewer-video.component.html',
styleUrls: ['./media-viewer-video.component.scss'],
})
export class MediaViewerVideoComponent implements OnInit {
@Input() medias: MediaViewerItem[];
filteredMedias: MediaViewerItem[];
isCollapsed: boolean;
currentIndex = 0;
replacements = {
video: './assets/images/replacement_video.svg',
audio: './assets/images/replacement_audio.svg',
};
replacementThumbnail: string;
ngOnInit() {
this.isCollapsed = false;
this.filteredMedias = this.medias.filter(
(media) => media.format === 'audio' || media.format === 'video'
);
}
/**
* This method sets the reviced index into currentIndex
* @param index Selected index
*/
selectedMedia(index: number) {
this.currentIndex = index;
}
/**
* This method increade the number of the currentIndex
*/
nextMedia() {
this.currentIndex++;
}
/**
* This method decrese the number of the currentIndex
*/
prevMedia() {
this.currentIndex--;
}
}

View File

@@ -0,0 +1,36 @@
<ng-container *ngVar="mediaList$ | async as mediaList">
<ds-loading
*ngIf="isLoading"
message="{{ 'loading.default' | translate }}"
[showMessage]="false"
></ds-loading>
<div class="media-viewer" *ngIf="!isLoading">
<ng-container *ngIf="mediaList.length > 0">
<ng-container *ngIf="videoOptions">
<ng-container
*ngIf="
mediaList[0]?.format === 'video' || mediaList[0]?.format === 'audio'
"
>
<ds-media-viewer-video [medias]="mediaList"></ds-media-viewer-video>
</ng-container>
</ng-container>
<ng-container *ngIf="mediaList[0]?.format === 'image'">
<ds-media-viewer-image [images]="mediaList"></ds-media-viewer-image>
</ng-container>
</ng-container>
<ng-container
*ngIf="
((mediaList[0]?.format !== 'image') &&
(!videoOptions || mediaList[0]?.format !== 'video') &&
(!videoOptions || mediaList[0]?.format !== 'audio')) ||
mediaList.length === 0
"
>
<ds-media-viewer-image
[image]="mediaList[0]?.thumbnail || thumbnailPlaceholder"
[preview]="false"
></ds-media-viewer-image>
</ng-container>
</div>
</ng-container>

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,143 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Bitstream } from '../../core/shared/bitstream.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { of as observableOf } from 'rxjs';
import { By } from '@angular/platform-browser';
import { MediaViewerComponent } from './media-viewer.component';
import { MockBitstreamFormat1 } from '../../shared/mocks/item.mock';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
import { VarDirective } from '../../shared/utils/var.directive';
import { MetadataFieldWrapperComponent } from '../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
describe('MediaViewerComponent', () => {
let comp: MediaViewerComponent;
let fixture: ComponentFixture<MediaViewerComponent>;
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201,
content:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: observableOf(MockBitstreamFormat1),
bundleName: 'ORIGINAL',
_links: {
self: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
},
content: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
},
},
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: {
'dc.title': [
{
language: null,
value: 'test_word.docx',
},
],
},
});
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(
createPaginatedList([mockBitstream])
),
});
const mockMediaViewerItem: MediaViewerItem = Object.assign(
new MediaViewerItem(),
{ bitstream: mockBitstream, format: 'image', thumbnail: null }
);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
BrowserAnimationsModule,
],
declarations: [
MediaViewerComponent,
VarDirective,
FileSizePipe,
MetadataFieldWrapperComponent,
],
providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MediaViewerComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
describe('when the bitstreams are loading', () => {
beforeEach(() => {
comp.mediaList$.next([mockMediaViewerItem]);
comp.videoOptions = true;
comp.isLoading = true;
fixture.detectChanges();
});
it('should call the createMediaViewerItem', () => {
const mediaItem = comp.createMediaViewerItem(
mockBitstream,
MockBitstreamFormat1,
undefined
);
expect(mediaItem).toBeTruthy();
expect(mediaItem.thumbnail).toBe(null);
});
it('should display a loading component', () => {
const loading = fixture.debugElement.query(By.css('ds-loading'));
expect(loading.nativeElement).toBeDefined();
});
});
describe('when the bitstreams loading is failed', () => {
beforeEach(() => {
comp.mediaList$.next([]);
comp.videoOptions = true;
comp.isLoading = false;
fixture.detectChanges();
});
it('should call the createMediaViewerItem', () => {
const mediaItem = comp.createMediaViewerItem(
mockBitstream,
MockBitstreamFormat1,
undefined
);
expect(mediaItem).toBeTruthy();
expect(mediaItem.thumbnail).toBe(null);
});
it('should display a default, thumbnail', () => {
const defaultThumbnail = fixture.debugElement.query(
By.css('ds-media-viewer-image')
);
expect(defaultThumbnail.nativeElement).toBeDefined();
});
});
});

View File

@@ -0,0 +1,114 @@
import { Component, Input, OnInit } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, take } from 'rxjs/operators';
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
import { Bitstream } from '../../core/shared/bitstream.model';
import { Item } from '../../core/shared/item.model';
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { hasValue } from '../../shared/empty.util';
import { followLink } from '../../shared/utils/follow-link-config.model';
/**
* This componenet renders the media viewers
*/
@Component({
selector: 'ds-media-viewer',
templateUrl: './media-viewer.component.html',
styleUrls: ['./media-viewer.component.scss'],
})
export class MediaViewerComponent implements OnInit {
@Input() item: Item;
@Input() videoOptions: boolean;
mediaList$: BehaviorSubject<MediaViewerItem[]>;
isLoading: boolean;
thumbnailPlaceholder = './assets/images/replacement_document.svg';
constructor(protected bitstreamDataService: BitstreamDataService) {}
/**
* This metod loads all the Bitstreams and Thumbnails and contert it to media item
*/
ngOnInit(): void {
this.mediaList$ = new BehaviorSubject([]);
this.isLoading = true;
this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => {
if (bitstreamsRD.payload.page.length === 0) {
this.isLoading = false;
this.mediaList$.next([]);
} else {
this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => {
for (
let index = 0;
index < bitstreamsRD.payload.page.length;
index++
) {
bitstreamsRD.payload.page[index].format
.pipe(getFirstSucceededRemoteDataPayload())
.subscribe((format) => {
const current = this.mediaList$.getValue();
const mediaItem = this.createMediaViewerItem(
bitstreamsRD.payload.page[index],
format,
thumbnailsRD.payload && thumbnailsRD.payload.page[index]
);
this.mediaList$.next([...current, mediaItem]);
});
}
this.isLoading = false;
});
}
});
}
/**
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
* @param bundleName Bundle name
*/
loadRemoteData(
bundleName: string
): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.bitstreamDataService
.findAllByItemAndBundleName(
this.item,
bundleName,
{},
true,
true,
followLink('format')
)
.pipe(
filter(
(bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) =>
hasValue(bitstreamsRD) &&
(hasValue(bitstreamsRD.errorMessage) || hasValue(bitstreamsRD.payload))
),
take(1)
);
}
/**
* This method create MediaViewerItem from incoming bitstreams
* @param original original remote data bitstream
* @param format original bitstream format
* @param thumbnail trunbnail remote data bitstream
*/
createMediaViewerItem(
original: Bitstream,
format: BitstreamFormat,
thumbnail: Bitstream
): MediaViewerItem {
const mediaItem = new MediaViewerItem();
mediaItem.bitstream = original;
mediaItem.format = format.mimetype.split('/')[0];
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
return mediaItem;
}
}

View File

@@ -8,9 +8,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ng-container *ngIf="!mediaViewer.image">
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> <ds-metadata-field-wrapper>
</ds-metadata-field-wrapper> <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
</ng-container>
<ng-container *ngIf="mediaViewer.image">
<ds-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-media-viewer>
</ng-container>
<ds-item-page-file-section [item]="object"></ds-item-page-file-section> <ds-item-page-file-section [item]="object"></ds-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-item-page-author-field [item]="object"></ds-item-page-author-field> <ds-item-page-author-field [item]="object"></ds-item-page-author-field>

View File

@@ -1,5 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { environment } from '../../../../../environments/environment';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
@@ -20,6 +21,7 @@ export class ItemComponent implements OnInit {
* Route to the item page * Route to the item page
*/ */
itemPageRoute: string; itemPageRoute: string;
mediaViewer = environment.mediaViewer;
constructor(protected bitstreamDataService: BitstreamDataService) { constructor(protected bitstreamDataService: BitstreamDataService) {
} }

View File

@@ -8,9 +8,14 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ng-container *ngIf="!mediaViewer.image">
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> <ds-metadata-field-wrapper>
</ds-metadata-field-wrapper> <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
</ng-container>
<ng-container *ngIf="mediaViewer.image">
<ds-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-media-viewer>
</ng-container>
<ds-item-page-file-section [item]="object"></ds-item-page-file-section> <ds-item-page-file-section [item]="object"></ds-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-item-page-author-field [item]="object"></ds-item-page-author-field> <ds-item-page-author-field [item]="object"></ds-item-page-author-field>

View File

@@ -1,5 +1,5 @@
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAccessControlModuleRoute } from '../admin-routing-paths'; import { getAccessControlModuleRoute } from '../app-routing-paths';
export const GROUP_EDIT_PATH = 'groups'; export const GROUP_EDIT_PATH = 'groups';

View File

@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { GroupFormComponent } from './group-registry/group-form/group-form.component'; import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths'; import { GROUP_EDIT_PATH } from './access-control-routing-paths';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -26,6 +26,6 @@ import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths';
/** /**
* Routing module for the AccessControl section of the admin sidebar * Routing module for the AccessControl section of the admin sidebar
*/ */
export class AdminAccessControlRoutingModule { export class AccessControlRoutingModule {
} }

View File

@@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module'; import { AccessControlRoutingModule } from './access-control-routing.module';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
import { GroupFormComponent } from './group-registry/group-form/group-form.component'; import { GroupFormComponent } from './group-registry/group-form/group-form.component';
@@ -15,7 +15,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
CommonModule, CommonModule,
SharedModule, SharedModule,
RouterModule, RouterModule,
AdminAccessControlRoutingModule AccessControlRoutingModule
], ],
declarations: [ declarations: [
EPeopleRegistryComponent, EPeopleRegistryComponent,
@@ -29,6 +29,6 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
/** /**
* This module handles all components related to the access control pages * This module handles all components related to the access control pages
*/ */
export class AdminAccessControlModule { export class AccessControlModule {
} }

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { type } from '../../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
/** /**
* For each action type in an action group, make a simple * For each action type in an action group, make a simple

View File

@@ -7,24 +7,24 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { FindListOptions } from '../../../core/data/request.models'; import { FindListOptions } from '../../core/data/request.models';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EPeopleRegistryComponent } from './epeople-registry.component'; import { EPeopleRegistryComponent } from './epeople-registry.component';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub'; import { RouterStub } from '../../shared/testing/router.stub';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
describe('EPeopleRegistryComponent', () => { describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent; let component: EPeopleRegistryComponent;
@@ -107,7 +107,7 @@ describe('EPeopleRegistryComponent', () => {
// empty // empty
}, },
getEPeoplePageRouterLink(): string { getEPeoplePageRouterLink(): string {
return '/admin/access-control/epeople'; return '/access-control/epeople';
} }
}; };
authorizationService = jasmine.createSpyObj('authorizationService', { authorizationService = jasmine.createSpyObj('authorizationService', {

View File

@@ -4,25 +4,25 @@ import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { EpersonDtoModel } from '../../../core/eperson/models/eperson-dto.model'; import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getAllSucceededRemoteData getAllSucceededRemoteData
} from '../../../core/shared/operators'; } from '../../core/shared/operators';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
@Component({ @Component({
selector: 'ds-epeople-registry', selector: 'ds-epeople-registry',

View File

@@ -1,6 +1,6 @@
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions'; import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers'; import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
import { EPersonMock } from '../../../shared/testing/eperson.mock'; import { EPersonMock } from '../../shared/testing/eperson.mock';
const initialState: EPeopleRegistryState = { const initialState: EPeopleRegistryState = {
editEPerson: null, editEPerson: null,

View File

@@ -1,4 +1,4 @@
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { import {
EPeopleRegistryAction, EPeopleRegistryAction,
EPeopleRegistryActionTypes, EPeopleRegistryActionTypes,

View File

@@ -6,26 +6,26 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { FindListOptions } from '../../../../core/data/request.models'; import { FindListOptions } from '../../../core/data/request.models';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EPersonFormComponent } from './eperson-form.component'; import { EPersonFormComponent } from './eperson-form.component';
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { AuthService } from '../../../../core/auth/auth.service'; import { AuthService } from '../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
describe('EPersonFormComponent', () => { describe('EPersonFormComponent', () => {
let component: EPersonFormComponent; let component: EPersonFormComponent;

View File

@@ -9,28 +9,28 @@ import {
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { combineLatest, Observable, of, Subscription } from 'rxjs'; import { combineLatest, Observable, of, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { switchMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { import {
getRemoteDataPayload, getRemoteDataPayload,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstCompletedRemoteData getFirstCompletedRemoteData
} from '../../../../core/shared/operators'; } from '../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { AuthService } from '../../../../core/auth/auth.service'; import { AuthService } from '../../../core/auth/auth.service';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { NoContent } from '../../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
@Component({ @Component({
selector: 'ds-eperson-form', selector: 'ds-eperson-form',

View File

@@ -9,30 +9,30 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../../../core/data/dso-change-analyzer.service';
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { UUIDService } from '../../../../core/shared/uuid.service'; import { UUIDService } from '../../../core/shared/uuid.service';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
import { GroupFormComponent } from './group-form.component'; import { GroupFormComponent } from './group-form.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
import { RouterMock } from '../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../shared/mocks/router.mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
describe('GroupFormComponent', () => { describe('GroupFormComponent', () => {
@@ -75,7 +75,7 @@ describe('GroupFormComponent', () => {
return observableOf(this.activeGroup); return observableOf(this.activeGroup);
}, },
getGroupRegistryRouterLink(): string { getGroupRegistryRouterLink(): string {
return '/admin/access-control/groups'; return '/access-control/groups';
}, },
editGroup(group: Group) { editGroup(group: Group) {
this.activeGroup = group; this.activeGroup = group;

View File

@@ -17,32 +17,32 @@ import {
Subscription Subscription
} from 'rxjs'; } from 'rxjs';
import { catchError, map, switchMap, take } from 'rxjs/operators'; import { catchError, map, switchMap, take } from 'rxjs/operators';
import { getCollectionEditRolesRoute } from '../../../../+collection-page/collection-page-routing-paths'; import { getCollectionEditRolesRoute } from '../../../+collection-page/collection-page-routing-paths';
import { getCommunityEditRolesRoute } from '../../../../+community-page/community-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../+community-page/community-page-routing-paths';
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { import {
getRemoteDataPayload, getRemoteDataPayload,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstCompletedRemoteData getFirstCompletedRemoteData
} from '../../../../core/shared/operators'; } from '../../../core/shared/operators';
import { AlertType } from '../../../../shared/alert/aletr-type'; import { AlertType } from '../../../shared/alert/aletr-type';
import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue, isNotEmpty, hasValueOperator } from '../../../../shared/empty.util'; import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
@Component({ @Component({

View File

@@ -7,25 +7,25 @@ import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { RestResponse } from '../../../../../core/cache/response.models'; import { RestResponse } from '../../../../core/cache/response.models';
import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { EPerson } from '../../../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
import { MembersListComponent } from './members-list.component'; import { MembersListComponent } from './members-list.component';
import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson.mock'; import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { RouterMock } from '../../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock';
describe('MembersListComponent', () => { describe('MembersListComponent', () => {
let component: MembersListComponent; let component: MembersListComponent;
@@ -66,7 +66,7 @@ describe('MembersListComponent', () => {
// empty // empty
}, },
getEPeoplePageRouterLink(): string { getEPeoplePageRouterLink(): string {
return '/admin/access-control/epeople'; return '/access-control/epeople';
} }
}; };
groupsDataServiceStub = { groupsDataServiceStub = {
@@ -97,7 +97,7 @@ describe('MembersListComponent', () => {
// empty // empty
}, },
getGroupEditPageRouterLink(group: Group): string { getGroupEditPageRouterLink(group: Group): string {
return '/admin/access-control/groups/' + group.id; return '/access-control/groups/' + group.id;
}, },
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> { deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {

View File

@@ -11,21 +11,20 @@ import {
ObservedValueOf, ObservedValueOf,
} from 'rxjs'; } from 'rxjs';
import { map, mergeMap, switchMap, take } from 'rxjs/operators'; import { map, mergeMap, switchMap, take } from 'rxjs/operators';
import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { EPerson } from '../../../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { import {
getRemoteDataPayload, getRemoteDataPayload,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstCompletedRemoteData, getFirstCompletedRemoteData, getAllCompletedRemoteData
getAllCompletedRemoteData } from '../../../../core/shared/operators';
} from '../../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; import {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model';
import { EpersonDtoModel } from '../../../../../core/eperson/models/eperson-dto.model';
/** /**
* Keys to keep track of specific subscriptions * Keys to keep track of specific subscriptions

View File

@@ -15,25 +15,25 @@ import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf, BehaviorSubject } from 'rxjs'; import { Observable, of as observableOf, BehaviorSubject } from 'rxjs';
import { RestResponse } from '../../../../../core/cache/response.models'; import { RestResponse } from '../../../../core/cache/response.models';
import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { GroupDataService } from '../../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
import { SubgroupsListComponent } from './subgroups-list.component'; import { SubgroupsListComponent } from './subgroups-list.component';
import { import {
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
createSuccessfulRemoteDataObject createSuccessfulRemoteDataObject
} from '../../../../../shared/remote-data.utils'; } from '../../../../shared/remote-data.utils';
import { RouterMock } from '../../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
describe('SubgroupsListComponent', () => { describe('SubgroupsListComponent', () => {
@@ -70,7 +70,7 @@ describe('SubgroupsListComponent', () => {
); );
}, },
getGroupEditPageRouterLink(group: Group): string { getGroupEditPageRouterLink(group: Group): string {
return '/admin/access-control/groups/' + group.id; return '/access-control/groups/' + group.id;
}, },
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> { searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') { if (query === '') {

View File

@@ -4,18 +4,18 @@ import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs'; import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators'; import { map, mergeMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { GroupDataService } from '../../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { import {
getRemoteDataPayload, getRemoteDataPayload,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstCompletedRemoteData getFirstCompletedRemoteData
} from '../../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { NoContent } from '../../../../../core/shared/NoContent.model'; import { NoContent } from '../../../../core/shared/NoContent.model';
/** /**
* Keys to keep track of specific subscriptions * Keys to keep track of specific subscriptions

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../core/eperson/models/group.model';
import { type } from '../../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
/** /**
* For each action type in an action group, make a simple * For each action type in an action group, make a simple

View File

@@ -1,4 +1,4 @@
import { GroupMock } from '../../../shared/testing/group-mock'; import { GroupMock } from '../../shared/testing/group-mock';
import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions'; import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions';
import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers'; import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers';

View File

@@ -1,4 +1,4 @@
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../core/eperson/models/group.model';
import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions'; import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions';
/** /**

View File

@@ -7,27 +7,27 @@ import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../core/eperson/models/group.model';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock';
import { GroupsRegistryComponent } from './groups-registry.component'; import { GroupsRegistryComponent } from './groups-registry.component';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { routeServiceStub } from '../../../shared/testing/route-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { RouterMock } from '../../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
describe('GroupRegistryComponent', () => { describe('GroupRegistryComponent', () => {
let component: GroupsRegistryComponent; let component: GroupsRegistryComponent;
@@ -98,10 +98,10 @@ describe('GroupRegistryComponent', () => {
} }
}, },
getGroupEditPageRouterLink(group: Group): string { getGroupEditPageRouterLink(group: Group): string {
return '/admin/access-control/groups/' + group.id; return '/access-control/groups/' + group.id;
}, },
getGroupRegistryRouterLink(): string { getGroupRegistryRouterLink(): string {
return '/admin/access-control/groups'; return '/access-control/groups';
}, },
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> { searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') { if (query === '') {

View File

@@ -10,29 +10,29 @@ import {
of as observableOf of as observableOf
} from 'rxjs'; } from 'rxjs';
import { catchError, map, switchMap, take } from 'rxjs/operators'; import { catchError, map, switchMap, take } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model'; import { GroupDtoModel } from '../../core/eperson/models/group-dto.model';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../core/eperson/models/group.model';
import { RouteService } from '../../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { import {
getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteData getFirstSucceededRemoteData
} from '../../../core/shared/operators'; } from '../../core/shared/operators';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
@Component({ @Component({
selector: 'ds-groups-registry', selector: 'ds-groups-registry',

View File

@@ -74,3 +74,9 @@ export const INFO_MODULE_PATH = 'info';
export function getInfoModulePath() { export function getInfoModulePath() {
return `/${INFO_MODULE_PATH}`; return `/${INFO_MODULE_PATH}`;
} }
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
export function getAccessControlModuleRoute() {
return `/${ACCESS_CONTROL_MODULE_PATH}`;
}

View File

@@ -4,7 +4,17 @@ import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
import { AuthenticatedGuard } from './core/auth/authenticated.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.guard';
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { ADMIN_MODULE_PATH, BITSTREAM_MODULE_PATH, FORBIDDEN_PATH, FORGOT_PASSWORD_PATH, INFO_MODULE_PATH, PROFILE_MODULE_PATH, REGISTER_PATH, WORKFLOW_ITEM_MODULE_PATH } from './app-routing-paths'; import {
ACCESS_CONTROL_MODULE_PATH,
ADMIN_MODULE_PATH,
BITSTREAM_MODULE_PATH,
FORBIDDEN_PATH,
FORGOT_PASSWORD_PATH,
INFO_MODULE_PATH,
PROFILE_MODULE_PATH,
REGISTER_PATH,
WORKFLOW_ITEM_MODULE_PATH,
} from './app-routing-paths';
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths'; import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths'; import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
@@ -14,6 +24,7 @@ import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -171,6 +182,11 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component
loadChildren: () => import('./statistics-page/statistics-page-routing.module') loadChildren: () => import('./statistics-page/statistics-page-routing.module')
.then((m) => m.StatisticsPageRoutingModule), .then((m) => m.StatisticsPageRoutingModule),
}, },
{
path: ACCESS_CONTROL_MODULE_PATH,
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
canActivate: [GroupAdministratorGuard],
},
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
]} ]}
],{ ],{

View File

@@ -3,11 +3,11 @@ import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store'
import { import {
ePeopleRegistryReducer, ePeopleRegistryReducer,
EPeopleRegistryState EPeopleRegistryState
} from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; } from './access-control/epeople-registry/epeople-registry.reducers';
import { import {
groupRegistryReducer, groupRegistryReducer,
GroupRegistryState GroupRegistryState
} from './+admin/admin-access-control/group-registry/group-registry.reducers'; } from './access-control/group-registry/group-registry.reducers';
import { import {
metadataRegistryReducer, metadataRegistryReducer,
MetadataRegistryState MetadataRegistryState

View File

@@ -102,7 +102,7 @@ export class AuthInterceptor implements HttpInterceptor {
private parseLocation(header: string): string { private parseLocation(header: string): string {
let location = header.trim(); let location = header.trim();
location = location.replace('location="', ''); location = location.replace('location="', '');
location = location.replace('"', ''); location = location.replace('"', ''); /* lgtm [js/incomplete-sanitization] */
let re = /%3A%2F%2F/g; let re = /%3A%2F%2F/g;
location = location.replace(re, '://'); location = location.replace(re, '://');
re = /%3A/g; re = /%3A/g;

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../feature-id';
/**
* Prevent unauthorized activating and loading of routes when the current authenticated user
* isn't a Collection administrator
*/
@Injectable({
providedIn: 'root'
})
export class CollectionAdministratorGuard extends FeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
/**
* Check group management rights
*/
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
return observableOf(FeatureID.IsCollectionAdmin);
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../feature-id';
/**
* Prevent unauthorized activating and loading of routes when the current authenticated user
* isn't a Community administrator
*/
@Injectable({
providedIn: 'root'
})
export class CommunityAdministratorGuard extends FeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
/**
* Check group management rights
*/
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
return observableOf(FeatureID.IsCommunityAdmin);
}
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
import { AuthorizationDataService } from '../authorization-data.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../../../auth/auth.service';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../feature-id';
/**
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
* management rights
*/
@Injectable({
providedIn: 'root'
})
export class GroupAdministratorGuard extends FeatureAuthorizationGuard {
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
super(authorizationService, router, authService);
}
/**
* Check group management rights
*/
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
return observableOf(FeatureID.CanManageGroups);
}
}

View File

@@ -9,4 +9,7 @@ export enum FeatureID {
WithdrawItem = 'withdrawItem', WithdrawItem = 'withdrawItem',
ReinstateItem = 'reinstateItem', ReinstateItem = 'reinstateItem',
EPersonRegistration = 'epersonRegistration', EPersonRegistration = 'epersonRegistration',
CanManageGroups = 'canManageGroups',
IsCollectionAdmin = 'isCollectionAdmin',
IsCommunityAdmin = 'isCommunityAdmin',
} }

View File

@@ -10,7 +10,7 @@ import { TestScheduler } from 'rxjs/testing';
import { import {
EPeopleRegistryCancelEPersonAction, EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction EPeopleRegistryEditEPersonAction
} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; } from '../../access-control/epeople-registry/epeople-registry.actions';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ChangeAnalyzer } from '../data/change-analyzer'; import { ChangeAnalyzer } from '../data/change-analyzer';

View File

@@ -7,8 +7,8 @@ import { find, map, take } from 'rxjs/operators';
import { import {
EPeopleRegistryCancelEPersonAction, EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction EPeopleRegistryEditEPersonAction
} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; } from '../../access-control/epeople-registry/epeople-registry.actions';
import { EPeopleRegistryState } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { hasValue, hasNoValue } from '../../shared/empty.util'; import { hasValue, hasNoValue } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
@@ -269,7 +269,7 @@ export class EPersonDataService extends DataService<EPerson> {
this.editEPerson(ePerson); this.editEPerson(ePerson);
} }
}); });
return '/admin/access-control/epeople'; return '/access-control/epeople';
} }
/** /**
@@ -277,7 +277,7 @@ export class EPersonDataService extends DataService<EPerson> {
* @param ePerson New EPerson to edit * @param ePerson New EPerson to edit
*/ */
public getEPeoplePageRouterLink(): string { public getEPeoplePageRouterLink(): string {
return '/admin/access-control/epeople'; return '/access-control/epeople';
} }
/** /**

View File

@@ -8,7 +8,7 @@ import { compare, Operation } from 'fast-json-patch';
import { import {
GroupRegistryCancelGroupAction, GroupRegistryCancelGroupAction,
GroupRegistryEditGroupAction GroupRegistryEditGroupAction
} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; } from '../../access-control/group-registry/group-registry.actions';
import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';

View File

@@ -7,8 +7,8 @@ import { filter, map, take } from 'rxjs/operators';
import { import {
GroupRegistryCancelGroupAction, GroupRegistryCancelGroupAction,
GroupRegistryEditGroupAction GroupRegistryEditGroupAction
} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; } from '../../access-control/group-registry/group-registry.actions';
import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers'; import { GroupRegistryState } from '../../access-control/group-registry/group-registry.reducers';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
@@ -209,7 +209,7 @@ export class GroupDataService extends DataService<Group> {
} }
public getGroupRegistryRouterLink(): string { public getGroupRegistryRouterLink(): string {
return '/admin/access-control/groups'; return '/access-control/groups';
} }
/** /**
@@ -240,7 +240,7 @@ export class GroupDataService extends DataService<Group> {
* @param groupID Group ID we want edit page for * @param groupID Group ID we want edit page for
*/ */
public getGroupEditPageRouterLinkWithID(groupId: string): string { public getGroupEditPageRouterLinkWithID(groupId: string): string {
return '/admin/access-control/groups/' + groupId; return '/access-control/groups/' + groupId;
} }
/** /**

View File

@@ -326,6 +326,25 @@ describe('RegistryService', () => {
}); });
}); });
describe('when createMetadataField is called with a blank qualifier', () => {
let result: Observable<MetadataField>;
let metadataField: MetadataField;
beforeEach(() => {
metadataField = mockFieldsList[0];
metadataField.qualifier = '';
result = registryService.createMetadataField(metadataField, mockSchemasList[0]);
});
it('should return the created metadata field with a null qualifier', (done) => {
metadataField.qualifier = null;
result.subscribe((field: MetadataField) => {
expect(field).toEqual(metadataField);
done();
});
});
});
describe('when updateMetadataField is called', () => { describe('when updateMetadataField is called', () => {
let result: Observable<MetadataField>; let result: Observable<MetadataField>;
@@ -341,6 +360,25 @@ describe('RegistryService', () => {
}); });
}); });
describe('when updateMetadataField is called with a blank qualifier', () => {
let result: Observable<MetadataField>;
let metadataField: MetadataField;
beforeEach(() => {
metadataField = mockFieldsList[0];
metadataField.qualifier = '';
result = registryService.updateMetadataField(metadataField);
});
it('should return the updated metadata field with a null qualifier', (done) => {
metadataField.qualifier = null;
result.subscribe((field: MetadataField) => {
expect(field).toEqual(metadataField);
done();
});
});
});
describe('when deleteMetadataSchema is called', () => { describe('when deleteMetadataSchema is called', () => {
let result: Observable<RemoteData<NoContent>>; let result: Observable<RemoteData<NoContent>>;

View File

@@ -245,6 +245,9 @@ export class RegistryService {
* @param schema The MetadataSchema to create the field in * @param schema The MetadataSchema to create the field in
*/ */
public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable<MetadataField> { public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable<MetadataField> {
if (!field.qualifier) {
field.qualifier = null;
}
return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe( return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe(
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
hasValueOperator(), hasValueOperator(),
@@ -260,6 +263,9 @@ export class RegistryService {
* @param field The MetadataField to update * @param field The MetadataField to update
*/ */
public updateMetadataField(field: MetadataField): Observable<MetadataField> { public updateMetadataField(field: MetadataField): Observable<MetadataField> {
if (!field.qualifier) {
field.qualifier = null;
}
return this.metadataFieldService.put(field).pipe( return this.metadataFieldService.put(field).pipe(
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
hasValueOperator(), hasValueOperator(),

View File

@@ -0,0 +1,21 @@
import { Bitstream } from './bitstream.model';
/**
* Model representing a media viewer item
*/
export class MediaViewerItem {
/**
* Incoming Bitsream
*/
bitstream: Bitstream;
/**
* Incoming Bitsream format type
*/
format: string;
/**
* Incoming Bitsream thumbnail
*/
thumbnail: string;
}

View File

@@ -20,6 +20,7 @@ const dcTitle0 = mdValue('Title 0');
const dcTitle1 = mdValue('Title 1'); const dcTitle1 = mdValue('Title 1');
const dcTitle2 = mdValue('Title 2', 'en_US'); const dcTitle2 = mdValue('Title 2', 'en_US');
const bar = mdValue('Bar'); const bar = mdValue('Bar');
const test = mdValue('Test');
const singleMap = { 'dc.title': [dcTitle0] }; const singleMap = { 'dc.title': [dcTitle0] };
@@ -30,6 +31,11 @@ const multiMap = {
'foo': [bar] 'foo': [bar]
}; };
const regexTestMap = {
'foolbar.baz': [test],
'foo.bard': [test],
};
const multiViewModelList = [ const multiViewModelList = [
{ key: 'dc.description', ...dcDescription, order: 0 }, { key: 'dc.description', ...dcDescription, order: 0 },
{ key: 'dc.description.abstract', ...dcAbstract, order: 0 }, { key: 'dc.description.abstract', ...dcAbstract, order: 0 },
@@ -98,6 +104,9 @@ describe('Metadata', () => {
testAll([multiMap, singleMap], 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]); testAll([multiMap, singleMap], 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]);
testAll([multiMap, singleMap], ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]); testAll([multiMap, singleMap], ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]);
}); });
describe('with regexTestMap', () => {
testAll(regexTestMap, 'foo.bar.*', []);
});
}); });
describe('allValues method', () => { describe('allValues method', () => {

View File

@@ -156,7 +156,7 @@ export class Metadata {
const outputKeys: string[] = []; const outputKeys: string[] = [];
for (const inputKey of inputKeys) { for (const inputKey of inputKeys) {
if (inputKey.includes('*')) { if (inputKey.includes('*')) {
const inputKeyRegex = new RegExp('^' + inputKey.replace('.', '\.').replace('*', '.*') + '$'); const inputKeyRegex = new RegExp('^' + inputKey.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
for (const mapKey of Object.keys(mdMap)) { for (const mapKey of Object.keys(mdMap)) {
if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) { if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) {
outputKeys.push(mapKey); outputKeys.push(mapKey);

View File

@@ -9,7 +9,7 @@ import { getAllCompletedRemoteData, getFirstCompletedRemoteData } from '../../..
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../../core/data/request.service';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { HALLink } from '../../../../core/shared/hal-link.model'; import { HALLink } from '../../../../core/shared/hal-link.model';
import { getGroupEditRoute } from '../../../../+admin/admin-access-control/admin-access-control-routing-paths'; import { getGroupEditRoute } from '../../../../access-control/access-control-routing-paths';
import { hasNoValue, hasValue } from '../../../empty.util'; import { hasNoValue, hasValue } from '../../../empty.util';
import { NoContent } from '../../../../core/shared/NoContent.model'; import { NoContent } from '../../../../core/shared/NoContent.model';

View File

@@ -11,12 +11,15 @@
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}"> 'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
<div [ngClass]="getClass('grid', 'control')"> <div [ngClass]="getClass('grid', 'control')">
<ng-container #componentViewContainer></ng-container> <ng-container #componentViewContainer></ng-container>
<small *ngIf="hasHint && (model.repeatable === false || context?.index === 0) && (!showErrorMessages || errorMessages.length === 0)" <small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small> class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null
&& (!showErrorMessages || errorMessages.length === 0)" class="clearfix w-100 mb-2"></div>
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]"> <div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small> <small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
</div> </div>
</div> </div>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" > <div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
@@ -32,12 +35,12 @@
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option> <option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select> </select>
</div> </div>
<div *ngIf="isRelationship" class="col-auto text-center" [class.invisible]="context?.index > 0"> <div *ngIf="isRelationship && !isVirtual()" class="col-auto text-center">
<button class="btn btn-secondary" <button class="btn btn-secondary"
type="button" type="button"
ngbTooltip="{{'form.lookup-help' | translate}}" ngbTooltip="{{'form.lookup-help' | translate}}"
placement="top" placement="top"
(click)="openLookup(); $event.stopPropagation();">{{'form.lookup' | translate}} (click)="openLookup(); $event.stopPropagation();"><i class="fa fa-search"></i>
</button> </button>
</div> </div>
</div> </div>
@@ -65,6 +68,9 @@
[relationshipOptions]="model.relationship" [relationshipOptions]="model.relationship"
> >
</ds-existing-relation-list-element> </ds-existing-relation-list-element>
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<div class="clearfix w-100 mb-2"></div>
</ng-container> </ng-container>
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@@ -37,7 +37,6 @@ import {
DynamicFormControl, DynamicFormControl,
DynamicFormControlContainerComponent, DynamicFormControlContainerComponent,
DynamicFormControlEvent, DynamicFormControlEvent,
DynamicFormControlEventType,
DynamicFormControlModel, DynamicFormControlModel,
DynamicFormLayout, DynamicFormLayout,
DynamicFormLayoutService, DynamicFormLayoutService,
@@ -91,10 +90,10 @@ import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-di
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
import { import {
getAllSucceededRemoteData, getAllSucceededRemoteData,
getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getPaginatedListPayload, getPaginatedListPayload,
getRemoteDataPayload, getRemoteDataPayload
getFirstSucceededRemoteData
} from '../../../../core/shared/operators'; } from '../../../../core/shared/operators';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
@@ -374,6 +373,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
} }
} }
hasRelationship() {
return isNotEmpty(this.model) && this.model.hasOwnProperty('relationship') && isNotEmpty(this.model.relationship);
}
isVirtual() {
const value: FormFieldMetadataValueObject = this.model.metadataValue;
return isNotEmpty(value) && value.isVirtual;
}
public hasResultsSelected(): Observable<boolean> { public hasResultsSelected(): Observable<boolean> {
return this.model.value.pipe(map((list: SearchResult<DSpaceObject>[]) => isNotEmpty(list))); return this.model.value.pipe(map((list: SearchResult<DSpaceObject>[]) => isNotEmpty(list)));
} }
@@ -385,6 +393,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, { this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
size: 'lg' size: 'lg'
}); });
if (hasValue(this.model.value)) {
this.submissionService.dispatchSave(this.model.submissionId);
}
const modalComp = this.modalRef.componentInstance; const modalComp = this.modalRef.componentInstance;
if (hasValue(this.model.value) && !this.model.readOnly) { if (hasValue(this.model.value) && !this.model.readOnly) {
@@ -395,18 +408,6 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
} }
} }
if (hasValue(this.model.value)) {
this.model.value = '';
this.onChange({
$event: { previousIndex: 0 },
context: { index: 0 },
control: this.control,
model: this.model,
type: DynamicFormControlEventType.Change
});
}
this.submissionService.dispatchSave(this.model.submissionId);
modalComp.repeatable = this.model.repeatable; modalComp.repeatable = this.model.repeatable;
modalComp.listId = this.listId; modalComp.listId = this.listId;
modalComp.relationshipOptions = this.model.relationship; modalComp.relationshipOptions = this.model.relationship;
@@ -437,6 +438,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
.forEach((sub) => sub.unsubscribe()); .forEach((sub) => sub.unsubscribe());
} }
get hasHint(): boolean {
return isNotEmpty(this.model.hint) && this.model.hint !== '&nbsp;';
}
/** /**
* Initialize this.item$ based on this.model.submissionId * Initialize this.item$ based on this.model.submissionId
*/ */

View File

@@ -8,6 +8,7 @@
</ng-container> </ng-container>
</span> </span>
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary"
title="{{'form.remove' | translate}}"
(click)="removeSelection()"> (click)="removeSelection()">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>

View File

@@ -14,6 +14,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../../../testing/translate-loader.mock';
describe('ExistingMetadataListElementComponent', () => { describe('ExistingMetadataListElementComponent', () => {
let component: ExistingMetadataListElementComponent; let component: ExistingMetadataListElementComponent;
@@ -65,6 +67,14 @@ describe('ExistingMetadataListElementComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
init(); init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})
],
declarations: [ExistingMetadataListElementComponent], declarations: [ExistingMetadataListElementComponent],
providers: [ providers: [
{ provide: SelectableListService, useValue: selectionService }, { provide: SelectableListService, useValue: selectionService },

View File

@@ -2,44 +2,42 @@
<div [id]="id" <div [id]="id"
[formArrayName]="model.id" [formArrayName]="model.id"
[ngClass]="getClass('element', 'control')"> [ngClass]="getClass('element', 'control')">
<div role="group"
formGroupName="0" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]"> <!-- Draggable Container -->
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model.groups[0]"></ng-container> <div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
<ng-container *ngTemplateOutlet="controlContainer; context: {$implicit: 0}"></ng-container> <!-- Draggable Items -->
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model.groups[0]"></ng-container> <div *ngFor="let groupModel of model.groups; let idx = index"
</div> role="group"
<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)"> [formGroupName]="idx"
<div *ngFor="let groupModel of model.groups; let idx = index" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]"
[ngClass]="{'pt-2 pb-2': idx > 0}" cdkDrag cdkDragHandle> cdkDrag
<div [formGroupName]="idx" cdkDragHandle
[class]="getClass('element', 'group') + ' ' + getClass('grid', 'group')" [cdkDragDisabled]="dragDisabled"
[ngClass]="{'d-flex align-items-center': idx > 0}" [cdkDragPreviewClass]="'ds-submission-reorder-dragging'">
> <!-- Item content -->
<ng-container *ngIf="idx > 0"> <i class="drag-icon fas fa-grip-vertical fa-fw" [class.invisible]="dragDisabled"></i>
<i class="drag-icon fas fa-grip-vertical fa-fw"></i> <ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container> <ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"
<ng-container *ngTemplateOutlet="controlContainer; context: {$implicit: idx}"></ng-container> [bindId]="false"
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container> [formGroup]="group"
</ng-container> [context]="groupModel"
</div> [group]="control.get([idx])"
</div> [hidden]="_model.hidden"
[layout]="formLayout"
[model]="_model"
[templates]="templates"
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
(dfBlur)="onBlur($event)"
(dfChange)="onChange($event)"
(dfFocus)="onFocus($event)"
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container>
</div> </div>
</div>
</div> </div>
</ng-container> </ng-container>
<ng-template #controlContainer let-idx>
<ds-dynamic-form-control-container *ngFor="let _model of model.groups[idx].group"
[bindId]="false"
[context]="model.groups[idx]"
[group]="control.get([idx])"
[hidden]="_model.hidden"
[layout]="formLayout"
[model]="_model"
[templates]="templates"
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
(dfBlur)="update($event, idx)"
(dfChange)="update($event, idx)"
(dfFocus)="onFocus($event)"
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
</ng-template>

View File

@@ -5,48 +5,41 @@
} }
.cdk-drag { .cdk-drag {
margin-left: calc(-2 * var(--bs-spacer)); margin-left: calc(-2.3 * var(--bs-spacer));
margin-right: calc(-0.5 * var(--bs-spacer)); margin-right: calc(-0.5 * var(--bs-spacer));
padding-right: calc(0.5 * var(--bs-spacer)); padding-right: calc(0.5 * var(--bs-spacer));
.drag-icon { .drag-icon {
visibility: hidden; visibility: hidden;
width: calc(2 * var(--bs-spacer)); width: calc(2 * var(--bs-spacer));
color: var(--bs-gray-600); color: var(--bs-gray-600);
margin: var(--bs-btn-padding-y) 0; margin: var(--bs-btn-padding-y) 0;
line-height: var(--bs-btn-line-height); line-height: var(--bs-btn-line-height);
text-indent: calc(0.5 * var(--bs-spacer)) text-indent: calc(0.5 * var(--bs-spacer))
} }
&:hover, &:focus { &:hover, &:focus {
cursor: grab; cursor: grab;
.drag-icon { .drag-icon {
visibility: visible; visibility: visible;
}
} }
}
} }
.cdk-drop-list-dragging { .cdk-drop-list-dragging {
.cdk-drag { .cdk-drag {
cursor: grabbing; cursor: grabbing;
.drag-icon { .drag-icon {
visibility: hidden; visibility: hidden;
}
} }
}
} }
.cdk-drag-preview { .cdk-drag-preview {
background-color: white; margin: 0;
border-radius: var(--bs-border-radius-sm); padding: 0;
margin-left: 0;
box-shadow: 0 5px 5px 0px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
.drag-icon {
visibility: visible;
}
} }
.cdk-drag-placeholder { .cdk-drag-placeholder {
opacity: 0; opacity: 0;
} }

View File

@@ -5,15 +5,15 @@ import {
DynamicFormArrayComponent, DynamicFormArrayComponent,
DynamicFormControlCustomEvent, DynamicFormControlCustomEvent,
DynamicFormControlEvent, DynamicFormControlEvent,
DynamicFormControlEventType, DynamicFormControlLayout,
DynamicFormControlLayout, DynamicFormLayout, DynamicFormLayout,
DynamicFormLayoutService, DynamicFormLayoutService,
DynamicFormValidationService, DynamicFormValidationService,
DynamicTemplateDirective DynamicTemplateDirective
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model';
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
import { hasValue } from '../../../../../empty.util'; import { hasValue } from '../../../../../empty.util';
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
@Component({ @Component({
selector: 'ds-dynamic-form-array', selector: 'ds-dynamic-form-array',
@@ -25,7 +25,7 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
@Input() formLayout: DynamicFormLayout; @Input() formLayout: DynamicFormLayout;
@Input() group: FormGroup; @Input() group: FormGroup;
@Input() layout: DynamicFormControlLayout; @Input() layout: DynamicFormControlLayout;
@Input() model: DynamicRowArrayModel; @Input() model: DynamicRowArrayModel;// DynamicRow?
@Input() templates: QueryList<DynamicTemplateDirective> | undefined; @Input() templates: QueryList<DynamicTemplateDirective> | undefined;
/* tslint:disable:no-output-rename */ /* tslint:disable:no-output-rename */
@@ -43,22 +43,25 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
} }
moveSelection(event: CdkDragDrop<Relationship>) { moveSelection(event: CdkDragDrop<Relationship>) {
// prevent propagating events generated releasing on the same position
if (event.previousIndex === event.currentIndex) {
return;
}
this.model.moveGroup(event.previousIndex, event.currentIndex - event.previousIndex); this.model.moveGroup(event.previousIndex, event.currentIndex - event.previousIndex);
const prevIndex = event.previousIndex - 1; const prevIndex = event.previousIndex;
const index = event.currentIndex - 1; const index = event.currentIndex;
if (hasValue(this.model.groups[index]) && hasValue((this.control as any).controls[index])) { if (hasValue(this.model.groups[index]) && hasValue((this.control as any).controls[index])) {
const $event = { this.onCustomEvent({
$event: { previousIndex: prevIndex }, previousIndex: prevIndex,
context: { index }, index,
control: (this.control as any).controls[index], arrayModel: this.model,
group: this.group,
model: this.model.groups[index].group[0], model: this.model.groups[index].group[0],
type: DynamicFormControlEventType.Change control: (this.control as any).controls[index]
}; }, 'move');
}
this.onChange($event);
}
} }
update(event: any, index: number) { update(event: any, index: number) {
@@ -68,4 +71,11 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
this.onChange($event); this.onChange($event);
} }
/**
* If the drag feature is disabled for this DynamicRowArrayModel.
*/
get dragDisabled(): boolean {
return this.model.groups.length === 1 || !this.model.isDraggable;
}
} }

View File

@@ -9,6 +9,7 @@ export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig
metadataKey: string; metadataKey: string;
metadataFields: string[]; metadataFields: string[];
hasSelectableMetadata: boolean; hasSelectableMetadata: boolean;
isDraggable: boolean;
} }
export class DynamicRowArrayModel extends DynamicFormArrayModel { export class DynamicRowArrayModel extends DynamicFormArrayModel {
@@ -19,6 +20,7 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel {
@serializable() metadataKey: string; @serializable() metadataKey: string;
@serializable() metadataFields: string[]; @serializable() metadataFields: string[];
@serializable() hasSelectableMetadata: boolean; @serializable() hasSelectableMetadata: boolean;
@serializable() isDraggable: boolean;
isRowArray = true; isRowArray = true;
constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) {
@@ -30,5 +32,6 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel {
this.metadataKey = config.metadataKey; this.metadataKey = config.metadataKey;
this.metadataFields = config.metadataFields; this.metadataFields = config.metadataFields;
this.hasSelectableMetadata = config.hasSelectableMetadata; this.hasSelectableMetadata = config.hasSelectableMetadata;
this.isDraggable = config.isDraggable;
} }
} }

View File

@@ -6,7 +6,8 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ngb-tabset> <ds-loading *ngIf="!item || !collection"></ds-loading>
<ngb-tabset *ngIf="item && collection">
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + relationshipOptions.relationshipType | translate : {count: (totalInternal$ | async)}"> <ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + relationshipOptions.relationshipType | translate : {count: (totalInternal$ | async)}">
<ng-template ngbTabContent> <ng-template ngbTabContent>
<ds-dynamic-lookup-relation-search-tab <ds-dynamic-lookup-relation-search-tab

View File

@@ -21,6 +21,10 @@ import { createPaginatedList } from '../../../../testing/utils.test';
import { ExternalSourceService } from '../../../../../core/data/external-source.service'; import { ExternalSourceService } from '../../../../../core/data/external-source.service';
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
import { SubmissionService } from '../../../../../submission/submission.service';
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import { Collection } from '../../../../../core/shared/collection.model';
describe('DsDynamicLookupRelationModalComponent', () => { describe('DsDynamicLookupRelationModalComponent', () => {
let component: DsDynamicLookupRelationModalComponent; let component: DsDynamicLookupRelationModalComponent;
@@ -28,6 +32,7 @@ describe('DsDynamicLookupRelationModalComponent', () => {
let item; let item;
let item1; let item1;
let item2; let item2;
let testWSI;
let searchResult1; let searchResult1;
let searchResult2; let searchResult2;
let listID; let listID;
@@ -41,6 +46,8 @@ describe('DsDynamicLookupRelationModalComponent', () => {
let lookupRelationService; let lookupRelationService;
let rdbService; let rdbService;
let submissionId; let submissionId;
let submissionService;
let submissionObjectDataService;
const externalSources = [ const externalSources = [
Object.assign(new ExternalSource(), { Object.assign(new ExternalSource(), {
@@ -56,11 +63,16 @@ describe('DsDynamicLookupRelationModalComponent', () => {
]; ];
const totalLocal = 10; const totalLocal = 10;
const totalExternal = 8; const totalExternal = 8;
const collection: Collection = new Collection();
function init() { function init() {
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} }); item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
testWSI = new WorkspaceItem();
testWSI.item = createSuccessfulRemoteDataObject$(item);
testWSI.collection = createSuccessfulRemoteDataObject$(collection);
searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 });
searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 });
listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3'; listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3';
@@ -87,6 +99,12 @@ describe('DsDynamicLookupRelationModalComponent', () => {
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
aggregate: createSuccessfulRemoteDataObject$(externalSources) aggregate: createSuccessfulRemoteDataObject$(externalSources)
}); });
submissionService = jasmine.createSpyObj('SubmissionService', {
dispatchSave: jasmine.createSpy('dispatchSave')
});
submissionObjectDataService = jasmine.createSpyObj('SubmissionObjectDataService', {
findById: createSuccessfulRemoteDataObject$(testWSI)
});
submissionId = '1234'; submissionId = '1234';
} }
@@ -111,6 +129,8 @@ describe('DsDynamicLookupRelationModalComponent', () => {
}, },
{ provide: RelationshipTypeService, useValue: {} }, { provide: RelationshipTypeService, useValue: {} },
{ provide: RemoteDataBuildService, useValue: rdbService }, { provide: RemoteDataBuildService, useValue: rdbService },
{ provide: SubmissionService, useValue: submissionService },
{ provide: SubmissionObjectDataService, useValue: submissionObjectDataService },
{ {
provide: Store, useValue: { provide: Store, useValue: {
// tslint:disable-next-line:no-empty // tslint:disable-next-line:no-empty

View File

@@ -11,7 +11,11 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob
import { RelationshipOptions } from '../../models/relationship-options.model'; import { RelationshipOptions } from '../../models/relationship-options.model';
import { SearchResult } from '../../../../search/search-result.model'; import { SearchResult } from '../../../../search/search-result.model';
import { Item } from '../../../../../core/shared/item.model'; import { Item } from '../../../../../core/shared/item.model';
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; import {
getAllSucceededRemoteData,
getAllSucceededRemoteDataPayload,
getRemoteDataPayload
} from '../../../../../core/shared/operators';
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions'; import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions';
import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../../core/data/relationship.service';
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
@@ -23,6 +27,12 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model
import { ExternalSourceService } from '../../../../../core/data/external-source.service'; import { ExternalSourceService } from '../../../../../core/data/external-source.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
import { followLink } from '../../../../utils/follow-link-config.model';
import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model';
import { Collection } from '../../../../../core/shared/collection.model';
import { SubmissionService } from '../../../../../submission/submission.service';
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
import { RemoteData } from '../../../../../core/data/remote-data';
@Component({ @Component({
selector: 'ds-dynamic-lookup-relation-modal', selector: 'ds-dynamic-lookup-relation-modal',
@@ -112,6 +122,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
*/ */
totalExternal$: Observable<number[]>; totalExternal$: Observable<number[]>;
/**
* List of subscriptions to unsubscribe from
*/
private subs: Subscription[] = [];
constructor( constructor(
public modal: NgbActiveModal, public modal: NgbActiveModal,
private selectableListService: SelectableListService, private selectableListService: SelectableListService,
@@ -121,14 +136,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
private lookupRelationService: LookupRelationService, private lookupRelationService: LookupRelationService,
private searchConfigService: SearchConfigurationService, private searchConfigService: SearchConfigurationService,
private rdbService: RemoteDataBuildService, private rdbService: RemoteDataBuildService,
private submissionService: SubmissionService,
private submissionObjectService: SubmissionObjectDataService,
private zone: NgZone, private zone: NgZone,
private store: Store<AppState>, private store: Store<AppState>,
private router: Router, private router: Router
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
this.setItem();
this.selection$ = this.selectableListService this.selection$ = this.selectableListService
.getSelectableList(this.listId) .getSelectableList(this.listId)
.pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : [])); .pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
@@ -188,6 +206,24 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
}); });
} }
/**
* Initialize this.item$ based on this.model.submissionId
*/
private setItem() {
const submissionObject$ = this.submissionObjectService
.findById(this.submissionId, true, true, followLink('item'), followLink('collection')).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload()
);
const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
this.subs.push(item$.subscribe((item) => this.item = item));
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
}
/** /**
* Add a subscription updating relationships with name variants * Add a subscription updating relationships with name variants
* @param sri The search result to track name variants for * @param sri The search result to track name variants for
@@ -243,5 +279,8 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
ngOnDestroy() { ngOnDestroy() {
this.router.navigate([], {}); this.router.navigate([], {});
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
} }
} }

View File

@@ -295,6 +295,7 @@ describe('FormBuilderService test suite', () => {
notRepeatable: false, notRepeatable: false,
relationshipConfig: undefined, relationshipConfig: undefined,
submissionId: '1234', submissionId: '1234',
isDraggable: true,
groupFactory: () => { groupFactory: () => {
return [ return [
new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }) new DynamicInputModel({ id: 'testFormRowArrayGroupInput' })

View File

@@ -28,9 +28,10 @@ import { DynamicRelationGroupModel } from './ds-dynamic-form-ui/models/relation-
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
import { isNgbDateStruct } from '../../date.util'; import { dateToString, isNgbDateStruct } from '../../date.util';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants';
import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models';
@Injectable() @Injectable()
export class FormBuilderService extends DynamicFormService { export class FormBuilderService extends DynamicFormService {
@@ -121,8 +122,15 @@ export class FormBuilderService extends DynamicFormService {
const normalizeValue = (controlModel, controlValue, controlModelIndex) => { const normalizeValue = (controlModel, controlValue, controlModelIndex) => {
const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null; const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null;
if (controlModel?.metadataValue?.authority?.includes(VIRTUAL_METADATA_PREFIX)) {
return controlModel.metadataValue;
}
if (isString(controlValue)) { if (isString(controlValue)) {
return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex); return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex);
} else if (isNgbDateStruct(controlValue)) {
return new FormFieldMetadataValueObject(dateToString(controlValue));
} else if (isObject(controlValue)) { } else if (isObject(controlValue)) {
const authority = (controlValue as any).authority || (controlValue as any).id || null; const authority = (controlValue as any).authority || (controlValue as any).id || null;
const place = controlModelIndex || (controlValue as any).place; const place = controlModelIndex || (controlValue as any).place;
@@ -240,7 +248,7 @@ export class FormBuilderService extends DynamicFormService {
} }
hasArrayGroupValue(model: DynamicFormControlModel): boolean { hasArrayGroupValue(model: DynamicFormControlModel): boolean {
return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG || model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY); return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG);
} }
hasMappedGroupValue(model: DynamicFormControlModel): boolean { hasMappedGroupValue(model: DynamicFormControlModel): boolean {
@@ -310,7 +318,7 @@ export class FormBuilderService extends DynamicFormService {
let tempModel: DynamicFormControlModel; let tempModel: DynamicFormControlModel;
if (this.isArrayGroup(model as DynamicFormControlModel)) { if (this.isArrayGroup(model as DynamicFormControlModel)) {
return hasValue((model as any).metadataKey) ? (model as any).metadataKey : model.index.toString(); return model.index.toString();
} else if (this.isModelInCustomGroup(model as DynamicFormControlModel)) { } else if (this.isModelInCustomGroup(model as DynamicFormControlModel)) {
tempModel = (model as any).parent; tempModel = (model as any).parent;
} else { } else {

View File

@@ -58,8 +58,20 @@ export class ConcatFieldParser extends FieldParser {
concatGroup.group = []; concatGroup.group = [];
concatGroup.separator = this.separator; concatGroup.separator = this.separator;
const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, false, false); const input1ModelConfig: DynamicInputModelConfig = this.initModel(
const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, false, false); id + CONCAT_FIRST_INPUT_SUFFIX,
false,
true,
true,
false
);
const input2ModelConfig: DynamicInputModelConfig = this.initModel(
id + CONCAT_SECOND_INPUT_SUFFIX,
false,
true,
true,
false
);
if (hasNoValue(concatGroup.hint) && hasValue(input1ModelConfig.hint) && hasNoValue(input2ModelConfig.hint)) { if (hasNoValue(concatGroup.hint) && hasValue(input1ModelConfig.hint) && hasNoValue(input2ModelConfig.hint)) {
concatGroup.hint = input1ModelConfig.hint; concatGroup.hint = input1ModelConfig.hint;

View File

@@ -15,6 +15,8 @@ import { setLayout } from './parser.utils';
import { ParserOptions } from './parser-options'; import { ParserOptions } from './parser-options';
import { RelationshipOptions } from '../models/relationship-options.model'; import { RelationshipOptions } from '../models/relationship-options.model';
import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model';
import { ParserType } from './parser-type';
import { isNgbDateStruct } from '../../../date.util';
export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>('submissionId'); export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>('submissionId');
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData'); export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
@@ -37,9 +39,8 @@ export abstract class FieldParser {
public parse() { public parse() {
if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable)) if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
&& (this.configData.input.type !== 'list') && (this.configData.input.type !== ParserType.List)
&& (this.configData.input.type !== 'tag') && (this.configData.input.type !== ParserType.Tag)
&& (this.configData.input.type !== 'group')
) { ) {
let arrayCounter = 0; let arrayCounter = 0;
let fieldArrayCounter = 0; let fieldArrayCounter = 0;
@@ -49,6 +50,11 @@ export abstract class FieldParser {
if (Array.isArray(this.configData.selectableMetadata) && this.configData.selectableMetadata.length === 1) { if (Array.isArray(this.configData.selectableMetadata) && this.configData.selectableMetadata.length === 1) {
metadataKey = this.configData.selectableMetadata[0].metadata; metadataKey = this.configData.selectableMetadata[0].metadata;
} }
let isDraggable = true;
if (this.configData.input.type === ParserType.Onebox && this.configData?.selectableMetadata?.length > 1) {
isDraggable = false;
}
const config = { const config = {
id: uniqueId() + '_array', id: uniqueId() + '_array',
label: this.configData.label, label: this.configData.label,
@@ -60,6 +66,7 @@ export abstract class FieldParser {
metadataKey, metadataKey,
metadataFields: this.getAllFieldIds(), metadataFields: this.getAllFieldIds(),
hasSelectableMetadata: isNotEmpty(this.configData.selectableMetadata), hasSelectableMetadata: isNotEmpty(this.configData.selectableMetadata),
isDraggable,
groupFactory: () => { groupFactory: () => {
let model; let model;
if ((arrayCounter === 0)) { if ((arrayCounter === 0)) {
@@ -69,19 +76,13 @@ export abstract class FieldParser {
const fieldArrayOfValueLength = this.getInitValueCount(arrayCounter - 1); const fieldArrayOfValueLength = this.getInitValueCount(arrayCounter - 1);
let fieldValue = null; let fieldValue = null;
if (fieldArrayOfValueLength > 0) { if (fieldArrayOfValueLength > 0) {
if (fieldArrayCounter === 0) { fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++);
fieldValue = ''; if (fieldArrayCounter === fieldArrayOfValueLength) {
} else {
fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter - 1);
}
fieldArrayCounter++;
if (fieldArrayCounter === fieldArrayOfValueLength + 1) {
fieldArrayCounter = 0; fieldArrayCounter = 0;
arrayCounter++; arrayCounter++;
} }
} }
model = this.modelFactory(fieldValue, false); model = this.modelFactory(fieldValue, false);
model.id = `${model.id}_${fieldArrayCounter}`;
} }
setLayout(model, 'element', 'host', 'col'); setLayout(model, 'element', 'host', 'col');
if (model.hasLanguages || isNotEmpty(model.relationship)) { if (model.hasLanguages || isNotEmpty(model.relationship)) {
@@ -130,7 +131,9 @@ export abstract class FieldParser {
return; return;
} }
if (typeof fieldValue === 'object') { if (isNgbDateStruct(fieldValue)) {
modelConfig.value = fieldValue;
} else if (typeof fieldValue === 'object') {
modelConfig.metadataValue = fieldValue; modelConfig.metadataValue = fieldValue;
modelConfig.language = fieldValue.language; modelConfig.language = fieldValue.language;
modelConfig.place = fieldValue.place; modelConfig.place = fieldValue.place;
@@ -210,10 +213,9 @@ export abstract class FieldParser {
} }
protected getInitArrayIndex() { protected getInitArrayIndex() {
let fieldCount = 0;
const fieldIds: any = this.getAllFieldIds(); const fieldIds: any = this.getAllFieldIds();
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) { if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) {
fieldCount = this.initFormValues[fieldIds].filter((value) => hasValue(value) && hasValue(value.value)).length; return this.initFormValues[fieldIds].length;
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) { } else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
let counter = 0; let counter = 0;
fieldIds.forEach((id) => { fieldIds.forEach((id) => {
@@ -221,9 +223,10 @@ export abstract class FieldParser {
counter = counter + this.initFormValues[id].length; counter = counter + this.initFormValues[id].length;
} }
}); });
fieldCount = counter; return (counter === 0) ? 1 : counter;
} else {
return 1;
} }
return (fieldCount === 0) ? 1 : fieldCount + 1;
} }
protected getFieldId(): string { protected getFieldId(): string {
@@ -245,7 +248,7 @@ export abstract class FieldParser {
} }
} }
protected initModel(id?: string, label = true, setErrors = true, hint = true) { protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true, hint = true) {
const controlModel = Object.create(null); const controlModel = Object.create(null);
@@ -316,7 +319,7 @@ export abstract class FieldParser {
protected setLabel(controlModel, label = true, labelEmpty = false) { protected setLabel(controlModel, label = true, labelEmpty = false) {
if (label) { if (label) {
controlModel.label = this.configData.label; controlModel.label = (labelEmpty) ? '&nbsp;' : this.configData.label;
} }
} }

View File

@@ -59,7 +59,7 @@ export class OneboxFieldParser extends FieldParser {
this.setLabel(inputSelectGroup, label); this.setLabel(inputSelectGroup, label);
inputSelectGroup.required = isNotEmpty(this.configData.mandatory); inputSelectGroup.required = isNotEmpty(this.configData.mandatory);
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false); const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false, false);
selectModelConfig.hint = null; selectModelConfig.hint = null;
this.setOptions(selectModelConfig); this.setOptions(selectModelConfig);
if (isNotEmpty(fieldValue)) { if (isNotEmpty(fieldValue)) {
@@ -67,7 +67,7 @@ export class OneboxFieldParser extends FieldParser {
} }
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false); const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false, false);
inputModelConfig.hint = null; inputModelConfig.hint = null;
this.setValues(inputModelConfig, fieldValue); this.setValues(inputModelConfig, fieldValue);
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;

View File

@@ -1,57 +1,68 @@
<div class="container-fluid"> <div class="container-fluid">
<form class="form-horizontal" [formGroup]="formGroup"> <form class="form-horizontal" [formGroup]="formGroup">
<ds-dynamic-form <ds-dynamic-form
[formId]="formId" [formId]="formId"
[formGroup]="formGroup" [formGroup]="formGroup"
[formModel]="formModel" [formModel]="formModel"
[formLayout]="formLayout" [formLayout]="formLayout"
(change)="$event.stopPropagation();" (change)="$event.stopPropagation();"
(dfBlur)="onBlur($event)" (dfBlur)="onBlur($event)"
(dfChange)="onChange($event)" (dfChange)="onChange($event)"
(dfFocus)="onFocus($event)"> (dfFocus)="onFocus($event)"
<ng-template modelType="ARRAY" let-group let-index="index" let-context="context"> (ngbEvent)="onCustomEvent($event)">
<!--Array with repeatable items--> <ng-template modelType="ARRAY" let-group let-index="index" let-context="context">
<div *ngIf="context.hasSelectableMetadata && !context.notRepeatable && index < 1" <!--Array with repeatable items-->
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end"> <div *ngIf="(!context.notRepeatable) && !isVirtual(context, index)"
<div class="btn-group" role="group" aria-label="Add and remove button"> class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end">
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary"
[disabled]="isItemReadOnly(context, index)" title="{{'form.remove' | translate}}"
(click)="insertItem($event, group.context, group.index)"> (click)="removeItem($event, context, index)"
<span aria-label="Add">{{'form.add' | translate}}</span> [disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
</button> <span attr.aria-label="{{'form.remove' | translate}}"><i class="fas fa-trash" aria-hidden="true"></i></span>
</div> </button>
</div> </div>
<div *ngIf="(!context.notRepeatable) && index === (group.context.groups.length - 1)" class="clearfix pl-4 w-100">
<!--Array with non repeatable items - Only delete button--> <div class="btn-group" role="group" aria-label="remove button">
<div *ngIf="context.notRepeatable && group.context.groups.length > 1 || index > 0 && !(group.group[0]?.value?.isVirtual || group.group[0]?.metadataValue?.isVirtual)" <button type="button" class="ds-form-add-more btn btn-link"
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end"> title="{{'form.add' | translate}}"
<div class="btn-group" role="group" aria-label="Remove button"> [disabled]="isItemReadOnly(context, index)"
<button type="button" class="btn btn-secondary" (click)="insertItem($event, group.context, group.context.groups.length)">
(click)="removeItem($event, context, index)" <span attr.aria-label="{{'form.add' | translate}}"><i class="fas fa-plus"></i> {{'form.add' | translate}}</span>
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)"> </button>
<i class="fas fa-trash" aria-hidden="true"></i> </div>
</button>
</div>
</div>
</ng-template>
</ds-dynamic-form>
<ng-content></ng-content>
<div *ngIf="displaySubmit">
<hr>
<div class="form-group row">
<div class="col text-right">
<button type="reset" class="btn btn-default" (click)="reset()">{{cancelLabel | translate}}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()"
[disabled]="!(isValid() | async)">{{submitLabel | translate}}
</button>
</div>
</div>
</div> </div>
</form> <!--Array with non repeatable items - Only discard button-->
<div *ngIf="context.notRepeatable && context.showButtons && group.context.groups.length > 1"
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end">
<div class="btn-group" role="group" aria-label="Remove button">
<button type="button" class="btn btn-secondary"
title="{{'form.discard' | translate}}"
(click)="removeItem($event, context, index)"
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
<span attr.aria-label="{{'form.discard' | translate}}">{{'form.discard' | translate}}</span>
</button>
</div>
</div>
</ng-template>
</ds-dynamic-form>
<ng-content></ng-content>
<div *ngIf="displaySubmit">
<hr>
<div class="form-group row">
<div class="col text-right">
<button type="reset" class="btn btn-default" (click)="reset()">{{cancelLabel | translate}}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()"
[disabled]="!(isValid() | async)">{{submitLabel | translate}}
</button>
</div>
</div>
</div>
</form>
</div> </div>

View File

@@ -15,6 +15,11 @@
box-shadow: none !important; box-shadow: none !important;
} }
button.ds-form-add-more:focus {
outline: none;
box-shadow: none !important;
}
.ds-form-input-value { .ds-form-input-value {
border-top-left-radius: 0 !important; border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important; border-bottom-left-radius: 0 !important;

View File

@@ -418,7 +418,7 @@ describe('FormComponent test suite', () => {
})); }));
it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => {
formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 0);
expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel))); expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel)));
})); }));
@@ -426,7 +426,7 @@ describe('FormComponent test suite', () => {
it('should emit removeArrayItem Event when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { it('should emit removeArrayItem Event when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => {
spyOn(formComp.removeArrayItem, 'emit'); spyOn(formComp.removeArrayItem, 'emit');
formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 0);
expect(formComp.removeArrayItem.emit).toHaveBeenCalled(); expect(formComp.removeArrayItem.emit).toHaveBeenCalled();
})); }));

View File

@@ -2,6 +2,7 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { import {
DynamicFormArrayModel, DynamicFormArrayModel,
DynamicFormControlEvent, DynamicFormControlEvent,
@@ -9,15 +10,14 @@ import {
DynamicFormGroupModel, DynamicFormGroupModel,
DynamicFormLayout, DynamicFormLayout,
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { findIndex } from 'lodash'; import { findIndex } from 'lodash';
import { FormBuilderService } from './builder/form-builder.service'; import { FormBuilderService } from './builder/form-builder.service';
import { Observable, Subscription } from 'rxjs';
import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util';
import { FormService } from './form.service'; import { FormService } from './form.service';
import { FormEntry, FormError } from './form.reducer'; import { FormEntry, FormError } from './form.reducer';
import { QUALDROP_GROUP_SUFFIX } from './builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model';
const QUALDROP_GROUP_REGEX = new RegExp(`${QUALDROP_GROUP_SUFFIX}_\\d+$`);
/** /**
* The default form component. * The default form component.
@@ -70,6 +70,7 @@ export class FormComponent implements OnDestroy, OnInit {
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>(); @Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>(); @Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>(); @Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('ngbEvent') customEvent: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/* tslint:enable:no-output-rename */ /* tslint:enable:no-output-rename */
@Output() addArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>(); @Output() addArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output() removeArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>(); @Output() removeArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@@ -87,9 +88,9 @@ export class FormComponent implements OnDestroy, OnInit {
@Output() submitForm: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>(); @Output() submitForm: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>();
/** /**
* An object of FormGroup type * Reference to NgbModal
*/ */
// public formGroup: FormGroup; modalRef: NgbModalRef;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
@@ -166,7 +167,6 @@ export class FormComponent implements OnDestroy, OnInit {
filter((formState: FormEntry) => !!formState && (isNotEmpty(formState.errors) || isNotEmpty(this.formErrors))), filter((formState: FormEntry) => !!formState && (isNotEmpty(formState.errors) || isNotEmpty(this.formErrors))),
map((formState) => formState.errors), map((formState) => formState.errors),
distinctUntilChanged()) distinctUntilChanged())
// .delay(100) // this terrible delay is here to prevent the detection change error
.subscribe((errors: FormError[]) => { .subscribe((errors: FormError[]) => {
const { formGroup, formModel } = this; const { formGroup, formModel } = this;
errors errors
@@ -187,7 +187,6 @@ export class FormComponent implements OnDestroy, OnInit {
if (field) { if (field) {
const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel); const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel);
this.formService.addErrorToField(field, model, error.message); this.formService.addErrorToField(field, model, error.message);
// this.formService.validateAllFormFields(formGroup);
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
} }
@@ -252,6 +251,10 @@ export class FormComponent implements OnDestroy, OnInit {
this.blur.emit(event); this.blur.emit(event);
} }
onCustomEvent(event: any) {
this.customEvent.emit(event);
}
onFocus(event: DynamicFormControlEvent): void { onFocus(event: DynamicFormControlEvent): void {
this.formService.setTouched(this.formId, this.formModel, event); this.formService.setTouched(this.formId, this.formModel, event);
this.focus.emit(event); this.focus.emit(event);
@@ -300,58 +303,25 @@ export class FormComponent implements OnDestroy, OnInit {
removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void {
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
this.removeArrayItem.emit(this.getEvent($event, arrayContext, index - 1, 'remove')); const event = this.getEvent($event, arrayContext, index, 'remove');
this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext);
this.formService.changeForm(this.formId, this.formModel); this.formService.changeForm(this.formId, this.formModel);
this.removeArrayItem.emit(event);
} }
insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void {
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext);
// First emit the new value so it can be sent to the server this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add'));
const value = formArrayControl.controls[0].value;
const event = this.getEvent($event, arrayContext, 0, 'add');
this.addArrayItem.emit(event);
this.change.emit(event);
// Next: update the UI so the user sees the changes
// without having to wait for the server's reply
// add an empty new field at the bottom
this.formBuilderService.addFormArrayGroup(formArrayControl, arrayContext);
// set that field to the new value
const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any;
if (model.hasAuthority) {
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)));
const valueKey = Object.keys(value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX)));
if (ctrlKey !== valueKey) {
Object.defineProperty(value, ctrlKey, Object.getOwnPropertyDescriptor(value, valueKey));
delete value[valueKey];
}
ctrl.setValue(value);
} else {
formArrayControl.controls[formArrayControl.length - 1].setValue(value);
}
// Clear the topmost field by removing the filled out version and inserting a new, empty version.
// Doing it this way ensures an empty value of the correct type is added without a bunch of ifs here
this.formBuilderService.removeFormArrayGroup(0, formArrayControl, arrayContext);
this.formBuilderService.insertFormArrayGroup(0, formArrayControl, arrayContext);
// Tell the formService that it should rerender.
this.formService.changeForm(this.formId, this.formModel); this.formService.changeForm(this.formId, this.formModel);
} }
isVirtual(arrayContext: DynamicFormArrayModel, index: number) {
const context = arrayContext.groups[index];
const value: FormFieldMetadataValueObject = (context.group[0] as any).metadataValue;
return isNotEmpty(value) && value.isVirtual;
}
protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent { protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent {
const context = arrayContext.groups[index]; const context = arrayContext.groups[index];
const itemGroupModel = context.context; const itemGroupModel = context.context;

View File

@@ -80,6 +80,7 @@ const rowArrayQualdropConfig = {
id: 'row_QUALDROP_GROUP', id: 'row_QUALDROP_GROUP',
initialCount: 1, initialCount: 1,
notRepeatable: true, notRepeatable: true,
isDraggable: false,
relationshipConfig: undefined, relationshipConfig: undefined,
groupFactory: () => { groupFactory: () => {
return [MockQualdropModel]; return [MockQualdropModel];

View File

@@ -31,9 +31,8 @@ import { RequestService } from '../../core/data/request.service';
import { NotificationsService } from '../notifications/notifications.service'; import { NotificationsService } from '../notifications/notifications.service';
import { dateToString, stringToNgbDateStruct } from '../date.util'; import { dateToString, stringToNgbDateStruct } from '../date.util';
import { followLink } from '../utils/follow-link-config.model'; import { followLink } from '../utils/follow-link-config.model';
import { ADMIN_MODULE_PATH } from '../../app-routing-paths'; import { ACCESS_CONTROL_MODULE_PATH } from '../../app-routing-paths';
import { GROUP_EDIT_PATH } from '../../+admin/admin-access-control/admin-access-control-routing-paths'; import { GROUP_EDIT_PATH } from '../../access-control/access-control-routing-paths';
import { ACCESS_CONTROL_MODULE_PATH } from '../../+admin/admin-routing-paths';
interface ResourcePolicyCheckboxEntry { interface ResourcePolicyCheckboxEntry {
id: string; id: string;
@@ -317,7 +316,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
map((group: Group) => group.id) map((group: Group) => group.id)
).subscribe((groupUUID) => { ).subscribe((groupUUID) => {
this.router.navigate([ADMIN_MODULE_PATH, ACCESS_CONTROL_MODULE_PATH, GROUP_EDIT_PATH, groupUUID]); this.router.navigate([ACCESS_CONTROL_MODULE_PATH, GROUP_EDIT_PATH, groupUUID]);
}) })
); );
} }

View File

@@ -52,7 +52,7 @@ export class MenuServiceStub {
deactivateSection(): void { /***/ deactivateSection(): void { /***/
} }
addSection(): void { /***/ addSection(menuID: MenuID, section: MenuSection): void { /***/
} }
removeSection(): void { /***/ removeSection(): void { /***/

View File

@@ -1,4 +1,4 @@
import { waitForAsync, TestBed } from '@angular/core/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { import {
@@ -35,10 +35,10 @@ describe('SectionFormOperationsService test suite', () => {
let serviceAsAny: any; let serviceAsAny: any;
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
add: jasmine.createSpy('add'), add: jasmine.createSpy('add'),
replace: jasmine.createSpy('replace'), replace: jasmine.createSpy('replace'),
remove: jasmine.createSpy('remove'), remove: jasmine.createSpy('remove'),
}); });
const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'test'); const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'test');
const dynamicFormControlChangeEvent: DynamicFormControlEvent = { const dynamicFormControlChangeEvent: DynamicFormControlEvent = {

View File

@@ -6,18 +6,10 @@ import {
DYNAMIC_FORM_CONTROL_TYPE_GROUP, DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DynamicFormArrayGroupModel, DynamicFormArrayGroupModel,
DynamicFormControlEvent, DynamicFormControlEvent,
DynamicFormControlModel DynamicFormControlModel, isDynamicFormControlEvent
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util';
hasNoValue,
hasValue,
isNotEmpty,
isNotNull,
isNotUndefined,
isNull,
isUndefined
} from '../../../shared/empty.util';
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object';
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
@@ -30,6 +22,8 @@ import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-fo
import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { deepClone } from 'fast-json-patch'; import { deepClone } from 'fast-json-patch';
import { dateToString, isNgbDateStruct } from '../../../shared/date.util';
import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
/** /**
* The service handling all form section operations * The service handling all form section operations
@@ -71,8 +65,8 @@ export class SectionFormOperationsService {
case 'change': case 'change':
this.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, hasStoredValue); this.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, hasStoredValue);
break; break;
case 'add': case 'move':
this.dispatchOperationsFromAddEvent(pathCombiner, event); this.dispatchOperationsFromMoveEvent(pathCombiner, event, previousValue);
break; break;
default: default:
break; break;
@@ -83,20 +77,29 @@ export class SectionFormOperationsService {
* Return index if specified field is part of fields array * Return index if specified field is part of fields array
* *
* @param event * @param event
* the [[DynamicFormControlEvent]] for the specified operation * the [[DynamicFormControlEvent]] | CustomEvent for the specified operation
* @return number * @return number
* the array index is part of array, zero otherwise * the array index is part of array, zero otherwise
*/ */
public getArrayIndexFromEvent(event: DynamicFormControlEvent): number { public getArrayIndexFromEvent(event: DynamicFormControlEvent | any): number {
let fieldIndex: number; let fieldIndex: number;
if (isNotEmpty(event)) { if (isNotEmpty(event)) {
if (isNull(event.context)) { if (isDynamicFormControlEvent(event)) {
// Check whether model is part of an Array of group // This is the case of a default insertItem/removeItem event
if (this.isPartOfArrayOfGroup(event.model)) {
fieldIndex = (event.model.parent as any).parent.index; if (isNull(event.context)) {
// Check whether model is part of an Array of group
if (this.isPartOfArrayOfGroup(event.model)) {
fieldIndex = (event.model.parent as any).parent.index;
}
} else {
fieldIndex = event.context.index;
} }
} else { } else {
fieldIndex = event.context.index; // This is the case of a custom event which contains indexes information
fieldIndex = event.index as any;
} }
} }
@@ -245,6 +248,8 @@ export class SectionFormOperationsService {
// Language without Authority (input, textArea) // Language without Authority (input, textArea)
fieldValue = new FormFieldMetadataValueObject(value, language); fieldValue = new FormFieldMetadataValueObject(value, language);
} }
} else if (isNgbDateStruct(value)) {
fieldValue = new FormFieldMetadataValueObject(dateToString(value));
} else if (value instanceof FormFieldLanguageValueObject || value instanceof VocabularyEntry } else if (value instanceof FormFieldLanguageValueObject || value instanceof VocabularyEntry
|| value instanceof VocabularyEntryDetail || isObject(value)) { || value instanceof VocabularyEntryDetail || isObject(value)) {
fieldValue = value; fieldValue = value;
@@ -291,11 +296,19 @@ export class SectionFormOperationsService {
protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner, protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner,
event: DynamicFormControlEvent, event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject): void { previousValue: FormFieldPreviousValueObject): void {
if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
// Model is a DynamicRowArrayModel
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context);
return;
}
const path = this.getFieldPathFromEvent(event); const path = this.getFieldPathFromEvent(event);
const value = this.getFieldValueFromChangeEvent(event); const value = this.getFieldValueFromChangeEvent(event);
console.log(value);
if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) { if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue); this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue);
} else if (isNotEmpty(value)) { } else if ((isNotEmpty(value) && typeof value === 'string') || (isNotEmpty(value) && value instanceof FormFieldMetadataValueObject && value.hasValue())) {
this.operationsBuilder.remove(pathCombiner.getPath(path)); this.operationsBuilder.remove(pathCombiner.getPath(path));
} }
} }
@@ -352,17 +365,25 @@ export class SectionFormOperationsService {
event: DynamicFormControlEvent, event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject, previousValue: FormFieldPreviousValueObject,
hasStoredValue: boolean): void { hasStoredValue: boolean): void {
if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
// Model is a DynamicRowArrayModel
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context);
return;
}
const path = this.getFieldPathFromEvent(event); const path = this.getFieldPathFromEvent(event);
const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event); const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event);
const value = this.getFieldValueFromChangeEvent(event); const value = this.getFieldValueFromChangeEvent(event);
// Detect which operation must be dispatched // Detect which operation must be dispatched
if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)) { if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)
|| this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
// It's a qualdrup model // It's a qualdrup model
this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue); this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue);
} else if (this.formBuilder.isRelationGroup(event.model)) { } else if (this.formBuilder.isRelationGroup(event.model)) {
// It's a relation model // It's a relation model
this.dispatchOperationsFromMap(this.getValueMap(value), pathCombiner, event, previousValue); this.dispatchOperationsFromMap(this.getValueMap(value), pathCombiner, event, previousValue);
} else if (this.formBuilder.hasArrayGroupValue(event.model) && hasNoValue((event.model as any).relationshipConfig)) { } else if (this.formBuilder.hasArrayGroupValue(event.model)) {
// Model has as value an array, so dispatch an add operation with entire block of values // Model has as value an array, so dispatch an add operation with entire block of values
this.operationsBuilder.add( this.operationsBuilder.add(
pathCombiner.getPath(segmentedPath), pathCombiner.getPath(segmentedPath),
@@ -398,13 +419,21 @@ export class SectionFormOperationsService {
value); value);
} }
previousValue.delete(); previousValue.delete();
} else if (value.hasValue() && (isUndefined(this.getArrayIndexFromEvent(event)) } else if (value.hasValue()) {
|| this.getArrayIndexFromEvent(event) === 0)) { // Here model has no previous value but a new one
if (isUndefined(this.getArrayIndexFromEvent(event)) || this.getArrayIndexFromEvent(event) === 0) {
// Model is single field or is part of an array model but is the first item, // Model is single field or is part of an array model but is the first item,
// so dispatch an add operation that initialize the values of a specific metadata // so dispatch an add operation that initialize the values of a specific metadata
this.operationsBuilder.add( this.operationsBuilder.add(
pathCombiner.getPath(segmentedPath), pathCombiner.getPath(segmentedPath),
value, true); value, true);
} else {
// Model is part of an array model but is not the first item,
// so dispatch an add operation that add a value to an existent metadata
this.operationsBuilder.add(
pathCombiner.getPath(path),
value);
}
} }
} }
@@ -454,4 +483,38 @@ export class SectionFormOperationsService {
previousValue.delete(); previousValue.delete();
} }
/**
* Handle form move operations
*
* @param pathCombiner
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
* @param event
* the [[DynamicFormControlEvent]] for the specified operation
* @param previousValue
* the [[FormFieldPreviousValueObject]] for the specified operation
*/
private dispatchOperationsFromMoveEvent(pathCombiner: JsonPatchOperationPathCombiner,
event: DynamicFormControlEvent,
previousValue: FormFieldPreviousValueObject) {
return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel);
}
/**
* Specific patch handler for a DynamicRowArrayModel.
* Configure a Patch ADD with the current array value.
* @param pathCombiner
* @param event
* @param model
*/
private handleArrayGroupPatch(pathCombiner: JsonPatchOperationPathCombiner,
event,
model: DynamicRowArrayModel) {
const arrayValue = this.formBuilder.getValueFromModel([model]);
const segmentedPath2 = this.getFieldPathSegmentedFromChangeEvent(event);
this.operationsBuilder.add(
pathCombiner.getPath(segmentedPath2),
arrayValue[segmentedPath2], false);
}
} }

Some files were not shown because too many files have changed in this diff Show More