diff --git a/package.json b/package.json index bd7827e333..18f3bf44e5 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,8 @@ "sortablejs": "1.10.1", "tslib": "^2.0.0", "webfontloader": "1.6.28", - "zone.js": "^0.10.3" + "zone.js": "^0.10.3", + "@kolkov/ngx-gallery": "^1.2.3" }, "devDependencies": { "@angular-builders/custom-webpack": "10.0.1", diff --git a/src/app/+admin/admin-routing-paths.ts b/src/app/+admin/admin-routing-paths.ts index 11eac49fe2..3168ea93c9 100644 --- a/src/app/+admin/admin-routing-paths.ts +++ b/src/app/+admin/admin-routing-paths.ts @@ -2,12 +2,7 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAdminModuleRoute } from '../app-routing-paths'; export const REGISTRIES_MODULE_PATH = 'registries'; -export const ACCESS_CONTROL_MODULE_PATH = 'access-control'; export function getRegistriesModuleRoute() { return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); } - -export function getAccessControlModuleRoute() { - return new URLCombiner(getAdminModuleRoute(), ACCESS_CONTROL_MODULE_PATH).toString(); -} diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index 6c44af8190..ee5cb8737b 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -6,7 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { 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({ imports: [ @@ -16,11 +16,6 @@ import { ACCESS_CONTROL_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-rout loadChildren: () => import('./admin-registries/admin-registries.module') .then((m) => m.AdminRegistriesModule), }, - { - path: ACCESS_CONTROL_MODULE_PATH, - loadChildren: () => import('./admin-access-control/admin-access-control.module') - .then((m) => m.AdminAccessControlModule), - }, { path: 'search', resolve: { breadcrumb: I18nBreadcrumbResolver }, diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts index ec571ff3d5..0a9ef512d7 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts @@ -16,6 +16,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { RouterTestingModule } from '@angular/router/testing'; import { ActivatedRoute } from '@angular/router'; 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', () => { let comp: AdminSidebarComponent; @@ -170,4 +172,150 @@ describe('AdminSidebarComponent', () => { 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, + })); + }); + }); + }); }); diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index bfc36cea24..3bf5938487 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -1,6 +1,6 @@ import { Component, Injector, OnInit } from '@angular/core'; 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 { AuthService } from '../../core/auth/auth.service'; import { ScriptDataService } from '../../core/data/processes/script-data.service'; @@ -76,9 +76,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { */ ngOnInit(): void { this.createMenu(); - this.createSiteAdministratorMenuSections(); - this.createExportMenuSections(); - this.createImportMenuSections(); super.ngOnInit(); this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth'); this.authService.isAuthenticated() @@ -102,192 +99,210 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { * Initialize all menu sections and items for this menu */ createMenu() { - const menuList = [ - /* News */ - { - id: 'new', - active: false, - 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, - }, + this.createMainMenuSections(); + this.createSiteAdministratorMenuSections(); + this.createExportMenuSections(); + this.createImportMenuSections(); + this.createAccessControlMenuSections(); + } - /* Edit */ - { - id: 'edit', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.edit' - } as TextMenuItemModel, - icon: 'pencil-alt', - index: 1 - }, - { - id: 'edit_community', - parentID: 'edit', - active: false, - visible: true, - model: { - type: MenuItemType.ONCLICK, - text: 'menu.section.edit_community', - function: () => { - this.modalService.open(EditCommunitySelectorComponent); - } - } as OnClickMenuItemModel, - }, - { - id: 'edit_collection', - parentID: 'edit', - active: false, - visible: true, - 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, - }, + /** + * Initialize the main menu sections. + * edit_community / edit_collection is only included if the current user is a Community or Collection admin + */ + createMainMenuSections() { + combineLatest([ + this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), + this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf) + ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => { + const menuList = [ + /* News */ + { + id: 'new', + active: false, + 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: isCommunityAdmin, + model: { + type: MenuItemType.ONCLICK, + text: 'menu.section.new_community', + function: () => { + this.modalService.open(CreateCommunityParentSelectorComponent); + } + } as OnClickMenuItemModel, + }, + { + id: 'new_collection', + parentID: 'new', + active: false, + visible: isCommunityAdmin, + 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: 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 */ - { - id: 'curation_tasks', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.curation_task', - link: '' - } as LinkMenuItemModel, - icon: 'filter', - index: 7 - }, + /* Edit */ + { + id: 'edit', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.edit' + } as TextMenuItemModel, + icon: 'pencil-alt', + index: 1 + }, + { + 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 */ - { - id: 'statistics_task', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics_task', - link: '' - } as LinkMenuItemModel, - icon: 'chart-bar', - index: 8 - }, + /* Curation tasks */ + { + id: 'curation_tasks', + active: false, + visible: isCollectionAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.curation_task', + link: '' + } as LinkMenuItemModel, + icon: 'filter', + index: 7 + }, - /* Control Panel */ - { - id: 'control_panel', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.control_panel', - link: '' - } as LinkMenuItemModel, - icon: 'cogs', - index: 9 - }, + /* Statistics */ + { + id: 'statistics_task', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics_task', + link: '' + } as LinkMenuItemModel, + icon: 'chart-bar', + index: 8 + }, - /* Processes */ - { - id: 'processes', - active: false, - visible: true, - 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 - }))); + /* Control Panel */ + { + id: 'control_panel', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.control_panel', + link: '' + } as LinkMenuItemModel, + icon: 'cogs', + index: 9 + }, + + /* Processes */ + { + 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() { this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => { 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 */ { 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 * @param event The animation event diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index 494edd71eb..25cdd67dcf 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; 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 { AdminRegistriesModule } from './admin-registries/admin-registries.module'; import { AdminRoutingModule } from './admin-routing.module'; @@ -21,7 +21,7 @@ const ENTRY_COMPONENTS = [ imports: [ AdminRoutingModule, AdminRegistriesModule, - AdminAccessControlModule, + AccessControlModule, AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index bbe2cb5e66..ed0ad1f021 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -35,7 +35,7 @@
- +
diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 5d76ce7859..6435df55ee 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -27,6 +27,8 @@ import { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { AuthService } from '../core/auth/auth.service'; 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'; @Component({ @@ -50,6 +52,11 @@ export class CollectionPageComponent implements OnInit { sortConfig: SortOptions }>; + /** + * Whether the current user is a Community admin + */ + isCollectionAdmin$: Observable; + /** * Route to the community page */ @@ -62,6 +69,7 @@ export class CollectionPageComponent implements OnInit { private route: ActivatedRoute, private router: Router, private authService: AuthService, + private authorizationDataService: AuthorizationDataService, ) { this.paginationConfig = new PaginationComponentOptions(); this.paginationConfig.id = 'collection-page-pagination'; @@ -81,6 +89,7 @@ export class CollectionPageComponent implements OnInit { filter((collection: Collection) => hasValue(collection)), mergeMap((collection: Collection) => collection.logo) ); + this.isCollectionAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCollectionAdmin); this.paginationChanges$ = new BehaviorSubject({ paginationConfig: this.paginationConfig, diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts index e41f0ebda4..e523cee991 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -12,6 +12,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; 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 @@ -26,6 +27,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit }, data: { breadcrumbKey: 'collection.edit' }, component: EditCollectionPageComponent, + canActivate: [CollectionAdministratorGuard], children: [ { path: '', diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 1b4f90a52b..cf7282eb4b 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -21,7 +21,7 @@
- +
diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index 7ba8dfc6c0..70259a599b 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -15,6 +15,8 @@ import { fadeInOut } from '../shared/animations/fade'; import { hasValue } from '../shared/empty.util'; import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators'; 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'; @Component({ @@ -33,6 +35,11 @@ export class CommunityPageComponent implements OnInit { */ communityRD$: Observable>; + /** + * Whether the current user is a Community admin + */ + isCommunityAdmin$: Observable; + /** * The logo of this community */ @@ -49,6 +56,7 @@ export class CommunityPageComponent implements OnInit { private route: ActivatedRoute, private router: Router, private authService: AuthService, + private authorizationDataService: AuthorizationDataService ) { } @@ -66,6 +74,6 @@ export class CommunityPageComponent implements OnInit { getAllSucceededRemoteDataPayload(), map((community) => getCommunityPageRoute(community.id)) ); + this.isCommunityAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCommunityAdmin); } - } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts index 440fa01a30..faebb1ef2e 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -10,6 +10,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; 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 @@ -24,6 +25,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit }, data: { breadcrumbKey: 'community.edit' }, component: EditCommunityPageComponent, + canActivate: [CommunityAdministratorGuard], children: [ { path: '', diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index 116a0feb21..ea78767df5 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -173,6 +173,19 @@ describe('ItemDeleteComponent', () => { .toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id)); 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', () => { it('should navigate to the homepage on successful deletion of the item', () => { diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts index 366b22bec7..a05ffec4f5 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts @@ -1,5 +1,5 @@ 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 { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { @@ -121,8 +121,11 @@ export class ItemDeleteComponent getFirstSucceededRemoteData(), getRemoteDataPayload(), map((relationshipTypes) => relationshipTypes.page), - switchMap((types) => - combineLatest(types.map((type) => this.getRelationships(type))).pipe( + switchMap((types) => { + if (types.length === 0) { + return observableOf(types); + } + return combineLatest(types.map((type) => this.getRelationships(type))).pipe( map((relationships) => types.reduce((includedTypes, type, index) => { if (!includedTypes.some((includedType) => includedType.id === type.id) @@ -133,8 +136,8 @@ export class ItemDeleteComponent } }, []) ), - ) - ), + ); + }) ); } else { this.types$ = observableOf([]); diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 0f66d5b55e..ede024953b 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -27,6 +27,10 @@ import { ResearchEntitiesModule } from '../entity-groups/research-entities/resea import { ThemedItemPageComponent } from './simple/themed-item-page.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; 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 = [ // put only entry components that use custom decorator @@ -53,6 +57,9 @@ const DECLARATIONS = [ ItemComponent, UploadBitstreamComponent, AbstractIncrementalListComponent, + MediaViewerComponent, + MediaViewerVideoComponent, + MediaViewerImageComponent ]; @NgModule({ @@ -65,6 +72,7 @@ const DECLARATIONS = [ JournalEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents(), IIIFEntitiesModule.withEntryComponents() + NgxGalleryModule, ], declarations: [ ...DECLARATIONS diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html new file mode 100644 index 0000000000..bafc6f079c --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss new file mode 100644 index 0000000000..72ce4b04d9 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss @@ -0,0 +1,6 @@ +.ngx-gallery { + display: inline-block; + margin-bottom: 20px; + width: 340px !important; + height: 279px !important; +} diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts new file mode 100644 index 0000000000..1f1fed789f --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts @@ -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; + + 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(), + [ + { 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); + }); +}); diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts new file mode 100644 index 0000000000..0c32b5603d --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts @@ -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; + + 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; + } +} diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html new file mode 100644 index 0000000000..a4493e36fc --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html @@ -0,0 +1,47 @@ + +
+ + + +
+ +
+ +
+
+
diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss new file mode 100644 index 0000000000..7702da7361 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss @@ -0,0 +1,4 @@ +video { + width: 340px; + height: 279px; +} diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts new file mode 100644 index 0000000000..88138a252f --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts @@ -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; + + 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(), + [ + { bitstream: mockBitstream, format: 'video', thumbnail: null }, + { bitstream: mockBitstream, format: 'video', thumbnail: null }, + ] + ); + const mockMediaViewerItem: MediaViewerItem[] = Object.assign( + new Array(), + [{ 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); + }); + }); + }); +}); diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts new file mode 100644 index 0000000000..4c578a51bb --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts @@ -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--; + } +} diff --git a/src/app/+item-page/media-viewer/media-viewer.component.html b/src/app/+item-page/media-viewer/media-viewer.component.html new file mode 100644 index 0000000000..b79b91629f --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.html @@ -0,0 +1,36 @@ + + +
+ + + + + + + + + + + + + +
+
diff --git a/src/app/+item-page/media-viewer/media-viewer.component.scss b/src/app/+item-page/media-viewer/media-viewer.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+item-page/media-viewer/media-viewer.component.spec.ts b/src/app/+item-page/media-viewer/media-viewer.component.spec.ts new file mode 100644 index 0000000000..ebea703ec8 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.spec.ts @@ -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; + + 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(); + }); + }); +}); diff --git a/src/app/+item-page/media-viewer/media-viewer.component.ts b/src/app/+item-page/media-viewer/media-viewer.component.ts new file mode 100644 index 0000000000..3f9de8ed3e --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.ts @@ -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; + + 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>> { + return this.bitstreamDataService + .findAllByItemAndBundleName( + this.item, + bundleName, + {}, + true, + true, + followLink('format') + ) + .pipe( + filter( + (bitstreamsRD: RemoteData>) => + 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; + } +} diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index fc8d06ac10..a004712e0f 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -8,9 +8,14 @@
- - - + + + + + + + + diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index a8119c8565..120eda930f 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { environment } from '../../../../../environments/environment'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; @@ -20,6 +21,7 @@ export class ItemComponent implements OnInit { * Route to the item page */ itemPageRoute: string; + mediaViewer = environment.mediaViewer; constructor(protected bitstreamDataService: BitstreamDataService) { } diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html index 241696f688..7a1366dda9 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -8,9 +8,14 @@
- - - + + + + + + + + diff --git a/src/app/+admin/admin-access-control/admin-access-control-routing-paths.ts b/src/app/access-control/access-control-routing-paths.ts similarity index 56% rename from src/app/+admin/admin-access-control/admin-access-control-routing-paths.ts rename to src/app/access-control/access-control-routing-paths.ts index 2080cb14a7..d229d12bd2 100644 --- a/src/app/+admin/admin-access-control/admin-access-control-routing-paths.ts +++ b/src/app/access-control/access-control-routing-paths.ts @@ -1,5 +1,5 @@ -import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { getAccessControlModuleRoute } from '../admin-routing-paths'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { getAccessControlModuleRoute } from '../app-routing-paths'; export const GROUP_EDIT_PATH = 'groups'; diff --git a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts b/src/app/access-control/access-control-routing.module.ts similarity index 90% rename from src/app/+admin/admin-access-control/admin-access-control-routing.module.ts rename to src/app/access-control/access-control-routing.module.ts index 10ac117b0f..2dbba19f70 100644 --- a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts +++ b/src/app/access-control/access-control-routing.module.ts @@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { GroupFormComponent } from './group-registry/group-form/group-form.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({ 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 */ -export class AdminAccessControlRoutingModule { +export class AccessControlRoutingModule { } diff --git a/src/app/+admin/admin-access-control/admin-access-control.module.ts b/src/app/access-control/access-control.module.ts similarity index 82% rename from src/app/+admin/admin-access-control/admin-access-control.module.ts rename to src/app/access-control/access-control.module.ts index 04051ff46c..0e872458bd 100644 --- a/src/app/+admin/admin-access-control/admin-access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { SharedModule } from '../../shared/shared.module'; -import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module'; +import { SharedModule } from '../shared/shared.module'; +import { AccessControlRoutingModule } from './access-control-routing.module'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-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, SharedModule, RouterModule, - AdminAccessControlRoutingModule + AccessControlRoutingModule ], declarations: [ EPeopleRegistryComponent, @@ -29,6 +29,6 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon /** * This module handles all components related to the access control pages */ -export class AdminAccessControlModule { +export class AccessControlModule { } diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.actions.ts b/src/app/access-control/epeople-registry/epeople-registry.actions.ts similarity index 91% rename from src/app/+admin/admin-access-control/epeople-registry/epeople-registry.actions.ts rename to src/app/access-control/epeople-registry/epeople-registry.actions.ts index 000dc48263..b8b1044362 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.actions.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; -import { EPerson } from '../../../core/eperson/models/eperson.model'; -import { type } from '../../../shared/ngrx/type'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { type } from '../../shared/ngrx/type'; /** * For each action type in an action group, make a simple diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html similarity index 100% rename from src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html rename to src/app/access-control/epeople-registry/epeople-registry.component.html diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts similarity index 87% rename from src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts rename to src/app/access-control/epeople-registry/epeople-registry.component.spec.ts index c104de0b17..2fb955bb02 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts @@ -7,24 +7,24 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { FindListOptions } from '../../../core/data/request.models'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; -import { EPerson } from '../../../core/eperson/models/eperson.model'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { FindListOptions } from '../../core/data/request.models'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { EPeopleRegistryComponent } from './epeople-registry.component'; -import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { RouterStub } from '../../../shared/testing/router.stub'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { RequestService } from '../../../core/data/request.service'; +import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { RequestService } from '../../core/data/request.service'; describe('EPeopleRegistryComponent', () => { let component: EPeopleRegistryComponent; @@ -107,7 +107,7 @@ describe('EPeopleRegistryComponent', () => { // empty }, getEPeoplePageRouterLink(): string { - return '/admin/access-control/epeople'; + return '/access-control/epeople'; } }; authorizationService = jasmine.createSpyObj('authorizationService', { diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts similarity index 88% rename from src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts rename to src/app/access-control/epeople-registry/epeople-registry.component.ts index 11b146b294..3e9b137965 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -4,25 +4,25 @@ import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { map, switchMap, take } from 'rxjs/operators'; -import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; -import { EPerson } from '../../../core/eperson/models/eperson.model'; -import { hasValue } from '../../../shared/empty.util'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { EpersonDtoModel } from '../../../core/eperson/models/eperson-dto.model'; -import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { getFirstCompletedRemoteData, getAllSucceededRemoteData -} from '../../../core/shared/operators'; -import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; +} from '../../core/shared/operators'; +import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { RequestService } from '../../../core/data/request.service'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { NoContent } from '../../../core/shared/NoContent.model'; +import { RequestService } from '../../core/data/request.service'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { NoContent } from '../../core/shared/NoContent.model'; @Component({ selector: 'ds-epeople-registry', diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.reducers.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts similarity index 96% rename from src/app/+admin/admin-access-control/epeople-registry/epeople-registry.reducers.spec.ts rename to src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts index 87ab70a942..7158acc79b 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.reducers.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts @@ -1,6 +1,6 @@ import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions'; import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers'; -import { EPersonMock } from '../../../shared/testing/eperson.mock'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; const initialState: EPeopleRegistryState = { editEPerson: null, diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.reducers.ts b/src/app/access-control/epeople-registry/epeople-registry.reducers.ts similarity index 93% rename from src/app/+admin/admin-access-control/epeople-registry/epeople-registry.reducers.ts rename to src/app/access-control/epeople-registry/epeople-registry.reducers.ts index 42b31d61e9..1e0319f3ba 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.reducers.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.reducers.ts @@ -1,4 +1,4 @@ -import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPeopleRegistryAction, EPeopleRegistryActionTypes, diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html similarity index 100% rename from src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html rename to src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts similarity index 87% rename from src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts rename to src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 1163490e12..454dad0018 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -6,26 +6,26 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { FindListOptions } from '../../../../core/data/request.models'; -import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; -import { EPerson } from '../../../../core/eperson/models/eperson.model'; -import { PageInfo } from '../../../../core/shared/page-info.model'; -import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { FindListOptions } from '../../../core/data/request.models'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { EPersonFormComponent } from './eperson-form.component'; -import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; -import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub'; -import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; -import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { createPaginatedList } from '../../../../shared/testing/utils.test'; -import { RequestService } from '../../../../core/data/request.service'; +import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { AuthService } from '../../../core/auth/auth.service'; +import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { RequestService } from '../../../core/data/request.service'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts similarity index 91% rename from src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts rename to src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 3c284735a9..ecb4606ae4 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -9,28 +9,28 @@ import { import { TranslateService } from '@ngx-translate/core'; import { combineLatest, Observable, of, Subscription } from 'rxjs'; import { switchMap, take } from 'rxjs/operators'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { EPerson } from '../../../../core/eperson/models/eperson.model'; -import { Group } from '../../../../core/eperson/models/group.model'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { Group } from '../../../core/eperson/models/group.model'; import { getRemoteDataPayload, getFirstSucceededRemoteData, getFirstCompletedRemoteData -} from '../../../../core/shared/operators'; -import { hasValue } from '../../../../shared/empty.util'; -import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; -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 { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; +} from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +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 { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { RequestService } from '../../../../core/data/request.service'; -import { NoContent } from '../../../../core/shared/NoContent.model'; +import { RequestService } from '../../../core/data/request.service'; +import { NoContent } from '../../../core/shared/NoContent.model'; @Component({ selector: 'ds-eperson-form', diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html similarity index 100% rename from src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html rename to src/app/access-control/group-registry/group-form/group-form.component.html diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts similarity index 77% rename from src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts rename to src/app/access-control/group-registry/group-form/group-form.component.spec.ts index fd5edf0354..d213a071d7 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -9,30 +9,30 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; -import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; -import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service'; -import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; -import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { Group } from '../../../../core/eperson/models/group.model'; -import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; -import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; -import { PageInfo } from '../../../../core/shared/page-info.model'; -import { UUIDService } from '../../../../core/shared/uuid.service'; -import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { DSOChangeAnalyzer } from '../../../core/data/dso-change-analyzer.service'; +import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { Group } from '../../../core/eperson/models/group.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { UUIDService } from '../../../core/shared/uuid.service'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; import { GroupFormComponent } from './group-form.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; -import { RouterMock } from '../../../../shared/mocks/router.mock'; -import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; +import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; +import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; +import { RouterMock } from '../../../shared/mocks/router.mock'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { Operation } from 'fast-json-patch'; describe('GroupFormComponent', () => { @@ -75,7 +75,7 @@ describe('GroupFormComponent', () => { return observableOf(this.activeGroup); }, getGroupRegistryRouterLink(): string { - return '/admin/access-control/groups'; + return '/access-control/groups'; }, editGroup(group: Group) { this.activeGroup = group; diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts similarity index 89% rename from src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts rename to src/app/access-control/group-registry/group-form/group-form.component.ts index 7984fc50d1..2b834bde1d 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -17,32 +17,32 @@ import { Subscription } from 'rxjs'; import { catchError, map, switchMap, take } from 'rxjs/operators'; -import { getCollectionEditRolesRoute } from '../../../../+collection-page/collection-page-routing-paths'; -import { getCommunityEditRolesRoute } from '../../../../+community-page/community-page-routing-paths'; -import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { RequestService } from '../../../../core/data/request.service'; -import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { Group } from '../../../../core/eperson/models/group.model'; -import { Collection } from '../../../../core/shared/collection.model'; -import { Community } from '../../../../core/shared/community.model'; -import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { getCollectionEditRolesRoute } from '../../../+collection-page/collection-page-routing-paths'; +import { getCommunityEditRolesRoute } from '../../../+community-page/community-page-routing-paths'; +import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { Group } from '../../../core/eperson/models/group.model'; +import { Collection } from '../../../core/shared/collection.model'; +import { Community } from '../../../core/shared/community.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getRemoteDataPayload, getFirstSucceededRemoteData, getFirstCompletedRemoteData -} from '../../../../core/shared/operators'; -import { AlertType } from '../../../../shared/alert/aletr-type'; -import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component'; -import { hasValue, isNotEmpty, hasValueOperator } from '../../../../shared/empty.util'; -import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { followLink } from '../../../../shared/utils/follow-link-config.model'; -import { NoContent } from '../../../../core/shared/NoContent.model'; +} from '../../../core/shared/operators'; +import { AlertType } from '../../../shared/alert/aletr-type'; +import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; +import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; import { Operation } from 'fast-json-patch'; @Component({ diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html similarity index 100% rename from src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html rename to src/app/access-control/group-registry/group-form/members-list/members-list.component.html diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts similarity index 84% rename from src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts rename to src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts index 10735cbde5..20419fb49d 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -7,25 +7,25 @@ import { Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; -import { RestResponse } from '../../../../../core/cache/response.models'; -import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../../../core/eperson/group-data.service'; -import { EPerson } from '../../../../../core/eperson/models/eperson.model'; -import { Group } from '../../../../../core/eperson/models/group.model'; -import { PageInfo } from '../../../../../core/shared/page-info.model'; -import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; +import { RestResponse } from '../../../../core/cache/response.models'; +import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; import { MembersListComponent } from './members-list.component'; -import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; -import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock'; -import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock'; -import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; -import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub'; -import { RouterMock } from '../../../../../shared/mocks/router.mock'; +import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; +import { RouterMock } from '../../../../shared/mocks/router.mock'; describe('MembersListComponent', () => { let component: MembersListComponent; @@ -66,7 +66,7 @@ describe('MembersListComponent', () => { // empty }, getEPeoplePageRouterLink(): string { - return '/admin/access-control/epeople'; + return '/access-control/epeople'; } }; groupsDataServiceStub = { @@ -97,7 +97,7 @@ describe('MembersListComponent', () => { // empty }, getGroupEditPageRouterLink(group: Group): string { - return '/admin/access-control/groups/' + group.id; + return '/access-control/groups/' + group.id; }, deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable { this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts similarity index 93% rename from src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts rename to src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index c2c267b453..6513881fbf 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts @@ -11,21 +11,20 @@ import { ObservedValueOf, } from 'rxjs'; import { map, mergeMap, switchMap, take } from 'rxjs/operators'; -import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../../../core/eperson/group-data.service'; -import { EPerson } from '../../../../../core/eperson/models/eperson.model'; -import { Group } from '../../../../../core/eperson/models/group.model'; +import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { Group } from '../../../../core/eperson/models/group.model'; import { getRemoteDataPayload, getFirstSucceededRemoteData, - getFirstCompletedRemoteData, - getAllCompletedRemoteData -} from '../../../../../core/shared/operators'; -import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; -import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; -import { EpersonDtoModel } from '../../../../../core/eperson/models/eperson-dto.model'; + getFirstCompletedRemoteData, getAllCompletedRemoteData +} from '../../../../core/shared/operators'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model'; /** * Keys to keep track of specific subscriptions diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html similarity index 100% rename from src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html rename to src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts similarity index 86% rename from src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts rename to src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts index 9841d2b02e..e839d77a6a 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -15,25 +15,25 @@ import { Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf, BehaviorSubject } from 'rxjs'; -import { RestResponse } from '../../../../../core/cache/response.models'; -import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { GroupDataService } from '../../../../../core/eperson/group-data.service'; -import { Group } from '../../../../../core/eperson/models/group.model'; -import { PageInfo } from '../../../../../core/shared/page-info.model'; -import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; +import { RestResponse } from '../../../../core/cache/response.models'; +import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; import { SubgroupsListComponent } from './subgroups-list.component'; import { createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject -} from '../../../../../shared/remote-data.utils'; -import { RouterMock } from '../../../../../shared/mocks/router.mock'; -import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; -import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub'; +} from '../../../../shared/remote-data.utils'; +import { RouterMock } from '../../../../shared/mocks/router.mock'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { map } from 'rxjs/operators'; describe('SubgroupsListComponent', () => { @@ -70,7 +70,7 @@ describe('SubgroupsListComponent', () => { ); }, getGroupEditPageRouterLink(group: Group): string { - return '/admin/access-control/groups/' + group.id; + return '/access-control/groups/' + group.id; }, searchGroups(query: string): Observable>> { if (query === '') { diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts similarity index 93% rename from src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts rename to src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index d754e71e4f..d9f03963d6 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -4,18 +4,18 @@ import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs'; import { map, mergeMap, take } from 'rxjs/operators'; -import { PaginatedList } from '../../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { GroupDataService } from '../../../../../core/eperson/group-data.service'; -import { Group } from '../../../../../core/eperson/models/group.model'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { Group } from '../../../../core/eperson/models/group.model'; import { getRemoteDataPayload, getFirstSucceededRemoteData, getFirstCompletedRemoteData -} from '../../../../../core/shared/operators'; -import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; -import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; -import { NoContent } from '../../../../../core/shared/NoContent.model'; +} from '../../../../core/shared/operators'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { NoContent } from '../../../../core/shared/NoContent.model'; /** * Keys to keep track of specific subscriptions diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts b/src/app/access-control/group-registry/group-registry.actions.ts similarity index 91% rename from src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts rename to src/app/access-control/group-registry/group-registry.actions.ts index b5ba3bc4b5..bc1c0b97a6 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts +++ b/src/app/access-control/group-registry/group-registry.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; -import { Group } from '../../../core/eperson/models/group.model'; -import { type } from '../../../shared/ngrx/type'; +import { Group } from '../../core/eperson/models/group.model'; +import { type } from '../../shared/ngrx/type'; /** * For each action type in an action group, make a simple diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts b/src/app/access-control/group-registry/group-registry.reducers.spec.ts similarity index 96% rename from src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts rename to src/app/access-control/group-registry/group-registry.reducers.spec.ts index 6c9f9d327a..de5b65f5ba 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts +++ b/src/app/access-control/group-registry/group-registry.reducers.spec.ts @@ -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 { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers'; diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts b/src/app/access-control/group-registry/group-registry.reducers.ts similarity index 93% rename from src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts rename to src/app/access-control/group-registry/group-registry.reducers.ts index eca6c282a7..8e288b7f3a 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts +++ b/src/app/access-control/group-registry/group-registry.reducers.ts @@ -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'; /** diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html similarity index 100% rename from src/app/+admin/admin-access-control/group-registry/groups-registry.component.html rename to src/app/access-control/group-registry/groups-registry.component.html diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts b/src/app/access-control/group-registry/groups-registry.component.spec.ts similarity index 80% rename from src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts rename to src/app/access-control/group-registry/groups-registry.component.spec.ts index dd08ea6772..b5b5a1b209 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts +++ b/src/app/access-control/group-registry/groups-registry.component.spec.ts @@ -7,27 +7,27 @@ import { Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; -import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { RequestService } from '../../../core/data/request.service'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { EPerson } from '../../../core/eperson/models/eperson.model'; -import { Group } from '../../../core/eperson/models/group.model'; -import { RouteService } from '../../../core/services/route.service'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { RequestService } from '../../core/data/request.service'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { Group } from '../../core/eperson/models/group.model'; +import { RouteService } from '../../core/services/route.service'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; import { GroupsRegistryComponent } from './groups-registry.component'; -import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { routeServiceStub } from '../../../shared/testing/route-service.stub'; -import { RouterMock } from '../../../shared/mocks/router.mock'; +import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { routeServiceStub } from '../../shared/testing/route-service.stub'; +import { RouterMock } from '../../shared/mocks/router.mock'; describe('GroupRegistryComponent', () => { let component: GroupsRegistryComponent; @@ -98,10 +98,10 @@ describe('GroupRegistryComponent', () => { } }, getGroupEditPageRouterLink(group: Group): string { - return '/admin/access-control/groups/' + group.id; + return '/access-control/groups/' + group.id; }, getGroupRegistryRouterLink(): string { - return '/admin/access-control/groups'; + return '/access-control/groups'; }, searchGroups(query: string): Observable>> { if (query === '') { diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/access-control/group-registry/groups-registry.component.ts similarity index 84% rename from src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts rename to src/app/access-control/group-registry/groups-registry.component.ts index 305da75eeb..bfe6d50c43 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/access-control/group-registry/groups-registry.component.ts @@ -10,29 +10,29 @@ import { of as observableOf } from 'rxjs'; import { catchError, map, switchMap, take } from 'rxjs/operators'; -import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { RequestService } from '../../../core/data/request.service'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { EPerson } from '../../../core/eperson/models/eperson.model'; -import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model'; -import { Group } from '../../../core/eperson/models/group.model'; -import { RouteService } from '../../../core/services/route.service'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { PaginatedList, buildPaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { RequestService } from '../../core/data/request.service'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { GroupDtoModel } from '../../core/eperson/models/group-dto.model'; +import { Group } from '../../core/eperson/models/group.model'; +import { RouteService } from '../../core/services/route.service'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData -} from '../../../core/shared/operators'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { hasValue } from '../../../shared/empty.util'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { NoContent } from '../../../core/shared/NoContent.model'; +} from '../../core/shared/operators'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { NoContent } from '../../core/shared/NoContent.model'; @Component({ selector: 'ds-groups-registry', diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 24108dadd5..08f7b9585f 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -74,3 +74,9 @@ export const INFO_MODULE_PATH = 'info'; export function getInfoModulePath() { return `/${INFO_MODULE_PATH}`; } + +export const ACCESS_CONTROL_MODULE_PATH = 'access-control'; + +export function getAccessControlModuleRoute() { + return `/${ACCESS_CONTROL_MODULE_PATH}`; +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 13e1133d82..ffbd993e8c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,7 +4,17 @@ import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { AuthenticatedGuard } from './core/auth/authenticated.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 { COMMUNITY_MODULE_PATH } from './+community-page/community-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 { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; +import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; @NgModule({ imports: [ @@ -171,6 +182,11 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component loadChildren: () => import('./statistics-page/statistics-page-routing.module') .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 }, ]} ],{ diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 5dffda5e94..5abc378702 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -3,11 +3,11 @@ import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store' import { ePeopleRegistryReducer, EPeopleRegistryState -} from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; +} from './access-control/epeople-registry/epeople-registry.reducers'; import { groupRegistryReducer, GroupRegistryState -} from './+admin/admin-access-control/group-registry/group-registry.reducers'; +} from './access-control/group-registry/group-registry.reducers'; import { metadataRegistryReducer, MetadataRegistryState diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 31de304665..7b9a08de92 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -102,7 +102,7 @@ export class AuthInterceptor implements HttpInterceptor { private parseLocation(header: string): string { let location = header.trim(); location = location.replace('location="', ''); - location = location.replace('"', ''); + location = location.replace('"', ''); /* lgtm [js/incomplete-sanitization] */ let re = /%3A%2F%2F/g; location = location.replace(re, '://'); re = /%3A/g; diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts new file mode 100644 index 0000000000..bc39397ed9 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts @@ -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 { + return observableOf(FeatureID.IsCollectionAdmin); + } +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts new file mode 100644 index 0000000000..afb1fea63d --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts @@ -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 { + return observableOf(FeatureID.IsCommunityAdmin); + } +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts new file mode 100644 index 0000000000..3fee767fdc --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts @@ -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 { + return observableOf(FeatureID.CanManageGroups); + } +} diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 3c17f43cb1..e3473a895e 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -9,4 +9,7 @@ export enum FeatureID { WithdrawItem = 'withdrawItem', ReinstateItem = 'reinstateItem', EPersonRegistration = 'epersonRegistration', + CanManageGroups = 'canManageGroups', + IsCollectionAdmin = 'isCollectionAdmin', + IsCommunityAdmin = 'isCommunityAdmin', } diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index 3af02b267b..cd7b664379 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -10,7 +10,7 @@ import { TestScheduler } from 'rxjs/testing'; import { EPeopleRegistryCancelEPersonAction, 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 { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index 79df246833..bceb38f163 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -7,8 +7,8 @@ import { find, map, take } from 'rxjs/operators'; import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction -} from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions'; -import { EPeopleRegistryState } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; +} from '../../access-control/epeople-registry/epeople-registry.actions'; +import { EPeopleRegistryState } from '../../access-control/epeople-registry/epeople-registry.reducers'; import { AppState } from '../../app.reducer'; import { hasValue, hasNoValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -269,7 +269,7 @@ export class EPersonDataService extends DataService { this.editEPerson(ePerson); } }); - return '/admin/access-control/epeople'; + return '/access-control/epeople'; } /** @@ -277,7 +277,7 @@ export class EPersonDataService extends DataService { * @param ePerson New EPerson to edit */ public getEPeoplePageRouterLink(): string { - return '/admin/access-control/epeople'; + return '/access-control/epeople'; } /** diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts index f77c831b55..378c3c9667 100644 --- a/src/app/core/eperson/group-data.service.spec.ts +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -8,7 +8,7 @@ import { compare, Operation } from 'fast-json-patch'; import { GroupRegistryCancelGroupAction, 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 { RequestParam } from '../cache/models/request-param.model'; import { CoreState } from '../core.reducers'; diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index dc5fd97d9a..5b8f474d1a 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -7,8 +7,8 @@ import { filter, map, take } from 'rxjs/operators'; import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction -} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; -import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers'; +} from '../../access-control/group-registry/group-registry.actions'; +import { GroupRegistryState } from '../../access-control/group-registry/group-registry.reducers'; import { AppState } from '../../app.reducer'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -209,7 +209,7 @@ export class GroupDataService extends DataService { } public getGroupRegistryRouterLink(): string { - return '/admin/access-control/groups'; + return '/access-control/groups'; } /** @@ -240,7 +240,7 @@ export class GroupDataService extends DataService { * @param groupID Group ID we want edit page for */ public getGroupEditPageRouterLinkWithID(groupId: string): string { - return '/admin/access-control/groups/' + groupId; + return '/access-control/groups/' + groupId; } /** diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index fdfe1c9fac..5f2f123f01 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -326,6 +326,25 @@ describe('RegistryService', () => { }); }); + describe('when createMetadataField is called with a blank qualifier', () => { + let result: Observable; + 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', () => { let result: Observable; @@ -341,6 +360,25 @@ describe('RegistryService', () => { }); }); + describe('when updateMetadataField is called with a blank qualifier', () => { + let result: Observable; + 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', () => { let result: Observable>; diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 9ac849bdd3..b7b35c6a5a 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -245,6 +245,9 @@ export class RegistryService { * @param schema The MetadataSchema to create the field in */ public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable { + if (!field.qualifier) { + field.qualifier = null; + } return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe( getFirstSucceededRemoteDataPayload(), hasValueOperator(), @@ -260,6 +263,9 @@ export class RegistryService { * @param field The MetadataField to update */ public updateMetadataField(field: MetadataField): Observable { + if (!field.qualifier) { + field.qualifier = null; + } return this.metadataFieldService.put(field).pipe( getFirstSucceededRemoteDataPayload(), hasValueOperator(), diff --git a/src/app/core/shared/media-viewer-item.model.ts b/src/app/core/shared/media-viewer-item.model.ts new file mode 100644 index 0000000000..cd3a31bd0b --- /dev/null +++ b/src/app/core/shared/media-viewer-item.model.ts @@ -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; +} diff --git a/src/app/core/shared/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts index ec0b3dd3ba..bc91d0585e 100644 --- a/src/app/core/shared/metadata.utils.spec.ts +++ b/src/app/core/shared/metadata.utils.spec.ts @@ -20,6 +20,7 @@ const dcTitle0 = mdValue('Title 0'); const dcTitle1 = mdValue('Title 1'); const dcTitle2 = mdValue('Title 2', 'en_US'); const bar = mdValue('Bar'); +const test = mdValue('Test'); const singleMap = { 'dc.title': [dcTitle0] }; @@ -30,6 +31,11 @@ const multiMap = { 'foo': [bar] }; +const regexTestMap = { + 'foolbar.baz': [test], + 'foo.bard': [test], +}; + const multiViewModelList = [ { key: 'dc.description', ...dcDescription, 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.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]); }); + describe('with regexTestMap', () => { + testAll(regexTestMap, 'foo.bar.*', []); + }); }); describe('allValues method', () => { diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index 612fba1d4a..3fbeb205d2 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -156,7 +156,7 @@ export class Metadata { const outputKeys: string[] = []; for (const inputKey of inputKeys) { 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)) { if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) { outputKeys.push(mapKey); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts index 7ec422e69d..d155d66a0a 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts @@ -9,7 +9,7 @@ import { getAllCompletedRemoteData, getFirstCompletedRemoteData } from '../../.. import { RequestService } from '../../../../core/data/request.service'; import { RemoteData } from '../../../../core/data/remote-data'; 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 { NoContent } from '../../../../core/shared/NoContent.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 7473f894fa..bfa9c214e9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -11,12 +11,15 @@ 'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
- + + +
-
- {{ message | translate: model.validators }} -
+
+ {{ message | translate: model.validators }} +
@@ -32,12 +35,12 @@
-
+
@@ -65,6 +68,9 @@ [relationshipOptions]="model.relationship" > + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 014c25e189..fd403561e9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -37,7 +37,6 @@ import { DynamicFormControl, DynamicFormControlContainerComponent, DynamicFormControlEvent, - DynamicFormControlEventType, DynamicFormControlModel, DynamicFormLayout, 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 { getAllSucceededRemoteData, + getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getPaginatedListPayload, - getRemoteDataPayload, - getFirstSucceededRemoteData + getRemoteDataPayload } from '../../../../core/shared/operators'; import { RemoteData } from '../../../../core/data/remote-data'; 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 { return this.model.value.pipe(map((list: SearchResult[]) => isNotEmpty(list))); } @@ -385,6 +393,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, { size: 'lg' }); + + if (hasValue(this.model.value)) { + this.submissionService.dispatchSave(this.model.submissionId); + } + const modalComp = this.modalRef.componentInstance; 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.listId = this.listId; modalComp.relationshipOptions = this.model.relationship; @@ -437,6 +438,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo .forEach((sub) => sub.unsubscribe()); } + get hasHint(): boolean { + return isNotEmpty(this.model.hint) && this.model.hint !== ' '; + } + /** * Initialize this.item$ based on this.model.submissionId */ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html index 57ab7d66d8..07ea131a00 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -8,6 +8,7 @@ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts index c606145c03..3a5623cfdd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -14,6 +14,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../../../../testing/translate-loader.mock'; describe('ExistingMetadataListElementComponent', () => { let component: ExistingMetadataListElementComponent; @@ -65,6 +67,14 @@ describe('ExistingMetadataListElementComponent', () => { beforeEach(waitForAsync(() => { init(); TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], declarations: [ExistingMetadataListElementComponent], providers: [ { provide: SelectableListService, useValue: selectionService }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index 7cd153ef5c..fe0c65ea73 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -2,44 +2,42 @@
-
- - - -
-
-
-
- - - - - - -
-
+ + +
+ +
+ + + + + +
+
+ + +
+ - - - - - diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss index 8fb13f9c55..634a197123 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss @@ -5,48 +5,41 @@ } .cdk-drag { - margin-left: calc(-2 * var(--bs-spacer)); - margin-right: calc(-0.5 * var(--bs-spacer)); - padding-right: calc(0.5 * var(--bs-spacer)); - .drag-icon { - visibility: hidden; - width: calc(2 * var(--bs-spacer)); - color: var(--bs-gray-600); - margin: var(--bs-btn-padding-y) 0; - line-height: var(--bs-btn-line-height); - text-indent: calc(0.5 * var(--bs-spacer)) - } + margin-left: calc(-2.3 * var(--bs-spacer)); + margin-right: calc(-0.5 * var(--bs-spacer)); + padding-right: calc(0.5 * var(--bs-spacer)); + .drag-icon { + visibility: hidden; + width: calc(2 * var(--bs-spacer)); + color: var(--bs-gray-600); + margin: var(--bs-btn-padding-y) 0; + line-height: var(--bs-btn-line-height); + text-indent: calc(0.5 * var(--bs-spacer)) + } - &:hover, &:focus { - cursor: grab; - .drag-icon { - visibility: visible; - } + &:hover, &:focus { + cursor: grab; + .drag-icon { + visibility: visible; } + } } .cdk-drop-list-dragging { - .cdk-drag { - cursor: grabbing; - .drag-icon { - visibility: hidden; - } + .cdk-drag { + cursor: grabbing; + .drag-icon { + visibility: hidden; } + } } .cdk-drag-preview { - background-color: white; - border-radius: var(--bs-border-radius-sm); - 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; - } + margin: 0; + padding: 0; } .cdk-drag-placeholder { - opacity: 0; + opacity: 0; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts index 80b6e000b8..8ab38454a7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts @@ -5,15 +5,15 @@ import { DynamicFormArrayComponent, DynamicFormControlCustomEvent, DynamicFormControlEvent, - DynamicFormControlEventType, - DynamicFormControlLayout, DynamicFormLayout, + DynamicFormControlLayout, + DynamicFormLayout, DynamicFormLayoutService, DynamicFormValidationService, DynamicTemplateDirective } from '@ng-dynamic-forms/core'; import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model'; -import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model'; import { hasValue } from '../../../../../empty.util'; +import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model'; @Component({ selector: 'ds-dynamic-form-array', @@ -25,7 +25,7 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { @Input() formLayout: DynamicFormLayout; @Input() group: FormGroup; @Input() layout: DynamicFormControlLayout; - @Input() model: DynamicRowArrayModel; + @Input() model: DynamicRowArrayModel;// DynamicRow? @Input() templates: QueryList | undefined; /* tslint:disable:no-output-rename */ @@ -43,22 +43,25 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { } moveSelection(event: CdkDragDrop) { + + // prevent propagating events generated releasing on the same position + if (event.previousIndex === event.currentIndex) { + return; + } + this.model.moveGroup(event.previousIndex, event.currentIndex - event.previousIndex); - const prevIndex = event.previousIndex - 1; - const index = event.currentIndex - 1; + const prevIndex = event.previousIndex; + const index = event.currentIndex; if (hasValue(this.model.groups[index]) && hasValue((this.control as any).controls[index])) { - const $event = { - $event: { previousIndex: prevIndex }, - context: { index }, - control: (this.control as any).controls[index], - group: this.group, + this.onCustomEvent({ + previousIndex: prevIndex, + index, + arrayModel: this.model, model: this.model.groups[index].group[0], - type: DynamicFormControlEventType.Change - }; - - this.onChange($event); - } + control: (this.control as any).controls[index] + }, 'move'); + } } update(event: any, index: number) { @@ -68,4 +71,11 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { this.onChange($event); } + + /** + * If the drag feature is disabled for this DynamicRowArrayModel. + */ + get dragDisabled(): boolean { + return this.model.groups.length === 1 || !this.model.isDraggable; + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index 8925d8fd87..d0b07de885 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -9,6 +9,7 @@ export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig metadataKey: string; metadataFields: string[]; hasSelectableMetadata: boolean; + isDraggable: boolean; } export class DynamicRowArrayModel extends DynamicFormArrayModel { @@ -19,6 +20,7 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel { @serializable() metadataKey: string; @serializable() metadataFields: string[]; @serializable() hasSelectableMetadata: boolean; + @serializable() isDraggable: boolean; isRowArray = true; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { @@ -30,5 +32,6 @@ export class DynamicRowArrayModel extends DynamicFormArrayModel { this.metadataKey = config.metadataKey; this.metadataFields = config.metadataFields; this.hasSelectableMetadata = config.hasSelectableMetadata; + this.isDraggable = config.isDraggable; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 3442ddb1ba..339f6f278d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -6,7 +6,8 @@