Merged submission module code

This commit is contained in:
Giuseppe
2018-07-26 18:36:36 +02:00
parent b6e4e2562d
commit 6f60cd68e2
179 changed files with 9143 additions and 77 deletions

View File

@@ -41,6 +41,43 @@ module.exports = {
// NOTE: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'
animate: 'scale'
},
// Submission settings
submission: {
autosave: {
// NOTE: which metadata trigger an autosave
metadata: ['dc.title', 'dc.identifier.doi', 'dc.identifier.pmid', 'dc.identifier.arxiv'],
// NOTE: every how many minutes submission is saved automatically
timer: 5
},
metadata: {
// NOTE: allow to set icons used to represent metadata belonging to a relation group
icons: [
/**
* NOTE: example of configuration
* {
* // NOTE: metadata name
* name: 'dc.author',
* config: {
* // NOTE: used when metadata value has an authority
* withAuthority: {
* // NOTE: fontawesome (v4.x) icon classes and bootstrap color utility classes can be used
* style: 'fa-user'
* },
* // NOTE: used when metadata value has not an authority
* withoutAuthority: {
* style: 'fa-user text-muted'
* }
* }
* }
*/
// default configuration
{
name: 'default',
config: {}
}
]
}
},
// Angular Universal settings
universal: {
preboot: true,

View File

@@ -79,7 +79,7 @@
"@angular/platform-server": "^5.2.5",
"@angular/router": "^5.2.5",
"@angularclass/bootloader": "1.0.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0",
"@ng-bootstrap/ng-bootstrap": "1.1.2",
"@ng-dynamic-forms/core": "5.4.7",
"@ng-dynamic-forms/ui-ng-bootstrap": "5.4.7",
"@ngrx/effects": "^5.1.0",
@@ -101,6 +101,7 @@
"core-js": "2.5.3",
"express": "4.16.2",
"express-session": "1.15.6",
"file-saver": "^1.3.8",
"font-awesome": "4.7.0",
"http-server": "0.11.1",
"https": "1.0.0",
@@ -133,6 +134,7 @@
"@types/deep-freeze": "0.1.1",
"@types/express": "^4.11.1",
"@types/express-serve-static-core": "4.11.1",
"@types/file-saver": "^1.3.0",
"@types/hammerjs": "2.0.35",
"@types/jasmine": "^2.8.6",
"@types/js-cookie": "2.1.0",

View File

@@ -210,6 +210,11 @@
"license": {
"notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission."
}
},
"submission": {
"sections": {
"init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below : <br> <br>"
}
}
},
"form": {
@@ -252,5 +257,95 @@
"errors": {
"invalid-user": "Invalid email or password."
}
},
"submission": {
"general":{
"cannot_submit": "You have not the privilege to make a new submission.",
"deposit": "Deposit",
"discard": {
"submit": "Discard",
"confirm": {
"cancel": "Cancel",
"submit": "Yes, I'm sure",
"title": "Discard submission",
"info": "This operation can't be undone. Are you sure?"
}
},
"save": "Save",
"save-later": "Save for later"
},
"submit": {
"title": "Submission"
},
"edit": {
"title": "Edit Submission"
},
"mydspace": {
},
"sections": {
"general": {
"add-more": "Add more",
"sections_not_valid": "There are incomplete sections.",
"deposit_success_notice": "Submission deposited successfully.",
"deposit_error_notice": "There was an issue when submitting the item, please try again later.",
"discard_success_notice": "Submission discarded successfully.",
"discard_error_notice": "There was an issue when discarding the item, please try again later.",
"save_success_notice": "Submission saved successfully.",
"metadata-extracted": "New metadata have been extracted and added to the <strong>{{sectionId}}</strong> section.",
"metadata-extracted-new-section": "New <strong>{{sectionId}}</strong> section has been added to submission."
},
"submit.progressbar.describe.stepone": "Describe",
"submit.progressbar.describe.steptwo": "Describe",
"submit.progressbar.describe.stepcustom": "Describe",
"submit.progressbar.describe.deduplication": "Potential duplicates",
"submit.progressbar.describe.recycle": "Recycle",
"submit.progressbar.upload": "Upload files",
"submit.progressbar.license": "Deposit license",
"submit.progressbar.cclicense": "Creative commons license",
"upload": {
"info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or <strong>upload additional files just dragging&dropping them everywhere in the page</strong>",
"drop-message": "Drop files to attach them to the item",
"upload-successful": "Upload successful",
"upload-failed": "Upload failed",
"header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):",
"header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicity decided for the single file, with the following group(s):",
"form": {
"access-condition-label": "Access condition type",
"from-label": "Access grant from",
"from-placeholder": "From",
"until-label": "Access grant until",
"until-placeholder": "Until",
"group-label": "Group"
}
},
"deduplication": {
"duplicated": "It's a duplicate",
"not_duplicated": "It's not a duplicate",
"duplicated_ctrl": "Mark the record to merge",
"duplicated_help": "Click here if this is a duplicate of your item",
"not_duplicated_help": "Click here if this is not a duplicate of your item",
"note_help": "Please enter your reason for the duplication into the box below.",
"note_placeholder": "Describe the reason of duplication",
"clear_decision": "Undo",
"clear_decision_help": "Click for clear the decision about this pontential duplicate",
"your_decision": "Your choice:",
"submitter_decision": "Submitter choice:",
"disclaimer": "The system has identified some potential duplicates. Please carefully review the list and flag each occurency with the appropriate choice or discard this submission.",
"disclaimer_ctrl": "The system has identified some potential duplicates. Please carefully review the list and the submitter comments and perform the appropriate action."
},
"recycle": {
"disclaimer": "The following existent information are not valid within the selected collection. Please copy them to the appropriate metadata if applicable. Use the discard button to remove these information when done."
}
}
},
"uploader": {
"drag-message": "Drag & Drop your files here",
"or": ", or",
"browse": "browse",
"queue-lenght": "Queue length",
"processing": "Processing"
}
}

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from './../shared/shared.module';
import { SharedModule } from '../shared/shared.module';
import { ItemPageComponent } from './simple/item-page.component';
import { ItemPageRoutingModule } from './item-page-routing.module';
@@ -18,11 +18,13 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil
import { CollectionsComponent } from './field-components/collections/collections.component';
import { FullItemPageComponent } from './full/full-item-page.component';
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
import { SubmissionModule } from '../submission/submission.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
SubmissionModule,
ItemPageRoutingModule
],
declarations: [

View File

@@ -6,7 +6,7 @@ import { LoginPageComponent } from './login-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', component: LoginPageComponent, data: { title: 'login.title' } }
{ path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } }
])
]
})

View File

@@ -1,20 +1,61 @@
import { Component, OnDestroy } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '../app.reducer';
import { ResetAuthenticationMessagesAction } from '../core/auth/auth.actions';
import {
AddAuthenticationMessageAction,
AuthenticatedAction,
AuthenticationSuccessAction,
ResetAuthenticationMessagesAction
} from '../core/auth/auth.actions';
import { Subscription } from 'rxjs/Subscription';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { ActivatedRoute } from '@angular/router';
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { Observable } from 'rxjs/Observable';
import { isAuthenticated } from '../core/auth/selectors';
@Component({
selector: 'ds-login-page',
styleUrls: ['./login-page.component.scss'],
templateUrl: './login-page.component.html'
})
export class LoginPageComponent implements OnDestroy {
export class LoginPageComponent implements OnDestroy, OnInit {
sub: Subscription;
constructor(private store: Store<AppState>) {}
constructor(private route: ActivatedRoute,
private store: Store<AppState>) {}
ngOnInit() {
const queryParamsObs = this.route.queryParams;
const authenticated = this.store.select(isAuthenticated);
this.sub = Observable.combineLatest(queryParamsObs, authenticated)
.filter(([params, auth]) => isNotEmpty(params.token) || isNotEmpty(params.expired))
.take(1)
.subscribe(([params, auth]) => {
const token = params.token;
let authToken: AuthTokenInfo;
if (!auth) {
if (isNotEmpty(token)) {
authToken = new AuthTokenInfo(token);
this.store.dispatch(new AuthenticatedAction(authToken));
} else if (isNotEmpty(params.expired)) {
this.store.dispatch(new AddAuthenticationMessageAction('auth.messages.expired'));
}
} else {
if (isNotEmpty(token)) {
authToken = new AuthTokenInfo(token);
this.store.dispatch(new AuthenticationSuccessAction(authToken));
}
}
})
}
ngOnDestroy() {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
// Clear all authentication messages when leaving login page
this.store.dispatch(new ResetAuthenticationMessagesAction());
}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { SubmissionSubmitComponent } from '../submission/submit/submission-submit.component';
@NgModule({
imports: [
RouterModule.forChild([
{
canActivate: [AuthenticatedGuard],
path: '',
pathMatch: 'full',
component: SubmissionSubmitComponent,
data: { title: 'submission.submit.title' }
}
])
]
})
export class SubmitPageRoutingModule { }

View File

@@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { SubmitPageRoutingModule } from './submit-page-routing.module';
import { SubmissionModule } from '../submission/submission.module';
@NgModule({
imports: [
SubmitPageRoutingModule,
CommonModule,
SharedModule,
SubmissionModule,
],
})
export class SubmitPageModule {
}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
canActivate: [AuthenticatedGuard],
path: ':id/edit',
component: SubmissionEditComponent,
data: { title: 'submission.edit.title' }
}
])
]
})
export class WorkflowitemsEditPageRoutingModule { }

View File

@@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { WorkflowitemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module';
import { SubmissionModule } from '../submission/submission.module';
@NgModule({
imports: [
WorkflowitemsEditPageRoutingModule,
CommonModule,
SharedModule,
SubmissionModule,
],
declarations: []
})
export class WorkflowitemsEditPageModule {
}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { SubmissionEditComponent } from '../submission/edit/submission-edit.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
canActivate: [AuthenticatedGuard],
path: ':id/edit',
component: SubmissionEditComponent,
data: { title: 'submission.edit.title' }
}
])
]
})
export class WorkspaceitemsEditPageRoutingModule { }

View File

@@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { WorkspaceitemsEditPageRoutingModule } from './workspaceitems-edit-page-routing.module';
import { SubmissionModule } from '../submission/submission.module';
@NgModule({
imports: [
WorkspaceitemsEditPageRoutingModule,
CommonModule,
SharedModule,
SubmissionModule,
],
declarations: []
})
export class WorkspaceitemsEditPageModule {
}

View File

@@ -15,6 +15,9 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowitemsEditPageModule' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
])
],

View File

@@ -13,5 +13,3 @@
<ds-footer></ds-footer>
</div>
</div>

View File

