Merge branch 'main' into iiif-mirador

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

View File

@@ -132,7 +132,8 @@
"sortablejs": "1.10.1",
"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",

View File

@@ -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();
}

View File

@@ -6,7 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { 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 },

View File

@@ -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,
}));
});
});
});
});

View File

@@ -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

View File

@@ -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,

View File

@@ -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">

View File

@@ -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,

View File

@@ -12,6 +12,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { 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: '',

View File

@@ -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">

View File

@@ -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);
}
}

View File

@@ -10,6 +10,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { 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: '',

View File

@@ -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', () => {

View File

@@ -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([]);

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@

View File

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

View File

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

View File

@@ -8,9 +8,14 @@
</div>
<div 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>

View File

@@ -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) {
}

View File

@@ -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>

View File

@@ -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';

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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

View File

@@ -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', {

View File

@@ -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',

View File

@@ -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,

View File

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

View File

@@ -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;

View File

@@ -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',

View File

@@ -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;

View File

@@ -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({

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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 === '') {

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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 === '') {

View File

@@ -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',

View File

@@ -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}`;
}

View File

@@ -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 },
]}
],{

View File

@@ -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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';

View File

@@ -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';
}
/**

View File

@@ -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';

View File

@@ -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;
}
/**

View File

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

View File

@@ -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(),

View File

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

View File

@@ -20,6 +20,7 @@ const dcTitle0 = mdValue('Title 0');
const dcTitle1 = mdValue('Title 1');
const 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', () => {

View File

@@ -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);

View File

@@ -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';

View File

@@ -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>

View File

@@ -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 !== '&nbsp;';
}
/**
* Initialize this.item$ based on this.model.submissionId
*/

View File

@@ -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>

View File

@@ -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 },

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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());
}
}

View File

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

View File

@@ -28,9 +28,10 @@ import { DynamicRelationGroupModel } from './ds-dynamic-form-ui/models/relation-
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
import { 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 {

View File

@@ -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;

View File

@@ -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) ? '&nbsp;' : this.configData.label;
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -418,7 +418,7 @@ describe('FormComponent test suite', () => {
}));
it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => {
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();
}));

View File

@@ -2,6 +2,7 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { 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;

View File

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

View File

@@ -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]);
})
);
}

View File

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

View File

@@ -1,4 +1,4 @@
import { waitForAsync, TestBed } from '@angular/core/testing';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import {
@@ -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 = {

View File

@@ -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