diff --git a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt index 6ad9e2c57..577fe97b1 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/RoutePaths.kt @@ -58,6 +58,21 @@ object RoutePaths { const val ES_ADMIN_DELETE_INDICES = "$ES_ADMIN/delete-indices" const val ES_ADMIN_REINDEX = "$ES_ADMIN/reindex" + const val METADATA_PATH = "$API/metadata" + const val JOB_CODE_PATH = "$METADATA_PATH/jobcodes" + const val JOB_CODE_CREATE = JOB_CODE_PATH + const val JOB_CODE_LIST = JOB_CODE_PATH + const val JOB_CODE_DETAIL = "$JOB_CODE_PATH/{id}" + const val JOB_CODE_UPDATE = "$JOB_CODE_DETAIL/update" + const val JOB_CODE_REMOVE = "$JOB_CODE_DETAIL/remove" + + const val NAMED_REFERENCES_PATH = "$METADATA_PATH/named-references" + const val NAMED_REFERENCES_CREATE = NAMED_REFERENCES_PATH + const val NAMED_REFERENCES_LIST = NAMED_REFERENCES_PATH + const val NAMED_REFERENCES_DETAIL = "$NAMED_REFERENCES_PATH/{id}" + const val NAMED_REFERENCES_UPDATE = "$NAMED_REFERENCES_DETAIL/update" + const val NAMED_REFERENCES_REMOVE = "$NAMED_REFERENCES_DETAIL/remove" + object QueryParams { const val FROM = "from" const val SIZE = "size" diff --git a/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt new file mode 100644 index 000000000..509448550 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/api/model/JobCodeUpdate.kt @@ -0,0 +1,20 @@ +package edu.wgu.osmt.api.model + +import com.fasterxml.jackson.annotation.JsonProperty +import edu.wgu.osmt.db.JobCodeLevel + +data class JobCodeUpdate( + @JsonProperty("code") + val code: String, + @JsonProperty("targetNode") + val targetNode: String? = null, + @JsonProperty("targetNodeName") + val targetNodeName: String? = null, + @JsonProperty("frameworkName") + val framework: String? = null, + @JsonProperty("level") + val level: JobCodeLevel? = null, + @JsonProperty("parents") + val parents: List? = null +) { +} \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt new file mode 100644 index 000000000..99fb58d6c --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeController.kt @@ -0,0 +1,82 @@ +package edu.wgu.osmt.jobcode; + +import edu.wgu.osmt.RoutePaths +import edu.wgu.osmt.api.model.ApiJobCode +import edu.wgu.osmt.api.model.JobCodeUpdate +import edu.wgu.osmt.elasticsearch.OffsetPageable +import edu.wgu.osmt.task.TaskResult +import edu.wgu.osmt.task.TaskStatus +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpEntity +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.stereotype.Controller +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.server.ResponseStatusException + +@Controller +@Transactional +class JobCodeController @Autowired constructor( + val jobCodeEsRepo: JobCodeEsRepo, + val jobCodeRepository: JobCodeRepository +) { + + @GetMapping(RoutePaths.JOB_CODE_LIST, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("isAuthenticated()") + fun allPaginated( + @RequestParam(required = true) size: Int, + @RequestParam(required = true) from: Int, + @RequestParam(required = false) sort: String? + ): HttpEntity> { + val searchResults = jobCodeEsRepo.typeAheadSearch("", OffsetPageable(from, size, null)) + return ResponseEntity.status(200).body(searchResults.map { ApiJobCode.fromJobCode(it.content) }.toList()) + } + + @GetMapping(RoutePaths.JOB_CODE_DETAIL, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("isAuthenticated()") + fun byId( + @PathVariable id: Long, + ): HttpEntity { + val jobCode = jobCodeRepository.findById(id) + if (jobCode != null) { + return ResponseEntity.status(200).body(ApiJobCode.fromJobCode(jobCode.toModel())) + } else { + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + } + + @PostMapping(RoutePaths.JOB_CODE_CREATE, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("hasAuthority(@appConfig.roleAdmin)") + fun createJobCode( + @RequestBody jobCodes: List + ): HttpEntity> { + val newJobCodes = jobCodeRepository.createFromApi(jobCodes) + return ResponseEntity.status(200).body(newJobCodes.map { ApiJobCode.fromJobCode(it.toModel()) }.toList()) + } + + @PostMapping(RoutePaths.JOB_CODE_UPDATE, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("hasAuthority(@appConfig.roleAdmin)") + fun updateJobCode( + @PathVariable id: Int, + @RequestBody jobCodeUpdate: JobCodeUpdate + ): HttpEntity { + return ResponseEntity.status(200).body(ApiJobCode(code = "1", targetNode = "target", targetNodeName = "targetNodeName", frameworkName = "frameworkName", parents = listOf())) + } + + @DeleteMapping(RoutePaths.JOB_CODE_REMOVE) + @PreAuthorize("hasAuthority(@appConfig.roleAdmin)") + fun deleteJobCode( + @PathVariable id: Int, + ): HttpEntity { + return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path")) + } + +} \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt index 6bc1208bc..f2e1b21a7 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeEsRepo.kt @@ -20,6 +20,8 @@ interface CustomJobCodeRepository { val elasticSearchTemplate: ElasticsearchRestTemplate fun typeAheadSearch(query: String): SearchHits + fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits + fun deleteIndex() { elasticSearchTemplate.indexOps(IndexCoordinates.of(INDEX_JOBCODE_DOC)).delete() } @@ -28,6 +30,15 @@ interface CustomJobCodeRepository { class CustomJobCodeRepositoryImpl @Autowired constructor(override val elasticSearchTemplate: ElasticsearchRestTemplate) : CustomJobCodeRepository { + override fun typeAheadSearch(query: String, pageable: OffsetPageable): SearchHits { + val nsq: NativeSearchQueryBuilder + val disjunctionQuery = JobCodeQueries.multiPropertySearch(query) + nsq = + NativeSearchQueryBuilder().withPageable(pageable).withQuery(disjunctionQuery) + .withSort(SortBuilders.fieldSort("${JobCode::code.name}.keyword").order(SortOrder.ASC)) + return elasticSearchTemplate.search(nsq.build(), JobCode::class.java) + } + override fun typeAheadSearch(query: String): SearchHits { val nsq: NativeSearchQueryBuilder diff --git a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt index e7d2186cd..32138c1de 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/jobcode/JobCodeRepository.kt @@ -1,5 +1,6 @@ package edu.wgu.osmt.jobcode +import edu.wgu.osmt.api.model.JobCodeUpdate import org.jetbrains.exposed.sql.SizedIterable import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.and @@ -19,6 +20,7 @@ interface JobCodeRepository { fun findByCodeOrCreate(code: String, framework: String? = null): JobCodeDao fun findBlsCode(code: String): JobCodeDao? fun create(code: String, framework: String? = null): JobCodeDao + fun createFromApi(jobCodes: List): List fun onetsByDetailCode(detailedCode: String): SizedIterable companion object { @@ -52,6 +54,19 @@ class JobCodeRepositoryImpl: JobCodeRepository { .firstOrNull()?.let { dao.wrapRow(it) } } + override fun createFromApi(jobCodes: List): List { + return jobCodes.map { jobCodeUpdate -> + dao.new { + this.code = jobCodeUpdate.code + this.framework = jobCodeUpdate.framework + this.name = jobCodeUpdate.targetNodeName + this.creationDate = LocalDateTime.now(ZoneOffset.UTC) + this.name = "my name" + this.major = "my major" + }.also { jobCodeEsRepo.save(it.toModel()) } + } + } + override fun findByCodeOrCreate(code: String, framework: String?): JobCodeDao { val existing = findByCode(code) return existing ?: create(code, framework) diff --git a/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt b/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt new file mode 100644 index 000000000..c530c8884 --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/keyword/ApiKeyword.kt @@ -0,0 +1,31 @@ +package edu.wgu.osmt.keyword + +import com.fasterxml.jackson.annotation.JsonProperty + +data class ApiKeywordUpdate( + @JsonProperty("name") + val name: String?, + @JsonProperty("value") + val value: String?, + @JsonProperty("type") + val type: KeywordTypeEnum, + @JsonProperty("framework") + val framework: String? +) { +} + +data class NamedReference( + val id: Long?, + val name: String?, + val value: String?, + val type: KeywordTypeEnum, + val framework: String? +) { + + companion object factory { + fun fromKeyword(keyword: Keyword): NamedReference { + return NamedReference(keyword.id, keyword.value, keyword.value, keyword.type, keyword.framework) + } + } + +} \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt b/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt new file mode 100644 index 000000000..a2dc13b2b --- /dev/null +++ b/api/src/main/kotlin/edu/wgu/osmt/keyword/NamedReferencesController.kt @@ -0,0 +1,85 @@ +package edu.wgu.osmt.keyword + +import edu.wgu.osmt.PaginationDefaults +import edu.wgu.osmt.RoutePaths +import edu.wgu.osmt.task.TaskResult +import edu.wgu.osmt.task.TaskStatus +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpEntity +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.stereotype.Controller +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.server.ResponseStatusException + +@Controller +@Transactional +class NamedReferencesController @Autowired constructor( + val keywordEsRepo: KeywordEsRepo, + val keywordRepository: KeywordRepository +) { + + @GetMapping(RoutePaths.NAMED_REFERENCES_LIST, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("isAuthenticated()") + fun allPaginated( + @RequestParam(required = true) type: String, + @RequestParam(required = false, defaultValue = PaginationDefaults.size.toString()) size: Int, + @RequestParam(required = false, defaultValue = "0") from: Int, + @RequestParam(required = false) sort: String? + ): HttpEntity> { + val keywordType = KeywordTypeEnum.forApiValue(type) ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST) + val searchResults = keywordEsRepo.typeAheadSearch("", keywordType) + return ResponseEntity.status(200).body(searchResults.map { NamedReference.fromKeyword(it.content) }.toList()) + } + + @GetMapping(RoutePaths.NAMED_REFERENCES_DETAIL, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("isAuthenticated()") + fun byId( + @PathVariable id: Long, + ): HttpEntity { + val keyword = keywordRepository.findById(id) + if (keyword != null) { + return ResponseEntity.status(200).body(NamedReference.fromKeyword(keyword.toModel())) + } else { + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + } + + @PostMapping(RoutePaths.NAMED_REFERENCES_CREATE, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("hasAuthority(@appConfig.roleAdmin)") + fun createNamedReference( + @RequestBody keywords: List + ): HttpEntity> { + return ResponseEntity.status(200).body( + keywords.map { + NamedReference(id = 1, name = it.name, value = it.value, type = it.type, framework = it.framework) + } + ) + } + + @PostMapping(RoutePaths.NAMED_REFERENCES_UPDATE, produces = [MediaType.APPLICATION_JSON_VALUE]) + @PreAuthorize("hasAuthority(@appConfig.roleAdmin)") + fun updateNamedReference( + @PathVariable id: Int, + @RequestBody apiKeyword: ApiKeywordUpdate + ): HttpEntity { + return ResponseEntity.status(200).body(NamedReference(134, "my name", "my value", KeywordTypeEnum.Keyword, "my framework")) + } + + @DeleteMapping(RoutePaths.NAMED_REFERENCES_REMOVE) + @PreAuthorize("hasAuthority(@appConfig.roleAdmin)") + fun deleteNamedReference( + @PathVariable id: Int, + ): HttpEntity { + return ResponseEntity.status(200).body(TaskResult(uuid = "uuid", contentType = "application/json", status = TaskStatus.Processing, apiResultPath = "path")) + } + +} \ No newline at end of file diff --git a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt index 95b106eaf..1e6b47949 100644 --- a/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt +++ b/api/src/main/kotlin/edu/wgu/osmt/security/SecurityConfig.kt @@ -34,6 +34,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile import org.springframework.http.HttpMethod.* +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter @@ -58,6 +59,7 @@ import javax.servlet.http.HttpServletResponse @Configuration @EnableWebSecurity @Profile("oauth2-okta | OTHER-OAUTH-PROFILE") +@EnableGlobalMethodSecurity(prePostEnabled = true) class SecurityConfig : WebSecurityConfigurerAdapter() { @Autowired diff --git a/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt new file mode 100644 index 000000000..d722194fc --- /dev/null +++ b/api/src/test/kotlin/edu/wgu/osmt/jobcode/JobCodeControllerTest.kt @@ -0,0 +1,86 @@ +package edu.wgu.osmt.jobcode + +import edu.wgu.osmt.BaseDockerizedTest +import edu.wgu.osmt.HasDatabaseReset +import edu.wgu.osmt.SpringTest +import edu.wgu.osmt.api.model.JobCodeUpdate +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.ZoneOffset + +@Transactional +internal class JobCodeControllerTest @Autowired constructor( + val jobCodeEsRepo: JobCodeEsRepo, + val jobCodeRepository: JobCodeRepository +) : SpringTest(), BaseDockerizedTest, HasDatabaseReset { + + @Autowired + lateinit var jobCodeController: JobCodeController + val dao = JobCodeDao.Companion + + @Test + fun `Index should return an array with almost one job code`() { + dao.new { + this.code = "code" + this.framework = "framework" + this.name = "targetNodeName" + this.creationDate = LocalDateTime.now(ZoneOffset.UTC) + this.name = "my name" + this.major = "my major" + }.also { jobCodeEsRepo.save(it.toModel()) } + val result = jobCodeController.allPaginated(50, 0, null) + Assertions.assertThat(result.body).hasSizeGreaterThan(0) + } + + @Test + fun `By id should find a job code`() { + val daoJobCode = dao.new { + this.code = "code" + this.framework = "framework" + this.name = "targetNodeName" + this.creationDate = LocalDateTime.now(ZoneOffset.UTC) + this.name = "my name" + this.major = "my major" + } + val esJobCode = jobCodeEsRepo.save(jobCodeEsRepo.save(daoJobCode.toModel())) + val result = esJobCode.id?.let { jobCodeController.byId(it) } + if (result != null) { + Assertions.assertThat(result.body).isNotNull + } + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + } + + @Test + fun `Create should return created job codes`() { + val result = jobCodeController.createJobCode( + listOf(JobCodeUpdate("my code", "my framework")) + ) + Assertions.assertThat(result.body).hasSizeGreaterThan(0) + } + + @Test + fun `Update should return job codes with updated properties`() { + val result = jobCodeController.updateJobCode( + 1, + JobCodeUpdate("my code", "my framework") + ) + Assertions.assertThat(result).isNotNull + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + } + + @Test + fun `Delete should return status 200`() { + val result = jobCodeController.deleteJobCode( + 1 + ) + Assertions.assertThat(result).isNotNull + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + } + + +} \ No newline at end of file diff --git a/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt b/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt new file mode 100644 index 000000000..29b7509f1 --- /dev/null +++ b/api/src/test/kotlin/edu/wgu/osmt/keyword/NamedReferencesControllerTest.kt @@ -0,0 +1,85 @@ +package edu.wgu.osmt.keyword + +import edu.wgu.osmt.BaseDockerizedTest +import edu.wgu.osmt.HasDatabaseReset +import edu.wgu.osmt.SpringTest +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.time.ZoneOffset + +@Transactional +internal class NamedReferencesControllerTest @Autowired constructor( + val keywordRepository: KeywordRepository, + val keywordEsRepo: KeywordEsRepo +) : SpringTest(), BaseDockerizedTest, HasDatabaseReset { + + @Autowired + lateinit var namedReferencesController: NamedReferencesController + + val dao = KeywordDao.Companion + + @Test + fun `Index should return an array with almost one named reference`() { + dao.new { + this.uri = "uri" + this.value = "value" + this.framework = "my framework" + this.type = KeywordTypeEnum.Keyword + this.creationDate = LocalDateTime.now(ZoneOffset.UTC) + this.updateDate = LocalDateTime.now(ZoneOffset.UTC) + }.also { keywordEsRepo.save(it.toModel()) } + val result = namedReferencesController.allPaginated(KeywordTypeEnum.Keyword.toString(), 50, 0, null) + Assertions.assertThat(result.body).hasSizeGreaterThan(0) + } + + @Test + fun `By id should find a named reference`() { + val daoKeyword = dao.new { + this.uri = "uri" + this.value = "value" + this.framework = "my framework" + this.type = KeywordTypeEnum.Keyword + this.creationDate = LocalDateTime.now(ZoneOffset.UTC) + this.updateDate = LocalDateTime.now(ZoneOffset.UTC) + } + val esKeyword = keywordEsRepo.save(daoKeyword.toModel()) + val result = esKeyword.id?.let { namedReferencesController.byId(it) } + if (result != null) { + Assertions.assertThat(result.body).isNotNull + } + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + } + + @Test + fun `Create should return created job codes`() { + val result = namedReferencesController.createNamedReference( + listOf(ApiKeywordUpdate("my keyword", "my value", KeywordTypeEnum.Keyword, "my framework")) + ) + Assertions.assertThat(result.body).hasSizeGreaterThan(0) + } + + @Test + fun `Update should return job codes with updated properties`() { + val result = namedReferencesController.updateNamedReference( + 1, + ApiKeywordUpdate("my keyword", "my value", KeywordTypeEnum.Keyword, "my framework") + ) + Assertions.assertThat(result).isNotNull + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + } + + @Test + fun `Delete should return status 200`() { + val result = namedReferencesController.deleteNamedReference( + 1 + ) + Assertions.assertThat(result).isNotNull + Assertions.assertThat((result as ResponseEntity).statusCode).isEqualTo(HttpStatus.OK) + } + +} \ No newline at end of file diff --git a/ui/src/app/my-workspace/my-workspace.component.spec.ts b/ui/src/app/my-workspace/my-workspace.component.spec.ts index 1951e9fed..8ea9f2d45 100644 --- a/ui/src/app/my-workspace/my-workspace.component.spec.ts +++ b/ui/src/app/my-workspace/my-workspace.component.spec.ts @@ -32,6 +32,7 @@ import {LabelWithFilterComponent} from "../table/skills-library-table/label-with import {SkillListRowComponent} from "../richskill/list/skill-list-row.component" import {StatusBarComponent} from "../core/status-bar.component" import {DotsMenuComponent} from "../table/skills-library-table/dots-menu.component" +import { ConvertToCollectionComponent } from "./convert-to-collection/convert-to-collection.component" describe("MyWorkspaceComponent", () => { let component: MyWorkspaceComponent @@ -45,6 +46,10 @@ describe("MyWorkspaceComponent", () => { { path: "my-workspace/uuid1/add-skills", component: ManageCollectionComponent + }, + { + path: "my-workspace/convert-to-collection", + component: ConvertToCollectionComponent } ]), HttpClientTestingModule,