@@ -27,7 +27,11 @@ body {
}
.main-content {
flex: 1 0 auto;
flex: 1 1 100%;
margin-top: $content-spacing;
margin-bottom: $content-spacing;
}
.alert.hide {
padding: 0;
margin: 0;
}

View File

@@ -31,6 +31,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
import { NotificationComponent } from './shared/notifications/notification/notification.component';
import { SharedModule } from './shared/shared.module';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
export function getConfig() {
return ENV_CONFIG;
@@ -58,6 +59,7 @@ if (!ENV_CONFIG.production) {
HttpClientModule,
AppRoutingModule,
CoreModule.forRoot(),
ScrollToModule.forRoot(),
NgbModule.forRoot(),
TranslateModule.forRoot(),
EffectsModule.forRoot(appEffects),

View File

@@ -1,4 +1,4 @@
import { autoserialize, inheritSerialization, autoserializeAs } from 'cerialize';
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Collection } from '../../shared/collection.model';
@@ -15,6 +15,20 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
@autoserialize
handle: string;
/**
* The Bitstream that represents the license of this Collection
*/
@autoserialize
@relationship(ResourceType.License, false)
license: string;
/**
* The Bitstream that represents the default Access Conditions of this Collection
*/
@autoserialize
@relationship(ResourceType.ResourcePolicy, false)
defaultAccessConditions: string;
/**
* The Bitstream that represents the logo of this Collection
*/

View File

@@ -0,0 +1,21 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { mapsTo } from '../builders/build-decorators';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { License } from '../../shared/license.model';
@mapsTo(License)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedLicense extends NormalizedDSpaceObject {
/**
* Is the license custom?
*/
@autoserialize
custom: boolean;
/**
* The text of the license
*/
@autoserialize
text: string;
}

View File

@@ -6,6 +6,12 @@ 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 { NormalizedEpersonModel } from '../../eperson/models/NormalizedEperson.model';
import { NormalizedGroupModel } from '../../eperson/models/NormalizedGroup.model';
import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model';
export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
@@ -25,6 +31,21 @@ export class NormalizedObjectFactory {
case ResourceType.Community: {
return NormalizedCommunity
}
case ResourceType.License: {
return NormalizedLicense
}
case ResourceType.ResourcePolicy: {
return NormalizedResourcePolicy
}
case ResourceType.Workspaceitem: {
return NormalizedWorkspaceItem
}
case ResourceType.Eperson: {
return NormalizedEpersonModel
}
case ResourceType.Group: {
return NormalizedGroupModel
}
default: {
return undefined;
}

View File

@@ -0,0 +1,45 @@
import { mapsTo } from '../builders/build-decorators';
import { autoserialize, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { ResourcePolicy } from '../../shared/resource-policy.model';
@mapsTo(ResourcePolicy)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedResourcePolicy extends NormalizedDSpaceObject {
/**
* The action of the resource policy
*/
@autoserialize
action: string;
/**
* The identifier of the resource policy
*/
@autoserialize
id: string;
/**
* The group uuid bound to the resource policy
*/
@autoserialize
groupUUID: string;
/**
* The end date of the resource policy
*/
@autoserialize
endDate: string;
/**
* The start date of the resource policy
*/
@autoserialize
startDate: string;
/**
* The type of the resource policy
*/
@autoserialize
rpType: string
}

View File

@@ -10,6 +10,7 @@ import { MetadataSchema } from '../metadata/metadataschema.model';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
import { AuthStatus } from '../auth/models/auth-status.model';
import { NormalizedObject } from './models/normalized-object.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
@@ -176,4 +177,34 @@ export class IntegrationSuccessResponse extends RestResponse {
}
}
export class PostPatchSuccessResponse extends RestResponse {
constructor(
public dataDefinition: any[],
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
}
export class SubmissionSuccessResponse extends RestResponse {
constructor(
public dataDefinition: Array<NormalizedObject | ConfigObject | string>,
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
}
export class EpersonSuccessResponse extends RestResponse {
constructor(
public epersonDefinition: NormalizedObject[],
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { ConfigService } from './config.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class SubmissionUploadsConfigService extends ConfigService {
protected linkPath = 'submissionuploads';
protected browseEndpoint = '';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected halService: HALEndpointService) {
super();
}
}

View File

@@ -4,11 +4,13 @@ import { ResponseCacheEffects } from './cache/response-cache.effects';
import { UUIDIndexEffects } from './index/index.effects';
import { RequestEffects } from './data/request.effects';
import { AuthEffects } from './auth/auth.effects';
import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects';
export const coreEffects = [
ResponseCacheEffects,
RequestEffects,
ObjectCacheEffects,
UUIDIndexEffects,
AuthEffects
AuthEffects,
JsonPatchOperationsEffects,
];

View File

@@ -1,9 +1,4 @@
import {
NgModule,
Optional,
SkipSelf,
ModuleWithProviders
} from '@angular/core';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
@@ -24,7 +19,9 @@ import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
import { FormOperationsService } from '../submission/sections/form/form-operations.service';
import { FormService } from '../shared/form/form.service';
import { GroupEpersonService } from './eperson/group-eperson.service';
import { HostWindowService } from '../shared/host-window.service';
import { ItemDataService } from './data/item-data.service';
import { MetadataService } from './metadata/metadata.service';
@@ -43,8 +40,12 @@ import { RouteService } from '../shared/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';
import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service';
import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service';
import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder';
import { AuthorityService } from './integration/authority.service';
import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service';
import { WorkspaceitemDataService } from './submission/workspaceitem-data.service';
import { UUIDService } from './shared/uuid.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthRequestService } from './auth/auth-request.service';
@@ -60,8 +61,12 @@ import { RegistryMetadataschemasResponseParsingService } from './data/registry-m
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { JsonPatchOperationsService } from './json-patch/json-patch-operations.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';
import { SubmissionRestService } from '../submission/submission-rest.service';
const IMPORTS = [
CommonModule,
@@ -90,7 +95,10 @@ const PROVIDERS = [
DynamicFormService,
DynamicFormValidationService,
FormBuilderService,
FormOperationsService,
FormService,
EpersonResponseParsingService,
GroupEpersonService,
HALEndpointService,
HostWindowService,
ItemDataService,
@@ -119,11 +127,20 @@ const PROVIDERS = [
RouteService,
SubmissionDefinitionsConfigService,
SubmissionFormsConfigService,
SubmissionRestService,
SubmissionSectionsConfigService,
SubmissionResponseParsingService,
JsonPatchOperationsBuilder,
JsonPatchOperationsService,
AuthorityService,
IntegrationResponseParsingService,
UploaderService,
UUIDService,
NotificationsService,
WorkspaceitemDataService,
WorkflowitemDataService,
UploaderService,
FileService,
// register AuthInterceptor as HttpInterceptor
{
provide: HTTP_INTERCEPTORS,

View File

@@ -5,6 +5,7 @@ import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reduc
import { indexReducer, IndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer';
import { authReducer, AuthState } from './auth/auth.reducer';
import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer';
export interface CoreState {
'data/object': ObjectCacheState,
@@ -12,6 +13,7 @@ export interface CoreState {
'data/request': RequestState,
'index': IndexState,
'auth': AuthState,
'json/patch': JsonPatchOperationsState
}
export const coreReducers: ActionReducerMap<CoreState> = {
@@ -19,7 +21,8 @@ export const coreReducers: ActionReducerMap<CoreState> = {
'data/response': responseCacheReducer,
'data/request': requestReducer,
'index': indexReducer,
'auth': authReducer
'auth': authReducer,
'json/patch': jsonPatchOperationsReducer
};
export const coreSelector = createFeatureSelector<CoreState>('core');

View File

@@ -17,7 +17,7 @@ function isPaginatedResponse(halObj: any) {
/* tslint:disable:max-classes-per-file */
class ProcessRequestDTO<ObjectDomain> {
export class ProcessRequestDTO<ObjectDomain> {
[key: string]: ObjectDomain[]
}

View File

@@ -15,6 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
protected linkPath = 'collections';
protected forceBypassCache = false;
constructor(
protected responseCache: ResponseCacheService,

View File

@@ -22,6 +22,7 @@ class NormalizedTestObject extends NormalizedObject {
}
class TestService extends ComColDataService<NormalizedTestObject, any> {
protected forceBypassCache = false;
constructor(
protected responseCache: ResponseCacheService,

View File

@@ -16,6 +16,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
protected linkPath = 'communities';
protected cds = this;
protected forceBypassCache = false;
constructor(
protected responseCache: ResponseCacheService,

View File

@@ -34,7 +34,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from config endpoint'),
{statusText: data.statusCode}
{ statusText: data.statusCode }
)
);
}

View File

@@ -10,6 +10,7 @@ import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data';
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
import { RequestService } from './request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
@@ -19,6 +20,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
protected abstract store: Store<CoreState>;
protected abstract linkPath: string;
protected abstract halService: HALEndpointService;
protected abstract forceBypassCache = false;
public abstract getScopedEndpoint(scope: string): Observable<string>
@@ -52,6 +54,36 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
}
}
protected getSearchByHref(endpoint, searchByLink, options: FindAllOptions = {}): Observable<string> {
let result: Observable<string>;
const args = [];
if (hasValue(options.scopeID)) {
result = Observable.of(`${endpoint}/${searchByLink}?uuid=${options.scopeID}`);
} else {
result = Observable.of(endpoint);
}
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
args.push(`page=${options.currentPage - 1}`);
}
if (hasValue(options.elementsPerPage)) {
args.push(`size=${options.elementsPerPage}`);
}
if (hasValue(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
if (isNotEmpty(args)) {
return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString());
} else {
return result;
}
}
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href))
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
@@ -61,7 +93,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
.take(1)
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request);
this.requestService.configure(request, this.forceBypassCache);
});
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs) as Observable<RemoteData<PaginatedList<TDomain>>>;
@@ -80,32 +112,69 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
.take(1)
.subscribe((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id);
this.requestService.configure(request);
this.requestService.configure(request, this.forceBypassCache);
});
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs);
}
findByHref(href: string): Observable<RemoteData<TDomain>> {
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href));
findByHref(href: string, options?: HttpOptions): Observable<RemoteData<TDomain>> {
this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href, null, options), this.forceBypassCache);
return this.rdbService.buildSingle<TNormalized, TDomain>(href);
}
// TODO implement, after the structure of the REST server's POST response is finalized
// create(dso: DSpaceObject): Observable<RemoteData<TDomain>> {
// const postHrefObs = this.getEndpoint();
//
// // TODO ID is unknown at this point
// const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id));
//
// postHrefObs
// .filter((href: string) => hasValue(href))
// .take(1)
// .subscribe((href: string) => {
// const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso);
// this.requestService.configure(request);
// });
//
// return this.rdbService.buildSingle<TNormalized, TDomain>(idHrefObs, this.normalizedResourceType);
// }
// TODO remove when search will be completed
public searchBySubmitter(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
return this.searchBy('submitter', options);
}
// TODO remove when search will be completed
searchByUser(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
return this.searchBy('user', options);
}
// TODO remove when search will be completed
protected searchBy(searchBy: string, options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
let url = null;
switch (searchBy) {
case 'user': {
url = 'search/findByUser';
break;
}
case 'submitter': {
url = 'search/findBySubmitter';
break;
}
}
const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href))
.flatMap((endpoint: string) => this.getSearchByHref(endpoint, url, options));
hrefObs
.filter((href: string) => hasValue(href))
.take(1)
.subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request, this.forceBypassCache);
});
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs) as Observable<RemoteData<PaginatedList<TDomain>>>;
}
// TODO implement, after the structure of the REST server's POST response is finalized
// create(dso: DSpaceObject): Observable<RemoteData<TDomain>> {
// const postHrefObs = this.getEndpoint();
//
// // TODO ID is unknown at this point
// const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id));
//
// postHrefObs
// .filter((href: string) => hasValue(href))
// .take(1)
// .subscribe((href: string) => {
// const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso);
// this.requestService.configure(request);
// });
//
// return this.rdbService.buildSingle<TNormalized, TDomain>(idHrefObs, this.normalizedResourceType);
// }
}

