Skip to content

Commit 8410074

Browse files
authored
Merge pull request DSpace#5135 from 4Science/task/main/DURACOM-456
[DSpace-CRIS] Bulk Import from Excel File (Frontend)
2 parents f7a95d9 + 7c415b0 commit 8410074

14 files changed

Lines changed: 630 additions & 1 deletion

src/app/app-routing-paths.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
export const COAR_NOTIFY_SUPPORT = 'coar-notify-support';
23

34
export const ADMIN_MODULE_PATH = 'admin';
@@ -56,4 +57,3 @@ export function getEditItemPageRoute() {
5657
return `/${EDIT_ITEM_PATH}`;
5758
}
5859
export const CORRECTION_TYPE_PATH = 'corrections';
59-

src/app/app.menus.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.me
1313
import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu';
1414
import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu';
1515
import { BrowseMenuProvider } from './shared/menu/providers/browse.menu';
16+
import { BulkImportMenuProvider } from './shared/menu/providers/bulk-import.menu';
1617
import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu';
1718
import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu';
1819
import { CommunityListMenuProvider } from './shared/menu/providers/community-list.menu';
@@ -112,6 +113,9 @@ export const MENUS = buildMenuStructure({
112113
ClaimMenuProvider.onRoute(
113114
MenuRoute.ITEM_PAGE,
114115
),
116+
BulkImportMenuProvider.onRoute(
117+
MenuRoute.COLLECTION_PAGE,
118+
),
115119
]),
116120
],
117121
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Route } from '@angular/router';
2+
3+
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
4+
import { bulkImportGuard } from './bulk-import.guard';
5+
import { BulkImportPageComponent } from './bulk-import-page.component';
6+
import { bulkImportPageResolver } from './bulk-import-page.resolver';
7+
8+
/**
9+
* RouterModule to help navigate to the page with the bulk import.
10+
*/
11+
export const ROUTES: Route[] = [
12+
{
13+
path: '',
14+
component: BulkImportPageComponent,
15+
resolve: {
16+
collection: bulkImportPageResolver,
17+
breadcrumb: i18nBreadcrumbResolver,
18+
},
19+
pathMatch: 'full',
20+
data: { title: 'bulk-import.title', breadcrumbKey: 'bulk-import' },
21+
canActivate: [bulkImportGuard],
22+
},
23+
];
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<div class="container">
2+
<h1 class="mb-3">{{ 'bulk-import.title' | translate }}</h1>
3+
<form [formGroup]="form" (ngSubmit)="submit()">
4+
<div class="form-group mb-2">
5+
<label for="name">{{ 'bulk-import.collection-name' | translate }}</label>
6+
<input type="text" class="form-control" id="name" formControlName="name">
7+
</div>
8+
<div class="form-group mb-2">
9+
<label for="file">{{ 'bulk-import.file' | translate }}</label>
10+
<input type="file" (change)="handleFileInput($event)" formControlName="file" id="file" class="form-control-file d-block" requireFile/>
11+
</div>
12+
<div class="form-check">
13+
<input type="checkbox" formControlName="abortOnError" id="abortOnError" class="form-check-input" />
14+
<label for="abortOnError" class="me-5">{{ 'bulk-import.abort-on-error' | translate }}</label>
15+
</div>
16+
17+
<button type="submit" class="btn btn-primary float-end" [dsBtnDisabled]="form.invalid" >
18+
<span>
19+
{{'bulk-import.submit' | translate}}
20+
</span>
21+
</button>
22+
<button type="button" class="btn btn-outline-secondary float-end me-2" (click)="goBack()">
23+
{{'bulk-import.back' | translate}}
24+
</button>
25+
26+
</form>
27+
</div>
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { CommonModule } from '@angular/common';
2+
import { NO_ERRORS_SCHEMA } from '@angular/core';
3+
import {
4+
ComponentFixture,
5+
fakeAsync,
6+
inject,
7+
TestBed,
8+
tick,
9+
} from '@angular/core/testing';
10+
import {
11+
FormsModule,
12+
ReactiveFormsModule,
13+
} from '@angular/forms';
14+
import { BrowserModule } from '@angular/platform-browser';
15+
import {
16+
ActivatedRoute,
17+
Router,
18+
} from '@angular/router';
19+
import { RouterTestingModule } from '@angular/router/testing';
20+
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
21+
import { ActivatedRouteStub } from '@dspace/core/testing/active-router.stub';
22+
import { NotificationsServiceStub } from '@dspace/core/testing/notifications-service.stub';
23+
import { getMockRequestService } from '@dspace/core/testing/request.service.mock';
24+
import { RouterMock } from '@dspace/core/testing/router.mock';
25+
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
26+
import { createSuccessfulRemoteDataObject } from '@dspace/core/utilities/remote-data.utils';
27+
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
28+
import {
29+
TranslateLoader,
30+
TranslateModule,
31+
} from '@ngx-translate/core';
32+
import { of } from 'rxjs';
33+
34+
import { AuthService } from '../core/auth/auth.service';
35+
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
36+
import { RestResponse } from '../core/cache/response.models';
37+
import { ScriptDataService } from '../core/data/processes/script-data.service';
38+
import { RequestService } from '../core/data/request.service';
39+
import { RequestEntry } from '../core/data/request-entry.model';
40+
import { Collection } from '../core/shared/collection.model';
41+
import { BulkImportPageComponent } from './bulk-import-page.component';
42+
43+
describe('BulkImportPageComponent', () => {
44+
45+
let component: BulkImportPageComponent;
46+
let fixture: ComponentFixture<BulkImportPageComponent>;
47+
48+
let requestService: any;
49+
let scriptDataService: any;
50+
let notificationsService: any;
51+
let route: any;
52+
let router: RouterMock;
53+
54+
const collection: Collection = Object.assign(new Collection(), {
55+
id: '626b80c5-ef15-4b29-8e69-bda89b0a7acf',
56+
name: 'Test collection',
57+
});
58+
59+
const file: File = new File(['test'], 'test.xls');
60+
61+
const fileList: any = {
62+
item: (index: number) => file,
63+
length: 10,
64+
};
65+
66+
const authService = jasmine.createSpyObj('authService', {
67+
isAuthenticated: of(true),
68+
setRedirectUrl: {},
69+
});
70+
71+
beforeEach(() => {
72+
requestService = getMockRequestService();
73+
scriptDataService = jasmine.createSpyObj('scriptDataService', {
74+
invoke: of(Object.assign(new RequestEntry(), {
75+
response: new RestResponse(true, 200, 'OK'),
76+
})),
77+
});
78+
notificationsService = new NotificationsServiceStub();
79+
80+
route = new ActivatedRouteStub({}, {
81+
collection: createSuccessfulRemoteDataObject( collection ),
82+
});
83+
router = new RouterMock();
84+
85+
TestBed.configureTestingModule({
86+
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule,
87+
TranslateModule.forRoot({
88+
loader: {
89+
provide: TranslateLoader,
90+
useClass: TranslateLoaderMock,
91+
},
92+
}), BulkImportPageComponent],
93+
providers: [
94+
BulkImportPageComponent,
95+
DSONameService,
96+
{ provide: RequestService, useValue: requestService },
97+
{ provide: RequestService, useValue: requestService },
98+
{ provide: ScriptDataService, useValue: scriptDataService },
99+
{ provide: NotificationsService, useValue: notificationsService },
100+
{ provide: ActivatedRoute, useValue: route },
101+
{ provide: Router, useValue: router },
102+
{ provide: AuthService, useValue: authService },
103+
],
104+
schemas: [NO_ERRORS_SCHEMA],
105+
}).compileComponents();
106+
107+
});
108+
109+
beforeEach(fakeAsync(() => {
110+
fixture = TestBed.createComponent(BulkImportPageComponent);
111+
component = fixture.componentInstance;
112+
fixture.detectChanges();
113+
tick();
114+
}));
115+
116+
it('should create BulkImportPageComponent', inject([BulkImportPageComponent], (comp: BulkImportPageComponent) => {
117+
expect(comp).toBeDefined();
118+
}));
119+
120+
describe('when the user submit the form', () => {
121+
122+
beforeEach(() => {
123+
component.form.value.abortOnError = true;
124+
component.form.value.file = fileList;
125+
component.setFile(fileList);
126+
component.submit();
127+
});
128+
129+
it('should invoke the bulk-import script', () => {
130+
expect(scriptDataService.invoke).toHaveBeenCalledWith('bulk-import', [
131+
{ name: '-c', value: '626b80c5-ef15-4b29-8e69-bda89b0a7acf' },
132+
{ name: '-f', value: 'test.xls' },
133+
{ name: '-er', value: true },
134+
], [file]);
135+
});
136+
137+
});
138+
139+
describe('when the user click on back button', () => {
140+
141+
beforeEach(() => {
142+
component.goBack();
143+
});
144+
145+
it('should nagivate to collection page', () => {
146+
expect(router.navigateByUrl).toHaveBeenCalledWith('/collections/626b80c5-ef15-4b29-8e69-bda89b0a7acf');
147+
});
148+
149+
});
150+
151+
});
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
Component,
3+
OnDestroy,
4+
OnInit,
5+
} from '@angular/core';
6+
import {
7+
FormBuilder,
8+
FormControl,
9+
FormGroup,
10+
ReactiveFormsModule,
11+
} from '@angular/forms';
12+
import {
13+
ActivatedRoute,
14+
Router,
15+
} from '@angular/router';
16+
import { NotificationsService } from '@dspace/core/notification-system/notifications.service';
17+
import { Process } from '@dspace/core/processes/process.model';
18+
import { ProcessParameter } from '@dspace/core/processes/process-parameter.model';
19+
import { getCollectionPageRoute } from '@dspace/core/router/utils/dso-route.utils';
20+
import {
21+
TranslateModule,
22+
TranslateService,
23+
} from '@ngx-translate/core';
24+
import { Subscription } from 'rxjs';
25+
import {
26+
map,
27+
take,
28+
} from 'rxjs/operators';
29+
import { BtnDisabledDirective } from 'src/app/shared/btn-disabled.directive';
30+
31+
import { AuthService } from '../core/auth/auth.service';
32+
import { DSONameService } from '../core/breadcrumbs/dso-name.service';
33+
import { ScriptDataService } from '../core/data/processes/script-data.service';
34+
import { RemoteData } from '../core/data/remote-data';
35+
import { RequestService } from '../core/data/request.service';
36+
import { redirectOn4xx } from '../core/shared/authorized.operators';
37+
import { Collection } from '../core/shared/collection.model';
38+
import { getFirstCompletedRemoteData } from '../core/shared/operators';
39+
import { FileValidator } from '../shared/utils/require-file.validator';
40+
41+
/**
42+
* Form values for bulk import page
43+
*/
44+
interface BulkImportFormValues {
45+
name: string;
46+
file?: File;
47+
abortOnError: boolean;
48+
}
49+
50+
/**
51+
* Page to perform an items bulk imports into the given collection.
52+
*/
53+
@Component({
54+
selector: 'ds-bulk-import-page',
55+
templateUrl: './bulk-import-page.component.html',
56+
imports: [
57+
BtnDisabledDirective,
58+
FileValidator,
59+
ReactiveFormsModule,
60+
TranslateModule,
61+
],
62+
})
63+
export class BulkImportPageComponent implements OnInit, OnDestroy {
64+
65+
collectionId: string;
66+
67+
form: FormGroup;
68+
69+
subs: Subscription[] = [];
70+
71+
private selectedFile: File;
72+
73+
constructor(
74+
private authService: AuthService,
75+
private formBuilder: FormBuilder,
76+
private dsoNameService: DSONameService,
77+
private scriptService: ScriptDataService,
78+
private notificationsService: NotificationsService,
79+
private translationService: TranslateService,
80+
private requestService: RequestService,
81+
private route: ActivatedRoute,
82+
private router: Router) {
83+
84+
}
85+
86+
ngOnInit(): void {
87+
88+
this.form = this.formBuilder.group({
89+
name: new FormControl({ value:'', disabled: true }),
90+
file: new FormControl(),
91+
abortOnError: new FormControl(false),
92+
});
93+
94+
this.subs.push(this.route.data.pipe(
95+
map((data) => data.collection as RemoteData<Collection>),
96+
redirectOn4xx(this.router, this.authService),
97+
take(1),
98+
).subscribe((remoteData) => {
99+
if (remoteData.payload) {
100+
const collection = remoteData.payload;
101+
this.collectionId = collection.id;
102+
this.form.controls.name.setValue(this.dsoNameService.getName(collection));
103+
}
104+
}));
105+
106+
}
107+
108+
/**
109+
* Validates the form, sets the parameters to correct values and invokes the script with the correct parameters
110+
* @param form
111+
*/
112+
submit() {
113+
if (!this.selectedFile) {
114+
this.notificationsService.error(this.translationService.get('bulk-import.error.no-file'));
115+
return;
116+
}
117+
118+
const values: BulkImportFormValues = this.form.value;
119+
120+
const stringParameters: ProcessParameter[] = [
121+
{ name: '-c', value: this.collectionId },
122+
{ name: '-f', value: this.selectedFile.name },
123+
];
124+
125+
if (values.abortOnError) {
126+
stringParameters.push( { name: '-er', value: values.abortOnError } );
127+
}
128+
129+
this.scriptService.invoke('bulk-import', stringParameters, [this.selectedFile])
130+
.pipe(getFirstCompletedRemoteData())
131+
.subscribe((rd: RemoteData<Process>) => {
132+
if (rd.isSuccess) {
133+
this.notificationsService.success(this.translationService.get('bulk-import.success'));
134+
this.navigateToProcesses();
135+
} else {
136+
this.notificationsService.error(this.translationService.get('bulk-import.error'));
137+
}
138+
});
139+
}
140+
141+
goBack(): void {
142+
this.router.navigateByUrl(getCollectionPageRoute(this.collectionId));
143+
}
144+
145+
146+
private navigateToProcesses() {
147+
this.requestService.setStaleByHrefSubstring('/processes');
148+
this.router.navigateByUrl('/processes');
149+
}
150+
151+
ngOnDestroy(): void {
152+
this.subs.forEach((sub) => sub.unsubscribe());
153+
}
154+
155+
public handleFileInput(event: Event) {
156+
const input = event.target as HTMLInputElement;
157+
if (input.files && input.files.length > 0) {
158+
this.setFile(input.files);
159+
}
160+
}
161+
162+
public setFile(files: FileList) {
163+
this.selectedFile = files.length > 0 ? files.item(0) : undefined;
164+
}
165+
166+
}

0 commit comments

Comments
 (0)