mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into iiif-mirador
This commit is contained in:
@@ -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",
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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 },
|
||||
|
@@ -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,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -35,7 +35,7 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<div class="pl-2">
|
||||
<ds-dso-page-edit-button [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
|
||||
<ds-dso-page-edit-button *ngIf="isCollectionAdmin$ | async" [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
|
@@ -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<boolean>;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
@@ -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: '',
|
||||
|
@@ -21,7 +21,7 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<div class="pl-2">
|
||||
<ds-dso-page-edit-button [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
|
||||
<ds-dso-page-edit-button *ngIf="isCommunityAdmin$ | async" [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
|
@@ -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<RemoteData<Community>>;
|
||||
|
||||
/**
|
||||
* Whether the current user is a Community admin
|
||||
*/
|
||||
isCommunityAdmin$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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: '',
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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<RelationshipType[]>((includedTypes, type, index) => {
|
||||
if (!includedTypes.some((includedType) => includedType.id === type.id)
|
||||
@@ -133,8 +136,8 @@ export class ItemDeleteComponent
|
||||
}
|
||||
}, [])
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.types$ = observableOf([]);
|
||||
|
@@ -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
|
||||
|
@@ -0,0 +1,7 @@
|
||||
<div [class.change-gallery]="isAuthenticated$ | async">
|
||||
<ngx-gallery
|
||||
class="ngx-gallery"
|
||||
[options]="galleryOptions"
|
||||
[images]="galleryImages"
|
||||
></ngx-gallery>
|
||||
</div>
|
@@ -0,0 +1,6 @@
|
||||
.ngx-gallery {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
width: 340px !important;
|
||||
height: 279px !important;
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
|
||||
|
||||
import { MediaViewerImageComponent } from './media-viewer-image.component';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
|
||||
describe('MediaViewerImageComponent', () => {
|
||||
let component: MediaViewerImageComponent;
|
||||
let fixture: ComponentFixture<MediaViewerImageComponent>;
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(false)
|
||||
});
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
},
|
||||
content: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
},
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
|
||||
new Array<MediaViewerItem>(),
|
||||
[
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
||||
]
|
||||
);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports:[],
|
||||
declarations: [MediaViewerImageComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaViewerImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.galleryOptions = [new NgxGalleryOptions({})];
|
||||
component.galleryImages = component.convertToGalleryImage(
|
||||
mockMediaViewerItems
|
||||
);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should contain a gallery options', () => {
|
||||
expect(component.galleryOptions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should contain an image array', () => {
|
||||
expect(component.galleryImages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
@@ -0,0 +1,88 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { NgxGalleryAnimation } from '@kolkov/ngx-gallery';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This componenet render an image gallery for the image viewer
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-media-viewer-image',
|
||||
templateUrl: './media-viewer-image.component.html',
|
||||
styleUrls: ['./media-viewer-image.component.scss'],
|
||||
})
|
||||
export class MediaViewerImageComponent implements OnInit {
|
||||
@Input() images: MediaViewerItem[];
|
||||
@Input() preview?: boolean;
|
||||
@Input() image?: string;
|
||||
|
||||
loggedin: boolean;
|
||||
|
||||
galleryOptions: NgxGalleryOptions[];
|
||||
galleryImages: NgxGalleryImage[];
|
||||
|
||||
/**
|
||||
* Whether or not the current user is authenticated
|
||||
*/
|
||||
isAuthenticated$: Observable<boolean>;
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
/**
|
||||
* Thi method sets up the gallery settings and data
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isAuthenticated$ = this.authService.isAuthenticated();
|
||||
this.galleryOptions = [
|
||||
{
|
||||
preview: this.preview !== undefined ? this.preview : true,
|
||||
image: true,
|
||||
imageSize: 'contain',
|
||||
thumbnails: false,
|
||||
imageArrows: false,
|
||||
startIndex: 0,
|
||||
imageAnimation: NgxGalleryAnimation.Slide,
|
||||
previewCloseOnEsc: true,
|
||||
previewZoom: true,
|
||||
previewRotate: true,
|
||||
previewFullscreen: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.image) {
|
||||
this.galleryImages = [
|
||||
{
|
||||
small: this.image,
|
||||
medium: this.image,
|
||||
big: this.image,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
this.galleryImages = this.convertToGalleryImage(this.images);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method convert an array of MediaViewerItem into NgxGalleryImage array
|
||||
* @param medias input NgxGalleryImage array
|
||||
*/
|
||||
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
|
||||
const mappadImages = [];
|
||||
for (const image of medias) {
|
||||
if (image.format === 'image') {
|
||||
mappadImages.push({
|
||||
small: image.thumbnail
|
||||
? image.thumbnail
|
||||
: './assets/images/replacement_image.svg',
|
||||
medium: image.thumbnail
|
||||
? image.thumbnail
|
||||
: './assets/images/replacement_image.svg',
|
||||
big: image.bitstream._links.content.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
return mappadImages;
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<video
|
||||
#media
|
||||
[src]="filteredMedias[currentIndex].bitstream._links.content.href"
|
||||
id="singleVideo"
|
||||
[poster]="
|
||||
filteredMedias[currentIndex].thumbnail ||
|
||||
replacements[filteredMedias[currentIndex].format]
|
||||
"
|
||||
preload="none"
|
||||
controls
|
||||
></video>
|
||||
<div class="buttons" *ngIf="filteredMedias?.length > 1">
|
||||
<button
|
||||
class="btn btn-primary previous"
|
||||
[disabled]="currentIndex === 0"
|
||||
(click)="prevMedia()"
|
||||
>
|
||||
{{ "media-viewer.previous" | translate }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary next"
|
||||
[disabled]="currentIndex === filteredMedias.length - 1"
|
||||
(click)="nextMedia()"
|
||||
>
|
||||
{{ "media-viewer.next" | translate }}
|
||||
</button>
|
||||
<div ngbDropdown class="d-inline-block">
|
||||
<button
|
||||
class="btn btn-outline-primary playlist"
|
||||
id="dropdownBasic1"
|
||||
ngbDropdownToggle
|
||||
>
|
||||
{{ "media-viewer.playlist" | translate }}
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<button
|
||||
ngbDropdownItem
|
||||
*ngFor="let item of filteredMedias; index as indexOfelement"
|
||||
class="list-element"
|
||||
(click)="selectedMedia(indexOfelement)"
|
||||
>
|
||||
{{ item.bitstream.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,4 @@
|
||||
video {
|
||||
width: 340px;
|
||||
height: 279px;
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||
import { FileSizePipe } from '../../../shared/utils/file-size-pipe';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { MetadataFieldWrapperComponent } from '../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
|
||||
import { MediaViewerVideoComponent } from './media-viewer-video.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('MediaViewerVideoComponent', () => {
|
||||
let component: MediaViewerVideoComponent;
|
||||
let fixture: ComponentFixture<MediaViewerVideoComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
declarations: [
|
||||
MediaViewerVideoComponent,
|
||||
VarDirective,
|
||||
FileSizePipe,
|
||||
MetadataFieldWrapperComponent,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
},
|
||||
content: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
},
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
|
||||
new Array<MediaViewerItem>(),
|
||||
[
|
||||
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
|
||||
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
|
||||
]
|
||||
);
|
||||
const mockMediaViewerItem: MediaViewerItem[] = Object.assign(
|
||||
new Array<MediaViewerItem>(),
|
||||
[{ bitstream: mockBitstream, format: 'video', thumbnail: null }]
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaViewerVideoComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.medias = mockMediaViewerItem;
|
||||
component.filteredMedias = mockMediaViewerItem;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('should show controller buttons when the having mode then one video', () => {
|
||||
beforeEach(() => {
|
||||
component.medias = mockMediaViewerItems;
|
||||
component.filteredMedias = mockMediaViewerItems;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show buttons', () => {
|
||||
const controllerButtons = fixture.debugElement.query(By.css('.buttons'));
|
||||
expect(controllerButtons).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the "Next" button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
component.currentIndex = 0;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should increase the index', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.next'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(component.currentIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "Previous" button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
component.currentIndex = 1;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should decrease the index', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.previous'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(component.currentIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "Playlist element" button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
component.isCollapsed = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should set the the index with the selected one', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.list-element'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(component.currentIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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--;
|
||||
}
|
||||
}
|
36
src/app/+item-page/media-viewer/media-viewer.component.html
Normal file
36
src/app/+item-page/media-viewer/media-viewer.component.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<ng-container *ngVar="mediaList$ | async as mediaList">
|
||||
<ds-loading
|
||||
*ngIf="isLoading"
|
||||
message="{{ 'loading.default' | translate }}"
|
||||
[showMessage]="false"
|
||||
></ds-loading>
|
||||
<div class="media-viewer" *ngIf="!isLoading">
|
||||
<ng-container *ngIf="mediaList.length > 0">
|
||||
<ng-container *ngIf="videoOptions">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
mediaList[0]?.format === 'video' || mediaList[0]?.format === 'audio'
|
||||
"
|
||||
>
|
||||
<ds-media-viewer-video [medias]="mediaList"></ds-media-viewer-video>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="mediaList[0]?.format === 'image'">
|
||||
<ds-media-viewer-image [images]="mediaList"></ds-media-viewer-image>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
((mediaList[0]?.format !== 'image') &&
|
||||
(!videoOptions || mediaList[0]?.format !== 'video') &&
|
||||
(!videoOptions || mediaList[0]?.format !== 'audio')) ||
|
||||
mediaList.length === 0
|
||||
"
|
||||
>
|
||||
<ds-media-viewer-image
|
||||
[image]="mediaList[0]?.thumbnail || thumbnailPlaceholder"
|
||||
[preview]="false"
|
||||
></ds-media-viewer-image>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
@@ -0,0 +1 @@
|
||||
|
143
src/app/+item-page/media-viewer/media-viewer.component.spec.ts
Normal file
143
src/app/+item-page/media-viewer/media-viewer.component.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MediaViewerComponent } from './media-viewer.component';
|
||||
import { MockBitstreamFormat1 } from '../../shared/mocks/item.mock';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { MetadataFieldWrapperComponent } from '../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||
|
||||
describe('MediaViewerComponent', () => {
|
||||
let comp: MediaViewerComponent;
|
||||
let fixture: ComponentFixture<MediaViewerComponent>;
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
},
|
||||
content: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
},
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(
|
||||
createPaginatedList([mockBitstream])
|
||||
),
|
||||
});
|
||||
|
||||
const mockMediaViewerItem: MediaViewerItem = Object.assign(
|
||||
new MediaViewerItem(),
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null }
|
||||
);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
declarations: [
|
||||
MediaViewerComponent,
|
||||
VarDirective,
|
||||
FileSizePipe,
|
||||
MetadataFieldWrapperComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaViewerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the bitstreams are loading', () => {
|
||||
beforeEach(() => {
|
||||
comp.mediaList$.next([mockMediaViewerItem]);
|
||||
comp.videoOptions = true;
|
||||
comp.isLoading = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call the createMediaViewerItem', () => {
|
||||
const mediaItem = comp.createMediaViewerItem(
|
||||
mockBitstream,
|
||||
MockBitstreamFormat1,
|
||||
undefined
|
||||
);
|
||||
expect(mediaItem).toBeTruthy();
|
||||
expect(mediaItem.thumbnail).toBe(null);
|
||||
});
|
||||
|
||||
it('should display a loading component', () => {
|
||||
const loading = fixture.debugElement.query(By.css('ds-loading'));
|
||||
expect(loading.nativeElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the bitstreams loading is failed', () => {
|
||||
beforeEach(() => {
|
||||
comp.mediaList$.next([]);
|
||||
comp.videoOptions = true;
|
||||
comp.isLoading = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call the createMediaViewerItem', () => {
|
||||
const mediaItem = comp.createMediaViewerItem(
|
||||
mockBitstream,
|
||||
MockBitstreamFormat1,
|
||||
undefined
|
||||
);
|
||||
expect(mediaItem).toBeTruthy();
|
||||
expect(mediaItem.thumbnail).toBe(null);
|
||||
});
|
||||
|
||||
it('should display a default, thumbnail', () => {
|
||||
const defaultThumbnail = fixture.debugElement.query(
|
||||
By.css('ds-media-viewer-image')
|
||||
);
|
||||
expect(defaultThumbnail.nativeElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
114
src/app/+item-page/media-viewer/media-viewer.component.ts
Normal file
114
src/app/+item-page/media-viewer/media-viewer.component.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { filter, take } from 'rxjs/operators';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* This componenet renders the media viewers
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ds-media-viewer',
|
||||
templateUrl: './media-viewer.component.html',
|
||||
styleUrls: ['./media-viewer.component.scss'],
|
||||
})
|
||||
export class MediaViewerComponent implements OnInit {
|
||||
@Input() item: Item;
|
||||
@Input() videoOptions: boolean;
|
||||
|
||||
mediaList$: BehaviorSubject<MediaViewerItem[]>;
|
||||
|
||||
isLoading: boolean;
|
||||
|
||||
thumbnailPlaceholder = './assets/images/replacement_document.svg';
|
||||
|
||||
constructor(protected bitstreamDataService: BitstreamDataService) {}
|
||||
|
||||
/**
|
||||
* This metod loads all the Bitstreams and Thumbnails and contert it to media item
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.mediaList$ = new BehaviorSubject([]);
|
||||
this.isLoading = true;
|
||||
this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => {
|
||||
if (bitstreamsRD.payload.page.length === 0) {
|
||||
this.isLoading = false;
|
||||
this.mediaList$.next([]);
|
||||
} else {
|
||||
this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => {
|
||||
for (
|
||||
let index = 0;
|
||||
index < bitstreamsRD.payload.page.length;
|
||||
index++
|
||||
) {
|
||||
bitstreamsRD.payload.page[index].format
|
||||
.pipe(getFirstSucceededRemoteDataPayload())
|
||||
.subscribe((format) => {
|
||||
const current = this.mediaList$.getValue();
|
||||
const mediaItem = this.createMediaViewerItem(
|
||||
bitstreamsRD.payload.page[index],
|
||||
format,
|
||||
thumbnailsRD.payload && thumbnailsRD.payload.page[index]
|
||||
);
|
||||
this.mediaList$.next([...current, mediaItem]);
|
||||
});
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
|
||||
* @param bundleName Bundle name
|
||||
*/
|
||||
loadRemoteData(
|
||||
bundleName: string
|
||||
): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||
return this.bitstreamDataService
|
||||
.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
bundleName,
|
||||
{},
|
||||
true,
|
||||
true,
|
||||
followLink('format')
|
||||
)
|
||||
.pipe(
|
||||
filter(
|
||||
(bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) =>
|
||||
hasValue(bitstreamsRD) &&
|
||||
(hasValue(bitstreamsRD.errorMessage) || hasValue(bitstreamsRD.payload))
|
||||
),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method create MediaViewerItem from incoming bitstreams
|
||||
* @param original original remote data bitstream
|
||||
* @param format original bitstream format
|
||||
* @param thumbnail trunbnail remote data bitstream
|
||||
*/
|
||||
createMediaViewerItem(
|
||||
original: Bitstream,
|
||||
format: BitstreamFormat,
|
||||
thumbnail: Bitstream
|
||||
): MediaViewerItem {
|
||||
const mediaItem = new MediaViewerItem();
|
||||
mediaItem.bitstream = original;
|
||||
mediaItem.format = format.mimetype.split('/')[0];
|
||||
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
|
||||
return mediaItem;
|
||||
}
|
||||
}
|
@@ -8,9 +8,14 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<ng-container *ngIf="!mediaViewer.image">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="mediaViewer.image">
|
||||
<ds-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-media-viewer>
|
||||
</ng-container>
|
||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
||||
|
@@ -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) {
|
||||
}
|
||||
|
@@ -8,9 +8,14 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<ng-container *ngIf="!mediaViewer.image">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="mediaViewer.image">
|
||||
<ds-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-media-viewer>
|
||||
</ng-container>
|
||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
||||
|
@@ -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';
|
||||
|
@@ -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 {
|
||||
|
||||
}
|
@@ -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 {
|
||||
|
||||
}
|
@@ -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
|
@@ -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', {
|
@@ -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',
|
@@ -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,
|
@@ -1,4 +1,4 @@
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import {
|
||||
EPeopleRegistryAction,
|
||||
EPeopleRegistryActionTypes,
|
@@ -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;
|
@@ -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',
|
@@ -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;
|
@@ -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({
|
@@ -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<RestResponse> {
|
||||
this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {
|
@@ -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
|
@@ -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<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
@@ -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
|
@@ -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
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
/**
|
@@ -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<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
@@ -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',
|
@@ -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}`;
|
||||
}
|
||||
|
@@ -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 },
|
||||
]}
|
||||
],{
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../feature-id';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user
|
||||
* isn't a Collection administrator
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CollectionAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group management rights
|
||||
*/
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.IsCollectionAdmin);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../feature-id';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user
|
||||
* isn't a Community administrator
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CommunityAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group management rights
|
||||
*/
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.IsCommunityAdmin);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../feature-id';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group
|
||||
* management rights
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GroupAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check group management rights
|
||||
*/
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanManageGroups);
|
||||
}
|
||||
}
|
@@ -9,4 +9,7 @@ export enum FeatureID {
|
||||
WithdrawItem = 'withdrawItem',
|
||||
ReinstateItem = 'reinstateItem',
|
||||
EPersonRegistration = 'epersonRegistration',
|
||||
CanManageGroups = 'canManageGroups',
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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<EPerson> {
|
||||
this.editEPerson(ePerson);
|
||||
}
|
||||
});
|
||||
return '/admin/access-control/epeople';
|
||||
return '/access-control/epeople';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,7 +277,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
* @param ePerson New EPerson to edit
|
||||
*/
|
||||
public getEPeoplePageRouterLink(): string {
|
||||
return '/admin/access-control/epeople';
|
||||
return '/access-control/epeople';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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';
|
||||
|
@@ -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<Group> {
|
||||
}
|
||||
|
||||
public getGroupRegistryRouterLink(): string {
|
||||
return '/admin/access-control/groups';
|
||||
return '/access-control/groups';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,7 +240,7 @@ export class GroupDataService extends DataService<Group> {
|
||||
* @param groupID Group ID we want edit page for
|
||||
*/
|
||||
public getGroupEditPageRouterLinkWithID(groupId: string): string {
|
||||
return '/admin/access-control/groups/' + groupId;
|
||||
return '/access-control/groups/' + groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -326,6 +326,25 @@ describe('RegistryService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when createMetadataField is called with a blank qualifier', () => {
|
||||
let result: Observable<MetadataField>;
|
||||
let metadataField: MetadataField;
|
||||
|
||||
beforeEach(() => {
|
||||
metadataField = mockFieldsList[0];
|
||||
metadataField.qualifier = '';
|
||||
result = registryService.createMetadataField(metadataField, mockSchemasList[0]);
|
||||
});
|
||||
|
||||
it('should return the created metadata field with a null qualifier', (done) => {
|
||||
metadataField.qualifier = null;
|
||||
result.subscribe((field: MetadataField) => {
|
||||
expect(field).toEqual(metadataField);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateMetadataField is called', () => {
|
||||
let result: Observable<MetadataField>;
|
||||
|
||||
@@ -341,6 +360,25 @@ describe('RegistryService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateMetadataField is called with a blank qualifier', () => {
|
||||
let result: Observable<MetadataField>;
|
||||
let metadataField: MetadataField;
|
||||
|
||||
beforeEach(() => {
|
||||
metadataField = mockFieldsList[0];
|
||||
metadataField.qualifier = '';
|
||||
result = registryService.updateMetadataField(metadataField);
|
||||
});
|
||||
|
||||
it('should return the updated metadata field with a null qualifier', (done) => {
|
||||
metadataField.qualifier = null;
|
||||
result.subscribe((field: MetadataField) => {
|
||||
expect(field).toEqual(metadataField);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when deleteMetadataSchema is called', () => {
|
||||
let result: Observable<RemoteData<NoContent>>;
|
||||
|
||||
|
@@ -245,6 +245,9 @@ export class RegistryService {
|
||||
* @param schema The MetadataSchema to create the field in
|
||||
*/
|
||||
public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable<MetadataField> {
|
||||
if (!field.qualifier) {
|
||||
field.qualifier = null;
|
||||
}
|
||||
return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
hasValueOperator(),
|
||||
@@ -260,6 +263,9 @@ export class RegistryService {
|
||||
* @param field The MetadataField to update
|
||||
*/
|
||||
public updateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||
if (!field.qualifier) {
|
||||
field.qualifier = null;
|
||||
}
|
||||
return this.metadataFieldService.put(field).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
hasValueOperator(),
|
||||
|
21
src/app/core/shared/media-viewer-item.model.ts
Normal file
21
src/app/core/shared/media-viewer-item.model.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Bitstream } from './bitstream.model';
|
||||
|
||||
/**
|
||||
* Model representing a media viewer item
|
||||
*/
|
||||
export class MediaViewerItem {
|
||||
/**
|
||||
* Incoming Bitsream
|
||||
*/
|
||||
bitstream: Bitstream;
|
||||
|
||||
/**
|
||||
* Incoming Bitsream format type
|
||||
*/
|
||||
format: string;
|
||||
|
||||
/**
|
||||
* Incoming Bitsream thumbnail
|
||||
*/
|
||||
thumbnail: string;
|
||||
}
|
@@ -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', () => {
|
||||
|
@@ -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);
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -11,12 +11,15 @@
|
||||
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
|
||||
<div [ngClass]="getClass('grid', 'control')">
|
||||
<ng-container #componentViewContainer></ng-container>
|
||||
<small *ngIf="hasHint && (model.repeatable === false || context?.index === 0) && (!showErrorMessages || errorMessages.length === 0)"
|
||||
class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
|
||||
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
|
||||
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
|
||||
<!-- In case of repeatable fields show empty space for all elements except the first -->
|
||||
<div *ngIf="context?.index !== null
|
||||
&& (!showErrorMessages || errorMessages.length === 0)" class="clearfix w-100 mb-2"></div>
|
||||
|
||||
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
|
||||
</div>
|
||||
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
|
||||
@@ -32,12 +35,12 @@
|
||||
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="isRelationship" class="col-auto text-center" [class.invisible]="context?.index > 0">
|
||||
<div *ngIf="isRelationship && !isVirtual()" class="col-auto text-center">
|
||||
<button class="btn btn-secondary"
|
||||
type="button"
|
||||
ngbTooltip="{{'form.lookup-help' | translate}}"
|
||||
placement="top"
|
||||
(click)="openLookup(); $event.stopPropagation();">{{'form.lookup' | translate}}
|
||||
(click)="openLookup(); $event.stopPropagation();"><i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,6 +68,9 @@
|
||||
[relationshipOptions]="model.relationship"
|
||||
>
|
||||
</ds-existing-relation-list-element>
|
||||
<small *ngIf="hasHint && (model.repeatable === false || context?.index === context?.context?.groups?.length - 1) && (!showErrorMessages || errorMessages.length === 0)"
|
||||
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
|
||||
<div class="clearfix w-100 mb-2"></div>
|
||||
</ng-container>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
@@ -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<boolean> {
|
||||
return this.model.value.pipe(map((list: SearchResult<DSpaceObject>[]) => isNotEmpty(list)));
|
||||
}
|
||||
@@ -385,6 +393,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
||||
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
|
||||
*/
|
||||
|
@@ -8,6 +8,7 @@
|
||||
</ng-container>
|
||||
</span>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
title="{{'form.remove' | translate}}"
|
||||
(click)="removeSelection()">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
@@ -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 },
|
||||
|
@@ -2,44 +2,42 @@
|
||||
<div [id]="id"
|
||||
[formArrayName]="model.id"
|
||||
[ngClass]="getClass('element', 'control')">
|
||||
<div role="group"
|
||||
formGroupName="0" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]">
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model.groups[0]"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="controlContainer; context: {$implicit: 0}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model.groups[0]"></ng-container>
|
||||
</div>
|
||||
<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
|
||||
<div *ngFor="let groupModel of model.groups; let idx = index"
|
||||
[ngClass]="{'pt-2 pb-2': idx > 0}" cdkDrag cdkDragHandle>
|
||||
<div [formGroupName]="idx"
|
||||
[class]="getClass('element', 'group') + ' ' + getClass('grid', 'group')"
|
||||
[ngClass]="{'d-flex align-items-center': idx > 0}"
|
||||
>
|
||||
<ng-container *ngIf="idx > 0">
|
||||
<i class="drag-icon fas fa-grip-vertical fa-fw"></i>
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="controlContainer; context: {$implicit: idx}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draggable Container -->
|
||||
<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
|
||||
<!-- Draggable Items -->
|
||||
<div *ngFor="let groupModel of model.groups; let idx = index"
|
||||
role="group"
|
||||
[formGroupName]="idx"
|
||||
[ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]"
|
||||
cdkDrag
|
||||
cdkDragHandle
|
||||
[cdkDragDisabled]="dragDisabled"
|
||||
[cdkDragPreviewClass]="'ds-submission-reorder-dragging'">
|
||||
<!-- Item content -->
|
||||
<i class="drag-icon fas fa-grip-vertical fa-fw" [class.invisible]="dragDisabled"></i>
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
<ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"
|
||||
[bindId]="false"
|
||||
[formGroup]="group"
|
||||
[context]="groupModel"
|
||||
[group]="control.get([idx])"
|
||||
[hidden]="_model.hidden"
|
||||
[layout]="formLayout"
|
||||
[model]="_model"
|
||||
[templates]="templates"
|
||||
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onChange($event)"
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
|
||||
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-template #controlContainer let-idx>
|
||||
<ds-dynamic-form-control-container *ngFor="let _model of model.groups[idx].group"
|
||||
[bindId]="false"
|
||||
[context]="model.groups[idx]"
|
||||
[group]="control.get([idx])"
|
||||
[hidden]="_model.hidden"
|
||||
[layout]="formLayout"
|
||||
[model]="_model"
|
||||
[templates]="templates"
|
||||
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
|
||||
(dfBlur)="update($event, idx)"
|
||||
(dfChange)="update($event, idx)"
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
|
||||
</ng-template>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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<DynamicTemplateDirective> | undefined;
|
||||
|
||||
/* tslint:disable:no-output-rename */
|
||||
@@ -43,22 +43,25 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
|
||||
}
|
||||
|
||||
moveSelection(event: CdkDragDrop<Relationship>) {
|
||||
|
||||
// prevent propagating events generated releasing on the same position
|
||||
if (event.previousIndex === event.currentIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.moveGroup(event.previousIndex, event.currentIndex - event.previousIndex);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,8 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ngb-tabset>
|
||||
<ds-loading *ngIf="!item || !collection"></ds-loading>
|
||||
<ngb-tabset *ngIf="item && collection">
|
||||
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + relationshipOptions.relationshipType | translate : {count: (totalInternal$ | async)}">
|
||||
<ng-template ngbTabContent>
|
||||
<ds-dynamic-lookup-relation-search-tab
|
||||
|
@@ -21,6 +21,10 @@ import { createPaginatedList } from '../../../../testing/utils.test';
|
||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
||||
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
|
||||
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
|
||||
describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
let component: DsDynamicLookupRelationModalComponent;
|
||||
@@ -28,6 +32,7 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
let item;
|
||||
let item1;
|
||||
let item2;
|
||||
let testWSI;
|
||||
let searchResult1;
|
||||
let searchResult2;
|
||||
let listID;
|
||||
@@ -41,6 +46,8 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
let lookupRelationService;
|
||||
let rdbService;
|
||||
let submissionId;
|
||||
let submissionService;
|
||||
let submissionObjectDataService;
|
||||
|
||||
const externalSources = [
|
||||
Object.assign(new ExternalSource(), {
|
||||
@@ -56,11 +63,16 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
];
|
||||
const totalLocal = 10;
|
||||
const totalExternal = 8;
|
||||
const collection: Collection = new Collection();
|
||||
|
||||
|
||||
function init() {
|
||||
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
|
||||
item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
|
||||
item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
|
||||
testWSI = new WorkspaceItem();
|
||||
testWSI.item = createSuccessfulRemoteDataObject$(item);
|
||||
testWSI.collection = createSuccessfulRemoteDataObject$(collection);
|
||||
searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 });
|
||||
searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 });
|
||||
listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3';
|
||||
@@ -87,6 +99,12 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
aggregate: createSuccessfulRemoteDataObject$(externalSources)
|
||||
});
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
dispatchSave: jasmine.createSpy('dispatchSave')
|
||||
});
|
||||
submissionObjectDataService = jasmine.createSpyObj('SubmissionObjectDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(testWSI)
|
||||
});
|
||||
submissionId = '1234';
|
||||
}
|
||||
|
||||
@@ -111,6 +129,8 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
},
|
||||
{ provide: RelationshipTypeService, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: SubmissionService, useValue: submissionService },
|
||||
{ provide: SubmissionObjectDataService, useValue: submissionObjectDataService },
|
||||
{
|
||||
provide: Store, useValue: {
|
||||
// tslint:disable-next-line:no-empty
|
||||
|
@@ -11,7 +11,11 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { SearchResult } from '../../../../search/search-result.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload
|
||||
} from '../../../../../core/shared/operators';
|
||||
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||
@@ -23,6 +27,12 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model
|
||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
||||
import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { SubmissionService } from '../../../../../submission/submission.service';
|
||||
import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-modal',
|
||||
@@ -112,6 +122,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
*/
|
||||
totalExternal$: Observable<number[]>;
|
||||
|
||||
/**
|
||||
* List of subscriptions to unsubscribe from
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
public modal: NgbActiveModal,
|
||||
private selectableListService: SelectableListService,
|
||||
@@ -121,14 +136,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
private lookupRelationService: LookupRelationService,
|
||||
private searchConfigService: SearchConfigurationService,
|
||||
private rdbService: RemoteDataBuildService,
|
||||
private submissionService: SubmissionService,
|
||||
private submissionObjectService: SubmissionObjectDataService,
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>,
|
||||
private router: Router,
|
||||
private router: Router
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setItem();
|
||||
this.selection$ = this.selectableListService
|
||||
.getSelectableList(this.listId)
|
||||
.pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []));
|
||||
@@ -188,6 +206,24 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this.item$ based on this.model.submissionId
|
||||
*/
|
||||
private setItem() {
|
||||
const submissionObject$ = this.submissionObjectService
|
||||
.findById(this.submissionId, true, true, followLink('item'), followLink('collection')).pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload()
|
||||
);
|
||||
|
||||
const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||
|
||||
this.subs.push(item$.subscribe((item) => this.item = item));
|
||||
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a subscription updating relationships with name variants
|
||||
* @param sri The search result to track name variants for
|
||||
@@ -243,5 +279,8 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
ngOnDestroy() {
|
||||
this.router.navigate([], {});
|
||||
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
||||
this.subs
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -295,6 +295,7 @@ describe('FormBuilderService test suite', () => {
|
||||
notRepeatable: false,
|
||||
relationshipConfig: undefined,
|
||||
submissionId: '1234',
|
||||
isDraggable: true,
|
||||
groupFactory: () => {
|
||||
return [
|
||||
new DynamicInputModel({ id: 'testFormRowArrayGroupInput' })
|
||||
|
@@ -28,9 +28,10 @@ import { DynamicRelationGroupModel } from './ds-dynamic-form-ui/models/relation-
|
||||
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
|
||||
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
|
||||
import { isNgbDateStruct } from '../../date.util';
|
||||
import { dateToString, isNgbDateStruct } from '../../date.util';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-ui/ds-dynamic-form-constants';
|
||||
import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||
import { VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models';
|
||||
|
||||
@Injectable()
|
||||
export class FormBuilderService extends DynamicFormService {
|
||||
@@ -121,8 +122,15 @@ export class FormBuilderService extends DynamicFormService {
|
||||
|
||||
const normalizeValue = (controlModel, controlValue, controlModelIndex) => {
|
||||
const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null;
|
||||
|
||||
if (controlModel?.metadataValue?.authority?.includes(VIRTUAL_METADATA_PREFIX)) {
|
||||
return controlModel.metadataValue;
|
||||
}
|
||||
|
||||
if (isString(controlValue)) {
|
||||
return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex);
|
||||
} else if (isNgbDateStruct(controlValue)) {
|
||||
return new FormFieldMetadataValueObject(dateToString(controlValue));
|
||||
} else if (isObject(controlValue)) {
|
||||
const authority = (controlValue as any).authority || (controlValue as any).id || null;
|
||||
const place = controlModelIndex || (controlValue as any).place;
|
||||
@@ -240,7 +248,7 @@ export class FormBuilderService extends DynamicFormService {
|
||||
}
|
||||
|
||||
hasArrayGroupValue(model: DynamicFormControlModel): boolean {
|
||||
return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG || model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY);
|
||||
return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG);
|
||||
}
|
||||
|
||||
hasMappedGroupValue(model: DynamicFormControlModel): boolean {
|
||||
@@ -310,7 +318,7 @@ export class FormBuilderService extends DynamicFormService {
|
||||
let tempModel: DynamicFormControlModel;
|
||||
|
||||
if (this.isArrayGroup(model as DynamicFormControlModel)) {
|
||||
return hasValue((model as any).metadataKey) ? (model as any).metadataKey : model.index.toString();
|
||||
return model.index.toString();
|
||||
} else if (this.isModelInCustomGroup(model as DynamicFormControlModel)) {
|
||||
tempModel = (model as any).parent;
|
||||
} else {
|
||||
|
@@ -58,8 +58,20 @@ export class ConcatFieldParser extends FieldParser {
|
||||
concatGroup.group = [];
|
||||
concatGroup.separator = this.separator;
|
||||
|
||||
const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, false, false);
|
||||
const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, false, false);
|
||||
const input1ModelConfig: DynamicInputModelConfig = this.initModel(
|
||||
id + CONCAT_FIRST_INPUT_SUFFIX,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
const input2ModelConfig: DynamicInputModelConfig = this.initModel(
|
||||
id + CONCAT_SECOND_INPUT_SUFFIX,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
if (hasNoValue(concatGroup.hint) && hasValue(input1ModelConfig.hint) && hasNoValue(input2ModelConfig.hint)) {
|
||||
concatGroup.hint = input1ModelConfig.hint;
|
||||
|
@@ -15,6 +15,8 @@ import { setLayout } from './parser.utils';
|
||||
import { ParserOptions } from './parser-options';
|
||||
import { RelationshipOptions } from '../models/relationship-options.model';
|
||||
import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model';
|
||||
import { ParserType } from './parser-type';
|
||||
import { isNgbDateStruct } from '../../../date.util';
|
||||
|
||||
export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>('submissionId');
|
||||
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
|
||||
@@ -37,9 +39,8 @@ export abstract class FieldParser {
|
||||
|
||||
public parse() {
|
||||
if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
|
||||
&& (this.configData.input.type !== 'list')
|
||||
&& (this.configData.input.type !== 'tag')
|
||||
&& (this.configData.input.type !== 'group')
|
||||
&& (this.configData.input.type !== ParserType.List)
|
||||
&& (this.configData.input.type !== ParserType.Tag)
|
||||
) {
|
||||
let arrayCounter = 0;
|
||||
let fieldArrayCounter = 0;
|
||||
@@ -49,6 +50,11 @@ export abstract class FieldParser {
|
||||
if (Array.isArray(this.configData.selectableMetadata) && this.configData.selectableMetadata.length === 1) {
|
||||
metadataKey = this.configData.selectableMetadata[0].metadata;
|
||||
}
|
||||
|
||||
let isDraggable = true;
|
||||
if (this.configData.input.type === ParserType.Onebox && this.configData?.selectableMetadata?.length > 1) {
|
||||
isDraggable = false;
|
||||
}
|
||||
const config = {
|
||||
id: uniqueId() + '_array',
|
||||
label: this.configData.label,
|
||||
@@ -60,6 +66,7 @@ export abstract class FieldParser {
|
||||
metadataKey,
|
||||
metadataFields: this.getAllFieldIds(),
|
||||
hasSelectableMetadata: isNotEmpty(this.configData.selectableMetadata),
|
||||
isDraggable,
|
||||
groupFactory: () => {
|
||||
let model;
|
||||
if ((arrayCounter === 0)) {
|
||||
@@ -69,19 +76,13 @@ export abstract class FieldParser {
|
||||
const fieldArrayOfValueLength = this.getInitValueCount(arrayCounter - 1);
|
||||
let fieldValue = null;
|
||||
if (fieldArrayOfValueLength > 0) {
|
||||
if (fieldArrayCounter === 0) {
|
||||
fieldValue = '';
|
||||
} else {
|
||||
fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter - 1);
|
||||
}
|
||||
fieldArrayCounter++;
|
||||
if (fieldArrayCounter === fieldArrayOfValueLength + 1) {
|
||||
fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++);
|
||||
if (fieldArrayCounter === fieldArrayOfValueLength) {
|
||||
fieldArrayCounter = 0;
|
||||
arrayCounter++;
|
||||
}
|
||||
}
|
||||
model = this.modelFactory(fieldValue, false);
|
||||
model.id = `${model.id}_${fieldArrayCounter}`;
|
||||
}
|
||||
setLayout(model, 'element', 'host', 'col');
|
||||
if (model.hasLanguages || isNotEmpty(model.relationship)) {
|
||||
@@ -130,7 +131,9 @@ export abstract class FieldParser {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof fieldValue === 'object') {
|
||||
if (isNgbDateStruct(fieldValue)) {
|
||||
modelConfig.value = fieldValue;
|
||||
} else if (typeof fieldValue === 'object') {
|
||||
modelConfig.metadataValue = fieldValue;
|
||||
modelConfig.language = fieldValue.language;
|
||||
modelConfig.place = fieldValue.place;
|
||||
@@ -210,10 +213,9 @@ export abstract class FieldParser {
|
||||
}
|
||||
|
||||
protected getInitArrayIndex() {
|
||||
let fieldCount = 0;
|
||||
const fieldIds: any = this.getAllFieldIds();
|
||||
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) {
|
||||
fieldCount = this.initFormValues[fieldIds].filter((value) => hasValue(value) && hasValue(value.value)).length;
|
||||
return this.initFormValues[fieldIds].length;
|
||||
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
|
||||
let counter = 0;
|
||||
fieldIds.forEach((id) => {
|
||||
@@ -221,9 +223,10 @@ export abstract class FieldParser {
|
||||
counter = counter + this.initFormValues[id].length;
|
||||
}
|
||||
});
|
||||
fieldCount = counter;
|
||||
return (counter === 0) ? 1 : counter;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
return (fieldCount === 0) ? 1 : fieldCount + 1;
|
||||
}
|
||||
|
||||
protected getFieldId(): string {
|
||||
@@ -245,7 +248,7 @@ export abstract class FieldParser {
|
||||
}
|
||||
}
|
||||
|
||||
protected initModel(id?: string, label = true, setErrors = true, hint = true) {
|
||||
protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true, hint = true) {
|
||||
|
||||
const controlModel = Object.create(null);
|
||||
|
||||
@@ -316,7 +319,7 @@ export abstract class FieldParser {
|
||||
|
||||
protected setLabel(controlModel, label = true, labelEmpty = false) {
|
||||
if (label) {
|
||||
controlModel.label = this.configData.label;
|
||||
controlModel.label = (labelEmpty) ? ' ' : this.configData.label;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -59,7 +59,7 @@ export class OneboxFieldParser extends FieldParser {
|
||||
this.setLabel(inputSelectGroup, label);
|
||||
inputSelectGroup.required = isNotEmpty(this.configData.mandatory);
|
||||
|
||||
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false);
|
||||
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false, false);
|
||||
selectModelConfig.hint = null;
|
||||
this.setOptions(selectModelConfig);
|
||||
if (isNotEmpty(fieldValue)) {
|
||||
@@ -67,7 +67,7 @@ export class OneboxFieldParser extends FieldParser {
|
||||
}
|
||||
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
|
||||
|
||||
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false);
|
||||
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false, false);
|
||||
inputModelConfig.hint = null;
|
||||
this.setValues(inputModelConfig, fieldValue);
|
||||
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;
|
||||
|
@@ -1,57 +1,68 @@
|
||||
<div class="container-fluid">
|
||||
<form class="form-horizontal" [formGroup]="formGroup">
|
||||
<form class="form-horizontal" [formGroup]="formGroup">
|
||||
|
||||
<ds-dynamic-form
|
||||
[formId]="formId"
|
||||
[formGroup]="formGroup"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
(change)="$event.stopPropagation();"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onChange($event)"
|
||||
(dfFocus)="onFocus($event)">
|
||||
<ng-template modelType="ARRAY" let-group let-index="index" let-context="context">
|
||||
<!--Array with repeatable items-->
|
||||
<div *ngIf="context.hasSelectableMetadata && !context.notRepeatable && index < 1"
|
||||
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end">
|
||||
<div class="btn-group" role="group" aria-label="Add and remove button">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
[disabled]="isItemReadOnly(context, index)"
|
||||
(click)="insertItem($event, group.context, group.index)">
|
||||
<span aria-label="Add">{{'form.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Array with non repeatable items - Only delete button-->
|
||||
<div *ngIf="context.notRepeatable && group.context.groups.length > 1 || index > 0 && !(group.group[0]?.value?.isVirtual || group.group[0]?.metadataValue?.isVirtual)"
|
||||
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end">
|
||||
<div class="btn-group" role="group" aria-label="Remove button">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
(click)="removeItem($event, context, index)"
|
||||
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</ds-dynamic-form>
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
<div *ngIf="displaySubmit">
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<button type="reset" class="btn btn-default" (click)="reset()">{{cancelLabel | translate}}</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="onSubmit()"
|
||||
[disabled]="!(isValid() | async)">{{submitLabel | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ds-dynamic-form
|
||||
[formId]="formId"
|
||||
[formGroup]="formGroup"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
(change)="$event.stopPropagation();"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onChange($event)"
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event)">
|
||||
<ng-template modelType="ARRAY" let-group let-index="index" let-context="context">
|
||||
<!--Array with repeatable items-->
|
||||
<div *ngIf="(!context.notRepeatable) && !isVirtual(context, index)"
|
||||
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
title="{{'form.remove' | translate}}"
|
||||
(click)="removeItem($event, context, index)"
|
||||
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||
<span attr.aria-label="{{'form.remove' | translate}}"><i class="fas fa-trash" aria-hidden="true"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="(!context.notRepeatable) && index === (group.context.groups.length - 1)" class="clearfix pl-4 w-100">
|
||||
<div class="btn-group" role="group" aria-label="remove button">
|
||||
<button type="button" class="ds-form-add-more btn btn-link"
|
||||
title="{{'form.add' | translate}}"
|
||||
[disabled]="isItemReadOnly(context, index)"
|
||||
(click)="insertItem($event, group.context, group.context.groups.length)">
|
||||
<span attr.aria-label="{{'form.add' | translate}}"><i class="fas fa-plus"></i> {{'form.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<!--Array with non repeatable items - Only discard button-->
|
||||
<div *ngIf="context.notRepeatable && context.showButtons && group.context.groups.length > 1"
|
||||
class="col-xs-2 d-flex flex-column justify-content-sm-start align-items-end">
|
||||
<div class="btn-group" role="group" aria-label="Remove button">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
title="{{'form.discard' | translate}}"
|
||||
(click)="removeItem($event, context, index)"
|
||||
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||
<span attr.aria-label="{{'form.discard' | translate}}">{{'form.discard' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</ds-dynamic-form>
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
<div *ngIf="displaySubmit">
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<button type="reset" class="btn btn-default" (click)="reset()">{{cancelLabel | translate}}</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="onSubmit()"
|
||||
[disabled]="!(isValid() | async)">{{submitLabel | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
@@ -15,6 +15,11 @@
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
button.ds-form-add-more:focus {
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ds-form-input-value {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
|
@@ -418,7 +418,7 @@ describe('FormComponent test suite', () => {
|
||||
}));
|
||||
|
||||
it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1);
|
||||
formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 0);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel)));
|
||||
}));
|
||||
@@ -426,7 +426,7 @@ describe('FormComponent test suite', () => {
|
||||
it('should emit removeArrayItem Event when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
spyOn(formComp.removeArrayItem, 'emit');
|
||||
|
||||
formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1);
|
||||
formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 0);
|
||||
|
||||
expect(formComp.removeArrayItem.emit).toHaveBeenCalled();
|
||||
}));
|
||||
|
@@ -2,6 +2,7 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import {
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlEvent,
|
||||
@@ -9,15 +10,14 @@ import {
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormLayout,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { findIndex } from 'lodash';
|
||||
|
||||
import { FormBuilderService } from './builder/form-builder.service';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util';
|
||||
import { FormService } from './form.service';
|
||||
import { FormEntry, FormError } from './form.reducer';
|
||||
import { QUALDROP_GROUP_SUFFIX } from './builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
|
||||
|
||||
const QUALDROP_GROUP_REGEX = new RegExp(`${QUALDROP_GROUP_SUFFIX}_\\d+$`);
|
||||
import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model';
|
||||
|
||||
/**
|
||||
* The default form component.
|
||||
@@ -70,6 +70,7 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('ngbEvent') customEvent: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
/* tslint:enable:no-output-rename */
|
||||
@Output() addArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output() removeArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@@ -87,9 +88,9 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
@Output() submitForm: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>();
|
||||
|
||||
/**
|
||||
* An object of FormGroup type
|
||||
* Reference to NgbModal
|
||||
*/
|
||||
// public formGroup: FormGroup;
|
||||
modalRef: NgbModalRef;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
@@ -166,7 +167,6 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
filter((formState: FormEntry) => !!formState && (isNotEmpty(formState.errors) || isNotEmpty(this.formErrors))),
|
||||
map((formState) => formState.errors),
|
||||
distinctUntilChanged())
|
||||
// .delay(100) // this terrible delay is here to prevent the detection change error
|
||||
.subscribe((errors: FormError[]) => {
|
||||
const { formGroup, formModel } = this;
|
||||
errors
|
||||
@@ -187,7 +187,6 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
if (field) {
|
||||
const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel);
|
||||
this.formService.addErrorToField(field, model, error.message);
|
||||
// this.formService.validateAllFormFields(formGroup);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
|
||||
}
|
||||
@@ -252,6 +251,10 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
this.blur.emit(event);
|
||||
}
|
||||
|
||||
onCustomEvent(event: any) {
|
||||
this.customEvent.emit(event);
|
||||
}
|
||||
|
||||
onFocus(event: DynamicFormControlEvent): void {
|
||||
this.formService.setTouched(this.formId, this.formModel, event);
|
||||
this.focus.emit(event);
|
||||
@@ -300,58 +303,25 @@ export class FormComponent implements OnDestroy, OnInit {
|
||||
|
||||
removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void {
|
||||
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
|
||||
this.removeArrayItem.emit(this.getEvent($event, arrayContext, index - 1, 'remove'));
|
||||
const event = this.getEvent($event, arrayContext, index, 'remove');
|
||||
this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext);
|
||||
this.formService.changeForm(this.formId, this.formModel);
|
||||
this.removeArrayItem.emit(event);
|
||||
}
|
||||
|
||||
insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void {
|
||||
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
|
||||
|
||||
// First emit the new value so it can be sent to the server
|
||||
const value = formArrayControl.controls[0].value;
|
||||
const event = this.getEvent($event, arrayContext, 0, 'add');
|
||||
this.addArrayItem.emit(event);
|
||||
this.change.emit(event);
|
||||
|
||||
// Next: update the UI so the user sees the changes
|
||||
// without having to wait for the server's reply
|
||||
|
||||
// add an empty new field at the bottom
|
||||
this.formBuilderService.addFormArrayGroup(formArrayControl, arrayContext);
|
||||
|
||||
// set that field to the new value
|
||||
const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any;
|
||||
if (model.hasAuthority) {
|
||||
model.value = Object.values(value)[0];
|
||||
const ctrl = formArrayControl.controls[formArrayControl.length - 1];
|
||||
const ctrlValue = ctrl.value;
|
||||
const ctrlValueKey = Object.keys(ctrlValue)[0];
|
||||
ctrl.setValue({
|
||||
[ctrlValueKey]: model.value
|
||||
});
|
||||
} else if (this.formBuilderService.isQualdropGroup(model)) {
|
||||
const ctrl = formArrayControl.controls[formArrayControl.length - 1];
|
||||
const ctrlKey = Object.keys(ctrl.value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX)));
|
||||
const valueKey = Object.keys(value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX)));
|
||||
if (ctrlKey !== valueKey) {
|
||||
Object.defineProperty(value, ctrlKey, Object.getOwnPropertyDescriptor(value, valueKey));
|
||||
delete value[valueKey];
|
||||
}
|
||||
ctrl.setValue(value);
|
||||
} else {
|
||||
formArrayControl.controls[formArrayControl.length - 1].setValue(value);
|
||||
}
|
||||
|
||||
// Clear the topmost field by removing the filled out version and inserting a new, empty version.
|
||||
// Doing it this way ensures an empty value of the correct type is added without a bunch of ifs here
|
||||
this.formBuilderService.removeFormArrayGroup(0, formArrayControl, arrayContext);
|
||||
this.formBuilderService.insertFormArrayGroup(0, formArrayControl, arrayContext);
|
||||
|
||||
// Tell the formService that it should rerender.
|
||||
this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext);
|
||||
this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add'));
|
||||
this.formService.changeForm(this.formId, this.formModel);
|
||||
}
|
||||
|
||||
isVirtual(arrayContext: DynamicFormArrayModel, index: number) {
|
||||
const context = arrayContext.groups[index];
|
||||
const value: FormFieldMetadataValueObject = (context.group[0] as any).metadataValue;
|
||||
return isNotEmpty(value) && value.isVirtual;
|
||||
}
|
||||
|
||||
protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent {
|
||||
const context = arrayContext.groups[index];
|
||||
const itemGroupModel = context.context;
|
||||
|
@@ -80,6 +80,7 @@ const rowArrayQualdropConfig = {
|
||||
id: 'row_QUALDROP_GROUP',
|
||||
initialCount: 1,
|
||||
notRepeatable: true,
|
||||
isDraggable: false,
|
||||
relationshipConfig: undefined,
|
||||
groupFactory: () => {
|
||||
return [MockQualdropModel];
|
||||
|
@@ -31,9 +31,8 @@ import { RequestService } from '../../core/data/request.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { dateToString, stringToNgbDateStruct } from '../date.util';
|
||||
import { followLink } from '../utils/follow-link-config.model';
|
||||
import { ADMIN_MODULE_PATH } from '../../app-routing-paths';
|
||||
import { GROUP_EDIT_PATH } from '../../+admin/admin-access-control/admin-access-control-routing-paths';
|
||||
import { ACCESS_CONTROL_MODULE_PATH } from '../../+admin/admin-routing-paths';
|
||||
import { ACCESS_CONTROL_MODULE_PATH } from '../../app-routing-paths';
|
||||
import { GROUP_EDIT_PATH } from '../../access-control/access-control-routing-paths';
|
||||
|
||||
interface ResourcePolicyCheckboxEntry {
|
||||
id: string;
|
||||
@@ -317,7 +316,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((group: Group) => group.id)
|
||||
).subscribe((groupUUID) => {
|
||||
this.router.navigate([ADMIN_MODULE_PATH, ACCESS_CONTROL_MODULE_PATH, GROUP_EDIT_PATH, groupUUID]);
|
||||
this.router.navigate([ACCESS_CONTROL_MODULE_PATH, GROUP_EDIT_PATH, groupUUID]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ export class MenuServiceStub {
|
||||
deactivateSection(): void { /***/
|
||||
}
|
||||
|
||||
addSection(): void { /***/
|
||||
addSection(menuID: MenuID, section: MenuSection): void { /***/
|
||||
}
|
||||
|
||||
removeSection(): void { /***/
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { waitForAsync, TestBed } from '@angular/core/testing';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
@@ -35,10 +35,10 @@ describe('SectionFormOperationsService test suite', () => {
|
||||
let serviceAsAny: any;
|
||||
|
||||
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
|
||||
add: jasmine.createSpy('add'),
|
||||
replace: jasmine.createSpy('replace'),
|
||||
remove: jasmine.createSpy('remove'),
|
||||
});
|
||||
add: jasmine.createSpy('add'),
|
||||
replace: jasmine.createSpy('replace'),
|
||||
remove: jasmine.createSpy('remove'),
|
||||
});
|
||||
const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'test');
|
||||
|
||||
const dynamicFormControlChangeEvent: DynamicFormControlEvent = {
|
||||
|
@@ -6,18 +6,10 @@ import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
|
||||
DynamicFormArrayGroupModel,
|
||||
DynamicFormControlEvent,
|
||||
DynamicFormControlModel
|
||||
DynamicFormControlModel, isDynamicFormControlEvent
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
isNotNull,
|
||||
isNotUndefined,
|
||||
isNull,
|
||||
isUndefined
|
||||
} from '../../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util';
|
||||
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||
import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object';
|
||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
@@ -30,6 +22,8 @@ import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-fo
|
||||
import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
|
||||
import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||
import { deepClone } from 'fast-json-patch';
|
||||
import { dateToString, isNgbDateStruct } from '../../../shared/date.util';
|
||||
import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
|
||||
|
||||
/**
|
||||
* The service handling all form section operations
|
||||
@@ -71,8 +65,8 @@ export class SectionFormOperationsService {
|
||||
case 'change':
|
||||
this.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, hasStoredValue);
|
||||
break;
|
||||
case 'add':
|
||||
this.dispatchOperationsFromAddEvent(pathCombiner, event);
|
||||
case 'move':
|
||||
this.dispatchOperationsFromMoveEvent(pathCombiner, event, previousValue);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -83,20 +77,29 @@ export class SectionFormOperationsService {
|
||||
* Return index if specified field is part of fields array
|
||||
*
|
||||
* @param event
|
||||
* the [[DynamicFormControlEvent]] for the specified operation
|
||||
* the [[DynamicFormControlEvent]] | CustomEvent for the specified operation
|
||||
* @return number
|
||||
* the array index is part of array, zero otherwise
|
||||
*/
|
||||
public getArrayIndexFromEvent(event: DynamicFormControlEvent): number {
|
||||
public getArrayIndexFromEvent(event: DynamicFormControlEvent | any): number {
|
||||
let fieldIndex: number;
|
||||
|
||||
if (isNotEmpty(event)) {
|
||||
if (isNull(event.context)) {
|
||||
// Check whether model is part of an Array of group
|
||||
if (this.isPartOfArrayOfGroup(event.model)) {
|
||||
fieldIndex = (event.model.parent as any).parent.index;
|
||||
if (isDynamicFormControlEvent(event)) {
|
||||
// This is the case of a default insertItem/removeItem event
|
||||
|
||||
if (isNull(event.context)) {
|
||||
// Check whether model is part of an Array of group
|
||||
if (this.isPartOfArrayOfGroup(event.model)) {
|
||||
fieldIndex = (event.model.parent as any).parent.index;
|
||||
}
|
||||
} else {
|
||||
fieldIndex = event.context.index;
|
||||
}
|
||||
|
||||
} else {
|
||||
fieldIndex = event.context.index;
|
||||
// This is the case of a custom event which contains indexes information
|
||||
fieldIndex = event.index as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +248,8 @@ export class SectionFormOperationsService {
|
||||
// Language without Authority (input, textArea)
|
||||
fieldValue = new FormFieldMetadataValueObject(value, language);
|
||||
}
|
||||
} else if (isNgbDateStruct(value)) {
|
||||
fieldValue = new FormFieldMetadataValueObject(dateToString(value));
|
||||
} else if (value instanceof FormFieldLanguageValueObject || value instanceof VocabularyEntry
|
||||
|| value instanceof VocabularyEntryDetail || isObject(value)) {
|
||||
fieldValue = value;
|
||||
@@ -291,11 +296,19 @@ export class SectionFormOperationsService {
|
||||
protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner,
|
||||
event: DynamicFormControlEvent,
|
||||
previousValue: FormFieldPreviousValueObject): void {
|
||||
|
||||
if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
|
||||
// Model is a DynamicRowArrayModel
|
||||
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context);
|
||||
return;
|
||||
}
|
||||
|
||||
const path = this.getFieldPathFromEvent(event);
|
||||
const value = this.getFieldValueFromChangeEvent(event);
|
||||
console.log(value);
|
||||
if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
|
||||
this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue);
|
||||
} else if (isNotEmpty(value)) {
|
||||
} else if ((isNotEmpty(value) && typeof value === 'string') || (isNotEmpty(value) && value instanceof FormFieldMetadataValueObject && value.hasValue())) {
|
||||
this.operationsBuilder.remove(pathCombiner.getPath(path));
|
||||
}
|
||||
}
|
||||
@@ -352,17 +365,25 @@ export class SectionFormOperationsService {
|
||||
event: DynamicFormControlEvent,
|
||||
previousValue: FormFieldPreviousValueObject,
|
||||
hasStoredValue: boolean): void {
|
||||
|
||||
if (event.context && event.context instanceof DynamicFormArrayGroupModel) {
|
||||
// Model is a DynamicRowArrayModel
|
||||
this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context);
|
||||
return;
|
||||
}
|
||||
|
||||
const path = this.getFieldPathFromEvent(event);
|
||||
const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event);
|
||||
const value = this.getFieldValueFromChangeEvent(event);
|
||||
// Detect which operation must be dispatched
|
||||
if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)) {
|
||||
if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)
|
||||
|| this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) {
|
||||
// It's a qualdrup model
|
||||
this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue);
|
||||
} else if (this.formBuilder.isRelationGroup(event.model)) {
|
||||
// It's a relation model
|
||||
this.dispatchOperationsFromMap(this.getValueMap(value), pathCombiner, event, previousValue);
|
||||
} else if (this.formBuilder.hasArrayGroupValue(event.model) && hasNoValue((event.model as any).relationshipConfig)) {
|
||||
} else if (this.formBuilder.hasArrayGroupValue(event.model)) {
|
||||
// Model has as value an array, so dispatch an add operation with entire block of values
|
||||
this.operationsBuilder.add(
|
||||
pathCombiner.getPath(segmentedPath),
|
||||
@@ -398,13 +419,21 @@ export class SectionFormOperationsService {
|
||||
value);
|
||||
}
|
||||
previousValue.delete();
|
||||
} else if (value.hasValue() && (isUndefined(this.getArrayIndexFromEvent(event))
|
||||
|| this.getArrayIndexFromEvent(event) === 0)) {
|
||||
} else if (value.hasValue()) {
|
||||
// Here model has no previous value but a new one
|
||||
if (isUndefined(this.getArrayIndexFromEvent(event)) || this.getArrayIndexFromEvent(event) === 0) {
|
||||
// Model is single field or is part of an array model but is the first item,
|
||||
// so dispatch an add operation that initialize the values of a specific metadata
|
||||
this.operationsBuilder.add(
|
||||
pathCombiner.getPath(segmentedPath),
|
||||
value, true);
|
||||
} else {
|
||||
// Model is part of an array model but is not the first item,
|
||||
// so dispatch an add operation that add a value to an existent metadata
|
||||
this.operationsBuilder.add(
|
||||
pathCombiner.getPath(path),
|
||||
value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,4 +483,38 @@ export class SectionFormOperationsService {
|
||||
|
||||
previousValue.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form move operations
|
||||
*
|
||||
* @param pathCombiner
|
||||
* the [[JsonPatchOperationPathCombiner]] object for the specified operation
|
||||
* @param event
|
||||
* the [[DynamicFormControlEvent]] for the specified operation
|
||||
* @param previousValue
|
||||
* the [[FormFieldPreviousValueObject]] for the specified operation
|
||||
*/
|
||||
private dispatchOperationsFromMoveEvent(pathCombiner: JsonPatchOperationPathCombiner,
|
||||
event: DynamicFormControlEvent,
|
||||
previousValue: FormFieldPreviousValueObject) {
|
||||
|
||||
return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific patch handler for a DynamicRowArrayModel.
|
||||
* Configure a Patch ADD with the current array value.
|
||||
* @param pathCombiner
|
||||
* @param event
|
||||
* @param model
|
||||
*/
|
||||
private handleArrayGroupPatch(pathCombiner: JsonPatchOperationPathCombiner,
|
||||
event,
|
||||
model: DynamicRowArrayModel) {
|
||||
const arrayValue = this.formBuilder.getValueFromModel([model]);
|
||||
const segmentedPath2 = this.getFieldPathSegmentedFromChangeEvent(event);
|
||||
this.operationsBuilder.add(
|
||||
pathCombiner.getPath(segmentedPath2),
|
||||
arrayValue[segmentedPath2], false);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user