View File

@@ -19,6 +19,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
protected linkPath = 'items';
protected forceBypassCache = false;
constructor(
protected responseCache: ResponseCacheService,

View File

@@ -11,6 +11,8 @@ import { ConfigResponseParsingService } from './config-response-parsing.service'
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpHeaders } from '@angular/common/http';
import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service';
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
/* tslint:disable:max-classes-per-file */
@@ -184,8 +186,8 @@ export class BrowseEntriesRequest extends GetRequest {
}
export class ConfigRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
constructor(uuid: string, href: string, public options?: HttpOptions) {
super(uuid, href, null, options);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
@@ -222,6 +224,85 @@ export class IntegrationRequest extends GetRequest {
return IntegrationResponseParsingService;
}
}
export class SubmissionFindAllRequest extends GetRequest {
constructor(uuid: string, href: string, public body?: FindAllOptions) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SubmissionResponseParsingService;
}
}
export class SubmissionFindByIDRequest extends GetRequest {
constructor(uuid: string,
href: string,
public resourceID: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SubmissionResponseParsingService;
}
}
export class SubmissionRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SubmissionResponseParsingService;
}
}
export class SubmissionDeleteRequest extends DeleteRequest {
constructor(public uuid: string,
public href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SubmissionResponseParsingService;
}
}
export class SubmissionPatchRequest extends PatchRequest {
constructor(public uuid: string,
public href: string,
public body?: any) {
super(uuid, href, body);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SubmissionResponseParsingService;
}
}
export class SubmissionPostRequest extends PostRequest {
constructor(public uuid: string,
public href: string,
public body?: any,
public options?: HttpOptions) {
super(uuid, href, body, options);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SubmissionResponseParsingService;
}
}
export class EpersonRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return EpersonResponseParsingService;
}
}
export class RequestError extends Error {
statusText: string;
}

View File

@@ -1,6 +1,8 @@
import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
import { MemoizedSelector, Store } from '@ngrx/store';
import { remove } from 'lodash';
import { Observable } from 'rxjs/Observable';
import { hasValue } from '../../shared/empty.util';
@@ -67,12 +69,28 @@ export class RequestService {
.flatMap((uuid: string) => this.getByUUID(uuid));
}
// TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
private clearRequestsOnTheirWayToTheStore(href) {
this.getByHref(href)
.take(1)
.subscribe((re: RequestEntry) => {
if (!hasValue(re)) {
this.responseCache.remove(href);
} else if (!re.responsePending) {
this.responseCache.remove(href);
remove(this.requestsOnTheirWayToTheStore, (item) => item === href);
}
});
}
// TODO to review "forceBypassCache" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
configure<T extends CacheableObject>(request: RestRequest, forceBypassCache: boolean = false): void {
const isGetRequest = request.method === RestRequestMethod.Get;
if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) {
if (forceBypassCache) {
this.clearRequestsOnTheirWayToTheStore(request.href);
}
if (!isGetRequest || !this.isCachedOrPending(request) || (forceBypassCache && !this.isPending(request))) {
this.dispatchRequest(request);
if (isGetRequest && !forceBypassCache) {
if (isGetRequest) {
this.trackRequestsOnTheirWayToTheStore(request);
}
}
@@ -121,7 +139,7 @@ export class RequestService {
*/
private trackRequestsOnTheirWayToTheStore(request: GetRequest) {
this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href];
this.store.select(this.entryFromUUIDSelector(request.href))
this.getByHref(request.href)
.filter((re: RequestEntry) => hasValue(re))
.take(1)
.subscribe((re: RequestEntry) => {

View File

@@ -0,0 +1,12 @@
import { PageInfo } from '../shared/page-info.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
/**
* A class to represent the data retrieved by a Eperson service
*/
export class EpersonData {
constructor(
public pageInfo: PageInfo,
public payload: NormalizedObject[]
) { }
}

View File

@@ -0,0 +1,21 @@
import { EpersonType } from './eperson-type';
import { GenericConstructor } from '../shared/generic-constructor';
import { NormalizedEpersonModel } from './models/NormalizedEperson.model';
import { NormalizedGroupModel } from './models/NormalizedGroup.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
export class EpersonObjectFactory {
public static getConstructor(type): GenericConstructor<NormalizedObject> {
switch (type) {
case EpersonType.EpersonsModel: {
return NormalizedEpersonModel
}
case EpersonType.GroupsModel: {
return NormalizedGroupModel
}
default: {
return undefined;
}
}
}
}

View File

@@ -0,0 +1,47 @@
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 {
EpersonSuccessResponse, ErrorResponse,
RestResponse
} from '../cache/response-cache.models';
import { isNotEmpty } from '../../shared/empty.util';
import { EpersonObjectFactory } from './eperson-object-factory';
import { EpersonType } from './eperson-type';
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 { DSpaceObject } from '../shared/dspace-object.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
@Injectable()
export class EpersonResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = EpersonObjectFactory;
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
const epersonDefinition = this.process<NormalizedObject,EpersonType>(data.payload, request.href);
return new EpersonSuccessResponse(epersonDefinition[Object.keys(epersonDefinition)[0]], data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from EPerson endpoint'),
{statusText: data.statusCode}
)
);
}
}
}

View File

@@ -0,0 +1,5 @@
export enum EpersonType {
EpersonsModel = 'eperson',
GroupsModel = 'group',
}

View File

@@ -0,0 +1,53 @@
import { Observable } from 'rxjs/Observable';
import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { EpersonSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { EpersonRequest, GetRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { isNotEmpty } from '../../shared/empty.util';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { EpersonData } from './eperson-data';
export abstract class EpersonService {
protected request: EpersonRequest;
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService;
protected abstract linkPath: string;
protected abstract browseEndpoint: string;
protected abstract halService: HALEndpointService;
protected getEperson(request: GetRequest): Observable<EpersonData> {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`Couldn't retrieve the EPerson`))),
successResponse
.filter((response: EpersonSuccessResponse) => isNotEmpty(response))
.map((response: EpersonSuccessResponse) => new EpersonData(response.pageInfo, response.epersonDefinition))
.distinctUntilChanged());
}
public getDataByHref(href: string): Observable<EpersonData> {
const request = new EpersonRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request);
return this.getEperson(request);
}
public getDataByUuid(uuid: string): Observable<EpersonData> {
return this.halService.getEndpoint(this.linkPath)
.map((endpoint: string) => this.getDataByIDHref(endpoint, uuid))
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => new EpersonRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: GetRequest) => this.requestService.configure(request))
.flatMap((request: GetRequest) => this.getEperson(request))
.distinctUntilChanged();
}
protected getDataByIDHref(endpoint, resourceID): string {
return `${endpoint}/${resourceID}`;
}
}

View File

@@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { EpersonService } from './eperson.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service';
import { isNotEmpty } from '../../shared/empty.util';
import { EpersonRequest, GetRequest } from '../data/request.models';
import { EpersonData } from './eperson-data';
import { EpersonSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { Observable } from 'rxjs/Observable';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { BrowseService } from '../browse/browse.service';
@Injectable()
export class GroupEpersonService extends EpersonService {
protected linkPath = 'groups';
protected browseEndpoint = '';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected bs: BrowseService,
protected halService: HALEndpointService) {
super();
}
protected getSearchHref(endpoint, groupName): string {
return `${endpoint}/search/isMemberOf?groupName=${groupName}`;
}
isMemberOf(groupName: string) {
return this.halService.getEndpoint(this.linkPath)
.map((endpoint: string) => this.getSearchHref(endpoint, groupName))
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => new EpersonRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: GetRequest) => this.requestService.configure(request))
.flatMap((request: GetRequest) => this.getSearch(request))
.distinctUntilChanged();
}
protected getSearch(request: GetRequest): Observable<EpersonData> {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.of(new EpersonData(undefined, undefined))),
successResponse
.filter((response: EpersonSuccessResponse) => isNotEmpty(response))
.map((response: EpersonSuccessResponse) => new EpersonData(response.pageInfo, response.epersonDefinition))
.distinctUntilChanged());
}
}

View File

@@ -1,10 +1,12 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
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';
import { NormalizedGroupModel } from './NormalizedGroup.model';
@mapsTo(Eperson)
@inheritSerialization(NormalizedDSpaceObject)
@@ -13,9 +15,8 @@ export class NormalizedEpersonModel extends NormalizedDSpaceObject implements Ca
@autoserialize
public handle: string;
@autoserialize
@relationship(ResourceType.Group, true)
groups: string[];
@autoserializeAs(NormalizedGroupModel)
groups: Group[];
@autoserialize
public netid: string;

View File

