diff --git a/src/app/app.component.scss b/src/app/app.component.scss index fa7e7a873a..7793b7529c 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,4 +1,3 @@ -@import '../styles/variables.scss'; @import '../styles/helpers/font_awesome_imports.scss'; @import '../../node_modules/bootstrap/scss/bootstrap.scss'; @import '../../node_modules/nouislider/distribute/nouislider.min'; @@ -48,4 +47,3 @@ ds-admin-sidebar { position: fixed; z-index: $sidebar-z-index; } - diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index bd2d832c67..5b78e3462f 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -26,7 +26,7 @@ import { HostWindowResizeAction } from './shared/host-window.actions'; import { MetadataService } from './core/metadata/metadata.service'; import { GLOBAL_CONFIG, ENV_CONFIG } from '../config'; -import { NativeWindowRef, NativeWindowService } from './shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { MockTranslateLoader } from './shared/mocks/mock-translate-loader'; import { MockMetadataService } from './shared/mocks/mock-metadata-service'; @@ -41,9 +41,11 @@ import { MenuServiceStub } from './shared/testing/menu-service-stub'; import { HostWindowService } from './shared/host-window.service'; import { HostWindowServiceStub } from './shared/testing/host-window-service-stub'; import { ActivatedRoute, Router } from '@angular/router'; -import { RouteService } from './shared/services/route.service'; +import { RouteService } from './core/services/route.service'; import { MockActivatedRoute } from './shared/mocks/mock-active-router'; import { MockRouter } from './shared/mocks/mock-router'; +import { MockCookieService } from './shared/mocks/mock-cookie.service'; +import { CookieService } from './core/services/cookie.service'; let comp: AppComponent; let fixture: ComponentFixture; @@ -78,6 +80,7 @@ describe('App component', () => { { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: CookieService, useValue: new MockCookieService()}, AppComponent, RouteService ], diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 37cc791558..50baaf6e57 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,19 +19,23 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config'; import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/host-window.reducer'; -import { NativeWindowRef, NativeWindowService } from './shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { isAuthenticated } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; -import { RouteService } from './shared/services/route.service'; +import { RouteService } from './core/services/route.service'; import variables from '../styles/_exposed_variables.scss'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; import { MenuID } from './shared/menu/initial-menus-state'; -import { Observable } from 'rxjs/internal/Observable'; +import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { slideSidebarPadding } from './shared/animations/slide'; -import { combineLatest as combineLatestObservable } from 'rxjs'; import { HostWindowService } from './shared/host-window.service'; +import { Theme } from '../config/theme.inferface'; +import { isNotEmpty } from './shared/empty.util'; +import { CookieService } from './core/services/cookie.service'; + +export const LANG_COOKIE = 'language_cookie'; @Component({ selector: 'ds-app', @@ -47,6 +51,7 @@ export class AppComponent implements OnInit, AfterViewInit { slideSidebarOver: Observable; collapsedSidebarWidth: Observable; totalSidebarWidth: Observable; + theme: Observable = of({} as any); constructor( @Inject(GLOBAL_CONFIG) public config: GlobalConfig, @@ -59,7 +64,8 @@ export class AppComponent implements OnInit, AfterViewInit { private router: Router, private cssService: CSSVariableService, private menuService: MenuService, - private windowService: HostWindowService + private windowService: HostWindowService, + private cookie: CookieService ) { // Load all the languages that are defined as active from the config file translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); @@ -67,11 +73,20 @@ export class AppComponent implements OnInit, AfterViewInit { // Load the default language from the config file translate.setDefaultLang(config.defaultLanguage); - // Attempt to get the browser language from the user - if (translate.getLangs().includes(translate.getBrowserLang())) { - translate.use(translate.getBrowserLang()); + // Attempt to get the language from a cookie + const lang = cookie.get(LANG_COOKIE); + if (isNotEmpty(lang)) { + // Cookie found + // Use the language from the cookie + translate.use(lang); } else { - translate.use(config.defaultLanguage); + // Cookie not found + // Attempt to get the browser language from the user + if (translate.getLangs().includes(translate.getBrowserLang())) { + translate.use(translate.getBrowserLang()); + } else { + translate.use(config.defaultLanguage); + } } metadata.listenForRouteChange(); @@ -83,6 +98,7 @@ export class AppComponent implements OnInit, AfterViewInit { } ngOnInit() { + const env: string = this.config.production ? 'Production' : 'Development'; const color: string = this.config.production ? 'red' : 'green'; console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ce5a2d78a2..916788df8c 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -39,6 +39,7 @@ import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/e import { NavbarModule } from './navbar/navbar.module'; import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module'; +import { ClientCookieService } from './core/services/client-cookie.service'; export function getConfig() { return ENV_CONFIG; @@ -97,7 +98,8 @@ const PROVIDERS = [ { provide: RouterStateSerializer, useClass: DSpaceRouterStateSerializer - } + }, + ClientCookieService ]; const DECLARATIONS = [ diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index ea2512a974..e3333fb34a 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -23,6 +23,10 @@ import { hasValue } from './shared/empty.util'; import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer'; import { menusReducer, MenusState } from './shared/menu/menu.reducer'; import { historyReducer, HistoryState } from './shared/history/history.reducer'; +import { + bitstreamFormatReducer, + BitstreamFormatRegistryState +} from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; export interface AppState { router: fromRouter.RouterReducerState; @@ -30,6 +34,7 @@ export interface AppState { hostWindow: HostWindowState; forms: FormState; metadataRegistry: MetadataRegistryState; + bitstreamFormats: BitstreamFormatRegistryState; notifications: NotificationsState; searchSidebar: SearchSidebarState; searchFilter: SearchFiltersState; @@ -44,6 +49,7 @@ export const appReducers: ActionReducerMap = { hostWindow: hostWindowReducer, forms: formReducer, metadataRegistry: metadataRegistryReducer, + bitstreamFormats: bitstreamFormatReducer, notifications: notificationsReducer, searchSidebar: sidebarReducer, searchFilter: filterReducer, diff --git a/src/app/core/auth/auth-object-factory.ts b/src/app/core/auth/auth-object-factory.ts deleted file mode 100644 index 02458f4e3e..0000000000 --- a/src/app/core/auth/auth-object-factory.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthType } from './auth-type'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; -import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { NormalizedGroup } from '../eperson/models/normalized-group.model'; - -export class AuthObjectFactory { - public static getConstructor(type): GenericConstructor> { - switch (type) { - case AuthType.EPerson: { - return NormalizedEPerson - } - - case AuthType.Group: { - return NormalizedGroup - } - - case AuthType.Status: { - return NormalizedAuthStatus - } - - default: { - return undefined; - } - } - } -} diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index 0b2c32fc04..112d60b8d2 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -35,95 +35,103 @@ describe('AuthResponseParsingService', () => { }); describe('parse', () => { - const validRequest = new AuthPostRequest( - '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', - 'https://rest.api/dspace-spring-rest/api/authn/login', - 'password=test&user=myself@testshib.org'); + let validRequest; + let validRequest2; + let validResponse; + let validResponse1; + let validResponse2; + beforeEach(() => { - const validRequest2 = new AuthGetRequest( - '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', - 'https://rest.api/dspace-spring-rest/api/authn/status'); + validRequest = new AuthPostRequest( + '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', + 'https://rest.api/dspace-spring-rest/api/authn/login', + 'password=test&user=myself@testshib.org'); - const validResponse = { - payload: { - authenticated: true, - id: null, - okay: true, - token: { - accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiI0ZGM3MGFiNS1jZDczLTQ5MmYtYjAwNy0zMTc5ZDJkOTI5NmIiLCJzZyI6W10sImV4cCI6MTUyNjMxODMyMn0.ASmvcbJFBfzhN7D5ncloWnaVZr5dLtgTuOgHaCKiimc', - expires: 1526318322000 - }, - } as AuthStatus, - statusCode: 200, - statusText: '200' - }; + validRequest2 = new AuthGetRequest( + '69f375b5-19f4-4453-8c7a-7dc5c55aafbb', + 'https://rest.api/dspace-spring-rest/api/authn/status'); - const validResponse1 = { - payload: {}, - statusCode: 404, - statusText: '404' - }; + validResponse = { + payload: { + authenticated: true, + id: null, + okay: true, + token: { + accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiI0ZGM3MGFiNS1jZDczLTQ5MmYtYjAwNy0zMTc5ZDJkOTI5NmIiLCJzZyI6W10sImV4cCI6MTUyNjMxODMyMn0.ASmvcbJFBfzhN7D5ncloWnaVZr5dLtgTuOgHaCKiimc', + expires: 1526318322000 + }, + } as AuthStatus, + statusCode: 200, + statusText: '200' + }; - const validResponse2 = { - payload: { - authenticated: true, - id: null, - okay: true, - type: 'status', - _embedded: { - eperson: { - canLogIn: true, - email: 'myself@testshib.org', - groups: [], - handle: null, - id: '4dc70ab5-cd73-492f-b007-3179d2d9296b', - lastActive: '2018-05-14T17:03:31.277+0000', - metadata: { - 'eperson.firstname': [ - { - language: null, - value: 'User' + validResponse1 = { + payload: {}, + statusCode: 404, + statusText: '404' + }; + + validResponse2 = { + payload: { + authenticated: true, + id: null, + okay: true, + type: 'status', + _embedded: { + eperson: { + canLogIn: true, + email: 'myself@testshib.org', + groups: [], + handle: null, + id: '4dc70ab5-cd73-492f-b007-3179d2d9296b', + lastActive: '2018-05-14T17:03:31.277+0000', + metadata: { + 'eperson.firstname': [ + { + language: null, + value: 'User' + } + ], + 'eperson.lastname': [ + { + language: null, + value: 'Test' + } + ], + 'eperson.language': [ + { + language: null, + value: 'en' + } + ] + }, + name: 'User Test', + netid: 'myself@testshib.org', + requireCertificate: false, + selfRegistered: false, + type: 'eperson', + uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b', + _links: { + self: { + href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' } - ], - 'eperson.lastname': [ - { - language: null, - value: 'Test' - } - ], - 'eperson.language': [ - { - language: null, - value: 'en' - } - ] - }, - name: 'User Test', - netid: 'myself@testshib.org', - requireCertificate: false, - selfRegistered: false, - type: 'eperson', - uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b', - _links: { - self: { - href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' } } + }, + _links: { + eperson: { + href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' + }, + self: { + href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' + } } }, - _links: { - eperson: { - href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' - }, - self: { - href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' - } - } - }, - statusCode: 200, - statusText: '200' + statusCode: 200, + statusText: '200' - }; + }; + }); it('should return a AuthStatusResponse if data contains a valid AuthStatus object as payload', () => { const response = service.parse(validRequest, validResponse); diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index c736c3b22b..a5a160531c 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@angular/core'; -import { AuthObjectFactory } from './auth-object-factory'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { AuthStatusResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; @@ -10,7 +9,6 @@ import { isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ResponseParsingService } from '../data/parsing.service'; import { RestRequest } from '../data/request.models'; -import { AuthType } from './auth-type'; import { AuthStatus } from './models/auth-status.model'; import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; import { NormalizedObject } from '../cache/models/normalized-object.model'; @@ -18,7 +16,6 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; @Injectable() export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = AuthObjectFactory; protected toCache = true; constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @@ -28,7 +25,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 200)) { - const response = this.process, AuthType>(data.payload, request.uuid); + const response = this.process>(data.payload, request.uuid); return new AuthStatusResponse(response, data.statusCode, data.statusText); } else { return new AuthStatusResponse(data.payload as NormalizedAuthStatus, data.statusCode, data.statusText); diff --git a/src/app/core/auth/auth-type.ts b/src/app/core/auth/auth-type.ts deleted file mode 100644 index f0460449ea..0000000000 --- a/src/app/core/auth/auth-type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum AuthType { - EPerson = 'eperson', - Status = 'status', - Group = 'group' -} diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index e766a45e48..5084dc8596 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -7,12 +7,12 @@ import { REQUEST } from '@nguniversal/express-engine/tokens'; import { of as observableOf } from 'rxjs'; import { authReducer, AuthState } from './auth.reducer'; -import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { AuthService } from './auth.service'; import { RouterStub } from '../../shared/testing/router-stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; -import { CookieService } from '../../shared/services/cookie.service'; +import { CookieService } from '../services/cookie.service'; import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub'; import { AuthRequestService } from './auth-request.service'; import { AuthStatus } from './models/auth-status.model'; @@ -20,14 +20,17 @@ import { AuthTokenInfo } from './models/auth-token-info.model'; import { EPerson } from '../eperson/models/eperson.model'; import { EPersonMock } from '../../shared/testing/eperson-mock'; import { AppState } from '../../app.reducer'; -import { ClientCookieService } from '../../shared/services/client-cookie.service'; +import { ClientCookieService } from '../services/client-cookie.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { routeServiceStub } from '../../shared/testing/route-service-stub'; +import { RouteService } from '../services/route.service'; describe('AuthService test', () => { let mockStore: Store; let authService: AuthService; + let routeServiceMock: RouteService; let authRequest; let window; let routerStub; @@ -74,6 +77,7 @@ describe('AuthService test', () => { { provide: NativeWindowService, useValue: window }, { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, + { provide: RouteService, useValue: routeServiceStub }, { provide: ActivatedRoute, useValue: routeStub }, { provide: Store, useValue: mockStore }, { provide: RemoteDataBuildService, useValue: rdbService }, @@ -138,6 +142,7 @@ describe('AuthService test', () => { { provide: AuthRequestService, useValue: authRequest }, { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, + { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: rdbService }, CookieService, AuthService @@ -145,13 +150,13 @@ describe('AuthService test', () => { }).compileComponents(); })); - beforeEach(inject([CookieService, AuthRequestService, Store, Router], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router) => { + beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService) => { store .subscribe((state) => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, undefined, authReqService, router, cookieService, store, rdbService); + authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, rdbService); })); it('should return true when user is logged in', () => { @@ -189,6 +194,7 @@ describe('AuthService test', () => { { provide: AuthRequestService, useValue: authRequest }, { provide: REQUEST, useValue: {} }, { provide: Router, useValue: routerStub }, + { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: rdbService }, ClientCookieService, CookieService, @@ -197,7 +203,7 @@ describe('AuthService test', () => { }).compileComponents(); })); - beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store, router: Router) => { + beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService) => { const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token'); expiredToken.expires = Date.now() - (1000 * 60 * 60); authenticatedState = { @@ -212,11 +218,14 @@ describe('AuthService test', () => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, undefined, authReqService, router, cookieService, store, rdbService); + authService = new AuthService({}, window, undefined, authReqService, router, routeService, cookieService, store, rdbService); storage = (authService as any).storage; + routeServiceMock = TestBed.get(RouteService); + routerStub = TestBed.get(Router); spyOn(storage, 'get'); spyOn(storage, 'remove'); spyOn(storage, 'set'); + })); it('should throw false when token is not valid', () => { @@ -238,5 +247,32 @@ describe('AuthService test', () => { expect(storage.remove).toHaveBeenCalled(); }); + it ('should set redirect url to previous page', () => { + spyOn(routeServiceMock, 'getHistory').and.callThrough(); + authService.redirectAfterLoginSuccess(true); + expect(routeServiceMock.getHistory).toHaveBeenCalled(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/collection/123']); + }); + + it ('should set redirect url to current page', () => { + spyOn(routeServiceMock, 'getHistory').and.callThrough(); + authService.redirectAfterLoginSuccess(false); + expect(routeServiceMock.getHistory).toHaveBeenCalled(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/home']); + }); + + it ('should redirect to / and not to /login', () => { + spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login'])); + authService.redirectAfterLoginSuccess(true); + expect(routeServiceMock.getHistory).toHaveBeenCalled(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/']); + }); + + it ('should redirect to / when no redirect url is found', () => { + spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf([''])); + authService.redirectAfterLoginSuccess(true); + expect(routeServiceMock.getHistory).toHaveBeenCalled(); + expect(routerStub.navigate).toHaveBeenCalledWith(['/']); + }); }); }); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a01768e687..5287e537ee 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -15,13 +15,14 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; -import { CookieService } from '../../shared/services/cookie.service'; +import { CookieService } from '../services/cookie.service'; import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions'; -import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import {RouteService} from '../services/route.service'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -45,6 +46,7 @@ export class AuthService { protected authRequestService: AuthRequestService, @Optional() @Inject(RESPONSE) private response: any, protected router: Router, + protected routeService: RouteService, protected storage: CookieService, protected store: Store, protected rdbService: RemoteDataBuildService @@ -337,7 +339,7 @@ export class AuthService { /** * Redirect to the route navigated before the login */ - public redirectToPreviousUrl() { + public redirectAfterLoginSuccess(isStandalonePage: boolean) { this.getRedirectUrl().pipe( take(1)) .subscribe((redirectUrl) => { @@ -346,18 +348,39 @@ export class AuthService { this.clearRedirectUrl(); this.router.onSameUrlNavigation = 'reload'; const url = decodeURIComponent(redirectUrl); - this.router.navigateByUrl(url); - /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ - // this._window.nativeWindow.location.href = url; + this.navigateToRedirectUrl(url); } else { - this.router.navigate(['/']); - /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ - // this._window.nativeWindow.location.href = '/'; + // If redirectUrl is empty use history. + this.routeService.getHistory().pipe( + take(1) + ).subscribe((history) => { + let redirUrl; + if (isStandalonePage) { + // For standalone login pages, use the previous route. + redirUrl = history[history.length - 2] || ''; + } else { + redirUrl = history[history.length - 1] || ''; + } + this.navigateToRedirectUrl(redirUrl); + }); } - }) + }); } + protected navigateToRedirectUrl(url: string) { + // in case the user navigates directly to /login (via bookmark, etc), or the route history is not found. + if (isEmpty(url) || url.startsWith(LOGIN_ROUTE)) { + this.router.navigate(['/']); + /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ + // this._window.nativeWindow.location.href = '/'; + } else { + /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ + // this._window.nativeWindow.location.href = url; + this.router.navigate([url]); + } + } + /** * Refresh route navigated */ @@ -400,4 +423,5 @@ export class AuthService { this.store.dispatch(new SetRedirectUrlAction('')); this.storage.remove(REDIRECT_COOKIE); } + } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 6e722a80c9..e0d568397a 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -4,20 +4,51 @@ import { EPerson } from '../../eperson/models/eperson.model'; import { RemoteData } from '../../data/remote-data'; import { Observable } from 'rxjs'; import { CacheableObject } from '../../cache/object-cache.reducer'; +import { ResourceType } from '../../shared/resource-type'; +/** + * Object that represents the authenticated status of a user + */ export class AuthStatus implements CacheableObject { + static type = new ResourceType('status'); + /** + * The unique identifier of this auth status + */ id: string; + /** + * The unique uuid of this auth status + */ + uuid: string; + + /** + * True if REST API is up and running, should never return false + */ okay: boolean; + /** + * If the auth status represents an authenticated state + */ authenticated: boolean; + /** + * Authentication error if there was one for this status + */ error?: AuthError; + /** + * The eperson of this auth status + */ eperson: Observable>; + /** + * True if the token is valid, false if there was no token or the token wasn't valid + */ token?: AuthTokenInfo; + /** + * The self link of this auth status' REST object + */ self: string; } diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts index a13a996604..3892bee408 100644 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ b/src/app/core/auth/models/normalized-auth-status.model.ts @@ -1,16 +1,22 @@ import { AuthStatus } from './auth-status.model'; import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; import { NormalizedObject } from '../../cache/models/normalized-object.model'; import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { EPerson } from '../../eperson/models/eperson.model'; @mapsTo(AuthStatus) @inheritSerialization(NormalizedObject) export class NormalizedAuthStatus extends NormalizedObject { + /** + * The unique identifier of this auth status + */ @autoserialize id: string; + /** + * The unique generated uuid of this auth status + */ @autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id') uuid: string; @@ -26,7 +32,10 @@ export class NormalizedAuthStatus extends NormalizedObject { @autoserialize authenticated: boolean; - @relationship(ResourceType.EPerson, false) + /** + * The self link to the eperson of this auth status + */ + @relationship(EPerson, false) @autoserialize eperson: string; } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index c344683e38..cf4d4a658e 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,12 +1,12 @@ -import { map, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpHeaders } from '@angular/common/http'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { AuthService } from './auth.service'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { AuthService, LOGIN_ROUTE } from './auth.service'; import { AuthTokenInfo } from './models/auth-token-info.model'; import { CheckAuthenticationTokenAction } from './auth.actions'; import { EPerson } from '../eperson/models/eperson.model'; @@ -54,7 +54,7 @@ export class ServerAuthService extends AuthService { /** * Redirect to the route navigated before the login */ - public redirectToPreviousUrl() { + public redirectAfterLoginSuccess(isStandalonePage: boolean) { this.getRedirectUrl().pipe( take(1)) .subscribe((redirectUrl) => { @@ -67,10 +67,15 @@ export class ServerAuthService extends AuthService { const url = decodeURIComponent(redirectUrl); this.router.navigateByUrl(url); } else { - this.router.navigate(['/']); + // If redirectUrl is empty use history. For ssr the history array should contain the requested url. + this.routeService.getHistory().pipe( + filter((history) => history.length > 0), + take(1) + ).subscribe((history) => { + this.navigateToRedirectUrl(history[history.length - 1] || ''); + }); } }) - } } diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index bf368e37ce..eb494d7bdb 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; import { Observable, of as observableOf } from 'rxjs'; -import { distinctUntilChanged, map, startWith, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { - ensureArrayHasValue, hasValue, + ensureArrayHasValue, + hasValue, hasValueOperator, isEmpty, isNotEmpty, @@ -23,7 +24,9 @@ import { BrowseEntry } from '../shared/browse-entry.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { configureRequest, - filterSuccessfulResponses, getBrowseDefinitionLinks, getFirstOccurrence, + filterSuccessfulResponses, + getBrowseDefinitionLinks, + getFirstOccurrence, getRemoteDataPayload, getRequestFromRequestHref } from '../shared/operators'; @@ -32,7 +35,6 @@ import { Item } from '../shared/item.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; import { GenericSuccessResponse } from '../cache/response.models'; -import { RequestEntry } from '../data/request.reducer'; /** * The service handling all browse requests diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 937fb37cd3..0bfb5f0321 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -1,23 +1,59 @@ import 'reflect-metadata'; import { GenericConstructor } from '../../shared/generic-constructor'; -import { CacheableObject } from '../object-cache.reducer'; +import { CacheableObject, TypedObject } from '../object-cache.reducer'; import { ResourceType } from '../../shared/resource-type'; const mapsToMetadataKey = Symbol('mapsTo'); const relationshipKey = Symbol('relationship'); const relationshipMap = new Map(); +const typeMap = new Map(); -export function mapsTo(value: GenericConstructor) { - return Reflect.metadata(mapsToMetadataKey, value); +/** + * Decorator function to map a normalized class to it's not-normalized counter part class + * It will also maps a type to the matching class + * @param value The not-normalized class to map to + */ +export function mapsTo(value: GenericConstructor) { + return function decorator(objectConstructor: GenericConstructor) { + Reflect.defineMetadata(mapsToMetadataKey, value, objectConstructor); + mapsToType((value as any).type, objectConstructor); + } } +/** + * Maps a type to the matching class + * @param value The resourse type + * @param objectConstructor The class to map to + */ +function mapsToType(value: ResourceType, objectConstructor: GenericConstructor) { + if (!objectConstructor || !value) { + return; + } + typeMap.set(value.value, objectConstructor); +} + +/** + * Returns the mapped class for the given normalized class + * @param target The normalized class + */ export function getMapsTo(target: any) { return Reflect.getOwnMetadata(mapsToMetadataKey, target); } -export function relationship(value: ResourceType, isList: boolean = false): any { +/** + * Returns the mapped class for the given type + * @param type The resource type + */ +export function getMapsToType(type: string | ResourceType) { + if (typeof(type) === 'object') { + type = (type as ResourceType).value; + } + return typeMap.get(type); +} + +export function relationship(value: GenericConstructor, isList: boolean = false): any { return function r(target: any, propertyKey: string, descriptor: PropertyDescriptor) { if (!target || !propertyKey) { return; @@ -28,8 +64,10 @@ export function relationship(value: ResourceType, isList: boolean = false): any metaDataList.push(propertyKey); } relationshipMap.set(target.constructor, metaDataList); - - return Reflect.metadata(relationshipKey, { resourceType: value, isList }).apply(this, arguments); + return Reflect.metadata(relationshipKey, { + resourceType: (value as any).type.value, + isList + }).apply(this, arguments); }; } diff --git a/src/app/core/cache/builders/normalized-object-build.service.ts b/src/app/core/cache/builders/normalized-object-build.service.ts index 79665fec3d..936548cdd4 100644 --- a/src/app/core/cache/builders/normalized-object-build.service.ts +++ b/src/app/core/cache/builders/normalized-object-build.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@angular/core'; import { NormalizedObject } from '../models/normalized-object.model'; -import { CacheableObject } from '../object-cache.reducer'; -import { getRelationships } from './build-decorators'; -import { NormalizedObjectFactory } from '../models/normalized-object-factory'; +import { getMapsToType, getRelationships } from './build-decorators'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { TypedObject } from '../object-cache.reducer'; /** * Return true if halObj has a value for `_links.self` @@ -35,8 +34,8 @@ export class NormalizedObjectBuildService { * * @param {TDomain} domainModel a domain model */ - normalize(domainModel: T): NormalizedObject { - const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type); + normalize(domainModel: T): NormalizedObject { + const normalizedConstructor = getMapsToType((domainModel as any).type); const relationships = getRelationships(normalizedConstructor) || []; const normalizedModel = Object.assign({}, domainModel) as any; diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index 272969050d..2f0e024521 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -4,6 +4,7 @@ import { PaginatedList } from '../../data/paginated-list'; import { PageInfo } from '../../shared/page-info.model'; import { RemoteData } from '../../data/remote-data'; import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils'; const pageInfo = new PageInfo(); const array = [ @@ -29,8 +30,8 @@ const array = [ }) ]; const paginatedList = new PaginatedList(pageInfo, array); -const arrayRD = new RemoteData(false, false, true, undefined, array); -const paginatedListRD = new RemoteData(false, false, true, undefined, paginatedList); +const arrayRD = createSuccessfulRemoteDataObject(array); +const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); describe('RemoteDataBuildService', () => { let service: RemoteDataBuildService; diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 563dce23d1..85ba7636b6 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -21,7 +21,8 @@ import { getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators'; -import { CacheableObject } from '../object-cache.reducer'; +import { CacheableObject, TypedObject } from '../object-cache.reducer'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; @Injectable() export class RemoteDataBuildService { @@ -200,7 +201,7 @@ export class RemoteDataBuildService { aggregate(input: Array>>): Observable> { if (isEmpty(input)) { - return observableOf(new RemoteData(false, false, true, null, [])); + return createSuccessfulRemoteDataObject$([]); } return observableCombineLatest(...input).pipe( diff --git a/src/app/core/cache/models/items/normalized-item-type.model.ts b/src/app/core/cache/models/items/normalized-item-type.model.ts index ed38d80a4b..fdb3b9e455 100644 --- a/src/app/core/cache/models/items/normalized-item-type.model.ts +++ b/src/app/core/cache/models/items/normalized-item-type.model.ts @@ -1,6 +1,5 @@ import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { ItemType } from '../../../shared/item-relationships/item-type.model'; -import { ResourceType } from '../../../shared/resource-type'; import { mapsTo } from '../../builders/build-decorators'; import { NormalizedObject } from '../normalized-object.model'; import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; @@ -11,7 +10,6 @@ import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; @mapsTo(ItemType) @inheritSerialization(NormalizedObject) export class NormalizedItemType extends NormalizedObject { - /** * The label that describes the ResourceType of the Item */ @@ -27,6 +25,6 @@ export class NormalizedItemType extends NormalizedObject { /** * The universally unique identifier of this ItemType */ - @autoserializeAs(new IDToUUIDSerializer(ResourceType.ItemType), 'id') + @autoserializeAs(new IDToUUIDSerializer(ItemType.type.value), 'id') uuid: string; } diff --git a/src/app/core/cache/models/items/normalized-relationship-type.model.ts b/src/app/core/cache/models/items/normalized-relationship-type.model.ts index d201fb2746..23c3333a9b 100644 --- a/src/app/core/cache/models/items/normalized-relationship-type.model.ts +++ b/src/app/core/cache/models/items/normalized-relationship-type.model.ts @@ -5,6 +5,7 @@ import { mapsTo, relationship } from '../../builders/build-decorators'; import { NormalizedDSpaceObject } from '../normalized-dspace-object.model'; import { NormalizedObject } from '../normalized-object.model'; import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; +import { ItemType } from '../../../shared/item-relationships/item-type.model'; /** * Normalized model class for a DSpace RelationshipType @@ -12,7 +13,6 @@ import { IDToUUIDSerializer } from '../../id-to-uuid-serializer'; @mapsTo(RelationshipType) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedRelationshipType extends NormalizedObject { - /** * The identifier of this RelationshipType */ @@ -23,7 +23,7 @@ export class NormalizedRelationshipType extends NormalizedObject { * The item to the left of this relationship */ @autoserialize - @relationship(ResourceType.Item, false) + @relationship(Item, false) leftItem: string; /** * The item to the right of this relationship */ @autoserialize - @relationship(ResourceType.Item, false) + @relationship(Item, false) rightItem: string; /** @@ -48,12 +49,12 @@ export class NormalizedRelationship extends NormalizedObject { * The type of Relationship */ @autoserialize - @relationship(ResourceType.RelationshipType, false) + @relationship(RelationshipType, false) relationshipType: string; /** * The universally unique identifier of this Relationship */ - @autoserializeAs(new IDToUUIDSerializer(ResourceType.Relationship), 'id') + @autoserializeAs(new IDToUUIDSerializer(Relationship.type.value), 'id') uuid: string; } diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts index 994792d535..2283ecb368 100644 --- a/src/app/core/cache/models/normalized-bitstream-format.model.ts +++ b/src/app/core/cache/models/normalized-bitstream-format.model.ts @@ -4,7 +4,7 @@ import { BitstreamFormat } from '../../shared/bitstream-format.model'; import { mapsTo } from '../builders/build-decorators'; import { IDToUUIDSerializer } from '../id-to-uuid-serializer'; import { NormalizedObject } from './normalized-object.model'; -import { SupportLevel } from './support-level.model'; +import { BitstreamFormatSupportLevel } from '../../shared/bitstream-format-support-level'; /** * Normalized model class for a Bitstream Format @@ -12,7 +12,6 @@ import { SupportLevel } from './support-level.model'; @mapsTo(BitstreamFormat) @inheritSerialization(NormalizedObject) export class NormalizedBitstreamFormat extends NormalizedObject { - /** * Short description of this Bitstream Format */ @@ -35,7 +34,7 @@ export class NormalizedBitstreamFormat extends NormalizedObject * The level of support the system offers for this Bitstream Format */ @autoserialize - supportLevel: SupportLevel; + supportLevel: BitstreamFormatSupportLevel; /** * True if the Bitstream Format is used to store system information, rather than the content of items in the system @@ -47,7 +46,7 @@ export class NormalizedBitstreamFormat extends NormalizedObject * String representing this Bitstream Format's file extension */ @autoserialize - extensions: string; + extensions: string[]; /** * Identifier for this Bitstream Format diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index 64a17aae84..a9e389fd41 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -1,9 +1,10 @@ -import { inheritSerialization, autoserialize } from 'cerialize'; +import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Bitstream } from '../../shared/bitstream.model'; import { mapsTo, relationship } from '../builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; +import { Item } from '../../shared/item.model'; +import { BitstreamFormat } from '../../shared/bitstream-format.model'; /** * Normalized model class for a DSpace Bitstream @@ -11,7 +12,6 @@ import { ResourceType } from '../../shared/resource-type'; @mapsTo(Bitstream) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedBitstream extends NormalizedDSpaceObject { - /** * The size of this bitstream in bytes */ @@ -28,7 +28,7 @@ export class NormalizedBitstream extends NormalizedDSpaceObject { * The format of this Bitstream */ @autoserialize - @relationship(ResourceType.BitstreamFormat, false) + @relationship(BitstreamFormat, false) format: string; /** @@ -41,14 +41,14 @@ export class NormalizedBitstream extends NormalizedDSpaceObject { * An array of Bundles that are direct parents of this Bitstream */ @autoserialize - @relationship(ResourceType.Item, true) + @relationship(Item, true) parents: string[]; /** * The Bundle that owns this Bitstream */ @autoserialize - @relationship(ResourceType.Item, false) + @relationship(Item, false) owner: string; /** diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts index 342b13629f..3f4e28bca5 100644 --- a/src/app/core/cache/models/normalized-bundle.model.ts +++ b/src/app/core/cache/models/normalized-bundle.model.ts @@ -3,7 +3,7 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Bundle } from '../../shared/bundle.model'; import { mapsTo, relationship } from '../builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; +import { Bitstream } from '../../shared/bitstream.model'; /** * Normalized model class for a DSpace Bundle @@ -15,7 +15,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject { * The primary bitstream of this Bundle */ @autoserialize - @relationship(ResourceType.Bitstream, false) + @relationship(Bitstream, false) primaryBitstream: string; /** @@ -32,7 +32,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject { * List of Bitstreams that are part of this Bundle */ @autoserialize - @relationship(ResourceType.Bitstream, true) + @relationship(Bitstream, true) bitstreams: string[]; } diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts index ddfcc29a2c..9b3419675a 100644 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -3,7 +3,15 @@ import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Collection } from '../../shared/collection.model'; import { mapsTo, relationship } from '../builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; +import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; +import { NormalizedBitstream } from './normalized-bitstream.model'; +import { NormalizedCommunity } from './normalized-community.model'; +import { NormalizedItem } from './normalized-item.model'; +import { License } from '../../shared/license.model'; +import { ResourcePolicy } from '../../shared/resource-policy.model'; +import { Bitstream } from '../../shared/bitstream.model'; +import { Community } from '../../shared/community.model'; +import { Item } from '../../shared/item.model'; /** * Normalized model class for a DSpace Collection @@ -22,42 +30,42 @@ export class NormalizedCollection extends NormalizedDSpaceObject { * The Bitstream that represents the license of this Collection */ @autoserialize - @relationship(ResourceType.License, false) + @relationship(License, false) license: string; /** * The Bitstream that represents the default Access Conditions of this Collection */ @autoserialize - @relationship(ResourceType.ResourcePolicy, false) + @relationship(ResourcePolicy, false) defaultAccessConditions: string; /** * The Bitstream that represents the logo of this Collection */ @deserialize - @relationship(ResourceType.Bitstream, false) + @relationship(Bitstream, false) logo: string; /** * An array of Communities that are direct parents of this Collection */ @deserialize - @relationship(ResourceType.Community, true) + @relationship(Community, true) parents: string[]; /** * The Community that owns this Collection */ @deserialize - @relationship(ResourceType.Community, false) + @relationship(Community, false) owner: string; /** * List of Items that are part of (not necessarily owned by) this Collection */ @deserialize - @relationship(ResourceType.Item, true) + @relationship(Item, true) items: string[]; } diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts index f561089949..173760ca72 100644 --- a/src/app/core/cache/models/normalized-community.model.ts +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -1,9 +1,13 @@ -import { autoserialize, deserialize, inheritSerialization, serialize } from 'cerialize'; +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Community } from '../../shared/community.model'; import { mapsTo, relationship } from '../builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +import { NormalizedBitstream } from './normalized-bitstream.model'; +import { NormalizedCollection } from './normalized-collection.model'; +import { Bitstream } from '../../shared/bitstream.model'; +import { Collection } from '../../shared/collection.model'; /** * Normalized model class for a DSpace Community @@ -11,7 +15,6 @@ import { ResourceType } from '../../shared/resource-type'; @mapsTo(Community) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedCommunity extends NormalizedDSpaceObject { - /** * A string representing the unique handle of this Community */ @@ -22,32 +25,32 @@ export class NormalizedCommunity extends NormalizedDSpaceObject { * The Bitstream that represents the logo of this Community */ @deserialize - @relationship(ResourceType.Bitstream, false) + @relationship(Bitstream, false) logo: string; /** * An array of Communities that are direct parents of this Community */ @deserialize - @relationship(ResourceType.Community, true) + @relationship(Community, true) parents: string[]; /** * The Community that owns this Community */ @deserialize - @relationship(ResourceType.Community, false) + @relationship(Community, false) owner: string; /** * List of Collections that are owned by this Community */ @deserialize - @relationship(ResourceType.Collection, true) + @relationship(Collection, true) collections: string[]; @deserialize - @relationship(ResourceType.Community, true) + @relationship(Community, true) subcommunities: string[]; } diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index e12faa4a77..3c43dd85dc 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,15 +1,15 @@ -import { autoserializeAs, deserializeAs } from 'cerialize'; +import { autoserializeAs, deserializeAs, autoserialize } from 'cerialize'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; -import { ResourceType } from '../../shared/resource-type'; import { mapsTo } from '../builders/build-decorators'; import { NormalizedObject } from './normalized-object.model'; +import { TypedObject } from '../object-cache.reducer'; /** * An model class for a DSpaceObject. */ @mapsTo(DSpaceObject) -export class NormalizedDSpaceObject extends NormalizedObject { +export class NormalizedDSpaceObject extends NormalizedObject implements TypedObject { /** * The link to the rest endpoint where this object can be found @@ -38,8 +38,8 @@ export class NormalizedDSpaceObject extends NormalizedOb /** * A string representing the kind of DSpaceObject, e.g. community, item, … */ - @autoserializeAs(String) - type: ResourceType; + @autoserialize + type: string; /** * All metadata of this DSpaceObject diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts index d2b7b9c92d..4afceb7612 100644 --- a/src/app/core/cache/models/normalized-item.model.ts +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -4,6 +4,12 @@ import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Item } from '../../shared/item.model'; import { mapsTo, relationship } from '../builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +import { NormalizedCollection } from './normalized-collection.model'; +import { NormalizedBitstream } from './normalized-bitstream.model'; +import { NormalizedRelationship } from './items/normalized-relationship.model'; +import { Collection } from '../../shared/collection.model'; +import { Bitstream } from '../../shared/bitstream.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; /** * Normalized model class for a DSpace Item @@ -46,25 +52,25 @@ export class NormalizedItem extends NormalizedDSpaceObject { * An array of Collections that are direct parents of this Item */ @deserialize - @relationship(ResourceType.Collection, true) + @relationship(Collection, true) parents: string[]; /** * The Collection that owns this Item */ @deserialize - @relationship(ResourceType.Collection, false) + @relationship(Collection, false) owningCollection: string; /** * List of Bitstreams that are owned by this Item */ @deserialize - @relationship(ResourceType.Bitstream, true) + @relationship(Bitstream, true) bitstreams: string[]; @autoserialize - @relationship(ResourceType.Relationship, true) + @relationship(Relationship, true) relationships: string[]; } diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts deleted file mode 100644 index aa1f6f2958..0000000000 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { NormalizedItemType } from './items/normalized-item-type.model'; -import { NormalizedRelationshipType } from './items/normalized-relationship-type.model'; -import { NormalizedRelationship } from './items/normalized-relationship.model'; -import { NormalizedBitstream } from './normalized-bitstream.model'; -import { NormalizedBundle } from './normalized-bundle.model'; -import { NormalizedItem } from './normalized-item.model'; -import { NormalizedCollection } from './normalized-collection.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { NormalizedCommunity } from './normalized-community.model'; -import { ResourceType } from '../../shared/resource-type'; -import { NormalizedObject } from './normalized-object.model'; -import { NormalizedLicense } from './normalized-license.model'; -import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; -import { NormalizedWorkspaceItem } from '../../submission/models/normalized-workspaceitem.model'; -import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model'; -import { NormalizedGroup } from '../../eperson/models/normalized-group.model'; -import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model'; -import { NormalizedClaimedTask } from '../../tasks/models/normalized-claimed-task-object.model'; -import { NormalizedPoolTask } from '../../tasks/models/normalized-pool-task-object.model'; -import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; -import { NormalizedMetadataSchema } from '../../metadata/normalized-metadata-schema.model'; -import { CacheableObject } from '../object-cache.reducer'; -import { NormalizedSubmissionDefinitionsModel } from '../../config/models/normalized-config-submission-definitions.model'; -import { NormalizedSubmissionFormsModel } from '../../config/models/normalized-config-submission-forms.model'; -import { NormalizedSubmissionSectionModel } from '../../config/models/normalized-config-submission-section.model'; - -export class NormalizedObjectFactory { - public static getConstructor(type: ResourceType): GenericConstructor> { - switch (type) { - case ResourceType.Bitstream: { - return NormalizedBitstream - } - case ResourceType.Bundle: { - return NormalizedBundle - } - case ResourceType.Item: { - return NormalizedItem - } - case ResourceType.Collection: { - return NormalizedCollection - } - case ResourceType.Community: { - return NormalizedCommunity - } - case ResourceType.BitstreamFormat: { - return NormalizedBitstreamFormat - } - case ResourceType.License: { - return NormalizedLicense - } - case ResourceType.ResourcePolicy: { - return NormalizedResourcePolicy - } - case ResourceType.Relationship: { - return NormalizedRelationship - } - case ResourceType.RelationshipType: { - return NormalizedRelationshipType - } - case ResourceType.ItemType: { - return NormalizedItemType - } - case ResourceType.EPerson: { - return NormalizedEPerson - } - case ResourceType.Group: { - return NormalizedGroup - } - case ResourceType.MetadataSchema: { - return NormalizedMetadataSchema - } - case ResourceType.MetadataField: { - return NormalizedGroup - } - case ResourceType.Workspaceitem: { - return NormalizedWorkspaceItem - } - case ResourceType.Workflowitem: { - return NormalizedWorkflowItem - } - case ResourceType.ClaimedTask: { - return NormalizedClaimedTask - } - case ResourceType.PoolTask: { - return NormalizedPoolTask - } - case ResourceType.SubmissionDefinition: - case ResourceType.SubmissionDefinitions: { - return NormalizedSubmissionDefinitionsModel - } - case ResourceType.SubmissionForm: - case ResourceType.SubmissionForms: { - return NormalizedSubmissionFormsModel - } - case ResourceType.SubmissionSection: - case ResourceType.SubmissionSections: { - return NormalizedSubmissionSectionModel - } - default: { - return undefined; - } - } - } -} diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts index 6ac8985d64..bb5f192a7a 100644 --- a/src/app/core/cache/models/normalized-object.model.ts +++ b/src/app/core/cache/models/normalized-object.model.ts @@ -1,25 +1,24 @@ -import { CacheableObject } from '../object-cache.reducer'; +import { CacheableObject, TypedObject } from '../object-cache.reducer'; import { autoserialize } from 'cerialize'; import { ResourceType } from '../../shared/resource-type'; /** * An abstract model class for a NormalizedObject. */ -export abstract class NormalizedObject implements CacheableObject { - +export abstract class NormalizedObject implements CacheableObject { /** * The link to the rest endpoint where this object can be found */ @autoserialize self: string; - /** - * A string representing the kind of DSpaceObject, e.g. community, item, … - */ - @autoserialize - type: ResourceType; - @autoserialize _links: { [name: string]: string - } + }; + + /** + * A string representing the kind of object + */ + @autoserialize + type: string; } diff --git a/src/app/core/cache/models/normalized-resource-policy.model.ts b/src/app/core/cache/models/normalized-resource-policy.model.ts index 9438c1da0a..cd25a0af05 100644 --- a/src/app/core/cache/models/normalized-resource-policy.model.ts +++ b/src/app/core/cache/models/normalized-resource-policy.model.ts @@ -12,7 +12,6 @@ import { ActionType } from './action-type.model'; @mapsTo(ResourcePolicy) @inheritSerialization(NormalizedObject) export class NormalizedResourcePolicy extends NormalizedObject { - /** * The action that is allowed by this Resource Policy */ diff --git a/src/app/core/cache/models/support-level.model.ts b/src/app/core/cache/models/support-level.model.ts index 30f759d55f..103ff2c626 100644 --- a/src/app/core/cache/models/support-level.model.ts +++ b/src/app/core/cache/models/support-level.model.ts @@ -8,7 +8,7 @@ export enum SupportLevel { Unknown = 0, /** - * Unknown for Bitstream Formats that are known to the system, but not fully supported + * Known for Bitstream Formats that are known to the system, but not fully supported */ Known = 1, diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index efa28d7249..a65e63ab86 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -9,6 +9,7 @@ import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; import { Operation } from 'fast-json-patch'; +import { Item } from '../shared/item.model'; class NullAction extends RemoveFromObjectCacheAction { type = null; @@ -28,6 +29,7 @@ describe('objectCacheReducer', () => { const testState = { [selfLink1]: { data: { + type: Item.type, self: selfLink1, foo: 'bar' }, @@ -39,6 +41,7 @@ describe('objectCacheReducer', () => { }, [selfLink2]: { data: { + type: Item.type, self: requestUUID2, foo: 'baz' }, @@ -67,7 +70,7 @@ describe('objectCacheReducer', () => { it('should add the payload to the cache in response to an ADD action', () => { const state = Object.create(null); - const objectToCache = { self: selfLink1 }; + const objectToCache = { self: selfLink1, type: Item.type }; const timeAdded = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; @@ -80,7 +83,12 @@ describe('objectCacheReducer', () => { }); it('should overwrite an object in the cache in response to an ADD action if it already exists', () => { - const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true }; + const objectToCache = { + self: selfLink1, + foo: 'baz', + somethingElse: true, + type: Item.type + }; const timeAdded = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; @@ -95,7 +103,7 @@ describe('objectCacheReducer', () => { it('should perform the ADD action without affecting the previous state', () => { const state = Object.create(null); - const objectToCache = { self: selfLink1 }; + const objectToCache = { self: selfLink1, type: Item.type }; const timeAdded = new Date().getTime(); const msToLive = 900000; const requestUUID = requestUUID1; diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 982c77341e..f41151fd90 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -32,15 +32,19 @@ export interface Patch { operations: Operation[]; } +export abstract class TypedObject { + static type: ResourceType; +} + +/* tslint:disable:max-classes-per-file */ /** * An interface to represent objects that can be cached * * A cacheable object should have a self link */ -export interface CacheableObject { +export class CacheableObject extends TypedObject { uuid?: string; self: string; - type?: ResourceType; // isNew: boolean; // dirtyType: DirtyType; // hasDirtyAttributes: boolean; @@ -59,6 +63,7 @@ export class ObjectCacheEntry implements CacheEntry { patches: Patch[] = []; isDirty: boolean; } +/* tslint:enable:max-classes-per-file */ /** * The ObjectCache State diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 20e12108ad..39dc10de2c 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -10,13 +10,13 @@ import { RemoveFromObjectCacheAction } from './object-cache.actions'; import { CoreState } from '../core.reducers'; -import { ResourceType } from '../shared/resource-type'; import { NormalizedItem } from './models/normalized-item.model'; import { first } from 'rxjs/operators'; import { Operation } from 'fast-json-patch'; import { RestRequestMethod } from '../data/rest-request-method'; import { AddToSSBAction } from './server-sync-buffer.actions'; import { Patch } from './object-cache.reducer'; +import { Item } from '../shared/item.model'; describe('ObjectCacheService', () => { let service: ObjectCacheService; @@ -28,7 +28,7 @@ describe('ObjectCacheService', () => { const msToLive = 900000; let objectToCache = { self: selfLink, - type: ResourceType.Item + type: Item.type }; let cacheEntry; let invalidCacheEntry; @@ -37,7 +37,7 @@ describe('ObjectCacheService', () => { function init() { objectToCache = { self: selfLink, - type: ResourceType.Item + type: Item.type }; cacheEntry = { data: objectToCache, diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index e6384571c3..11f3a6ce3e 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -4,13 +4,12 @@ import { applyPatch, Operation } from 'fast-json-patch'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; -import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; import { RestRequestMethod } from '../data/rest-request-method'; import { selfLinkFromUuidSelector } from '../index/index.selectors'; import { GenericConstructor } from '../shared/generic-constructor'; -import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; import { AddPatchObjectCacheAction, @@ -21,6 +20,7 @@ import { import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { AddToSSBAction } from './server-sync-buffer.actions'; +import { getMapsToType } from './builders/build-decorators'; /** * The base selector function to select the object cache in the store @@ -68,8 +68,8 @@ export class ObjectCacheService { * @param href * The unique href of the object to be removed */ - remove(uuid: string): void { - this.store.dispatch(new RemoveFromObjectCacheAction(uuid)); + remove(href: string): void { + this.store.dispatch(new RemoveFromObjectCacheAction(href)); } /** @@ -109,7 +109,7 @@ export class ObjectCacheService { } ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor> = NormalizedObjectFactory.getConstructor(entry.data.type); + const type: GenericConstructor> = getMapsToType((entry.data as any).type); return Object.assign(new type(), entry.data) as NormalizedObject }) ); @@ -224,6 +224,18 @@ export class ObjectCacheService { return result; } + /** + * Create an observable that emits a new value whenever the availability of the cached object changes. + * The value it emits is a boolean stating if the object exists in cache or not. + * @param selfLink The self link of the object to observe + */ + hasBySelfLinkObservable(selfLink: string): Observable { + return this.store.pipe( + select(entryFromSelfLinkSelector(selfLink)), + map((entry: ObjectCacheEntry) => this.isValid(entry)) + ); + } + /** * Check whether an ObjectCacheEntry should still be cached * diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index b3523addc5..03233e616b 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -8,12 +8,12 @@ import { IntegrationModel } from '../integration/models/integration.model'; import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; -import { MetadataSchema } from '../metadata/metadataschema.model'; -import { MetadataField } from '../metadata/metadatafield.model'; import { PaginatedList } from '../data/paginated-list'; import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { MetadataField } from '../metadata/metadata-field.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { diff --git a/src/app/core/config/config-response-parsing.service.spec.ts b/src/app/core/config/config-response-parsing.service.spec.ts index 7c69f1bdb3..90dd1670b8 100644 --- a/src/app/core/config/config-response-parsing.service.spec.ts +++ b/src/app/core/config/config-response-parsing.service.spec.ts @@ -8,8 +8,8 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { PaginatedList } from '../data/paginated-list'; import { PageInfo } from '../shared/page-info.model'; -import { NormalizedSubmissionDefinitionsModel } from './models/normalized-config-submission-definitions.model'; import { NormalizedSubmissionSectionModel } from './models/normalized-config-submission-section.model'; +import { NormalizedSubmissionDefinitionModel } from './models/normalized-config-submission-definition.model'; describe('ConfigResponseParsingService', () => { let service: ConfigResponseParsingService; @@ -173,7 +173,7 @@ describe('ConfigResponseParsingService', () => { self: 'https://rest.api/config/submissiondefinitions/traditional/sections' }); const definitions = - Object.assign(new NormalizedSubmissionDefinitionsModel(), { + Object.assign(new NormalizedSubmissionDefinitionModel(), { isDefault: true, name: 'traditional', type: 'submissiondefinition', diff --git a/src/app/core/config/config-response-parsing.service.ts b/src/app/core/config/config-response-parsing.service.ts index b81dc07624..08fe581406 100644 --- a/src/app/core/config/config-response-parsing.service.ts +++ b/src/app/core/config/config-response-parsing.service.ts @@ -5,10 +5,8 @@ import { RestRequest } from '../data/request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; -import { ConfigObjectFactory } from './models/config-object-factory'; import { ConfigObject } from './models/config.model'; -import { ConfigType } from './models/config-type'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; @@ -16,8 +14,6 @@ import { ObjectCacheService } from '../cache/object-cache.service'; @Injectable() export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected objectFactory = ConfigObjectFactory; protected toCache = false; constructor( @@ -28,7 +24,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === 201 || data.statusCode === 200)) { - const configDefinition = this.process(data.payload, request.uuid); + const configDefinition = this.process(data.payload, request.uuid); return new ConfigSuccessResponse(configDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload)); } else { return new ErrorResponse( diff --git a/src/app/core/config/models/config-object-factory.ts b/src/app/core/config/models/config-object-factory.ts deleted file mode 100644 index 44b2e377c4..0000000000 --- a/src/app/core/config/models/config-object-factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { GenericConstructor } from '../../shared/generic-constructor'; -import { ConfigType } from './config-type'; -import { ConfigObject } from './config.model'; -import { NormalizedSubmissionDefinitionsModel } from './normalized-config-submission-definitions.model'; -import { NormalizedSubmissionFormsModel } from './normalized-config-submission-forms.model'; -import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model'; -import { NormalizedSubmissionUploadsModel } from './normalized-config-submission-uploads.model'; - -/** - * Class to return normalized models for config objects - */ -export class ConfigObjectFactory { - public static getConstructor(type): GenericConstructor { - switch (type) { - case ConfigType.SubmissionDefinition: - case ConfigType.SubmissionDefinitions: { - return NormalizedSubmissionDefinitionsModel - } - case ConfigType.SubmissionForm: - case ConfigType.SubmissionForms: { - return NormalizedSubmissionFormsModel - } - case ConfigType.SubmissionSection: - case ConfigType.SubmissionSections: { - return NormalizedSubmissionSectionModel - } - case ConfigType.SubmissionUpload: - case ConfigType.SubmissionUploads: { - return NormalizedSubmissionUploadsModel - } - default: { - return undefined; - } - } - } -} diff --git a/src/app/core/config/models/config-submission-definition.model.ts b/src/app/core/config/models/config-submission-definition.model.ts new file mode 100644 index 0000000000..0449e6a964 --- /dev/null +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -0,0 +1,22 @@ +import { ConfigObject } from './config.model'; +import { SubmissionSectionModel } from './config-submission-section.model'; +import { PaginatedList } from '../../data/paginated-list'; +import { ResourceType } from '../../shared/resource-type'; + +/** + * Class for the configuration describing the submission + */ +export class SubmissionDefinitionModel extends ConfigObject { + static type = new ResourceType('submissiondefinition'); + + /** + * A boolean representing if this submission definition is the default or not + */ + isDefault: boolean; + + /** + * A list of SubmissionSectionModel that are present in this submission definition + */ + sections: PaginatedList; + +} diff --git a/src/app/core/config/models/config-submission-definitions.model.ts b/src/app/core/config/models/config-submission-definitions.model.ts index 8bbbc90056..d9892f542f 100644 --- a/src/app/core/config/models/config-submission-definitions.model.ts +++ b/src/app/core/config/models/config-submission-definitions.model.ts @@ -1,17 +1,7 @@ -import { ConfigObject } from './config.model'; -import { SubmissionSectionModel } from './config-submission-section.model'; -import { PaginatedList } from '../../data/paginated-list'; +import { SubmissionDefinitionModel } from './config-submission-definition.model'; +import { ResourceType } from '../../shared/resource-type'; -export class SubmissionDefinitionsModel extends ConfigObject { - - /** - * A boolean representing if this submission definition is the default or not - */ - isDefault: boolean; - - /** - * A list of SubmissionSectionModel that are present in this submission definition - */ - sections: PaginatedList; +export class SubmissionDefinitionsModel extends SubmissionDefinitionModel { + static type = new ResourceType('submissiondefinitions'); } diff --git a/src/app/core/config/models/config-submission-form.model.ts b/src/app/core/config/models/config-submission-form.model.ts new file mode 100644 index 0000000000..a65d285c95 --- /dev/null +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -0,0 +1,22 @@ +import { ConfigObject } from './config.model'; +import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; +import { ResourceType } from '../../shared/resource-type'; + +/** + * An interface that define a form row and its properties. + */ +export interface FormRowModel { + fields: FormFieldModel[]; +} + +/** + * A model class for a NormalizedObject. + */ +export class SubmissionFormModel extends ConfigObject { + static type = new ResourceType('submissionform'); + + /** + * An array of [FormRowModel] that are present in this form + */ + rows: FormRowModel[]; +} diff --git a/src/app/core/config/models/config-submission-forms.model.ts b/src/app/core/config/models/config-submission-forms.model.ts index ee0962f0e9..017d7d68cc 100644 --- a/src/app/core/config/models/config-submission-forms.model.ts +++ b/src/app/core/config/models/config-submission-forms.model.ts @@ -1,20 +1,9 @@ -import { ConfigObject } from './config.model'; -import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; - -/** - * An interface that define a form row and its properties. - */ -export interface FormRowModel { - fields: FormFieldModel[]; -} +import { SubmissionFormModel } from './config-submission-form.model'; +import { ResourceType } from '../../shared/resource-type'; /** * A model class for a NormalizedObject. */ -export class SubmissionFormsModel extends ConfigObject { - - /** - * An array of [FormRowModel] that are present in this form - */ - rows: FormRowModel[]; +export class SubmissionFormsModel extends SubmissionFormModel { + static type = new ResourceType('submissionforms'); } diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts index 377a8869e1..4c560fa631 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -1,5 +1,6 @@ import { ConfigObject } from './config.model'; import { SectionsType } from '../../../submission/sections/sections-type'; +import { ResourceType } from '../../shared/resource-type'; /** * An interface that define section visibility and its properties. @@ -10,6 +11,7 @@ export interface SubmissionSectionVisibility { } export class SubmissionSectionModel extends ConfigObject { + static type = new ResourceType('submissionsection'); /** * The header for this section diff --git a/src/app/core/config/models/config-submission-sections.model.ts b/src/app/core/config/models/config-submission-sections.model.ts new file mode 100644 index 0000000000..ae7b133391 --- /dev/null +++ b/src/app/core/config/models/config-submission-sections.model.ts @@ -0,0 +1,6 @@ +import { SubmissionSectionModel } from './config-submission-section.model'; +import { ResourceType } from '../../shared/resource-type'; + +export class SubmissionSectionsModel extends SubmissionSectionModel { + static type = new ResourceType('submissionsections'); +} diff --git a/src/app/core/config/models/config-submission-uploads.model.ts b/src/app/core/config/models/config-submission-uploads.model.ts index 8bb9ba7f1e..812a590041 100644 --- a/src/app/core/config/models/config-submission-uploads.model.ts +++ b/src/app/core/config/models/config-submission-uploads.model.ts @@ -1,9 +1,10 @@ import { ConfigObject } from './config.model'; import { AccessConditionOption } from './config-access-condition-option.model'; import { SubmissionFormsModel } from './config-submission-forms.model'; +import { ResourceType } from '../../shared/resource-type'; export class SubmissionUploadsModel extends ConfigObject { - + static type = new ResourceType('submissionupload'); /** * A list of available bitstream access conditions */ diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index 81f20a0b3c..20d67ec69d 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -8,11 +8,6 @@ export abstract class ConfigObject implements CacheableObject { */ public name: string; - /** - * A string representing the kind of config object - */ - public type: ResourceType; - /** * The links to all related resources returned by the rest api. */ diff --git a/src/app/core/config/models/normalized-config-submission-definition.model.ts b/src/app/core/config/models/normalized-config-submission-definition.model.ts new file mode 100644 index 0000000000..cb56e01acf --- /dev/null +++ b/src/app/core/config/models/normalized-config-submission-definition.model.ts @@ -0,0 +1,28 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { SubmissionSectionModel } from './config-submission-section.model'; +import { PaginatedList } from '../../data/paginated-list'; +import { NormalizedConfigObject } from './normalized-config.model'; +import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; +import { mapsTo } from '../../cache/builders/build-decorators'; +import { SubmissionDefinitionModel } from './config-submission-definition.model'; + +/** + * Normalized class for the configuration describing the submission + */ +@mapsTo(SubmissionDefinitionModel) +@inheritSerialization(NormalizedConfigObject) +export class NormalizedSubmissionDefinitionModel extends NormalizedConfigObject { + + /** + * A boolean representing if this submission definition is the default or not + */ + @autoserialize + isDefault: boolean; + + /** + * A list of SubmissionSectionModel that are present in this submission definition + */ + @autoserializeAs(SubmissionSectionModel) + sections: PaginatedList; + +} diff --git a/src/app/core/config/models/normalized-config-submission-definitions.model.ts b/src/app/core/config/models/normalized-config-submission-definitions.model.ts index 3887c566c1..4c52d96458 100644 --- a/src/app/core/config/models/normalized-config-submission-definitions.model.ts +++ b/src/app/core/config/models/normalized-config-submission-definitions.model.ts @@ -1,25 +1,13 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { SubmissionSectionModel } from './config-submission-section.model'; -import { PaginatedList } from '../../data/paginated-list'; +import { inheritSerialization } from 'cerialize'; import { NormalizedConfigObject } from './normalized-config.model'; import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; +import { mapsTo } from '../../cache/builders/build-decorators'; +import { NormalizedSubmissionDefinitionModel } from './normalized-config-submission-definition.model'; /** * Normalized class for the configuration describing the submission */ +@mapsTo(SubmissionDefinitionsModel) @inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionDefinitionsModel extends NormalizedConfigObject { - - /** - * A boolean representing if this submission definition is the default or not - */ - @autoserialize - isDefault: boolean; - - /** - * A list of SubmissionSectionModel that are present in this submission definition - */ - @autoserializeAs(SubmissionSectionModel) - sections: PaginatedList; - +export class NormalizedSubmissionDefinitionsModel extends NormalizedSubmissionDefinitionModel { } diff --git a/src/app/core/config/models/normalized-config-submission-form.model.ts b/src/app/core/config/models/normalized-config-submission-form.model.ts new file mode 100644 index 0000000000..afdfef4818 --- /dev/null +++ b/src/app/core/config/models/normalized-config-submission-form.model.ts @@ -0,0 +1,18 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { NormalizedConfigObject } from './normalized-config.model'; +import { mapsTo } from '../../cache/builders/build-decorators'; +import { FormRowModel, SubmissionFormModel } from './config-submission-form.model'; + +/** + * Normalized class for the configuration describing the submission form + */ +@mapsTo(SubmissionFormModel) +@inheritSerialization(NormalizedConfigObject) +export class NormalizedSubmissionFormModel extends NormalizedConfigObject { + + /** + * An array of [FormRowModel] that are present in this form + */ + @autoserialize + rows: FormRowModel[]; +} diff --git a/src/app/core/config/models/normalized-config-submission-forms.model.ts b/src/app/core/config/models/normalized-config-submission-forms.model.ts index a957e8c7fa..c040a94587 100644 --- a/src/app/core/config/models/normalized-config-submission-forms.model.ts +++ b/src/app/core/config/models/normalized-config-submission-forms.model.ts @@ -1,16 +1,12 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; -import { NormalizedConfigObject } from './normalized-config.model'; -import { FormRowModel, SubmissionFormsModel } from './config-submission-forms.model'; +import { inheritSerialization } from 'cerialize'; +import { mapsTo } from '../../cache/builders/build-decorators'; +import { SubmissionFormsModel } from './config-submission-forms.model'; +import { NormalizedSubmissionFormModel } from './normalized-config-submission-form.model'; /** * Normalized class for the configuration describing the submission form */ -@inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionFormsModel extends NormalizedConfigObject { - - /** - * An array of [FormRowModel] that are present in this form - */ - @autoserialize - rows: FormRowModel[]; +@mapsTo(SubmissionFormsModel) +@inheritSerialization(NormalizedSubmissionFormModel) +export class NormalizedSubmissionFormsModel extends NormalizedSubmissionFormModel { } diff --git a/src/app/core/config/models/normalized-config-submission-section.model.ts b/src/app/core/config/models/normalized-config-submission-section.model.ts index c876acf607..364a981060 100644 --- a/src/app/core/config/models/normalized-config-submission-section.model.ts +++ b/src/app/core/config/models/normalized-config-submission-section.model.ts @@ -1,14 +1,18 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { SectionsType } from '../../../submission/sections/sections-type'; import { NormalizedConfigObject } from './normalized-config.model'; -import { SubmissionFormsModel } from './config-submission-forms.model'; -import { SubmissionSectionVisibility } from './config-submission-section.model'; +import { + SubmissionSectionModel, + SubmissionSectionVisibility +} from './config-submission-section.model'; +import { mapsTo } from '../../cache/builders/build-decorators'; /** * Normalized class for the configuration describing the submission section */ +@mapsTo(SubmissionSectionModel) @inheritSerialization(NormalizedConfigObject) -export class NormalizedSubmissionSectionModel extends NormalizedConfigObject { +export class NormalizedSubmissionSectionModel extends NormalizedConfigObject { /** * The header for this section diff --git a/src/app/core/config/models/normalized-config-submission-sections.model.ts b/src/app/core/config/models/normalized-config-submission-sections.model.ts new file mode 100644 index 0000000000..fb1e4c671a --- /dev/null +++ b/src/app/core/config/models/normalized-config-submission-sections.model.ts @@ -0,0 +1,18 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { SectionsType } from '../../../submission/sections/sections-type'; +import { NormalizedConfigObject } from './normalized-config.model'; +import { + SubmissionSectionModel, + SubmissionSectionVisibility +} from './config-submission-section.model'; +import { mapsTo } from '../../cache/builders/build-decorators'; +import { SubmissionSectionsModel } from './config-submission-sections.model'; +import { NormalizedSubmissionSectionModel } from './normalized-config-submission-section.model'; + +/** + * Normalized class for the configuration describing the submission section + */ +@mapsTo(SubmissionSectionsModel) +@inheritSerialization(NormalizedSubmissionSectionModel) +export class NormalizedSubmissionSectionsModel extends NormalizedSubmissionSectionModel { +} diff --git a/src/app/core/config/models/normalized-config-submission-uploads.model.ts b/src/app/core/config/models/normalized-config-submission-uploads.model.ts index e49171d6a7..7a21c15912 100644 --- a/src/app/core/config/models/normalized-config-submission-uploads.model.ts +++ b/src/app/core/config/models/normalized-config-submission-uploads.model.ts @@ -3,10 +3,12 @@ import { AccessConditionOption } from './config-access-condition-option.model'; import { SubmissionFormsModel } from './config-submission-forms.model'; import { NormalizedConfigObject } from './normalized-config.model'; import { SubmissionUploadsModel } from './config-submission-uploads.model'; +import { mapsTo } from '../../cache/builders/build-decorators'; /** * Normalized class for the configuration describing the submission upload section */ +@mapsTo(SubmissionUploadsModel) @inheritSerialization(NormalizedConfigObject) export class NormalizedSubmissionUploadsModel extends NormalizedConfigObject { diff --git a/src/app/core/config/models/normalized-config.model.ts b/src/app/core/config/models/normalized-config.model.ts index 0b75158588..1bf4ffb826 100644 --- a/src/app/core/config/models/normalized-config.model.ts +++ b/src/app/core/config/models/normalized-config.model.ts @@ -1,6 +1,6 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedObject } from '../../cache/models/normalized-object.model'; -import { CacheableObject } from '../../cache/object-cache.reducer'; +import { CacheableObject, TypedObject } from '../../cache/object-cache.reducer'; import { ResourceType } from '../../shared/resource-type'; /** @@ -15,12 +15,6 @@ export abstract class NormalizedConfigObject implemen @autoserialize public name: string; - /** - * A string representing the kind of config object - */ - @autoserialize - public type: ResourceType; - /** * The links to all related resources returned by the rest api. */ diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 9ade23e6c5..0eabfc5dc8 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,4 +1,3 @@ - import { ObjectCacheEffects } from './cache/object-cache.effects'; import { UUIDIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; @@ -6,7 +5,7 @@ import { AuthEffects } from './auth/auth.effects'; import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects'; -import { RouteEffects } from '../shared/services/route.effects'; +import { RouteEffects } from './services/route.effects'; export const coreEffects = [ RequestEffects, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 6550435aa3..f4d4dcb269 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,21 +1,20 @@ -import { - ModuleWithProviders, - NgModule, - Optional, - SkipSelf -} from '@angular/core'; +import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; -import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { + DynamicFormLayoutService, + DynamicFormService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { coreEffects } from './core.effects'; import { coreReducers } from './core.reducers'; import { isNotEmpty } from '../shared/empty.util'; -import { ApiService } from '../shared/services/api.service'; +import { ApiService } from './services/api.service'; import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; import { CollectionDataService } from './data/collection-data.service'; import { CommunityDataService } from './data/community-data.service'; @@ -35,12 +34,12 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { RequestService } from './data/request.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; -import { ServerResponseService } from '../shared/services/server-response.service'; -import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service'; +import { ServerResponseService } from './services/server-response.service'; +import { NativeWindowFactory, NativeWindowService } from './services/window.service'; import { BrowseService } from './browse/browse.service'; import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; import { ConfigResponseParsingService } from './config/config-response-parsing.service'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from './services/route.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; @@ -60,11 +59,12 @@ import { HALEndpointService } from './shared/hal-endpoint.service'; import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service'; import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; +import { ResourcePolicyService } from './data/resource-policy.service'; import { RegistryService } from './registry/registry.service'; import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service'; import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; -import { WorkflowitemDataService } from './submission/workflowitem-data.service'; +import { WorkflowItemDataService } from './submission/workflowitem-data.service'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { UploaderService } from '../shared/uploader/uploader.service'; import { FileService } from './shared/file.service'; @@ -81,12 +81,44 @@ import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service'; import { SearchService } from '../+search-page/search-service/search.service'; +import { NormalizedCollection } from './cache/models/normalized-collection.model'; +import { NormalizedCommunity } from './cache/models/normalized-community.model'; +import { NormalizedDSpaceObject } from './cache/models/normalized-dspace-object.model'; +import { NormalizedBitstream } from './cache/models/normalized-bitstream.model'; +import { NormalizedBundle } from './cache/models/normalized-bundle.model'; +import { NormalizedBitstreamFormat } from './cache/models/normalized-bitstream-format.model'; +import { NormalizedItem } from './cache/models/normalized-item.model'; +import { NormalizedEPerson } from './eperson/models/normalized-eperson.model'; +import { NormalizedGroup } from './eperson/models/normalized-group.model'; +import { NormalizedResourcePolicy } from './cache/models/normalized-resource-policy.model'; +import { NormalizedMetadataSchema } from './metadata/normalized-metadata-schema.model'; +import { NormalizedMetadataField } from './metadata/normalized-metadata-field.model'; +import { NormalizedLicense } from './cache/models/normalized-license.model'; +import { NormalizedWorkflowItem } from './submission/models/normalized-workflowitem.model'; +import { NormalizedWorkspaceItem } from './submission/models/normalized-workspaceitem.model'; +import { NormalizedSubmissionDefinitionsModel } from './config/models/normalized-config-submission-definitions.model'; +import { NormalizedSubmissionFormsModel } from './config/models/normalized-config-submission-forms.model'; +import { NormalizedSubmissionSectionModel } from './config/models/normalized-config-submission-section.model'; +import { NormalizedAuthStatus } from './auth/models/normalized-auth-status.model'; +import { NormalizedAuthorityValue } from './integration/models/normalized-authority-value.model'; +import { RelationshipService } from './data/relationship.service'; import { RoleService } from './roles/role.service'; import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; import { PoolTaskDataService } from './tasks/pool-task-data.service'; import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; +import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; +import { NormalizedClaimedTask } from './tasks/models/normalized-claimed-task-object.model'; +import { NormalizedTaskObject } from './tasks/models/normalized-task-object.model'; +import { NormalizedPoolTask } from './tasks/models/normalized-pool-task-object.model'; +import { NormalizedRelationship } from './cache/models/items/normalized-relationship.model'; +import { NormalizedRelationshipType } from './cache/models/items/normalized-relationship-type.model'; +import { NormalizedItemType } from './cache/models/items/normalized-item-type.model'; +import { MetadatafieldParsingService } from './data/metadatafield-parsing.service'; +import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model'; +import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model'; +import { BrowseDefinition } from './shared/browse-definition.model'; const IMPORTS = [ CommonModule, @@ -94,13 +126,9 @@ const IMPORTS = [ EffectsModule.forFeature(coreEffects) ]; -const DECLARATIONS = [ +const DECLARATIONS = []; -]; - -const EXPORTS = [ - -]; +const EXPORTS = []; const PROVIDERS = [ ApiService, @@ -125,7 +153,9 @@ const PROVIDERS = [ MetadataService, ObjectCacheService, PaginationComponentOptions, + ResourcePolicyService, RegistryService, + BitstreamFormatDataService, NormalizedObjectBuildService, RemoteDataBuildService, RequestService, @@ -156,11 +186,12 @@ const PROVIDERS = [ AuthorityService, IntegrationResponseParsingService, MetadataschemaParsingService, + MetadatafieldParsingService, UploaderService, UUIDService, NotificationsService, WorkspaceitemDataService, - WorkflowitemDataService, + WorkflowItemDataService, UploaderService, FileService, DSpaceObjectDataService, @@ -170,6 +201,7 @@ const PROVIDERS = [ MenuService, ObjectUpdatesService, SearchService, + RelationshipService, MyDSpaceGuard, RoleService, TaskResponseParsingService, @@ -186,6 +218,42 @@ const PROVIDERS = [ { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; +/** + * Declaration needed to make sure all decorator functions are called in time + */ +export const normalizedModels = + [ + NormalizedDSpaceObject, + NormalizedBundle, + NormalizedBitstream, + NormalizedBitstreamFormat, + NormalizedItem, + NormalizedCollection, + NormalizedCommunity, + NormalizedEPerson, + NormalizedGroup, + NormalizedResourcePolicy, + NormalizedMetadataSchema, + NormalizedMetadataField, + NormalizedLicense, + NormalizedWorkflowItem, + NormalizedWorkspaceItem, + NormalizedSubmissionDefinitionsModel, + NormalizedSubmissionFormsModel, + NormalizedSubmissionSectionModel, + NormalizedSubmissionUploadsModel, + NormalizedAuthStatus, + NormalizedAuthorityValue, + NormalizedBrowseEntry, + BrowseDefinition, + NormalizedClaimedTask, + NormalizedTaskObject, + NormalizedPoolTask, + NormalizedRelationship, + NormalizedRelationshipType, + NormalizedItemType + ]; + @NgModule({ imports: [ ...IMPORTS @@ -200,8 +268,8 @@ const PROVIDERS = [ ...PROVIDERS ] }) -export class CoreModule { +export class CoreModule { static forRoot(): ModuleWithProviders { return { ngModule: CoreModule, @@ -211,10 +279,9 @@ export class CoreModule { }; } - constructor( @Optional() @SkipSelf() parentModule: CoreModule) { + constructor(@Optional() @SkipSelf() parentModule: CoreModule) { if (isNotEmpty(parentModule)) { throw new Error('CoreModule is already loaded. Import it in the AppModule only'); } } - } diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 7aecb91a7a..4fcf36f9cc 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -13,7 +13,7 @@ import { objectUpdatesReducer, ObjectUpdatesState } from './data/object-updates/object-updates.reducer'; -import { routeReducer, RouteState } from '../shared/services/route.reducer'; +import { routeReducer, RouteState } from './services/route.reducer'; export interface CoreState { 'cache/object': ObjectCacheState, diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 4ede02778c..0ed5dc363c 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -7,16 +7,16 @@ import { GlobalConfig } from '../../../config/global-config.interface'; import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service'; - +import { ResourceType } from '../shared/resource-type'; +import { getMapsToType } from '../cache/builders/build-decorators'; /* tslint:disable:max-classes-per-file */ export abstract class BaseResponseParsingService { protected abstract EnvConfig: GlobalConfig; protected abstract objectCache: ObjectCacheService; - protected abstract objectFactory: any; protected abstract toCache: boolean; - protected process(data: any, requestUUID: string): any { + protected process(data: any, requestUUID: string): any { if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; @@ -31,7 +31,7 @@ export abstract class BaseResponseParsingService { .keys(data._embedded) .filter((property) => data._embedded.hasOwnProperty(property)) .forEach((property) => { - const parsedObj = this.process(data._embedded[property], requestUUID); + const parsedObj = this.process(data._embedded[property], requestUUID); if (isNotEmpty(parsedObj)) { if (isRestPaginatedList(data._embedded[property])) { object[property] = parsedObj; @@ -60,7 +60,7 @@ export abstract class BaseResponseParsingService { } } - protected processPaginatedList(data: any, requestUUID: string): PaginatedList { + protected processPaginatedList(data: any, requestUUID: string): PaginatedList { const pageInfo: PageInfo = this.processPageInfo(data); let list = data._embedded; @@ -74,7 +74,7 @@ export abstract class BaseResponseParsingService { return new PaginatedList(pageInfo, page, ); } - protected processArray(data: any, requestUUID: string): ObjectDomain[] { + protected processArray(data: any, requestUUID: string): ObjectDomain[] { let array: ObjectDomain[] = []; data.forEach((datum) => { array = [...array, this.process(datum, requestUUID)]; @@ -83,10 +83,10 @@ export abstract class BaseResponseParsingService { return array; } - protected deserialize(obj): any { - const type: ObjectType = obj.type; + protected deserialize(obj): any { + const type: string = obj.type; if (hasValue(type)) { - const normObjConstructor = this.objectFactory.getConstructor(type) as GenericConstructor; + const normObjConstructor = getMapsToType(type) as GenericConstructor; if (hasValue(normObjConstructor)) { const serializer = new DSpaceRESTv2Serializer(normObjConstructor); @@ -104,7 +104,7 @@ export abstract class BaseResponseParsingService { } } - protected cache(obj, requestUUID) { + protected cache(obj, requestUUID) { if (this.toCache) { this.addToObjectCache(obj, requestUUID); } diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts new file mode 100644 index 0000000000..f3ce478236 --- /dev/null +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -0,0 +1,293 @@ +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { RequestEntry } from './request.reducer'; +import { RestResponse } from '../cache/response.models'; +import { Observable, of as observableOf } from 'rxjs'; +import { Action, Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { async } from '@angular/core/testing'; +import { + BitstreamFormatsRegistryDeselectAction, + BitstreamFormatsRegistryDeselectAllAction, + BitstreamFormatsRegistrySelectAction +} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { TestScheduler } from 'rxjs/testing'; + +describe('BitstreamFormatDataService', () => { + let service: BitstreamFormatDataService; + let requestService; + let scheduler: TestScheduler; + + const bitstreamFormatsEndpoint = 'https://rest.api/core/bitstream-formats'; + const bitstreamFormatsIdEndpoint = 'https://rest.api/core/bitstream-formats/format-id'; + + const responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + responseCacheEntry.completed = true; + + const store = { + dispatch(action: Action) { + // Do Nothing + } + } as Store; + + const objectCache = {} as ObjectCacheService; + const halEndpointService = { + getEndpoint(linkPath: string): Observable { + return cold('a', {a: bitstreamFormatsEndpoint}); + } + } as HALEndpointService; + + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; + const rdbService = {} as RemoteDataBuildService; + + function initTestService(halService) { + return new BitstreamFormatDataService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + describe('getBrowseEndpoint', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should get the browse endpoint', () => { + const result = service.getBrowseEndpoint(); + const expected = cold('b', {b: bitstreamFormatsEndpoint}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getUpdateEndpoint', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should get the update endpoint', () => { + const formatId = 'format-id'; + + const result = service.getUpdateEndpoint(formatId); + const expected = cold('b', {b: bitstreamFormatsIdEndpoint}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getCreateEndpoint', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should get the create endpoint ', () => { + + const result = service.getCreateEndpoint(); + const expected = cold('b', {b: bitstreamFormatsEndpoint}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('updateBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should update the bitstream format', () => { + const updatedBistreamFormat = new BitstreamFormat(); + updatedBistreamFormat.uuid = 'updated-uuid'; + + const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')}); + const result = service.updateBitstreamFormat(updatedBistreamFormat); + + expect(result).toBeObservable(expected); + + }); + }); + + describe('createBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should create a new bitstream format', () => { + const newFormat = new BitstreamFormat(); + newFormat.uuid = 'new-uuid'; + + const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')}); + const result = service.createBitstreamFormat(newFormat); + + expect(result).toBeObservable(expected); + }); + }); + + describe('clearBitStreamFormatRequests', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + const halService = { + getEndpoint(linkPath: string): Observable { + return observableOf(bitstreamFormatsEndpoint); + } + } as HALEndpointService; + service = initTestService(halService); + service.clearBitStreamFormatRequests().subscribe(); + })); + it('should remove the bitstream format hrefs in the request service', () => { + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(bitstreamFormatsEndpoint); + }); + }); + + describe('selectBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + spyOn(store, 'dispatch'); + })); + it('should add a selected bitstream to the store', () => { + const format = new BitstreamFormat(); + format.uuid = 'uuid'; + + service.selectBitstreamFormat(format); + expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistrySelectAction(format)); + }); + }); + + describe('deselectBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + spyOn(store, 'dispatch'); + })); + it('should remove a bitstream from the store', () => { + const format = new BitstreamFormat(); + format.uuid = 'uuid'; + + service.deselectBitstreamFormat(format); + expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistryDeselectAction(format)); + }); + }); + + describe('deselectAllBitstreamFormats', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + spyOn(store, 'dispatch'); + + })); + it('should remove all bitstreamFormats from the store', () => { + service.deselectAllBitstreamFormats(); + expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistryDeselectAllAction()); + }); + }); + + describe('delete', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: hot('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + const halService = { + getEndpoint(linkPath: string): Observable { + return observableOf(bitstreamFormatsEndpoint); + } + } as HALEndpointService; + service = initTestService(halService); + })); + it('should delete a bitstream format', () => { + const format = new BitstreamFormat(); + format.uuid = 'format-uuid'; + format.id = 'format-id'; + + const expected = cold('(b|)', {b: true}); + const result = service.delete(format); + + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts new file mode 100644 index 0000000000..a5638183c0 --- /dev/null +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -0,0 +1,183 @@ +import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { createSelector, select, Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { DeleteByIDRequest, FindAllOptions, PostRequest, PutRequest } from './request.models'; +import { Observable } from 'rxjs'; +import { find, map, tap } from 'rxjs/operators'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged'; +import { RestResponse } from '../cache/response.models'; +import { AppState } from '../../app.reducer'; +import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { + BitstreamFormatsRegistryDeselectAction, + BitstreamFormatsRegistryDeselectAllAction, + BitstreamFormatsRegistrySelectAction +} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { hasValue } from '../../shared/empty.util'; +import { RequestEntry } from './request.reducer'; + +const bitstreamFormatsStateSelector = (state: AppState) => state.bitstreamFormats; +const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector, + (bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats); + +/** + * A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint + */ +@Injectable() +export class BitstreamFormatDataService extends DataService { + + protected linkPath = 'bitstreamformats'; + protected forceBypassCache = false; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint for browsing bitstream formats + * @param {FindAllOptions} options + * @returns {Observable} + */ + getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } + + /** + * Get the endpoint to update an existing bitstream format + * @param formatId + */ + public getUpdateEndpoint(formatId: string): Observable { + return this.getBrowseEndpoint().pipe( + map((endpoint: string) => this.getIDHref(endpoint, formatId)) + ); + } + + /** + * Get the endpoint to create a new bitstream format + */ + public getCreateEndpoint(): Observable { + return this.getBrowseEndpoint(); + } + + /** + * Update an existing bitstreamFormat + * @param bitstreamFormat + */ + updateBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable { + const requestId = this.requestService.generateRequestId(); + + this.getUpdateEndpoint(bitstreamFormat.id).pipe( + distinctUntilChanged(), + map((endpointURL: string) => + new PutRequest(requestId, endpointURL, bitstreamFormat)), + configureRequest(this.requestService)).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + + } + + /** + * Create a new BitstreamFormat + * @param BitstreamFormat + */ + public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable { + const requestId = this.requestService.generateRequestId(); + + this.getCreateEndpoint().pipe( + map((endpointURL: string) => { + return new PostRequest(requestId, endpointURL, bitstreamFormat); + }), + configureRequest(this.requestService) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + } + + /** + * Clears the cache of the list of BitstreamFormats + */ + public clearBitStreamFormatRequests(): Observable { + return this.getBrowseEndpoint().pipe( + tap((href: string) => this.requestService.removeByHrefSubstring(href)) + ); + } + + /** + * Gets all the selected BitstreamFormats from the store + */ + public getSelectedBitstreamFormats(): Observable { + return this.store.pipe(select(selectedBitstreamFormatSelector)); + } + + /** + * Adds a BistreamFormat to the selected BitstreamFormats in the store + * @param bitstreamFormat + */ + public selectBitstreamFormat(bitstreamFormat: BitstreamFormat) { + this.store.dispatch(new BitstreamFormatsRegistrySelectAction(bitstreamFormat)); + } + + /** + * Removes a BistreamFormat from the list of selected BitstreamFormats in the store + * @param bitstreamFormat + */ + public deselectBitstreamFormat(bitstreamFormat: BitstreamFormat) { + this.store.dispatch(new BitstreamFormatsRegistryDeselectAction(bitstreamFormat)); + } + + /** + * Removes all BitstreamFormats from the list of selected BitstreamFormats in the store + */ + public deselectAllBitstreamFormats() { + this.store.dispatch(new BitstreamFormatsRegistryDeselectAllAction()); + } + + /** + * Delete an existing DSpace Object on the server + * @param format The DSpace Object to be removed + * Return an observable that emits true when the deletion was successful, false when it failed + */ + delete(format: BitstreamFormat): Observable { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, format.id))); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href, format.id); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } +} diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts index 4690d738ed..a2f5f21312 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.ts @@ -3,24 +3,17 @@ import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { - ErrorResponse, - GenericSuccessResponse, - RestResponse -} from '../cache/response.models'; +import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { BrowseEntry } from '../shared/browse-entry.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; +import { NormalizedBrowseEntry } from '../shared/normalized-browse-entry.model'; @Injectable() export class BrowseEntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = { - getConstructor: () => BrowseEntry - }; protected toCache = false; constructor( @@ -33,7 +26,7 @@ export class BrowseEntriesResponseParsingService extends BaseResponseParsingServ if (isNotEmpty(data.payload)) { let browseEntries = []; if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceRESTv2Serializer(BrowseEntry); + const serializer = new DSpaceRESTv2Serializer(NormalizedBrowseEntry); browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); } return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts index fb950f6c68..324b36199a 100644 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -18,10 +18,6 @@ import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object */ @Injectable() export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected objectFactory = { - getConstructor: () => DSpaceObject - }; protected toCache = false; constructor( diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index c1b0566e0b..8d0fe7cd41 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -113,7 +113,6 @@ describe('BrowseResponseParsingService', () => { } ], defaultSortOrder: 'ASC', - type: 'browse', metadataKeys: [ 'dc.date.issued' ], @@ -139,7 +138,6 @@ describe('BrowseResponseParsingService', () => { } ], defaultSortOrder: 'ASC', - type: 'browse', metadataKeys: [ 'dc.contributor.*', 'dc.creator' @@ -173,6 +171,5 @@ describe('BrowseResponseParsingService', () => { const response = service.parse(validRequest, validResponse); expect((response as GenericSuccessResponse).payload).toEqual(definitions); }); - }); }); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 993954a360..04e483604c 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs/internal/Observable'; import { FindAllOptions } from './request.models'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; +import { SearchParam } from '../cache/models/search-param.model'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -40,6 +41,36 @@ export class CollectionDataService extends ComColDataService { super(); } + /** + * Get all collections the user is authorized to submit to + * + * @param options The [[FindAllOptions]] object + * @return Observable>> + * collection list + */ + getAuthorizedCollection(options: FindAllOptions = {}): Observable>> { + const searchHref = 'findAuthorized'; + + return this.searchBy(searchHref, options).pipe( + filter((collections: RemoteData>) => !collections.isResponsePending)); + } + + /** + * Get all collections the user is authorized to submit to, by community + * + * @param communityId The community id + * @param options The [[FindAllOptions]] object + * @return Observable>> + * collection list + */ + getAuthorizedCollectionByCommunity(communityId: string, options: FindAllOptions = {}): Observable>> { + const searchHref = 'findAuthorizedByCommunity'; + options.searchParams = [new SearchParam('uuid', communityId)]; + + return this.searchBy(searchHref, options).pipe( + filter((collections: RemoteData>) => !collections.isResponsePending)); + } + /** * Find whether there is a collection whom user has authorization to submit to * diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 7f628fc5b9..b5232b0bff 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -89,7 +89,7 @@ describe('ComColDataService', () => { function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { getEndpoint: hot('--a-', { a: communitiesEndpoint }), - getIDHref: cold('b-', { b: communityEndpoint }) + getIDHref: communityEndpoint }); } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 9d82cc5047..68eb3e4880 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -31,7 +31,7 @@ export abstract class ComColDataService extends DataS return this.halService.getEndpoint(linkPath); } else { const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( - mergeMap((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)), + map((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)), filter((href: string) => isNotEmpty(href)), take(1), tap((href: string) => { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index fc4da69a5c..ad0db51980 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -31,12 +31,12 @@ import { configureRequest, getResponseFromEntry } from '../shared/operators'; import { ErrorResponse, RestResponse } from '../cache/response.models'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { CacheableObject } from '../cache/object-cache.reducer'; import { RequestEntry } from './request.reducer'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { ChangeAnalyzer } from './change-analyzer'; import { RestRequestMethod } from './rest-request-method'; +import { getMapsToType } from '../cache/builders/build-decorators'; export abstract class DataService { protected abstract requestService: RequestService; @@ -243,7 +243,7 @@ export abstract class DataService { ); const normalizedObject: NormalizedObject = this.dataBuildService.normalize(dso); - const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(normalizedObject); + const serializedDso = new DSpaceRESTv2Serializer(getMapsToType((dso as any).type)).serialize(normalizedObject); const request$ = endpoint$.pipe( take(1), diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index eb95cdae8a..d6c3b2caa6 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -4,8 +4,6 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../../config'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { ResourceType } from '../shared/resource-type'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; import { RestRequest } from './request.models'; @@ -17,8 +15,6 @@ import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable() export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected objectFactory = NormalizedObjectFactory; protected toCache = true; constructor( @@ -34,7 +30,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem if (hasValue(data.payload) && hasValue(data.payload.page) && data.payload.page.totalElements === 0) { processRequestDTO = { page: [] }; } else { - processRequestDTO = this.process, ResourceType>(data.payload, request.uuid); + processRequestDTO = this.process>(data.payload, request.uuid); } let objectList = processRequestDTO; 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 e65e317642..15f520b249 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -15,7 +15,6 @@ import { GLOBAL_CONFIG } from '../../../config'; @Injectable() export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - objectFactory = {}; toCache = false; constructor( @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index e03c1a78df..b67cef97c0 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -17,7 +17,6 @@ import { GLOBAL_CONFIG } from '../../../config'; @Injectable() export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - objectFactory = {}; toCache = false; constructor( 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 e7665ebed2..49d72e0a01 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -12,7 +12,6 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - objectFactory = {}; toCache = false; constructor( @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f6adbb23c2..07d8ed8405 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,8 +1,8 @@ -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, find, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; @@ -12,14 +12,17 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindAllOptions, PatchRequest, RestRequest } from './request.models'; +import { FindAllOptions, PatchRequest, PutRequest, RestRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { configureRequest, getRequestFromRequestHref } from '../shared/operators'; import { RequestEntry } from './request.reducer'; +import { RestResponse } from '../cache/response.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Collection } from '../shared/collection.model'; @Injectable() export class ItemDataService extends DataService { @@ -118,4 +121,43 @@ export class ItemDataService extends DataService { map((requestEntry: RequestEntry) => requestEntry.response) ); } + + /** + * Get the endpoint to move the item + * @param itemId + */ + public getMoveItemEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, itemId)), + map((endpoint: string) => `${endpoint}/owningCollection`) + ); + } + + /** + * Move the item to a different owning collection + * @param itemId + * @param collection + */ + public moveToCollection(itemId: string, collection: Collection): Observable { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getMoveItemEndpoint(itemId); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PutRequest(requestId, href, collection.self, options); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } } diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 1d2bf3b221..4baca6e8ed 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -9,19 +9,42 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindAllOptions } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { MetadataSchema } from '../metadata/metadataschema.model'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { HttpClient } from '@angular/common/http'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { ChangeAnalyzer } from './change-analyzer'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; + +/* tslint:disable:max-classes-per-file */ +class DataServiceImpl extends DataService { + protected linkPath = 'metadataschemas'; + protected forceBypassCache = false; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } + + getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); + } +} /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ @Injectable() -export class MetadataSchemaDataService extends DataService { - protected linkPath = 'metadataschemas'; - protected forceBypassCache = false; +export class MetadataSchemaDataService { + private dataService: DataServiceImpl; constructor( protected requestService: RequestService, @@ -33,17 +56,6 @@ export class MetadataSchemaDataService extends DataService { protected dataBuildService: NormalizedObjectBuildService, protected http: HttpClient, protected notificationsService: NotificationsService) { - super(); + this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); } - - /** - * Get the endpoint for browsing metadataschemas - * @param {FindAllOptions} options - * @returns {Observable} - */ - public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { - - return null; - } - } diff --git a/src/app/core/data/metadatafield-parsing.service.ts b/src/app/core/data/metadatafield-parsing.service.ts index f9582c394d..092285e9c5 100644 --- a/src/app/core/data/metadatafield-parsing.service.ts +++ b/src/app/core/data/metadatafield-parsing.service.ts @@ -3,8 +3,8 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; import { Injectable } from '@angular/core'; -import { MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models'; -import { MetadataField } from '../metadata/metadatafield.model'; +import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models'; +import { MetadataField } from '../metadata/metadata-field.model'; /** * A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse diff --git a/src/app/core/data/metadataschema-parsing.service.ts b/src/app/core/data/metadataschema-parsing.service.ts index f76d6ed2e3..3e9fd257bb 100644 --- a/src/app/core/data/metadataschema-parsing.service.ts +++ b/src/app/core/data/metadataschema-parsing.service.ts @@ -1,10 +1,10 @@ -import { MetadataSchema } from '../metadata/metadataschema.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; import { Injectable } from '@angular/core'; import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; @Injectable() export class MetadataschemaParsingService implements ResponseParsingService { diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 22d5fd3e77..08745f9223 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -105,6 +105,27 @@ export class ObjectUpdatesService { })) } + /** + * Method that combines the state's updates (excluding updates that aren't part of the initialFields) with + * the initial values (when there's no update) to create a FieldUpdates object + * @param url The URL of the page for which the FieldUpdates should be requested + * @param initialFields The initial values of the fields + */ + getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable { + const objectUpdates = this.getObjectEntry(url); + return objectUpdates.pipe(map((objectEntry) => { + const fieldUpdates: FieldUpdates = {}; + for (const object of initialFields) { + let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; + if (isEmpty(fieldUpdate)) { + fieldUpdate = { field: object, changeType: undefined }; + } + fieldUpdates[object.uuid] = fieldUpdate; + } + return fieldUpdates; + })) + } + /** * Method to check if a specific field is currently editable in the store * @param url The URL of the page on which the field resides diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts new file mode 100644 index 0000000000..0ced517d74 --- /dev/null +++ b/src/app/core/data/relationship.service.spec.ts @@ -0,0 +1,157 @@ +import { RelationshipService } from './relationship.service'; +import { RequestService } from './request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RequestEntry } from './request.reducer'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { ResourceType } from '../shared/resource-type'; +import { Relationship } from '../shared/item-relationships/relationship.model'; +import { RemoteData } from './remote-data'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { Item } from '../shared/item.model'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { DeleteRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Observable } from 'rxjs/internal/Observable'; + +describe('RelationshipService', () => { + let service: RelationshipService; + let requestService: RequestService; + + const restEndpointURL = 'https://rest.api/'; + const relationshipsEndpointURL = `${restEndpointURL}/relationships`; + const halService: any = new HALEndpointServiceStub(restEndpointURL); + const rdbService = getMockRemoteDataBuildService(); + const objectCache = Object.assign({ + /* tslint:disable:no-empty */ + remove: () => {} + /* tslint:enable:no-empty */ + }) as ObjectCacheService; + + const relationshipType = Object.assign(new RelationshipType(), { + id: '1', + uuid: '1', + leftLabel: 'isAuthorOfPublication', + rightLabel: 'isPublicationOfAuthor' + }); + + const relationship1 = Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/2', + id: '2', + uuid: '2', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + const relationship2 = Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/3', + id: '3', + uuid: '3', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + + const relationships = [ relationship1, relationship2 ]; + + const item = Object.assign(new Item(), { + self: 'fake-item-url/publication', + id: 'publication', + uuid: 'publication', + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + }); + + const relatedItem1 = Object.assign(new Item(), { + id: 'author1', + uuid: 'author1' + }); + const relatedItem2 = Object.assign(new Item(), { + id: 'author2', + uuid: 'author2' + }); + relationship1.leftItem = getRemotedataObservable(relatedItem1); + relationship1.rightItem = getRemotedataObservable(item); + relationship2.leftItem = getRemotedataObservable(relatedItem2); + relationship2.rightItem = getRemotedataObservable(item); + const relatedItems = [relatedItem1, relatedItem2]; + + const itemService = jasmine.createSpyObj('itemService', { + findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((relatedItem) => relatedItem.id === uuid)[0]) + }); + + function initTestService() { + return new RelationshipService( + requestService, + halService, + rdbService, + itemService, + objectCache + ); + } + + const getRequestEntry$ = (successful: boolean) => { + return observableOf({ + response: { isSuccessful: successful, payload: relationships } as any + } as RequestEntry) + }; + + beforeEach(() => { + requestService = getMockRequestService(getRequestEntry$(true)); + service = initTestService(); + }); + + describe('deleteRelationship', () => { + beforeEach(() => { + spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1)); + spyOn(objectCache, 'remove'); + service.deleteRelationship(relationships[0].uuid).subscribe(); + }); + + it('should send a DeleteRequest', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid); + expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); + }); + + it('should clear the related items their cache', () => { + expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self); + expect(objectCache.remove).toHaveBeenCalledWith(item.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); + }); + }); + + describe('getItemRelationshipsArray', () => { + it('should return the item\'s relationships in the form of an array', () => { + service.getItemRelationshipsArray(item).subscribe((result) => { + expect(result).toEqual(relationships); + }); + }); + }); + + describe('getItemRelationshipLabels', () => { + it('should return the correct labels', () => { + service.getItemRelationshipLabels(item).subscribe((result) => { + expect(result).toEqual([relationshipType.rightLabel]); + }); + }); + }); + + describe('getRelatedItems', () => { + it('should return the related items', () => { + service.getRelatedItems(item).subscribe((result) => { + expect(result).toEqual(relatedItems); + }); + }); + }); + + describe('getRelatedItemsByLabel', () => { + it('should return the related items by label', () => { + service.getRelatedItemsByLabel(item, relationshipType.rightLabel).subscribe((result) => { + expect(result).toEqual(relatedItems); + }); + }); + }) + +}); + +function getRemotedataObservable(obj: any): Observable> { + return observableOf(new RemoteData(false, false, true, undefined, obj)); +} diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts new file mode 100644 index 0000000000..b07e4b714c --- /dev/null +++ b/src/app/core/data/relationship.service.ts @@ -0,0 +1,235 @@ +import { Injectable } from '@angular/core'; +import { RequestService } from './request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; +import { distinctUntilChanged, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; +import { + configureRequest, + filterSuccessfulResponses, + getRemoteDataPayload, getResponseFromEntry, + getSucceededRemoteData +} from '../shared/operators'; +import { DeleteRequest, RestRequest } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { RestResponse } from '../cache/response.models'; +import { Item } from '../shared/item.model'; +import { Relationship } from '../shared/item-relationships/relationship.model'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { RemoteData } from './remote-data'; +import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; +import { zip as observableZip } from 'rxjs'; +import { PaginatedList } from './paginated-list'; +import { ItemDataService } from './item-data.service'; +import { + compareArraysUsingIds, filterRelationsByTypeLabel, + relationsToItems +} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { ObjectCacheService } from '../cache/object-cache.service'; + +/** + * The service handling all relationship requests + */ +@Injectable() +export class RelationshipService { + protected linkPath = 'relationships'; + + constructor(protected requestService: RequestService, + protected halService: HALEndpointService, + protected rdbService: RemoteDataBuildService, + protected itemService: ItemDataService, + protected objectCache: ObjectCacheService) { + } + + /** + * Get the endpoint for a relationship by ID + * @param uuid + */ + getRelationshipEndpoint(uuid: string) { + return this.halService.getEndpoint(this.linkPath).pipe( + map((href: string) => `${href}/${uuid}`) + ); + } + + /** + * Find a relationship by its UUID + * @param uuid + */ + findById(uuid: string): Observable> { + const href$ = this.getRelationshipEndpoint(uuid); + return this.rdbService.buildSingle(href$); + } + + /** + * Send a delete request for a relationship by ID + * @param uuid + */ + deleteRelationship(uuid: string): Observable { + return this.getRelationshipEndpoint(uuid).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService), + switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), + getResponseFromEntry(), + tap(() => this.clearRelatedCache(uuid)) + ); + } + + /** + * Get a combined observable containing an array of all relationships in an item, as well as an array of the relationships their types + * This is used for easier access of a relationship's type because they exist as observables + * @param item + */ + getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> { + return observableCombineLatest( + this.getItemRelationshipsArray(item), + this.getItemRelationshipTypesArray(item) + ); + } + + /** + * Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships their types + * This is used for easier access of a relationship's type and left and right items because they exist as observables + * @param item + */ + getItemResolvedRelatedItemsAndTypes(item: Item): Observable<[Item[], Item[], RelationshipType[]]> { + return observableCombineLatest( + this.getItemLeftRelatedItemArray(item), + this.getItemRightRelatedItemArray(item), + this.getItemRelationshipTypesArray(item) + ); + } + + /** + * Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships themselves + * This is used for easier access of the relationship and their left and right items because they exist as observables + * @param item + */ + getItemResolvedRelatedItemsAndRelationships(item: Item): Observable<[Item[], Item[], Relationship[]]> { + return observableCombineLatest( + this.getItemLeftRelatedItemArray(item), + this.getItemRightRelatedItemArray(item), + this.getItemRelationshipsArray(item) + ); + } + + /** + * Get an item their relationships in the form of an array + * @param item + */ + getItemRelationshipsArray(item: Item): Observable { + return item.relationships.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((rels: PaginatedList) => rels.page), + hasValueOperator(), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + /** + * Get an item their relationship types in the form of an array + * @param item + */ + getItemRelationshipTypesArray(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + flatMap((rels: Relationship[]) => + observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe( + map(([...arr]: Array>) => arr.map((d: RemoteData) => d.payload).filter((type) => hasValue(type))), + filter((arr) => arr.length === rels.length) + ) + ), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + /** + * Get an item his relationship's left-side related items in the form of an array + * @param item + */ + getItemLeftRelatedItemArray(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.leftItem)).pipe( + map(([...arr]: Array>) => arr.map((rd: RemoteData) => rd.payload).filter((i) => hasValue(i))), + filter((arr) => arr.length === rels.length) + )), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + /** + * Get an item his relationship's right-side related items in the form of an array + * @param item + */ + getItemRightRelatedItemArray(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.rightItem)).pipe( + map(([...arr]: Array>) => arr.map((rd: RemoteData) => rd.payload).filter((i) => hasValue(i))), + filter((arr) => arr.length === rels.length) + )), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + /** + * Get an array of an item their unique relationship type's labels + * The array doesn't contain any duplicate labels + * @param item + */ + getItemRelationshipLabels(item: Item): Observable { + return this.getItemResolvedRelatedItemsAndTypes(item).pipe( + map(([leftItems, rightItems, relTypesCurrentPage]) => { + return relTypesCurrentPage.map((type, index) => { + if (leftItems[index].uuid === item.uuid) { + return type.leftwardType; + } else { + return type.rightwardType; + } + }); + }), + map((labels: string[]) => Array.from(new Set(labels))) + ) + } + + /** + * Resolve a given item's relationships into related items and return the items as an array + * @param item + */ + getRelatedItems(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + relationsToItems(item.uuid) + ); + } + + /** + * Resolve a given item's relationships into related items, filtered by a relationship label + * and return the items as an array + * @param item + * @param label + */ + getRelatedItemsByLabel(item: Item, label: string): Observable { + return this.getItemResolvedRelsAndTypes(item).pipe( + filterRelationsByTypeLabel(label), + relationsToItems(item.uuid) + ); + } + + /** + * Clear object and request caches of the items related to a relationship (left and right items) + * @param uuid + */ + clearRelatedCache(uuid: string) { + this.findById(uuid).pipe( + getSucceededRemoteData(), + flatMap((rd: RemoteData) => observableCombineLatest(rd.payload.leftItem.pipe(getSucceededRemoteData()), rd.payload.rightItem.pipe(getSucceededRemoteData()))), + take(1) + ).subscribe(([leftItem, rightItem]) => { + this.objectCache.remove(leftItem.payload.self); + this.objectCache.remove(rightItem.payload.self); + this.requestService.removeByHrefSubstring(leftItem.payload.self); + this.requestService.removeByHrefSubstring(rightItem.payload.self); + }); + } + +} diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 5e7bec698b..9ef85bfe8b 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -17,10 +17,10 @@ import { RequestError, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { catchError, filter, flatMap, map, take, tap } from 'rxjs/operators'; import { ErrorResponse, RestResponse } from '../cache/response.models'; import { StoreActionTypes } from '../../store.actions'; +import { getMapsToType } from '../cache/builders/build-decorators'; export const addToResponseCacheAndCompleteAction = (request: RestRequest, envConfig: GlobalConfig) => (source: Observable): Observable => @@ -45,7 +45,7 @@ export class RequestEffects { flatMap((request: RestRequest) => { let body; if (isNotEmpty(request.body)) { - const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type)); + const serializer = new DSpaceRESTv2Serializer(getMapsToType(request.body.type)); body = serializer.serialize(request.body); } return this.restApi.request(request.method, request.href, body, request.options).pipe( diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 83071382ed..0980d48537 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -3,7 +3,7 @@ import { HttpHeaders } from '@angular/common/http'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { Observable, race as observableRace } from 'rxjs'; -import { filter, find, map, mergeMap, take } from 'rxjs/operators'; +import { filter, map, mergeMap, take } from 'rxjs/operators'; import { cloneDeep, remove } from 'lodash'; import { AppState } from '../../app.reducer'; @@ -65,8 +65,7 @@ const uuidsFromHrefSubstringSelector = const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => { let result = []; if (isNotEmpty(state)) { - result = Object.values(state) - .filter((value: string) => value.startsWith(href)); + result = Object.keys(state).filter((key) => key.startsWith(href)).map((key) => state[key]); } return result; }; @@ -263,12 +262,13 @@ export class RequestService { */ private clearRequestsOnTheirWayToTheStore(request: GetRequest) { this.getByHref(request.href).pipe( - find((re: RequestEntry) => hasValue(re))) - .subscribe((re: RequestEntry) => { - if (!re.responsePending) { - remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href); - } - }); + filter((re: RequestEntry) => hasValue(re)), + take(1) + ).subscribe((re: RequestEntry) => { + if (!re.responsePending) { + remove(this.requestsOnTheirWayToTheStore, (item) => item === request.href); + } + }); } /** @@ -315,4 +315,15 @@ export class RequestService { return result; } + /** + * Create an observable that emits a new value whenever the availability of the cached request changes. + * The value it emits is a boolean stating if the request exists in cache or not. + * @param href The href of the request to observe + */ + hasByHrefObservable(href: string): Observable { + return this.getByHref(href).pipe( + map((requestEntry: RequestEntry) => this.isValid(requestEntry)) + ); + } + } diff --git a/src/app/core/data/resource-policy.service.spec.ts b/src/app/core/data/resource-policy.service.spec.ts new file mode 100644 index 0000000000..35d28684a7 --- /dev/null +++ b/src/app/core/data/resource-policy.service.spec.ts @@ -0,0 +1,77 @@ +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ResourcePolicy } from '../shared/resource-policy.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { GetRequest } from './request.models'; +import { RequestService } from './request.service'; +import { ResourcePolicyService } from './resource-policy.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; + +describe('ResourcePolicyService', () => { + let scheduler: TestScheduler; + let service: ResourcePolicyService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + const testObject = { + uuid: '664184ee-b254-45e8-970d-220e5ccc060b' + } as ResourcePolicy; + const requestURL = `https://rest.api/rest/api/resourcepolicies/${testObject.uuid}`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + beforeEach(() => { + scheduler = getTestScheduler(); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('a', { + a: { + payload: testObject + } + }) + }); + objectCache = {} as ObjectCacheService; + const halService = {} as HALEndpointService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; + + service = new ResourcePolicyService( + requestService, + rdbService, + dataBuildService, + objectCache, + halService, + notificationsService, + http, + comparator + ) + }); + + describe('findByHref', () => { + it('should configure the proper GetRequest', () => { + scheduler.schedule(() => service.findByHref(requestURL)); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(new GetRequest(requestUUID, requestURL, null), false); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findByHref(requestURL); + const expected = cold('a', { + a: { + payload: testObject + } + }); + expect(result).toBeObservable(expected); + }); + }); +}); diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/data/resource-policy.service.ts new file mode 100644 index 0000000000..1a6a1afedc --- /dev/null +++ b/src/app/core/data/resource-policy.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { FindAllOptions } from '../data/request.models'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ResourcePolicy } from '../shared/resource-policy.model'; +import { RemoteData } from '../data/remote-data'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ChangeAnalyzer } from './change-analyzer'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; + +/* tslint:disable:max-classes-per-file */ +class DataServiceImpl extends DataService { + protected linkPath = 'resourcepolicies'; + protected forceBypassCache = false; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } + + getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); + } +} + +/** + * A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint + */ +@Injectable() +export class ResourcePolicyService { + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); + } + + findByHref(href: string, options?: HttpOptions): Observable> { + return this.dataService.findByHref(href, options); + } +} diff --git a/src/app/core/eperson/eperson-response-parsing.service.ts b/src/app/core/eperson/eperson-response-parsing.service.ts index 6c591b0b99..481f37d1fa 100644 --- a/src/app/core/eperson/eperson-response-parsing.service.ts +++ b/src/app/core/eperson/eperson-response-parsing.service.ts @@ -9,8 +9,6 @@ import { BaseResponseParsingService } from '../data/base-response-parsing.servic import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; -import { ResourceType } from '../shared/resource-type'; import { DSpaceObject } from '../shared/dspace-object.model'; /** @@ -19,7 +17,6 @@ import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable() export class EpersonResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = NormalizedObjectFactory; protected toCache = false; constructor( @@ -31,7 +28,7 @@ export class EpersonResponseParsingService extends BaseResponseParsingService im parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { - const epersonDefinition = this.process(data.payload, request.href); + const epersonDefinition = this.process(data.payload, request.href); return new EpersonSuccessResponse(epersonDefinition[Object.keys(epersonDefinition)[0]], data.statusCode, data.statusText, this.processPageInfo(data.payload)); } else { return new ErrorResponse( diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index f8c11c1201..d99a059e8b 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -4,8 +4,10 @@ import { DSpaceObject } from '../../shared/dspace-object.model'; import { Group } from './group.model'; import { RemoteData } from '../../data/remote-data'; import { PaginatedList } from '../../data/paginated-list'; +import { ResourceType } from '../../shared/resource-type'; export class EPerson extends DSpaceObject { + static type = new ResourceType('eperson'); /** * A string representing the unique handle of this Collection diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 91ce5d90f3..9c14c20de7 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -3,8 +3,10 @@ import { Observable } from 'rxjs'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; +import { ResourceType } from '../../shared/resource-type'; export class Group extends DSpaceObject { + static type = new ResourceType('group'); /** * List of Groups that this Group belong to diff --git a/src/app/core/eperson/models/normalized-eperson.model.ts b/src/app/core/eperson/models/normalized-eperson.model.ts index ad4b20ee80..bf644a83ef 100644 --- a/src/app/core/eperson/models/normalized-eperson.model.ts +++ b/src/app/core/eperson/models/normalized-eperson.model.ts @@ -5,12 +5,11 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { EPerson } from './eperson.model'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; +import { Group } from './group.model'; @mapsTo(EPerson) @inheritSerialization(NormalizedDSpaceObject) export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject, ListableObject { - /** * A string representing the unique handle of this EPerson */ @@ -21,7 +20,7 @@ export class NormalizedEPerson extends NormalizedDSpaceObject implement * List of Groups that this EPerson belong to */ @deserialize - @relationship(ResourceType.Group, true) + @relationship(Group, true) groups: string[]; /** diff --git a/src/app/core/eperson/models/normalized-group.model.ts b/src/app/core/eperson/models/normalized-group.model.ts index f86bec8628..329ffb8adf 100644 --- a/src/app/core/eperson/models/normalized-group.model.ts +++ b/src/app/core/eperson/models/normalized-group.model.ts @@ -5,7 +5,6 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { Group } from './group.model'; -import { ResourceType } from '../../shared/resource-type'; @mapsTo(Group) @inheritSerialization(NormalizedDSpaceObject) @@ -15,7 +14,7 @@ export class NormalizedGroup extends NormalizedDSpaceObject implements Ca * List of Groups that this Group belong to */ @deserialize - @relationship(ResourceType.Group, true) + @relationship(Group, true) groups: string[]; /** diff --git a/src/app/core/integration/integration-object-factory.ts b/src/app/core/integration/integration-object-factory.ts deleted file mode 100644 index f66a070fdf..0000000000 --- a/src/app/core/integration/integration-object-factory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { GenericConstructor } from '../shared/generic-constructor'; -import { IntegrationType } from './intergration-type'; -import { IntegrationModel } from './models/integration.model'; -import { NormalizedAuthorityValue } from './models/normalized-authority-value.model'; - -export class IntegrationObjectFactory { - public static getConstructor(type): GenericConstructor { - switch (type) { - case IntegrationType.Authority: { - return NormalizedAuthorityValue; - } - default: { - return undefined; - } - } - } -} diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts index 2d3693cf3d..8cc0f8d252 100644 --- a/src/app/core/integration/integration-response-parsing.service.ts +++ b/src/app/core/integration/integration-response-parsing.service.ts @@ -2,27 +2,20 @@ import { Inject, Injectable } from '@angular/core'; import { RestRequest } from '../data/request.models'; import { ResponseParsingService } from '../data/parsing.service'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { - ErrorResponse, - IntegrationSuccessResponse, - RestResponse -} from '../cache/response.models'; +import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; -import { IntegrationObjectFactory } from './integration-object-factory'; import { BaseResponseParsingService } from '../data/base-response-parsing.service'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; import { IntegrationModel } from './models/integration.model'; -import { IntegrationType } from './intergration-type'; import { AuthorityValue } from './models/authority.value'; import { PaginatedList } from '../data/paginated-list'; @Injectable() export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = IntegrationObjectFactory; protected toCache = true; constructor( @@ -34,7 +27,7 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { - const dataDefinition = this.process(data.payload, request.uuid); + const dataDefinition = this.process(data.payload, request.uuid); return new IntegrationSuccessResponse(this.processResponse(dataDefinition), data.statusCode, data.statusText, this.processPageInfo(data.payload)); } else { return new ErrorResponse( @@ -49,7 +42,7 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic protected processResponse(data: PaginatedList): any { const returnList = Array.of(); data.page.forEach((item, index) => { - if (item.type === IntegrationType.Authority) { + if (item.type === AuthorityValue.type.value) { data.page[index] = Object.assign(new AuthorityValue(), item); } }); diff --git a/src/app/core/integration/intergration-type.ts b/src/app/core/integration/intergration-type.ts deleted file mode 100644 index 882dc6d8ce..0000000000 --- a/src/app/core/integration/intergration-type.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export enum IntegrationType { - Authority = 'authority' -} diff --git a/src/app/core/integration/models/authority.value.ts b/src/app/core/integration/models/authority.value.ts index 31cb0a5787..4c6a7c01cb 100644 --- a/src/app/core/integration/models/authority.value.ts +++ b/src/app/core/integration/models/authority.value.ts @@ -3,11 +3,13 @@ import { isNotEmpty } from '../../../shared/empty.util'; import { PLACEHOLDER_PARENT_METADATA } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { OtherInformation } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { MetadataValueInterface } from '../../shared/metadata.models'; +import { ResourceType } from '../../shared/resource-type'; /** * Class representing an authority object */ export class AuthorityValue extends IntegrationModel implements MetadataValueInterface { + static type = new ResourceType('authority'); /** * The identifier of this authority diff --git a/src/app/core/metadata/metadata-field.model.ts b/src/app/core/metadata/metadata-field.model.ts new file mode 100644 index 0000000000..288934e52d --- /dev/null +++ b/src/app/core/metadata/metadata-field.model.ts @@ -0,0 +1,53 @@ +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { MetadataSchema } from './metadata-schema.model'; +import { ResourceType } from '../shared/resource-type'; + +/** + * Class the represents a metadata field + */ +export class MetadataField implements ListableObject { + static type = new ResourceType('metadatafield'); + + /** + * The identifier of this metadata field + */ + id: number; + + /** + * The self link of this metadata field + */ + self: string; + + /** + * The element of this metadata field + */ + element: string; + + /** + * The qualifier of this metadata field + */ + qualifier: string; + + /** + * The scope note of this metadata field + */ + scopeNote: string; + + /** + * The metadata schema object of this metadata field + */ + schema: MetadataSchema; + + /** + * Method to print this metadata field as a string + * @param separator The separator between the schema, element and qualifier in the string + */ + toString(separator: string = '.'): string { + let key = this.schema.prefix + separator + this.element; + if (isNotEmpty(this.qualifier)) { + key += separator + this.qualifier; + } + return key; + } +} diff --git a/src/app/core/metadata/metadata-schema.model.ts b/src/app/core/metadata/metadata-schema.model.ts new file mode 100644 index 0000000000..bc05e475cc --- /dev/null +++ b/src/app/core/metadata/metadata-schema.model.ts @@ -0,0 +1,29 @@ +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { ResourceType } from '../shared/resource-type'; + +/** + * Class that represents a metadata schema + */ +export class MetadataSchema implements ListableObject { + static type = new ResourceType('metadataschema'); + + /** + * The unique identifier for this metadata schema + */ + id: number; + + /** + * The REST link to itself + */ + self: string; + + /** + * A unique prefix that defines this schema + */ + prefix: string; + + /** + * The namespace of this metadata schema + */ + namespace: string; +} diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index cfb5a0751d..80ce33b370 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -38,6 +38,7 @@ import { EmptyError } from 'rxjs/internal-compatibility'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { MetadataValue } from '../shared/metadata.models'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -206,13 +207,7 @@ describe('MetadataService', () => { }); const mockRemoteData = (mockItem: Item): Observable> => { - return observableOf(new RemoteData( - false, - false, - true, - undefined, - MockItem - )); + return createSuccessfulRemoteDataObject$(MockItem); }; const mockType = (mockItem: Item, type: string): Item => { diff --git a/src/app/core/metadata/metadatafield.model.ts b/src/app/core/metadata/metadatafield.model.ts deleted file mode 100644 index ba28b59d0e..0000000000 --- a/src/app/core/metadata/metadatafield.model.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MetadataSchema } from './metadataschema.model'; -import { autoserialize } from 'cerialize'; -import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { isNotEmpty } from '../../shared/empty.util'; - -export class MetadataField implements ListableObject { - @autoserialize - id: number; - - @autoserialize - self: string; - - @autoserialize - element: string; - - @autoserialize - qualifier: string; - - @autoserialize - scopeNote: string; - - @autoserialize - schema: MetadataSchema; - - toString(separator: string = '.'): string { - let key = this.schema.prefix + separator + this.element; - if (isNotEmpty(this.qualifier)) { - key += separator + this.qualifier; - } - return key; - } -} diff --git a/src/app/core/metadata/metadataschema.model.ts b/src/app/core/metadata/metadataschema.model.ts deleted file mode 100644 index 13fb8e8b4e..0000000000 --- a/src/app/core/metadata/metadataschema.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { autoserialize } from 'cerialize'; -import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; - -export class MetadataSchema implements ListableObject { - @autoserialize - id: number; - - @autoserialize - self: string; - - @autoserialize - prefix: string; - - @autoserialize - namespace: string; -} diff --git a/src/app/core/metadata/normalized-metadata-field.model.ts b/src/app/core/metadata/normalized-metadata-field.model.ts new file mode 100644 index 0000000000..c6b2ee32f8 --- /dev/null +++ b/src/app/core/metadata/normalized-metadata-field.model.ts @@ -0,0 +1,51 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { mapsTo, relationship } from '../cache/builders/build-decorators'; +import { MetadataField } from './metadata-field.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { MetadataSchema } from './metadata-schema.model'; + +/** + * Class the represents a normalized metadata field + */ +@mapsTo(MetadataField) +@inheritSerialization(NormalizedObject) +export class NormalizedMetadataField extends NormalizedObject implements ListableObject { + + /** + * The identifier of this normalized metadata field + */ + @autoserialize + id: number; + + /** + * The self link of this normalized metadata field + */ + @autoserialize + self: string; + + /** + * The element of this normalized metadata field + */ + @autoserialize + element: string; + + /** + * The qualifier of this normalized metadata field + */ + @autoserialize + qualifier: string; + + /** + * The scope note of this normalized metadata field + */ + @autoserialize + scopeNote: string; + + /** + * The link to the metadata schema of this normalized metadata field + */ + @deserialize + @relationship(MetadataSchema) + schema: string; +} diff --git a/src/app/core/metadata/normalized-metadata-schema.model.ts b/src/app/core/metadata/normalized-metadata-schema.model.ts index c121938940..47c7233d81 100644 --- a/src/app/core/metadata/normalized-metadata-schema.model.ts +++ b/src/app/core/metadata/normalized-metadata-schema.model.ts @@ -1,13 +1,14 @@ -import { autoserialize } from 'cerialize'; +import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { mapsTo } from '../cache/builders/build-decorators'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { MetadataSchema } from './metadataschema.model'; +import { MetadataSchema } from './metadata-schema.model'; /** * Normalized class for a DSpace MetadataSchema */ @mapsTo(MetadataSchema) +@inheritSerialization(NormalizedObject) export class NormalizedMetadataSchema extends NormalizedObject implements ListableObject { /** * The unique identifier for this schema diff --git a/src/app/core/registry/mock-bitstream-format.model.ts b/src/app/core/registry/mock-bitstream-format.model.ts deleted file mode 100644 index f5811e367c..0000000000 --- a/src/app/core/registry/mock-bitstream-format.model.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class BitstreamFormat { - shortDescription: string; - description: string; - mimetype: string; - supportLevel: number; - internal: boolean; - extensions: string; -} diff --git a/src/app/core/registry/registry-bitstreamformats-response.model.ts b/src/app/core/registry/registry-bitstreamformats-response.model.ts index 81de379e9e..ddf926f3be 100644 --- a/src/app/core/registry/registry-bitstreamformats-response.model.ts +++ b/src/app/core/registry/registry-bitstreamformats-response.model.ts @@ -1,9 +1,11 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, deserialize } from 'cerialize'; import { PageInfo } from '../shared/page-info.model'; import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { relationship } from '../cache/builders/build-decorators'; export class RegistryBitstreamformatsResponse { - @autoserializeAs(BitstreamFormat) + @deserialize + @relationship(BitstreamFormat, true) bitstreamformats: BitstreamFormat[]; @autoserialize diff --git a/src/app/core/registry/registry-metadatafields-response.model.ts b/src/app/core/registry/registry-metadatafields-response.model.ts index 19ec537dfb..984603e42e 100644 --- a/src/app/core/registry/registry-metadatafields-response.model.ts +++ b/src/app/core/registry/registry-metadatafields-response.model.ts @@ -1,14 +1,31 @@ import { PageInfo } from '../shared/page-info.model'; -import { autoserialize, autoserializeAs } from 'cerialize'; -import { MetadataField } from '../metadata/metadatafield.model'; +import { autoserialize, deserialize } from 'cerialize'; +import { ResourceType } from '../shared/resource-type'; +import { relationship } from '../cache/builders/build-decorators'; +import { NormalizedMetadataField } from '../metadata/normalized-metadata-field.model'; +import { MetadataField } from '../metadata/metadata-field.model'; +/** + * Class that represents a response with a registry's metadata fields + */ export class RegistryMetadatafieldsResponse { - @autoserializeAs(MetadataField) + static type = new ResourceType('metadatafield'); + /** + * List of metadata fields in the response + */ + @deserialize + @relationship(MetadataField, true) metadatafields: MetadataField[]; + /** + * Page info of this response + */ @autoserialize page: PageInfo; + /** + * The REST link to this response + */ @autoserialize self: string; } diff --git a/src/app/core/registry/registry-metadataschemas-response.model.ts b/src/app/core/registry/registry-metadataschemas-response.model.ts index 5f4799abd7..fc53b354a5 100644 --- a/src/app/core/registry/registry-metadataschemas-response.model.ts +++ b/src/app/core/registry/registry-metadataschemas-response.model.ts @@ -1,9 +1,11 @@ -import { MetadataSchema } from '../metadata/metadataschema.model'; import { PageInfo } from '../shared/page-info.model'; -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, deserialize } from 'cerialize'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { relationship } from '../cache/builders/build-decorators'; export class RegistryMetadataschemasResponse { - @autoserializeAs(MetadataSchema) + @deserialize + @relationship(MetadataSchema, true) metadataschemas: MetadataSchema[]; @autoserialize diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index 8274ceef60..455a8043da 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -12,7 +12,6 @@ import { PageInfo } from '../shared/page-info.model'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { - RegistryBitstreamformatsSuccessResponse, RegistryMetadatafieldsSuccessResponse, RegistryMetadataschemasSuccessResponse, RestResponse @@ -20,7 +19,6 @@ import { import { Component } from '@angular/core'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; -import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model'; import { map } from 'rxjs/operators'; import { Store, StoreModule } from '@ngrx/store'; import { MockStore } from '../../shared/testing/mock-store'; @@ -39,10 +37,12 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { MetadataSchema } from '../metadata/metadataschema.model'; -import { MetadataField } from '../metadata/metadatafield.model'; +import { ResourceType } from '../shared/resource-type'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; -@Component({ template: '' }) +@Component({template: ''}) class DummyComponent { } @@ -59,13 +59,15 @@ describe('RegistryService', () => { id: 1, self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1', prefix: 'dc', - namespace: 'http://dublincore.org/documents/dcmi-terms/' - }, + namespace: 'http://dublincore.org/documents/dcmi-terms/', + type: MetadataSchema.type +}, { id: 2, self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2', prefix: 'mock', - namespace: 'http://dspace.org/mockschema' + namespace: 'http://dspace.org/mockschema', + type: MetadataSchema.type } ]; const mockFieldsList = [ @@ -75,7 +77,8 @@ describe('RegistryService', () => { element: 'contributor', qualifier: 'advisor', scopeNote: null, - schema: mockSchemasList[0] + schema: mockSchemasList[0], + type: MetadataField.type }, { id: 2, @@ -83,7 +86,8 @@ describe('RegistryService', () => { element: 'contributor', qualifier: 'author', scopeNote: null, - schema: mockSchemasList[0] + schema: mockSchemasList[0], + type: MetadataField.type }, { id: 3, @@ -91,7 +95,8 @@ describe('RegistryService', () => { element: 'contributor', qualifier: 'editor', scopeNote: 'test scope note', - schema: mockSchemasList[1] + schema: mockSchemasList[1], + type: MetadataField.type }, { id: 4, @@ -99,7 +104,8 @@ describe('RegistryService', () => { element: 'contributor', qualifier: 'illustrator', scopeNote: null, - schema: mockSchemasList[1] + schema: mockSchemasList[1], + type: MetadataField.type } ]; @@ -119,12 +125,12 @@ describe('RegistryService', () => { toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => { return observableCombineLatest(requestEntryObs, payloadObs).pipe(map(([req, pay]) => { - return { req, pay }; + return {req, pay}; }) ); }, aggregate: (input: Array>>): Observable> => { - return observableOf(new RemoteData(false, false, true, null, [])); + return createSuccessfulRemoteDataObject$([]); } }; @@ -135,11 +141,11 @@ describe('RegistryService', () => { DummyComponent ], providers: [ - { provide: RequestService, useValue: getMockRequestService() }, - { provide: RemoteDataBuildService, useValue: rdbStub }, - { provide: HALEndpointService, useValue: halServiceStub }, - { provide: Store, useClass: MockStore }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + {provide: RequestService, useValue: getMockRequestService()}, + {provide: RemoteDataBuildService, useValue: rdbStub}, + {provide: HALEndpointService, useValue: halServiceStub}, + {provide: Store, useClass: MockStore}, + {provide: NotificationsService, useValue: new NotificationsServiceStub()}, RegistryService ] }); @@ -154,7 +160,7 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), {response: response}); beforeEach(() => { (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); @@ -183,7 +189,7 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), {response: response}); beforeEach(() => { (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); @@ -212,7 +218,7 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), {response: response}); beforeEach(() => { (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); @@ -235,35 +241,6 @@ describe('RegistryService', () => { }); }); - describe('when requesting bitstreamformats', () => { - const queryResponse = Object.assign(new RegistryBitstreamformatsResponse(), { - bitstreamformats: mockFieldsList, - page: pageInfo - }); - const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, 200, 'OK', pageInfo); - const responseEntry = Object.assign(new RequestEntry(), { response: response }); - - beforeEach(() => { - (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); - /* tslint:disable:no-empty */ - registryService.getBitstreamFormats(pagination).subscribe((value) => { - }); - /* tslint:enable:no-empty */ - }); - - it('should call getEndpoint on the halService', () => { - expect((registryService as any).halService.getEndpoint).toHaveBeenCalled(); - }); - - it('should send out the request on the request service', () => { - expect((registryService as any).requestService.configure).toHaveBeenCalled(); - }); - - it('should call getByHref on the request service with the correct request url', () => { - expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams); - }); - }); - describe('when dispatching to the store', () => { beforeEach(() => { spyOn(mockStore, 'dispatch'); @@ -276,7 +253,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryEditSchemaAction with the correct schema', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditSchemaAction(mockSchemasList[0])); - }) + }); }); describe('when calling cancelEditMetadataSchema', () => { @@ -286,7 +263,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryCancelSchemaAction', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelSchemaAction()); - }) + }); }); describe('when calling selectMetadataSchema', () => { @@ -296,7 +273,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistrySelectSchemaAction with the correct schema', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectSchemaAction(mockSchemasList[0])); - }) + }); }); describe('when calling deselectMetadataSchema', () => { @@ -306,7 +283,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryDeselectSchemaAction with the correct schema', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectSchemaAction(mockSchemasList[0])); - }) + }); }); describe('when calling deselectAllMetadataSchema', () => { @@ -316,7 +293,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryDeselectAllSchemaAction', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllSchemaAction()); - }) + }); }); describe('when calling editMetadataField', () => { @@ -326,7 +303,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryEditFieldAction with the correct Field', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditFieldAction(mockFieldsList[0])); - }) + }); }); describe('when calling cancelEditMetadataField', () => { @@ -336,7 +313,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryCancelFieldAction', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelFieldAction()); - }) + }); }); describe('when calling selectMetadataField', () => { @@ -346,7 +323,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistrySelectFieldAction with the correct Field', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectFieldAction(mockFieldsList[0])); - }) + }); }); describe('when calling deselectMetadataField', () => { @@ -356,7 +333,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryDeselectFieldAction with the correct Field', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectFieldAction(mockFieldsList[0])); - }) + }); }); describe('when calling deselectAllMetadataField', () => { @@ -366,7 +343,7 @@ describe('RegistryService', () => { it('should dispatch a MetadataRegistryDeselectAllFieldAction', () => { expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllFieldAction()); - }) + }); }); }); @@ -409,7 +386,7 @@ describe('RegistryService', () => { result.subscribe((response: RestResponse) => { expect(response.isSuccessful).toBe(true); }); - }) + }); }); describe('when deleteMetadataField is called', () => { @@ -423,7 +400,7 @@ describe('RegistryService', () => { result.subscribe((response: RestResponse) => { expect(response.isSuccessful).toBe(true); }); - }) + }); }); describe('when clearMetadataSchemaRequests is called', () => { diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 137b4c3a87..206426588e 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -3,15 +3,13 @@ import { Injectable } from '@angular/core'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; import { PageInfo } from '../shared/page-info.model'; -import { MetadataSchema } from '../metadata/metadataschema.model'; -import { MetadataField } from '../metadata/metadatafield.model'; -import { BitstreamFormat } from './mock-bitstream-format.model'; import { CreateMetadataFieldRequest, CreateMetadataSchemaRequest, DeleteRequest, GetRequest, - RestRequest, UpdateMetadataFieldRequest, + RestRequest, + UpdateMetadataFieldRequest, UpdateMetadataSchemaRequest } from '../data/request.models'; import { GenericConstructor } from '../shared/generic-constructor'; @@ -21,24 +19,19 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RequestService } from '../data/request.service'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { - ErrorResponse, MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, - RegistryBitstreamformatsSuccessResponse, + MetadatafieldSuccessResponse, + MetadataschemaSuccessResponse, RegistryMetadatafieldsSuccessResponse, - RegistryMetadataschemasSuccessResponse, RestResponse + RegistryMetadataschemasSuccessResponse, + RestResponse } from '../cache/response.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; -import { hasValue, hasNoValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service'; -import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model'; -import { - configureRequest, - getResponseFromEntry, - getSucceededRemoteData -} from '../shared/operators'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; import { createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers'; @@ -54,16 +47,17 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { distinctUntilChanged, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; -import { ResourceType } from '../shared/resource-type'; import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { getMapsToType } from '../cache/builders/build-decorators'; const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry; const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema); @@ -71,12 +65,16 @@ const selectedMetadataSchemasSelector = createSelector(metadataRegistryStateSele const editMetadataFieldSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editField); const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.selectedFields); +/** + * Service for registry related CRUD actions such as metadata schema, metadata field and bitstream format + */ @Injectable() export class RegistryService { private metadataSchemasPath = 'metadataschemas'; private metadataFieldsPath = 'metadatafields'; - private bitstreamFormatsPath = 'bitstreamformats'; + + // private bitstreamFormatsPath = 'bitstreamformats'; constructor(protected requestService: RequestService, private rdb: RemoteDataBuildService, @@ -87,6 +85,10 @@ export class RegistryService { } + /** + * Retrieves all metadata schemas + * @param pagination The pagination info used to retrieve the schemas + */ public getMetadataSchemas(pagination: PaginationComponentOptions): Observable>> { const requestObs = this.getMetadataSchemasRequestObs(pagination); @@ -117,6 +119,10 @@ export class RegistryService { return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); } + /** + * Retrieves a metadata schema by its name + * @param schemaName The name of the schema to find + */ public getMetadataSchemaByName(schemaName: string): Observable> { // Temporary pagination to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { @@ -142,6 +148,11 @@ export class RegistryService { return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs); } + /** + * retrieves all metadata fields that belong to a certain metadata schema + * @param schema The schema to filter by + * @param pagination The pagination info used to retrieve the fields + */ public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable>> { const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema); @@ -181,7 +192,7 @@ export class RegistryService { */ public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable>> { if (hasNoValue(pagination)) { - pagination = { currentPage: 1, pageSize: 10000 } as any; + pagination = {currentPage: 1, pageSize: 10000} as any; } const requestObs = this.getMetadataFieldsRequestObs(pagination); @@ -215,36 +226,6 @@ export class RegistryService { return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); } - public getBitstreamFormats(pagination: PaginationComponentOptions): Observable>> { - const requestObs = this.getBitstreamFormatsRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rbrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse) - ); - - const bitstreamformatsObs: Observable = rbrObs.pipe( - map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(bitstreamformatsObs, pageInfoObs).pipe( - map(([bitstreamformats, pageInfo]) => { - return new PaginatedList(pageInfo, bitstreamformats); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); - } - public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable { return this.halService.getEndpoint(this.metadataSchemasPath).pipe( map((url: string) => { @@ -307,78 +288,97 @@ export class RegistryService { ); } - private getBitstreamFormatsRequestObs(pagination: PaginationComponentOptions): Observable { - return this.halService.getEndpoint(this.bitstreamFormatsPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryBitstreamformatsResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); - } - public editMetadataSchema(schema: MetadataSchema) { this.store.dispatch(new MetadataRegistryEditSchemaAction(schema)); } + /** + * Method to cancel editing a metadata schema, dispatches a cancel schema action + */ public cancelEditMetadataSchema() { this.store.dispatch(new MetadataRegistryCancelSchemaAction()); } + /** + * Method to retrieve the metadata schema that are currently being edited + */ public getActiveMetadataSchema(): Observable { return this.store.pipe(select(editMetadataSchemaSelector)); } + /** + * Method to select a metadata schema, dispatches a select schema action + * @param schema The schema that's being selected + */ public selectMetadataSchema(schema: MetadataSchema) { - this.store.dispatch(new MetadataRegistrySelectSchemaAction(schema)) + this.store.dispatch(new MetadataRegistrySelectSchemaAction(schema)); } + /** + * Method to deselect a metadata schema, dispatches a deselect schema action + * @param schema The schema that's it being deselected + */ public deselectMetadataSchema(schema: MetadataSchema) { - this.store.dispatch(new MetadataRegistryDeselectSchemaAction(schema)) + this.store.dispatch(new MetadataRegistryDeselectSchemaAction(schema)); } + /** + * Method to deselect all currently selected metadata schema, dispatches a deselect all schema action + */ public deselectAllMetadataSchema() { - this.store.dispatch(new MetadataRegistryDeselectAllSchemaAction()) + this.store.dispatch(new MetadataRegistryDeselectAllSchemaAction()); } + /** + * Method to retrieve the metadata schemas that are currently selected + */ public getSelectedMetadataSchemas(): Observable { return this.store.pipe(select(selectedMetadataSchemasSelector)); } - + /** + * Method to start editing a metadata field, dispatches an edit field action + * @param field The field that's being edited + */ public editMetadataField(field: MetadataField) { this.store.dispatch(new MetadataRegistryEditFieldAction(field)); } + /** + * Method to cancel editing a metadata field, dispatches a cancel field action + */ public cancelEditMetadataField() { this.store.dispatch(new MetadataRegistryCancelFieldAction()); } - + /** + * Method to retrieve the metadata field that are currently being edited + */ public getActiveMetadataField(): Observable { return this.store.pipe(select(editMetadataFieldSelector)); } - + /** + * Method to select a metadata field, dispatches a select field action + * @param field The field that's being selected + */ public selectMetadataField(field: MetadataField) { - this.store.dispatch(new MetadataRegistrySelectFieldAction(field)) + this.store.dispatch(new MetadataRegistrySelectFieldAction(field)); } - + /** + * Method to deselect a metadata field, dispatches a deselect field action + * @param field The field that's it being deselected + */ public deselectMetadataField(field: MetadataField) { - this.store.dispatch(new MetadataRegistryDeselectFieldAction(field)) + this.store.dispatch(new MetadataRegistryDeselectFieldAction(field)); } - + /** + * Method to deselect all currently selected metadata fields, dispatches a deselect all field action + */ public deselectAllMetadataField() { - this.store.dispatch(new MetadataRegistryDeselectAllFieldAction()) + this.store.dispatch(new MetadataRegistryDeselectAllFieldAction()); } + /** + * Method to retrieve the metadata fields that are currently selected + */ public getSelectedMetadataFields(): Observable { return this.store.pipe(select(selectedMetadataFieldsSelector)); } @@ -400,7 +400,7 @@ export class RegistryService { distinctUntilChanged() ); - const serializedSchema = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(ResourceType.MetadataSchema)).serialize(schema as NormalizedMetadataSchema); + const serializedSchema = new DSpaceRESTv2Serializer(getMapsToType(MetadataSchema.type)).serialize(schema as NormalizedMetadataSchema); const request$ = endpoint$.pipe( take(1), @@ -431,7 +431,7 @@ export class RegistryService { this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); } } else { - this.showNotifications(true, isUpdate, false, { prefix: schema.prefix }); + this.showNotifications(true, isUpdate, false, {prefix: schema.prefix}); return response; } }), @@ -444,14 +444,21 @@ export class RegistryService { ); } + /** + * Method to delete a metadata schema + * @param id The id of the metadata schema to delete + */ public deleteMetadataSchema(id: number): Observable { return this.delete(this.metadataSchemasPath, id); } + /** + * Method that clears a cached metadata schema request and returns its REST url + */ public clearMetadataSchemaRequests(): Observable { return this.halService.getEndpoint(this.metadataSchemasPath).pipe( tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ) + ); } /** @@ -501,7 +508,7 @@ export class RegistryService { } } else { const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`; - this.showNotifications(true, isUpdate, true, { field: fieldString }); + this.showNotifications(true, isUpdate, true, {field: fieldString}); return response; } }), @@ -514,14 +521,20 @@ export class RegistryService { ); } + /** + * Method to delete a metadata field + * @param id The id of the metadata field to delete + */ public deleteMetadataField(id: number): Observable { return this.delete(this.metadataFieldsPath, id); } - + /** + * Method that clears a cached metadata field request and returns its REST url + */ public clearMetadataFieldRequests(): Observable { return this.halService.getEndpoint(this.metadataFieldsPath).pipe( tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ) + ); } private delete(path: string, id: number): Observable { @@ -557,9 +570,9 @@ export class RegistryService { ); messages.subscribe(([head, content]) => { if (success) { - this.notificationsService.success(head, content) + this.notificationsService.success(head, content); } else { - this.notificationsService.error(head, content) + this.notificationsService.error(head, content); } }); } diff --git a/src/app/shared/services/api.service.ts b/src/app/core/services/api.service.ts similarity index 100% rename from src/app/shared/services/api.service.ts rename to src/app/core/services/api.service.ts diff --git a/src/app/shared/services/client-cookie.service.ts b/src/app/core/services/client-cookie.service.ts similarity index 100% rename from src/app/shared/services/client-cookie.service.ts rename to src/app/core/services/client-cookie.service.ts diff --git a/src/app/shared/services/cookie.service.spec.ts b/src/app/core/services/cookie.service.spec.ts similarity index 100% rename from src/app/shared/services/cookie.service.spec.ts rename to src/app/core/services/cookie.service.spec.ts diff --git a/src/app/shared/services/cookie.service.ts b/src/app/core/services/cookie.service.ts similarity index 100% rename from src/app/shared/services/cookie.service.ts rename to src/app/core/services/cookie.service.ts diff --git a/src/app/shared/services/route.actions.ts b/src/app/core/services/route.actions.ts similarity index 100% rename from src/app/shared/services/route.actions.ts rename to src/app/core/services/route.actions.ts diff --git a/src/app/shared/services/route.effects.ts b/src/app/core/services/route.effects.ts similarity index 100% rename from src/app/shared/services/route.effects.ts rename to src/app/core/services/route.effects.ts diff --git a/src/app/shared/services/route.reducer.ts b/src/app/core/services/route.reducer.ts similarity index 100% rename from src/app/shared/services/route.reducer.ts rename to src/app/core/services/route.reducer.ts diff --git a/src/app/shared/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts similarity index 97% rename from src/app/shared/services/route.service.spec.ts rename to src/app/core/services/route.service.spec.ts index c6003521a7..ae31f28384 100644 --- a/src/app/shared/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -6,9 +6,9 @@ import { Store } from '@ngrx/store'; import { getTestScheduler, hot } from 'jasmine-marbles'; import { RouteService } from './route.service'; -import { MockRouter } from '../mocks/mock-router'; +import { MockRouter } from '../../shared/mocks/mock-router'; import { TestScheduler } from 'rxjs/testing'; -import { AddUrlToHistoryAction } from '../history/history.actions'; +import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; describe('RouteService', () => { let scheduler: TestScheduler; diff --git a/src/app/shared/services/route.service.ts b/src/app/core/services/route.service.ts similarity index 95% rename from src/app/shared/services/route.service.ts rename to src/app/core/services/route.service.ts index dc626484c1..65aa858945 100644 --- a/src/app/shared/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -12,12 +12,12 @@ import { combineLatest, Observable } from 'rxjs'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { isEqual } from 'lodash'; -import { AddUrlToHistoryAction } from '../history/history.actions'; -import { historySelector } from '../history/selectors'; +import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; +import { historySelector } from '../../shared/history/selectors'; import { SetParametersAction, SetQueryParametersAction } from './route.actions'; -import { CoreState } from '../../core/core.reducers'; -import { hasValue } from '../empty.util'; -import { coreSelector } from '../../core/core.selectors'; +import { CoreState } from '../core.reducers'; +import { hasValue } from '../../shared/empty.util'; +import { coreSelector } from '../core.selectors'; /** * Selector to select all route parameters from the store diff --git a/src/app/shared/services/server-cookie.service.ts b/src/app/core/services/server-cookie.service.ts similarity index 100% rename from src/app/shared/services/server-cookie.service.ts rename to src/app/core/services/server-cookie.service.ts diff --git a/src/app/shared/services/server-response.service.ts b/src/app/core/services/server-response.service.ts similarity index 100% rename from src/app/shared/services/server-response.service.ts rename to src/app/core/services/server-response.service.ts diff --git a/src/app/shared/services/window.service.ts b/src/app/core/services/window.service.ts similarity index 100% rename from src/app/shared/services/window.service.ts rename to src/app/core/services/window.service.ts diff --git a/src/app/core/shared/bitstream-format-support-level.ts b/src/app/core/shared/bitstream-format-support-level.ts new file mode 100644 index 0000000000..d92aac7708 --- /dev/null +++ b/src/app/core/shared/bitstream-format-support-level.ts @@ -0,0 +1,5 @@ +export enum BitstreamFormatSupportLevel { + Known = 'KNOWN', + Unknown = 'UNKNOWN', + Supported = 'SUPPORTED' +} diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts index 9af345e607..0e1279e978 100644 --- a/src/app/core/shared/bitstream-format.model.ts +++ b/src/app/core/shared/bitstream-format.model.ts @@ -1,12 +1,14 @@ - -import { CacheableObject } from '../cache/object-cache.reducer'; +import { CacheableObject, TypedObject } from '../cache/object-cache.reducer'; import { ResourceType } from './resource-type'; +import { BitstreamFormatSupportLevel } from './bitstream-format-support-level'; /** * Model class for a Bitstream Format */ export class BitstreamFormat implements CacheableObject { + static type = new ResourceType('bitstreamformat'); + bitstreamformat /** * Short description of this Bitstream Format */ @@ -25,7 +27,7 @@ export class BitstreamFormat implements CacheableObject { /** * The level of support the system offers for this Bitstream Format */ - supportLevel: number; + supportLevel: BitstreamFormatSupportLevel; /** * True if the Bitstream Format is used to store system information, rather than the content of items in the system @@ -35,21 +37,23 @@ export class BitstreamFormat implements CacheableObject { /** * String representing this Bitstream Format's file extension */ - extensions: string; + extensions: string[]; /** * The link to the rest endpoint where this Bitstream Format can be found */ self: string; - /** - * A ResourceType representing the kind of Object of this BitstreamFormat - */ - type: ResourceType; - /** * Universally unique identifier for this Bitstream Format */ uuid: string; + /** + * Identifier for this Bitstream Format + * Note that this ID is unique for bitstream formats, + * but might not be unique across different object types + */ + id: string; + } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 794282e867..887f7d0843 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -3,8 +3,10 @@ import { RemoteData } from '../data/remote-data'; import { Item } from './item.model'; import { BitstreamFormat } from './bitstream-format.model'; import { Observable } from 'rxjs'; +import { ResourceType } from './resource-type'; export class Bitstream extends DSpaceObject { + static type = new ResourceType('bitstream'); /** * The size of this bitstream in bytes @@ -40,5 +42,4 @@ export class Bitstream extends DSpaceObject { * The URL to retrieve this Bitstream's file */ content: string; - } diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index 05263858c6..9fafe7e321 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -1,7 +1,11 @@ import { autoserialize, autoserializeAs } from 'cerialize'; import { SortOption } from './sort-option.model'; +import { ResourceType } from './resource-type'; +import { TypedObject } from '../cache/object-cache.reducer'; + +export class BrowseDefinition implements TypedObject { + static type = new ResourceType('browse'); -export class BrowseDefinition { @autoserialize id: string; @@ -14,9 +18,6 @@ export class BrowseDefinition { @autoserializeAs('order') defaultSortOrder: string; - @autoserialize - type: string; - @autoserializeAs('metadata') metadataKeys: string[]; diff --git a/src/app/core/shared/browse-entry.model.ts b/src/app/core/shared/browse-entry.model.ts index 932c6946d1..d5d4093c81 100644 --- a/src/app/core/shared/browse-entry.model.ts +++ b/src/app/core/shared/browse-entry.model.ts @@ -1,21 +1,31 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { TypedObject } from '../cache/object-cache.reducer'; +import { ResourceType } from './resource-type'; -export class BrowseEntry implements ListableObject { +/** + * Class object representing a browse entry + * This class is not normalized because browse entries do not have self links + */ +export class BrowseEntry implements ListableObject, TypedObject { + static type = new ResourceType('browseEntry'); - @autoserialize - type: string; - - @autoserialize + /** + * The authority string of this browse entry + */ authority: string; - @autoserialize + /** + * The value of this browse entry + */ value: string; - @autoserializeAs('valueLang') + /** + * The language of the value of this browse entry + */ language: string; - @autoserialize + /** + * The count of this browse entry + */ count: number; - } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 3f5b5df877..9b00f6efa0 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -3,8 +3,11 @@ import { Bitstream } from './bitstream.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs'; +import { ResourceType } from './resource-type'; export class Bundle extends DSpaceObject { + static type = new ResourceType('bundle'); + /** * The primary bitstream of this Bundle */ diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 0471d1fbbb..642fe50736 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -6,8 +6,10 @@ import { Observable } from 'rxjs'; import { License } from './license.model'; import { ResourcePolicy } from './resource-policy.model'; import { PaginatedList } from '../data/paginated-list'; +import { ResourceType } from './resource-type'; export class Collection extends DSpaceObject { + static type = new ResourceType('collection'); /** * A string representing the unique handle of this Collection @@ -80,5 +82,4 @@ export class Collection extends DSpaceObject { owner: Observable>; items: Observable>; - } diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index c4e703fd7f..b61ddfd7f9 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -4,8 +4,10 @@ import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs'; import { PaginatedList } from '../data/paginated-list'; +import { ResourceType } from './resource-type'; export class Community extends DSpaceObject { + static type = new ResourceType('community'); /** * A string representing the unique handle of this Community diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 063398b339..26f76c5ce2 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,18 +1,26 @@ import { Observable } from 'rxjs'; -import { MetadataMap, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models'; +import { + MetadataMap, + MetadataValue, + MetadataValueFilter, + MetadatumViewModel +} from './metadata.models'; import { Metadata } from './metadata.utils'; -import { isUndefined } from '../../shared/empty.util'; +import { hasNoValue, isUndefined } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { RemoteData } from '../data/remote-data'; -import { ResourceType } from './resource-type'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { hasNoValue } from '../../shared/empty.util'; +import { ResourceType } from './resource-type'; /** * An abstract model class for a DSpaceObject. */ export class DSpaceObject implements CacheableObject, ListableObject { + /** + * A string representing the kind of DSpaceObject, e.g. community, item, … + */ + static type = new ResourceType('dspaceobject'); private _name: string; @@ -28,11 +36,6 @@ export class DSpaceObject implements CacheableObject, ListableObject { */ uuid: string; - /** - * A string representing the kind of DSpaceObject, e.g. community, item, … - */ - type: ResourceType; - /** * The name for this DSpaceObject */ diff --git a/src/app/core/shared/item-relationships/item-type.model.ts b/src/app/core/shared/item-relationships/item-type.model.ts index e4f98ab653..2635f154a8 100644 --- a/src/app/core/shared/item-relationships/item-type.model.ts +++ b/src/app/core/shared/item-relationships/item-type.model.ts @@ -5,6 +5,8 @@ import { ResourceType } from '../resource-type'; * Describes a type of Item */ export class ItemType implements CacheableObject { + static type = new ResourceType('entitytype'); + /** * The identifier of this ItemType */ @@ -15,11 +17,6 @@ export class ItemType implements CacheableObject { */ self: string; - /** - * The type of Resource this is - */ - type: ResourceType; - /** * The universally unique identifier of this ItemType */ diff --git a/src/app/core/shared/item-relationships/relationship-type.model.ts b/src/app/core/shared/item-relationships/relationship-type.model.ts index 404d8cdb4b..06ac94b041 100644 --- a/src/app/core/shared/item-relationships/relationship-type.model.ts +++ b/src/app/core/shared/item-relationships/relationship-type.model.ts @@ -8,16 +8,13 @@ import { ItemType } from './item-type.model'; * Describes a type of Relationship between multiple possible Items */ export class RelationshipType implements CacheableObject { + static type = new ResourceType('relationshiptype'); + /** * The link to the rest endpoint where this object can be found */ self: string; - /** - * The type of Resource this is - */ - type: ResourceType; - /** * The label that describes this RelationshipType */ @@ -36,7 +33,7 @@ export class RelationshipType implements CacheableObject { /** * The label that describes the Relation to the left of this RelationshipType */ - leftLabel: string; + leftwardType: string; /** * The maximum amount of Relationships allowed to the left of this RelationshipType @@ -51,7 +48,7 @@ export class RelationshipType implements CacheableObject { /** * The label that describes the Relation to the right of this RelationshipType */ - rightLabel: string; + rightwardType: string; /** * The maximum amount of Relationships allowed to the right of this RelationshipType diff --git a/src/app/core/shared/item-relationships/relationship.model.ts b/src/app/core/shared/item-relationships/relationship.model.ts index 63fe7987ab..9ab9751489 100644 --- a/src/app/core/shared/item-relationships/relationship.model.ts +++ b/src/app/core/shared/item-relationships/relationship.model.ts @@ -9,16 +9,13 @@ import { Item } from '../item.model'; * Describes a Relationship between two Items */ export class Relationship implements CacheableObject { + static type = new ResourceType('relationship'); + /** * The link to the rest endpoint where this object can be found */ self: string; - /** - * The type of Resource this is - */ - type: ResourceType; - /** * The universally unique identifier of this Relationship */ diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index 2e5388dc4d..f20fb3299a 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -1,10 +1,10 @@ import { Observable, of as observableOf } from 'rxjs'; import { Item } from './item.model'; -import { RemoteData } from '../data/remote-data'; import { Bitstream } from './bitstream.model'; import { isEmpty } from '../../shared/empty.util'; import { first, map } from 'rxjs/operators'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; describe('Item', () => { @@ -32,12 +32,11 @@ describe('Item', () => { content: bitstream2Path }]; - remoteDataThumbnail = createRemoteDataObject(thumbnail); - remoteDataFiles = createRemoteDataObject(bitstreams); - remoteDataAll = createRemoteDataObject([...bitstreams, thumbnail]); + remoteDataThumbnail = createSuccessfulRemoteDataObject$(thumbnail); + remoteDataFiles = createSuccessfulRemoteDataObject$(bitstreams); + remoteDataAll = createSuccessfulRemoteDataObject$([...bitstreams, thumbnail]); // Create Bundles - const bundles = [ { @@ -51,7 +50,6 @@ describe('Item', () => { }]; item = Object.assign(new Item(), { bitstreams: remoteDataAll }); - }); it('should return the bitstreams related to this item with the specified bundle name', () => { @@ -99,16 +97,4 @@ describe('Item', () => { }); }); - }); - -function createRemoteDataObject(object: any) { - return observableOf(new RemoteData( - false, - false, - true, - undefined, - object - )); - -} diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 839103b9f5..a3e625c022 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -5,11 +5,14 @@ import { DSpaceObject } from './dspace-object.model'; import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; import { Bitstream } from './bitstream.model'; -import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { PaginatedList } from '../data/paginated-list'; import { Relationship } from './item-relationships/relationship.model'; +import { ResourceType } from './resource-type'; +import { getSucceededRemoteData } from './operators'; export class Item extends DSpaceObject { + static type = new ResourceType('item'); /** * A string representing the unique handle of this Item @@ -95,7 +98,7 @@ export class Item extends DSpaceObject { */ getBitstreamsByBundleName(bundleName: string): Observable { return this.bitstreams.pipe( - filter((rd: RemoteData>) => !rd.isResponsePending && isNotUndefined(rd.payload)), + getSucceededRemoteData(), map((rd: RemoteData>) => rd.payload.page), filter((bitstreams: Bitstream[]) => hasValue(bitstreams)), take(1), diff --git a/src/app/core/shared/license.model.ts b/src/app/core/shared/license.model.ts index a04422242a..fa49e1f430 100644 --- a/src/app/core/shared/license.model.ts +++ b/src/app/core/shared/license.model.ts @@ -1,6 +1,8 @@ import { DSpaceObject } from './dspace-object.model'; +import { ResourceType } from './resource-type'; export class License extends DSpaceObject { + static type = new ResourceType('license'); /** * Is the license custom? diff --git a/src/app/core/shared/normalized-browse-entry.model.ts b/src/app/core/shared/normalized-browse-entry.model.ts new file mode 100644 index 0000000000..949758cb67 --- /dev/null +++ b/src/app/core/shared/normalized-browse-entry.model.ts @@ -0,0 +1,36 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { BrowseEntry } from './browse-entry.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { mapsTo } from '../cache/builders/build-decorators'; + +/** + * Class object representing a browse entry + * This class is not normalized because browse entries do not have self links + */ +@mapsTo(BrowseEntry) +@inheritSerialization(NormalizedObject) +export class NormalizedBrowseEntry extends NormalizedObject { + /** + * The authority string of this browse entry + */ + @autoserialize + authority: string; + + /** + * The value of this browse entry + */ + @autoserialize + value: string; + + /** + * The language of the value of this browse entry + */ + @autoserializeAs('valueLang') + language: string; + + /** + * The count of this browse entry + */ + @autoserialize + count: number; +} diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index 564b0ff319..56b5d5db7e 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -18,6 +18,10 @@ import { import { RemoteData } from '../data/remote-data'; import { RemoteDataError } from '../data/remote-data-error'; import { of as observableOf } from 'rxjs'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject +} from '../../shared/testing/utils'; describe('Core Module - RxJS Operators', () => { let scheduler: TestScheduler; @@ -180,17 +184,17 @@ describe('Core Module - RxJS Operators', () => { describe('getSucceededRemoteData', () => { it('should return the first() hasSucceeded RemoteData Observable', () => { const testRD = { - a: new RemoteData(false, false, true, null, undefined), - b: new RemoteData(false, false, false, null, 'b'), + a: createSuccessfulRemoteDataObject(undefined), + b: createFailedRemoteDataObject( 'b'), c: new RemoteData(false, false, undefined, null, 'c'), - d: new RemoteData(false, false, true, null, 'd'), - e: new RemoteData(false, false, true, null, 'e'), + d: createSuccessfulRemoteDataObject('d'), + e: createSuccessfulRemoteDataObject('e'), }; const source = hot('abcde', testRD); const result = source.pipe(getSucceededRemoteData()); result.subscribe((value) => expect(value) - .toEqual(new RemoteData(false, false, true, null, 'd'))); + .toEqual(createSuccessfulRemoteDataObject('d'))); }); }); @@ -202,21 +206,21 @@ describe('Core Module - RxJS Operators', () => { }); it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error', () => { - const testRD = new RemoteData(false, false, false, new RemoteDataError(404, 'Not Found', 'Object was not found'), undefined); + const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(404, 'Not Found', 'Object was not found')); observableOf(testRD).pipe(redirectToPageNotFoundOn404(router)).subscribe(); expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true }); }); it('should not call navigateByUrl to a 404 page, when the remote data contains another error than a 404', () => { - const testRD = new RemoteData(false, false, false, new RemoteDataError(500, 'Server Error', 'Something went wrong'), undefined); + const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(500, 'Server Error', 'Something went wrong')); observableOf(testRD).pipe(redirectToPageNotFoundOn404(router)).subscribe(); expect(router.navigateByUrl).not.toHaveBeenCalled(); }); it('should not call navigateByUrl to a 404 page, when the remote data contains no error', () => { - const testRD = new RemoteData(false, false, true, null, undefined); + const testRD = createSuccessfulRemoteDataObject(undefined); observableOf(testRD).pipe(redirectToPageNotFoundOn404(router)).subscribe(); expect(router.navigateByUrl).not.toHaveBeenCalled(); @@ -242,11 +246,11 @@ describe('Core Module - RxJS Operators', () => { describe('getAllSucceededRemoteData', () => { it('should return all hasSucceeded RemoteData Observables', () => { const testRD = { - a: new RemoteData(false, false, true, null, undefined), - b: new RemoteData(false, false, false, null, 'b'), + a: createSuccessfulRemoteDataObject(undefined), + b: createFailedRemoteDataObject('b'), c: new RemoteData(false, false, undefined, null, 'c'), - d: new RemoteData(false, false, true, null, 'd'), - e: new RemoteData(false, false, true, null, 'e'), + d: createSuccessfulRemoteDataObject('d'), + e: createSuccessfulRemoteDataObject('e'), }; const source = hot('abcde', testRD); const result = source.pipe(getAllSucceededRemoteData()); diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index ae46691e39..d46c688e68 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -91,7 +91,7 @@ export const toDSpaceObjectListRD = () => source.pipe( filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { - const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult) => searchResult.indexableObject); + const dsoPage: T[] = rd.payload.page.filter((result) => hasValue(result)).map((searchResult: SearchResult) => searchResult.indexableObject); const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList; return Object.assign(rd, { payload: payload }); }) diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts index ee3d5293f5..a80446a369 100644 --- a/src/app/core/shared/resource-policy.model.ts +++ b/src/app/core/shared/resource-policy.model.ts @@ -1,12 +1,13 @@ import { CacheableObject } from '../cache/object-cache.reducer'; import { ResourceType } from './resource-type'; -import { Group } from '../eperson/models/group.model'; import { ActionType } from '../cache/models/action-type.model'; /** * Model class for a Resource Policy */ export class ResourcePolicy implements CacheableObject { + static type = new ResourceType('resourcePolicy'); + /** * The action that is allowed by this Resource Policy */ @@ -27,11 +28,6 @@ export class ResourcePolicy implements CacheableObject { */ self: string; - /** - * A ResourceType representing the kind of Object of this ResourcePolicy - */ - type: ResourceType; - /** * The universally unique identifier for this Resource Policy */ diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index bdcc3a52f6..657837fbec 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -1,28 +1,7 @@ -export enum ResourceType { - DSpaceObject = 'dspaceobject', - Bundle = 'bundle', - Bitstream = 'bitstream', - BitstreamFormat = 'bitstreamformat', - Item = 'item', - Collection = 'collection', - Community = 'community', - EPerson = 'eperson', - Group = 'group', - ResourcePolicy = 'resourcePolicy', - MetadataSchema = 'metadataschema', - MetadataField = 'metadatafield', - Relationship = 'relationship', - RelationshipType = 'relationshiptype', - ItemType = 'entitytype', - License = 'license', - Workflowitem = 'workflowitem', - Workspaceitem = 'workspaceitem', - SubmissionDefinitions = 'submissiondefinitions', - SubmissionDefinition = 'submissiondefinition', - SubmissionForm = 'submissionform', - SubmissionForms = 'submissionforms', - SubmissionSections = 'submissionsections', - SubmissionSection = 'submissionsection', - ClaimedTask = 'claimedtask', - PoolTask = 'pooltask' +/** + * Class that represents the type of an object as returned by the REST server + */ +export class ResourceType { + constructor(public value: string) { + } } diff --git a/src/app/core/submission/models/normalized-workflowitem.model.ts b/src/app/core/submission/models/normalized-workflowitem.model.ts index a3fa8992a2..e96024b4ae 100644 --- a/src/app/core/submission/models/normalized-workflowitem.model.ts +++ b/src/app/core/submission/models/normalized-workflowitem.model.ts @@ -1,43 +1,46 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { Workflowitem } from './workflowitem.model'; +import { WorkflowItem } from './workflowitem.model'; import { NormalizedSubmissionObject } from './normalized-submission-object.model'; -import { ResourceType } from '../../shared/resource-type'; +import { Collection } from '../../shared/collection.model'; +import { Item } from '../../shared/item.model'; +import { SubmissionDefinitionsModel } from '../../config/models/config-submission-definitions.model'; +import { EPerson } from '../../eperson/models/eperson.model'; /** * An model class for a NormalizedWorkflowItem. */ -@mapsTo(Workflowitem) +@mapsTo(WorkflowItem) @inheritSerialization(NormalizedSubmissionObject) -export class NormalizedWorkflowItem extends NormalizedSubmissionObject { +export class NormalizedWorkflowItem extends NormalizedSubmissionObject { /** * The collection this workflowitem belonging to */ @autoserialize - @relationship(ResourceType.Collection, false) + @relationship(Collection, false) collection: string; /** * The item created with this workflowitem */ @autoserialize - @relationship(ResourceType.Item, false) + @relationship(Item, false) item: string; /** * The configuration object that define this workflowitem */ @autoserialize - @relationship(ResourceType.SubmissionDefinition, false) + @relationship(SubmissionDefinitionsModel, false) submissionDefinition: string; /** * The EPerson who submit this workflowitem */ @autoserialize - @relationship(ResourceType.EPerson, false) + @relationship(EPerson, false) submitter: string; } diff --git a/src/app/core/submission/models/normalized-workspaceitem.model.ts b/src/app/core/submission/models/normalized-workspaceitem.model.ts index 7c15925c98..4275420191 100644 --- a/src/app/core/submission/models/normalized-workspaceitem.model.ts +++ b/src/app/core/submission/models/normalized-workspaceitem.model.ts @@ -1,45 +1,47 @@ import { autoserialize, inheritSerialization } from 'cerialize'; -import { Workspaceitem } from './workspaceitem.model'; +import { WorkspaceItem } from './workspaceitem.model'; import { NormalizedSubmissionObject } from './normalized-submission-object.model'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; -import { ResourceType } from '../../shared/resource-type'; -import { Workflowitem } from './workflowitem.model'; +import { Item } from '../../shared/item.model'; +import { Collection } from '../../shared/collection.model'; +import { SubmissionDefinitionModel } from '../../config/models/config-submission-definition.model'; +import { EPerson } from '../../eperson/models/eperson.model'; /** * An model class for a NormalizedWorkspaceItem. */ -@mapsTo(Workspaceitem) +@mapsTo(WorkspaceItem) @inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedSubmissionObject) -export class NormalizedWorkspaceItem extends NormalizedSubmissionObject { +export class NormalizedWorkspaceItem extends NormalizedSubmissionObject { /** * The collection this workspaceitem belonging to */ @autoserialize - @relationship(ResourceType.Collection, false) + @relationship(Collection, false) collection: string; /** * The item created with this workspaceitem */ @autoserialize - @relationship(ResourceType.Item, false) + @relationship(Item, false) item: string; /** * The configuration object that define this workspaceitem */ @autoserialize - @relationship(ResourceType.SubmissionDefinition, false) + @relationship(SubmissionDefinitionModel, false) submissionDefinition: string; /** * The EPerson who submit this workspaceitem */ @autoserialize - @relationship(ResourceType.EPerson, false) + @relationship(EPerson, false) submitter: string; } diff --git a/src/app/core/submission/models/workflowitem.model.ts b/src/app/core/submission/models/workflowitem.model.ts index f1a0467f43..4cfc4d7fa1 100644 --- a/src/app/core/submission/models/workflowitem.model.ts +++ b/src/app/core/submission/models/workflowitem.model.ts @@ -1,7 +1,9 @@ -import { Workspaceitem } from './workspaceitem.model'; +import { WorkspaceItem } from './workspaceitem.model'; +import { ResourceType } from '../../shared/resource-type'; /** - * A model class for a Workflowitem. + * A model class for a WorkflowItem. */ -export class Workflowitem extends Workspaceitem { +export class WorkflowItem extends WorkspaceItem { + static type = new ResourceType('workflowitem'); } diff --git a/src/app/core/submission/models/workspaceitem.model.ts b/src/app/core/submission/models/workspaceitem.model.ts index 6548191ba2..c4bb5b7520 100644 --- a/src/app/core/submission/models/workspaceitem.model.ts +++ b/src/app/core/submission/models/workspaceitem.model.ts @@ -1,8 +1,10 @@ import { SubmissionObject } from './submission-object.model'; +import { ResourceType } from '../../shared/resource-type'; /** - * A model class for a Workspaceitem. + * A model class for a WorkspaceItem. */ -export class Workspaceitem extends SubmissionObject { +export class WorkspaceItem extends SubmissionObject { + static type = new ResourceType('workspaceitem'); } diff --git a/src/app/core/submission/submission-resource-type.ts b/src/app/core/submission/submission-resource-type.ts deleted file mode 100644 index f5b8e2c423..0000000000 --- a/src/app/core/submission/submission-resource-type.ts +++ /dev/null @@ -1,21 +0,0 @@ -export enum SubmissionResourceType { - Bundle = 'bundle', - Bitstream = 'bitstream', - BitstreamFormat = 'bitstreamformat', - Item = 'item', - Collection = 'collection', - Community = 'community', - ResourcePolicy = 'resourcePolicy', - License = 'license', - EPerson = 'eperson', - Group = 'group', - WorkspaceItem = 'workspaceitem', - WorkflowItem = 'workflowitem', - SubmissionDefinitions = 'submissiondefinitions', - SubmissionDefinition = 'submissiondefinition', - SubmissionForm = 'submissionform', - SubmissionForms = 'submissionforms', - SubmissionSections = 'submissionsections', - SubmissionSection = 'submissionsection', - Authority = 'authority' -} diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index 21135be463..de7d683d91 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -10,12 +10,10 @@ import { BaseResponseParsingService } from '../data/base-response-parsing.servic import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { SubmissionResourceType } from './submission-resource-type'; import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model'; import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; import { FormFieldMetadataValueObject } from '../../shared/form/builder/models/form-field-metadata-value.model'; import { SubmissionObject } from './models/submission-object.model'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; /** * Export a function to check if object has same properties of FormFieldMetadataValueObject @@ -75,7 +73,6 @@ export function normalizeSectionData(obj: any, objIndex?: number) { @Injectable() export class SubmissionResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = NormalizedObjectFactory; protected toCache = false; constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @@ -94,7 +91,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && this.isSuccessStatus(data.statusCode)) { - const dataDefinition = this.processResponse(data.payload, request.href); + const dataDefinition = this.processResponse(data.payload, request.href); return new SubmissionSuccessResponse(dataDefinition, data.statusCode, data.statusText, this.processPageInfo(data.payload)); } else if (isEmpty(data.payload) && this.isSuccessStatus(data.statusCode)) { return new SubmissionSuccessResponse(null, data.statusCode, data.statusText); @@ -115,8 +112,8 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService * @param {string} requestHref * @returns {any[]} */ - protected processResponse(data: any, requestHref: string): any[] { - const dataDefinition = this.process(data, requestHref); + protected processResponse(data: any, requestHref: string): any[] { + const dataDefinition = this.process(data, requestHref); const normalizedDefinition = Array.of(); const processedList = Array.isArray(dataDefinition) ? dataDefinition : Array.of(dataDefinition); @@ -131,7 +128,10 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService // Iterate over all workspaceitem's sections Object.keys(item.sections) .forEach((sectionId) => { - if (typeof item.sections[sectionId] === 'object' && isNotEmpty(item.sections[sectionId])) { + if (typeof item.sections[sectionId] === 'object' && (isNotEmpty(item.sections[sectionId]) && + // When Upload section is disabled, add to submission only if there are files + (!item.sections[sectionId].hasOwnProperty('files') || isNotEmpty((item.sections[sectionId] as any).files)))) { + const normalizedSectionData = Object.create({}); // Iterate over all sections property Object.keys(item.sections[sectionId]) diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index e739a62e81..fd769745da 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -6,7 +6,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; -import { Workflowitem } from './models/workflowitem.model'; +import { WorkflowItem } from './models/workflowitem.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindAllOptions } from '../data/request.models'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; @@ -18,12 +18,12 @@ import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; * A service that provides methods to make REST requests with workflowitems endpoint. */ @Injectable() -export class WorkflowitemDataService extends DataService { +export class WorkflowItemDataService extends DataService { protected linkPath = 'workflowitems'; protected forceBypassCache = true; constructor( - protected comparator: DSOChangeAnalyzer, + protected comparator: DSOChangeAnalyzer, protected dataBuildService: NormalizedObjectBuildService, protected halService: HALEndpointService, protected http: HttpClient, diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 3bb3eb1ee8..de671d57c7 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -6,24 +6,24 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; -import { Workspaceitem } from './models/workspaceitem.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindAllOptions } from '../data/request.models'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { WorkspaceItem } from './models/workspaceitem.model'; /** * A service that provides methods to make REST requests with workspaceitems endpoint. */ @Injectable() -export class WorkspaceitemDataService extends DataService { +export class WorkspaceitemDataService extends DataService { protected linkPath = 'workspaceitems'; protected forceBypassCache = true; constructor( - protected comparator: DSOChangeAnalyzer, + protected comparator: DSOChangeAnalyzer, protected dataBuildService: NormalizedObjectBuildService, protected halService: HALEndpointService, protected http: HttpClient, diff --git a/src/app/core/tasks/models/claimed-task-object.model.ts b/src/app/core/tasks/models/claimed-task-object.model.ts index 212e75ed95..2f427f586f 100644 --- a/src/app/core/tasks/models/claimed-task-object.model.ts +++ b/src/app/core/tasks/models/claimed-task-object.model.ts @@ -1,8 +1,9 @@ import { TaskObject } from './task-object.model'; +import { ResourceType } from '../../shared/resource-type'; /** * A model class for a ClaimedTask. */ export class ClaimedTask extends TaskObject { - + static type = new ResourceType('claimedtask'); } diff --git a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts index c2c3f12bc4..d43a277f02 100644 --- a/src/app/core/tasks/models/normalized-claimed-task-object.model.ts +++ b/src/app/core/tasks/models/normalized-claimed-task-object.model.ts @@ -2,7 +2,9 @@ import { NormalizedTaskObject } from './normalized-task-object.model'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { autoserialize, inheritSerialization } from 'cerialize'; import { ClaimedTask } from './claimed-task-object.model'; -import { ResourceType } from '../../shared/resource-type'; +import { EPerson } from '../../eperson/models/eperson.model'; +import { Group } from '../../eperson/models/group.model'; +import { WorkflowItem } from '../../submission/models/workflowitem.model'; /** * A normalized model class for a ClaimedTask. @@ -10,7 +12,6 @@ import { ResourceType } from '../../shared/resource-type'; @mapsTo(ClaimedTask) @inheritSerialization(NormalizedTaskObject) export class NormalizedClaimedTask extends NormalizedTaskObject { - /** * The task identifier */ @@ -29,11 +30,25 @@ export class NormalizedClaimedTask extends NormalizedTaskObject { @autoserialize action: string; + /** + * The eperson object for this task + */ + @autoserialize + @relationship(EPerson, false) + eperson: string; + + /** + * The group object for this task + */ + @autoserialize + @relationship(Group, false) + group: string; + /** * The workflowitem object whom this task is related */ @autoserialize - @relationship(ResourceType.Workflowitem, false) + @relationship(WorkflowItem, false) workflowitem: string; } diff --git a/src/app/core/tasks/models/normalized-pool-task-object.model.ts b/src/app/core/tasks/models/normalized-pool-task-object.model.ts index 22cda6ff9c..bfc782f182 100644 --- a/src/app/core/tasks/models/normalized-pool-task-object.model.ts +++ b/src/app/core/tasks/models/normalized-pool-task-object.model.ts @@ -2,7 +2,8 @@ import { NormalizedTaskObject } from './normalized-task-object.model'; import { PoolTask } from './pool-task-object.model'; import { autoserialize, inheritSerialization } from 'cerialize'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; +import { Group } from '../../eperson/models/group.model'; +import { WorkflowItem } from '../../submission/models/workflowitem.model'; /** * A normalized model class for a PoolTask. @@ -10,7 +11,6 @@ import { ResourceType } from '../../shared/resource-type'; @mapsTo(PoolTask) @inheritSerialization(NormalizedTaskObject) export class NormalizedPoolTask extends NormalizedTaskObject { - /** * The task identifier */ @@ -29,10 +29,17 @@ export class NormalizedPoolTask extends NormalizedTaskObject { @autoserialize action: string; + /** + * The group object for this task + */ + @autoserialize + @relationship(Group, false) + group: string; + /** * The workflowitem object whom this task is related */ @autoserialize - @relationship(ResourceType.Workflowitem, false) + @relationship(WorkflowItem, false) workflowitem: string; } diff --git a/src/app/core/tasks/models/normalized-task-object.model.ts b/src/app/core/tasks/models/normalized-task-object.model.ts index 52c274e3a8..2c96b95393 100644 --- a/src/app/core/tasks/models/normalized-task-object.model.ts +++ b/src/app/core/tasks/models/normalized-task-object.model.ts @@ -1,16 +1,18 @@ import { autoserialize, inheritSerialization } from 'cerialize'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; -import { ResourceType } from '../../shared/resource-type'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { TaskObject } from './task-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model'; +import { Group } from '../../eperson/models/group.model'; +import { EPerson } from '../../eperson/models/eperson.model'; +import { WorkflowItem } from '../../submission/models/workflowitem.model'; /** * An abstract normalized model class for a TaskObject. */ @mapsTo(TaskObject) @inheritSerialization(NormalizedDSpaceObject) -export abstract class NormalizedTaskObject extends NormalizedDSpaceObject { +export class NormalizedTaskObject extends NormalizedDSpaceObject { /** * The task identifier @@ -30,10 +32,24 @@ export abstract class NormalizedTaskObject extends Norma @autoserialize action: string; + /** + * The eperson object for this task + */ + @autoserialize + @relationship(EPerson, false) + eperson: string; + + /** + * The group object for this task + */ + @autoserialize + @relationship(Group, false) + group: string; + /** * The workflowitem object whom this task is related */ @autoserialize - @relationship(ResourceType.Workflowitem, false) + @relationship(WorkflowItem, false) workflowitem: string; } diff --git a/src/app/core/tasks/models/pool-task-object.model.ts b/src/app/core/tasks/models/pool-task-object.model.ts index 8d98d3e1a5..876b62373d 100644 --- a/src/app/core/tasks/models/pool-task-object.model.ts +++ b/src/app/core/tasks/models/pool-task-object.model.ts @@ -1,8 +1,9 @@ import { TaskObject } from './task-object.model'; +import { ResourceType } from '../../shared/resource-type'; /** * A model class for a PoolTask. */ export class PoolTask extends TaskObject { - + static type = new ResourceType('pooltask'); } diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index 97a1c9f59e..14dbbd7301 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -4,12 +4,16 @@ import { CacheableObject } from '../../cache/object-cache.reducer'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { RemoteData } from '../../data/remote-data'; -import { Workflowitem } from '../../submission/models/workflowitem.model'; +import { WorkflowItem } from '../../submission/models/workflowitem.model'; +import { Group } from '../../eperson/models/group.model'; +import { EPerson } from '../../eperson/models/eperson.model'; +import { ResourceType } from '../../shared/resource-type'; /** * An abstract model class for a TaskObject. */ export class TaskObject extends DSpaceObject implements CacheableObject, ListableObject { + static type = new ResourceType('taskobject'); /** * The task identifier @@ -26,8 +30,18 @@ export class TaskObject extends DSpaceObject implements CacheableObject, Listabl */ action: string; + /** + * The group of this task + */ + eperson: Observable>; + + /** + * The group of this task + */ + group: Observable>; + /** * The workflowitem object whom this task is related */ - workflowitem: Observable> | Workflowitem; + workflowitem: Observable> | WorkflowItem; } diff --git a/src/app/core/tasks/task-response-parsing.service.ts b/src/app/core/tasks/task-response-parsing.service.ts index 7445f9d267..090b67ccbb 100644 --- a/src/app/core/tasks/task-response-parsing.service.ts +++ b/src/app/core/tasks/task-response-parsing.service.ts @@ -8,7 +8,6 @@ import { BaseResponseParsingService } from '../data/base-response-parsing.servic import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { ErrorResponse, RestResponse, TaskResponse } from '../cache/response.models'; /** @@ -17,7 +16,6 @@ import { ErrorResponse, RestResponse, TaskResponse } from '../cache/response.mod @Injectable() export class TaskResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = NormalizedObjectFactory; protected toCache = false; /** diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html new file mode 100644 index 0000000000..3aa79fc70a --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html @@ -0,0 +1,30 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts new file mode 100644 index 0000000000..d13feda406 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts @@ -0,0 +1,50 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalIssueGridElementComponent } from './journal-issue-grid-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'creativework.datePublished': [ + { + language: null, + value: '2015-06-26' + } + ], + 'journal.title': [ + { + language: 'en_US', + value: 'The journal title' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalIssueGridElementComponent', getEntityGridElementTestComponent(JournalIssueGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'journal-title'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts new file mode 100644 index 0000000000..06c27ebacf --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('JournalIssue', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-issue-grid-element', + styleUrls: ['./journal-issue-grid-element.component.scss'], + templateUrl: './journal-issue-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Journal Issue + */ +export class JournalIssueGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html new file mode 100644 index 0000000000..b2b251f550 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html @@ -0,0 +1,30 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts new file mode 100644 index 0000000000..8c854aeb77 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts @@ -0,0 +1,50 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalVolumeGridElementComponent } from './journal-volume-grid-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'creativework.datePublished': [ + { + language: null, + value: '2015-06-26' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'A description for the journal volume' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalVolumeGridElementComponent', getEntityGridElementTestComponent(JournalVolumeGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'description'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts new file mode 100644 index 0000000000..e5183536ef --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('JournalVolume', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-volume-grid-element', + styleUrls: ['./journal-volume-grid-element.component.scss'], + templateUrl: './journal-volume-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Journal Volume + */ +export class JournalVolumeGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html new file mode 100644 index 0000000000..af0739004c --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html @@ -0,0 +1,35 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + {{dso.firstMetadataValue('creativework.editor')}} + + , + {{dso.firstMetadataValue('creativework.publisher')}} + + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts new file mode 100644 index 0000000000..0d0f77842a --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts @@ -0,0 +1,56 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalGridElementComponent } from './journal-grid-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'creativework.editor': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'creativework.publisher': [ + { + language: 'en_US', + value: 'A company' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'This is the description' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalGridElementComponent', getEntityGridElementTestComponent(JournalGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['editor', 'publisher', 'description'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts new file mode 100644 index 0000000000..7f23211538 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('Journal', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-grid-element', + styleUrls: ['./journal-grid-element.component.scss'], + templateUrl: './journal-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Journal + */ +export class JournalGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html index 65a10ec1b7..030a26df39 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html @@ -4,14 +4,14 @@ [innerHTML]="firstMetadataValue('dc.title')"> - - + - - + - diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts index c5757f3d51..24498088cb 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts @@ -20,13 +20,13 @@ const mockItemWithMetadata: Item = Object.assign(new Item(), { value: 'This is just another title' } ], - 'journalvolume.identifier.volume': [ + 'publicationvolume.volumeNumber': [ { language: 'en_US', value: '1234' } ], - 'journalissue.identifier.number': [ + 'publicationissue.issueNumber': [ { language: 'en_US', value: '5678' diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html index 7d7f0cf731..4e6e34d3d6 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html @@ -10,9 +10,9 @@ - - + () diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts index 6ea5c80a5f..15f5424960 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts @@ -26,7 +26,7 @@ const mockItemWithMetadata: Item = Object.assign(new Item(), { value: 'This is just another journal title' } ], - 'journalvolume.identifier.volume': [ + 'publicationvolume.volumeNumber': [ { language: 'en_US', value: '1234' diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html index 32c8074503..0e46e921bb 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html @@ -4,9 +4,9 @@ [innerHTML]="firstMetadataValue('dc.title')"> - - + diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts index ff419148c6..204672dfe9 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts @@ -20,7 +20,7 @@ const mockItemWithMetadata: Item = Object.assign(new Item(), { value: 'This is just another title' } ], - 'journal.identifier.issn': [ + 'creativeworkseries.issn': [ { language: 'en_US', value: '1234' diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index 5d96abb82b..eebd3e03c8 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -7,11 +7,15 @@ + +
@@ -34,11 +38,11 @@ [label]="'relationships.isPublicationOfJournalIssue' | translate">
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts index 0711a67492..00403473a7 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.spec.ts @@ -8,29 +8,30 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; const mockItem: Item = Object.assign(new Item(), { - bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: { - 'journalissue.identifier.number': [ + 'publicationissue.issueNumber': [ { language: 'en_US', value: '1234' } ], - 'journalissue.issuedate': [ + 'creativework.datePublished': [ { language: 'en_US', value: '2018' } ], - 'journalissue.identifier.description': [ + 'dc.description': [ { language: 'en_US', value: 'desc' } ], - 'journalissue.identifier.keyword': [ + 'creativework.keywords': [ { language: 'en_US', value: 'keyword' diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts index a355431100..b584fa3285 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts @@ -4,10 +4,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { - filterRelationsByTypeLabel, - relationsToItems -} from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getRelatedItemsByTypeLabel } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; @rendersItemType('JournalIssue', ItemViewMode.Full) @Component({ @@ -34,12 +31,10 @@ export class JournalIssueComponent extends ItemComponent { if (isNotEmpty(this.resolvedRelsAndTypes$)) { this.volumes$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isJournalVolumeOfIssue'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isJournalVolumeOfIssue') ); this.publications$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isPublicationOfJournalIssue'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isPublicationOfJournalIssue') ); } } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index 18bf1701fc..83626c7ae7 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -7,11 +7,11 @@
@@ -25,7 +25,7 @@ [label]="'relationships.isIssueOf' | translate">
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts index 3beca0c17a..d2b3420b2a 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.spec.ts @@ -8,23 +8,24 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; const mockItem: Item = Object.assign(new Item(), { - bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: { - 'journalvolume.identifier.volume': [ + 'publicationvolume.volumeNumber': [ { language: 'en_US', value: '1234' } ], - 'journalvolume.issuedate': [ + 'creativework.datePublished': [ { language: 'en_US', value: '2018' } ], - 'journalvolume.identifier.description': [ + 'dc.description': [ { language: 'en_US', value: 'desc' diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts index a851bfebbe..66df0b8104 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts @@ -4,10 +4,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { - filterRelationsByTypeLabel, - relationsToItems -} from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getRelatedItemsByTypeLabel } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; @rendersItemType('JournalVolume', ItemViewMode.Full) @Component({ @@ -34,12 +31,10 @@ export class JournalVolumeComponent extends ItemComponent { if (isNotEmpty(this.resolvedRelsAndTypes$)) { this.journals$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isJournalOfVolume'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isJournalOfVolume') ); this.issues$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isIssueOfJournalVolume'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isIssueOfJournalVolume') ); } } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html index 2ab3430256..a82d3c5df6 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html @@ -7,15 +7,15 @@
@@ -25,7 +25,7 @@ [label]="'relationships.isVolumeOf' | translate">
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 4189713cb9..c66c3a2462 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -15,26 +15,27 @@ import { isNotEmpty } from '../../../../shared/empty.util'; import { JournalComponent } from './journal.component'; import { of as observableOf } from 'rxjs'; import { GenericItemPageFieldComponent } from '../../../../+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; let comp: JournalComponent; let fixture: ComponentFixture; const mockItem: Item = Object.assign(new Item(), { - bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: { - 'journal.identifier.issn': [ + 'creativeworkseries.issn': [ { language: 'en_US', value: '1234' } ], - 'journal.publisher': [ + 'creativework.publisher': [ { language: 'en_US', value: 'a publisher' } ], - 'journal.identifier.description': [ + 'dc.description': [ { language: 'en_US', value: 'desc' diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts index 99183651e1..a8f071d78a 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts @@ -4,10 +4,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { - filterRelationsByTypeLabel, - relationsToItems -} from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getRelatedItemsByTypeLabel } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; @rendersItemType('Journal', ItemViewMode.Full) @Component({ @@ -29,8 +26,7 @@ export class JournalComponent extends ItemComponent { if (isNotEmpty(this.resolvedRelsAndTypes$)) { this.volumes$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isVolumeOfJournal'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isVolumeOfJournal') ); } } 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 50ec160650..4033645e1b 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -9,6 +9,9 @@ import { JournalListElementComponent } from './item-list-elements/journal/journa import { JournalIssueListElementComponent } from './item-list-elements/journal-issue/journal-issue-list-element.component'; import { JournalVolumeListElementComponent } from './item-list-elements/journal-volume/journal-volume-list-element.component'; import { TooltipModule } from 'ngx-bootstrap'; +import { JournalIssueGridElementComponent } from './item-grid-elements/journal-issue/journal-issue-grid-element.component'; +import { JournalVolumeGridElementComponent } from './item-grid-elements/journal-volume/journal-volume-grid-element.component'; +import { JournalGridElementComponent } from './item-grid-elements/journal/journal-grid-element.component'; const ENTRY_COMPONENTS = [ JournalComponent, @@ -16,7 +19,10 @@ const ENTRY_COMPONENTS = [ JournalVolumeComponent, JournalListElementComponent, JournalIssueListElementComponent, - JournalVolumeListElementComponent + JournalVolumeListElementComponent, + JournalIssueGridElementComponent, + JournalVolumeGridElementComponent, + JournalGridElementComponent ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html new file mode 100644 index 0000000000..a4765c4e8f --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html @@ -0,0 +1,35 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+

+ + {{dso.firstMetadataValue('organization.address.addressCountry')}} + + , + {{dso.firstMetadataValue('organization.address.addressLocality')}} + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts new file mode 100644 index 0000000000..15c7b75bf5 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts @@ -0,0 +1,56 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { OrgunitGridElementComponent } from './orgunit-grid-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'organization.foundingDate': [ + { + language: null, + value: '2015-06-26' + } + ], + 'organization.address.addressCountry': [ + { + language: 'en_US', + value: 'Belgium' + } + ], + 'organization.address.addressLocality': [ + { + language: 'en_US', + value: 'Brussels' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('OrgunitGridElementComponent', getEntityGridElementTestComponent(OrgunitGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'country', 'city'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts new file mode 100644 index 0000000000..0effc22027 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('OrgUnit', ItemViewMode.Card) +@Component({ + selector: 'ds-orgunit-grid-element', + styleUrls: ['./orgunit-grid-element.component.scss'], + templateUrl: './orgunit-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Organisation Unit + */ +export class OrgunitGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html new file mode 100644 index 0000000000..331c2bd520 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -0,0 +1,30 @@ + +
+ +
+ + +
+
+
+ + +

+
+ +

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts new file mode 100644 index 0000000000..25268261e1 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts @@ -0,0 +1,50 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { PersonGridElementComponent } from './person-grid-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'person.email': [ + { + language: 'en_US', + value: 'Smith-Donald@gmail.com' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Web Developer' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('PersonGridElementComponent', getEntityGridElementTestComponent(PersonGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['email', 'jobtitle'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts new file mode 100644 index 0000000000..bf7b8aa119 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; +import { focusShadow } from '../../../../shared/animations/focus'; + +@rendersItemType('Person', ItemViewMode.Card) +@Component({ + selector: 'ds-person-grid-element', + styleUrls: ['./person-grid-element.component.scss'], + templateUrl: './person-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Person + */ +export class PersonGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html new file mode 100644 index 0000000000..889276b29b --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -0,0 +1,25 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts new file mode 100644 index 0000000000..969912976c --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts @@ -0,0 +1,44 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { ProjectGridElementComponent } from './project-grid-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'The project description' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('ProjectGridElementComponent', getEntityGridElementTestComponent(ProjectGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['description'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts new file mode 100644 index 0000000000..15d525fcf2 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('Project', ItemViewMode.Card) +@Component({ + selector: 'ds-project-grid-element', + styleUrls: ['./project-grid-element.component.scss'], + templateUrl: './project-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Project + */ +export class ProjectGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.html index a809c0f655..8d312fb7c0 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.html @@ -1,13 +1,13 @@ + [innerHTML]="firstMetadataValue('organization.legalName')"> - + [innerHTML]="firstMetadataValue('dc.description')"> diff --git a/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.spec.ts index ef5d7a0b4e..dd2b138abb 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-list-element.component.spec.ts @@ -20,7 +20,7 @@ const mockItemWithMetadata: Item = Object.assign(new Item(), { value: 'This is just another title' } ], - 'orgunit.identifier.description': [ + 'dc.description': [ { language: 'en_US', value: 'A description about the OrgUnit' diff --git a/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-metadata-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-metadata-list-element.component.html index 463770c0ae..ea429e87c6 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/orgunit/orgunit-metadata-list-element.component.html @@ -1,13 +1,13 @@ - - + diff --git a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html index 52b69453ce..c88b77083d 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html @@ -1,12 +1,12 @@ + [innerHTML]="firstMetadataValue('person.familyName') + ', ' + firstMetadataValue('person.givenName')"> - - + diff --git a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts index 7c5240da95..3b6aeae45b 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts @@ -20,7 +20,7 @@ const mockItemWithMetadata: Item = Object.assign(new Item(), { value: 'This is just another title' } ], - 'person.identifier.jobtitle': [ + 'person.jobTitle': [ { language: 'en_US', value: 'Developer' diff --git a/src/app/entity-groups/research-entities/item-list-elements/person/person-metadata-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/person/person-metadata-list-element.component.html index 3dfe17debc..1125c2fb9b 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/person/person-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/person/person-metadata-list-element.component.html @@ -1,8 +1,8 @@ - - + @@ -10,6 +10,6 @@ diff --git a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html index 6f0faa90ef..3e979b4e4d 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html @@ -1,16 +1,16 @@ - - - - - - - - - + [innerHTML]="firstMetadataValue('dc.title')"> + + + + + + + + + + diff --git a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts index f74d7931b0..02dc3f6d73 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts @@ -20,12 +20,12 @@ const mockItemWithMetadata: Item = Object.assign(new Item(), { value: 'This is just another title' } ], - 'project.identifier.status': [ - { - language: 'en_US', - value: 'A status about the project' - } - ] + // 'project.identifier.status': [ + // { + // language: 'en_US', + // value: 'A status about the project' + // } + // ] } }); const mockItemWithoutMetadata: Item = Object.assign(new Item(), { @@ -61,27 +61,27 @@ describe('ProjectListElementComponent', () => { })); - describe('When the item has a status', () => { - beforeEach(() => { - projectListElementComponent.item = mockItemWithMetadata; - fixture.detectChanges(); - }); - - it('should show the status span', () => { - const statusField = fixture.debugElement.query(By.css('span.item-list-status')); - expect(statusField).not.toBeNull(); - }); - }); - - describe('When the item has no status', () => { - beforeEach(() => { - projectListElementComponent.item = mockItemWithoutMetadata; - fixture.detectChanges(); - }); - - it('should not show the status span', () => { - const statusField = fixture.debugElement.query(By.css('span.item-list-status')); - expect(statusField).toBeNull(); - }); - }); + // describe('When the item has a status', () => { + // beforeEach(() => { + // projectListElementComponent.item = mockItemWithMetadata; + // fixture.detectChanges(); + // }); + // + // it('should show the status span', () => { + // const statusField = fixture.debugElement.query(By.css('span.item-list-status')); + // expect(statusField).not.toBeNull(); + // }); + // }); + // + // describe('When the item has no status', () => { + // beforeEach(() => { + // projectListElementComponent.item = mockItemWithoutMetadata; + // fixture.detectChanges(); + // }); + // + // it('should not show the status span', () => { + // const statusField = fixture.debugElement.query(By.css('span.item-list-status')); + // expect(statusField).toBeNull(); + // }); + // }); }); diff --git a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html index 0446ac6861..92ac3eac30 100644 --- a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html @@ -1,5 +1,5 @@

- {{'orgunit.page.titleprefix' | translate}} + {{'orgunit.page.titleprefix' | translate}}

@@ -7,19 +7,19 @@
@@ -37,7 +37,7 @@ [label]="'relationships.isPublicationOf' | translate">
diff --git a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.spec.ts index 52caf69d72..a49105b2e3 100644 --- a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.spec.ts @@ -8,35 +8,36 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; const mockItem: Item = Object.assign(new Item(), { - bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: { - 'orgunit.identifier.dateestablished': [ + 'organization.foundingDate': [ { language: 'en_US', value: '2018' } ], - 'orgunit.identifier.city': [ + 'organization.address.addressLocality': [ { language: 'en_US', value: 'New York' } ], - 'orgunit.identifier.country': [ + 'organization.adress.addressCountry': [ { language: 'en_US', value: 'USA' } ], - 'orgunit.identifier.id': [ + 'dc.identifier': [ { language: 'en_US', value: '1' } ], - 'orgunit.identifier.description': [ + 'dc.description': [ { language: 'en_US', value: 'desc' diff --git a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.ts b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.ts index 7101f05d35..031ca14ebb 100644 --- a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.ts @@ -4,10 +4,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { - filterRelationsByTypeLabel, - relationsToItems -} from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getRelatedItemsByTypeLabel } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; @rendersItemType('OrgUnit', ItemViewMode.Full) @Component({ @@ -39,18 +36,15 @@ export class OrgunitComponent extends ItemComponent implements OnInit { if (isNotEmpty(this.resolvedRelsAndTypes$)) { this.people$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isPersonOfOrgUnit'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isPersonOfOrgUnit') ); this.projects$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isProjectOfOrgUnit'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isProjectOfOrgUnit') ); this.publications$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isPublicationOfOrgUnit'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isPublicationOfOrgUnit') ); } }} diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index 88cd647645..04d7b9e062 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -1,5 +1,5 @@

- {{'person.page.titleprefix' | translate}} + {{'person.page.titleprefix' | translate}}

@@ -7,21 +7,21 @@ + + + + - - - - + + + +
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts index beadbbef79..4c523b81cb 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.spec.ts @@ -8,47 +8,48 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; const mockItem: Item = Object.assign(new Item(), { - bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: { - 'person.identifier.email': [ + 'person.email': [ { language: 'en_US', value: 'fake@email.com' } ], - 'person.identifier.orcid': [ - { - language: 'en_US', - value: 'ORCID-1' - } - ], - 'person.identifier.birthdate': [ + // 'person.identifier.orcid': [ + // { + // language: 'en_US', + // value: 'ORCID-1' + // } + // ], + 'person.birthDate': [ { language: 'en_US', value: '1993' } ], - 'person.identifier.staffid': [ - { - language: 'en_US', - value: '1' - } - ], - 'person.identifier.jobtitle': [ + // 'person.identifier.staffid': [ + // { + // language: 'en_US', + // value: '1' + // } + // ], + 'person.jobTitle': [ { language: 'en_US', value: 'Developer' } ], - 'person.identifier.lastname': [ + 'person.familyName': [ { language: 'en_US', value: 'Doe' } ], - 'person.identifier.firstname': [ + 'person.givenName': [ { language: 'en_US', value: 'John' diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts index ec91561eb9..8b36175b96 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts @@ -6,10 +6,7 @@ import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.compo import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { - filterRelationsByTypeLabel, - relationsToItems -} from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getRelatedItemsByTypeLabel } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; @rendersItemType('Person', ItemViewMode.Full) @Component({ @@ -57,18 +54,15 @@ export class PersonComponent extends ItemComponent { if (isNotEmpty(this.resolvedRelsAndTypes$)) { this.publications$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isPublicationOfAuthor'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isPublicationOfAuthor') ); this.projects$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isProjectOfPerson'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isProjectOfPerson') ); this.orgUnits$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isOrgUnitOfPerson'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isOrgUnitOfPerson') ); this.fixedFilterQuery = this.fixedFilterService.getQueryByRelations('isAuthorOfPublication', this.item.id); diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 08e386182b..4e9a130b8c 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -1,15 +1,15 @@

- {{'project.page.titleprefix' | translate}} + {{'project.page.titleprefix' | translate}}

- - + + + + @@ -19,13 +19,13 @@ [label]="'project.page.funder'"> - - + + + +
diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts b/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts index 1e1fd42517..5185857494 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.spec.ts @@ -8,35 +8,36 @@ import { createRelationshipsObservable, getItemPageFieldsTest } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; const mockItem: Item = Object.assign(new Item(), { - bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: { - 'project.identifier.status': [ - { - language: 'en_US', - value: 'published' - } - ], - 'project.identifier.id': [ + // 'project.identifier.status': [ + // { + // language: 'en_US', + // value: 'published' + // } + // ], + 'dc.identifier': [ { language: 'en_US', value: '1' } ], - 'project.identifier.expectedcompletion': [ - { - language: 'en_US', - value: 'exp comp' - } - ], - 'project.identifier.description': [ + // 'project.identifier.expectedcompletion': [ + // { + // language: 'en_US', + // value: 'exp comp' + // } + // ], + 'dc.description': [ { language: 'en_US', value: 'keyword' } ], - 'project.identifier.keyword': [ + 'dc.subject': [ { language: 'en_US', value: 'keyword' diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.ts b/src/app/entity-groups/research-entities/item-pages/project/project.component.ts index 46bc7c9f88..13c2b54ba4 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.ts @@ -5,10 +5,7 @@ import { MetadataRepresentation } from '../../../../core/shared/metadata-represe import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { - filterRelationsByTypeLabel, - relationsToItems -} from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { getRelatedItemsByTypeLabel } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; @rendersItemType('Project', ItemViewMode.Full) @Component({ @@ -47,18 +44,15 @@ export class ProjectComponent extends ItemComponent implements OnInit { this.contributors$ = this.buildRepresentations('OrgUnit', 'project.contributor.other'); this.people$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isPersonOfProject'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isPersonOfProject') ); this.publications$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isPublicationOfProject'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isPublicationOfProject') ); this.orgUnits$ = this.resolvedRelsAndTypes$.pipe( - filterRelationsByTypeLabel('isOrgUnitOfProject'), - relationsToItems(this.item.id) + getRelatedItemsByTypeLabel(this.item.id, 'isOrgUnitOfProject') ); } } 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 ba28f174df..099fa2a6a3 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -11,6 +11,9 @@ import { PersonMetadataListElementComponent } from './item-list-elements/person/ import { PersonListElementComponent } from './item-list-elements/person/person-list-element.component'; import { ProjectListElementComponent } from './item-list-elements/project/project-list-element.component'; import { TooltipModule } from 'ngx-bootstrap'; +import { PersonGridElementComponent } from './item-grid-elements/person/person-grid-element.component'; +import { OrgunitGridElementComponent } from './item-grid-elements/orgunit/orgunit-grid-element.component'; +import { ProjectGridElementComponent } from './item-grid-elements/project/project-grid-element.component'; const ENTRY_COMPONENTS = [ OrgunitComponent, @@ -20,7 +23,10 @@ const ENTRY_COMPONENTS = [ OrgUnitMetadataListElementComponent, PersonListElementComponent, PersonMetadataListElementComponent, - ProjectListElementComponent + ProjectListElementComponent, + PersonGridElementComponent, + OrgunitGridElementComponent, + ProjectGridElementComponent ]; @NgModule({ diff --git a/src/app/footer/footer.component.scss b/src/app/footer/footer.component.scss index bd141706da..51201774d5 100644 --- a/src/app/footer/footer.component.scss +++ b/src/app/footer/footer.component.scss @@ -1,4 +1,3 @@ -@import '../../styles/variables.scss'; $footer-bg: $gray-100; $footer-border: 1px solid darken($footer-bg, 10%); $footer-padding: $spacer * 1.5; diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss index f514508385..c3eba35b79 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -1,5 +1,3 @@ -@import '../../styles/variables.scss'; - @media screen and (max-width: map-get($grid-breakpoints, md)) { :host.open { background-color: $white; diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss index df4c0b8fb8..70c66f119d 100644 --- a/src/app/header/header.component.scss +++ b/src/app/header/header.component.scss @@ -1,5 +1,3 @@ -@import '../../styles/variables.scss'; - .navbar-brand img { height: $header-logo-height; @media screen and (max-width: map-get($grid-breakpoints, sm)) { @@ -10,3 +8,14 @@ background-image: none !important; line-height: 1.5; } + +.navbar ::ng-deep { + a { + color: $header-icon-color; + + &:hover, &focus { + color: darken($header-icon-color, 15%); + } + } +} + diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss index 1fb78bef0d..f724c3e751 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss @@ -1,5 +1,3 @@ -@import '../../../styles/variables.scss'; - .dropdown-menu { overflow: hidden; min-width: 100%; diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index 947b785196..d0fa04991d 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -1,5 +1,3 @@ -@import '../../styles/variables.scss'; - nav.navbar { border-bottom: 1px $gray-400 solid; align-items: baseline; @@ -34,6 +32,4 @@ nav.navbar { } padding: 0; } -} - - +} \ No newline at end of file diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index 2d937fd84e..ca054a662b 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -11,6 +11,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Injector, NO_ERRORS_SCHEMA } from '@angular/core'; import { MenuService } from '../shared/menu/menu.service'; import { MenuServiceStub } from '../shared/testing/menu-service-stub'; +import { ENV_CONFIG, GLOBAL_CONFIG } from '../../config'; let comp: NavbarComponent; let fixture: ComponentFixture; @@ -30,6 +31,7 @@ describe('NavbarComponent', () => { { provide: Injector, useValue: {} }, { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 48b316af4b..4c7c3cd030 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -1,4 +1,4 @@ -import { Component, Injector, OnInit } from '@angular/core'; +import { Component, Inject, Injector, OnInit } from '@angular/core'; import { slideMobileNav } from '../shared/animations/slide'; import { MenuComponent } from '../shared/menu/menu.component'; import { MenuService } from '../shared/menu/menu.service'; @@ -6,14 +6,15 @@ import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state'; import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { HostWindowService } from '../shared/host-window.service'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../config'; /** * Component representing the public navbar */ @Component({ selector: 'ds-navbar', - styleUrls: ['navbar.component.scss'], - templateUrl: 'navbar.component.html', + styleUrls: ['./navbar.component.scss'], + templateUrl: './navbar.component.html', animations: [slideMobileNav] }) export class NavbarComponent extends MenuComponent implements OnInit { @@ -23,7 +24,8 @@ export class NavbarComponent extends MenuComponent implements OnInit { */ menuID = MenuID.PUBLIC; - constructor(protected menuService: MenuService, + constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig, + protected menuService: MenuService, protected injector: Injector, public windowService: HostWindowService ) { @@ -39,7 +41,7 @@ export class NavbarComponent extends MenuComponent implements OnInit { * Initialize all menu sections and items for this menu */ createMenu() { - const menuList = [ + const menuList: any[] = [ /* News */ { id: 'browse_global', @@ -62,50 +64,6 @@ export class NavbarComponent extends MenuComponent implements OnInit { // link: '#' // } as LinkMenuItemModel, // }, - { - id: 'browse_global_global_by_title', - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.browse_global_by_title', - link: '/browse/title' - } as LinkMenuItemModel, - }, - { - id: 'browse_global_global_by_issue_date', - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.browse_global_by_issue_date', - link: '/browse/dateissued' - } as LinkMenuItemModel, - }, - { - id: 'browse_global_by_author', - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.browse_global_by_author', - link: '/browse/author' - } as LinkMenuItemModel, - }, - { - id: 'browse_global_by_subject', - parentID: 'browse_global', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.browse_global_by_subject', - link: '/browse/subject' - } as LinkMenuItemModel, - }, /* Statistics */ { @@ -120,6 +78,21 @@ export class NavbarComponent extends MenuComponent implements OnInit { index: 2 }, ]; + // Read the different Browse-By types from config and add them to the browse menu + const types = this.config.browseBy.types; + types.forEach((typeConfig) => { + menuList.push({ + id: `browse_global_by_${typeConfig.id}`, + parentID: 'browse_global', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_by_${typeConfig.id}`, + link: `/browse/${typeConfig.id}` + } as LinkMenuItemModel + }); + }); menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); } diff --git a/src/app/pagenotfound/pagenotfound.component.scss b/src/app/pagenotfound/pagenotfound.component.scss index da97dd7a62..e69de29bb2 100644 --- a/src/app/pagenotfound/pagenotfound.component.scss +++ b/src/app/pagenotfound/pagenotfound.component.scss @@ -1 +0,0 @@ -@import '../../styles/variables.scss'; diff --git a/src/app/pagenotfound/pagenotfound.component.ts b/src/app/pagenotfound/pagenotfound.component.ts index 6e173b4139..b11de58269 100644 --- a/src/app/pagenotfound/pagenotfound.component.ts +++ b/src/app/pagenotfound/pagenotfound.component.ts @@ -1,4 +1,4 @@ -import { ServerResponseService } from '../shared/services/server-response.service'; +import { ServerResponseService } from '../core/services/server-response.service'; import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { AuthService } from '../core/auth/auth.service'; diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index b560283ad5..4df07880d8 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -3,7 +3,8 @@ diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index bae345d009..5592b88c86 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -17,6 +17,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { createSuccessfulRemoteDataObject$ } from '../testing/utils'; describe('BrowseByComponent', () => { let comp: BrowseByComponent; @@ -42,7 +43,7 @@ describe('BrowseByComponent', () => { ] }) ]; - const mockItemsRD$ = observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItems))); + const mockItemsRD$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockItems)); beforeEach(async(() => { TestBed.configureTestingModule({ diff --git a/src/app/shared/chips/chips.component.scss b/src/app/shared/chips/chips.component.scss index 9d7eae7edd..76be755920 100644 --- a/src/app/shared/chips/chips.component.scss +++ b/src/app/shared/chips/chips.component.scss @@ -1,5 +1,3 @@ -@import "../../../styles/variables"; - .chip-selected { background-color: map-get($theme-colors, info) !important; } diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/chips/models/chips-item.model.ts index 540f94166f..913232fa71 100644 --- a/src/app/shared/chips/models/chips-item.model.ts +++ b/src/app/shared/chips/models/chips-item.model.ts @@ -2,6 +2,7 @@ import { isObject, uniqueId } from 'lodash'; import { hasValue, isNotEmpty } from '../../empty.util'; import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { ConfidenceType } from '../../../core/integration/models/confidence-type'; +import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; export interface ChipsItemIcon { metadata: string; @@ -62,7 +63,7 @@ export class ChipsItem { if (this._item.hasOwnProperty(icon.metadata) && (((typeof this._item[icon.metadata] === 'string') && hasValue(this._item[icon.metadata])) || (this._item[icon.metadata] as FormFieldMetadataValueObject).hasValue()) - && !(this._item[icon.metadata] as FormFieldMetadataValueObject).hasPlaceholder()) { + && !this.hasPlaceholder(this._item[icon.metadata])) { if ((icon.visibleWhenAuthorityEmpty || (this._item[icon.metadata] as FormFieldMetadataValueObject).confidence !== ConfidenceType.CF_UNSET) && isNotEmpty(icon.style)) { @@ -109,4 +110,9 @@ export class ChipsItem { this.display = value; } + + private hasPlaceholder(value: any) { + return (typeof value === 'string') ? (value === PLACEHOLDER_PARENT_METADATA) : + (value as FormFieldMetadataValueObject).hasPlaceholder() + } } diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index 3f52f0e46a..1b44970402 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -103,7 +103,7 @@ describe('ComColFormComponent', () => { ...randomMD, ...abstractMD }, - type: ResourceType.Community + type: Community.type }, ) ); diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index e24676a646..8d1d5c1dca 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -9,8 +9,9 @@ import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynami import { TranslateService } from '@ngx-translate/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models'; -import { isNotEmpty } from '../../empty.util'; import { ResourceType } from '../../../core/shared/resource-type'; +import { isNotEmpty } from '../../empty.util'; +import { Community } from '../../../core/shared/community.model'; /** * A form for creating and editing Communities or Collections @@ -29,7 +30,7 @@ export class ComColFormComponent implements OnInit { /** * Type of DSpaceObject that the form represents */ - protected type; + protected type: ResourceType; /** * @type {string} Key prefix used to generate form labels @@ -99,7 +100,7 @@ export class ComColFormComponent implements OnInit { ...this.dso.metadata, ...formMetadata }, - type: ResourceType.Community + type: Community.type }); this.submitForm.emit(updatedDSO); } @@ -110,11 +111,11 @@ export class ComColFormComponent implements OnInit { private updateFieldTranslations() { this.formModel.forEach( (fieldModel: DynamicInputModel) => { - fieldModel.label = this.translate.instant(this.type + this.LABEL_KEY_PREFIX + fieldModel.id); + fieldModel.label = this.translate.instant(this.type.value + this.LABEL_KEY_PREFIX + fieldModel.id); if (isNotEmpty(fieldModel.validators)) { fieldModel.errorMessages = {}; Object.keys(fieldModel.validators).forEach((key) => { - fieldModel.errorMessages[key] = this.translate.instant(this.type + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + fieldModel.errorMessages[key] = this.translate.instant(this.type.value + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); }); } } diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 4dad4a703f..6ad2e5b5e1 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -1,10 +1,9 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CommunityDataService } from '../../../core/data/community-data.service'; -import { RouteService } from '../../services/route.service'; +import { RouteService } from '../../../core/services/route.service'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; import { SharedModule } from '../../shared.module'; import { CommonModule } from '@angular/common'; @@ -13,6 +12,10 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { CreateComColPageComponent } from './create-comcol-page.component'; import { DataService } from '../../../core/data/data.service'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$ +} from '../../testing/utils'; describe('CreateComColPageComponent', () => { let comp: CreateComColPageComponent; @@ -46,14 +49,14 @@ describe('CreateComColPageComponent', () => { }); communityDataServiceStub = { - findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { + findById: (uuid) => createSuccessfulRemoteDataObject$(Object.assign(new Community(), { uuid: uuid, metadata: [{ key: 'dc.title', value: community.name }] - }))), - create: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) + })), + create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity) }; @@ -109,7 +112,7 @@ describe('CreateComColPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(dsoDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity)); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index c9fcfecb97..e07f2a5a0a 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Community } from '../../../core/shared/community.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { Observable } from 'rxjs'; -import { RouteService } from '../../services/route.service'; +import { RouteService } from '../../../core/services/route.service'; import { Router } from '@angular/router'; import { RemoteData } from '../../../core/data/remote-data'; import { isNotEmpty, isNotUndefined } from '../../empty.util'; diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts index e2e73bae14..57c860e04f 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts @@ -47,10 +47,10 @@ export class DeleteComColPageComponent implements .pipe(first()) .subscribe((success: boolean) => { if (success) { - const successMessage = this.translate.instant(dso.type + '.delete.notification.success'); + const successMessage = this.translate.instant((dso as any).type + '.delete.notification.success'); this.notifications.success(successMessage) } else { - const errorMessage = this.translate.instant(dso.type + '.delete.notification.fail'); + const errorMessage = this.translate.instant((dso as any).type + '.delete.notification.fail'); this.notifications.error(errorMessage) } this.router.navigate(['/']); diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html index f9ef4e5232..1c73fbb3df 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html @@ -1,7 +1,6 @@

{{'browse.comcol.head' | translate}}

diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts index 85d40a77e0..dcc7840bb4 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -1,4 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; /** * A component to display the "Browse By" section of a Community or Collection page @@ -8,9 +10,22 @@ import { Component, Input } from '@angular/core'; selector: 'ds-comcol-page-browse-by', templateUrl: './comcol-page-browse-by.component.html', }) -export class ComcolPageBrowseByComponent { +export class ComcolPageBrowseByComponent implements OnInit { /** * The ID of the Community or Collection */ @Input() id: string; + + /** + * List of currently active browse configurations + */ + types: BrowseByTypeConfig[]; + + constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig) { + } + + ngOnInit(): void { + this.types = this.config.browseBy.types; + } + } diff --git a/src/app/shared/comcol-page-content/comcol-page-content.component.scss b/src/app/shared/comcol-page-content/comcol-page-content.component.scss index ad84b72f8c..e69de29bb2 100644 --- a/src/app/shared/comcol-page-content/comcol-page-content.component.scss +++ b/src/app/shared/comcol-page-content/comcol-page-content.component.scss @@ -1 +0,0 @@ -@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/shared/comcol-page-header/comcol-page-header.component.scss b/src/app/shared/comcol-page-header/comcol-page-header.component.scss index ad84b72f8c..e69de29bb2 100644 --- a/src/app/shared/comcol-page-header/comcol-page-header.component.scss +++ b/src/app/shared/comcol-page-header/comcol-page-header.component.scss @@ -1 +0,0 @@ -@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/shared/comcol-page-logo/comcol-page-logo.component.scss b/src/app/shared/comcol-page-logo/comcol-page-logo.component.scss index 50be6f5ad0..e69de29bb2 100644 --- a/src/app/shared/comcol-page-logo/comcol-page-logo.component.scss +++ b/src/app/shared/comcol-page-logo/comcol-page-logo.component.scss @@ -1 +0,0 @@ -@import '../../../styles/variables.scss'; diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 5ec553222b..234f13f4b1 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -5,12 +5,11 @@ import { DSOSelectorComponent } from './dso-selector.component'; import { SearchService } from '../../../+search-page/search-service/search.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; -import { RemoteData } from '../../../core/data/remote-data'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { Item } from '../../../core/shared/item.model'; -import { of as observableOf } from 'rxjs'; import { PaginatedList } from '../../../core/data/paginated-list'; import { MetadataValue } from '../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject$ } from '../../testing/utils'; describe('DSOSelectorComponent', () => { let component: DSOSelectorComponent; @@ -30,7 +29,7 @@ describe('DSOSelectorComponent', () => { searchResult.indexableObject = item; searchResult.hitHighlights = {}; const searchService = jasmine.createSpyObj('searchService', { - search: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(undefined, [searchResult]))) + search: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [searchResult])) }); beforeEach(async(() => { @@ -68,6 +67,4 @@ describe('DSOSelectorComponent', () => { expect(searchService.search).toHaveBeenCalledWith(searchOptions); }); - -}) -; +}); diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts index 9efeddeeab..97957d5250 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts @@ -10,6 +10,7 @@ import * as collectionRouter from '../../../../+collection-page/collection-page- import { Community } from '../../../../core/shared/community.model'; import { CreateCollectionParentSelectorComponent } from './create-collection-parent-selector.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('CreateCollectionParentSelectorComponent', () => { let component: CreateCollectionParentSelectorComponent; @@ -26,7 +27,7 @@ describe('CreateCollectionParentSelectorComponent', () => { })] }; const router = new RouterStub(); - const communityRD = new RemoteData(false, false, true, undefined, community); + const communityRD = createSuccessfulRemoteDataObject(community); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const createPath = 'testCreatePath'; diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts index e1bb9c7997..4871d74b98 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts @@ -10,6 +10,7 @@ import * as communityRouter from '../../../../+community-page/community-page-rou import { Community } from '../../../../core/shared/community.model'; import { CreateCommunityParentSelectorComponent } from './create-community-parent-selector.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('CreateCommunityParentSelectorComponent', () => { let component: CreateCommunityParentSelectorComponent; @@ -20,7 +21,7 @@ describe('CreateCommunityParentSelectorComponent', () => { community.uuid = '1234-1234-1234-1234'; community.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Community title', language: undefined })] }; const router = new RouterStub(); - const communityRD = new RemoteData(false, false, true, undefined, community); + const communityRD = createSuccessfulRemoteDataObject(community); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const createPath = 'testCreatePath'; diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts index 19bb58eb5a..2c9b2499ab 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts @@ -9,6 +9,7 @@ import { RouterStub } from '../../../testing/router-stub'; import { Collection } from '../../../../core/shared/collection.model'; import { CreateItemParentSelectorComponent } from './create-item-parent-selector.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('CreateItemParentSelectorComponent', () => { let component: CreateItemParentSelectorComponent; @@ -19,7 +20,7 @@ describe('CreateItemParentSelectorComponent', () => { collection.uuid = '1234-1234-1234-1234'; collection.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Collection title', language: undefined })] }; const router = new RouterStub(); - const collectionRD = new RemoteData(false, false, true, undefined, collection); + const collectionRD = createSuccessfulRemoteDataObject(collection); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const createPath = 'testCreatePath'; diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html index 88f4a6f917..1181e097eb 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html @@ -5,6 +5,6 @@
-
\ No newline at end of file +
diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index 4ceaeccb3a..15f23d1fe6 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -14,6 +14,7 @@ import { By } from '@angular/platform-browser'; import { DSOSelectorComponent } from '../dso-selector/dso-selector.component'; import { MockComponent } from 'ng-mocks'; import { MetadataValue } from '../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../testing/utils'; describe('DSOSelectorModalWrapperComponent', () => { let component: DSOSelectorModalWrapperComponent; @@ -29,7 +30,7 @@ describe('DSOSelectorModalWrapperComponent', () => { })] }; - const itemRD = new RemoteData(false, false, true, undefined, item); + const itemRD = createSuccessfulRemoteDataObject(item); const modalStub = jasmine.createSpyObj('modalStub', ['close']); beforeEach(async(() => { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts index 5e60348527..cbb8fb654e 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts @@ -10,6 +10,7 @@ import * as collectionRouter from '../../../../+collection-page/collection-page- import { EditCollectionSelectorComponent } from './edit-collection-selector.component'; import { Collection } from '../../../../core/shared/collection.model'; import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('EditCollectionSelectorComponent', () => { let component: EditCollectionSelectorComponent; @@ -20,7 +21,7 @@ describe('EditCollectionSelectorComponent', () => { collection.uuid = '1234-1234-1234-1234'; collection.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Collection title', language: undefined })] }; const router = new RouterStub(); - const collectionRD = new RemoteData(false, false, true, undefined, collection); + const collectionRD = createSuccessfulRemoteDataObject(collection); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const editPath = 'testEditPath'; diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts index ac558a074a..46684e6cfb 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts @@ -10,6 +10,7 @@ import * as communityRouter from '../../../../+community-page/community-page-rou import { EditCommunitySelectorComponent } from './edit-community-selector.component'; import { Community } from '../../../../core/shared/community.model'; import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('EditCommunitySelectorComponent', () => { let component: EditCommunitySelectorComponent; @@ -20,7 +21,7 @@ describe('EditCommunitySelectorComponent', () => { community.uuid = '1234-1234-1234-1234'; community.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Community title', language: undefined })] }; const router = new RouterStub(); - const communityRD = new RemoteData(false, false, true, undefined, community); + const communityRD = createSuccessfulRemoteDataObject(community); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const editPath = 'testEditPath'; diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts index 8ac04bb335..86066916a6 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts @@ -10,6 +10,7 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { RouterStub } from '../../../testing/router-stub'; import * as itemRouter from '../../../../+item-page/item-page-routing.module'; import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('EditItemSelectorComponent', () => { let component: EditItemSelectorComponent; @@ -20,7 +21,7 @@ describe('EditItemSelectorComponent', () => { item.uuid = '1234-1234-1234-1234'; item.metadata = { 'dc.title': [Object.assign(new MetadataValue(), { value: 'Item title', language: undefined })] }; const router = new RouterStub(); - const itemRD = new RemoteData(false, false, true, undefined, item); + const itemRD = createSuccessfulRemoteDataObject(item); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const editPath = 'testEditPath'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index cead04f797..52a924604f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -14,7 +14,8 @@ - +
{{ message | translate:model.validators }} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts index fc618023f9..66bdf97dad 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -1,4 +1,7 @@ import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; + +import { Subject } from 'rxjs'; + import { isNotEmpty } from '../../../../empty.util'; import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; @@ -16,12 +19,16 @@ export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() separator: string; @serializable() hasLanguages = false; isCustomGroup = true; + valueUpdates: Subject; constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); this.separator = config.separator + ' '; + + this.valueUpdates = new Subject(); + this.valueUpdates.subscribe((value: string) => this.value = value); } get value() { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 860c481820..4e4a944319 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -28,6 +28,7 @@ export class DsDynamicInputModel extends DynamicInputModel { constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); + this.hint = config.hint; this.readOnly = config.readOnly; this.value = config.value; this.language = config.language; @@ -57,11 +58,7 @@ export class DsDynamicInputModel extends DynamicInputModel { } get hasLanguages(): boolean { - if (this.languageCodes && this.languageCodes.length > 1) { - return true; - } else { - return false; - } + return this.languageCodes && this.languageCodes.length > 1; } get language(): string { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts index 6bd5a604a0..5d2cbc58b7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts @@ -1,5 +1,5 @@ -import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model'; +import { DynamicFormControlLayout, DynamicFormGroupModel, serializable } from '@ng-dynamic-forms/core'; +import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { Subject } from 'rxjs'; import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model'; import { LanguageCode } from '../../models/form-field-language-value.model'; @@ -12,6 +12,7 @@ export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfi languageCodes?: LanguageCode[]; language?: string; readOnly: boolean; + hint?: string; } export class DynamicQualdropModel extends DynamicFormGroupModel { @@ -20,6 +21,7 @@ export class DynamicQualdropModel extends DynamicFormGroupModel { @serializable() languageUpdates: Subject; @serializable() hasLanguages = false; @serializable() readOnly: boolean; + @serializable() hint: string; isCustomGroup = true; constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) { @@ -33,6 +35,8 @@ export class DynamicQualdropModel extends DynamicFormGroupModel { this.languageUpdates.subscribe((lang: string) => { this.language = lang; }); + + this.hint = config.hint; } get value() { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index cb2d1fe217..3cfb5980c6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -20,11 +20,10 @@ [disabled]="isInputDisabled()" [placeholder]="model.placeholder | translate" [readonly]="model.readOnly" - (change)="$event.preventDefault()" + (change)="onChange($event)" (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" - (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();" - (input)="onInput($event)"> + (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();">
@@ -40,11 +39,10 @@ [disabled]="firstInputValue.length === 0 || isInputDisabled()" [placeholder]="model.secondPlaceholder | translate" [readonly]="model.readOnly" - (change)="$event.preventDefault()" + (change)="onChange($event)" (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" - (click)="$event.stopPropagation(); sdRef.close();" - (input)="onInput($event)"> + (click)="$event.stopPropagation(); sdRef.close();">
+
+
+
+ + diff --git a/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts new file mode 100644 index 0000000000..4229060e86 --- /dev/null +++ b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts @@ -0,0 +1,71 @@ +import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DsoInputSuggestionsComponent } from './dso-input-suggestions.component'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +describe('DsoInputSuggestionsComponent', () => { + + let comp: DsoInputSuggestionsComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let el: HTMLElement; + + const dso1 = { + uuid: 'test-uuid-1', + name: 'test-name-1' + } as DSpaceObject; + + const dso2 = { + uuid: 'test-uuid-2', + name: 'test-name-2' + } as DSpaceObject; + + const dso3 = { + uuid: 'test-uuid-3', + name: 'test-name-3' + } as DSpaceObject; + + const suggestions = [dso1, dso2, dso3]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule], + declarations: [DsoInputSuggestionsComponent], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(DsoInputSuggestionsComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsoInputSuggestionsComponent); + + comp = fixture.componentInstance; // LoadingComponent test instance + comp.suggestions = suggestions; + // query for the message