diff --git a/ui/angular.json b/ui/angular.json index 05cdfc9e3..4487e4895 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -49,6 +49,7 @@ } ], "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "node_modules/@concentricsky/wgu-design-system-patternlibrary/dist/css/screen.css", "src/styles.scss" ], @@ -126,6 +127,7 @@ "src/assets" ], "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" ], "scripts": [], @@ -145,9 +147,6 @@ } }, "cli": { - "analytics": false, - "schematicCollections": [ - "@angular-eslint/schematics" - ] + "analytics": false } } diff --git a/ui/package-lock.json b/ui/package-lock.json index fa0f68d4e..ab18c14c0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -587,6 +587,28 @@ "@angular/core": "16.0.4" } }, + "node_modules/@angular/cdk": { + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "optionalDependencies": { + "parse5": "^5.0.0" + }, + "peerDependencies": { + "@angular/common": "^12.0.0 || ^13.0.0-0", + "@angular/core": "^12.0.0 || ^13.0.0-0", + "rxjs": "^6.5.3 || ^7.0.0" + } + }, + "node_modules/@angular/cdk/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "optional": true + }, "node_modules/@angular/cli": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.0.4.tgz", diff --git a/ui/package.json b/ui/package.json index 26031491d..2e649917f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "@angular/compiler": "^16.0.2", "@angular/core": "^16.0.2", "@angular/forms": "^16.0.2", + "@angular/material": "^16.0.2", "@angular/platform-browser": "^16.0.2", "@angular/platform-browser-dynamic": "^16.0.2", "@angular/router": "^16.0.2", @@ -45,6 +46,7 @@ "@angular-eslint/eslint-plugin-template": "16.0.2", "@angular-eslint/schematics": "16.0.2", "@angular-eslint/template-parser": "16.0.2", + "@angular/cdk": "^16.0.2", "@angular/cli": "^16.0.2", "@angular/compiler-cli": "^16.0.2", "@types/jasmine": "~4.3.2", diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index c2cc34cc0..2622c73e2 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -25,6 +25,7 @@ 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 { MetadataListComponent } from "./metadata/detail/metadata-list/metadata-list.component" const routes: Routes = [ @@ -181,6 +182,11 @@ const routes: Routes = [ roles: ActionByRoles.get(ButtonAction.MyWorkspace) } }, + { + path: "metadata", + component: MetadataListComponent, + canActivate: [AuthGuard], + }, /* PUBLIC VIEWS */ {path: "skills/:uuid", component: RichSkillPublicComponent}, {path: "collections/:uuid", component: CollectionPublicComponent}, diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 204856367..f28be2974 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -16,8 +16,8 @@ import {HeaderComponent} from "./navigation/header.component" import {FooterComponent} from "./navigation/footer.component" import {SkillCollectionsDisplayComponent} from "./richskill/form/skill-collections-display.component" import {ToastComponent} from "./toast/toast.component" -import {PillComponent} from "./core/pill/pill.component"; -import {PillGroupComponent} from "./core/pill/group/pill-group.component"; +import {PillComponent} from "./core/pill/pill.component" +import {PillGroupComponent} from "./core/pill/group/pill-group.component" import {AuthService} from "./auth/auth-service" import {AuthGuard} from "./auth/auth.guard" import {CommoncontrolsComponent} from "./navigation/commoncontrols.component" @@ -110,6 +110,14 @@ import { OsmtFormModule } from "./form/osmt-form.module" import { ConvertToCollectionComponent } from "./my-workspace/convert-to-collection/convert-to-collection.component" import { SizePaginationComponent } from "./table/skills-library-table/size-pagination/size-pagination.component" import {OsmtTableModule} from "./table/osmt-table.module" +import { NoopAnimationsModule } from "@angular/platform-browser/animations" +import { MatMenuModule } from "@angular/material/menu" +import { MetadataListComponent } from "./metadata/detail/metadata-list/metadata-list.component" +import { JobCodeListRowComponent } from "./metadata/job-codes/job-code-list-row/job-code-list-row.component" +import { JobCodeTableComponent } from "./metadata/job-codes/job-code-table/job-code-table.component" +import { NamedReferenceListRowComponent } from "./metadata/named-references/named-reference-list-row/named-reference-list-row.component" +import { NamedReferenceTableComponent } from "./metadata/named-references/named-reference-table/named-reference-table.component" +import { MetadataSelectorComponent } from "./metadata/detail/metadata-selector/metadata-selector.component" export function initializeApp( appConfig: AppConfig, @@ -229,6 +237,12 @@ export function initializeApp( CollectionPipe, ConvertToCollectionComponent, SizePaginationComponent, + MetadataListComponent, + JobCodeListRowComponent, + JobCodeTableComponent, + NamedReferenceListRowComponent, + NamedReferenceTableComponent, + MetadataSelectorComponent, ], imports: [ NgIdleKeepaliveModule.forRoot(), @@ -241,7 +255,9 @@ export function initializeApp( OsmtCoreModule, OsmtFormModule, FormsModule, - OsmtTableModule + OsmtTableModule, + NoopAnimationsModule, + MatMenuModule ], providers: [ EnvironmentService, diff --git a/ui/src/app/auth/auth-roles.ts b/ui/src/app/auth/auth-roles.ts index 80916fa18..791d54882 100644 --- a/ui/src/app/auth/auth-roles.ts +++ b/ui/src/app/auth/auth-roles.ts @@ -14,7 +14,8 @@ export enum ButtonAction { LibraryExport, ExportDraftCollection, DeleteCollection, - MyWorkspace + MyWorkspace, + MetadataAdmin, } export const ActionByRoles = new Map([ @@ -28,7 +29,8 @@ export const ActionByRoles = new Map([ [ButtonAction.LibraryExport, [OSMT_ADMIN]], [ButtonAction.ExportDraftCollection, [OSMT_ADMIN]], [ButtonAction.DeleteCollection, [OSMT_ADMIN]], - [ButtonAction.MyWorkspace, [OSMT_ADMIN, OSMT_CURATOR]] + [ButtonAction.MyWorkspace, [OSMT_ADMIN, OSMT_CURATOR]], + [ButtonAction.MetadataAdmin, [OSMT_ADMIN]], ]) //TODO migrate AuthServiceWgu & AuthService.hasRole & isEnabledByRoles into a singleton here. HDN Sept 15, 2022 diff --git a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.spec.ts b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.spec.ts index e01ea87ce..418eef5c1 100644 --- a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.spec.ts +++ b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.spec.ts @@ -10,7 +10,7 @@ import {EnvironmentService} from "src/app/core/environment.service" import {ActivatedRouteStubSpec} from "test/util/activated-route-stub.spec" import {KeywordSearchService} from "../../../richskill/service/keyword-search.service" import {KeywordSearchServiceStub} from "../../../../../test/resource/mock-stubs" -import {ApiJobCode, IJobCode} from "../../../job-codes/Jobcode" +import { ApiJobCode, IJobCode } from "../../../metadata/job-codes/Jobcode" import {FormFieldJobCodeSearchMultiSelect} from "./form-field-jobcode-search-multi-select.component" @Component({ diff --git a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.ts b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.ts index 318b6d117..41f67a185 100644 --- a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.ts +++ b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-multi-select.component.ts @@ -2,7 +2,7 @@ import {Component} from "@angular/core" import {Observable} from "rxjs" import {map} from "rxjs/operators" import {KeywordSearchService} from "../../../richskill/service/keyword-search.service" -import {IJobCode} from "../../../job-codes/Jobcode" +import { IJobCode } from "../../../metadata/job-codes/Jobcode" import {isJobCode} from "./form-field-jobcode-search-select.utilities" import {areSearchResultsEqual, labelFor, searchResultFromString} from "./form-field-jobcode-search-select.utilities" import {AbstractFormFieldSearchMultiSelect} from "../abstract-form-field-search-multi-select.component" diff --git a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.spec.ts b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.spec.ts index f5faad8a7..1e6f9e306 100644 --- a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.spec.ts +++ b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.spec.ts @@ -10,7 +10,7 @@ import {EnvironmentService} from "src/app/core/environment.service" import {ActivatedRouteStubSpec} from "test/util/activated-route-stub.spec" import {KeywordSearchService} from "../../../richskill/service/keyword-search.service" import {KeywordSearchServiceStub} from "../../../../../test/resource/mock-stubs" -import {ApiJobCode, IJobCode} from "../../../job-codes/Jobcode" +import { ApiJobCode, IJobCode } from "../../../metadata/job-codes/Jobcode" import {FormFieldJobCodeSearchSelect} from "./form-field-jobcode-search-select.component" @Component({ diff --git a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.ts b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.ts index feedab67b..9a35e54d2 100644 --- a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.ts +++ b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.component.ts @@ -2,7 +2,7 @@ import {Component} from "@angular/core" import {Observable} from "rxjs" import {map} from "rxjs/operators" import {KeywordSearchService} from "../../../richskill/service/keyword-search.service" -import {IJobCode} from "../../../job-codes/Jobcode" +import { IJobCode } from "../../../metadata/job-codes/Jobcode" import {isJobCode} from "./form-field-jobcode-search-select.utilities" import {areSearchResultsEqual, labelFor, searchResultFromString} from "./form-field-jobcode-search-select.utilities" import {AbstractFormFieldSearchSingleSelect} from "../abstract-form-field-search-single-select.component" diff --git a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.spec.ts b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.spec.ts index 4b5fd4e9c..f6918249d 100644 --- a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.spec.ts +++ b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.spec.ts @@ -1,6 +1,6 @@ // noinspection LocalVariableNamingConventionJS import {async, ComponentFixture, TestBed} from "@angular/core/testing" -import {IJobCode} from "../../../job-codes/Jobcode" +import { IJobCode } from "../../../metadata/job-codes/Jobcode" import {createMockJobcode} from "../../../../../test/resource/mock-data" import { areSearchResultsEqual, diff --git a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.ts b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.ts index 97cb1e69b..a5cf2f12d 100644 --- a/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.ts +++ b/ui/src/app/form/form-field-search-select/jobcode/form-field-jobcode-search-select.utilities.ts @@ -1,4 +1,4 @@ -import {IJobCode} from "../../../job-codes/Jobcode" +import { IJobCode } from "../../../metadata/job-codes/Jobcode" export const areSearchResultsEqual = (result1: IJobCode, result2: IJobCode): boolean => { if (result1 && result2) { diff --git a/ui/src/app/metadata/PaginatedMetadata.ts b/ui/src/app/metadata/PaginatedMetadata.ts new file mode 100644 index 000000000..fb5bb9373 --- /dev/null +++ b/ui/src/app/metadata/PaginatedMetadata.ts @@ -0,0 +1,11 @@ +import { ApiJobCode } from "./job-codes/Jobcode" +import { ApiNamedReference } from "./named-references/NamedReference" + +export class PaginatedMetadata { + totalCount = 0 + metadata: ApiJobCode[]|ApiNamedReference[] = [] + constructor(metadata: ApiJobCode[]|ApiNamedReference[], totalCount: number) { + this.metadata = metadata + this.totalCount = totalCount + } +} diff --git a/ui/src/app/metadata/abstract-data.service.spec.ts b/ui/src/app/metadata/abstract-data.service.spec.ts new file mode 100644 index 000000000..af5423540 --- /dev/null +++ b/ui/src/app/metadata/abstract-data.service.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from "@angular/core/testing" +import { Router } from "@angular/router" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { Location } from "@angular/common" +import { AbstractDataService } from "./abstract-data.service" +import { EnvironmentService } from "../core/environment.service" +import { AppConfig } from "../app.config" +import { AuthService } from "../auth/auth-service" +import { AuthServiceStub, RouterStub } from "@test/resource/mock-stubs" + +describe("AbstractAdminService", () => { + let testService: AbstractDataService + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + EnvironmentService, + AppConfig, + AbstractDataService, + Location, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: Router, useClass: RouterStub } + ]}) + testService = TestBed.inject(AbstractDataService) + }) + + it("should be created", () => { + expect(testService).toBeTruthy() + }) +}) diff --git a/ui/src/app/metadata/abstract-data.service.ts b/ui/src/app/metadata/abstract-data.service.ts new file mode 100644 index 000000000..0f68b1c10 --- /dev/null +++ b/ui/src/app/metadata/abstract-data.service.ts @@ -0,0 +1,37 @@ +import { Location } from "@angular/common" +import { HttpClient, HttpResponse } from "@angular/common/http" +import { Injectable } from "@angular/core" +import { Router } from "@angular/router" +import { Observable } from "rxjs" +import { share } from "rxjs/operators" +import { AbstractService, ApiGetParams } from "../abstract.service" +import { AuthService } from "../auth/auth-service" + +@Injectable({ providedIn: "root" }) +export abstract class AbstractDataService extends AbstractService { + + protected constructor(httpClient: HttpClient, authService: AuthService, router: Router, location: Location) { + super(httpClient, authService, router, location) + } + + /** + * Perform a patch request. + * + * const {body, headers, status, type, url} = response + * + * @param path The relative path to the endpoint + * @param headers Json blob defining headers + * @param params Json blob defining path params + * @param body Json blob defining the changes to be applied to the object + */ + patch({path, headers, params, body}: ApiGetParams): Observable> { + const observable = this.httpClient.patch(this.buildUrl(path + "/update"), body, { + headers: this.wrapHeaders(headers), + params, + observe: "response"}).pipe(share()) + observable + .subscribe(() => {}, (err) => { this.redirectToLogin(err) }) + return observable + } + +} diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html new file mode 100644 index 000000000..ef3cea520 --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.html @@ -0,0 +1,83 @@ + + + + diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts new file mode 100644 index 000000000..7f7c951ab --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" +import { MetadataListComponent } from "./metadata-list.component" +import { MetadataType } from "../../rsd-metadata.enum" +import { PaginatedMetadata } from "../../PaginatedMetadata" +import { ApiJobCode } from "../../job-codes/Jobcode" +import { AuthServiceStub } from "@test/resource/mock-stubs"; +import { AuthService } from "../../../auth/auth-service"; + +describe("ManageMetadataComponent", () => { + let component: MetadataListComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MetadataListComponent ], + providers: [ + { provide: AuthService, useClass: AuthServiceStub }, + ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataListComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) + it("isJobCodeDataSelected returns false if JobCode MetadataType is not selected", () => { + expect(component.isJobCodeDataSelected).toBeFalse() + }) + it("isJobCodeDataSelected returns true if JobCode MetadataType is selected", () => { + component.selectedMetadataType = MetadataType.JobCode + expect(component.isJobCodeDataSelected).toBeTrue() + }) + it("emptyResults returns true if Metadata is empty", () => { + component.results = new PaginatedMetadata([], 0) + expect(component.emptyResults).toBeTrue() + }) + it("emptyResults returns false if Metadata is not empty", () => { + component.results = new PaginatedMetadata([new ApiJobCode(), new ApiJobCode()], 2) + expect(component.emptyResults).toBeFalse() + }) +}) diff --git a/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts new file mode 100644 index 000000000..461069dc7 --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-list/metadata-list.component.ts @@ -0,0 +1,196 @@ +import {Component, OnInit, ViewChild} from "@angular/core" +import { FormControl, FormGroup } from "@angular/forms" +import { Observable, Subject } from "rxjs" +import { PaginatedMetadata } from "../../PaginatedMetadata" +import { ApiSortOrder } from "../../../richskill/ApiSkill" +import { ApiJobCode, IJobCode } from "../../job-codes/Jobcode" +import { TableActionBarComponent } from "../../../table/skills-library-table/table-action-bar.component" +import { Whitelabelled } from "../../../../whitelabel" +import { ApiNamedReference, INamedReference } from "../../named-references/NamedReference" +import { TableActionDefinition } from "../../../table/skills-library-table/has-action-definitions" +import { ButtonAction } from "../../../auth/auth-roles" +import { AuthService } from "../../../auth/auth-service" +import { MetadataType } from "../../rsd-metadata.enum" + +@Component({ + selector: "app-metadata-list", + templateUrl: "./metadata-list.component.html" +}) +export class MetadataListComponent extends Whitelabelled implements OnInit { + + @ViewChild(TableActionBarComponent) actionBar!: TableActionBarComponent + + title = "Metadata" + handleSelectedMetadata?: IJobCode[]|INamedReference[] + selectedMetadataType = "category" + matchingQuery?: string[] + + typeControl: FormControl = new FormControl(this.selectedMetadataType) + columnSort: ApiSortOrder = ApiSortOrder.NameAsc + + from = 0 + size = 50 + showSearchEmptyMessage = false + resultsLoaded: Observable | undefined + canDeleteMetadata = this.authService.isEnabledByRoles(ButtonAction.MetadataAdmin) + + searchForm = new FormGroup({ + search: new FormControl("") + }) + sampleJobCodeResult = new PaginatedMetadata([ + new ApiJobCode({code: "code1", targetNodeName: "targetNodeName1", frameworkName: "frameworkName1", url: "url1", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code2", targetNodeName: "targetNodeName2", frameworkName: "frameworkName2", url: "url2", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code3", targetNodeName: "targetNodeName3", frameworkName: "frameworkName3", url: "url3", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code4", targetNodeName: "targetNodeName4", frameworkName: "frameworkName4", url: "url4", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code5", targetNodeName: "targetNodeName5", frameworkName: "frameworkName5", url: "url5", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code6", targetNodeName: "targetNodeName6", frameworkName: "frameworkName6", url: "url6", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code7", targetNodeName: "targetNodeName7", frameworkName: "frameworkName7", url: "url7", broad: "broad1", level: "Broad"}), + new ApiJobCode({code: "code8", targetNodeName: "targetNodeName8", frameworkName: "frameworkName8", url: "url8", broad: "broad1", level: "Broad"}), + ], 8) + + sampleNamedReferenceResult = new PaginatedMetadata([ + new ApiNamedReference({id: "id1", framework: "framework1", name: "name1", type: MetadataType.Category, value: "value1"}), + new ApiNamedReference({id: "id2", framework: "framework2", name: "name2", type: MetadataType.Category, value: "value2"}), + new ApiNamedReference({id: "id3", framework: "framework3", name: "name3", type: MetadataType.Category, value: "value3"}), + new ApiNamedReference({id: "id4", framework: "framework4", name: "name4", type: MetadataType.Category, value: "value4"}), + new ApiNamedReference({id: "id5", framework: "framework5", name: "name5", type: MetadataType.Category, value: "value5"}), + new ApiNamedReference({id: "id6", framework: "framework6", name: "name6", type: MetadataType.Category, value: "value6"}), + new ApiNamedReference({id: "id7", framework: "framework7", name: "name7", type: MetadataType.Category, value: "value7"}), + new ApiNamedReference({id: "id8", framework: "framework8", name: "name8", type: MetadataType.Category, value: "value8"}), + ], 8) + + results: PaginatedMetadata = this.sampleNamedReferenceResult + + clearSelectedItemsFromTable = new Subject() + constructor(protected authService: AuthService) { + super() + } + + ngOnInit(): void { + this.typeControl.valueChanges.subscribe( + value => { + this.selectedMetadataType = value + if (this.selectedMetadataType === MetadataType.JobCode) { + this.results = this.sampleJobCodeResult + } + else { + this.results = this.sampleNamedReferenceResult + } + }) + this.searchForm.get("search")?.valueChanges.subscribe( value => this.matchingQuery = value) + } + + clearSearch(): boolean { + this.searchForm.reset() + return false + } + + handleDefaultSubmit(): boolean { + this.loadNextPage() + this.from = 0 + + return false + } + loadNextPage(): void {} + handleSelectAll(selectAllChecked: boolean): void {} + + handleNewSelection(selected: IJobCode[]|INamedReference[]): void { + this.handleSelectedMetadata = selected + } + + handleHeaderColumnSort(sort: ApiSortOrder): void { + this.columnSort = sort + this.from = 0 + this.loadNextPage() + } + + get totalCount(): number { + return this.results?.totalCount ?? 0 + } + + get metadataCountLabel(): string { + if (this.totalCount > 0) { + if (this.selectedMetadataType !== MetadataType.Category) { + return `${this.totalCount} ${this.selectedMetadataType}${this.totalCount > 1 ? "s" : ""}` + } + else if (this.totalCount > 1) { + return `${this.totalCount} categories` + } + else { + return `${this.totalCount} category` + } + } + return `0 ${this.selectedMetadataType}s` + } + get firstRecordNo(): number { + return this.from + 1 + } + get lastRecordNo(): number { + return Math.min(this.from + this.curPageCount, this.totalCount) + } + + get totalPageCount(): number { + return Math.ceil(this.totalCount / this.size) + } + get currentPageNo(): number { + return Math.floor(this.from / this.size) + 1 + } + + get curPageCount(): number { + return this.results?.metadata.length ?? 0 + } + + get emptyResults(): boolean { + return this.curPageCount < 1 + } + get isJobCodeDataSelected(): boolean { + console.log(this.selectedMetadataType === MetadataType.JobCode.toString()) + return this.selectedMetadataType === MetadataType.JobCode + } + + getSelectAllCount(): number { + return this.curPageCount + } + + getSelectAllEnabled(): boolean { + return true + } + + focusActionBar(): void { + this.actionBar?.focus() + } + getJobCodes(): IJobCode[] { + return (this.results?.metadata) as IJobCode[] + } + + getNamedReferences(): INamedReference[] { + return (this.results?.metadata) as INamedReference[] + } + public get searchQuery(): string { + return this.searchForm.get("search")?.value ?? "" + } + + navigateToPage(newPageNo: number): void { + this.from = (newPageNo - 1) * this.size + this.loadNextPage() + } + + handlePageClicked(newPageNo: number): void { + this.navigateToPage(newPageNo) + } + + rowActions(): TableActionDefinition[] { + const tableActions = [] + if (this.canDeleteMetadata) { + tableActions.push(new TableActionDefinition({ + label: `Delete`, + callback: (action: TableActionDefinition, skill?: IJobCode|INamedReference) => this.handleClickDeleteItem(action, skill), + })) + } + return tableActions + } + + // tslint:disable-next-line:typedef + private handleClickDeleteItem(action: TableActionDefinition, skill: IJobCode|INamedReference | undefined) { + } +} diff --git a/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.html b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.html new file mode 100644 index 000000000..89bd0ea40 --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.html @@ -0,0 +1,10 @@ + diff --git a/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.scss b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.scss new file mode 100644 index 000000000..343dc9e7c --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.scss @@ -0,0 +1,4 @@ +.metadata-selector { + display: flex; + margin-bottom: 20px; +} diff --git a/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.spec.ts b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.spec.ts new file mode 100644 index 000000000..93b21eef9 --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" +import { MetadataSelectorComponent } from "./metadata-selector.component" + +describe("MetadataSelectorComponent", () => { + let component: MetadataSelectorComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MetadataSelectorComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataSelectorComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) + + it("Should call onValueChange with a param of type string", () => { + const param = "category" + const onValueChangeSpy = spyOn(component, "onValueChange").withArgs(param).and.callThrough() + + component.onValueChange("category") + + expect(onValueChangeSpy).toHaveBeenCalled() + expect(typeof param).toEqual("string") + }) +}) diff --git a/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.ts b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.ts new file mode 100644 index 000000000..d8c4a605e --- /dev/null +++ b/ui/src/app/metadata/detail/metadata-selector/metadata-selector.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from "@angular/core" +import { FormControl } from "@angular/forms" +import { MetadataType } from "../../rsd-metadata.enum" + +@Component({ + selector: "app-metadata-selector", + templateUrl: "./metadata-selector.component.html", + styleUrls: ["./metadata-selector.component.scss"] +}) +export class MetadataSelectorComponent { + + @Input() + control?: FormControl + @Input() + currentSelection?: string + + protected readonly MetadataType = MetadataType + @Input() + isVisible: () => boolean = () => false + + onValueChange(value: string): void { + this.control?.patchValue(value) + this.currentSelection = value + } +} diff --git a/ui/src/app/job-codes/Jobcode.spec.ts b/ui/src/app/metadata/job-codes/Jobcode.spec.ts similarity index 74% rename from ui/src/app/job-codes/Jobcode.spec.ts rename to ui/src/app/metadata/job-codes/Jobcode.spec.ts index eec42f096..01a20f806 100644 --- a/ui/src/app/job-codes/Jobcode.spec.ts +++ b/ui/src/app/metadata/job-codes/Jobcode.spec.ts @@ -1,5 +1,5 @@ -import { createMockJobcode } from "test/resource/mock-data" -import { deepEqualSkipOuterType, mismatched } from "../../../test/util/deep-equals" +import { createMockJobcode } from "@test/resource/mock-data" +import { deepEqualSkipOuterType, mismatched } from "@test/util/deep-equals" import { ApiJobCode, IJobCode } from "./Jobcode" diff --git a/ui/src/app/job-codes/Jobcode.ts b/ui/src/app/metadata/job-codes/Jobcode.ts similarity index 81% rename from ui/src/app/job-codes/Jobcode.ts rename to ui/src/app/metadata/job-codes/Jobcode.ts index f1169c754..b02e43f1c 100644 --- a/ui/src/app/job-codes/Jobcode.ts +++ b/ui/src/app/metadata/job-codes/Jobcode.ts @@ -24,6 +24,14 @@ export class ApiJobCode implements IJobCode { frameworkName?: string level?: JobCodeLevel parents?: IJobCode[] + major?: string + majorCode?: string + minor?: string + minorCode?: string + broad?: string + broadCode?: string + detailed?: string + url?: string constructor(o?: IJobCode) { if (o !== undefined) { diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html new file mode 100644 index 000000000..fa7651888 --- /dev/null +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.html @@ -0,0 +1,36 @@ + + + + {{jobCode.targetNodeName}} + + + {{jobCode.level}} + + + {{jobCode.parents}} + + + {{jobCode.frameworkName}} + + + +
+
+ + +
+ +
+
+ +
+ + + + + +
diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.spec.ts b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.spec.ts new file mode 100644 index 000000000..4b1accb59 --- /dev/null +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" +import { JobCodeListRowComponent } from "./job-code-list-row.component" + +describe("JobCodeListRowComponent", () => { + let component: JobCodeListRowComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ JobCodeListRowComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(JobCodeListRowComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts new file mode 100644 index 000000000..09f3b223a --- /dev/null +++ b/ui/src/app/metadata/job-codes/job-code-list-row/job-code-list-row.component.ts @@ -0,0 +1,41 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core" +import { TableActionDefinition } from "../../../table/skills-library-table/has-action-definitions" +import { SvgHelper, SvgIcon } from "../../../core/SvgHelper" +import { ApiJobCode, IJobCode } from "../Jobcode" + +@Component({ + // tslint:disable-next-line:component-selector + selector: "[app-job-code-list-row]", + templateUrl: "./job-code-list-row.component.html" +}) +export class JobCodeListRowComponent { + @Input() jobCode?: ApiJobCode + @Input() id = "job-code-list-row" + @Input() isSelected = false + @Input() rowActions: TableActionDefinition[] = [] + + @Output() rowSelected = new EventEmitter() + @Output() focusActionBar = new EventEmitter() + checkIcon = SvgHelper.path(SvgIcon.CHECK) + + constructor() { } + + selected(): void { + this.rowSelected.emit(this.jobCode) + } + + handleClick(action: TableActionDefinition): boolean { + action.fire(this.jobCode) + return false + } + + focusFirstColumnInRow(): boolean { + const ref = document.getElementById(`${this.id}-header-name`) + if (ref) { + ref.focus() + return true + } else { + return false + } + } +} diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html new file mode 100644 index 000000000..e7e622ca2 --- /dev/null +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.spec.ts b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.spec.ts new file mode 100644 index 000000000..34370ccda --- /dev/null +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" +import { JobCodeTableComponent } from "./job-code-table.component" + +describe("JobCodeTableComponent", () => { + let component: JobCodeTableComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ JobCodeTableComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(JobCodeTableComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts new file mode 100644 index 000000000..7fdece162 --- /dev/null +++ b/ui/src/app/metadata/job-codes/job-code-table/job-code-table.component.ts @@ -0,0 +1,22 @@ +import { AfterViewInit, Component, Input, QueryList, ViewChildren } from "@angular/core" +import { AbstractTableComponent } from "../../../table/abstract-table.component" +import { JobCodeListRowComponent } from "../job-code-list-row/job-code-list-row.component" +import { IJobCode } from "../Jobcode" + +@Component({ + selector: "app-job-code-table", + templateUrl: "./job-code-table.component.html" +}) +export class JobCodeTableComponent extends AbstractTableComponent implements AfterViewInit { + + @ViewChildren(JobCodeListRowComponent) rowReferences: QueryList | undefined = undefined + + @Input() allowSorting = false + + ngAfterViewInit(): void { + if (this.rowReferences && this.rowReferences.length > 0) { + this.rowReferences.first.focusFirstColumnInRow() + } + } +} + diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts b/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts new file mode 100644 index 000000000..3d9122dd7 --- /dev/null +++ b/ui/src/app/metadata/job-codes/service/job-code.service.spec.ts @@ -0,0 +1,176 @@ +import { fakeAsync, TestBed, tick } from "@angular/core/testing" +import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing" +import { Location } from "@angular/common" +import { Router } from "@angular/router" +import { JobCodeService, PaginatedJobCodes } from "./job-code.service" +import { AuthServiceData, AuthServiceStub, RouterData, RouterStub } from "@test/resource/mock-stubs" +import { AppConfig } from "../../../app.config" +import { EnvironmentService } from "../../../core/environment.service" +import { AuthService } from "../../../auth/auth-service" +import { + apiTaskResultForDeleteJobCode, + createMockJobcode, + createMockPaginatedJobCodes +} from "@test/resource/mock-data" +import { ApiSortOrder } from "../../../richskill/ApiSkill" +import { ApiJobCode, ApiJobCodeUpdate } from "../Jobcode" +import { ApiBatchResult } from "../../../richskill/ApiBatchResult" + +const ASYNC_WAIT_PERIOD = 3000 + +describe("JobCodeService", () => { + let testService: JobCodeService + let httpTestingController: HttpTestingController + + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + EnvironmentService, + AppConfig, + JobCodeService, + Location, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: Router, useClass: RouterStub } + ]}).compileComponents() + testService = TestBed.inject(JobCodeService) + httpTestingController = TestBed.inject(HttpTestingController) + const appConfig = TestBed.inject(AppConfig) + AppConfig.settings = appConfig.defaultConfig() + }) + + it("JobCode should be created", () => { + expect(testService).toBeTruthy() + }) + + it("getJobCodes should return Array of JobCodes", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const path = "api/metadata/jobcodes?sort=name.asc&size=3&from=0" + const testData: PaginatedJobCodes = createMockPaginatedJobCodes(3, 10) + + // Act + // noinspection LocalVariableNamingConventionJS + const result$ = testService.getJobCodes(testData.jobCodes.length, 0, ApiSortOrder.NameAsc) + + // Assert + result$ + .subscribe((data: PaginatedJobCodes) => { + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("GET") + req.flush(testData.jobCodes, { + headers: { "x-total-count": "" + testData.totalCount} + }) + }) + + it("getJobCodeById should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const id = "12345" + const path = "api/metadata/jobcodes/" + id + const testData: ApiJobCode = new ApiJobCode(createMockJobcode(42, "my jobcode name", id)) + + // Act + // noinspection LocalVariableNamingConventionJS + const result$ = testService.getJobCodeById(id) + + // Assert + result$ + .subscribe((data: ApiJobCode) => { + expect(data).toEqual(testData) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("GET") + req.flush(testData) + }) + + it("createJobCode should return a JobCode", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const path = "api/metadata/jobcodes" + const testData = [ + new ApiJobCode(createMockJobcode()) + ] + const expected = testData[0] + const input = new ApiJobCodeUpdate({ + code : expected.code, + targetNodeName : expected.targetNodeName, + targetNode : expected.targetNode, + frameworkName : expected.frameworkName, + level : expected.level, + parents : [] + }) + + // Act + const result$ = testService.createJobCode(input) + + // Assert + result$ + .subscribe((data: ApiJobCode) => { + expect(data).toEqual(expected) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path) + expect(req.request.method).toEqual("POST") + req.flush(testData) + }) + + it("updateJobCode should return", () => { + // Arrange + RouterData.commands = [] + AuthServiceData.isDown = false + const testData = new ApiJobCode(createMockJobcode()) + const expected = testData + const id = expected.code + const path = "api/metadata/jobcodes/" + id + const input = new ApiJobCodeUpdate({ + code: expected.code, + targetNodeName: expected.targetNodeName, + targetNode: expected.targetNode, + frameworkName: expected.frameworkName, + level: expected.level, + parents: expected.parents + }) + + // Act + const result$ = testService.updateJobCode(id, input) + + // Assert + result$ + .subscribe((data: ApiJobCode) => { + expect(data).toEqual(expected) + expect(RouterData.commands).toEqual([ ]) // No errors + expect(AuthServiceData.isDown).toEqual(false) + }) + + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + "/" + path + "/update") + expect(req.request.method).toEqual("POST") + req.flush(testData) + }) + + it("deleteJobCodeWithResult() should works", fakeAsync(() => { + const result$ = testService.deleteJobCodeWithResult(apiTaskResultForDeleteJobCode.uuid) + tick(ASYNC_WAIT_PERIOD) + // Assert + result$.subscribe((data: ApiBatchResult) => { + expect(RouterData.commands).toEqual([]) // No Errors + }) + const req = httpTestingController.expectOne(AppConfig.settings.baseApiUrl + `/api/metadata/jobcodes/${apiTaskResultForDeleteJobCode.uuid}/remove`) + expect(req.request.method).toEqual("DELETE") + expect(req.request.headers.get("Accept")).toEqual("application/json") + })) +}) diff --git a/ui/src/app/metadata/job-codes/service/job-code.service.ts b/ui/src/app/metadata/job-codes/service/job-code.service.ts new file mode 100644 index 000000000..67698b34c --- /dev/null +++ b/ui/src/app/metadata/job-codes/service/job-code.service.ts @@ -0,0 +1,97 @@ +import { Location } from "@angular/common" +import { HttpClient, HttpHeaders } from "@angular/common/http" +import { Injectable } from "@angular/core" +import { Router } from "@angular/router" +import { Observable } from "rxjs" +import { map, share } from "rxjs/operators" +import { ApiJobCode, IJobCode, IJobCodeUpdate } from "../Jobcode" +import { AuthService } from "../../../auth/auth-service"; +import { AbstractDataService } from "../../abstract-data.service"; +import { ApiSortOrder } from "../../../richskill/ApiSkill"; +import { ApiBatchResult } from "../../../richskill/ApiBatchResult"; +import { ApiTaskResult, ITaskResult } from "../../../task/ApiTaskResult"; + +@Injectable({ + providedIn: "root" +}) +export class JobCodeService extends AbstractDataService{ + + private baseServiceUrl = "api/metadata/jobcodes" + + constructor(protected httpClient: HttpClient, protected authService: AuthService, + protected router: Router, protected location: Location) { + super(httpClient, authService, router, location) + } + + getJobCodes( + size: number = 50, + from: number = 0, + sort: ApiSortOrder | undefined + ): Observable { + const params = this.buildTableParams(size, from, undefined, sort) + return this.get({ + path: `${this.baseServiceUrl}`, + params, + }) + .pipe(share()) + .pipe(map(({body, headers}) => { + return new PaginatedJobCodes( + body || [], + Number(headers.get("X-Total-Count")) + ) + })) + } + + getJobCodeById(id: string): Observable { + const errorMsg = `Could not find JobCode with id [${id}]` + return this.get({ + path: `${this.baseServiceUrl}/${id}` + }) + .pipe(share()) + .pipe(map(({body}) => new ApiJobCode(this.safeUnwrapBody(body, errorMsg)))) + } + + createJobCode(newObject: IJobCode): Observable { + const errorMsg = `Error creating JobCode` + return this.post({ + path: this.baseServiceUrl, + body: [newObject] + }) + .pipe(share()) + .pipe(map(({body}) => this.safeUnwrapBody(body, errorMsg).map(s => new ApiJobCode(s))[0])) + } + + updateJobCode(id: string, updateObject: IJobCodeUpdate): Observable { + const errorMsg = `Could not find JobCode with id: [${id}]` + return this.post({ + path: `${this.baseServiceUrl}/${id}/update`, + body: updateObject + }) + .pipe(share()) + .pipe(map(({body}) => new ApiJobCode(this.safeUnwrapBody(body, errorMsg)))) + } + + deleteJobCodeWithResult(id: string): Observable { + return this.pollForTaskResult(this.deleteJobCode(id)) + } + + deleteJobCode(id: string): Observable { + return this.httpClient.delete(this.buildUrl("api/metadata/jobcodes/" + id + "/remove"), { + headers: this.wrapHeaders(new HttpHeaders({ + Accept: "application/json" + } + )) + }) + .pipe(share()) + .pipe(map((body) => new ApiTaskResult(this.safeUnwrapBody(body, "unwrap failure")))) + } +} + +export class PaginatedJobCodes { + totalCount = 0 + jobCodes: ApiJobCode[] = [] + constructor(jobCodes: ApiJobCode[], totalCount: number) { + this.jobCodes = jobCodes + this.totalCount = totalCount + } +} diff --git a/ui/src/app/metadata/named-references/NamedReference.ts b/ui/src/app/metadata/named-references/NamedReference.ts new file mode 100644 index 000000000..0f00ead0b --- /dev/null +++ b/ui/src/app/metadata/named-references/NamedReference.ts @@ -0,0 +1,25 @@ +import {MetadataType} from "../rsd-metadata.enum" +import {Meta} from "@angular/platform-browser" + +export interface INamedReference { + id: string + name?: string + value: string + type?: MetadataType + framework: string +} + +export class ApiNamedReference implements INamedReference { + id = "" + framework = "" + name?: string = "" + type?: MetadataType = MetadataType.Category + value = "" + + constructor(o?: INamedReference) { + if (o !== undefined) { + Object.assign(this, o) + } + } + +} diff --git a/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.html b/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.html new file mode 100644 index 000000000..e423fc6ac --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.html @@ -0,0 +1,35 @@ + + + + {{namedReference.name}} + + + {{namedReference.framework}} + + + {{namedReference.value}} + + + + +
+
+ + +
+ +
+
+ +
+ + + + + + +
diff --git a/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.spec.ts b/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.spec.ts new file mode 100644 index 000000000..907310f3f --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" + +import { NamedReferenceListRowComponent } from "./named-reference-list-row.component" + +describe("NamedReferenceListRowComponent", () => { + let component: NamedReferenceListRowComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NamedReferenceListRowComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(NamedReferenceListRowComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.ts b/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.ts new file mode 100644 index 000000000..1b56caed7 --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference-list-row/named-reference-list-row.component.ts @@ -0,0 +1,41 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core" +import { TableActionDefinition } from "../../../table/skills-library-table/has-action-definitions" +import { SvgHelper, SvgIcon } from "../../../core/SvgHelper" +import { ApiNamedReference, INamedReference } from "../NamedReference" + +@Component({ + // tslint:disable-next-line:component-selector + selector: "[app-named-reference-list-row]", + templateUrl: "./named-reference-list-row.component.html" +}) +export class NamedReferenceListRowComponent { + @Input() namedReference?: ApiNamedReference + @Input() id = "named-reference-list-row" + @Input() isSelected = false + @Input() rowActions: TableActionDefinition[] = [] + + @Output() rowSelected = new EventEmitter() + @Output() focusActionBar = new EventEmitter() + checkIcon = SvgHelper.path(SvgIcon.CHECK) + + constructor() { } + + selected(): void { + this.rowSelected.emit(this.namedReference) + } + + handleClick(action: TableActionDefinition): boolean { + action.fire(this.namedReference) + return false + } + + focusFirstColumnInRow(): boolean { + const ref = document.getElementById(`${this.id}-header-name`) + if (ref) { + ref.focus() + return true + } else { + return false + } + } +} diff --git a/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.html b/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.html new file mode 100644 index 000000000..b22a3c3d0 --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.html @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Named ReferencesNameFrameworkValue
diff --git a/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.spec.ts b/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.spec.ts new file mode 100644 index 000000000..e88afdcfe --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing" + +import { NamedReferenceTableComponent } from "./named-reference-table.component" + +describe("NamedReferenceTableComponent", () => { + let component: NamedReferenceTableComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NamedReferenceTableComponent ] + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(NamedReferenceTableComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it("should create", () => { + expect(component).toBeTruthy() + }) +}) diff --git a/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.ts b/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.ts new file mode 100644 index 000000000..f9af27b8a --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference-table/named-reference-table.component.ts @@ -0,0 +1,21 @@ +import { AfterViewInit, Component, Input, QueryList, ViewChildren } from "@angular/core" +import { AbstractTableComponent } from "../../../table/abstract-table.component" +import { NamedReferenceListRowComponent } from "../named-reference-list-row/named-reference-list-row.component" +import { INamedReference } from "../NamedReference" + +@Component({ + selector: "app-named-reference-table", + templateUrl: "./named-reference-table.component.html" +}) +export class NamedReferenceTableComponent extends AbstractTableComponent implements AfterViewInit { + + @ViewChildren(NamedReferenceListRowComponent) rowReferences: QueryList | undefined = undefined + + @Input() allowSorting = false + + ngAfterViewInit(): void { + if (this.rowReferences && this.rowReferences.length > 0) { + this.rowReferences.first.focusFirstColumnInRow() + } + } +} diff --git a/ui/src/app/metadata/named-references/named-reference.service.spec.ts b/ui/src/app/metadata/named-references/named-reference.service.spec.ts new file mode 100644 index 000000000..4ed0c1c9f --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference.service.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from "@angular/core/testing" +import { HttpClientTestingModule } from "@angular/common/http/testing" +import { NamedReferenceService } from "./named-reference.service" +import { Location } from "@angular/common" +import { Router } from "@angular/router" +import { EnvironmentService } from "../../core/environment.service" +import { AppConfig } from "../../app.config" +import { AuthService } from "../../auth/auth-service" +import { AuthServiceStub, RouterStub } from "@test/resource/mock-stubs" + +describe("NamedReferenceService", () => { + let testService: NamedReferenceService + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + EnvironmentService, + AppConfig, + NamedReferenceService, + Location, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: Router, useClass: RouterStub } + ]}) + testService = TestBed.inject(NamedReferenceService) + }) + + it("should be created", () => { + expect(testService).toBeTruthy() + }) +}) diff --git a/ui/src/app/metadata/named-references/named-reference.service.ts b/ui/src/app/metadata/named-references/named-reference.service.ts new file mode 100644 index 000000000..fa551ec9d --- /dev/null +++ b/ui/src/app/metadata/named-references/named-reference.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from "@angular/core" +import { HttpClient } from "@angular/common/http" +import { Router } from "@angular/router" +import { Location } from "@angular/common" +import { AuthService } from "../../auth/auth-service" +import { AbstractDataService } from "../abstract-data.service" +import { ApiNamedReference } from "./NamedReference" + +@Injectable({ + providedIn: "root" +}) +export class NamedReferenceService extends AbstractDataService{ + + private baseServiceUrl = "api/named-references" + + constructor(protected httpClient: HttpClient, protected authService: AuthService, + protected router: Router, protected location: Location) { + super(httpClient, authService, router, location) + } +} + +export class PaginatedNamedReferences { + totalCount = 0 + namedReferences: ApiNamedReference[] = [] + constructor(namedReferences: ApiNamedReference[], totalCount: number) { + this.namedReferences = namedReferences + this.totalCount = totalCount + } +} diff --git a/ui/src/app/metadata/rsd-metadata.enum.ts b/ui/src/app/metadata/rsd-metadata.enum.ts new file mode 100644 index 000000000..bad69fee3 --- /dev/null +++ b/ui/src/app/metadata/rsd-metadata.enum.ts @@ -0,0 +1,10 @@ +export enum MetadataType { + Category = "category", + Keyword = "keyword", + Standard = "standard", + Certification = "certification", + Alignment = "alignment", + Employer = "employer", + Author = "author", + JobCode = "jobcode" +} diff --git a/ui/src/app/models/filter-dropdown.model.ts b/ui/src/app/models/filter-dropdown.model.ts index 57b1f6353..4c6ee1385 100644 --- a/ui/src/app/models/filter-dropdown.model.ts +++ b/ui/src/app/models/filter-dropdown.model.ts @@ -1,5 +1,5 @@ import {ApiNamedReference} from "../richskill/ApiSkill" -import {ApiJobCode} from "../job-codes/Jobcode" +import { ApiJobCode } from "../metadata/job-codes/Jobcode" export interface FilterDropdown { categories: ApiNamedReference[] diff --git a/ui/src/app/navigation/commoncontrols-mobile.component.html b/ui/src/app/navigation/commoncontrols-mobile.component.html index 34fee092b..f4c6b0465 100644 --- a/ui/src/app/navigation/commoncontrols-mobile.component.html +++ b/ui/src/app/navigation/commoncontrols-mobile.component.html @@ -16,6 +16,12 @@ Categories + +
  • + +
  • diff --git a/ui/src/app/navigation/header.component.html b/ui/src/app/navigation/header.component.html index b16dcf208..2f29054ba 100644 --- a/ui/src/app/navigation/header.component.html +++ b/ui/src/app/navigation/header.component.html @@ -21,18 +21,13 @@

    Site Navigation