@@ -2,6 +2,8 @@ import { DSpaceObject } from '../../shared/dspace-object.model';
export class Group extends DSpaceObject {
public groups: Group[];
public handle: string;
public permanent: boolean;

View File

@@ -1,5 +1,6 @@
import { IntegrationModel } from './integration.model';
import { autoserialize } from 'cerialize';
import { isNotEmpty } from '../../../shared/empty.util';
export class AuthorityValueModel extends IntegrationModel {
@@ -17,4 +18,8 @@ export class AuthorityValueModel extends IntegrationModel {
@autoserialize
language: string;
hasValue(): boolean {
return isNotEmpty(this.value);
}
}

View File

@@ -0,0 +1,45 @@
/**
* Combines a variable number of strings representing parts
* of a relative REST URL in to a single, absolute REST URL
*
*/
import { isNotUndefined } from '../../../shared/empty.util';
export interface JsonPatchOperationPathObject {
rootElement: string;
subRootElement: string;
path: string;
}
export class JsonPatchOperationPathCombiner {
private _rootElement: string;
private _subRootElement: string;
constructor(rootElement, ...subRootElements: string[]) {
this._rootElement = rootElement;
this._subRootElement = subRootElements.join('/');
}
get rootElement(): string {
return this._rootElement;
}
get subRootElement(): string {
return this._subRootElement;
}
public getPath(fragment?: string|string[]): JsonPatchOperationPathObject {
if (isNotUndefined(fragment) && Array.isArray(fragment)) {
fragment = fragment.join('/');
}
let path;
if (isNotUndefined(fragment)) {
path = '/' + this._rootElement + '/' + this._subRootElement + '/' + fragment;
} else {
path = '/' + this._rootElement + '/' + this._subRootElement;
}
return {rootElement: this._rootElement, subRootElement: this._subRootElement, path: path};
}
}

View File

@@ -0,0 +1,111 @@
import { Store } from '@ngrx/store';
import { CoreState } from '../../core.reducers';
import {
NewPatchAddOperationAction,
NewPatchRemoveOperationAction,
NewPatchReplaceOperationAction
} from '../json-patch-operations.actions';
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
import { Injectable } from '@angular/core';
import { isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { dateToGMTString } from '../../../shared/date.util';
import { AuthorityValueModel } from '../../integration/models/authority-value.model';
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
@Injectable()
export class JsonPatchOperationsBuilder {
constructor(private store: Store<CoreState>) {
}
add(path: JsonPatchOperationPathObject, value, first = false, plain = false) {
this.store.dispatch(
new NewPatchAddOperationAction(
path.rootElement,
path.subRootElement,
path.path, this.prepareValue(value, plain, first)));
}
replace(path: JsonPatchOperationPathObject, value, plain = false) {
this.store.dispatch(
new NewPatchReplaceOperationAction(
path.rootElement,
path.subRootElement,
path.path,
this.prepareValue(value, plain, false)));
}
remove(path: JsonPatchOperationPathObject) {
this.store.dispatch(
new NewPatchRemoveOperationAction(
path.rootElement,
path.subRootElement,
path.path));
}
protected prepareValue(value: any, plain: boolean, first: boolean) {
let operationValue: any = null;
if (isNotEmpty(value)) {
if (plain) {
operationValue = value;
} else {
if (Array.isArray(value)) {
operationValue = [];
value.forEach((entry) => {
if ((typeof entry === 'object')) {
operationValue.push(this.prepareObjectValue(entry));
} else {
operationValue.push(new FormFieldMetadataValueObject(entry));
// operationValue.push({value: entry});
// operationValue.push(entry);
}
});
} else if (typeof value === 'object') {
operationValue = this.prepareObjectValue(value);
} else {
operationValue = new FormFieldMetadataValueObject(value);
}
}
}
return (first && !Array.isArray(operationValue)) ? [operationValue] : operationValue;
}
protected prepareObjectValue(value: any) {
let operationValue = Object.create({});
if (isEmpty(value) || value instanceof FormFieldMetadataValueObject) {
operationValue = value;
} else if (value instanceof Date) {
operationValue = new FormFieldMetadataValueObject(dateToGMTString(value));
} else if (value instanceof AuthorityValueModel) {
operationValue = this.prepareAuthorityValue(value);
} else if (value instanceof FormFieldLanguageValueObject) {
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
} else if (value.hasOwnProperty('value')) {
operationValue = new FormFieldMetadataValueObject(value.value);
// operationValue = value;
} else {
Object.keys(value)
.forEach((key) => {
if (typeof value[key] === 'object') {
operationValue[key] = this.prepareObjectValue(value[key]);
} else {
operationValue[key] = value[key];
}
});
// operationValue = {value: value};
}
return operationValue;
}
protected prepareAuthorityValue(value: any) {
let operationValue: any = null;
if (isNotEmpty(value.id)) {
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.id);
} else {
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
}
return operationValue;
}
}

View File

@@ -0,0 +1,279 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const JsonPatchOperationsActionTypes = {
NEW_JSON_PATCH_ADD_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_ADD_OPERATION'),
NEW_JSON_PATCH_COPY_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_COPY_OPERATION'),
NEW_JSON_PATCH_MOVE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_MOVE_OPERATION'),
NEW_JSON_PATCH_REMOVE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_REMOVE_OPERATION'),
NEW_JSON_PATCH_REPLACE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_REPLACE_OPERATION'),
COMMIT_JSON_PATCH_OPERATIONS: type('dspace/core/patch/COMMIT_JSON_PATCH_OPERATIONS'),
ROLLBACK_JSON_PATCH_OPERATIONS: type('dspace/core/patch/ROLLBACK_JSON_PATCH_OPERATIONS'),
FLUSH_JSON_PATCH_OPERATIONS: type('dspace/core/patch/FLUSH_JSON_PATCH_OPERATIONS'),
START_TRANSACTION_JSON_PATCH_OPERATIONS: type('dspace/core/patch/START_TRANSACTION_JSON_PATCH_OPERATIONS'),
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to commit the current transaction
*/
export class CommitPatchOperationsAction implements Action {
type = JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS;
payload: {
resourceType: string;
resourceId: string;
};
/**
* Create a new CommitPatchOperationsAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
*/
constructor(resourceType: string, resourceId: string) {
this.payload = { resourceType, resourceId };
}
}
/**
* An ngrx action to rollback the current transaction
*/
export class RollbacktPatchOperationsAction implements Action {
type = JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS;
payload: {
resourceType: string;
resourceId: string;
};
/**
* Create a new CommitPatchOperationsAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
*/
constructor(resourceType: string, resourceId: string) {
this.payload = { resourceType, resourceId };
}
}
/**
* An ngrx action to initiate a transaction block
*/
export class StartTransactionPatchOperationsAction implements Action {
type = JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS;
payload: {
resourceType: string;
resourceId: string;
startTime: number;
};
/**
* Create a new CommitPatchOperationsAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
* @param startTime
* the start timestamp
*/
constructor(resourceType: string, resourceId: string, startTime: number) {
this.payload = { resourceType, resourceId, startTime };
}
}
/**
* An ngrx action to flush list of the JSON Patch operations
*/
export class FlushPatchOperationsAction implements Action {
type = JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS;
payload: {
resourceType: string;
resourceId: string;
};
/**
* Create a new FlushPatchOperationsAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
*/
constructor(resourceType: string, resourceId: string) {
this.payload = { resourceType, resourceId };
}
}
/**
* An ngrx action to Add new HTTP/PATCH ADD operations to state
*/
export class NewPatchAddOperationAction implements Action {
type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION;
payload: {
resourceType: string;
resourceId: string;
path: string;
value: any
};
/**
* Create a new NewPatchAddOperationAction
*
* @param resourceType
* the resource's type where to add operation
* @param resourceId
* the resource's ID
* @param path
* the path of the operation
* @param value
* the operation's payload
*/
constructor(resourceType: string, resourceId: string, path: string, value: any) {
this.payload = { resourceType, resourceId, path, value };
}
}
/**
* An ngrx action to add new JSON Patch COPY operation to state
*/
export class NewPatchCopyOperationAction implements Action {
type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION;
payload: {
resourceType: string;
resourceId: string;
from: string;
path: string;
};
/**
* Create a new NewPatchCopyOperationAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
* @param from
* the path to copy the value from
* @param path
* the path where to copy the value
*/
constructor(resourceType: string, resourceId: string, from: string, path: string) {
this.payload = { resourceType, resourceId, from, path };
}
}
/**
* An ngrx action to Add new JSON Patch MOVE operation to state
*/
export class NewPatchMoveOperationAction implements Action {
type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION;
payload: {
resourceType: string;
resourceId: string;
from: string;
path: string;
};
/**
* Create a new NewPatchMoveOperationAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
* @param from
* the path to move the value from
* @param path
* the path where to move the value
*/
constructor(resourceType: string, resourceId: string, from: string, path: string) {
this.payload = { resourceType, resourceId, from, path };
}
}
/**
* An ngrx action to Add new JSON Patch REMOVE operation to state
*/
export class NewPatchRemoveOperationAction implements Action {
type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION;
payload: {
resourceType: string;
resourceId: string;
path: string;
};
/**
* Create a new NewPatchRemoveOperationAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
* @param path
* the path of the operation
*/
constructor(resourceType: string, resourceId: string, path: string) {
this.payload = { resourceType, resourceId, path };
}
}
/**
* An ngrx action to add new JSON Patch REPLACE operation to state
*/
export class NewPatchReplaceOperationAction implements Action {
type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION;
payload: {
resourceType: string;
resourceId: string;
path: string;
value: any
};
/**
* Create a new NewPatchReplaceOperationAction
*
* @param resourceType
* the resource's type
* @param resourceId
* the resource's ID
* @param path
* the path of the operation
* @param value
* the operation's payload
*/
constructor(resourceType: string, resourceId: string, path: string, value: any) {
this.payload = { resourceType, resourceId, path, value };
}
}
/* tslint:enable:max-classes-per-file */
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
*/
export type PatchOperationsActions
= CommitPatchOperationsAction
| FlushPatchOperationsAction
| NewPatchAddOperationAction
| NewPatchCopyOperationAction
| NewPatchMoveOperationAction
| NewPatchRemoveOperationAction
| NewPatchReplaceOperationAction
| RollbacktPatchOperationsAction
| StartTransactionPatchOperationsAction

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import {
CommitPatchOperationsAction, FlushPatchOperationsAction,
JsonPatchOperationsActionTypes
} from './json-patch-operations.actions';
@Injectable()
export class JsonPatchOperationsEffects {
@Effect() commit$ = this.actions$
.ofType(JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS)
.map((action: CommitPatchOperationsAction) => {
return new FlushPatchOperationsAction(action.payload.resourceType, action.payload.resourceId);
});
constructor(private actions$: Actions) {}
}

View File

@@ -0,0 +1,292 @@
import { hasValue, isNotEmpty, isNotUndefined, isNull } from '../../shared/empty.util';
import {
FlushPatchOperationsAction,
PatchOperationsActions,
JsonPatchOperationsActionTypes,
NewPatchAddOperationAction,
NewPatchCopyOperationAction,
NewPatchMoveOperationAction,
NewPatchRemoveOperationAction,
NewPatchReplaceOperationAction,
CommitPatchOperationsAction,
StartTransactionPatchOperationsAction,
RollbacktPatchOperationsAction
} from './json-patch-operations.actions';
import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model';
export interface JsonPatchOperationObject {
operation: JsonPatchOperationModel;
timeAdded: number;
}
export interface JsonPatchOperationsEntry {
body: JsonPatchOperationObject[];
}
export interface JsonPatchOperationsResourceEntry {
children: { [resourceId: string]: JsonPatchOperationsEntry };
transactionStartTime: number;
commitPending: boolean;
}
/**
* The JSON patch operations State
*
* Consists of a map with a namespace as key,
* and an array of JsonPatchOperationModel as values
*/
export interface JsonPatchOperationsState {
[resourceType: string]: JsonPatchOperationsResourceEntry;
}
const initialState: JsonPatchOperationsState = Object.create(null);
export function jsonPatchOperationsReducer(state = initialState, action: PatchOperationsActions): JsonPatchOperationsState {
switch (action.type) {
case JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS: {
return commitOperations(state, action as CommitPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS: {
return flushOperation(state, action as FlushPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION: {
return newOperation(state, action as NewPatchAddOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION: {
return newOperation(state, action as NewPatchCopyOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION: {
return newOperation(state, action as NewPatchMoveOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION: {
return newOperation(state, action as NewPatchRemoveOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION: {
return newOperation(state, action as NewPatchReplaceOperationAction);
}
case JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS: {
return rollbackOperations(state, action as RollbacktPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS: {
return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction);
}
default: {
return state;
}
}
}
/**
* Set the transaction start time.
*
* @param state
* the current state
* @param action
* an StartTransactionPatchOperationsAction
* @return JsonPatchOperationsState
* the new state.
*/
function startTransactionPatchOperations(state: JsonPatchOperationsState, action: StartTransactionPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])
&& isNull(state[ action.payload.resourceType ].transactionStartTime)) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: state[ action.payload.resourceType ].children,
transactionStartTime: action.payload.startTime,
commitPending: true
})
});
} else {
return state;
}
}
/**
* Set commit pending state.
*
* @param state
* the current state
* @param action
* an CommitPatchOperationsAction
* @return JsonPatchOperationsState
* the new state, with the section new validity status.
*/
function commitOperations(state: JsonPatchOperationsState, action: CommitPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])
&& state[ action.payload.resourceType ].commitPending) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: state[ action.payload.resourceType ].children,
transactionStartTime: state[ action.payload.resourceType ].transactionStartTime,
commitPending: false
})
});
} else {
return state;
}
}
/**
* Set commit pending state.
*
* @param state
* the current state
* @param action
* an RollbacktPatchOperationsAction
* @return JsonPatchOperationsState
* the new state.
*/
function rollbackOperations(state: JsonPatchOperationsState, action: RollbacktPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])
&& state[ action.payload.resourceType ].commitPending) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: state[ action.payload.resourceType ].children,
transactionStartTime: null,
commitPending: false
})
});
} else {
return state;
}
}
/**
* Add new JSON patch operation list.
*
* @param state
* the current state
* @param action
* an NewPatchAddOperationAction
* @return JsonPatchOperationsState
* the new state, with the section new validity status.
*/
function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperationsState {
const newState = Object.assign({}, state);
const newBody = addOperationToList(
(hasValue(newState[ action.payload.resourceType ])
&& hasValue(newState[ action.payload.resourceType ].children)
&& hasValue(newState[ action.payload.resourceType ].children[ action.payload.resourceId ])
&& isNotEmpty(newState[ action.payload.resourceType ].children[ action.payload.resourceId ].body))
? newState[ action.payload.resourceType ].children[ action.payload.resourceId ].body : Array.of(),
action.type,
action.payload.path,
hasValue(action.payload.value) ? action.payload.value : null);
if (hasValue(newState[ action.payload.resourceType ])
&& hasValue(newState[ action.payload.resourceType ].children)) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: Object.assign({}, state[ action.payload.resourceType ].children, {
[action.payload.resourceId]: {
body: newBody,
}
}),
transactionStartTime: state[ action.payload.resourceType ].transactionStartTime,
commitPending: isNotUndefined(state[ action.payload.resourceType ].commitPending) ? state[ action.payload.resourceType ].commitPending : false
})
});
} else {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, {
children: {
[action.payload.resourceId]: {
body: newBody,
}
},
transactionStartTime: null,
commitPending: false
})
});
}
}
/**
* Set the section validity.
*
* @param state
* the current state
* @param action
* an LoadSubmissionFormAction
* @return SubmissionObjectState
* the new state, with the section new validity status.
*/
function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])) {
let newChildren;
if (isNotUndefined(action.payload.resourceId)) {
// flush only specified child's operations
if (hasValue(state[ action.payload.resourceType ].children)
&& hasValue(state[ action.payload.resourceType ].children[ action.payload.resourceId ])) {
newChildren = Object.assign({}, state[ action.payload.resourceType ].children, {
[action.payload.resourceId]: {
body: state[ action.payload.resourceType ].children[ action.payload.resourceId ].body
.filter((entry) => entry.timeAdded > state[ action.payload.resourceType ].transactionStartTime)
}
});
} else {
newChildren = state[ action.payload.resourceType ].children;
}
} else {
// flush all children's operations
newChildren = state[ action.payload.resourceType ].children;
Object.keys(newChildren)
.forEach((resourceId) => {
newChildren = Object.assign({}, newChildren, {
[resourceId]: {
body: newChildren[ resourceId ].body
.filter((entry) => entry.timeAdded > state[ action.payload.resourceType ].transactionStartTime)
}
});
})
}
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: newChildren,
transactionStartTime: null,
commitPending: state[ action.payload.resourceType ].commitPending
})
});
} else {
return state;
}
}
function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?) {
const newBody = Array.from(body);
switch (actionType) {
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION:
newBody.push(makeOperationEntry({
op: JsonPatchOperationType.add,
path: targetPath,
value: value
}));
break;
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION:
newBody.push(makeOperationEntry({
op: JsonPatchOperationType.replace,
path: targetPath,
value: value
}));
break;
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION:
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath }));
break;
}
return newBody;
}
function makeOperationEntry(operation) {
return { operation: operation, timeAdded: new Date().getTime() };
}

