diff --git a/README.md b/README.md index 1beb1bf30c..176c90d91b 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ dspace-angular │ ├── plugins * Folder for Cypress plugins (if any) │ ├── support * Folder for global e2e test actions/commands (run for all tests) │ └── tsconfig.json * TypeScript configuration file for e2e tests -├── docker * +├── docker * See docker/README.md for details │ ├── cli.assetstore.yml * │ ├── cli.ingest.yml * │ ├── cli.yml * @@ -367,8 +367,6 @@ dspace-angular │ ├── docker-compose-ci.yml * │ ├── docker-compose-rest.yml * │ ├── docker-compose.yml * -│ ├── environment.dev.ts * -│ ├── local.cfg * │ └── README.md * ├── docs * Folder for documentation │ └── Configuration.md * Configuration documentation diff --git a/cypress/integration/search-page.spec.ts b/cypress/integration/search-page.spec.ts index a2bfbe6a5b..859c765d2e 100644 --- a/cypress/integration/search-page.spec.ts +++ b/cypress/integration/search-page.spec.ts @@ -53,7 +53,7 @@ describe('Search Page', () => { // Click to display grid view // TODO: These buttons should likely have an easier way to uniquely select - cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?spc.sf=score&spc.sd=DESC&view=grid"] > .fas').click(); + cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?view=grid"] > .fas').click(); // tag must be loaded cy.get('ds-search-page').should('exist'); diff --git a/docker/README.md b/docker/README.md index b0943562af..a2f4ef3362 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,10 +29,6 @@ docker push dspace/dspace-angular:dspace-7_x - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. - cli.assetstore.yml - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing. -- environment.dev.ts - - Environment file for running DSpace Angular in Docker -- local.cfg - - Environment file for running the DSpace 7 REST API in Docker. ## To refresh / pull DSpace images from Dockerhub diff --git a/docker/cli.yml b/docker/cli.yml index 36f63b2cff..54b83d4503 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -18,10 +18,19 @@ services: dspace-cli: image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" container_name: dspace-cli - #environment: + environment: + # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. + # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml + # __P__ => "." (e.g. dspace__P__dir => dspace.dir) + # __D__ => "-" (e.g. google__D__metadata => google-metadata) + # dspace.dir + dspace__P__dir: /dspace + # db.url: Ensure we are using the 'dspacedb' image for our database + db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' + # solr.server: Ensure we are using the 'dspacesolr' image for Solr + solr__P__server: http://dspacesolr:8983/solr volumes: - "assetstore:/dspace/assetstore" - - "./local.cfg:/dspace/config/local.cfg" entrypoint: /dspace/bin/dspace command: help networks: diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 18fa152c9d..a895314a17 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -17,6 +17,19 @@ services: # DSpace (backend) webapp container dspace: container_name: dspace + environment: + # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. + # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml + # __P__ => "." (e.g. dspace__P__dir => dspace.dir) + # __D__ => "-" (e.g. google__D__metadata => google-metadata) + # dspace.dir, dspace.server.url and dspace.ui.url + dspace__P__dir: /dspace + dspace__P__server__P__url: http://localhost:8080/server + dspace__P__ui__P__url: http://localhost:4000 + # db.url: Ensure we are using the 'dspacedb' image for our database + db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' + # solr.server: Ensure we are using the 'dspacesolr' image for Solr + solr__P__server: http://dspacesolr:8983/solr depends_on: - dspacedb image: dspace/dspace:dspace-7_x-test @@ -29,7 +42,6 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - - "./local.cfg:/dspace/config/local.cfg" # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - solr_configs:/dspace/solr # Ensure that the database is ready BEFORE starting tomcat diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index 3534682afc..b73f1b7a39 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -13,10 +13,32 @@ version: '3.7' networks: dspacenet: + ipam: + config: + # Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container. + # If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below. + - subnet: 172.23.0.0/16 services: # DSpace (backend) webapp container dspace: container_name: dspace + environment: + # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. + # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml + # __P__ => "." (e.g. dspace__P__dir => dspace.dir) + # __D__ => "-" (e.g. google__D__metadata => google-metadata) + # dspace.dir, dspace.server.url, dspace.ui.url and dspace.name + dspace__P__dir: /dspace + dspace__P__server__P__url: http://localhost:8080/server + dspace__P__ui__P__url: http://localhost:4000 + dspace__P__name: 'DSpace Started with Docker Compose' + # db.url: Ensure we are using the 'dspacedb' image for our database + db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' + # solr.server: Ensure we are using the 'dspacesolr' image for Solr + solr__P__server: http://dspacesolr:8983/solr + # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests + # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. + proxies__P__trusted__P__ipranges: '172.23.0' image: dspace/dspace:dspace-7_x-test depends_on: - dspacedb @@ -29,7 +51,6 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - - "./local.cfg:/dspace/config/local.cfg" # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - solr_configs:/dspace/solr # Ensure that the database is ready BEFORE starting tomcat diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e518dc99d2..1387b1de39 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,10 +16,14 @@ services: dspace-angular: container_name: dspace-angular environment: - DSPACE_HOST: dspace-angular - DSPACE_NAMESPACE: / - DSPACE_PORT: '4000' - DSPACE_SSL: "false" + DSPACE_UI_SSL: 'false' + DSPACE_UI_HOST: dspace-angular + DSPACE_UI_PORT: '4000' + DSPACE_UI_NAMESPACE: / + DSPACE_REST_SSL: 'false' + DSPACE_REST_HOST: localhost + DSPACE_REST_PORT: 8080 + DSPACE_REST_NAMESPACE: /server image: dspace/dspace-angular:dspace-7_x build: context: .. @@ -33,5 +37,3 @@ services: target: 9876 stdin_open: true tty: true - volumes: - - ./environment.dev.ts:/app/src/environments/environment.dev.ts diff --git a/docker/environment.dev.ts b/docker/environment.dev.ts deleted file mode 100644 index 0e603ef11d..0000000000 --- a/docker/environment.dev.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -// This file is based on environment.template.ts provided by Angular UI -export const environment = { - // Default to using the local REST API (running in Docker) - rest: { - ssl: false, - host: 'localhost', - port: 8080, - // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript - nameSpace: '/server' - } -}; diff --git a/docker/local.cfg b/docker/local.cfg deleted file mode 100644 index a511c25789..0000000000 --- a/docker/local.cfg +++ /dev/null @@ -1,6 +0,0 @@ -dspace.dir=/dspace -db.url=jdbc:postgresql://dspacedb:5432/dspace -dspace.server.url=http://localhost:8080/server -dspace.ui.url=http://localhost:4000 -dspace.name=DSpace Started with Docker Compose -solr.server=http://dspacesolr:8983/solr diff --git a/package.json b/package.json index 403973ef98..278afdf6c3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:dev:config", + "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts index 0e872458bd..891238bbed 100644 --- a/src/app/access-control/access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -9,13 +9,15 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component'; import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; +import { FormModule } from '../shared/form/form.module'; @NgModule({ imports: [ CommonModule, SharedModule, RouterModule, - AccessControlRoutingModule + AccessControlRoutingModule, + FormModule ], declarations: [ EPeopleRegistryComponent, diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 45326c1abc..41ae67423c 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -19,7 +19,7 @@ class="btn btn-outline-secondary"> {{messagePrefix + '.return' | translate}}
-
@@ -36,9 +36,13 @@ + +
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
+ + { let component: EPersonFormComponent; @@ -42,6 +42,7 @@ describe('EPersonFormComponent', () => { let authService: AuthServiceStub; let authorizationService: AuthorizationDataService; let groupsDataService: GroupDataService; + let epersonRegistrationService: EpersonRegistrationService; let paginationService; @@ -199,12 +200,18 @@ describe('EPersonFormComponent', () => { { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: PaginationService, useValue: paginationService }, - { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) } + { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}, + { provide: EpersonRegistrationService, useValue: epersonRegistrationService }, + EPeopleRegistryComponent ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); + epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { + registerEmail: createSuccessfulRemoteDataObject$(null) + }); + beforeEach(() => { fixture = TestBed.createComponent(EPersonFormComponent); component = fixture.componentInstance; @@ -514,4 +521,23 @@ describe('EPersonFormComponent', () => { expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson); }); }); + + describe('Reset Password', () => { + let ePersonId; + let ePersonEmail; + + beforeEach(() => { + ePersonId = 'testEPersonId'; + ePersonEmail = 'person.email@4science.it'; + component.epersonInitial = Object.assign(new EPerson(), { + id: ePersonId, + email: ePersonEmail + }); + component.resetPassword(); + }); + + it('should call epersonRegistrationService.registerEmail', () => { + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail); + }); + }); }); diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 723939df77..05fc3189d0 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -34,6 +34,8 @@ import { NoContent } from '../../../core/shared/NoContent.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { ValidateEmailNotTaken } from './validators/email-taken.validator'; +import { Registration } from '../../../core/shared/registration.model'; +import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; @Component({ selector: 'ds-eperson-form', @@ -121,7 +123,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Observable whether or not the admin is allowed to reset the EPerson's password * TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false) */ - canReset$: Observable = observableOf(false); + canReset$: Observable; /** * Observable whether or not the admin is allowed to delete the EPerson @@ -167,17 +169,20 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ emailValueChangeSubscribe: Subscription; - constructor(protected changeDetectorRef: ChangeDetectorRef, - public epersonService: EPersonDataService, - public groupsDataService: GroupDataService, - private formBuilderService: FormBuilderService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private authService: AuthService, - private authorizationService: AuthorizationDataService, - private modalService: NgbModal, - private paginationService: PaginationService, - public requestService: RequestService) { + constructor( + protected changeDetectorRef: ChangeDetectorRef, + public epersonService: EPersonDataService, + public groupsDataService: GroupDataService, + private formBuilderService: FormBuilderService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private authService: AuthService, + private authorizationService: AuthorizationDataService, + private modalService: NgbModal, + private paginationService: PaginationService, + public requestService: RequestService, + private epersonRegistrationService: EpersonRegistrationService, + ) { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.epersonInitial = eperson; if (hasValue(eperson)) { @@ -310,6 +315,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.canDelete$ = activeEPerson$.pipe( switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) ); + this.canReset$ = observableOf(true); }); } @@ -479,6 +485,26 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.isImpersonated = false; } + /** + * Sends an email to current eperson address with the information + * to reset password + */ + resetPassword() { + if (hasValue(this.epersonInitial.email)) { + this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData()) + .subscribe((response: RemoteData) => { + if (response.hasSucceeded) { + this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'), + this.translateService.get('forgot-email.form.success.content', {email: this.epersonInitial.email})); + } else { + this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'), + this.translateService.get('forgot-email.form.error.content', {email: this.epersonInitial.email})); + } + } + ); + } + } + /** * Cancel the current edit when component is destroyed & unsub all subscriptions */ diff --git a/src/app/admin/admin-registries/admin-registries.module.ts b/src/app/admin/admin-registries/admin-registries.module.ts index 5c82bb2ec9..65f7b61419 100644 --- a/src/app/admin/admin-registries/admin-registries.module.ts +++ b/src/app/admin/admin-registries/admin-registries.module.ts @@ -8,6 +8,7 @@ import { SharedModule } from '../../shared/shared.module'; import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component'; import { MetadataFieldFormComponent } from './metadata-schema/metadata-field-form/metadata-field-form.component'; import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.module'; +import { FormModule } from '../../shared/form/form.module'; @NgModule({ imports: [ @@ -15,7 +16,8 @@ import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.mo SharedModule, RouterModule, BitstreamFormatsModule, - AdminRegistriesRoutingModule + AdminRegistriesRoutingModule, + FormModule ], declarations: [ MetadataRegistryComponent, diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts index 1667a07c0b..afbe35a1f6 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts @@ -7,13 +7,15 @@ import { FormatFormComponent } from './format-form/format-form.component'; import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component'; import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module'; import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component'; +import { FormModule } from '../../../shared/form/form.module'; @NgModule({ imports: [ CommonModule, SharedModule, RouterModule, - BitstreamFormatsRoutingModule + BitstreamFormatsRoutingModule, + FormModule ], declarations: [ BitstreamFormatsComponent, diff --git a/src/app/admin/admin-search-page/admin-search.module.ts b/src/app/admin/admin-search-page/admin-search.module.ts index ecf8d8a2d2..353d6dd498 100644 --- a/src/app/admin/admin-search-page/admin-search.module.ts +++ b/src/app/admin/admin-search-page/admin-search.module.ts @@ -10,6 +10,7 @@ import { CollectionAdminSearchResultGridElementComponent } from './admin-search- import { ItemAdminSearchResultActionsComponent } from './admin-search-results/item-admin-search-result-actions.component'; import { JournalEntitiesModule } from '../../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../../entity-groups/research-entities/research-entities.module'; +import { SearchModule } from '../../shared/search/search.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -24,6 +25,7 @@ const ENTRY_COMPONENTS = [ @NgModule({ imports: [ + SearchModule, SharedModule.withEntryComponents(), JournalEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents() diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts index 948d7d86bc..65026c1504 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts @@ -18,6 +18,8 @@ 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; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { Item } from '../../core/shared/item.model'; describe('AdminSidebarComponent', () => { let comp: AdminSidebarComponent; @@ -26,6 +28,28 @@ describe('AdminSidebarComponent', () => { let authorizationService: AuthorizationDataService; let scriptService; + + const mockItem = Object.assign(new Item(), { + id: 'fake-id', + uuid: 'fake-id', + handle: 'fake/handle', + lastModified: '2018', + _links: { + self: { + href: 'https://localhost:8000/items/fake-id' + } + } + }); + + + const routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(mockItem) + }), + children: [] + }; + + beforeEach(waitForAsync(() => { authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true) @@ -42,6 +66,7 @@ describe('AdminSidebarComponent', () => { { provide: ActivatedRoute, useValue: {} }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ScriptDataService, useValue: scriptService }, + { provide: ActivatedRoute, useValue: routeStub }, { provide: NgbModal, useValue: { open: () => {/*comment*/ @@ -229,19 +254,19 @@ describe('AdminSidebarComponent', () => { it('should contain site admin section', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'admin_search', visible: true, + id: 'admin_search', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'registries', visible: true, + id: 'registries', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'registries', visible: true, + parentID: 'registries', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'curation_tasks', visible: true, + id: 'curation_tasks', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'workflow', visible: true, + id: 'workflow', visible: true, })); }); }); @@ -259,7 +284,7 @@ describe('AdminSidebarComponent', () => { it('should show edit_community', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_community', visible: true, + id: 'edit_community', visible: true, })); }); }); @@ -277,7 +302,7 @@ describe('AdminSidebarComponent', () => { it('should show edit_collection', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'edit_collection', visible: true, + id: 'edit_collection', visible: true, })); }); }); @@ -295,10 +320,10 @@ describe('AdminSidebarComponent', () => { it('should show access control section', () => { expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - id: 'access_control', visible: true, + id: 'access_control', visible: true, })); expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({ - parentID: 'access_control', visible: true, + parentID: 'access_control', visible: true, })); }); }); diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index f0d583744c..c81b2e6e93 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -21,6 +21,7 @@ import { MenuService } from '../../shared/menu/menu.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { Router, ActivatedRoute } from '@angular/router'; /** * Component representing the admin sidebar @@ -63,14 +64,15 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { inFocus$: BehaviorSubject; constructor(protected menuService: MenuService, - protected injector: Injector, - private variableService: CSSVariableService, - private authService: AuthService, - private modalService: NgbModal, - private authorizationService: AuthorizationDataService, - private scriptDataService: ScriptDataService, + protected injector: Injector, + private variableService: CSSVariableService, + private authService: AuthService, + private modalService: NgbModal, + public authorizationService: AuthorizationDataService, + private scriptDataService: ScriptDataService, + public route: ActivatedRoute ) { - super(menuService, injector); + super(menuService, injector, authorizationService, route); this.inFocus$ = new BehaviorSubject(false); } @@ -144,7 +146,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { type: MenuItemType.TEXT, text: 'menu.section.new' } as TextMenuItemModel, - icon: 'plus', + icon: 'plus', index: 0 }, { diff --git a/src/app/admin/admin-workflow-page/admin-workflow.module.ts b/src/app/admin/admin-workflow-page/admin-workflow.module.ts index 02e5e01023..85e8f00a46 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow.module.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow.module.ts @@ -5,6 +5,7 @@ import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './adm import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-search-results/workflow-item-admin-workflow-actions.component'; import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; +import { SearchModule } from '../../shared/search/search.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -14,6 +15,7 @@ const ENTRY_COMPONENTS = [ @NgModule({ imports: [ + SearchModule, SharedModule.withEntryComponents() ], declarations: [ diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 6c756dd851..b28a0cf89e 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -24,7 +24,7 @@ const ENTRY_COMPONENTS = [ AccessControlModule, AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), - SharedModule, + SharedModule ], declarations: [ AdminCurationTasksComponent, diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index db6b22a023..d9c3410931 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -89,6 +89,12 @@ export function getPageNotFoundRoute() { return `/${PAGE_NOT_FOUND_PATH}`; } +export const INTERNAL_SERVER_ERROR = '500'; + +export function getPageInternalServerErrorRoute() { + return `/${INTERNAL_SERVER_ERROR}`; +} + export const INFO_MODULE_PATH = 'info'; export function getInfoModulePath() { return `/${INFO_MODULE_PATH}`; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 04d2c55bdd..88f7791b1b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -11,10 +11,12 @@ import { FORBIDDEN_PATH, FORGOT_PASSWORD_PATH, INFO_MODULE_PATH, + INTERNAL_SERVER_ERROR, + LEGACY_BITSTREAM_MODULE_PATH, PROFILE_MODULE_PATH, REGISTER_PATH, + REQUEST_COPY_MODULE_PATH, WORKFLOW_ITEM_MODULE_PATH, - LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_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'; @@ -26,14 +28,25 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut 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'; +import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; +import { ServerCheckGuard } from './core/server-check/server-check.guard'; @NgModule({ imports: [ - RouterModule.forRoot([{ - path: '', canActivate: [AuthBlockingGuard], + RouterModule.forRoot([ + { path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent }, + { + path: '', + canActivate: [AuthBlockingGuard], + canActivateChild: [ServerCheckGuard], children: [ { path: '', redirectTo: '/home', pathMatch: 'full' }, - { path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] }, + { + path: 'reload/:rnd', + component: ThemedPageNotFoundComponent, + pathMatch: 'full', + canActivate: [ReloadGuard] + }, { path: 'home', loadChildren: () => import('./home-page/home-page.module') @@ -89,7 +102,8 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu .then((m) => m.ItemPageModule), canActivate: [EndUserAgreementCurrentUserGuard] }, - { path: 'entities/:entity-type', + { + path: 'entities/:entity-type', loadChildren: () => import('./item-page/item-page.module') .then((m) => m.ItemPageModule), canActivate: [EndUserAgreementCurrentUserGuard] @@ -133,12 +147,12 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu { path: 'login', loadChildren: () => import('./login-page/login-page.module') - .then((m) => m.LoginPageModule), + .then((m) => m.LoginPageModule) }, { path: 'logout', loadChildren: () => import('./logout-page/logout-page.module') - .then((m) => m.LogoutPageModule), + .then((m) => m.LogoutPageModule) }, { path: 'submit', @@ -178,7 +192,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu }, { path: INFO_MODULE_PATH, - loadChildren: () => import('./info/info.module').then((m) => m.InfoModule), + loadChildren: () => import('./info/info.module').then((m) => m.InfoModule) }, { path: REQUEST_COPY_MODULE_PATH, @@ -192,7 +206,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu { path: 'statistics', loadChildren: () => import('./statistics-page/statistics-page-routing.module') - .then((m) => m.StatisticsPageRoutingModule), + .then((m) => m.StatisticsPageRoutingModule) }, { path: ACCESS_CONTROL_MODULE_PATH, @@ -200,9 +214,10 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu canActivate: [GroupAdministratorGuard], }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, - ]} - ],{ - onSameUrlNavigation: 'reload', + ] + } + ], { + onSameUrlNavigation: 'reload', }) ], exports: [RouterModule], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 98b2d9fe92..67bccd9105 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -54,11 +54,10 @@ import { ThemedFooterComponent } from './footer/themed-footer.component'; import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component'; import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; +import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; +import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component'; -import { UUIDService } from './core/shared/uuid.service'; -import { CookieService } from './core/services/cookie.service'; - -import { AppConfig, APP_CONFIG } from '../config/app-config.interface'; +import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; export function getConfig() { return environment; @@ -156,21 +155,6 @@ const PROVIDERS = [ useClass: LogInterceptor, multi: true }, - // insert the unique id of the user that is using the application utilizing cookies - { - provide: APP_INITIALIZER, - useFactory: (cookieService: CookieService, uuidService: UUIDService) => { - const correlationId = cookieService.get('CORRELATION-ID'); - - // Check if cookie exists, if don't, set it with unique id - if (!correlationId) { - cookieService.set('CORRELATION-ID', uuidService.generate()); - } - return () => true; - }, - multi: true, - deps: [CookieService, UUIDService] - }, { provide: DYNAMIC_ERROR_MESSAGES_MATCHER, useValue: ValidateEmailErrorStateMatcher @@ -199,7 +183,9 @@ const DECLARATIONS = [ ThemedBreadcrumbsComponent, ForbiddenComponent, ThemedForbiddenComponent, - IdleModalComponent + IdleModalComponent, + ThemedPageInternalServerErrorComponent, + PageInternalServerErrorComponent ]; const EXPORTS = [ diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index a02095d834..5bd4f745d9 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -49,6 +49,7 @@ import { import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer'; +import { correlationIdReducer } from './correlation-id/correlation-id.reducer'; export interface AppState { router: fromRouter.RouterReducerState; @@ -69,6 +70,7 @@ export interface AppState { communityList: CommunityListState; epeopleRegistry: EPeopleRegistryState; groupRegistry: GroupRegistryState; + correlationId: string; } export const appReducers: ActionReducerMap = { @@ -90,6 +92,7 @@ export const appReducers: ActionReducerMap = { communityList: CommunityListReducer, epeopleRegistry: ePeopleRegistryReducer, groupRegistry: groupRegistryReducer, + correlationId: correlationIdReducer }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/bitstream-page/bitstream-page.module.ts b/src/app/bitstream-page/bitstream-page.module.ts index 80e5ad14e3..d168a06db2 100644 --- a/src/app/bitstream-page/bitstream-page.module.ts +++ b/src/app/bitstream-page/bitstream-page.module.ts @@ -4,6 +4,8 @@ import { SharedModule } from '../shared/shared.module'; import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component'; import { BitstreamPageRoutingModule } from './bitstream-page-routing.module'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; +import { FormModule } from '../shared/form/form.module'; +import { ResourcePoliciesModule } from '../shared/resource-policies/resource-policies.module'; /** * This module handles all components that are necessary for Bitstream related pages @@ -12,7 +14,9 @@ import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bit imports: [ CommonModule, SharedModule, - BitstreamPageRoutingModule + BitstreamPageRoutingModule, + FormModule, + ResourcePoliciesModule ], declarations: [ BitstreamAuthorizationsComponent, diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index e977b52ad6..1bdbb91a8b 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -63,7 +63,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { this.browseId = params.id || this.defaultBrowseId; this.startsWith = +params.startsWith || params.startsWith; const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); - this.updatePageWithItems(searchOptions, this.value); + this.updatePageWithItems(searchOptions, this.value, undefined); this.updateParent(params.scope); this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope); })); diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts index 6655f98392..f789389697 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -99,6 +99,11 @@ export class BrowseByMetadataPageComponent implements OnInit { */ value = ''; + /** + * The authority key (may be undefined) associated with {@link #value}. + */ + authority: string; + /** * The current startsWith option (fetched and updated from query-params) */ @@ -123,11 +128,12 @@ export class BrowseByMetadataPageComponent implements OnInit { }) ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { this.browseId = params.id || this.defaultBrowseId; + this.authority = params.authority; this.value = +params.value || params.value || ''; this.startsWith = +params.startsWith || params.startsWith; const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId); if (isNotEmpty(this.value)) { - this.updatePageWithItems(searchOptions, this.value); + this.updatePageWithItems(searchOptions, this.value, this.authority); } else { this.updatePage(searchOptions); } @@ -166,8 +172,8 @@ export class BrowseByMetadataPageComponent implements OnInit { * scope: string } * @param value The value of the browse-entry to display items for */ - updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) { - this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions); + updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string, authority: string) { + this.items$ = this.browseService.getBrowseItemsFor(value, authority, searchOptions); } /** diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts index b4a8331458..b2798b7fa8 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts @@ -46,7 +46,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { }) ).subscribe(([params, currentPage, currentSort]: [Params, PaginationComponentOptions, SortOptions]) => { this.browseId = params.id || this.defaultBrowseId; - this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined); + this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId), undefined, undefined); this.updateParent(params.scope); })); this.updateStartsWithTextOptions(); diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts index cd44e52b81..e1dfaacea5 100644 --- a/src/app/browse-by/browse-by.module.ts +++ b/src/app/browse-by/browse-by.module.ts @@ -6,6 +6,7 @@ import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse- import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component'; import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component'; import { ThemedBrowseBySwitcherComponent } from './browse-by-switcher/themed-browse-by-switcher.component'; +import { ComcolModule } from '../shared/comcol/comcol.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -17,6 +18,7 @@ const ENTRY_COMPONENTS = [ @NgModule({ imports: [ CommonModule, + ComcolModule, SharedModule ], declarations: [ diff --git a/src/app/collection-page/collection-form/collection-form.component.ts b/src/app/collection-page/collection-form/collection-form.component.ts index 7835ccc8e5..bb84153835 100644 --- a/src/app/collection-page/collection-form/collection-form.component.ts +++ b/src/app/collection-page/collection-form/collection-form.component.ts @@ -10,7 +10,7 @@ import { } from '@ng-dynamic-forms/core'; import { Collection } from '../../core/shared/collection.model'; -import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CommunityDataService } from '../../core/data/community-data.service'; import { AuthService } from '../../core/auth/auth.service'; @@ -28,8 +28,8 @@ import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type */ @Component({ selector: 'ds-collection-form', - styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'], - templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html' + styleUrls: ['../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.scss'], + templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html' }) export class CollectionFormComponent extends ComColFormComponent implements OnInit { /** diff --git a/src/app/collection-page/collection-form/collection-form.module.ts b/src/app/collection-page/collection-form/collection-form.module.ts index a9ff5f3ea9..ddf18f0586 100644 --- a/src/app/collection-page/collection-form/collection-form.module.ts +++ b/src/app/collection-page/collection-form/collection-form.module.ts @@ -2,9 +2,13 @@ import { NgModule } from '@angular/core'; import { CollectionFormComponent } from './collection-form.component'; import { SharedModule } from '../../shared/shared.module'; +import { ComcolModule } from '../../shared/comcol/comcol.module'; +import { FormModule } from '../../shared/form/form.module'; @NgModule({ imports: [ + ComcolModule, + FormModule, SharedModule ], declarations: [ diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 0dfd013449..e8d8d3eb11 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -33,7 +33,7 @@ import { ErrorComponent } from '../../shared/error/error.component'; import { LoadingComponent } from '../../shared/loading/loading.component'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchService } from '../../core/shared/search/search.service'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts index 9a93457436..3172616efc 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -9,10 +9,11 @@ import { Collection } from '../../core/shared/collection.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { map, startWith, switchMap, take } from 'rxjs/operators'; import { - getRemoteDataPayload, - getFirstSucceededRemoteData, - toDSpaceObjectListRD, - getFirstCompletedRemoteData, getAllSucceededRemoteData + getAllSucceededRemoteData, + getFirstCompletedRemoteData, + getFirstSucceededRemoteData, + getRemoteDataPayload, + toDSpaceObjectListRD } from '../../core/shared/operators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; @@ -24,7 +25,7 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { isNotEmpty } from '../../shared/empty.util'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { SearchService } from '../../core/shared/search/search.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { NoContent } from '../../core/shared/NoContent.model'; diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index 366e1da7b1..be602f8458 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -1,13 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { - BehaviorSubject, - combineLatest as observableCombineLatest, - Observable, - Subject -} from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs'; import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators'; -import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model'; import { SearchService } from '../core/shared/search/search.service'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { CollectionDataService } from '../core/data/collection-data.service'; @@ -103,20 +98,20 @@ export class CollectionPageComponent implements OnInit { const currentSort$ = this.paginationService.getCurrentSort(this.paginationConfig.id, this.sortConfig); this.itemRD$ = observableCombineLatest([currentPagination$, currentSort$]).pipe( - switchMap(([currentPagination, currentSort ]) => this.collectionRD$.pipe( + switchMap(([currentPagination, currentSort]) => this.collectionRD$.pipe( getFirstSucceededRemoteData(), map((rd) => rd.payload.id), switchMap((id: string) => { return this.searchService.search( - new PaginatedSearchOptions({ - scope: id, - pagination: currentPagination, - sort: currentSort, - dsoTypes: [DSpaceObjectType.ITEM] - })).pipe(toDSpaceObjectListRD()) as Observable>>; + new PaginatedSearchOptions({ + scope: id, + pagination: currentPagination, + sort: currentSort, + dsoTypes: [DSpaceObjectType.ITEM] + })).pipe(toDSpaceObjectListRD()) as Observable>>; }), startWith(undefined) // Make sure switching pages shows loading component - ) + ) ) ); diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts index a13731ed23..3652823200 100644 --- a/src/app/collection-page/collection-page.module.ts +++ b/src/app/collection-page/collection-page.module.ts @@ -14,6 +14,7 @@ import { SearchService } from '../core/shared/search/search.service'; import { StatisticsModule } from '../statistics/statistics.module'; import { CollectionFormModule } from './collection-form/collection-form.module'; import { ThemedCollectionPageComponent } from './themed-collection-page.component'; +import { ComcolModule } from '../shared/comcol/comcol.module'; @NgModule({ imports: [ @@ -22,7 +23,8 @@ import { ThemedCollectionPageComponent } from './themed-collection-page.componen CollectionPageRoutingModule, StatisticsModule.forRoot(), EditItemPageModule, - CollectionFormModule + CollectionFormModule, + ComcolModule ], declarations: [ CollectionPageComponent, diff --git a/src/app/collection-page/create-collection-page/create-collection-page.component.ts b/src/app/collection-page/create-collection-page/create-collection-page.component.ts index a38739c407..a938b37ce1 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/collection-page/create-collection-page/create-collection-page.component.ts @@ -2,12 +2,12 @@ import { Component } from '@angular/core'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; -import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; +import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import {RequestService} from '../../core/data/request.service'; +import { RequestService } from '../../core/data/request.service'; /** * Component that represents the page where a user can create a new Collection diff --git a/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts index 8daba0abfc..3e30373070 100644 --- a/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts +++ b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts @@ -1,11 +1,11 @@ import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component'; +import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; import { TranslateService } from '@ngx-translate/core'; -import {RequestService} from '../../core/data/request.service'; +import { RequestService } from '../../core/data/request.service'; /** * Component that represents the page where a user can delete an existing Collection diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index 8cb10775ac..cfaad3767e 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { Collection } from '../../../core/shared/collection.model'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { ActivatedRoute, Router } from '@angular/router'; diff --git a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts index 401065a661..985290a592 100644 --- a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -12,6 +12,7 @@ import { RequestService } from '../../../core/data/request.service'; import { RouterTestingModule } from '@angular/router/testing'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ComcolModule } from '../../../shared/comcol/comcol.module'; describe('CollectionRolesComponent', () => { @@ -65,6 +66,7 @@ describe('CollectionRolesComponent', () => { TestBed.configureTestingModule({ imports: [ + ComcolModule, SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts index aff1995a14..62fbb3ee3d 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; +import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; import { getCollectionPageRoute } from '../collection-page-routing-paths'; @@ -9,7 +9,7 @@ import { getCollectionPageRoute } from '../collection-page-routing-paths'; */ @Component({ selector: 'ds-edit-collection', - templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' + templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCollectionPageComponent extends EditComColPageComponent { type = 'collection'; diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts index 0b09542fa0..45612be41a 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts @@ -10,6 +10,9 @@ import { CollectionSourceComponent } from './collection-source/collection-source import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; import { CollectionFormModule } from '../collection-form/collection-form.module'; import { CollectionSourceControlsComponent } from './collection-source/collection-source-controls/collection-source-controls.component'; +import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; +import { FormModule } from '../../shared/form/form.module'; +import { ComcolModule } from '../../shared/comcol/comcol.module'; /** * Module that contains all components related to the Edit Collection page administrator functionality @@ -19,7 +22,10 @@ import { CollectionSourceControlsComponent } from './collection-source/collectio CommonModule, SharedModule, EditCollectionPageRoutingModule, - CollectionFormModule + CollectionFormModule, + ResourcePoliciesModule, + FormModule, + ComcolModule ], declarations: [ EditCollectionPageComponent, diff --git a/src/app/community-page/community-form/community-form.component.ts b/src/app/community-page/community-form/community-form.component.ts index 59480e8f68..a3730fd418 100644 --- a/src/app/community-page/community-form/community-form.component.ts +++ b/src/app/community-page/community-form/community-form.component.ts @@ -6,7 +6,7 @@ import { DynamicTextAreaModel } from '@ng-dynamic-forms/core'; import { Community } from '../../core/shared/community.model'; -import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CommunityDataService } from '../../core/data/community-data.service'; @@ -19,8 +19,8 @@ import { ObjectCacheService } from '../../core/cache/object-cache.service'; */ @Component({ selector: 'ds-community-form', - styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'], - templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html' + styleUrls: ['../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.scss'], + templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html' }) export class CommunityFormComponent extends ComColFormComponent { /** diff --git a/src/app/community-page/community-form/community-form.module.ts b/src/app/community-page/community-form/community-form.module.ts index 36dbea5c0d..925d218973 100644 --- a/src/app/community-page/community-form/community-form.module.ts +++ b/src/app/community-page/community-form/community-form.module.ts @@ -2,9 +2,13 @@ import { NgModule } from '@angular/core'; import { CommunityFormComponent } from './community-form.component'; import { SharedModule } from '../../shared/shared.module'; +import { ComcolModule } from '../../shared/comcol/comcol.module'; +import { FormModule } from '../../shared/form/form.module'; @NgModule({ imports: [ + ComcolModule, + FormModule, SharedModule ], declarations: [ diff --git a/src/app/community-page/community-page.module.ts b/src/app/community-page/community-page.module.ts index 3ae75f166c..724b762e90 100644 --- a/src/app/community-page/community-page.module.ts +++ b/src/app/community-page/community-page.module.ts @@ -12,6 +12,7 @@ import { DeleteCommunityPageComponent } from './delete-community-page/delete-com import { StatisticsModule } from '../statistics/statistics.module'; import { CommunityFormModule } from './community-form/community-form.module'; import { ThemedCommunityPageComponent } from './themed-community-page.component'; +import { ComcolModule } from '../shared/comcol/comcol.module'; const DECLARATIONS = [CommunityPageComponent, ThemedCommunityPageComponent, @@ -26,7 +27,8 @@ const DECLARATIONS = [CommunityPageComponent, SharedModule, CommunityPageRoutingModule, StatisticsModule.forRoot(), - CommunityFormModule + CommunityFormModule, + ComcolModule ], declarations: [ ...DECLARATIONS diff --git a/src/app/community-page/create-community-page/create-community-page.component.ts b/src/app/community-page/create-community-page/create-community-page.component.ts index be3385b92f..b332fad100 100644 --- a/src/app/community-page/create-community-page/create-community-page.component.ts +++ b/src/app/community-page/create-community-page/create-community-page.component.ts @@ -3,7 +3,7 @@ import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; -import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; +import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { RequestService } from '../../core/data/request.service'; diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.ts b/src/app/community-page/delete-community-page/delete-community-page.component.ts index ec51076bbc..0cccc503e1 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.ts +++ b/src/app/community-page/delete-community-page/delete-community-page.component.ts @@ -2,10 +2,10 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component'; +import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import {RequestService} from '../../core/data/request.service'; +import { RequestService } from '../../core/data/request.service'; /** * Component that represents the page where a user can delete an existing Community diff --git a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts index c4bb88289f..a2dbfa6eb6 100644 --- a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts +++ b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { ActivatedRoute, Router } from '@angular/router'; import { Community } from '../../../core/shared/community.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; diff --git a/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts b/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts index b61705126c..d1188df02d 100644 --- a/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts @@ -12,6 +12,7 @@ import { SharedModule } from '../../../shared/shared.module'; import { RouterTestingModule } from '@angular/router/testing'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ComcolModule } from '../../../shared/comcol/comcol.module'; describe('CommunityRolesComponent', () => { @@ -50,6 +51,7 @@ describe('CommunityRolesComponent', () => { TestBed.configureTestingModule({ imports: [ + ComcolModule, SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), diff --git a/src/app/community-page/edit-community-page/edit-community-page.component.ts b/src/app/community-page/edit-community-page/edit-community-page.component.ts index 836384ab84..54a6ee4944 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { ActivatedRoute, Router } from '@angular/router'; -import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; +import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { getCommunityPageRoute } from '../community-page-routing-paths'; /** @@ -9,7 +9,7 @@ import { getCommunityPageRoute } from '../community-page-routing-paths'; */ @Component({ selector: 'ds-edit-community', - templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' + templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCommunityPageComponent extends EditComColPageComponent { type = 'community'; diff --git a/src/app/community-page/edit-community-page/edit-community-page.module.ts b/src/app/community-page/edit-community-page/edit-community-page.module.ts index 3f7511f700..2b0fc73f2a 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.module.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.module.ts @@ -8,6 +8,8 @@ import { CommunityMetadataComponent } from './community-metadata/community-metad import { CommunityRolesComponent } from './community-roles/community-roles.component'; import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; import { CommunityFormModule } from '../community-form/community-form.module'; +import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; +import { ComcolModule } from '../../shared/comcol/comcol.module'; /** * Module that contains all components related to the Edit Community page administrator functionality @@ -17,7 +19,9 @@ import { CommunityFormModule } from '../community-form/community-form.module'; CommonModule, SharedModule, EditCommunityPageRoutingModule, - CommunityFormModule + CommunityFormModule, + ComcolModule, + ResourcePoliciesModule ], declarations: [ EditCommunityPageComponent, diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index a28add2e30..ac68fadb31 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -129,6 +129,7 @@ describe('BrowseService', () => { describe('getBrowseEntriesFor and findList', () => { // should contain special characters such that url encoding can be tested as well const mockAuthorName = 'Donald Smith & Sons'; + const mockAuthorityKey = 'some authority key ?=;'; beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); @@ -155,7 +156,7 @@ describe('BrowseService', () => { it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName); - scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); + scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, undefined, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', { @@ -164,6 +165,20 @@ describe('BrowseService', () => { }); }); + describe('when getBrowseItemsFor is called with a valid filter value and authority key', () => { + it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { + const expected = browseDefinitions[1]._links.items.href + + '?filterValue=' + encodeURIComponent(mockAuthorName) + + '&filterAuthority=' + encodeURIComponent(mockAuthorityKey); + + scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, mockAuthorityKey, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); + scheduler.flush(); + + expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findAllByHref)).toBeObservable(cold('(a|)', { + a: expected + })); + }); + }); }); describe('getBrowseURLFor', () => { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index ffc6f313b9..05e625d6c1 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -105,7 +105,7 @@ export class BrowseService { * @param options Options to narrow down your search * @returns {Observable>>} */ - getBrowseItemsFor(filterValue: string, options: BrowseEntrySearchOptions): Observable>> { + getBrowseItemsFor(filterValue: string, filterAuthority: string, options: BrowseEntrySearchOptions): Observable>> { const href$ = this.getBrowseDefinitions().pipe( getBrowseDefinitionLinks(options.metadataDefinition), hasValueOperator(), @@ -132,6 +132,9 @@ export class BrowseService { if (isNotEmpty(filterValue)) { args.push(`filterValue=${encodeURIComponent(filterValue)}`); } + if (isNotEmpty(filterAuthority)) { + args.push(`filterAuthority=${encodeURIComponent(filterAuthority)}`); + } if (isNotEmpty(args)) { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 026c87be9d..2c9a24cb80 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -163,6 +163,7 @@ import { RootDataService } from './data/root-data.service'; import { Root } from './data/root.model'; import { SearchConfig } from './shared/search/search-filters/search-config.model'; import { SequenceService } from './shared/sequence.service'; +import { GroupDataService } from './eperson/group-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -285,6 +286,7 @@ const PROVIDERS = [ VocabularyService, VocabularyTreeviewService, SequenceService, + GroupDataService ]; /** diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index bff21d2c8d..3c885c0afd 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -20,7 +20,7 @@ import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { FindListOptions, GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { Bitstream } from '../shared/bitstream.model'; import { RequestEntryState } from './request.reducer'; diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts index a3a0a532ec..d2fc9e6d96 100644 --- a/src/app/core/data/external-source.service.ts +++ b/src/app/core/data/external-source.service.ts @@ -12,7 +12,7 @@ import { HttpClient } from '@angular/common/http'; import { FindListOptions } from './request.models'; import { Observable } from 'rxjs'; import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list.model'; diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index fc543c9072..8c24bd61d9 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; -import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; +import { SearchFilterConfig } from '../../shared/search/models/search-filter-config.model'; import { ParsedResponse } from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RestRequest } from './request.models'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; -import { FacetConfigResponse } from '../../shared/search/facet-config-response.model'; +import { FacetConfigResponse } from '../../shared/search/models/facet-config-response.model'; @Injectable() export class FacetConfigResponseParsingService extends DspaceRestResponseParsingService { diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 6b9e832685..12a2d4ba8c 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; -import { FacetValue } from '../../shared/search/facet-value.model'; +import { FacetValue } from '../../shared/search/models/facet-value.model'; import { ParsedResponse } from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RestRequest } from './request.models'; -import { FacetValues } from '../../shared/search/facet-values.model'; +import { FacetValues } from '../../shared/search/models/facet-values.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; @Injectable() diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts new file mode 100644 index 0000000000..680495686e --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { SingleFeatureAuthorizationGuard } from './single-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 StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard { + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { + super(authorizationService, router, authService); + } + + /** + * Check group management rights + */ + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanViewUsageStatistics); + } +} diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 15eba0e5db..01a482dc79 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -13,6 +13,7 @@ export enum FeatureID { CanManageGroup = 'canManageGroup', IsCollectionAdmin = 'isCollectionAdmin', IsCommunityAdmin = 'isCommunityAdmin', + CanChangePassword = 'canChangePassword', CanDownload = 'canDownload', CanRequestACopy = 'canRequestACopy', CanManageVersions = 'canManageVersions', @@ -25,4 +26,5 @@ export enum FeatureID { CanEditVersion = 'canEditVersion', CanDeleteVersion = 'canDeleteVersion', CanCreateVersion = 'canCreateVersion', + CanViewUsageStatistics = 'canViewUsageStatistics', } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c31b6b3c97..a8d380124e 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -25,7 +25,7 @@ import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models'; import { RequestService } from './request.service'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { Bundle } from '../shared/bundle.model'; import { MetadataMap } from '../shared/metadata.models'; import { BundleDataService } from './bundle-data.service'; diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index 876336bfa9..c9e523f796 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -5,9 +5,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util import { createPaginatedList } from '../../shared/testing/utils.test'; import { buildPaginatedList } from './paginated-list.model'; import { PageInfo } from '../shared/page-info.model'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; -import { SearchResult } from '../../shared/search/search-result.model'; +import { SearchResult } from '../../shared/search/models/search-result.model'; import { Item } from '../shared/item.model'; import { skip, take } from 'rxjs/operators'; import { ExternalSource } from '../shared/external-source.model'; diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 7ecf3a19cc..7808a24e92 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -1,11 +1,11 @@ import { ExternalSourceService } from './external-source.service'; import { SearchService } from '../shared/search/search.service'; import { concat, distinctUntilChanged, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { Observable, ReplaySubject } from 'rxjs'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list.model'; -import { SearchResult } from '../../shared/search/search-result.model'; +import { SearchResult } from '../../shared/search/models/search-result.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; import { Item } from '../shared/item.model'; diff --git a/src/app/core/data/mydspace-response-parsing.service.ts b/src/app/core/data/mydspace-response-parsing.service.ts index f71eaeb811..e111aca9dd 100644 --- a/src/app/core/data/mydspace-response-parsing.service.ts +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -4,7 +4,7 @@ import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RestRequest } from './request.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { hasValue } from '../../shared/empty.util'; -import { SearchObjects } from '../../shared/search/search-objects.model'; +import { SearchObjects } from '../../shared/search/models/search-objects.model'; import { MetadataMap, MetadataValue } from '../shared/metadata.models'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; diff --git a/src/app/core/data/root-data.service.spec.ts b/src/app/core/data/root-data.service.spec.ts index 9e5bb9a68f..e3e23dac12 100644 --- a/src/app/core/data/root-data.service.spec.ts +++ b/src/app/core/data/root-data.service.spec.ts @@ -1,13 +1,16 @@ import { RootDataService } from './root-data.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { RemoteData } from './remote-data'; import { Root } from './root.model'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { cold } from 'jasmine-marbles'; describe('RootDataService', () => { let service: RootDataService; let halService: HALEndpointService; + let restService; let rootEndpoint; beforeEach(() => { @@ -15,7 +18,10 @@ describe('RootDataService', () => { halService = jasmine.createSpyObj('halService', { getRootHref: rootEndpoint }); - service = new RootDataService(null, null, null, null, halService, null, null, null); + restService = jasmine.createSpyObj('halService', { + get: jasmine.createSpy('get') + }); + service = new RootDataService(null, null, null, null, halService, null, null, null, restService); (service as any).dataService = jasmine.createSpyObj('dataService', { findByHref: createSuccessfulRemoteDataObject$({}) }); @@ -35,4 +41,37 @@ describe('RootDataService', () => { }); }); }); + + describe('checkServerAvailability', () => { + let result$: Observable; + + it('should return observable of true when root endpoint is available', () => { + const mockResponse = { + statusCode: 200, + statusText: 'OK' + } as RawRestResponse; + + restService.get.and.returnValue(of(mockResponse)); + result$ = service.checkServerAvailability(); + + expect(result$).toBeObservable(cold('(a|)', { + a: true + })); + }); + + it('should return observable of false when root endpoint is not available', () => { + const mockResponse = { + statusCode: 500, + statusText: 'Internal Server Error' + } as RawRestResponse; + + restService.get.and.returnValue(of(mockResponse)); + result$ = service.checkServerAvailability(); + + expect(result$).toBeObservable(cold('(a|)', { + a: false + })); + }); + + }); }); diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts index 8b4e836671..ef542199dd 100644 --- a/src/app/core/data/root-data.service.ts +++ b/src/app/core/data/root-data.service.ts @@ -17,6 +17,10 @@ import { RemoteData } from './remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FindListOptions } from './request.models'; import { PaginatedList } from './paginated-list.model'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { catchError, map } from 'rxjs/operators'; +import { of } from 'rxjs/internal/observable/of'; /* tslint:disable:max-classes-per-file */ @@ -59,10 +63,24 @@ export class RootDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { + protected comparator: DefaultChangeAnalyzer, + protected restService: DspaceRestService) { this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); } + /** + * Check if root endpoint is available + */ + checkServerAvailability(): Observable { + return this.restService.get(this.halService.getRootHref()).pipe( + catchError((err ) => { + console.error(err); + return of(false); + }), + map((res: RawRestResponse) => res.statusCode === 200) + ); + } + /** * Find the {@link Root} object of the REST API * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's @@ -106,5 +124,12 @@ export class RootDataService { findAllByHref(href: string | Observable, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + + /** + * Set to sale the root endpoint cache hit + */ + invalidateRootCache() { + this.requestService.setStaleByHrefSubstring(this.halService.getRootHref()); + } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index be2fbe90fc..814a2f8d1f 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { hasValue } from '../../shared/empty.util'; -import { SearchObjects } from '../../shared/search/search-objects.model'; +import { SearchObjects } from '../../shared/search/models/search-objects.model'; import { ParsedResponse } from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; diff --git a/src/app/core/data/version-history-data.service.ts b/src/app/core/data/version-history-data.service.ts index 4268516e6b..9aa4f055ff 100644 --- a/src/app/core/data/version-history-data.service.ts +++ b/src/app/core/data/version-history-data.service.ts @@ -12,7 +12,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { FindListOptions, PostRequest, RestRequest } from './request.models'; import { Observable, of } from 'rxjs'; -import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list.model'; import { Version } from '../shared/version.model'; diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 5b8f474d1a..9b20650725 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -40,9 +40,7 @@ const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegis /** * Provides methods to retrieve eperson group resources from the REST API & Group related CRUD actions. */ -@Injectable({ - providedIn: 'root' -}) +@Injectable() @dataService(GROUP) export class GroupDataService extends DataService { protected linkPath = 'groups'; diff --git a/src/app/core/log/log.interceptor.spec.ts b/src/app/core/log/log.interceptor.spec.ts index 9bda4b7934..cae9c32202 100644 --- a/src/app/core/log/log.interceptor.spec.ts +++ b/src/app/core/log/log.interceptor.spec.ts @@ -9,12 +9,17 @@ import { RestRequestMethod } from '../data/rest-request-method'; import { CookieService } from '../services/cookie.service'; import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { RouterStub } from '../../shared/testing/router.stub'; +import { CorrelationIdService } from '../../correlation-id/correlation-id.service'; +import { UUIDService } from '../shared/uuid.service'; +import { StoreModule } from '@ngrx/store'; +import { appReducers, storeModuleConfig } from '../../app.reducer'; describe('LogInterceptor', () => { let service: DspaceRestService; let httpMock: HttpTestingController; let cookieService: CookieService; + let correlationIdService: CorrelationIdService; const router = Object.assign(new RouterStub(),{url : '/statistics'}); // Mock payload/statuses are dummy content as we are not testing the results @@ -28,7 +33,10 @@ describe('LogInterceptor', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [ + HttpClientTestingModule, + StoreModule.forRoot(appReducers, storeModuleConfig), + ], providers: [ DspaceRestService, // LogInterceptor, @@ -39,14 +47,18 @@ describe('LogInterceptor', () => { }, { provide: CookieService, useValue: new CookieServiceMock() }, { provide: Router, useValue: router }, + { provide: CorrelationIdService, useClass: CorrelationIdService }, + { provide: UUIDService, useClass: UUIDService }, ], }); - service = TestBed.get(DspaceRestService); - httpMock = TestBed.get(HttpTestingController); - cookieService = TestBed.get(CookieService); + service = TestBed.inject(DspaceRestService); + httpMock = TestBed.inject(HttpTestingController); + cookieService = TestBed.inject(CookieService); + correlationIdService = TestBed.inject(CorrelationIdService); cookieService.set('CORRELATION-ID','123455'); + correlationIdService.initCorrelationId(); }); diff --git a/src/app/core/log/log.interceptor.ts b/src/app/core/log/log.interceptor.ts index bf843f1da8..aff1a24963 100644 --- a/src/app/core/log/log.interceptor.ts +++ b/src/app/core/log/log.interceptor.ts @@ -3,9 +3,8 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/c import { Router } from '@angular/router'; import { Observable } from 'rxjs'; - -import { CookieService } from '../services/cookie.service'; import { hasValue } from '../../shared/empty.util'; +import { CorrelationIdService } from '../../correlation-id/correlation-id.service'; /** * Log Interceptor intercepting Http Requests & Responses to @@ -15,12 +14,12 @@ import { hasValue } from '../../shared/empty.util'; @Injectable() export class LogInterceptor implements HttpInterceptor { - constructor(private cookieService: CookieService, private router: Router) {} + constructor(private cidService: CorrelationIdService, private router: Router) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { - // Get Unique id of the user from the cookies - const correlationId = this.cookieService.get('CORRELATION-ID'); + // Get the correlation id for the user from the store + const correlationId = this.cidService.getCorrelationId(); // Add headers from the intercepted request let headers = request.headers; diff --git a/src/app/core/server-check/server-check.guard.spec.ts b/src/app/core/server-check/server-check.guard.spec.ts new file mode 100644 index 0000000000..1f126be5e5 --- /dev/null +++ b/src/app/core/server-check/server-check.guard.spec.ts @@ -0,0 +1,68 @@ +import { ServerCheckGuard } from './server-check.guard'; +import { Router } from '@angular/router'; + +import { of } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; +import { RootDataService } from '../data/root-data.service'; +import SpyObj = jasmine.SpyObj; + +describe('ServerCheckGuard', () => { + let guard: ServerCheckGuard; + let router: SpyObj; + let rootDataServiceStub: SpyObj; + + rootDataServiceStub = jasmine.createSpyObj('RootDataService', { + checkServerAvailability: jasmine.createSpy('checkServerAvailability'), + invalidateRootCache: jasmine.createSpy('invalidateRootCache') + }); + router = jasmine.createSpyObj('Router', { + navigateByUrl: jasmine.createSpy('navigateByUrl') + }); + + beforeEach(() => { + guard = new ServerCheckGuard(router, rootDataServiceStub); + }); + + afterEach(() => { + router.navigateByUrl.calls.reset(); + rootDataServiceStub.invalidateRootCache.calls.reset(); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + describe('when root endpoint has succeeded', () => { + beforeEach(() => { + rootDataServiceStub.checkServerAvailability.and.returnValue(of(true)); + }); + + it('should not redirect to error page', () => { + guard.canActivateChild({} as any, {} as any).pipe( + take(1) + ).subscribe((canActivate: boolean) => { + expect(canActivate).toEqual(true); + expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled(); + expect(router.navigateByUrl).not.toHaveBeenCalled(); + }); + }); + }); + + describe('when root endpoint has not succeeded', () => { + beforeEach(() => { + rootDataServiceStub.checkServerAvailability.and.returnValue(of(false)); + }); + + it('should redirect to error page', () => { + guard.canActivateChild({} as any, {} as any).pipe( + take(1) + ).subscribe((canActivate: boolean) => { + expect(canActivate).toEqual(false); + expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled(); + expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute()); + }); + }); + }); +}); diff --git a/src/app/core/server-check/server-check.guard.ts b/src/app/core/server-check/server-check.guard.ts new file mode 100644 index 0000000000..8a0e26c01d --- /dev/null +++ b/src/app/core/server-check/server-check.guard.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { take, tap } from 'rxjs/operators'; + +import { RootDataService } from '../data/root-data.service'; +import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; + +@Injectable({ + providedIn: 'root' +}) +/** + * A guard that checks if root api endpoint is reachable. + * If not redirect to 500 error page + */ +export class ServerCheckGuard implements CanActivateChild { + constructor(private router: Router, private rootDataService: RootDataService) { + } + + /** + * True when root api endpoint is reachable. + */ + canActivateChild( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable { + + return this.rootDataService.checkServerAvailability().pipe( + take(1), + tap((isAvailable: boolean) => { + if (!isAvailable) { + this.rootDataService.invalidateRootCache(); + this.router.navigateByUrl(getPageInternalServerErrorRoute()); + } + }) + ); + + } +} diff --git a/src/app/core/services/server-response.service.ts b/src/app/core/services/server-response.service.ts index df5662991d..02e00446bc 100644 --- a/src/app/core/services/server-response.service.ts +++ b/src/app/core/services/server-response.service.ts @@ -31,4 +31,8 @@ export class ServerResponseService { setNotFound(message = 'Not found'): this { return this.setStatus(404, message); } + + setInternalServerError(message = 'Internal Server Error'): this { + return this.setStatus(500, message); + } } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 3be04447ab..ea2a0283eb 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -13,7 +13,7 @@ import { withLatestFrom } from 'rxjs/operators'; import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; -import { SearchResult } from '../../shared/search/search-result.model'; +import { SearchResult } from '../../shared/search/models/search-result.model'; import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RestRequest } from '../data/request.models'; diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index 805ecd0486..96f9ac5018 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -2,8 +2,8 @@ import { SearchConfigurationService } from './search-configuration.service'; import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; -import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; -import { SearchFilter } from '../../../shared/search/search-filter.model'; +import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; +import { SearchFilter } from '../../../shared/search/models/search-filter.model'; import { of as observableOf } from 'rxjs'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 8c37fbc8f5..4e911b913e 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -3,31 +3,27 @@ import { ActivatedRoute, Params } from '@angular/router'; import { BehaviorSubject, - combineLatest, combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, map, startWith, switchMap, take } from 'rxjs/operators'; +import { filter, map, startWith } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { SearchOptions } from '../../../shared/search/search-options.model'; -import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; -import { SearchFilter } from '../../../shared/search/search-filter.model'; +import { SearchOptions } from '../../../shared/search/models/search-options.model'; +import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; +import { SearchFilter } from '../../../shared/search/models/search-filter.model'; import { RemoteData } from '../../data/remote-data'; import { DSpaceObjectType } from '../dspace-object-type.model'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; import { RouteService } from '../../services/route.service'; -import { - getAllSucceededRemoteDataPayload, - getFirstSucceededRemoteData -} from '../operators'; +import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteData } from '../operators'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { SearchConfig } from './search-filters/search-config.model'; +import { SearchConfig, SortConfig } from './search-filters/search-config.model'; import { SearchService } from './search.service'; -import { of } from 'rxjs'; import { PaginationService } from '../../pagination/pagination.service'; +import { ViewMode } from '../view-mode.model'; /** * Service that performs all actions that have to do with the current search configuration @@ -35,7 +31,21 @@ import { PaginationService } from '../../pagination/pagination.service'; @Injectable() export class SearchConfigurationService implements OnDestroy { + /** + * Default pagination id + */ public paginationID = 'spc'; + + /** + * Emits the current search options + */ + public searchOptions: BehaviorSubject; + + /** + * Emits the current search options including pagination and sort + */ + public paginatedSearchOptions: BehaviorSubject; + /** * Default pagination settings */ @@ -45,16 +55,6 @@ export class SearchConfigurationService implements OnDestroy { currentPage: 1 }); - /** - * Default sort settings - */ - protected defaultSort = new SortOptions('score', SortDirection.DESC); - - /** - * Default configuration parameter setting - */ - protected defaultConfiguration; - /** * Default scope setting */ @@ -71,23 +71,14 @@ export class SearchConfigurationService implements OnDestroy { protected _defaults: Observable>; /** - * Emits the current search options + * A map of subscriptions to unsubscribe from on destroy */ - public searchOptions: BehaviorSubject; - - /** - * Emits the current search options including pagination and sort - */ - public paginatedSearchOptions: BehaviorSubject; - - /** - * List of subscriptions to unsubscribe from on destroy - */ - protected subs: Subscription[] = []; + protected subs: Map = new Map(null); /** * Initialize the search options * @param {RouteService} routeService + * @param {PaginationService} paginationService * @param {ActivatedRoute} route */ constructor(protected routeService: RouteService, @@ -98,29 +89,28 @@ export class SearchConfigurationService implements OnDestroy { } /** - * Initialize the search options + * Default values for the Search Options */ - protected initDefaults() { - this.defaults - .pipe(getFirstSucceededRemoteData()) - .subscribe((defRD: RemoteData) => { - const defs = defRD.payload; - this.paginatedSearchOptions = new BehaviorSubject(defs); - this.searchOptions = new BehaviorSubject(defs); - this.subs.push(this.subscribeToSearchOptions(defs)); - this.subs.push(this.subscribeToPaginatedSearchOptions(defs.pagination.id, defs)); - } - ); + get defaults(): Observable> { + if (hasNoValue(this._defaults)) { + const options = new PaginatedSearchOptions({ + pagination: this.defaultPagination, + scope: this.defaultScope, + query: this.defaultQuery + }); + this._defaults = createSuccessfulRemoteDataObject$(options, new Date().getTime()); + } + return this._defaults; } /** * @returns {Observable} Emits the current configuration string */ getCurrentConfiguration(defaultConfiguration: string) { - return observableCombineLatest( + return observableCombineLatest([ this.routeService.getQueryParameterValue('configuration').pipe(startWith(undefined)), this.routeService.getRouteParameterValue('configuration').pipe(startWith(undefined)) - ).pipe( + ]).pipe( map(([queryConfig, routeConfig]) => { return queryConfig || routeConfig || defaultConfiguration; }) @@ -208,59 +198,91 @@ export class SearchConfigurationService implements OnDestroy { } /** - * Creates an observable of SearchConfig every time the configuration$ stream emits. - * @param configuration$ - * @param service + * @returns {Observable} Emits the current view mode */ - getConfigurationSearchConfigObservable(configuration$: Observable, service: SearchService): Observable { - return configuration$.pipe( - distinctUntilChanged(), - switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)), - getAllSucceededRemoteDataPayload()); - } - - /** - * Every time searchConfig change (after a configuration change) it update the navigation with the default sort option - * and emit the new paginateSearchOptions value. - * @param configuration$ - * @param service - */ - initializeSortOptionsFromConfiguration(searchConfig$: Observable) { - const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([ - of(searchConfig), - this.paginatedSearchOptions.pipe(take(1)) - ]))).subscribe(([searchConfig, searchOptions]) => { - const field = searchConfig.sortOptions[0].name; - const direction = searchConfig.sortOptions[0].sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC; - const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { - sort: new SortOptions(field, direction) - }); - this.paginationService.updateRoute(this.paginationID, - { - sortDirection: updateValue.sort.direction, - sortField: updateValue.sort.field, - }); - this.paginatedSearchOptions.next(updateValue); - }); - this.subs.push(subscription); - } - - /** - * Creates an observable of available SortOptions[] every time the searchConfig$ stream emits. - * @param searchConfig$ - * @param service - */ - getConfigurationSortOptionsObservable(searchConfig$: Observable): Observable { - return searchConfig$.pipe(map((searchConfig) => { - const sortOptions = []; - searchConfig.sortOptions.forEach(sortOption => { - sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC)); - sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC)); - }); - return sortOptions; + getCurrentViewMode(defaultViewMode: ViewMode) { + return this.routeService.getQueryParameterValue('view').pipe(map((viewMode) => { + return viewMode || defaultViewMode; })); } + /** + * Creates an observable of SearchConfig every time the configuration stream emits. + * @param configuration The search configuration + * @param service The search service to use + * @param scope The search scope if exists + */ + getConfigurationSearchConfig(configuration: string, service: SearchService, scope?: string): Observable { + return service.getSearchConfigurationFor(scope, configuration).pipe( + getAllSucceededRemoteDataPayload() + ); + } + + /** + * Return the SortOptions list available for the given SearchConfig + * @param searchConfig The SearchConfig object + */ + getConfigurationSortOptions(searchConfig: SearchConfig): SortOptions[] { + return searchConfig.sortOptions.map((entry: SortConfig) => ({ + field: entry.name, + direction: entry.sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC + })); + } + + setPaginationId(paginationId): void { + if (isNotEmpty(paginationId)) { + const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue(); + const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, { + pagination: Object.assign({}, currentValue.pagination, { + id: paginationId + }) + }); + // unsubscribe from subscription related to old pagination id + this.unsubscribeFromSearchOptions(this.paginationID); + + // change to the new pagination id + this.paginationID = paginationId; + this.paginatedSearchOptions.next(updatedValue); + this.setSearchSubscription(this.paginationID, this.paginatedSearchOptions.value); + } + } + + /** + * Make sure to unsubscribe from all existing subscription to prevent memory leaks + */ + ngOnDestroy(): void { + this.subs + .forEach((subs: Subscription[]) => subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()) + ); + + this.subs = new Map(null); + } + + /** + * Initialize the search options + */ + protected initDefaults() { + this.defaults + .pipe(getFirstSucceededRemoteData()) + .subscribe((defRD: RemoteData) => { + const defs = defRD.payload; + this.paginatedSearchOptions = new BehaviorSubject(defs); + this.searchOptions = new BehaviorSubject(defs); + this.setSearchSubscription(this.paginationID, defs); + }); + } + + private setSearchSubscription(paginationID: string, defaults: PaginatedSearchOptions) { + this.unsubscribeFromSearchOptions(paginationID); + const subs = [ + this.subscribeToSearchOptions(defaults), + this.subscribeToPaginatedSearchOptions(paginationID || defaults.pagination.id, defaults) + ]; + this.subs.set(this.paginationID, subs); + } + /** * Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update * @param {SearchOptions} defaults Default values for when no parameters are available @@ -273,7 +295,8 @@ export class SearchConfigurationService implements OnDestroy { this.getQueryPart(defaults.query), this.getDSOTypePart(), this.getFiltersPart(), - this.getFixedFilterPart() + this.getFixedFilterPart(), + this.getViewModePart(defaults.view) ).subscribe((update) => { const currentValue: SearchOptions = this.searchOptions.getValue(); const updatedValue: SearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update); @@ -283,19 +306,21 @@ export class SearchConfigurationService implements OnDestroy { /** * Sets up a subscription to all necessary parameters to make sure the paginatedSearchOptions emits a new value every time they update + * @param {string} paginationId The pagination ID * @param {PaginatedSearchOptions} defaults Default values for when no parameters are available * @returns {Subscription} The subscription to unsubscribe from */ private subscribeToPaginatedSearchOptions(paginationId: string, defaults: PaginatedSearchOptions): Subscription { return observableMerge( + this.getConfigurationPart(defaults.configuration), this.getPaginationPart(paginationId, defaults.pagination), this.getSortPart(paginationId, defaults.sort), - this.getConfigurationPart(defaults.configuration), this.getScopePart(defaults.scope), this.getQueryPart(defaults.query), this.getDSOTypePart(), this.getFiltersPart(), - this.getFixedFilterPart() + this.getFixedFilterPart(), + this.getViewModePart(defaults.view) ).subscribe((update) => { const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue(); const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update); @@ -304,30 +329,16 @@ export class SearchConfigurationService implements OnDestroy { } /** - * Default values for the Search Options + * Unsubscribe from all subscriptions related to the given paginationID + * @param paginationId The pagination id */ - get defaults(): Observable> { - if (hasNoValue(this._defaults)) { - const options = new PaginatedSearchOptions({ - pagination: this.defaultPagination, - configuration: this.defaultConfiguration, - sort: this.defaultSort, - scope: this.defaultScope, - query: this.defaultQuery - }); - this._defaults = createSuccessfulRemoteDataObject$(options, new Date().getTime()); + private unsubscribeFromSearchOptions(paginationId: string): void { + if (this.subs.has(this.paginationID)) { + this.subs.get(this.paginationID) + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + this.subs.delete(paginationId); } - return this._defaults; - } - - /** - * Make sure to unsubscribe from all existing subscription to prevent memory leaks - */ - ngOnDestroy(): void { - this.subs.forEach((sub) => { - sub.unsubscribe(); - }); - this.subs = []; } /** @@ -404,4 +415,13 @@ export class SearchConfigurationService implements OnDestroy { }), ); } + + /** + * @returns {Observable} Emits the current view mode as a partial SearchOptions object + */ + private getViewModePart(defaultViewMode: ViewMode): Observable { + return this.getCurrentViewMode(defaultViewMode).pipe(map((view) => { + return { view }; + })); + } } diff --git a/src/app/core/shared/search/search-filter.service.spec.ts b/src/app/core/shared/search/search-filter.service.spec.ts index 045b2b17c9..a42bf8e5f6 100644 --- a/src/app/core/shared/search/search-filter.service.spec.ts +++ b/src/app/core/shared/search/search-filter.service.spec.ts @@ -10,8 +10,8 @@ import { SearchFilterToggleAction } from '../../../shared/search/search-filters/search-filter/search-filter.actions'; import { SearchFiltersState } from '../../../shared/search/search-filters/search-filter/search-filter.reducer'; -import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model'; -import { FilterType } from '../../../shared/search/filter-type.model'; +import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; +import { FilterType } from '../../../shared/search/models/filter-type.model'; import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { of as observableOf } from 'rxjs'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; diff --git a/src/app/core/shared/search/search-filter.service.ts b/src/app/core/shared/search/search-filter.service.ts index 84d7268abb..00125e31f5 100644 --- a/src/app/core/shared/search/search-filter.service.ts +++ b/src/app/core/shared/search/search-filter.service.ts @@ -16,7 +16,7 @@ import { SearchFilterToggleAction } from '../../../shared/search/search-filters/search-filter/search-filter.actions'; import { hasValue, isNotEmpty, } from '../../../shared/empty.util'; -import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model'; +import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; import { RouteService } from '../../services/route.service'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; diff --git a/src/app/core/shared/search/search-filters/search-config.model.ts b/src/app/core/shared/search/search-filters/search-config.model.ts index 725761fe7b..e789de0f80 100644 --- a/src/app/core/shared/search/search-filters/search-config.model.ts +++ b/src/app/core/shared/search/search-filters/search-config.model.ts @@ -29,7 +29,7 @@ export class SearchConfig implements CacheableObject { * The configured sort options. */ @autoserialize - sortOptions: SortOption[]; + sortOptions: SortConfig[]; /** * The object type. @@ -63,7 +63,7 @@ export interface FilterConfig { /** * Interface to model sort option's configuration. */ -export interface SortOption { +export interface SortConfig { name: string; sortOrder: string; } diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts index 00f10230c3..f9b768655e 100644 --- a/src/app/core/shared/search/search.service.spec.ts +++ b/src/app/core/shared/search/search.service.spec.ts @@ -9,7 +9,7 @@ import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { RouterStub } from '../../../shared/testing/router.stub'; import { HALEndpointService } from '../hal-endpoint.service'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { RemoteData } from '../../data/remote-data'; import { RequestEntry } from '../../data/request.reducer'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; @@ -21,11 +21,8 @@ import { RouteService } from '../../services/route.service'; import { routeServiceStub } from '../../../shared/testing/route-service.stub'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { SearchObjects } from '../../../shared/search/search-objects.model'; +import { SearchObjects } from '../../../shared/search/models/search-objects.model'; import { PaginationService } from '../../pagination/pagination.service'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; -import { FindListOptions } from '../../data/request.models'; import { SearchConfigurationService } from './search-configuration.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 91916a35ac..f70416594d 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -14,24 +14,24 @@ import { GenericConstructor } from '../generic-constructor'; import { HALEndpointService } from '../hal-endpoint.service'; import { URLCombiner } from '../../url-combiner/url-combiner'; import { hasValue, hasValueOperator, isNotEmpty } from '../../../shared/empty.util'; -import { SearchOptions } from '../../../shared/search/search-options.model'; -import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model'; +import { SearchOptions } from '../../../shared/search/models/search-options.model'; +import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; import { SearchResponseParsingService } from '../../data/search-response-parsing.service'; -import { SearchObjects } from '../../../shared/search/search-objects.model'; +import { SearchObjects } from '../../../shared/search/models/search-objects.model'; import { FacetValueResponseParsingService } from '../../data/facet-value-response-parsing.service'; import { FacetConfigResponseParsingService } from '../../data/facet-config-response-parsing.service'; -import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { CommunityDataService } from '../../data/community-data.service'; import { ViewMode } from '../view-mode.model'; import { DSpaceObjectDataService } from '../../data/dspace-object-data.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../operators'; import { RouteService } from '../../services/route.service'; -import { SearchResult } from '../../../shared/search/search-result.model'; +import { SearchResult } from '../../../shared/search/models/search-result.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator'; -import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model'; -import { FacetValues } from '../../../shared/search/facet-values.model'; +import { FacetConfigResponse } from '../../../shared/search/models/facet-config-response.model'; +import { FacetValues } from '../../../shared/search/models/facet-values.model'; import { SearchConfig } from './search-filters/search-config.model'; import { PaginationService } from '../../pagination/pagination.service'; import { SearchConfigurationService } from './search-configuration.service'; @@ -407,6 +407,7 @@ export class SearchService implements OnDestroy { /** * Changes the current view mode in the current URL * @param {ViewMode} viewMode Mode to switch to + * @param {string[]} searchLinkParts */ setViewMode(viewMode: ViewMode, searchLinkParts?: string[]) { this.paginationService.getCurrentPagination(this.searchConfigurationService.paginationID, new PaginationComponentOptions()).pipe(take(1)) diff --git a/src/app/core/shared/version.model.ts b/src/app/core/shared/version.model.ts index 48d6eb0b68..7207637a21 100644 --- a/src/app/core/shared/version.model.ts +++ b/src/app/core/shared/version.model.ts @@ -47,6 +47,12 @@ export class Version extends DSpaceObject { @autoserialize summary: string; + /** + * The name of the submitter of this version + */ + @autoserialize + submitterName: string; + /** * The Date this version was created */ diff --git a/src/app/core/shared/view-mode.model.ts b/src/app/core/shared/view-mode.model.ts index c2f076a5e5..a27cb6954b 100644 --- a/src/app/core/shared/view-mode.model.ts +++ b/src/app/core/shared/view-mode.model.ts @@ -3,8 +3,8 @@ */ export enum ViewMode { - ListElement = 'listElement', - GridElement = 'gridElement', - DetailedListElement = 'detailedListElement', - StandalonePage = 'standalonePage', + ListElement = 'list', + GridElement = 'grid', + DetailedListElement = 'detailed', + StandalonePage = 'standalone', } diff --git a/src/app/correlation-id/correlation-id.actions.ts b/src/app/correlation-id/correlation-id.actions.ts new file mode 100644 index 0000000000..d57d41a637 --- /dev/null +++ b/src/app/correlation-id/correlation-id.actions.ts @@ -0,0 +1,21 @@ +import { type } from '../shared/ngrx/type'; +import { Action } from '@ngrx/store'; + +export const CorrelationIDActionTypes = { + SET: type('dspace/core/correlationId/SET') +}; + +/** + * Action for setting a new correlation ID + */ +export class SetCorrelationIdAction implements Action { + type = CorrelationIDActionTypes.SET; + + constructor(public payload: string) { + } +} + +/** + * Type alias for all correlation ID actions + */ +export type CorrelationIdAction = SetCorrelationIdAction; diff --git a/src/app/correlation-id/correlation-id.reducer.spec.ts b/src/app/correlation-id/correlation-id.reducer.spec.ts new file mode 100644 index 0000000000..c784def1d9 --- /dev/null +++ b/src/app/correlation-id/correlation-id.reducer.spec.ts @@ -0,0 +1,23 @@ +import { correlationIdReducer } from './correlation-id.reducer'; +import { SetCorrelationIdAction } from './correlation-id.actions'; + +describe('correlationIdReducer', () => { + it('should set the correlatinId with SET action', () => { + const initialState = null; + const currentState = correlationIdReducer(initialState, new SetCorrelationIdAction('new ID')); + + expect(currentState).toBe('new ID'); + }); + + it('should leave correlatinId unchanged otherwise', () => { + const initialState = null; + + let currentState = correlationIdReducer(initialState, { type: 'unknown' } as any); + expect(currentState).toBe(null); + + currentState = correlationIdReducer(currentState, new SetCorrelationIdAction('new ID')); + currentState = correlationIdReducer(currentState, { type: 'unknown' } as any); + + expect(currentState).toBe('new ID'); + }); +}); diff --git a/src/app/correlation-id/correlation-id.reducer.ts b/src/app/correlation-id/correlation-id.reducer.ts new file mode 100644 index 0000000000..b7525b0b1c --- /dev/null +++ b/src/app/correlation-id/correlation-id.reducer.ts @@ -0,0 +1,27 @@ +import { + CorrelationIdAction, + CorrelationIDActionTypes, + SetCorrelationIdAction +} from './correlation-id.actions'; +import { AppState } from '../app.reducer'; + +const initialState = null; + +export const correlationIdSelector = (state: AppState) => state.correlationId; + +/** + * Reducer that handles actions to update the correlation ID + * @param {string} state the previous correlation ID (null if unset) + * @param {CorrelationIdAction} action the action to perform + * @return {string} the new correlation ID + */ +export const correlationIdReducer = (state = initialState, action: CorrelationIdAction): string => { + switch (action.type) { + case CorrelationIDActionTypes.SET: { + return (action as SetCorrelationIdAction).payload; + } + default: { + return state; + } + } +}; diff --git a/src/app/correlation-id/correlation-id.service.spec.ts b/src/app/correlation-id/correlation-id.service.spec.ts new file mode 100644 index 0000000000..64a4d1068a --- /dev/null +++ b/src/app/correlation-id/correlation-id.service.spec.ts @@ -0,0 +1,83 @@ +import { CorrelationIdService } from './correlation-id.service'; +import { CookieServiceMock } from '../shared/mocks/cookie.service.mock'; +import { UUIDService } from '../core/shared/uuid.service'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { TestBed } from '@angular/core/testing'; +import { Store, StoreModule } from '@ngrx/store'; +import { appReducers, AppState, storeModuleConfig } from '../app.reducer'; +import { SetCorrelationIdAction } from './correlation-id.actions'; + +describe('CorrelationIdService', () => { + let service: CorrelationIdService; + + let cookieService; + let uuidService; + let store; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot(appReducers, storeModuleConfig), + ], + }).compileComponents(); + }); + + beforeEach(() => { + cookieService = new CookieServiceMock(); + uuidService = new UUIDService(); + store = TestBed.inject(Store) as MockStore; + service = new CorrelationIdService(cookieService, uuidService, store); + }); + + describe('getCorrelationId', () => { + it('should get from from store', () => { + expect(service.getCorrelationId()).toBe(null); + store.dispatch(new SetCorrelationIdAction('some value')); + expect(service.getCorrelationId()).toBe('some value'); + }); + }); + + + describe('initCorrelationId', () => { + const cookieCID = 'cookie CID'; + const storeCID = 'store CID'; + + it('should set cookie and store values to a newly generated value if neither ex', () => { + service.initCorrelationId(); + + expect(cookieService.get('CORRELATION-ID')).toBeTruthy(); + expect(service.getCorrelationId()).toBeTruthy(); + expect(cookieService.get('CORRELATION-ID')).toEqual(service.getCorrelationId()); + }); + + it('should set store value to cookie value if present', () => { + expect(service.getCorrelationId()).toBe(null); + + cookieService.set('CORRELATION-ID', cookieCID); + + service.initCorrelationId(); + + expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID); + expect(service.getCorrelationId()).toBe(cookieCID); + }); + + it('should set cookie value to store value if present', () => { + store.dispatch(new SetCorrelationIdAction(storeCID)); + + service.initCorrelationId(); + + expect(cookieService.get('CORRELATION-ID')).toBe(storeCID); + expect(service.getCorrelationId()).toBe(storeCID); + }); + + it('should set store value to cookie value if both are present', () => { + cookieService.set('CORRELATION-ID', cookieCID); + store.dispatch(new SetCorrelationIdAction(storeCID)); + + service.initCorrelationId(); + + expect(cookieService.get('CORRELATION-ID')).toBe(cookieCID); + expect(service.getCorrelationId()).toBe(cookieCID); + }); + }); +}); diff --git a/src/app/correlation-id/correlation-id.service.ts b/src/app/correlation-id/correlation-id.service.ts new file mode 100644 index 0000000000..6f4b2a5341 --- /dev/null +++ b/src/app/correlation-id/correlation-id.service.ts @@ -0,0 +1,64 @@ +import { CookieService } from '../core/services/cookie.service'; +import { UUIDService } from '../core/shared/uuid.service'; +import { Store, select } from '@ngrx/store'; +import { AppState } from '../app.reducer'; +import { isEmpty } from '../shared/empty.util'; +import { correlationIdSelector } from './correlation-id.reducer'; +import { take } from 'rxjs/operators'; +import { SetCorrelationIdAction } from './correlation-id.actions'; +import { Injectable } from '@angular/core'; + +/** + * Service to manage the correlation id, an id used to give context to server side logs + */ +@Injectable({ + providedIn: 'root' +}) +export class CorrelationIdService { + + constructor( + protected cookieService: CookieService, + protected uuidService: UUIDService, + protected store: Store, + ) { + } + + /** + * Initialize the correlation id based on the cookie or the ngrx store + */ + initCorrelationId(): void { + // first see of there's a cookie with a correlation-id + let correlationId = this.cookieService.get('CORRELATION-ID'); + + // if there isn't see if there's an ID in the store + if (isEmpty(correlationId)) { + correlationId = this.getCorrelationId(); + } + + // if no id was found, create a new id + if (isEmpty(correlationId)) { + correlationId = this.uuidService.generate(); + } + + // Store the correct id both in the store and as a cookie to ensure they're in sync + this.store.dispatch(new SetCorrelationIdAction(correlationId)); + this.cookieService.set('CORRELATION-ID', correlationId); + } + + /** + * Get the correlation id from the store + */ + getCorrelationId(): string { + let correlationId; + + this.store.pipe( + select(correlationIdSelector), + take(1) + ).subscribe((storeId: string) => { + // we can do this because ngrx selects are synchronous + correlationId = storeId; + }); + + return correlationId; + } +} diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts index dc88490eac..3bf861e10d 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -19,6 +19,7 @@ import { JournalVolumeSearchResultGridElementComponent } from './item-grid-eleme import { JournalVolumeSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component'; import { JournalIssueSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component'; import { JournalSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component'; +import { ItemSharedModule } from '../../item-page/item-shared.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -45,6 +46,7 @@ const ENTRY_COMPONENTS = [ @NgModule({ imports: [ CommonModule, + ItemSharedModule, SharedModule ], declarations: [ diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index c9d6c2d5b6..721a22be08 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { SharedModule } from '../../shared/shared.module'; import { OrgUnitComponent } from './item-pages/org-unit/org-unit.component'; import { PersonComponent } from './item-pages/person/person.component'; @@ -27,6 +28,7 @@ import { ExternalSourceEntryListSubmissionElementComponent } from './submission/ import { OrgUnitSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component'; import { PersonSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component'; import { ProjectSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component'; +import { ItemSharedModule } from '../../item-page/item-shared.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -65,7 +67,9 @@ const COMPONENTS = [ @NgModule({ imports: [ CommonModule, - SharedModule + ItemSharedModule, + SharedModule, + NgbTooltipModule ], declarations: [ ...COMPONENTS, @@ -79,7 +83,7 @@ export class ResearchEntitiesModule { static withEntryComponents() { return { ngModule: ResearchEntitiesModule, - providers: ENTRY_COMPONENTS.map((component) => ({provide: component})) + providers: ENTRY_COMPONENTS.map((component) => ({ provide: component })) }; } } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html index 87a422e7db..c4e31d3d81 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html @@ -15,7 +15,7 @@ -
+
{{'profile.card.security' | translate}}
{ let component: ProfilePageComponent; @@ -28,10 +31,13 @@ describe('ProfilePageComponent', () => { let epersonService; let notificationsService; + const canChangePassword = new BehaviorSubject(true); + function init() { user = Object.assign(new EPerson(), { id: 'userId', - groups: createSuccessfulRemoteDataObject$(createPaginatedList([])) + groups: createSuccessfulRemoteDataObject$(createPaginatedList([])), + _links: {self: {href: 'test.com/uuid/1234567654321'}} }); initialState = { core: { @@ -74,6 +80,7 @@ describe('ProfilePageComponent', () => { { provide: EPersonDataService, useValue: epersonService }, { provide: NotificationsService, useValue: notificationsService }, { provide: AuthService, useValue: authService }, + { provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) }, provideMockStore({ initialState }), ], schemas: [NO_ERRORS_SCHEMA] @@ -183,7 +190,7 @@ describe('ProfilePageComponent', () => { component.setPasswordValue('testest'); component.setInvalid(false); - operations = [{op: 'add', path: '/password', value: 'testest'}]; + operations = [{ op: 'add', path: '/password', value: 'testest' }]; result = component.updateSecurity(); }); @@ -196,4 +203,36 @@ describe('ProfilePageComponent', () => { }); }); }); + + describe('canChangePassword$', () => { + describe('when the user is allowed to change their password', () => { + beforeEach(() => { + canChangePassword.next(true); + }); + + it('should contain true', () => { + getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: true }); + }); + + it('should show the security section on the page', () => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.security-section'))).not.toBeNull(); + }); + }); + + describe('when the user is not allowed to change their password', () => { + beforeEach(() => { + canChangePassword.next(false); + }); + + it('should contain false', () => { + getTestScheduler().expectObservable(component.canChangePassword$).toBe('(a)', { a: false }); + }); + + it('should not show the security section on the page', () => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.security-section'))).toBeNull(); + }); + }); + }); }); diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index e9c4dec832..fece166a59 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -18,6 +18,8 @@ import { hasValue, isNotEmpty } from '../shared/empty.util'; import { followLink } from '../shared/utils/follow-link-config.model'; import { AuthService } from '../core/auth/auth.service'; import { Operation } from 'fast-json-patch'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../core/data/feature-authorization/feature-id'; @Component({ selector: 'ds-profile-page', @@ -67,11 +69,13 @@ export class ProfilePageComponent implements OnInit { * The authenticated user */ private currentUser: EPerson; + canChangePassword$: Observable; constructor(private authService: AuthService, private notificationsService: NotificationsService, private translate: TranslateService, - private epersonService: EPersonDataService) { + private epersonService: EPersonDataService, + private authorizationService: AuthorizationDataService) { } ngOnInit(): void { @@ -83,6 +87,7 @@ export class ProfilePageComponent implements OnInit { tap((user: EPerson) => this.currentUser = user) ); this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups)); + this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href))); } /** diff --git a/src/app/profile-page/profile-page.module.ts b/src/app/profile-page/profile-page.module.ts index 0b2274f2d4..dc9595140b 100644 --- a/src/app/profile-page/profile-page.module.ts +++ b/src/app/profile-page/profile-page.module.ts @@ -6,12 +6,14 @@ import { ProfilePageComponent } from './profile-page.component'; import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component'; import { ThemedProfilePageComponent } from './themed-profile-page.component'; +import { FormModule } from '../shared/form/form.module'; @NgModule({ imports: [ ProfilePageRoutingModule, CommonModule, - SharedModule + SharedModule, + FormModule ], exports: [ ProfilePageSecurityFormComponent, diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index 6ba859ef23..dc44095573 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -1,5 +1,5 @@ import { map } from 'rxjs/operators'; -import { Component, Inject, OnInit, Input } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; @@ -19,6 +19,7 @@ import { ThemeConfig } from '../../config/theme.model'; import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider'; import { environment } from '../../environments/environment'; import { slideSidebarPadding } from '../shared/animations/slide'; +import { getPageInternalServerErrorRoute } from '../app-routing-paths'; @Component({ selector: 'ds-root', @@ -68,9 +69,13 @@ export class RootComponent implements OnInit { this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth'); const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN); - this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm()) + this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()]) .pipe( map(([collapsed, mobile]) => collapsed || mobile) ); + + if (this.router.url === getPageInternalServerErrorRoute()) { + this.shouldShowRouteLoader = false; + } } } diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts index ba08c7ca75..fbdcdc951c 100644 --- a/src/app/search-navbar/search-navbar.component.spec.ts +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -2,18 +2,14 @@ import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angul import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Router } from '@angular/router'; +import { NavigationExtras, Router } from '@angular/router'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { SearchService } from '../core/shared/search/search.service'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { SearchNavbarComponent } from './search-navbar.component'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { of as observableOf } from 'rxjs'; -import { PaginationService } from '../core/pagination/pagination.service'; -import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; +import { RouterTestingModule } from '@angular/router/testing'; describe('SearchNavbarComponent', () => { let component: SearchNavbarComponent; @@ -41,6 +37,7 @@ describe('SearchNavbarComponent', () => { FormsModule, ReactiveFormsModule, BrowserAnimationsModule, + RouterTestingModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -49,10 +46,7 @@ describe('SearchNavbarComponent', () => { })], declarations: [SearchNavbarComponent], providers: [ - { provide: SearchService, useValue: mockSearchService }, - { provide: PaginationService, useValue: paginationService }, - { provide: Router, useValue: routerStub }, - { provide: SearchConfigurationService, useValue: {paginationID: 'page-id'} } + { provide: SearchService, useValue: mockSearchService } ] }) .compileComponents(); @@ -61,8 +55,8 @@ describe('SearchNavbarComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SearchNavbarComponent); component = fixture.componentInstance; + router = TestBed.inject(Router); fixture.detectChanges(); - router = (component as any).router; }); it('should create', () => { @@ -73,7 +67,7 @@ describe('SearchNavbarComponent', () => { beforeEach(fakeAsync(() => { spyOn(component, 'expand').and.callThrough(); spyOn(component, 'onSubmit').and.callThrough(); - spyOn(router, 'navigate').and.callThrough(); + spyOn(router, 'navigate'); const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); searchIcon.triggerEventHandler('click', { preventDefault: () => {/**/ @@ -99,8 +93,9 @@ describe('SearchNavbarComponent', () => { fixture.detectChanges(); })); it('to search page with empty query', () => { + const extras: NavigationExtras = {queryParams: { query: '' }, queryParamsHandling: 'merge'}; expect(component.onSubmit).toHaveBeenCalledWith({ query: '' }); - expect(paginationService.updateRouteWithUrl).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['search'], extras); }); }); }); @@ -123,8 +118,10 @@ describe('SearchNavbarComponent', () => { fixture.detectChanges(); })); it('to search page with query', async () => { + const extras: NavigationExtras = { queryParams: { query: 'test' }, queryParamsHandling: 'merge'}; expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' }); - expect(paginationService.updateRouteWithUrl).toHaveBeenCalled(); + + expect(router.navigate).toHaveBeenCalledWith(['search'], extras); }); }); }); diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts index 1e509a180b..ccdaa27861 100644 --- a/src/app/search-navbar/search-navbar.component.ts +++ b/src/app/search-navbar/search-navbar.component.ts @@ -3,8 +3,6 @@ import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { SearchService } from '../core/shared/search/search.service'; import { expandSearchInput } from '../shared/animations/slide'; -import { PaginationService } from '../core/pagination/pagination.service'; -import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; /** * The search box in the header that expands on focus and collapses on focus out @@ -26,9 +24,7 @@ export class SearchNavbarComponent { // Search input field @ViewChild('searchInput') searchField: ElementRef; - constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService, - private paginationService: PaginationService, - private searchConfig: SearchConfigurationService) { + constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService) { this.searchForm = this.formBuilder.group(({ query: '', })); @@ -65,8 +61,13 @@ export class SearchNavbarComponent { */ onSubmit(data: any) { this.collapse(); - const linkToNavigateTo = this.searchService.getSearchLink().split('/'); + const queryParams = Object.assign({}, data); + const linkToNavigateTo = [this.searchService.getSearchLink().replace('/', '')]; this.searchForm.reset(); - this.paginationService.updateRouteWithUrl(this.searchConfig.paginationID, linkToNavigateTo, {page: 1}, data); + + this.router.navigate(linkToNavigateTo, { + queryParams: queryParams, + queryParamsHandling: 'merge' + }); } } diff --git a/src/app/search-page/configuration-search-page.component.spec.ts b/src/app/search-page/configuration-search-page.component.spec.ts index 5ca593981f..4468aad9c4 100644 --- a/src/app/search-page/configuration-search-page.component.spec.ts +++ b/src/app/search-page/configuration-search-page.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { configureSearchComponentTestingModule } from './search.component.spec'; +import { configureSearchComponentTestingModule } from '../shared/search/search.component.spec'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { Component, ViewChild } from '@angular/core'; diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index df25febde7..8ccafb3bf6 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -1,17 +1,10 @@ import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { SearchComponent } from './search.component'; -import { - ChangeDetectionStrategy, - Component, - Inject, - Input, - OnInit -} from '@angular/core'; +import { SearchComponent } from '../shared/search/search.component'; +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { hasValue } from '../shared/empty.util'; import { RouteService } from '../core/services/route.service'; import { SearchService } from '../core/shared/search/search.service'; import { Router } from '@angular/router'; @@ -21,8 +14,8 @@ import { Router } from '@angular/router'; */ @Component({ selector: 'ds-configuration-search-page', - styleUrls: ['./search.component.scss'], - templateUrl: './search.component.html', + styleUrls: ['../shared/search/search.component.scss'], + templateUrl: '../shared/search/search.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [pushInOut], providers: [ @@ -33,18 +26,7 @@ import { Router } from '@angular/router'; ] }) -export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit { - /** - * The configuration to use for the search options - * If empty, the configuration will be determined by the route parameter called 'configuration' - */ - @Input() configuration: string; - - /** - * The actual query for the fixed filter. - * If empty, the query will be determined by the route parameter called 'filter' - */ - @Input() fixedFilterQuery: string; +export class ConfigurationSearchPageComponent extends SearchComponent { constructor(protected service: SearchService, protected sidebarService: SidebarService, @@ -55,20 +37,4 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements super(service, sidebarService, windowService, searchConfigService, routeService, router); } - /** - * Listening to changes in the paginated search options - * If something changes, update the search results - * - * Listen to changes in the scope - * If something changes, update the list of scopes for the dropdown - */ - ngOnInit(): void { - super.ngOnInit(); - if (hasValue(this.configuration)) { - this.routeService.setParameter('configuration', this.configuration); - } - if (hasValue(this.fixedFilterQuery)) { - this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery); - } - } } diff --git a/src/app/search-page/search-page.component.ts b/src/app/search-page/search-page.component.ts index 393af37c27..38647b779b 100644 --- a/src/app/search-page/search-page.component.ts +++ b/src/app/search-page/search-page.component.ts @@ -1,8 +1,16 @@ import { Component } from '@angular/core'; +import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; @Component({ selector: 'ds-search-page', templateUrl: './search-page.component.html', + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } + ] }) /** * This component represents the whole search page diff --git a/src/app/search-page/search-page.module.ts b/src/app/search-page/search-page.module.ts index 0cf6164f8a..758eca15c0 100644 --- a/src/app/search-page/search-page.module.ts +++ b/src/app/search-page/search-page.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; -import { SearchComponent } from './search.component'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { SearchTrackerComponent } from './search-tracker.component'; @@ -14,10 +13,10 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; import { ThemedSearchPageComponent } from './themed-search-page.component'; +import { SearchModule } from '../shared/search/search.module'; const components = [ SearchPageComponent, - SearchComponent, SearchTrackerComponent, ThemedSearchPageComponent ]; @@ -25,6 +24,7 @@ const components = [ @NgModule({ imports: [ CommonModule, + SearchModule, SharedModule.withEntryComponents(), CoreModule.forRoot(), StatisticsModule.forRoot(), diff --git a/src/app/search-page/search-tracker.component.ts b/src/app/search-page/search-tracker.component.ts index e7f59a2f23..f766e6f669 100644 --- a/src/app/search-page/search-tracker.component.ts +++ b/src/app/search-page/search-tracker.component.ts @@ -1,15 +1,15 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Angulartics2 } from 'angulartics2'; import { map, switchMap } from 'rxjs/operators'; -import { SearchComponent } from './search.component'; +import { SearchComponent } from '../shared/search/search.component'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { HostWindowService } from '../shared/host-window.service'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; import { RouteService } from '../core/services/route.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchService } from '../core/shared/search/search.service'; -import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; -import { SearchObjects } from '../shared/search/search-objects.model'; +import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model'; +import { SearchObjects } from '../shared/search/models/search-objects.model'; import { Router } from '@angular/router'; import { RemoteData } from '../core/data/remote-data'; import { DSpaceObject } from '../core/shared/dspace-object.model'; diff --git a/src/app/search-page/search.component.html b/src/app/search-page/search.component.html deleted file mode 100644 index 3489cccdfb..0000000000 --- a/src/app/search-page/search.component.html +++ /dev/null @@ -1,59 +0,0 @@ -
-
-
- -
-
-
- -
-
- -
-
-
- - -
- -
-
-
- - - - - - - - - - -
-
- -
-
-
diff --git a/src/app/search-page/search.component.spec.ts b/src/app/search-page/search.component.spec.ts deleted file mode 100644 index 445f4614d3..0000000000 --- a/src/app/search-page/search.component.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; - -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { Store } from '@ngrx/store'; -import { TranslateModule } from '@ngx-translate/core'; -import { cold, hot } from 'jasmine-marbles'; -import { of as observableOf } from 'rxjs'; -import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { CommunityDataService } from '../core/data/community-data.service'; -import { HostWindowService } from '../shared/host-window.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { SearchComponent } from './search.component'; -import { SearchService } from '../core/shared/search/search.service'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ActivatedRoute } from '@angular/router'; -import { By } from '@angular/platform-browser'; -import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; -import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { SearchFilterService } from '../core/shared/search/search-filter.service'; -import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; -import { RouteService } from '../core/services/route.service'; -import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service.stub'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; -import { SidebarServiceStub } from '../shared/testing/sidebar-service.stub'; - -let comp: SearchComponent; -let fixture: ComponentFixture; -let searchServiceObject: SearchService; -let searchConfigurationServiceObject: SearchConfigurationService; -const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: observableOf(true) -}); -const pagination: PaginationComponentOptions = new PaginationComponentOptions(); -pagination.id = 'search-results-pagination'; -pagination.currentPage = 1; -pagination.pageSize = 10; -const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null }; -const sort: SortOptions = new SortOptions('score', SortDirection.DESC); -const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']); -const searchServiceStub = jasmine.createSpyObj('SearchService', { - search: mockResults, - getSearchLink: '/search', - getScopes: observableOf(['test-scope']), - getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]}) -}); -const configurationParam = 'default'; -const queryParam = 'test query'; -const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; -const fixedFilter = 'fixed filter'; -const paginatedSearchOptions = new PaginatedSearchOptions({ - configuration: configurationParam, - query: queryParam, - scope: scopeParam, - fixedFilter: fixedFilter, - pagination, - sort -}); -const activatedRouteStub = { - snapshot: { - queryParamMap: new Map([ - ['query', queryParam], - ['scope', scopeParam] - ]) - }, - queryParams: observableOf({ - query: queryParam, - scope: scopeParam - }) -}; - -const routeServiceStub = { - getRouteParameterValue: () => { - return observableOf(''); - }, - getQueryParameterValue: () => { - return observableOf(''); - }, - getQueryParamsWithPrefix: () => { - return observableOf(''); - } -}; - -export function configureSearchComponentTestingModule(compType, additionalDeclarations: any[] = []) { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule], - declarations: [compType, ...additionalDeclarations], - providers: [ - { provide: SearchService, useValue: searchServiceStub }, - { - provide: CommunityDataService, - useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll']) - }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, - { provide: RouteService, useValue: routeServiceStub }, - { - provide: Store, useValue: store - }, - { - provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService', - { - isXs: observableOf(true), - isSm: observableOf(false), - isXsOrSm: observableOf(true) - }) - }, - { - provide: SidebarService, - useValue: SidebarServiceStub - }, - { - provide: SearchFilterService, - useValue: {} - }, - { - provide: SearchConfigurationService, - useValue: { - paginatedSearchOptions: hot('a', { - a: paginatedSearchOptions - }), - getCurrentScope: (a) => observableOf('test-id'), - /* tslint:disable:no-empty */ - updateFixedFilter: (newFilter) => { - } - /* tslint:enable:no-empty */ - } - }, - { - provide: SEARCH_CONFIG_SERVICE, - useValue: new SearchConfigurationServiceStub() - }, - ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideComponent(compType, { - set: { changeDetection: ChangeDetectionStrategy.Default } - }).compileComponents(); -} - -describe('SearchComponent', () => { - beforeEach(waitForAsync(() => { - configureSearchComponentTestingModule(SearchComponent); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(SearchComponent); - comp = fixture.componentInstance; // SearchComponent test instance - comp.inPlaceSearch = false; - fixture.detectChanges(); - searchServiceObject = (comp as any).service; - searchConfigurationServiceObject = (comp as any).searchConfigService; - }); - - afterEach(() => { - comp = null; - searchServiceObject = null; - searchConfigurationServiceObject = null; - }); - - it('should get the scope and query from the route parameters', () => { - - searchConfigurationServiceObject.paginatedSearchOptions.next(paginatedSearchOptions); - expect(comp.searchOptions$).toBeObservable(cold('b', { - b: paginatedSearchOptions - })); - - }); - - describe('when the open sidebar button is clicked in mobile view', () => { - - beforeEach(() => { - spyOn(comp, 'openSidebar'); - const openSidebarButton = fixture.debugElement.query(By.css('.open-sidebar')); - openSidebarButton.triggerEventHandler('click', null); - }); - - it('should trigger the openSidebar function', () => { - expect(comp.openSidebar).toHaveBeenCalled(); - }); - - }); - - describe('when stable', () => { - - beforeEach(() => { - fixture.detectChanges(); - }); - - it('should have initialized the sortOptions$ observable', (done) => { - - comp.sortOptions$.subscribe((sortOptions) => { - - expect(sortOptions.length).toEqual(2); - expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC)); - expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC)); - done(); - }); - - }); - - }); -}); diff --git a/src/app/search-page/search.component.ts b/src/app/search-page/search.component.ts deleted file mode 100644 index 2972175bdf..0000000000 --- a/src/app/search-page/search.component.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { startWith, switchMap } from 'rxjs/operators'; -import { PaginatedList } from '../core/data/paginated-list.model'; -import { RemoteData } from '../core/data/remote-data'; -import { DSpaceObject } from '../core/shared/dspace-object.model'; -import { pushInOut } from '../shared/animations/push'; -import { HostWindowService } from '../shared/host-window.service'; -import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { hasValue, isEmpty } from '../shared/empty.util'; -import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { RouteService } from '../core/services/route.service'; -import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; -import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; -import { SearchResult } from '../shared/search/search-result.model'; -import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { SearchService } from '../core/shared/search/search.service'; -import { currentPath } from '../shared/utils/route.utils'; -import { Router } from '@angular/router'; -import { Context } from '../core/shared/context.model'; -import { SortOptions } from '../core/cache/models/sort-options.model'; -import { followLink } from '../shared/utils/follow-link-config.model'; -import { Item } from '../core/shared/item.model'; - -@Component({ - selector: 'ds-search', - styleUrls: ['./search.component.scss'], - templateUrl: './search.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - animations: [pushInOut], - providers: [ - { - provide: SEARCH_CONFIG_SERVICE, - useClass: SearchConfigurationService - } - ] -}) - -/** - * This component renders a sidebar, a search input bar and the search results. - */ -export class SearchComponent implements OnInit { - /** - * The current search results - */ - resultsRD$: BehaviorSubject>>> = new BehaviorSubject(null); - - /** - * The current paginated search options - */ - searchOptions$: Observable; - - /** - * The current available sort options - */ - sortOptions$: Observable; - - /** - * Emits true if were on a small screen - */ - isXsOrSm$: Observable; - - /** - * Subscription to unsubscribe from - */ - sub: Subscription; - - /** - * True when the search component should show results on the current page - */ - @Input() inPlaceSearch = true; - - /** - * Whether or not the search bar should be visible - */ - @Input() - searchEnabled = true; - - /** - * The width of the sidebar (bootstrap columns) - */ - @Input() - sideBarWidth = 3; - - /** - * The currently applied configuration (determines title of search) - */ - @Input() - configuration$: Observable; - - /** - * The current context - */ - @Input() - context: Context; - - /** - * Link to the search page - */ - searchLink: string; - - /** - * Observable for whether or not the sidebar is currently collapsed - */ - isSidebarCollapsed$: Observable; - - constructor(protected service: SearchService, - protected sidebarService: SidebarService, - protected windowService: HostWindowService, - @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, - protected routeService: RouteService, - protected router: Router) { - this.isXsOrSm$ = this.windowService.isXsOrSm(); - } - - /** - * Listening to changes in the paginated search options - * If something changes, update the search results - * - * Listen to changes in the scope - * If something changes, update the list of scopes for the dropdown - */ - ngOnInit(): void { - this.isSidebarCollapsed$ = this.isSidebarCollapsed(); - this.searchLink = this.getSearchLink(); - this.searchOptions$ = this.getSearchOptions(); - this.sub = this.searchOptions$.pipe( - switchMap((options) => this.service.search( - options, undefined, false, true, followLink('thumbnail', { isOptional: true }) - ).pipe(getFirstCompletedRemoteData(), startWith(undefined)) - ) - ).subscribe((results) => { - this.resultsRD$.next(results); - }); - - if (isEmpty(this.configuration$)) { - this.configuration$ = this.searchConfigService.getCurrentConfiguration('default'); - } - - const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(this.configuration$, this.service); - - this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$); - this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$); - - } - - /** - * Get the current paginated search options - * @returns {Observable} - */ - protected getSearchOptions(): Observable { - return this.searchConfigService.paginatedSearchOptions; - } - - /** - * Set the sidebar to a collapsed state - */ - public closeSidebar(): void { - this.sidebarService.collapse(); - } - - /** - * Set the sidebar to an expanded state - */ - public openSidebar(): void { - this.sidebarService.expand(); - } - - /** - * Check if the sidebar is collapsed - * @returns {Observable} emits true if the sidebar is currently collapsed, false if it is expanded - */ - private isSidebarCollapsed(): Observable { - return this.sidebarService.isCollapsed; - } - - /** - * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true - */ - private getSearchLink(): string { - if (this.inPlaceSearch) { - return currentPath(this.router); - } - return this.service.getSearchLink(); - } - - /** - * Unsubscribe from the subscription - */ - ngOnDestroy(): void { - if (hasValue(this.sub)) { - this.sub.unsubscribe(); - } - } -} diff --git a/src/app/search-page/themed-configuration-search-page.component.ts b/src/app/search-page/themed-configuration-search-page.component.ts index 1419d18a88..e4d6e93402 100644 --- a/src/app/search-page/themed-configuration-search-page.component.ts +++ b/src/app/search-page/themed-configuration-search-page.component.ts @@ -55,7 +55,7 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent
- + {{ 'nav.login' | translate }}
+ {{ ('submission.sections.describe.relationship-lookup.search-tab.loading' | translate) }} + + +
+ + + +
- - - +
-
+
+ + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts index f2e7efeaa5..72c026e42e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts @@ -4,26 +4,23 @@ import { DsDynamicLookupRelationSearchTabComponent } from './dynamic-lookup-rela import { SearchService } from '../../../../../../core/shared/search/search.service'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; -import { RouteService } from '../../../../../../core/services/route.service'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../../../../utils/var.directive'; import { RelationshipOptions } from '../../../models/relationship-options.model'; import { of as observableOf } from 'rxjs'; -import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils'; import { buildPaginatedList } from '../../../../../../core/data/paginated-list.model'; import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; import { Item } from '../../../../../../core/shared/item.model'; -import { ActivatedRoute } from '@angular/router'; import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; import { RelationshipService } from '../../../../../../core/data/relationship.service'; import { relatedRelationships } from '../../../../../testing/related-relationships.mock'; -import { RelationshipTypeService } from '../../../../../../core/data/relationship-type.service'; import { RelationshipType } from '../../../../../../core/shared/item-relationships/relationship-type.model'; - +import { SearchObjects } from '../../../../../search/models/search-objects.model'; describe('DsDynamicLookupRelationSearchTabComponent', () => { @@ -43,6 +40,7 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { let selection$; let results; + let searchResult; let selectableListService; let lookupRelationService; const relationshipService = jasmine.createSpyObj('searchByItemsAndType',{ @@ -81,6 +79,9 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { selection$ = observableOf([searchResult1, searchResult2]); results = buildPaginatedList(undefined, [searchResult1, searchResult2, searchResult3]); + searchResult = Object.assign(new SearchObjects(), { + page: [searchResult1, searchResult2, searchResult3] + }); selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']); lookupRelationService = jasmine.createSpyObj('lookupRelationService', { getLocalResults: createSuccessfulRemoteDataObject$(results) @@ -103,14 +104,6 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { paginatedSearchOptions: observableOf(pSearchOptions) } }, - { - provide: RouteService, useValue: { - setParameter: () => { - // do nothing - } - } - }, - { provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } }, { provide: LookupRelationService, useValue: lookupRelationService }, { provide: PaginationService, useValue: new PaginationServiceStub() }, { provide: RelationshipService, useValue: relationshipService } @@ -188,6 +181,7 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { describe('check searchByItemsAndType', () => { it('should call relationshipService.searchByItemsAndType', () => { + component.onResultFound(searchResult); expect(relationshipService.searchByItemsAndType).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index 96af591540..f70f073743 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -2,20 +2,17 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { Item } from '../../../../../../core/shared/item.model'; -import { SearchResult } from '../../../../../search/search-result.model'; +import { SearchResult } from '../../../../../search/models/search-result.model'; import { PaginatedList } from '../../../../../../core/data/paginated-list.model'; -import { RemoteData } from '../../../../../../core/data/remote-data'; import { Observable } from 'rxjs'; import { RelationshipOptions } from '../../../models/relationship-options.model'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { SearchService } from '../../../../../../core/shared/search/search.service'; -import { ActivatedRoute, Router } from '@angular/router'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { hasValue } from '../../../../../empty.util'; -import { map, mapTo, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { map, mapTo, switchMap, take, tap } from 'rxjs/operators'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../../../../core/shared/operators'; -import { RouteService } from '../../../../../../core/services/route.service'; import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; import { Context } from '../../../../../../core/shared/context.model'; import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; @@ -24,6 +21,9 @@ import { RelationshipService } from '../../../../../../core/data/relationship.se import { RelationshipType } from '../../../../../../core/shared/item-relationships/relationship-type.model'; import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model'; +import { SearchObjects } from '../../../../../search/models/search-objects.model'; +import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; @Component({ @@ -107,7 +107,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest /** * Search results */ - resultsRD$: Observable>>>; + resultsRD$: BehaviorSubject> = new BehaviorSubject>(null); /** * Are all results selected? @@ -142,13 +142,15 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest */ linkTypes = CollectionElementLinkType; + /** + * Emits an event with the current search result entries + */ + @Output() resultFound: EventEmitter> = new EventEmitter>(); + constructor( private searchService: SearchService, - private router: Router, - private route: ActivatedRoute, private selectableListService: SelectableListService, public searchConfigService: SearchConfigurationService, - private routeService: RouteService, public lookupRelationService: LookupRelationService, private relationshipService: RelationshipService, private paginationService: PaginationService @@ -160,21 +162,6 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest */ ngOnInit(): void { this.resetRoute(); - this.routeService.setParameter('fixedFilterQuery', this.relationship.filter); - this.routeService.setParameter('configuration', this.relationship.searchConfiguration); - this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe( - switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, options).pipe( - tap( res => { - if ( !!res && res.hasSucceeded && this.isEditRelationship ) { - const idOfItems = res.payload.page.map( itemSearchResult => { - return itemSearchResult.indexableObject.uuid; - }); - this.setSelectedIds(idOfItems,res.payload.page); - } - }), - startWith(undefined), - )) - ); } /** @@ -188,7 +175,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest * Selects a page in the store * @param page The page to select */ - selectPage(page: SearchResult[]) { + selectPage(page: SearchResult[]) { this.selection$ .pipe(take(1)) .subscribe((selection: SearchResult[]) => { @@ -202,7 +189,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest * Deselects a page in the store * @param page the page to deselect */ - deselectPage(page: SearchResult[]) { + deselectPage(page: SearchResult[]) { this.allSelected = false; this.selection$ .pipe(take(1)) @@ -247,7 +234,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest * @param idOfItems the uuid of items that are being checked * @param resultListOfItems the list of results of the items */ - setSelectedIds(idOfItems, resultListOfItems) { + setSelectedIds(idOfItems: string[], resultListOfItems: SearchResult[]) { let relationType = this.relationshipType.rightwardType; if ( this.isLeft ) { relationType = this.relationshipType.leftwardType; @@ -267,7 +254,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest } const uuid = arrUrl[ arrUrl.length - 1 ]; - return this.getRelatedItem(uuid,resultListOfItems); + return this.getRelatedItem(uuid, resultListOfItems); }); selectableObject = selectableObject.filter( (selObject) => { @@ -287,11 +274,11 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.allSelected = false; this.selection$ .pipe(take(1)) - .subscribe((selection: SearchResult[]) => this.deselectObject.emit(...selection)); + .subscribe((selection: SearchResult[]) => this.deselectObject.emit(...selection)); this.selectableListService.deselectAll(this.listId); } - getRelatedItem(uuid: string, resultList: SearchResult[]) { + getRelatedItem(uuid: string, resultList: SearchResult[]) { return resultList.find( (resultItem) => { return resultItem.indexableObject.uuid === uuid; }); @@ -306,4 +293,15 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.subscription.unsubscribe(); } } + + onResultFound($event: SearchObjects) { + this.resultsRD$.next($event); + this.resultFound.emit($event); + if (this.isEditRelationship ) { + const idOfItems = $event.page.map( itemSearchResult => { + return itemSearchResult.indexableObject.uuid; + }); + this.setSelectedIds(idOfItems, $event.page); + } + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts index dbe46609a6..472a719b27 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts @@ -4,7 +4,7 @@ import { SearchConfigurationService } from '../../../../../../core/shared/search import { NO_ERRORS_SCHEMA } from '@angular/core'; import { VarDirective } from '../../../../../utils/var.directive'; import { Observable, of as observableOf } from 'rxjs'; -import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model'; import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; import { Item } from '../../../../../../core/shared/item.model'; import { DsDynamicLookupRelationSelectionTabComponent } from './dynamic-lookup-relation-selection-tab.component'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index 9ba0ee2413..a78bf8896c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -8,7 +8,7 @@ import { map, switchMap, take } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { buildPaginatedList, PaginatedList } from '../../../../../../core/data/paginated-list.model'; import { Router } from '@angular/router'; -import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { Context } from '../../../../../../core/shared/context.model'; import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils'; diff --git a/src/app/shared/form/form.module.ts b/src/app/shared/form/form.module.ts new file mode 100644 index 0000000000..62ab5bd647 --- /dev/null +++ b/src/app/shared/form/form.module.ts @@ -0,0 +1,82 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormComponent } from './form.component'; +import { DsDynamicFormComponent } from './builder/ds-dynamic-form-ui/ds-dynamic-form.component'; +import { + DsDynamicFormControlContainerComponent, + dsDynamicFormControlMapFn +} from './builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; +import { DsDynamicListComponent } from './builder/ds-dynamic-form-ui/models/list/dynamic-list.component'; +import { DsDynamicLookupComponent } from './builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; +import { DsDynamicDisabledComponent } from './builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component'; +import { DsDynamicLookupRelationModalComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component'; +import { DsDynamicScrollableDropdownComponent } from './builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; +import { DsDynamicTagComponent } from './builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component'; +import { DsDynamicOneboxComponent } from './builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; +import { DsDynamicRelationGroupComponent } from './builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components'; +import { DsDatePickerComponent } from './builder/ds-dynamic-form-ui/models/date-picker/date-picker.component'; +import { DsDynamicFormGroupComponent } from './builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component'; +import { DsDynamicFormArrayComponent } from './builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component'; +import { DsDatePickerInlineComponent } from './builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component'; +import { DsDynamicLookupRelationSearchTabComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component'; +import { DsDynamicLookupRelationSelectionTabComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component'; +import { DsDynamicLookupRelationExternalSourceTabComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component'; +import { SharedModule } from '../shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchModule } from '../search/search.module'; +import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; +import { ExistingMetadataListElementComponent } from './builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; +import { ExistingRelationListElementComponent } from './builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component'; +import { ExternalSourceEntryImportModalComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component'; +import { CustomSwitchComponent } from './builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component'; +import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; + +const COMPONENTS = [ + CustomSwitchComponent, + DsDynamicFormComponent, + DsDynamicFormControlContainerComponent, + DsDynamicListComponent, + DsDynamicLookupComponent, + DsDynamicLookupRelationSearchTabComponent, + DsDynamicLookupRelationSelectionTabComponent, + DsDynamicLookupRelationExternalSourceTabComponent, + DsDynamicDisabledComponent, + DsDynamicLookupRelationModalComponent, + DsDynamicScrollableDropdownComponent, + DsDynamicTagComponent, + DsDynamicOneboxComponent, + DsDynamicRelationGroupComponent, + DsDatePickerComponent, + DsDynamicFormGroupComponent, + DsDynamicFormArrayComponent, + DsDatePickerInlineComponent, + ExistingMetadataListElementComponent, + ExistingRelationListElementComponent, + ExternalSourceEntryImportModalComponent, + FormComponent +]; + +@NgModule({ + declarations: [ + ...COMPONENTS + ], + imports: [ + CommonModule, + DynamicFormsCoreModule, + DynamicFormsNGBootstrapUIModule, + SearchModule, + SharedModule, + TranslateModule + ], + exports: [ + ...COMPONENTS + ], + providers: [ + { + provide: DYNAMIC_FORM_CONTROL_MAP_FN, + useValue: dsDynamicFormControlMapFn + } + ] +}) +export class FormModule { +} diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html index 886c8f31c0..01edfc3e2c 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html @@ -27,7 +27,7 @@