diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index c2cc34cc0..5329a4a2b 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -25,7 +25,9 @@ import {BatchImportComponent} from "./richskill/import/batch-import.component" import { ActionByRoles, ButtonAction } from "./auth/auth-roles" import {MyWorkspaceComponent} from "./my-workspace/my-workspace.component" import {ConvertToCollectionComponent} from "./my-workspace/convert-to-collection/convert-to-collection.component" - +import { + BatchImportCollectionComponent +} from "./collection/create-collection/batch-import-collection/batch-import-collection.component" const routes: Routes = [ { path: "", redirectTo: "/skills", pathMatch: "full" }, @@ -106,6 +108,14 @@ const routes: Routes = [ }, canDeactivate: [FormDirtyGuard] }, + {path: "collections/create/batch-import", + component: BatchImportCollectionComponent, + canActivate: [AuthGuard], + data: { + roles: ActionByRoles.get(ButtonAction.CollectionCreate) + }, + canDeactivate: [FormDirtyGuard] + }, // collection search results {path: "collections/search", component: CollectionSearchResultsComponent, diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index a70f67182..abc1c1dd0 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -84,7 +84,11 @@ import {PublishCollectionComponent} from "./collection/detail/publish-collection import {BlockingLoaderComponent} from "./core/blocking-loader.component" import {CollectionSkillSearchComponent} from "./collection/collection-skill-search.component" import {BatchImportComponent} from "./richskill/import/batch-import.component" -import {FieldMappingSelectComponent, FieldMappingTableComponent} from "./richskill/import/field-mapping-table.component" +import { + FieldMappingSelectComponent, + FieldMappingTableComponent, + BatchImportDestinationSelectComponent +} from "./richskill/import/field-mapping-table.component" import { ImportPreviewTableComponent, NamedReferenceComponent @@ -111,6 +115,7 @@ import {OsmtTableModule} from "./table/osmt-table.module" import { getBaseApi } from "./api-versions" import { InlineHeadingComponent } from './richskill/import/inline-heading/inline-heading.component' import { InlineErrorComponent } from "./richskill/import/inline-error/inline-error.component" +import { BatchImportCollectionComponent } from './collection/create-collection/batch-import-collection/batch-import-collection.component' export function initializeApp( appConfig: AppConfig, @@ -217,8 +222,10 @@ export function initializeApp( CollectionSkillSearchComponent, BatchImportComponent, FieldMappingTableComponent, + BatchImportDestinationSelectComponent, FieldMappingSelectComponent, ImportPreviewTableComponent, + InlineHeadingComponent, NamedReferenceComponent, InlineErrorComponent, AuditLogComponent, @@ -229,7 +236,7 @@ export function initializeApp( CollectionPipe, ConvertToCollectionComponent, SizePaginationComponent, - InlineHeadingComponent, + BatchImportCollectionComponent, ], imports: [ NgIdleKeepaliveModule.forRoot(), diff --git a/ui/src/app/collection/create-collection/batch-import-collection/batch-import-collection.component.spec.ts b/ui/src/app/collection/create-collection/batch-import-collection/batch-import-collection.component.spec.ts new file mode 100644 index 000000000..c232e85e8 --- /dev/null +++ b/ui/src/app/collection/create-collection/batch-import-collection/batch-import-collection.component.spec.ts @@ -0,0 +1,83 @@ +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRouteStubSpec } from "@test/util/activated-route-stub.spec"; +import { Type } from "@angular/core"; +import { RouterTestingModule } from "@angular/router/testing"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { Location } from "@angular/common"; +import { Title } from "@angular/platform-browser"; +import { BatchImportCollectionComponent } from './batch-import-collection.component'; +import { AppConfig } from "../../../app.config"; +import { ToastService } from "../../../toast/toast.service"; +import { EnvironmentService } from "../../../core/environment.service"; +import { CollectionServiceStub, EnvironmentServiceStub, RouterStub } from "@test/resource/mock-stubs"; +import { CollectionService } from "../../service/collection.service"; + + +let activatedRoute: ActivatedRouteStubSpec +let component: BatchImportCollectionComponent +let fixture: ComponentFixture + +export function createComponent(T: Type): Promise { + fixture = TestBed.createComponent(T) + component = fixture.componentInstance + + // 1st change detection triggers ngOnInit which gets a hero + fixture.detectChanges() + + return fixture.whenStable().then(() => { + // 2nd change detection displays the async-fetched hero + fixture.detectChanges() + }) +} + +describe('BatchImportCollectionComponent', () => { + beforeEach(() => { + activatedRoute = new ActivatedRouteStubSpec() + }) + + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [BatchImportCollectionComponent], + imports: [ + RouterTestingModule, + HttpClientTestingModule + ], + providers: [ + AppConfig, + Location, + Title, + ToastService, + { provide: EnvironmentService, useClass: EnvironmentServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useClass: RouterStub }, + ] + }) + .compileComponents() + + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + + const environmentService = TestBed.inject(EnvironmentService) + environmentService.environment.editableAuthor = true + AppConfig.settings.editableAuthor = true // Doubly sure + + activatedRoute.setParams({ uuid: "uuid1" }) + createComponent(BatchImportCollectionComponent) + })); + + it("should be created", () => { + expect(component).toBeTruthy() + }) + + it("nameLabel should return", () => { + component.collectionUuid = "" + expect(component.nameLabel).toEqual("New Collection Name") + + component.collectionUuid = "uuid" + expect(component.nameLabel).toEqual("Collection Name") + }) +}); diff --git a/ui/src/app/collection/create-collection/batch-import-collection/batch-import-collection.component.ts b/ui/src/app/collection/create-collection/batch-import-collection/batch-import-collection.component.ts new file mode 100644 index 000000000..993c21f67 --- /dev/null +++ b/ui/src/app/collection/create-collection/batch-import-collection/batch-import-collection.component.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import { Title } from "@angular/platform-browser"; +import { Location } from "@angular/common"; +import { ActivatedRoute, Router } from "@angular/router"; +import { CollectionFormComponent } from "../collection-form.component"; +import { ExtrasSelectedSkillsState } from "../../add-skills-collection.component"; +import { ApiSearch, ApiSkillListUpdate } from "../../../richskill/service/rich-skill-search.service"; +import { CollectionService } from "../../service/collection.service"; +import { ToastService } from "../../../toast/toast.service"; + +@Component({ + selector: 'app-batch-import-collection', + templateUrl: "../collection-form.component.html" +}) +export class BatchImportCollectionComponent extends CollectionFormComponent { + + state?: ExtrasSelectedSkillsState + + constructor( + protected collectionService: CollectionService, + protected loc: Location, + protected router: Router, + protected route: ActivatedRoute, + protected toastService: ToastService, + protected titleService: Title + ) { + super(collectionService, loc, router, route, toastService, titleService) + this.state = this.router.getCurrentNavigation()?.extras.state as ExtrasSelectedSkillsState + } + + onSubmit(): void { + console.log(this.state?.selectedSkills?.length) + const updateObject = this.updateObject() + this.collectionSaved = this.collectionService.createCollection(updateObject) + const update = new ApiSkillListUpdate({ + add: new ApiSearch({uuids: this.state?.selectedSkills?.map(it => it.uuid) }) + }) + + if (this.collectionSaved) { + this.collectionSaved.subscribe(collection => { + this.collectionForm.markAsPristine() + this.collectionService.updateSkillsWithResult(collection.uuid, update, undefined).subscribe(result => { + if (result) { + const message = `You added ${result.modifiedCount} RSDs to the collection.` + this.toastService.showToast("Success!", message) + this.toastService.hideBlockingLoader() + this.router.navigate([`/collections/${collection.uuid}/manage`]) + } + }) + }) + } + } + +} diff --git a/ui/src/app/richskill/import/BatchImportOptionsEnum.ts b/ui/src/app/richskill/import/BatchImportOptionsEnum.ts new file mode 100644 index 000000000..7b7a216ef --- /dev/null +++ b/ui/src/app/richskill/import/BatchImportOptionsEnum.ts @@ -0,0 +1,5 @@ +export enum BatchImportOptionsEnum { + existing = "existing", + new = "new", + workspace = "workspace", +} diff --git a/ui/src/app/richskill/import/batch-import.component.html b/ui/src/app/richskill/import/batch-import.component.html index 22336dd73..a9549e301 100644 --- a/ui/src/app/richskill/import/batch-import.component.html +++ b/ui/src/app/richskill/import/batch-import.component.html @@ -24,8 +24,35 @@