View File

@@ -0,0 +1,127 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { hasValue, isEmpty, isNotEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util';
import { ErrorResponse, PostPatchSuccessResponse, RestResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { PatchRequest, RestRequest, SubmissionPatchRequest } from '../data/request.models';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { CoreState } from '../core.reducers';
import { Store } from '@ngrx/store';
import { jsonPatchOperationsByResourceType } from './selectors';
import { JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
import {
CommitPatchOperationsAction,
RollbacktPatchOperationsAction,
StartTransactionPatchOperationsAction
} from './json-patch-operations.actions';
import { JsonPatchOperationModel } from './json-patch.model';
@Injectable()
export class JsonPatchOperationsService<ResponseDefinitionDomain> {
protected linkPath;
constructor(protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected halService: HALEndpointService) {
}
protected submitData(request: RestRequest): Observable<ResponseDefinitionDomain> {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`Couldn't send data to server`))),
successResponse
.filter((response: PostPatchSuccessResponse) => isNotEmpty(response))
.map((response: PostPatchSuccessResponse) => response.dataDefinition)
.distinctUntilChanged());
}
protected submitJsonPatchOperations(hrefObs: Observable<string>, resourceType: string, resourceId?: string) {
let startTransactionTime = null;
const [patchRequestObs, emptyRequestObs] = hrefObs
.flatMap((endpointURL: string) => {
return this.store.select(jsonPatchOperationsByResourceType(resourceType))
.take(1)
.filter((operationsList: JsonPatchOperationsResourceEntry) => isUndefined(operationsList) || !(operationsList.commitPending))
.do(() => startTransactionTime = new Date().getTime())
.map((operationsList: JsonPatchOperationsResourceEntry) => {
const body: JsonPatchOperationModel[] = [];
if (isNotEmpty(operationsList)) {
if (isNotEmpty(resourceId)) {
if (isNotUndefined(operationsList.children[resourceId]) && isNotEmpty(operationsList.children[resourceId].body)) {
operationsList.children[resourceId].body.forEach((entry) => {
body.push(entry.operation);
});
}
} else {
Object.keys(operationsList.children)
.filter((key) => operationsList.children.hasOwnProperty(key))
.filter((key) => hasValue(operationsList.children[key]))
.filter((key) => hasValue(operationsList.children[key].body))
.forEach((key) => {
operationsList.children[key].body.forEach((entry) => {
body.push(entry.operation);
});
})
}
}
return new SubmissionPatchRequest(this.requestService.generateRequestId(), endpointURL, body);
});
})
.partition((request: PatchRequest) => isNotEmpty(request.body));
return Observable.merge(
emptyRequestObs
.filter((request: PatchRequest) => isEmpty(request.body))
.do(() => startTransactionTime = null)
.map(() => null),
patchRequestObs
.filter((request: PatchRequest) => isNotEmpty(request.body))
.do(() => this.store.dispatch(new StartTransactionPatchOperationsAction(resourceType, resourceId, startTransactionTime)))
.do((request: PatchRequest) => this.requestService.configure(request, true))
.flatMap((request: PatchRequest) => {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.filter((entry: ResponseCacheEntry) => startTransactionTime < entry.timeAdded)
.take(1)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse
.do(() => this.store.dispatch(new RollbacktPatchOperationsAction(resourceType, resourceId)))
.flatMap((response: ErrorResponse) => Observable.of(new Error(`Couldn't patch operations`))),
successResponse
.filter((response: PostPatchSuccessResponse) => isNotEmpty(response))
.do(() => this.store.dispatch(new CommitPatchOperationsAction(resourceType, resourceId)))
.map((response: PostPatchSuccessResponse) => response.dataDefinition)
.distinctUntilChanged());
})
);
}
protected getEndpointByIDHref(endpoint, resourceID): string {
return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
}
public jsonPatchByResourceType(linkName: string, scopeId: string, resourceType: string,) {
const hrefObs = this.halService.getEndpoint(linkName)
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId));
return this.submitJsonPatchOperations(hrefObs, resourceType);
}
public jsonPatchByResourceID(linkName: string, scopeId: string, resourceType: string, resourceId: string) {
const hrefObs = this.halService.getEndpoint(linkName)
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId));
return this.submitJsonPatchOperations(hrefObs, resourceType, resourceId);
}
}

View File

@@ -0,0 +1,14 @@
export enum JsonPatchOperationType {
test = 'test',
remove = 'remove',
add = 'add',
replace = 'replace',
move = 'move',
copy = 'copy',
}
export class JsonPatchOperationModel {
op: JsonPatchOperationType;
path: string;
value: any;
}

View File

@@ -0,0 +1,34 @@
// @TODO: Merge with keySelector function present in 'src/app/core/shared/selectors.ts'
import { createSelector, MemoizedSelector, Selector } from '@ngrx/store';
import { hasValue } from '../../shared/empty.util';
import { coreSelector, CoreState } from '../core.reducers';
import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer';
export function keySelector<T, V>(parentSelector: Selector<any, any>, subState: string, key: string): MemoizedSelector<T, V> {
return createSelector(parentSelector, (state: T) => {
if (hasValue(state[subState])) {
return state[subState][key];
} else {
return undefined;
}
});
}
export function subStateSelector<T, V>(parentSelector: Selector<any, any>, subState: string): MemoizedSelector<T, V> {
return createSelector(parentSelector, (state: T) => {
if (hasValue(state[subState])) {
return state[subState];
} else {
return undefined;
}
});
}
export function jsonPatchOperationsByResourceType(resourceType: string): MemoizedSelector<CoreState, JsonPatchOperationsResourceEntry> {
return keySelector<CoreState, JsonPatchOperationsResourceEntry>(coreSelector,'json/patch', resourceType);
}
export function jsonPatchOperationsByResourcId(resourceType: string, resourceId: string): MemoizedSelector<CoreState, JsonPatchOperationsEntry> {
const resourceTypeSelector = jsonPatchOperationsByResourceType(resourceType);
return subStateSelector<CoreState, JsonPatchOperationsEntry>(resourceTypeSelector, resourceId);
}

View File