Batch Import

+
+ +
+ + +
+ +
+ + + +
+
+ +
+
+ -
+
@@ -198,6 +225,9 @@

Import More RSDs + diff --git a/ui/src/app/richskill/import/batch-import.component.scss b/ui/src/app/richskill/import/batch-import.component.scss new file mode 100644 index 000000000..121fd156d --- /dev/null +++ b/ui/src/app/richskill/import/batch-import.component.scss @@ -0,0 +1,3 @@ +.t-import-margin-bottom { + margin-bottom: -133px; +} diff --git a/ui/src/app/richskill/import/batch-import.component.spec.ts b/ui/src/app/richskill/import/batch-import.component.spec.ts index 5c1c07a74..6deece2db 100644 --- a/ui/src/app/richskill/import/batch-import.component.spec.ts +++ b/ui/src/app/richskill/import/batch-import.component.spec.ts @@ -7,12 +7,14 @@ import { ActivatedRoute, Router } from "@angular/router" import { Papa, ParseResult } from "ngx-papaparse" import { ActivatedRouteStubSpec } from "test/util/activated-route-stub.spec" import { TestPage } from "test/util/test-page.spec" -import { EnvironmentServiceStub, RichSkillServiceStub } from "../../../../test/resource/mock-stubs" +import {CollectionServiceStub, EnvironmentServiceStub, RichSkillServiceStub} from "../../../../test/resource/mock-stubs" import { AppConfig } from "../../app.config" import { EnvironmentService } from "../../core/environment.service" import { ToastService } from "../../toast/toast.service" import { RichSkillService } from "../service/rich-skill.service" import { BatchImportComponent, ImportStep } from "./batch-import.component" +import { CollectionService } from "../../collection/service/collection.service"; +import { BatchImportOptionsEnum } from "./BatchImportOptionsEnum"; class Page extends TestPage { @@ -43,13 +45,11 @@ export function createComponent(T: Type): Promise { }) } - let activatedRoute: ActivatedRouteStubSpec let component: BatchImportComponent let fixture: ComponentFixture let page: Page - describe("BatchImportComponent", () => { beforeEach(() => { activatedRoute = new ActivatedRouteStubSpec() @@ -75,6 +75,7 @@ describe("BatchImportComponent", () => { { provide: ActivatedRoute, useValue: activatedRoute }, { provide: Router, useValue: routerSpy }, { provide: RichSkillService, useClass: RichSkillServiceStub }, + { provide: CollectionService, useClass: CollectionServiceStub } ] }) .compileComponents() @@ -318,6 +319,15 @@ describe("BatchImportComponent", () => { // Assert expect(component.importSimilarSkills).toBeFalse() }) + + it("getImportOptionButtonLabel() should return correct values", () => { + component.target = BatchImportOptionsEnum.new + expect(component.getImportOptionButtonLabel()).toEqual("Add to a new Collection") + component.target = BatchImportOptionsEnum.workspace + expect(component.getImportOptionButtonLabel()).toEqual("Add to Workspace") + component.target = BatchImportOptionsEnum.existing + expect(component.getImportOptionButtonLabel()).toEqual("Add to existing Collection") + }) }) diff --git a/ui/src/app/richskill/import/batch-import.component.ts b/ui/src/app/richskill/import/batch-import.component.ts index d66018b23..4c7e1d39e 100644 --- a/ui/src/app/richskill/import/batch-import.component.ts +++ b/ui/src/app/richskill/import/batch-import.component.ts @@ -17,6 +17,9 @@ import {forkJoin, Observable} from "rxjs" import {SvgHelper, SvgIcon} from "../../core/SvgHelper" import {Title} from "@angular/platform-browser"; import {AppConfig} from "../../app.config" +import { BatchImportOptionsEnum } from "./BatchImportOptionsEnum"; +import { ApiSearch, ApiSkillListUpdate } from "../service/rich-skill-search.service"; +import { CollectionService } from "../../collection/service/collection.service"; import { ApiSkillSummary } from "../ApiSkillSummary" @@ -39,6 +42,12 @@ export const importSkillHeaderOrder = [ {field: "occupations", label: "Occupations"}, {field: "employers", label: "Employers"}, ] + +export const importSkillTargetOptions = [ + {target: "existing", label: "Existing Collection"}, + {target: "new", label: "New Collection"}, + {target: "workspace", label: "User Workspace"}, +] export const allMappingHeaderOrder = (alignmentCount: number = 3): {field: string, label: string}[] => { const alignmentHeaders = [...Array(alignmentCount).keys()].map(i => { const label = (i > 0) ? ` ${i+1}` : "" @@ -127,6 +136,7 @@ export class BatchImportComponent extends QuickLinksHelper implements OnInit { previewSkills?: ApiSkillUpdate[] auditedSkills?: AuditedImportSkill[] importedSkills?: AuditedImportSkill[] + skillsToBeImported?: ApiSkill[] alignmentCount: number = 3 @@ -136,6 +146,7 @@ export class BatchImportComponent extends QuickLinksHelper implements OnInit { docIcon = SvgHelper.path(SvgIcon.DOC) isHover: boolean = false + target: string = "" get similarSkillCount(): number { return (this.similarSkills?.filter(it => it).length ?? 0) @@ -147,10 +158,12 @@ export class BatchImportComponent extends QuickLinksHelper implements OnInit { protected route: ActivatedRoute, protected location: Location, protected papa: Papa, - protected titleService: Title + protected titleService: Title, + protected collectionService: CollectionService ) { super() this.resetState() + this.route.queryParams.subscribe(params => this.target = params.to) } ngOnInit(): void { @@ -207,7 +220,6 @@ export class BatchImportComponent extends QuickLinksHelper implements OnInit { this.stepLoaded = undefined } - handleClickNext(): boolean { this.focusAndScrollIntoView(this.stepHeadingRef.nativeElement) this.showStepLoader() @@ -531,6 +543,7 @@ export class BatchImportComponent extends QuickLinksHelper implements OnInit { ).subscribe(results => { if (results) { this.hideStepLoader() + this.skillsToBeImported = results } }) @@ -539,5 +552,80 @@ export class BatchImportComponent extends QuickLinksHelper implements OnInit { handleSimilarityOk(importSimilar: boolean): void { this.importSimilarSkills = importSimilar } + + get validImportSkillsCount(): boolean { + return this.skillsToBeImported ? this.skillsToBeImported?.length > 0 : false + } + + protected handleClickAddToWorkspace(): void { + let skillListUpdate = new ApiSkillListUpdate({ + add: new ApiSearch( + {uuids:this.skillsToBeImported?.map(skill => skill.uuid)} + ) + }) + this.toastService.showBlockingLoader() + this.collectionService.getWorkspace().subscribe(workspace => { + this.collectionService.updateSkillsWithResult(workspace.uuid, skillListUpdate, undefined).subscribe(result => { + if (result) { + const message = `You added ${result.modifiedCount} RSDs to your workspace.` + this.toastService.showToast("Success!", message) + this.toastService.hideBlockingLoader() + } + }) + }) + } + + protected handleAddToExistingCollection() { + this.router.navigate(["/collections/add-skills"], + {state:{selectedSkills: this.skillsToBeImported, totalCount:this.skillsToBeImported?.length}} + ) + } + + protected handleAddToANewCollection() { + this.router.navigate(["/collections/create/batch-import"], + {state: {selectedSkills: this.skillsToBeImported, totalCount:this.skillsToBeImported?.length}} + ) + } + + protected getBatchImportAction() { + switch (this.target) { + case BatchImportOptionsEnum.existing: { + this.handleAddToExistingCollection() + break + } + case BatchImportOptionsEnum.new: { + this.handleAddToANewCollection() + break + } + case BatchImportOptionsEnum.workspace: { + this.handleClickAddToWorkspace() + break + } + default: { + break + } + } + } + + getImportOptionButtonLabel(): string { + switch (this.target) { + case BatchImportOptionsEnum.existing: { + return "Add to existing Collection" + } + case BatchImportOptionsEnum.new: { + return "Add to a new Collection" + } + case BatchImportOptionsEnum.workspace: { + return "Add to Workspace" + } + default: { + return "" + } + } + } + + protected updateTarget(destination: string) { + this.target = destination + } } diff --git a/ui/src/app/richskill/import/field-mapping-table.component.ts b/ui/src/app/richskill/import/field-mapping-table.component.ts index e162a4892..a155d7b93 100644 --- a/ui/src/app/richskill/import/field-mapping-table.component.ts +++ b/ui/src/app/richskill/import/field-mapping-table.component.ts @@ -1,5 +1,9 @@ import {Component, EventEmitter, Input, OnInit, Output} from "@angular/core"; -import {allMappingHeaderOrder, importSkillHeaderOrder, importSkillHeaders} from "./batch-import.component"; +import { + allMappingHeaderOrder, + importSkillTargetOptions, + importSkillHeaderOrder, +} from "./batch-import.component"; interface MappingChanged { @@ -71,3 +75,45 @@ export class FieldMappingSelectComponent { }) } } + +@Component({ + selector: "app-batch-import-destination-select", + template: ` + + +
Select Import Destination
+ + +
+ +
+ +
+
+ + ` +}) +export class BatchImportDestinationSelectComponent { + + @Input() data: string = "" + @Input() value: string = "" + @Output() mappingChanged = new EventEmitter() + + get optionElements(): {target: string, label: string}[] { + return importSkillTargetOptions + } + + handleChange($event: Event): void { + const target = $event.target as HTMLSelectElement + const value = target.value + this.mappingChanged.emit( + value + ) + } +}