@@ -3,6 +3,8 @@ import { Bitstream } from './bitstream.model';
import { Item } from './item.model';
import { RemoteData } from '../data/remote-data';
import { Observable } from 'rxjs/Observable';
import { License } from './license.model';
import { ResourcePolicy } from './resource-policy.model';
export class Collection extends DSpaceObject {
@@ -39,7 +41,7 @@ export class Collection extends DSpaceObject {
* The license of this Collection
* Corresponds to the metadata field dc.rights.license
*/
get license(): string {
get dcLicense(): string {
return this.findMetadata('dc.rights.license');
}
@@ -51,11 +53,21 @@ export class Collection extends DSpaceObject {
return this.findMetadata('dc.description.tableofcontents');
}
/**
* The deposit license of this Collection
*/
license: Observable<RemoteData<License>>;
/**
* The Bitstream that represents the logo of this Collection
*/
logo: Observable<RemoteData<Bitstream>>;
/**
* The default access conditions of this Collection
*/
defaultAccessConditions: Observable<RemoteData<ResourcePolicy[]>>;
/**
* An array of Collections that are direct parents of this Collection
*/

View File

@@ -0,0 +1,8 @@
export class AccessConditionOption {
name: string;
groupUUID: string;
hasStartDate: boolean;
hasEndDate: boolean;
maxStartDate: string;
maxEndDate: string;
}

View File

@@ -1,5 +1,4 @@
import { GenericConstructor } from '../../shared/generic-constructor';
import { GenericConstructor } from '../generic-constructor';
import { SubmissionSectionModel } from './config-submission-section.model';
import { SubmissionFormsModel } from './config-submission-forms.model';
@@ -7,6 +6,7 @@ import { SubmissionDefinitionsModel } from './config-submission-definitions.mode
import { ConfigType } from './config-type';
import { ConfigObject } from './config.model';
import { ConfigAuthorityModel } from './config-authority.model';
import { SubmissionUploadsModel } from './config-submission-uploads.model';
export class ConfigObjectFactory {
public static getConstructor(type): GenericConstructor<ConfigObject> {
@@ -23,6 +23,10 @@ export class ConfigObjectFactory {
case ConfigType.SubmissionSections: {
return SubmissionSectionModel
}
case ConfigType.SubmissionUpload:
case ConfigType.SubmissionUploads: {
return SubmissionUploadsModel
}
case ConfigType.Authority: {
return ConfigAuthorityModel
}

View File

@@ -1,5 +1,6 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { autoserialize, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model';
import { SectionsType } from '../../../submission/sections/sections-type';
@inheritSerialization(ConfigObject)
export class SubmissionSectionModel extends ConfigObject {
@@ -11,7 +12,7 @@ export class SubmissionSectionModel extends ConfigObject {
mandatory: boolean;
@autoserialize
sectionType: string;
sectionType: SectionsType;
@autoserialize
visibility: {

View File

@@ -0,0 +1,21 @@
import {autoserialize, autoserializeAs, inheritSerialization} from 'cerialize';
import { ConfigObject } from './config.model';
import { AccessConditionOption } from './config-access-condition-option.model';
import {SubmissionFormsModel} from './config-submission-forms.model';
@inheritSerialization(ConfigObject)
export class SubmissionUploadsModel extends ConfigObject {
@autoserialize
accessConditionOptions: AccessConditionOption[];
@autoserializeAs(SubmissionFormsModel)
metadata: SubmissionFormsModel[];
@autoserialize
required: boolean;
@autoserialize
maxSize: number;
}

View File

@@ -2,7 +2,6 @@
* TODO replace with actual string enum after upgrade to TypeScript 2.4:
* https://github.com/Microsoft/TypeScript/pull/15486
*/
import { ResourceType } from '../resource-type';
export enum ConfigType {
SubmissionDefinitions = 'submissiondefinitions',
@@ -11,5 +10,6 @@ export enum ConfigType {
SubmissionForms = 'submissionforms',
SubmissionSections = 'submissionsections',
SubmissionSection = 'submissionsection',
Authority = 'authority'
SubmissionUploads = 'submissionuploads',
SubmissionUpload = 'submissionupload',
}

View File

@@ -60,9 +60,9 @@ export class DSpaceObject implements CacheableObject, ListableObject {
* @return string
*/
findMetadata(key: string, language?: string): string {
const metadatum = this.metadata.find((m: Metadatum) => {
const metadatum = (this.metadata) ? this.metadata.find((m: Metadatum) => {
return m.key === key && (isEmpty(language) || m.language === language)
});
}) : null;
if (isNotEmpty(metadatum)) {
return metadatum.value;
} else {
@@ -81,7 +81,7 @@ export class DSpaceObject implements CacheableObject, ListableObject {
* @return Array<Metadatum>
*/
filterMetadata(keys: string[]): Metadatum[] {
return this.metadata.filter((metadatum: Metadatum) => {
return (this.metadata || []).filter((metadatum: Metadatum) => {
return keys.some((key) => key === metadatum.key);
});
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { HttpHeaders } from '@angular/common/http';
import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { RestRequestMethod } from '../data/request.models';
import { saveAs } from 'file-saver';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
@Injectable()
export class FileService {
constructor(
private restService: DSpaceRESTv2Service
) { }
downloadFile(url: string) {
const headers = new HttpHeaders();
const options: HttpOptions = Object.create({headers, responseType: 'blob'});
return this.restService.request(RestRequestMethod.Get, url, null, options)
.subscribe((data) => {
saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data));
});
}
/**
* Derives file name from the http response
* by looking inside content-disposition
* @param res http DSpaceRESTV2Response
*/
getFileNameFromResponseContentDisposition(res: DSpaceRESTV2Response) {
const contentDisposition = res.headers.get('content-disposition') || '';
const matches = /filename="([^;]+)"/ig.exec(contentDisposition) || [];
const fileName = (matches[1] || 'untitled').trim().replace(/\.[^/.]+$/, '');
return fileName;
};
}

View File

@@ -55,8 +55,7 @@ export class HALEndpointService {
return endpointMap[subPath];
} else {
/*TODO remove if/else block once the rest response contains _links for facets*/
currentPath += '/' + subPath;
return currentPath;
return currentPath + '/' + subPath;
}
}),
])

View File

@@ -88,8 +88,10 @@ export class Item extends DSpaceObject {
*/
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
return this.bitstreams
.filter((rd: RemoteData<Bitstream[]>) => rd.hasSucceeded)
.map((rd: RemoteData<Bitstream[]>) => rd.payload)
.filter((bitstreams: Bitstream[]) => hasValue(bitstreams))
.first()
.startWith([])
.map((bitstreams) => {
return bitstreams

View File

@@ -0,0 +1,14 @@
import { DSpaceObject } from './dspace-object.model';
export class License extends DSpaceObject {
/**
* Is the license custom?
*/
custom: boolean;
/**
* The text of the license
*/
text: string;
}

View File

@@ -38,9 +38,9 @@ export const getResourceLinksFromResponse = () =>
map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks),
);
export const configureRequest = (requestService: RequestService) =>
export const configureRequest = (requestService: RequestService, forceBypassCache?: boolean) =>
(source: Observable<RestRequest>): Observable<RestRequest> =>
source.pipe(tap((request: RestRequest) => requestService.configure(request)));
source.pipe(tap((request: RestRequest) => requestService.configure(request, forceBypassCache)));
export const getRemoteDataPayload = () =>
<T>(source: Observable<RemoteData<T>>): Observable<T> =>

View File

@@ -0,0 +1,14 @@
export enum PatchOperationType {
test = 'test',
remove = 'remove',
add = 'add',
replace = 'replace',
move = 'move',
copy = 'copy',
}
export class PatchOperationModel {
op: PatchOperationType;
path: string;
value: any;
}

View File

@@ -0,0 +1,34 @@
import { DSpaceObject } from './dspace-object.model';
export class ResourcePolicy extends DSpaceObject {
/**
* The action of the resource policy
*/
action: string;
/**
* The identifier of the resource policy
*/
id: string;
/**
* The group uuid bound to the resource policy
*/
groupUUID: string;
/**
* The end date of the resource policy
*/
endDate: string;
/**
* The start date of the resource policy
*/
startDate: string;
/**
* The type of the resource policy
*/
rpType: string
}

View File

@@ -8,4 +8,8 @@ export enum ResourceType {
Community = 'community',
Eperson = 'eperson',
Group = 'group',
ResourcePolicy = 'resourcePolicy',
License = 'license',
Workflowitem = 'workflowitem',
Workspaceitem = 'workspaceitem',
}

View File

@@ -0,0 +1,11 @@
import { autoserialize } from 'cerialize';
export class SubmitDataResponseDefinitionObject {
@autoserialize
public name: string;
@autoserialize
public type: string;
}

View File

@@ -0,0 +1,4 @@
import { Workspaceitem } from './workspaceitem.model';
export class EditItem extends Workspaceitem {
}

View File

@@ -0,0 +1,47 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { NormalizedWorkspaceItem } from './normalized-workspaceitem.model';
import { NormalizedSubmissionObject } from './normalized-submission-object.model';
import { ResourceType } from '../../shared/resource-type';
import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model';
import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
import { SubmissionObjectError } from './submission-object.model';
import { EditItem } from './edititem.model';
@mapsTo(EditItem)
@inheritSerialization(NormalizedWorkspaceItem)
export class NormalizedEditItem extends NormalizedSubmissionObject {
/**
* The item identifier
*/
@autoserialize
id: string;
/**
* The item last modified date
*/
@autoserialize
lastModified: Date;
@autoserialize
@relationship(ResourceType.Collection, true)
collection: string[];
@autoserialize
@relationship(ResourceType.Item, true)
item: string[];
@autoserialize
sections: WorkspaceitemSectionsObject;
@autoserializeAs(SubmissionDefinitionsModel)
submissionDefinition: SubmissionDefinitionsModel;
@autoserialize
@relationship(ResourceType.Eperson, true)
submitter: string[];
@autoserialize
errors: SubmissionObjectError[]
}

View File

@@ -0,0 +1,8 @@
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
/**
* An abstract model class for a DSpaceObject.
*/
export abstract class NormalizedSubmissionObject extends NormalizedDSpaceObject {
}

View File

@@ -0,0 +1,47 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { Workflowitem } from './workflowitem.model';
import { NormalizedWorkspaceItem } from './normalized-workspaceitem.model';
import { NormalizedSubmissionObject } from './normalized-submission-object.model';
import { ResourceType } from '../../shared/resource-type';
import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model';
import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
import { SubmissionObjectError } from './submission-object.model';
@mapsTo(Workflowitem)
@inheritSerialization(NormalizedWorkspaceItem)
export class NormalizedWorkflowItem extends NormalizedSubmissionObject {
/**
* The workspaceitem identifier
*/
@autoserialize
id: string;
/**
* The workspaceitem last modified date
*/
@autoserialize
lastModified: Date;
@autoserialize
@relationship(ResourceType.Collection, true)
collection: string[];
@autoserialize
@relationship(ResourceType.Item, true)
item: string[];
@autoserialize
sections: WorkspaceitemSectionsObject;
@autoserializeAs(SubmissionDefinitionsModel)
submissionDefinition: SubmissionDefinitionsModel;
@autoserialize
@relationship(ResourceType.Eperson, true)
submitter: string[];
@autoserialize
errors: SubmissionObjectError[]
}

View File

@@ -0,0 +1,51 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { Workspaceitem } from './workspaceitem.model';
import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
import { NormalizedSubmissionObject } from './normalized-submission-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { NormalizedCollection } from '../../cache/models/normalized-collection.model';
import { ResourceType } from '../../shared/resource-type';
import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model';
import { Eperson } from '../../eperson/models/eperson.model';
import { SubmissionObjectError } from './submission-object.model';
@mapsTo(Workspaceitem)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedWorkspaceItem extends NormalizedSubmissionObject {
/**
* The workspaceitem identifier
*/
@autoserialize
id: string;
/**
* The workspaceitem last modified date
*/
@autoserialize
lastModified: Date;
@autoserialize
@relationship(ResourceType.Collection, true)
collection: string[];
@autoserialize
@relationship(ResourceType.Item, true)
item: string[];
@autoserialize
sections: WorkspaceitemSectionsObject;
@autoserializeAs(SubmissionDefinitionsModel)
submissionDefinition: SubmissionDefinitionsModel;
@autoserialize
@relationship(ResourceType.Eperson, true)
submitter: string[];
@autoserialize
errors: SubmissionObjectError[]
}

View File

@@ -0,0 +1,43 @@
import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
import { RemoteData } from '../../data/remote-data';
import { Collection } from '../../shared/collection.model';
import { Item } from '../../shared/item.model';
import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model';
import { Observable } from 'rxjs/Observable';
import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
export interface SubmissionObjectError {
message: string,
paths: string[],
}
/**
* An abstract model class for a DSpaceObject.
*/
export abstract class SubmissionObject extends DSpaceObject implements CacheableObject, ListableObject {
/**
* The workspaceitem identifier
*/
id: string;
/**
* The workspaceitem last modified date
*/
lastModified: Date;
collection: Observable<RemoteData<Collection[]>> | Collection[];
item: Observable<RemoteData<Item[]>> | Item[];
sections: WorkspaceitemSectionsObject;
submissionDefinition: SubmissionDefinitionsModel;
submitter: Observable<RemoteData<Eperson[]>> | Eperson[];
errors: SubmissionObjectError[];
}

View File

@@ -0,0 +1,7 @@
export class SubmissionUploadFileAccessConditionObject {
id: string;
name: string;
groupUUID: string;
startDate: string;
endDate: string;
}

View File

@@ -0,0 +1,4 @@
import { Workspaceitem } from './workspaceitem.model';
export class Workflowitem extends Workspaceitem {
}

View File

@@ -0,0 +1,21 @@
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
import { FormFieldChangedObject } from '../../../shared/form/builder/models/form-field-unexpected-object.model';
import { DSpaceObject } from '../../shared/dspace-object.model';
export interface WorkspaceitemSectionDeduplicationObject {
matches: DeduplicationSchema[];
}
export interface DeduplicationSchema {
submitterDecision?: string; // [reject|verify]
submitterNote?: string;
submitterTime?: string; // (readonly)
workflowDecision?: string; // [reject|verify]
workflowNote?: string;
workflowTime?: string; // (readonly)
matchObject?: DSpaceObject; // item, workspaceItem, workflowItem
}

View File

@@ -0,0 +1,5 @@
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
export interface WorkspaceitemSectionFormObject {
[metadata: string]: FormFieldMetadataValueObject;
}

View File

@@ -0,0 +1,5 @@
export interface WorkspaceitemSectionLicenseObject {
url: string;
acceptanceDate: string;
granted: boolean;
}

View File

@@ -0,0 +1,8 @@
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
export interface WorkspaceitemSectionRecycleObject {
unexpected: any;
metadata: FormFieldMetadataValueObject[];
files: WorkspaceitemSectionUploadFileObject[];
}

View File

@@ -0,0 +1,15 @@
import { SubmissionUploadFileAccessConditionObject } from './submission-upload-file-access-condition.model';
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
export class WorkspaceitemSectionUploadFileObject {
uuid: string;
metadata: WorkspaceitemSectionFormObject;
sizeBytes: number;
checkSum: {
checkSumAlgorithm: string;
value: string;
};
url: string;
thumbnail: string;
accessConditions: SubmissionUploadFileAccessConditionObject[];
}

View File

@@ -0,0 +1,5 @@
import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model';
export interface WorkspaceitemSectionUploadObject {
files: WorkspaceitemSectionUploadFileObject[];
}

View File

@@ -0,0 +1,65 @@
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
import { isNotEmpty, isNotNull } from '../../../shared/empty.util';
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
import { WorkspaceitemSectionRecycleObject } from './workspaceitem-section-recycle.model';
import { WorkspaceitemSectionDeduplicationObject } from './workspaceitem-section-deduplication.model';
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
export class WorkspaceitemSectionsObject {
[name: string]: WorkspaceitemSectionDataType;
}
export function isServerFormValue(obj: any): boolean {
return (typeof obj === 'object'
&& obj.hasOwnProperty('value')
&& obj.hasOwnProperty('language')
&& obj.hasOwnProperty('authority')
&& obj.hasOwnProperty('confidence')
&& obj.hasOwnProperty('place'))
}
export function normalizeSectionData(obj: any) {
let result: any = obj;
if (isNotNull(obj)) {
// If is an Instance of FormFieldMetadataValueObject normalize it
if (typeof obj === 'object' && isServerFormValue(obj)) {
// If authority property is set normalize as a FormFieldMetadataValueObject object
/* NOTE: Data received from server could have authority property equal to null, but into form
field's model is required a FormFieldMetadataValueObject object as field value, so double-check in
field's parser and eventually instantiate it */
// if (isNotEmpty(obj.authority)) {
// result = new FormFieldMetadataValueObject(obj.value, obj.language, obj.authority, (obj.display || obj.value), obj.place, obj.confidence);
// } else if (isNotEmpty(obj.language)) {
// const languageValue = new FormFieldLanguageValueObject(obj.value, obj.language);
// result = languageValue;
// } else {
// // Normalize as a string value
// result = obj.value;
// }
result = new FormFieldMetadataValueObject(obj.value, obj.language, obj.authority, (obj.display || obj.value), obj.place, obj.confidence);
} else if (Array.isArray(obj)) {
result = [];
obj.forEach((item, index) => {
result[index] = normalizeSectionData(item);
});
} else if (typeof obj === 'object') {
result = Object.create({});
Object.keys(obj)
.forEach((key) => {
result[key] = normalizeSectionData(obj[key]);
});
}
}
return result;
}
export type WorkspaceitemSectionDataType
= WorkspaceitemSectionUploadObject
| WorkspaceitemSectionFormObject
| WorkspaceitemSectionLicenseObject
| WorkspaceitemSectionRecycleObject
| WorkspaceitemSectionDeduplicationObject
| string;

View File

@@ -0,0 +1,5 @@
import { SubmissionObject } from './submission-object.model';
export class Workspaceitem extends SubmissionObject {
}

View File

@@ -0,0 +1,69 @@
import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model';
import { SubmissionFormsModel } from '../shared/config/config-submission-forms.model';
import { SubmissionSectionModel } from '../shared/config/config-submission-section.model';
import { GenericConstructor } from '../shared/generic-constructor';
import { NormalizedBitstream } from '../cache/models/normalized-bitstream.model';
import { NormalizedBundle } from '../cache/models/normalized-bundle.model';
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { NormalizedItem } from '../cache/models/normalized-item.model';
import { NormalizedLicense } from '../cache/models/normalized-license.model';
import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ConfigObject } from '../shared/config/config.model';
import { SubmissionResourceType } from './submission-resource-type';
import { NormalizedResourcePolicy } from '../cache/models/normalized-resource-policy.model';
import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model';
import { NormalizedEditItem } from './models/normalized-edititem.model';
export class NormalizedSubmissionObjectFactory {
public static getConstructor(type: SubmissionResourceType): GenericConstructor<NormalizedObject | ConfigObject> {
switch (type) {
case SubmissionResourceType.Bitstream: {
return NormalizedBitstream
}
case SubmissionResourceType.Bundle: {
return NormalizedBundle
}
case SubmissionResourceType.Item: {
return NormalizedItem
}
case SubmissionResourceType.Collection: {
return NormalizedCollection
}
case SubmissionResourceType.Community: {
return NormalizedCommunity
}
case SubmissionResourceType.ResourcePolicy: {
return NormalizedResourcePolicy
}
case SubmissionResourceType.License: {
return NormalizedLicense
}
case SubmissionResourceType.WorkspaceItem: {
return NormalizedWorkspaceItem
}
case SubmissionResourceType.WorkflowItem: {
return NormalizedWorkflowItem
}
case SubmissionResourceType.EditItem: {
return NormalizedEditItem
}
case SubmissionResourceType.SubmissionDefinition:
case SubmissionResourceType.SubmissionDefinitions: {
return SubmissionDefinitionsModel
}
case SubmissionResourceType.SubmissionForm:
case SubmissionResourceType.SubmissionForms: {
return SubmissionFormsModel
}
case SubmissionResourceType.SubmissionSection:
case SubmissionResourceType.SubmissionSections: {
return SubmissionSectionModel
}
default: {
return undefined;
}
}
}
}

View File

@@ -0,0 +1,24 @@
/**
* TODO replace with actual string enum after upgrade to TypeScript 2.4:
* https://github.com/Microsoft/TypeScript/pull/15486
*/
export enum SubmissionResourceType {
Bundle = 'bundle',
Bitstream = 'bitstream',
BitstreamFormat = 'bitstreamformat',
Item = 'item',
Collection = 'collection',
Community = 'community',
ResourcePolicy = 'resourcePolicies',
License = 'license',
WorkspaceItem = 'workspaceitem',
WorkflowItem = 'workflowitem',
EditItem = 'edititem',
SubmissionDefinitions = 'submissiondefinitions',
SubmissionDefinition = 'submissiondefinition',
SubmissionForm = 'submissionform',
SubmissionForms = 'submissionforms',
SubmissionSections = 'submissionsections',
SubmissionSection = 'submissionsection',
Authority = 'authority'
}

View File

@@ -0,0 +1,99 @@
import { Inject, Injectable } from '@angular/core';
import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response-cache.models';
import { isEmpty, isNotEmpty, isNotNull } from '../../shared/empty.util';
import { ConfigObject } from '../shared/config/config.model';
import { BaseResponseParsingService, ProcessRequestDTO } 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 { NormalizedSubmissionObjectFactory } from './normalized-submission-object-factory';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { SubmissionResourceType } from './submission-resource-type';
import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model';
import { normalizeSectionData } from './models/workspaceitem-sections.model';
import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model';
import { NormalizedEditItem } from './models/normalized-edititem.model';
@Injectable()
export class SubmissionResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = NormalizedSubmissionObjectFactory;
protected toCache = false;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload)
&& isNotEmpty(data.payload._links)
&& (data.statusCode === '201' || data.statusCode === '200')) {
const dataDefinition = this.processResponse<NormalizedObject | ConfigObject, SubmissionResourceType>(data.payload, request.href);
return new SubmissionSuccessResponse(dataDefinition[Object.keys(dataDefinition)[0]], data.statusCode, this.processPageInfo(data.payload));
} else if (isEmpty(data.payload) && data.statusCode === '204') {
// Response from a DELETE request
return new SubmissionSuccessResponse(null, data.statusCode);
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from server'),
{statusText: data.statusCode}
)
);
}
}
protected processResponse<ObjectDomain, ObjectType>(data: any, requestHref: string): ProcessRequestDTO<ObjectDomain> {
const dataDefinition = this.process<NormalizedObject | ConfigObject, SubmissionResourceType>(data, requestHref);
const normalizedDefinition = Object.create({});
normalizedDefinition[Object.keys(dataDefinition)[0]] = [];
dataDefinition[Object.keys(dataDefinition)[0]].forEach((item, index) => {
let normalizedItem = Object.assign({}, item);
// In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form
if (item instanceof NormalizedWorkspaceItem
|| item instanceof NormalizedWorkflowItem
|| item instanceof NormalizedEditItem) {
if (item.sections) {
const precessedSection = Object.create({});
// Iterate over all workspaceitem's sections
Object.keys(item.sections)
.forEach((sectionId) => {
if (typeof item.sections[sectionId] === 'object' && isNotEmpty(item.sections[sectionId])) {
const normalizedSectionData = Object.create({});
// Iterate over all sections property
Object.keys(item.sections[sectionId])
.forEach((metdadataId) => {
const entry = item.sections[sectionId][metdadataId];
// If entry is not an array, for sure is not a section of type form
if (isNotNull(entry) && Array.isArray(entry)) {
normalizedSectionData[metdadataId] = [];
entry.forEach((valueItem) => {
// Parse value and normalize it
const normValue = normalizeSectionData(valueItem);
if (isNotEmpty(normValue)) {
normalizedSectionData[metdadataId].push(normValue);
}
});
} else {
normalizedSectionData[metdadataId] = entry;
}
});
precessedSection[sectionId] = normalizedSectionData;
}
});
normalizedItem = Object.assign({}, item, {sections: precessedSection});
}
}
normalizedDefinition[Object.keys(dataDefinition)[0]][index] = normalizedItem;
});
return normalizedDefinition as ProcessRequestDTO<ObjectDomain>;
}
}

View File

@@ -0,0 +1,5 @@
export enum SubmissionScopeType {
WorkspaceItem = 'WORKSPACE',
WorkflowItem = 'WORKFLOW',
EditItem = 'ITEM',
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model';
import { Workflowitem } from './models/workflowitem.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class WorkflowitemDataService extends DataService<NormalizedWorkflowItem, Workflowitem> {
protected linkPath = 'workflowitems';
protected forceBypassCache = true;
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected bs: BrowseService,
protected halService: HALEndpointService) {
super();
}
public getScopedEndpoint(scopeID: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { Workspaceitem } from './models/workspaceitem.model';
import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class WorkspaceitemDataService extends DataService<NormalizedWorkspaceItem, Workspaceitem> {
protected linkPath = 'workspaceitems';
protected forceBypassCache = true;
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected bs: BrowseService,
protected halService: HALEndpointService) {
super();
}
public getScopedEndpoint(scopeID: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
}

View File

@@ -0,0 +1,9 @@
<div *ngIf="!dismissed" class="alert {{type}} alert-dismissible fade show" role="alert" [@enterLeave]="animate">
<span *ngIf="content" [innerHTML]="content | translate"></span>
<ng-content></ng-content>
<button *ngIf="dismissible" type="button" class="close" data-dismiss="alert" aria-label="Close" (click)="dismiss()">
<span aria-hidden="true">&times;</span>
</button>
</div>

View File

@@ -0,0 +1,3 @@
.close:focus {
outline: none !important;
}

View File

@@ -0,0 +1,44 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { trigger } from '@angular/animations';
import { AlertType } from './aletrs-type';
import { fadeOutLeave, fadeOutState } from '../animations/fade';
@Component({
selector: 'ds-alert',
encapsulation: ViewEncapsulation.None,
animations: [
trigger('enterLeave', [
fadeOutLeave, fadeOutState,
])
],
templateUrl: './alerts.component.html',
styleUrls: ['./alerts.component.scss']
})
export class AlertsComponent {
@Input() content: string;
@Input() dismissible = false;
@Input() type: AlertType;
@Output() close: EventEmitter<any> = new EventEmitter<any>();
public animate = 'fadeIn';
public dismissed = false;
constructor(private cdr: ChangeDetectorRef) {
}
dismiss() {
if (this.dismissible) {
this.animate = 'fadeOut';
this.cdr.detectChanges();
setTimeout(() => {
this.dismissed = true;
this.close.emit();
this.cdr.detectChanges();
}, 300);
}
}
}

View File

@@ -0,0 +1,6 @@
export enum AlertType {
Success = 'alert-success',
Error = 'alert-danger',
Info = 'alert-info',
Warning = 'alert-warning'
}

View File

@@ -93,6 +93,51 @@ export const hasValueOperator = () =>
<T>(source: Observable<T>): Observable<T> =>
source.pipe(filter((obj: T) => hasValue(obj)));
/**
* Returns true if the passed value is null or undefined.
* hasUndefinedValue(); // false
* hasUndefinedValue(null); // false
* hasUndefinedValue(undefined); // false
* hasUndefinedValue(''); // true
* hasUndefinedValue({undefined, obj}); // true
* hasUndefinedValue([undefined, val]); // true
*/
export function hasUndefinedValue(obj?: any): boolean {
let result = false;
if (isUndefined(obj) || isNull(obj)) {
return false;
}
const objectType = typeof obj;
if (objectType === 'object') {
if (Object.keys(obj).length === 0) {
return false;
}
Object.entries(obj).forEach(([key, value]) => {
if (isUndefined(value)) {
result = true
}
})
}
return result;
}
/**
* Returns true if the passed value is null or undefined.
* hasUndefinedValue(); // true
* hasUndefinedValue(null); // true
* hasUndefinedValue(undefined); // true
* hasUndefinedValue(''); // false
* hasUndefinedValue({undefined, obj}); // false
* hasUndefinedValue([undefined, val]); // false
*/
export function hasNoUndefinedValue(obj?: any): boolean {
return !hasUndefinedValue(obj);
}
/**
* Verifies that a value is `null` or an empty string, empty array,
* or empty function.

View File

@@ -89,7 +89,8 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit {
this.chips = new Chips(
initChipsValue,
'value',
this.model.mandatoryField);
this.model.mandatoryField,
this.EnvConfig.submission.metadata.icons);
this.subs.push(
this.chips.chipsItems
.subscribe((subItems: any[]) => {

View File

@@ -10,6 +10,7 @@ import { FormFieldModel } from '../models/form-field.model';
import { ParserType } from './parser-type';
import { ParserOptions } from './parser-options';
import { ParserFactory } from './parser-factory';
import { TranslateService } from '@ngx-translate/core';
export const ROW_ID_PREFIX = 'df-row-group-config-';
@@ -49,7 +50,7 @@ export class RowParser {
if (parserCo) {
fieldModel = new parserCo(fieldData, this.initFormValues, parserOptions).parse();
} else {
throw new Error(`unknown form control model type defined with label "${fieldData.label}"`);
throw new Error(`unknown form control model type "${fieldData.input.type}" defined for Input field with label "${fieldData.label}".`, );
}
if (fieldModel) {

View File

@@ -55,8 +55,6 @@ import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dyn
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { TextMaskModule } from 'angular2-text-mask';
import { NotificationComponent } from './notifications/notification/notification.component';
import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component';
import { DragClickDirective } from './utils/drag-click.directive';
import { TruncatePipe } from './utils/truncate.pipe';
import { TruncatableComponent } from './truncatable/truncatable.component';
@@ -72,6 +70,7 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
import { AlertsComponent } from './alerts/alerts.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -104,6 +103,7 @@ const PIPES = [
const COMPONENTS = [
// put shared components here
AlertsComponent,
AuthNavMenuComponent,
ChipsComponent,
ComcolPageContentComponent,

View File

@@ -1,5 +1,6 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { TruncatableService } from '../truncatable.service';
import { hasValue } from '../../empty.util';
@Component({
selector: 'ds-truncatable-part',
@@ -34,6 +35,8 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.sub.unsubscribe();
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -10,7 +10,7 @@
}
.ds-base-drop-zone p {
height: 42px;
min-height: 42px;
}
.ds-document-drop-zone {

View File

@@ -0,0 +1,7 @@
<div class="submission-submit-container" >
<ds-submission-submit-form [collectionId]="collectionId"
[sections]="sections"
[selfUrl]="selfUrl"
[submissionDefinition]="submissionDefinition"
[submissionId]="submissionId"></ds-submission-submit-form>
</div>

Some files were not shown because too many files have changed in this diff Show More