From a73e33cefbd437e7ee8498d209a3b16a12d0cc73 Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Fri, 31 Jul 2020 15:15:32 +0000 Subject: [PATCH 01/15] [ci-skip]prepare release theGardener-1.3.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 00973142b..f1b48e220 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.3.2-SNAPSHOT + 1.3.2 kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - HEAD + theGardener-1.3.2 From 18c166bfb55236fb2cce6a7484077c0a24413864 Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Fri, 31 Jul 2020 15:15:35 +0000 Subject: [PATCH 02/15] [ci-skip]prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f1b48e220..af9483bd0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.3.2 + 1.3.3-SNAPSHOT kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - theGardener-1.3.2 + HEAD From 6d88925a1b2dc698c31c95782738ec061c3e83fb Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Fri, 28 Aug 2020 17:16:42 +0200 Subject: [PATCH 03/15] search page wip search page auto commit auto commit handle pages attached by the node.directoryPath no results search input breadcrumb breadcrumb breadcrumb breadcrumb remove code linked to kelkoo --- app/controllers/PageController.scala | 7 +- app/repositories/HierarchyRepository.scala | 6 + app/services/HierarchyService.scala | 55 +- app/services/PageService.scala | 21 +- app/services/SearchService.scala | 82 +- conf/routes | 2 +- documentation/Changelog.md | 5 + frontend/package-lock.json | 46 +- .../page/header/header.component.html | 37 +- .../page/header/header.component.scss | 10 +- .../page/header/header.component.spec.ts | 2 + .../page/header/header.component.ts | 16 +- frontend/src/app/_models/page.ts | 1 + frontend/src/app/_models/search.ts | 27 + .../src/app/_services/page.service.spec.ts | 16 + frontend/src/app/_services/page.service.ts | 83 +- .../src/app/_services/route.service.spec.ts | 17 +- frontend/src/app/_services/route.service.ts | 36 +- frontend/src/app/_testUtils/test-data.spec.ts | 1096 +++++++++-------- frontend/src/app/app-routing.module.ts | 8 +- frontend/src/app/app.module.ts | 8 + .../search-page/search-page.component.html | 9 + .../search-page/search-page.component.scss | 31 + .../search-page/search-page.component.ts | 34 + .../search-query/search-query.component.html | 4 + .../search-query/search-query.component.scss | 16 + .../search-query/search-query.component.ts | 30 + .../search-result-item.component.html | 4 + .../search-result-item.component.scss | 6 + .../search-result-item.component.ts | 31 + .../search-results.component.html | 10 + .../search-results.component.scss | 0 .../search-results.component.ts | 14 + local-conf/application.dev.conf | 2 - test/OnGoingBDDTest.scala | 2 - test/features/navigation/provide_menu.feature | 2 +- test/features/search/search_pages.feature | 297 +++++ .../search/search_through_name_pages.feature | 142 --- test/services/SearchServiceTest.scala | 68 +- test/steps/CommonSteps.scala | 4 +- 40 files changed, 1456 insertions(+), 831 deletions(-) create mode 100644 frontend/src/app/_models/search.ts create mode 100644 frontend/src/app/output/search/search-page/search-page.component.html create mode 100644 frontend/src/app/output/search/search-page/search-page.component.scss create mode 100644 frontend/src/app/output/search/search-page/search-page.component.ts create mode 100644 frontend/src/app/output/search/search-query/search-query.component.html create mode 100644 frontend/src/app/output/search/search-query/search-query.component.scss create mode 100644 frontend/src/app/output/search/search-query/search-query.component.ts create mode 100644 frontend/src/app/output/search/search-result-item/search-result-item.component.html create mode 100644 frontend/src/app/output/search/search-result-item/search-result-item.component.scss create mode 100644 frontend/src/app/output/search/search-result-item/search-result-item.component.ts create mode 100644 frontend/src/app/output/search/search-results/search-results.component.html create mode 100644 frontend/src/app/output/search/search-results/search-results.component.scss create mode 100644 frontend/src/app/output/search/search-results/search-results.component.ts create mode 100644 test/features/search/search_pages.feature delete mode 100644 test/features/search/search_through_name_pages.feature diff --git a/app/controllers/PageController.scala b/app/controllers/PageController.scala index 07829e39a..67e51bbf9 100644 --- a/app/controllers/PageController.scala +++ b/app/controllers/PageController.scala @@ -30,11 +30,10 @@ class PageController @Inject()(pageService: PageService, searchService: SearchSe } } - @ApiOperation(value = "Search pages through keywords", response = classOf[PageIndexDocument], responseContainer = "list") + @ApiOperation(value = "Search pages through a keyword", response = classOf[SearchResult]) @ApiResponses(Array(new ApiResponse(code = 404, message = "Page not found"))) - def searchPage(keywords: String): Action[AnyContent] = Action { - implicit val itemsFormat = Json.format[PageIndexDocument] - Ok(Json.toJson(searchService.searchForPage(keywords))) + def searchPage(keyword: String): Action[AnyContent] = Action { + Ok(Json.toJson(searchService.searchForPage(keyword))) } } diff --git a/app/repositories/HierarchyRepository.scala b/app/repositories/HierarchyRepository.scala index 657bd66cb..3bfd477f2 100644 --- a/app/repositories/HierarchyRepository.scala +++ b/app/repositories/HierarchyRepository.scala @@ -67,6 +67,12 @@ class HierarchyRepository @Inject()(db: Database) { } } + def findByDirectoryPath(directoryPath: String): Option[HierarchyNode] = { + db.withConnection { implicit connection => + SQL"SELECT * FROM hierarchyNode WHERE directoryPath = $directoryPath".as(parser.*).headOption + } + } + def findAllByProjectId(projectId: String): Seq[HierarchyNode] = { db.withConnection { implicit connection => SQL"SELECT * FROM project_hierarchyNode INNER JOIN hierarchyNode on (id = hierarchyId) WHERE projectId = $projectId".as(parser.*) diff --git a/app/services/HierarchyService.scala b/app/services/HierarchyService.scala index 2ecb53f45..37fdc956b 100644 --- a/app/services/HierarchyService.scala +++ b/app/services/HierarchyService.scala @@ -24,22 +24,47 @@ class HierarchyService @Inject()(hierarchyRepository: HierarchyRepository) { case Some(shortcut) => !hierarchy.id.startsWith(shortcut) } - def getHierarchyPath(pageJoinProject: PageJoinProject): String = { - val projectHierarchies = hierarchyRepository.findAllByProjectId(pageJoinProject.project.id) - if (projectHierarchies.nonEmpty) { - val hierarchyId = projectHierarchies.headOption.map(_.id).getOrElse("") - val splitedId = hierarchyId.split('.') - var hierarchyPath = "" - var currentHierarchyId = "." - for (id <- splitedId) { - if (id.nonEmpty) { - currentHierarchyId += id + "." - hierarchyPath += "/" + hierarchyRepository.findById(currentHierarchyId).map(_.name).getOrElse("Not Found") + def getHierarchyPath(pageJoinProject: PageJoinProject): Option[String] = { + val projectHierarchy = hierarchyRepository.findAllByProjectId(pageJoinProject.project.id).headOption match { + case Some(h) => Some(h) + case None => hierarchyRepository.findByDirectoryPath(pageJoinProject.directory.path) + } + projectHierarchy match { + case Some(h) => + val hierarchyId = h.id + val spitedId = hierarchyId.split('.') + var hierarchyPath = "" + var currentHierarchyId = "." + for (id <- spitedId) { + if (id.nonEmpty) { + currentHierarchyId += id + "." + hierarchyPath += "/" + hierarchyRepository.findById(currentHierarchyId).map(_.slugName).getOrElse("Not Found") + } + } + Some(hierarchyPath) + case None => None + } + } + + def getBreadcrumb(pageJoinProject: PageJoinProject): String = { + val projectHierarchy = hierarchyRepository.findAllByProjectId(pageJoinProject.project.id).headOption match { + case Some(h) => Some(h) + case None => hierarchyRepository.findByDirectoryPath(pageJoinProject.directory.path) + } + projectHierarchy match { + case Some(h) => + val hierarchyId = h.id + val spitedId = hierarchyId.split('.') + var hierarchyPath = "" + var currentHierarchyId = "." + for (id <- spitedId) { + if (id.nonEmpty) { + currentHierarchyId += id + "." + hierarchyPath += "/" + hierarchyRepository.findById(currentHierarchyId).map(_.name).getOrElse("Not Found") + } } - } - hierarchyPath + "/" + pageJoinProject.branch.name + pageJoinProject.page.relativePath - } else { - pageJoinProject.page.path + hierarchyPath.replaceFirst("/", "") + " / " + pageJoinProject.project.name + pageJoinProject.directory.relativePath.replaceAll("/", " / ") + pageJoinProject.page.label + case None => "" } } diff --git a/app/services/PageService.scala b/app/services/PageService.scala index 5a1dba665..3c5cfb036 100644 --- a/app/services/PageService.scala +++ b/app/services/PageService.scala @@ -245,7 +245,9 @@ class PageService @Inject()(config: Configuration, projectRepository: ProjectRep def computePageFromPathUsingDatabaseBis(pageJoinProjectOpt: Option[PageJoinProject], path: String, forceRefresh: Boolean = true): Future[Option[PageWithContent]] = { pageJoinProjectOpt match { case Some(pageJoinProject) => - indexService.addDocument(PageIndexDocument(hierarchyService.getHierarchyPath(pageJoinProject), pageJoinProject.page.path, pageJoinProject.branch.name, pageJoinProject.page.label, pageJoinProject.page.description, pageJoinProject.page.markdown.getOrElse(""))) + if (pageJoinProject.branch.isStable) { + insertOrUpdateIndex(pageJoinProject) + } val key = computePageCacheKey(path) if (cache.get(key).isEmpty || forceRefresh || pageJoinProject.page.dependOnOpenApi) { logger.debug(s"Page computed: $path") @@ -273,6 +275,23 @@ class PageService @Inject()(config: Configuration, projectRepository: ProjectRep } } + private def insertOrUpdateIndex(pageJoinProject: PageJoinProject) = { + + hierarchyService.getHierarchyPath(pageJoinProject).map{ hierarchy => + val document = PageIndexDocument(id = hierarchy + "/" + pageJoinProject.page.path, + hierarchy = hierarchy, + path = pageJoinProject.page.path, + breadcrumb = hierarchyService.getBreadcrumb(pageJoinProject), + project = pageJoinProject.project.name, + branch = pageJoinProject.branch.name, + label = pageJoinProject.page.label, + description = pageJoinProject.page.description, + pageContent = pageJoinProject.page.markdown.getOrElse("")) + indexService.insertOrUpdateDocument(document) + } + + } + def processPage(currentDirectory: Directory, localDirectoryPath: String, name: String, order: Int): Option[Page] = { val pageFile = new File(localDirectoryPath + "/" + name + ".md") if (pageFile.exists()) { diff --git a/app/services/SearchService.scala b/app/services/SearchService.scala index 3be24609f..d0fa535cb 100644 --- a/app/services/SearchService.scala +++ b/app/services/SearchService.scala @@ -1,33 +1,93 @@ package services +import com.outr.lucene4s.query.{Sort, TermSearchTerm} import com.outr.lucene4s.{DirectLucene, parse} -import com.outr.lucene4s.query.Sort import javax.inject.{Inject, Singleton} +import play.api.libs.json.Json +case class SearchResult(items: Seq[SearchResultItem]) + +case class SearchResultItem(page: PageIndexDocument, highlights: Seq[HighlightedFragment]) + +case class PageIndexDocument(id: String, hierarchy: String, path: String, breadcrumb: String, project: String, branch: String, label: String, description: String, pageContent: String) + +case class HighlightedFragment(fragment: String, word: String) + +object HighlightedFragment { + implicit val format = Json.format[HighlightedFragment] +} + +object PageIndexDocument { + implicit val format = Json.format[PageIndexDocument] +} + +object SearchResultItem { + implicit val format = Json.format[SearchResultItem] +} + +object SearchResult { + implicit val format = Json.format[SearchResult] +} -case class PageIndexDocument(hierarchy: String, path: String, branch: String, label: String, description: String, pageContent: String) @Singleton class IndexService { - val luceneSearchIndex = new DirectLucene(uniqueFields = List("path"), defaultFullTextSearchable = true) + val luceneSearchIndex = new DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = true) + private val id = luceneSearchIndex.create.field[String]("id") private val hierarchy = luceneSearchIndex.create.field[String]("hierarchy") private val path = luceneSearchIndex.create.field[String]("path", fullTextSearchable = false) + private val breadcrumb = luceneSearchIndex.create.field[String]("breadcrumb") + private val project = luceneSearchIndex.create.field[String]("project") private val branch = luceneSearchIndex.create.field[String]("branch") private val label = luceneSearchIndex.create.field[String]("label") private val description = luceneSearchIndex.create.field[String]("description") private val pageContent = luceneSearchIndex.create.field[String]("pageContent", sortable = false) - def addDocument(document: PageIndexDocument): Unit = { - luceneSearchIndex.doc().fields(hierarchy(document.hierarchy), description(document.description), label(document.label), this.path(document.path), branch(document.branch), pageContent(document.pageContent)).index() - } + def insertOrUpdateDocument(document: PageIndexDocument): Unit = { - def query(keywords: String): Seq[PageIndexDocument] = { - val results = luceneSearchIndex.query().sort(Sort.Score).filter(parse("label:" + keywords + "*^100 | pageContent:" + keywords + "*^30 | description:" + keywords + "*^50 | branch:" + keywords + "*^30")).highlight().search() - results.results.map { - result => PageIndexDocument(result(hierarchy), result(path), result(branch), result(label), result(description), result(pageContent)) + var i = 0 + while (exists(document) && i < 10) { + luceneSearchIndex.delete(new TermSearchTerm(Some(id), document.id)) + luceneSearchIndex.commit() + i = i + 1 } + + luceneSearchIndex.doc().fields(id(document.id), + hierarchy(document.hierarchy), + path(document.path), + breadcrumb(document.breadcrumb), + project(document.project), + branch(document.branch), + label(document.label), + description(document.description), + pageContent(document.pageContent) + ).index() + luceneSearchIndex.commit() + } + + private def exists(document: PageIndexDocument) = { + luceneSearchIndex.query().filter(new TermSearchTerm(Some(id), document.id)).search().results.length > 0 + } + + def query(keywords: String): SearchResult = { + val results = luceneSearchIndex.query().sort(Sort.Score).filter(parse("id:" + keywords + "*^20 " + " | " + + "breadcrumb:" + keywords + "*^50 " + " | " + + "project:" + keywords + "*^30 " + " | " + + "branch:" + keywords + "*^30" + " | " + + "label:" + keywords + "*^100 " + " | " + + "description:" + keywords + "*^100 " + " | " + + "pageContent:" + keywords + "*^30" + )).highlight().limit(50).search() + SearchResult(results.results.map { + result => + val highlighting = result.highlighting(breadcrumb).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(label).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(description).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(pageContent).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: Nil + SearchResultItem(PageIndexDocument(result(id), result(hierarchy), result(path), result(breadcrumb), result(project), result(branch), result(label), result(description), result(pageContent)), highlighting.flatten) + }) } def reset(): Unit = { @@ -38,7 +98,7 @@ class IndexService { class SearchService @Inject()(pageIndex: IndexService) { - def searchForPage(keywords: String): Seq[PageIndexDocument] = { + def searchForPage(keywords: String): SearchResult = { pageIndex.query(keywords) } diff --git a/conf/routes b/conf/routes index 86c793d70..907b59755 100644 --- a/conf/routes +++ b/conf/routes @@ -41,7 +41,7 @@ GET /api/config controllers.ConfigCon GET /api/directories controllers.DirectoryController.getDirectoryFromPath(path: String) GET /api/pages controllers.PageController.getPageFromPath(path: String) -GET /api/pages/search controllers.PageController.searchPage(keywords: String) +GET /api/pages/search controllers.PageController.searchPage(keyword: String) POST /api/admin/menu/refreshFromDatabase controllers.AdminController.refreshMenu() POST /api/admin/projects/refreshFromDatabase controllers.AdminController.refreshAllProjectsFromDatabase() diff --git a/documentation/Changelog.md b/documentation/Changelog.md index 218186a9a..1f195ff8a 100644 --- a/documentation/Changelog.md +++ b/documentation/Changelog.md @@ -8,6 +8,11 @@ } ``` +### [2020-09][1.4.0] + +The main features is: +- [0655 - Search](https://github.com/KelkooGroup/theGardener/issues/655) + ### [2020-07][1.3.2] The main features is: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index da1b4da9c..bd447c7fb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5556,14 +5556,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5582,7 +5580,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5762,8 +5759,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5869,8 +5865,7 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } } @@ -16623,8 +16618,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -16652,7 +16646,6 @@ "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -16667,8 +16660,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -16679,8 +16671,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -16797,8 +16788,7 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -16810,7 +16800,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -16825,7 +16814,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -16833,14 +16821,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -16859,7 +16845,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -16949,8 +16934,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -16962,7 +16946,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -17048,8 +17031,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -17085,7 +17067,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -17105,7 +17086,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -17149,14 +17129,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/frontend/src/app/_components/page/header/header.component.html b/frontend/src/app/_components/page/header/header.component.html index 7ecd7c313..79399e1bf 100644 --- a/frontend/src/app/_components/page/header/header.component.html +++ b/frontend/src/app/_components/page/header/header.component.html @@ -1,26 +1,29 @@
-
diff --git a/frontend/src/app/_components/page/header/header.component.scss b/frontend/src/app/_components/page/header/header.component.scss index fdf75a66e..364f7034f 100644 --- a/frontend/src/app/_components/page/header/header.component.scss +++ b/frontend/src/app/_components/page/header/header.component.scss @@ -19,7 +19,7 @@ flex-wrap: wrap; } -.header-logo, .header-navigation, .header-logo-the-gardener { +.header-logo, .header-navigation, .header-search, .header-logo-the-gardener { flex: 1; margin-top: 0; margin-bottom: 0; @@ -81,6 +81,10 @@ } +.header-navigation{ + padding-bottom: 8px; +} + .header-navigation a { transition: all 0.3s ease; /* Add transition for hover effects */ text-decoration: none; @@ -93,6 +97,10 @@ } } +.header-search{ + padding-right: 8px; +} + .header-navigation-responsive { list-style: none; } diff --git a/frontend/src/app/_components/page/header/header.component.spec.ts b/frontend/src/app/_components/page/header/header.component.spec.ts index af2105dc2..7365d8b45 100644 --- a/frontend/src/app/_components/page/header/header.component.spec.ts +++ b/frontend/src/app/_components/page/header/header.component.spec.ts @@ -17,6 +17,7 @@ import {MatIconModule} from '@angular/material/icon'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {FormsModule} from '@angular/forms'; import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SearchQueryComponent} from "../../../output/search/search-query/search-query.component"; describe('HeaderComponent', () => { let component: HeaderComponent; @@ -31,6 +32,7 @@ describe('HeaderComponent', () => { HeaderComponent, NavigateMobileMenuComponent, NavigateMenuItemComponent, + SearchQueryComponent ], imports: [ MatTabsModule, diff --git a/frontend/src/app/_components/page/header/header.component.ts b/frontend/src/app/_components/page/header/header.component.ts index 58d4ab299..b79f3335f 100644 --- a/frontend/src/app/_components/page/header/header.component.ts +++ b/frontend/src/app/_components/page/header/header.component.ts @@ -18,6 +18,7 @@ export class HeaderComponent implements OnInit { @Input() appTitle?: string; items: Array; + url: string; constructor(private menuService: MenuService, private routeService: RouteService, @@ -33,9 +34,10 @@ export class HeaderComponent implements OnInit { this.items = result; if (this.items.length > 0) { const currentRoute = this.getCurrentRoute(); - - if ( !currentRoute.nodes || currentRoute.nodes?.length === 0 || currentRoute.page === undefined) { - this.navigateTo(this.items[0]); + if ( this.url== "" || this.url== undefined || this.routeService.isNavigationUrl(this.url)) { + if (!currentRoute.nodes || currentRoute.nodes?.length === 0 || currentRoute.page === undefined) { + this.navigateTo(this.items[0]); + } } } }, error => { @@ -47,9 +49,15 @@ export class HeaderComponent implements OnInit { let params : NavigationParams; if (this.activatedRoute.firstChild && this.activatedRoute.firstChild.snapshot){ params = this.activatedRoute.firstChild.snapshot.params; + if (this.activatedRoute.firstChild.snapshot.url){ + this.url = this.activatedRoute.firstChild.snapshot.url.join('/'); + } } if (!params && this.activatedRoute && this.activatedRoute.snapshot){ params = this.activatedRoute.snapshot.params; + if (this.activatedRoute.firstChild.snapshot.url){ + this.url = this.activatedRoute.firstChild.snapshot.url.join('/'); + } } return this.routeService.navigationParamsToNavigationRoute(params); } @@ -60,6 +68,6 @@ export class HeaderComponent implements OnInit { } isItemInActivatedRoute(item: MenuHierarchy) { - return this.getCurrentRoute().nodes[0] === item.name; + return this.getCurrentRoute().nodes[0] === item.name; } } diff --git a/frontend/src/app/_models/page.ts b/frontend/src/app/_models/page.ts index f0d0c58ea..967f86b53 100644 --- a/frontend/src/app/_models/page.ts +++ b/frontend/src/app/_models/page.ts @@ -1,6 +1,7 @@ import {Scenario} from './gherkin'; import {OpenApiModel, OpenApiPath} from './open-api'; + export interface Page { title: string; path: string; diff --git a/frontend/src/app/_models/search.ts b/frontend/src/app/_models/search.ts new file mode 100644 index 000000000..345ab12c0 --- /dev/null +++ b/frontend/src/app/_models/search.ts @@ -0,0 +1,27 @@ + + +export interface SearchResult { + items: Array; +} + +export interface SearchResultItem { + page: PageIndexDocument; + highlights: Array; +} + +export interface PageIndexDocument { + id: string; + hierarchy: string; + path: string; + breadcrumb: string; + project: string; + branch: string; + label: string; + description: string; + pageContent: string; +} + +export interface HighlightedFragment { + fragment: string; + word: string; +} diff --git a/frontend/src/app/_services/page.service.spec.ts b/frontend/src/app/_services/page.service.spec.ts index 5ba9e66db..2ae5c91ff 100644 --- a/frontend/src/app/_services/page.service.spec.ts +++ b/frontend/src/app/_services/page.service.spec.ts @@ -7,6 +7,7 @@ import { PAGE_SERVICE_RESPONSE, PAGE_WITH_EXTERNAL_LINK_SERVICE_RESPONSE, PAGE_WITH_SCENARIO, + SEARCH_RESULTS } from '../_testUtils/test-data.spec'; import {IncludeExternalPagePart, MarkdownPart} from '../_models/page'; @@ -54,6 +55,21 @@ describe('PageService', () => { req.flush([DIRECTORIES_SERVICE_RESPONSE]); })); + xit('should search pages for keyword', async(() => { + pageService.searchPages('suggestions') + .subscribe(result => { + expect(result.items).toBeDefined(); + expect(result.items.length).toBe(1); + expect(result.items[0].page.label).toBe("The context"); + }); + + + const req = httpMock.expectOne(mockRequest => + mockRequest.method === 'GET' && mockRequest.url === 'api/pages/search' && mockRequest.params.get('keyword') === 'suggestions'); + req.flush([SEARCH_RESULTS]); + })); + + it('should get page for path', async(() => { pageService.getPage('publisherManagementWS>qa>_constraints_overview') .subscribe(page => { diff --git a/frontend/src/app/_services/page.service.ts b/frontend/src/app/_services/page.service.ts index 93ba02834..1c1bfd371 100644 --- a/frontend/src/app/_services/page.service.ts +++ b/frontend/src/app/_services/page.service.ts @@ -5,48 +5,59 @@ import {DirectoryApi, PageApi} from '../_models/hierarchy'; import {map} from 'rxjs/operators'; import {Page} from '../_models/page'; import {RouteService} from "./route.service"; +import {SearchResult} from "../_models/search"; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class PageService { - constructor(private http: HttpClient, - private routeService: RouteService) { - } + constructor(private http: HttpClient, + private routeService: RouteService) { + } + + getRootDirectoryForPath(path: string): Observable { + const url = `api/directories`; + const params = new HttpParams().set('path', this.routeService.urlToRelativePath(path)); + return this.http.get>(url, {params}) + .pipe( + map(res => res[0]) + ); + } - getRootDirectoryForPath(path: string): Observable { - const url = `api/directories`; - const params = new HttpParams().set('path', this.routeService.urlToRelativePath(path)); - return this.http.get>(url, {params}) - .pipe( - map(res => res[0]) - ); - } + getPage(path: string): Observable { + const url = `api/pages`; + const params = new HttpParams().set('path', this.routeService.urlToRelativePath(path)); + return this.http.get>(url, {params}) + .pipe( + map(res => res[0]), + map(page => { + const res: Page = { + title: this.getPageTitle(page), + path: page.path, + order: page.order, + parts: page.content, + sourceUrl: page.sourceUrl + }; + return res; + }) + ); + } - getPage(path: string): Observable { - const url = `api/pages`; - const params = new HttpParams().set('path', this.routeService.urlToRelativePath(path)); - return this.http.get>(url, {params}) - .pipe( - map(res => res[0]), - map(page => { - const res: Page = { - title: this.getPageTitle(page), - path: page.path, - order: page.order, - parts: page.content, - sourceUrl: page.sourceUrl - }; - return res; - }) - ); - } + searchPages(keyword: string): Observable { + const url = `api/pages/search`; + const params = new HttpParams().set('keyword', keyword); + return this.http.get(url, {params}) + .pipe( + map(res => res) + ); + } - private getPageTitle(page: PageApi): string { - if (page.content && page.content.length === 1 && page.content[0].type === 'includeExternalPage') { - return undefined; - } else { - return page.description; + private getPageTitle(page: PageApi): string { + if (page.content && page.content.length === 1 && page.content[0].type === 'includeExternalPage') { + return undefined; + } else { + return page.description; + } } - } + } diff --git a/frontend/src/app/_services/route.service.spec.ts b/frontend/src/app/_services/route.service.spec.ts index 74c3dcdc5..1f723b50f 100644 --- a/frontend/src/app/_services/route.service.spec.ts +++ b/frontend/src/app/_services/route.service.spec.ts @@ -11,6 +11,12 @@ describe('RouteService', () => { service = TestBed.get(RouteService); }); + it('isNavigationUrl ', () => { + expect(service.isNavigationUrl("app/documentation/search")).toEqual(false); + expect(service.isNavigationUrl("app/documentation/navigate")).toEqual(true); + + }); + it('navigationParamsToNavigationRoute focus on nodes', () => { expect(service.navigationParamsToNavigationRoute({nodes: "_publisher"}).nodes).toEqual(["publisher"]); expect(service.navigationParamsToNavigationRoute({nodes: "publisher"}).nodes).toEqual(["publisher"]); @@ -84,8 +90,13 @@ describe('RouteService', () => { }); it('backEndPathToNavigationRoute focus on directory', () => { - expect(service.backEndPathToNavigationRoute("publisherSystems>>/")).toEqual({ project: "publisherSystems", branch: "_", directories: [] }); - expect(service.backEndPathToNavigationRoute("publisherData>>/Public/")).toEqual({ project: "publisherData", branch: "_", directories: ["Public"] }); + expect(service.backEndPathToNavigationRoute("publisherSystems>>/")).toEqual({ project: "publisherSystems", branch: "_", directories: [], page:undefined }); + expect(service.backEndPathToNavigationRoute("publisherData>>/Public/")).toEqual({ project: "publisherData", branch: "_", directories: ["Public"], page:undefined }); + }); + + it('backEndHierarchyAndPathToFrontEndPath ', () => { + expect(service.backEndHierarchyAndPathToFrontEndPath("/platform/publisher/systems/mgt","pmws>qa>/Features/Events/Constraints")).toEqual("_platform_publisher_systems_mgt/pmws/qa/_Features_Events/Constraints"); + expect(service.backEndHierarchyAndPathToFrontEndPath("/platform/publisher/systems/mgt","pmbo>qa>/Meta")).toEqual("_platform_publisher_systems_mgt/pmbo/qa/_/Meta"); }); it('menuHierarchyToFrontEndPath focus on node without a directory ', () => { @@ -147,7 +158,7 @@ describe('RouteService', () => { expect(RouteService.legacyFullFrontEndUrlToFullFrontEndUrl("app/documentation/navigate/_publisher/publisherSystems/_/_/Services")).toEqual("app/documentation/navigate/_publisher/publisherSystems/_/_/Services"); }); - fit('relativeUrlToFullFrontEndUrl ', () => { + it('relativeUrlToFullFrontEndUrl ', () => { expect(service.relativeUrlToFullFrontEndUrl('../OfferSearch/Feature',{nodes: '_platform_publisher_systems_public',project: 'shoppingAPIPublic',branch: 'feature_PUB-4629-review-doc',directories: '_Features_OfferFeeds',page: 'Feature'} )) .toEqual("app/documentation/navigate/_platform_publisher_systems_public/shoppingAPIPublic/feature_PUB-4629-review-doc/_Features_OfferSearch/Feature"); diff --git a/frontend/src/app/_services/route.service.ts b/frontend/src/app/_services/route.service.ts index be9c00e24..7424d0930 100644 --- a/frontend/src/app/_services/route.service.ts +++ b/frontend/src/app/_services/route.service.ts @@ -5,6 +5,7 @@ import {MenuHierarchy} from '../_models/menu'; export const EMPTY_CHAR = '_'; export const EMPTY_CHAR_REGEX = /_/g; export const NAVIGATE_PATH = 'app/documentation/navigate/'; +export const SEARCH_PATH = 'app/documentation/search'; @Injectable({ providedIn: 'root' @@ -239,14 +240,24 @@ export class RouteService { } let directoriesAsString = backEndPathElements[2]; let directories = [] as Array; - if (directoriesAsString.length > 1 && directoriesAsString[0] === '/' && directoriesAsString[directoriesAsString.length - 1] === '/') { - directoriesAsString = directoriesAsString.substr(1, directoriesAsString.length - 2); - directories = directoriesAsString.split('/'); + let page = undefined; + + if (directoriesAsString.length == 1) { + + } else { + if (directoriesAsString.length > 1 && directoriesAsString[directoriesAsString.length - 1] === '/') { + directoriesAsString = directoriesAsString.substr(1, directoriesAsString.length - 2); + directories = directoriesAsString.split('/'); + } else { + directories = directoriesAsString.split('/'); + page = directories.pop() + } } return { project: projectId, branch, - directories + directories, + page }; } @@ -300,4 +311,21 @@ export class RouteService { && currentNavigationRoute.directories?.join(EMPTY_CHAR) === menuItemNavigationRoute.directories?.join(EMPTY_CHAR) && currentNavigationRoute.page === menuItemNavigationRoute.page; } + + isNavigationUrl(relativeUrl: string): Boolean { + return relativeUrl != undefined && relativeUrl.startsWith("app/documentation/navigate"); + } + + backEndHierarchyAndPathToFrontEndPath(hierarchy: string, path: string) { + + let navigationRoute = this.backEndPathToNavigationRoute(path); + let hierarchyFrontEndPath = hierarchy.replace(/\//g, EMPTY_CHAR); + let directories; + if (navigationRoute.directories.length > 1) { + directories = navigationRoute.directories.join(EMPTY_CHAR) + } else { + directories = EMPTY_CHAR + } + return hierarchyFrontEndPath + '/' + navigationRoute.project + '/' + navigationRoute.branch + '/' + directories + '/' + navigationRoute.page; + } } diff --git a/frontend/src/app/_testUtils/test-data.spec.ts b/frontend/src/app/_testUtils/test-data.spec.ts index 889a611e9..3e7bf2cfc 100644 --- a/frontend/src/app/_testUtils/test-data.spec.ts +++ b/frontend/src/app/_testUtils/test-data.spec.ts @@ -1,585 +1,613 @@ import {DirectoryApi, HierarchyNodeApi, PageApi} from '../_models/hierarchy'; import {MenuHierarchy, MenuType} from '../_models/menu'; +import {SearchResult} from "../_models/search"; export const MENU_HEADER_SERVICE_RESPONSE: MenuHierarchy = { - name: 'root', - label: 'Root', - type: 'Node' as MenuType, - depth: 0, - route: {nodes: [], directories: [] as Array}, - children: [ - { - name: 'publisher', - label: 'Publisher', - type: 'Node' as MenuType, - depth: 1, - route: {nodes: ['publisher'], directories: [] as Array}, - children: [] as Array - }, - { - name: 'tools', - label: 'Tools', - type: 'Node' as MenuType, - depth: 1, - route: {nodes: ['tools'], directories: [] as Array}, - children: [] as Array - } - ] + name: 'root', + label: 'Root', + type: 'Node' as MenuType, + depth: 0, + route: {nodes: [], directories: [] as Array}, + children: [ + { + name: 'publisher', + label: 'Publisher', + type: 'Node' as MenuType, + depth: 1, + route: {nodes: ['publisher'], directories: [] as Array}, + children: [] as Array + }, + { + name: 'tools', + label: 'Tools', + type: 'Node' as MenuType, + depth: 1, + route: {nodes: ['tools'], directories: [] as Array}, + children: [] as Array + } + ] }; export const MENU_SUBMENU_SERVICE_RESPONSE: HierarchyNodeApi = { - id: '.01.', - hierarchy: '_eng', - slugName: 'eng', - name: 'Engineering view', - childrenLabel: 'System groups', - childLabel: 'System group', - projects: [], - children: [ - { - id: '.01.01.', - hierarchy: '_eng_library', - slugName: 'library', - name: 'Library system group', - childrenLabel: 'Systems', - childLabel: 'System', - projects: [], - children: [ + id: '.01.', + hierarchy: '_eng', + slugName: 'eng', + name: 'Engineering view', + childrenLabel: 'System groups', + childLabel: 'System group', + projects: [], + children: [ { - id: '.01.01.01.', - hierarchy: '_eng_library_suggestion', - slugName: 'suggestion', - name: 'Suggestion system', - childrenLabel: 'Projects', - childLabel: 'Project', - projects: [ - { - id: 'suggestionsReports', - path: 'suggestionsReports', - label: 'Suggestions Reports', - stableBranch: 'master', - branches: [ + id: '.01.01.', + hierarchy: '_eng_library', + slugName: 'library', + name: 'Library system group', + childrenLabel: 'Systems', + childLabel: 'System', + projects: [], + children: [ { - name: 'master', - path: 'suggestionsReports>master' - } - ] - }, - { - id: 'suggestionsWS', - path: 'suggestionsWS', - label: 'Suggestions WebServices', - stableBranch: 'master', - branches: [ + id: '.01.01.01.', + hierarchy: '_eng_library_suggestion', + slugName: 'suggestion', + name: 'Suggestion system', + childrenLabel: 'Projects', + childLabel: 'Project', + projects: [ + { + id: 'suggestionsReports', + path: 'suggestionsReports', + label: 'Suggestions Reports', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'suggestionsReports>master' + } + ] + }, + { + id: 'suggestionsWS', + path: 'suggestionsWS', + label: 'Suggestions WebServices', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'suggestionsWS>master', + rootDirectory: { + id: '1', + path: 'suggestionsWS>master>/', + name: 'root', + label: 'SuggestionsWS', + description: 'Suggestions WebServices', + order: 0, + children: [ + { + id: '2', + path: 'suggestionsWS>master>/suggestions/', + name: 'suggestions', + label: 'Suggestions', + description: 'Suggestions...', + order: 0, + children: [] + }, + { + id: '3', + path: 'suggestionsWS>master>/admin/', + name: 'admin', + label: 'Admin', + description: 'Administration...', + order: 1, + children: [] + } + ] + } + }, + { + name: 'bugfix/351', + path: 'suggestionsWS>bugfix/351' + } + ] + } + ], + children: [] + }, { - name: 'master', - path: 'suggestionsWS>master', - rootDirectory: { - id: '1', - path: 'suggestionsWS>master>/', - name: 'root', - label: 'SuggestionsWS', - description: 'Suggestions WebServices', - order: 0, - children: [ - { - id: '2', - path: 'suggestionsWS>master>/suggestions/', - name: 'suggestions', - label: 'Suggestions', - description: 'Suggestions...', - order: 0, - children: [] - }, - { - id: '3', - path: 'suggestionsWS>master>/admin/', - name: 'admin', - label: 'Admin', - description: 'Administration...', - order: 1, - children: [] - } - ] - } + id: '.01.01.02.', + hierarchy: '_eng_library_user', + slugName: 'user', + name: 'User system', + childrenLabel: 'Projects', + childLabel: 'Project', + projects: [ + { + id: 'usersWS', + path: 'usersWS', + label: 'Users WebServices', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'usersWS>master' + } + ] + } + ], + children: [] }, { - name: 'bugfix/351', - path: 'suggestionsWS>bugfix/351' + id: '.01.01.03.', + hierarchy: '_eng_library_search', + slugName: 'search', + name: 'Search system', + childrenLabel: 'Projects', + childLabel: 'Project', + projects: [], + children: [] } - ] - } - ], - children: [] + ] + } + ] +}; + +export const DIRECTORIES_SERVICE_RESPONSE: DirectoryApi = { + id: '11', + path: 'publisherManagementWS>qa>/constraints/', + name: 'constraints', + label: 'Constraints', + description: 'Filter offers provided to the publisher', + order: 0, + pages: [ + { + path: 'publisherManagementWS>qa>/constraints/overview', + relativePath: 'overview', + name: 'overview', + label: 'Overview', + description: 'overview', + order: 0, }, { - id: '.01.01.02.', - hierarchy: '_eng_library_user', - slugName: 'user', - name: 'User system', - childrenLabel: 'Projects', - childLabel: 'Project', - projects: [ - { - id: 'usersWS', - path: 'usersWS', - label: 'Users WebServices', - stableBranch: 'master', - branches: [ - { - name: 'master', - path: 'usersWS>master' - } - ] - } - ], - children: [] + path: 'publisherManagementWS>qa>/constraints/for_a_publisher', + relativePath: 'for_a_publisher', + name: 'for_a_publisher', + label: 'For a publisher', + description: 'for_a_publisher', + order: 1, }, { - id: '.01.01.03.', - hierarchy: '_eng_library_search', - slugName: 'search', - name: 'Search system', - childrenLabel: 'Projects', - childLabel: 'Project', - projects: [], - children: [] + path: 'publisherManagementWS>qa>/constraints/for_a_merchant', + relativePath: 'for_a_merchant', + name: 'for_a_merchant', + label: 'For a merchant', + description: 'for_a_merchant', + order: 3, + }, + { + path: 'publisherManagementWS>qa>/constraints/for_an_offer', + relativePath: 'for_an_offer', + name: 'for_an_offer', + label: 'For an offer', + description: 'for_an_offer', + order: 2, } - ] - } - ] -}; - -export const DIRECTORIES_SERVICE_RESPONSE: DirectoryApi = { - id: '11', - path: 'publisherManagementWS>qa>/constraints/', - name: 'constraints', - label: 'Constraints', - description: 'Filter offers provided to the publisher', - order: 0, - pages: [ - { - path: 'publisherManagementWS>qa>/constraints/overview', - relativePath: 'overview', - name: 'overview', - label: 'Overview', - description: 'overview', - order: 0, - }, - { - path: 'publisherManagementWS>qa>/constraints/for_a_publisher', - relativePath: 'for_a_publisher', - name: 'for_a_publisher', - label: 'For a publisher', - description: 'for_a_publisher', - order: 1, - }, - { - path: 'publisherManagementWS>qa>/constraints/for_a_merchant', - relativePath: 'for_a_merchant', - name: 'for_a_merchant', - label: 'For a merchant', - description: 'for_a_merchant', - order: 3, - }, - { - path: 'publisherManagementWS>qa>/constraints/for_an_offer', - relativePath: 'for_an_offer', - name: 'for_an_offer', - label: 'For an offer', - description: 'for_an_offer', - order: 2, - } - ] + ] }; export const PAGE_SERVICE_RESPONSE: PageApi = { - path: 'publisherManagementWS>qa>/constraints/overview', - relativePath: '/constraints/overview', - name: 'overview', - label: 'overview', - description: 'overview', - order: 0, - // tslint:disable-next-line:max-line-length - content: [ - { - type: 'markdown', - data: { - markdown: 'For various reasons, the offers provided to the publishers can be filtered. We don\'t necessary want to provide all the\n' + - 'offers to the publishers :\n' + - '\n' + - '![Overview](../assets/images/constraints_overview.png) \n' + - '\n' + - 'This is the constraints objective. The constraints can be defined manually by the **BizDevs** or automatically by the\n' + - '**TrafficOptimizer**.\n' + - '\n' + - '### Reference and application\n' + - '\n' + - 'The constraints are defined and stored in PMWS component. The impact of those constraints is implemented at the client\n' + - 'level:\n' + - '- **eCS** and **ShoppingAPI** are filtering the call to Search6\n' + - '- **FeedService** is filtering the offers provided (offers coming from OfferProcessing)\n' + - '- others systems are also using the constraints to filter offers provided to external clients : example GSA Exporter or\n' + - 'COP. \n' + - '\n' + - '\n' + - 'The constraints are stored against a profile (which is linked to a contract, which is linked to the publisher\n' + - 'itself ([See details](thegardener://${current.project}/${current.branch}/overview))). All trackings of a profile share\n' + - 'the same constraints.\n' + - '\n' + - 'Clients are most of the time using a tracking as input data to find out what are the constraints to be applied.\n' + - '\n' + - '### Different sources of constraints\n' + - '\n' + - 'There are several ways to define the constraints :\n' + - '- for a publisher, [filter the merchants that can provide offers](thegardener://${current.project}/${current.branch}/constraints/for_a_publisher). \n' + - '- for a merchant, [filter the publishers that can receive offers](thegardener://${current.project}/${current.branch}/constraints/for_a_merchant).\n' + - '- moreover, [offers can be filtered for various reasons](thegardener://${current.project}/${current.branch}/constraints/for_an_offer).\n' + - '\n' + - '![Sources](../assets/images/constraints_sources_overview.png)\n' + - '\n' + - 'All those filters are cumulative : **each offer need to pass through all the filters**. In other words it\'s a AND\n' + - 'between each constraint. \n' + - '\n' + - 'We can see the impact of [those constraints on the PMBO](thegardener://${current.project}/${current.branch}/constraints/from_pmbo). ' - } - } - ] + path: 'publisherManagementWS>qa>/constraints/overview', + relativePath: '/constraints/overview', + name: 'overview', + label: 'overview', + description: 'overview', + order: 0, + // tslint:disable-next-line:max-line-length + content: [ + { + type: 'markdown', + data: { + markdown: 'For various reasons, the offers provided to the publishers can be filtered. We don\'t necessary want to provide all the\n' + + 'offers to the publishers :\n' + + '\n' + + '![Overview](../assets/images/constraints_overview.png) \n' + + '\n' + + 'This is the constraints objective. The constraints can be defined manually by the **BizDevs** or automatically by the\n' + + '**TrafficOptimizer**.\n' + + '\n' + + '### Reference and application\n' + + '\n' + + 'The constraints are defined and stored in PMWS component. The impact of those constraints is implemented at the client\n' + + 'level:\n' + + '- **eCS** and **ShoppingAPI** are filtering the call to Search6\n' + + '- **FeedService** is filtering the offers provided (offers coming from OfferProcessing)\n' + + '- others systems are also using the constraints to filter offers provided to external clients : example GSA Exporter or\n' + + 'COP. \n' + + '\n' + + '\n' + + 'The constraints are stored against a profile (which is linked to a contract, which is linked to the publisher\n' + + 'itself ([See details](thegardener://${current.project}/${current.branch}/overview))). All trackings of a profile share\n' + + 'the same constraints.\n' + + '\n' + + 'Clients are most of the time using a tracking as input data to find out what are the constraints to be applied.\n' + + '\n' + + '### Different sources of constraints\n' + + '\n' + + 'There are several ways to define the constraints :\n' + + '- for a publisher, [filter the merchants that can provide offers](thegardener://${current.project}/${current.branch}/constraints/for_a_publisher). \n' + + '- for a merchant, [filter the publishers that can receive offers](thegardener://${current.project}/${current.branch}/constraints/for_a_merchant).\n' + + '- moreover, [offers can be filtered for various reasons](thegardener://${current.project}/${current.branch}/constraints/for_an_offer).\n' + + '\n' + + '![Sources](../assets/images/constraints_sources_overview.png)\n' + + '\n' + + 'All those filters are cumulative : **each offer need to pass through all the filters**. In other words it\'s a AND\n' + + 'between each constraint. \n' + + '\n' + + 'We can see the impact of [those constraints on the PMBO](thegardener://${current.project}/${current.branch}/constraints/from_pmbo). ' + } + } + ] }; export const PAGE_WITH_EXTERNAL_LINK_SERVICE_RESPONSE: PageApi = { - path: 'publisherManagementWS>qa>/constraints/overview', - relativePath: '/constraints/overview', - name: 'overview', - label: 'overview', - description: 'overview', - order: 0, - // tslint:disable-next-line:max-line-length - content: [{ - type: 'includeExternalPage', - data: { - includeExternalPage: 'http://publisher.corp.kelkoo.net/docs/#/Contact%20Management/getContact' - } - }] + path: 'publisherManagementWS>qa>/constraints/overview', + relativePath: '/constraints/overview', + name: 'overview', + label: 'overview', + description: 'overview', + order: 0, + // tslint:disable-next-line:max-line-length + content: [{ + type: 'includeExternalPage', + data: { + includeExternalPage: 'http://publisher.corp.kelkoo.net/docs/#/Contact%20Management/getContact' + } + }] }; export const PAGE_WITH_SCENARIO: PageApi = { - path: 'suggestionsWS>master>/context', - relativePath: '/context', - name: 'context', - label: 'The context', - description: 'Why providing suggestions', - order: 0, - content: [ - { - type: 'markdown', - data: { - markdown: '**Feature**: Provide book suggestions\n\n\n' - } - }, - { - type: 'scenarios', - data: { - scenarios: { - id: '692', - branchId: '44', - path: 'test/features/register_projects/register_a_project.feature', - background: { - id: '0', - keyword: 'Background', - name: '', - description: '', - steps: [ - { - id: '0', - keyword: 'Given', - text: 'the database is empty', - argument: [] - }, - { - id: '1', - keyword: 'And', - text: 'the cache is empty', - argument: [] - } - ] - }, - tags: [], - language: 'en', - keyword: 'Feature', - name: 'Register a project', - description: 'As a user,\n I want to register my project into theGardener\n So that my project BDD features will be shared with all users', - scenarios: [ - { - keyword: 'Scenario', - name: 'get a project', - description: '', - tags: [ - 'level_2_technical_details', - 'nominal_case', - 'valid' - ], - abstractionLevel: 'level_2_technical_details', - id: '4968', - caseType: 'nominal_case', - steps: [ - { - id: '0', - keyword: 'Given', - text: 'we have the following projects', - argument: [ - [ - 'id', - 'name', - 'repositoryUrl', - 'stableBranch', - 'featuresRootPath' + path: 'suggestionsWS>master>/context', + relativePath: '/context', + name: 'context', + label: 'The context', + description: 'Why providing suggestions', + order: 0, + content: [ + { + type: 'markdown', + data: { + markdown: '**Feature**: Provide book suggestions\n\n\n' + } + }, + { + type: 'scenarios', + data: { + scenarios: { + id: '692', + branchId: '44', + path: 'test/features/register_projects/register_a_project.feature', + background: { + id: '0', + keyword: 'Background', + name: '', + description: '', + steps: [ + { + id: '0', + keyword: 'Given', + text: 'the database is empty', + argument: [] + }, + { + id: '1', + keyword: 'And', + text: 'the cache is empty', + argument: [] + } + ] + }, + tags: [], + language: 'en', + keyword: 'Feature', + name: 'Register a project', + description: 'As a user,\n I want to register my project into theGardener\n So that my project BDD features will be shared with all users', + scenarios: [ + { + keyword: 'Scenario', + name: 'get a project', + description: '', + tags: [ + 'level_2_technical_details', + 'nominal_case', + 'valid' + ], + abstractionLevel: 'level_2_technical_details', + id: '4968', + caseType: 'nominal_case', + steps: [ + { + id: '0', + keyword: 'Given', + text: 'we have the following projects', + argument: [ + [ + 'id', + 'name', + 'repositoryUrl', + 'stableBranch', + 'featuresRootPath' + ], + [ + 'suggestionsWS', + 'Suggestions WebServices', + 'git@gitlab.corp.kelkoo.net:library/suggestionsWS.git', + 'master', + 'test/features' + ] + ] + }, + { + id: '1', + keyword: 'When', + text: 'I perform a \"GET\" on following URL \"/api/projects/suggestionsWS\"', + argument: [] + }, + { + id: '2', + keyword: 'Then', + text: 'I get a response with status \"200\"', + argument: [] + }, + { + id: '3', + keyword: 'And', + text: 'I get the following json response body', + argument: [ + [ + '{\n \"id\": \"suggestionsWS\",\n \"name\": \"Suggestions WebServices\",\n \"repositoryUrl\": \"git@gitlab.corp.kelkoo.net:library/suggestionsWS.git\",\n \"stableBranch\": \"master\",\n \"featuresRootPath\": \"test/features\"\n}' + ] + ] + } + ], + workflowStep: 'valid' + } ], - [ - 'suggestionsWS', - 'Suggestions WebServices', - 'git@gitlab.corp.kelkoo.net:library/suggestionsWS.git', - 'master', - 'test/features' - ] - ] - }, - { - id: '1', - keyword: 'When', - text: 'I perform a \"GET\" on following URL \"/api/projects/suggestionsWS\"', - argument: [] - }, - { - id: '2', - keyword: 'Then', - text: 'I get a response with status \"200\"', - argument: [] - }, - { - id: '3', - keyword: 'And', - text: 'I get the following json response body', - argument: [ - [ - '{\n \"id\": \"suggestionsWS\",\n \"name\": \"Suggestions WebServices\",\n \"repositoryUrl\": \"git@gitlab.corp.kelkoo.net:library/suggestionsWS.git\",\n \"stableBranch\": \"master\",\n \"featuresRootPath\": \"test/features\"\n}' - ] - ] + comments: [] } - ], - workflowStep: 'valid' } - ], - comments: [] + }, + { + type: 'markdown', + data: { + markdown: '\n**Footer**\n\n' + } } - } - }, - { - type: 'markdown', - data: { - markdown: '\n**Footer**\n\n' - } - } - ] + ] }; export const MENU_SERVICE_RESPONSE: HierarchyNodeApi = { - id: '.', - hierarchy: '_', - slugName: 'root', - name: 'Hierarchy root', - childrenLabel: 'Views', - childLabel: 'View', - projects: [], - children: [ - { - id: '.01.', - hierarchy: '_eng', - slugName: 'eng', - name: 'Engineering view', - childrenLabel: 'System groups', - childLabel: 'System group', - projects: [], - children: [ + id: '.', + hierarchy: '_', + slugName: 'root', + name: 'Hierarchy root', + childrenLabel: 'Views', + childLabel: 'View', + projects: [], + children: [ { - id: '.01.01.', - hierarchy: '_eng_library', - slugName: 'library', - name: 'Library system group', - childrenLabel: 'Systems', - childLabel: 'System', - projects: [], - children: [ - { - id: '.01.01.01.', - hierarchy: '_eng_library_suggestion', - slugName: 'suggestion', - name: 'Suggestion system', - childrenLabel: 'Projects', - childLabel: 'Project', - projects: [ - { - id: 'suggestionsReports', - path: 'suggestionsReports', - label: 'Suggestions Reports', - stableBranch: 'master', - branches: [ - { - name: 'master', - path: 'suggestionsReports>master' - } - ] - }, + id: '.01.', + hierarchy: '_eng', + slugName: 'eng', + name: 'Engineering view', + childrenLabel: 'System groups', + childLabel: 'System group', + projects: [], + children: [ { - id: 'suggestionsWS', - path: 'suggestionsWS', - label: 'Suggestions WebServices', - stableBranch: 'master', - branches: [ - { - name: 'master', - path: 'suggestionsWS>master', - rootDirectory: { - id: '1', - path: 'suggestionsWS>master>/', - name: 'root', - label: 'SuggestionsWS', - description: 'Suggestions WebServices', - order: 0, - pages: [ - 'context' - ], - children: [ - { - id: '2', - path: 'suggestionsWS>master>/suggestions/', - name: 'suggestions', - label: 'Suggestions', - description: 'Suggestions...', - order: 0, - pages: [ - 'suggestionsWS>master>/suggestions/suggestion', - 'suggestionsWS>master>/suggestions/examples' + id: '.01.01.', + hierarchy: '_eng_library', + slugName: 'library', + name: 'Library system group', + childrenLabel: 'Systems', + childLabel: 'System', + projects: [], + children: [ + { + id: '.01.01.01.', + hierarchy: '_eng_library_suggestion', + slugName: 'suggestion', + name: 'Suggestion system', + childrenLabel: 'Projects', + childLabel: 'Project', + projects: [ + { + id: 'suggestionsReports', + path: 'suggestionsReports', + label: 'Suggestions Reports', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'suggestionsReports>master' + } + ] + }, + { + id: 'suggestionsWS', + path: 'suggestionsWS', + label: 'Suggestions WebServices', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'suggestionsWS>master', + rootDirectory: { + id: '1', + path: 'suggestionsWS>master>/', + name: 'root', + label: 'SuggestionsWS', + description: 'Suggestions WebServices', + order: 0, + pages: [ + 'context' + ], + children: [ + { + id: '2', + path: 'suggestionsWS>master>/suggestions/', + name: 'suggestions', + label: 'Suggestions', + description: 'Suggestions...', + order: 0, + pages: [ + 'suggestionsWS>master>/suggestions/suggestion', + 'suggestionsWS>master>/suggestions/examples' + ], + children: [] + }, + { + id: '3', + path: 'suggestionsWS>master>/admin/', + name: 'admin', + label: 'Admin', + description: 'Administration...', + order: 1, + pages: [ + 'suggestionsWS>master>/admin/admin' + ], + children: [] + } + ] + } + }, + { + name: 'bugfix/351', + path: 'suggestionsWS>bugfix/351' + } + ] + } ], children: [] - }, - { - id: '3', - path: 'suggestionsWS>master>/admin/', - name: 'admin', - label: 'Admin', - description: 'Administration...', - order: 1, - pages: [ - 'suggestionsWS>master>/admin/admin' + }, + { + id: '.01.01.02.', + hierarchy: '_eng_library_user', + slugName: 'user', + name: 'User system', + childrenLabel: 'Projects', + childLabel: 'Project', + projects: [ + { + id: 'usersWS', + path: 'usersWS', + label: 'Users WebServices', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'usersWS>master' + } + ] + } ], children: [] - } - ] - } - }, - { - name: 'bugfix/351', - path: 'suggestionsWS>bugfix/351' - } - ] + }, + { + id: '.01.01.03.', + hierarchy: '_eng_library_search', + slugName: 'search', + name: 'Search system', + childrenLabel: 'Projects', + childLabel: 'Project', + projects: [], + children: [] + } + ] } - ], - children: [] - }, - { - id: '.01.01.02.', - hierarchy: '_eng_library_user', - slugName: 'user', - name: 'User system', - childrenLabel: 'Projects', - childLabel: 'Project', - projects: [ + ] + }, + { + id: '.02.', + hierarchy: '_biz', + slugName: 'biz', + name: 'Business view', + childrenLabel: 'Units', + childLabel: 'Unit', + projects: [ { - id: 'usersWS', - path: 'usersWS', - label: 'Users WebServices', - stableBranch: 'master', - branches: [ - { - name: 'master', - path: 'usersWS>master' - } - ] + id: 'suggestionsWS', + path: 'suggestionsWS', + label: 'Suggestions WebServices', + stableBranch: 'master', + branches: [ + { + name: 'master', + path: 'suggestionsWS>master' + }, + { + name: 'bugfix/351', + path: 'suggestionsWS>bugfix/351' + } + ] } - ], - children: [] - }, - { - id: '.01.01.03.', - hierarchy: '_eng_library_search', - slugName: 'search', - name: 'Search system', - childrenLabel: 'Projects', - childLabel: 'Project', - projects: [], - children: [] - } - ] + ], + children: [] } - ] - }, - { - id: '.02.', - hierarchy: '_biz', - slugName: 'biz', - name: 'Business view', - childrenLabel: 'Units', - childLabel: 'Unit', - projects: [ + ] +}; + +export const MARKDOWN_WITH_ESCAPED_MARKDOWN: PageApi = { + path: 'theGardener>master>/guides/write', + relativePath: '/guides/write', + name: 'write', + label: 'write', + description: 'write', + order: 3, + content: [ { - id: 'suggestionsWS', - path: 'suggestionsWS', - label: 'Suggestions WebServices', - stableBranch: 'master', - branches: [ - { - name: 'master', - path: 'suggestionsWS>master' - }, - { - name: 'bugfix/351', - path: 'suggestionsWS>bugfix/351' + type: 'markdown', + data: { + markdown: '**Markdown containing escaped Markdown\n' + + '````\n' + + '```thegardener\n' + + '{\n' + + ' "page" :\n' + + ' {\n' + + ' "label": "Write documentation",\n' + + ' "description": "How to write documentation with theGardener format ?"\n' + + ' }\n' + + '}\n' + + '```\n' + + '````' } - ] } - ], - children: [] - } - ] + ] }; -export const MARKDOWN_WITH_ESCAPED_MARKDOWN: PageApi = { - path: 'theGardener>master>/guides/write', - relativePath: '/guides/write', - name: 'write', - label: 'write', - description: 'write', - order: 3, - content: [ - { - type: 'markdown', - data: { - markdown: '**Markdown containing escaped Markdown\n' + - '````\n' + - '```thegardener\n' + - '{\n' + - ' "page" :\n' + - ' {\n' + - ' "label": "Write documentation",\n' + - ' "description": "How to write documentation with theGardener format ?"\n' + - ' }\n' + - '}\n' + - '```\n' + - '````' - } - } - ] +export const SEARCH_RESULTS: SearchResult ={ + "items": [ + { + "page": { + "id": "/suggestion/suggestionsWS>master>/context", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/context", + "breadcrumb": " Suggestion system / Suggestions WebServices / The context", + "project": "Suggestions WebServices", + "branch": "master", + "label": "The context", + "description": "Why providing suggestions", + "pageContent": "**Feature**: Provide book suggestions" + }, + "highlights": [ + { + "fragment": " Suggestion system / Suggestions WebServices / The context", + "word": "context" + }, + { + "fragment": "The context", + "word": "context" + } + ] + } + ] }; - diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index a94ee6697..4f7814cf9 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,6 +1,7 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {NavigatePageComponent} from './output/navigate/navigate-page.component'; +import {SearchPageComponent} from "./output/search/search-page/search-page.component"; const routes: Routes = [ { @@ -39,7 +40,12 @@ const routes: Routes = [ path: 'app/documentation/navigate/:nodes/:project/:branch/:directories/:page', component: NavigatePageComponent, pathMatch: 'full', - } + }, + { + path: 'app/documentation/search', + component: SearchPageComponent, + pathMatch: 'full', + }, ]; @NgModule({ diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 91a85edce..9901b5890 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -49,6 +49,10 @@ import {AnchorPipe} from './anchor.pipe'; import {OpenApiModelComponent} from './output/page-content/open-api-model/open-api-model.component'; import { OpenApiEndPointsComponent } from './output/page-content/open-api-end-points/open-api-end-points.component'; import { NavigateMobileMenuComponent } from './output/navigate/navigate-mobile-menu/navigate-mobile-menu.component'; +import { SearchPageComponent } from './output/search/search-page/search-page.component'; +import { SearchResultsComponent } from './output/search/search-results/search-results.component'; +import { SearchResultItemComponent } from './output/search/search-result-item/search-result-item.component'; +import { SearchQueryComponent } from './output/search/search-query/search-query.component'; const nonProductionProviders = [{ provide: HTTP_INTERCEPTORS, @@ -78,6 +82,10 @@ const nonProductionProviders = [{ OpenApiModelComponent, OpenApiEndPointsComponent, NavigateMobileMenuComponent, + SearchPageComponent, + SearchResultsComponent, + SearchResultItemComponent, + SearchQueryComponent, ], imports: [ HttpClientModule, diff --git a/frontend/src/app/output/search/search-page/search-page.component.html b/frontend/src/app/output/search/search-page/search-page.component.html new file mode 100644 index 000000000..384da39fe --- /dev/null +++ b/frontend/src/app/output/search/search-page/search-page.component.html @@ -0,0 +1,9 @@ +
+
+

Search results for

+

'{{keyword}}'

+
+
+ +
+
diff --git a/frontend/src/app/output/search/search-page/search-page.component.scss b/frontend/src/app/output/search/search-page/search-page.component.scss new file mode 100644 index 000000000..c72f4a107 --- /dev/null +++ b/frontend/src/app/output/search/search-page/search-page.component.scss @@ -0,0 +1,31 @@ +@import "backoffice"; + +.page { + display: grid; + grid-template-columns: minmax(300px, 20%) 1fr; + grid-template-areas: "sidebar content"; + width: 100%; + height: 100%; + + @media screen and (max-width: 600px) { + display: grid; + grid-template-columns: 1fr; + grid-template-areas: "content"; + } +} + +.content { + grid-area: content; + overflow: auto; + margin-top: 3em; + margin-left: 2em; + + @media screen and (max-width: 600px) { + grid-column: 1 / span 2; + } +} + +.title { + margin-left: 1em; +} + diff --git a/frontend/src/app/output/search/search-page/search-page.component.ts b/frontend/src/app/output/search/search-page/search-page.component.ts new file mode 100644 index 000000000..001e554b6 --- /dev/null +++ b/frontend/src/app/output/search/search-page/search-page.component.ts @@ -0,0 +1,34 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {ActivatedRoute} from "@angular/router"; +import {PageService} from "../../../_services/page.service"; +import {SearchResult} from "../../../_models/search"; +import {NotificationService} from "../../../_services/notification.service"; + +@Component({ + selector: 'app-search-page', + templateUrl: './search-page.component.html', + styleUrls: ['./search-page.component.scss'] +}) +export class SearchPageComponent implements OnInit { + + @Input() keyword: string; + searchResult: SearchResult; + + constructor( private activatedRoute: ActivatedRoute, private notificationService: NotificationService, private pageService: PageService) { } + + ngOnInit(): void { + this.activatedRoute.queryParams.subscribe(queryParams => { + this.keyword = queryParams.keyword + this.search(); + }); + } + + private search() { + this.pageService.searchPages(this.keyword).subscribe( + result => { + this.searchResult = result; + }, error => { + this.notificationService.showError('Error while searching in the page index', error); + }); + } +} diff --git a/frontend/src/app/output/search/search-query/search-query.component.html b/frontend/src/app/output/search/search-query/search-query.component.html new file mode 100644 index 000000000..dc9eff539 --- /dev/null +++ b/frontend/src/app/output/search/search-query/search-query.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/frontend/src/app/output/search/search-query/search-query.component.scss b/frontend/src/app/output/search/search-query/search-query.component.scss new file mode 100644 index 000000000..2c4e39f2c --- /dev/null +++ b/frontend/src/app/output/search/search-query/search-query.component.scss @@ -0,0 +1,16 @@ +@import "../../../../stylesheets/backoffice"; + +.searchContainer { + display: inline-flex; + flex: 1 1 300px; + position: relative; + border: 1px solid $color-border-lighter; + border-radius: 5px; + overflow: hidden; +} + +.searchIcon { + padding: 0.5rem; +} + + diff --git a/frontend/src/app/output/search/search-query/search-query.component.ts b/frontend/src/app/output/search/search-query/search-query.component.ts new file mode 100644 index 000000000..f4a9e3428 --- /dev/null +++ b/frontend/src/app/output/search/search-query/search-query.component.ts @@ -0,0 +1,30 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from "@angular/router"; +import {SEARCH_PATH} from "../../../_services/route.service"; + +@Component({ + selector: 'app-search-query', + templateUrl: './search-query.component.html', + styleUrls: ['./search-query.component.scss'] +}) +export class SearchQueryComponent implements OnInit { + + @Input() keyword: string + + constructor(private activatedRoute: ActivatedRoute, private router: Router) { + } + + ngOnInit(): void { + if (this.activatedRoute.queryParams) { + this.activatedRoute.queryParams.subscribe(queryParams => { + this.keyword = queryParams.keyword + }); + } + } + + search() { + if (this.keyword != "") { + this.router.navigateByUrl(SEARCH_PATH + `?keyword=${this.keyword.trim()}`) + } + } +} diff --git a/frontend/src/app/output/search/search-result-item/search-result-item.component.html b/frontend/src/app/output/search/search-result-item/search-result-item.component.html new file mode 100644 index 000000000..270f7c019 --- /dev/null +++ b/frontend/src/app/output/search/search-result-item/search-result-item.component.html @@ -0,0 +1,4 @@ +{{ showIndex() }} - {{ item.page.breadcrumb }} +

+ "... ..." +

diff --git a/frontend/src/app/output/search/search-result-item/search-result-item.component.scss b/frontend/src/app/output/search/search-result-item/search-result-item.component.scss new file mode 100644 index 000000000..dfa8552ed --- /dev/null +++ b/frontend/src/app/output/search/search-result-item/search-result-item.component.scss @@ -0,0 +1,6 @@ +@import "backoffice"; + +::ng-deep em{ + font-weight: bold; + color: $color-selected-item; +} diff --git a/frontend/src/app/output/search/search-result-item/search-result-item.component.ts b/frontend/src/app/output/search/search-result-item/search-result-item.component.ts new file mode 100644 index 000000000..0230f330e --- /dev/null +++ b/frontend/src/app/output/search/search-result-item/search-result-item.component.ts @@ -0,0 +1,31 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {PageIndexDocument, SearchResultItem} from "../../../_models/search"; +import {NAVIGATE_PATH, RouteService} from "../../../_services/route.service"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-search-result-item', + templateUrl: './search-result-item.component.html', + styleUrls: ['./search-result-item.component.scss'] +}) +export class SearchResultItemComponent implements OnInit { + + @Input() item: SearchResultItem; + @Input() index: number; + + + constructor(private routeService: RouteService,private router: Router) { } + + ngOnInit(): void { + } + + navigateTo(page: PageIndexDocument){ + let path = this.routeService.backEndHierarchyAndPathToFrontEndPath(page.hierarchy,page.path); + this.router.navigateByUrl( NAVIGATE_PATH + path); + } + + showIndex(): string { + return (this.index + 1).toString() + } + +} diff --git a/frontend/src/app/output/search/search-results/search-results.component.html b/frontend/src/app/output/search/search-results/search-results.component.html new file mode 100644 index 000000000..1b18a89a6 --- /dev/null +++ b/frontend/src/app/output/search/search-results/search-results.component.html @@ -0,0 +1,10 @@ + +
+
+ +
+
+ + + No results + diff --git a/frontend/src/app/output/search/search-results/search-results.component.scss b/frontend/src/app/output/search/search-results/search-results.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/output/search/search-results/search-results.component.ts b/frontend/src/app/output/search/search-results/search-results.component.ts new file mode 100644 index 000000000..71fd17a20 --- /dev/null +++ b/frontend/src/app/output/search/search-results/search-results.component.ts @@ -0,0 +1,14 @@ +import {Component, Input} from '@angular/core'; +import {SearchResult} from "../../../_models/search"; + +@Component({ + selector: 'app-search-results', + templateUrl: './search-results.component.html', + styleUrls: ['./search-results.component.scss'] +}) +export class SearchResultsComponent { + + @Input() searchResult: SearchResult; + + +} diff --git a/local-conf/application.dev.conf b/local-conf/application.dev.conf index 68690a609..79104cc6a 100644 --- a/local-conf/application.dev.conf +++ b/local-conf/application.dev.conf @@ -5,8 +5,6 @@ application.title = "In documentation we trust." application.logoSrc = "assets/images/logo-white.png" -projects.synchronize.from.remote.enabled=false - db.default.driver=com.mysql.jdbc.Driver db.default.url="jdbc:mysql://dc1-thegardener-dev-srv-01.dev.dc1.kelkoo.net:3306/thegardener" db.default.username=root diff --git a/test/OnGoingBDDTest.scala b/test/OnGoingBDDTest.scala index dbbab22c8..16a2f9836 100644 --- a/test/OnGoingBDDTest.scala +++ b/test/OnGoingBDDTest.scala @@ -1,9 +1,7 @@ import io.cucumber.junit.{Cucumber, CucumberOptions} -import org.junit._ import org.junit.runner._ @RunWith(classOf[Cucumber]) -@Ignore @CucumberOptions( features = Array("test/features"), glue = Array("steps"), diff --git a/test/features/navigation/provide_menu.feature b/test/features/navigation/provide_menu.feature index 04cd41f9d..840efe750 100644 --- a/test/features/navigation/provide_menu.feature +++ b/test/features/navigation/provide_menu.feature @@ -807,7 +807,7 @@ Feature: Provide menu } """ - @level_2_technical_details @nominal_case @valid @ongoing + @level_2_technical_details @nominal_case @valid Scenario: provide sub menu with shortcuts Given the hierarchy nodes are | id | slugName | name | childrenLabel | childLabel | directoryPath | shortcut | diff --git a/test/features/search/search_pages.feature b/test/features/search/search_pages.feature new file mode 100644 index 000000000..ef87b45cc --- /dev/null +++ b/test/features/search/search_pages.feature @@ -0,0 +1,297 @@ +@ongoing +Feature: Full text search pages + + Background: + Given the database is empty + And the cache is empty + And No project is checkout + And the remote projects are empty + And the hierarchy nodes are + | id | slugName | name | childrenLabel | childLabel | directoryPath | + | . | root | Hierarchy root | Views | View | | + | .01. | suggestion | Suggestion system | Projects | Project | | + | .02. | lib | Library | Projects | Project | libraryDoc>master>/ | + And we have the following projects + | id | name | repositoryUrl | stableBranch | featuresRootPath | documentationRootPath | + | suggestionsWS | Suggestions WebServices | target/remote/data/GetFeatures/library/suggestionsWS/ | master | test/features | doc | + | libraryDoc | library system group documentation | target/remote/data/GetFeatures/library/libraryDoc/ | master | test/features | doc | + And the links between hierarchy nodes are + | projectId | hierarchyId | + | suggestionsWS | .01. | + And we have those branches in the database + | id | name | isStable | projectId | + | 1 | master | true | suggestionsWS | + | 2 | feature/654-simple-full-text-search-on-pages | false | suggestionsWS | + | 3 | master | true | libraryDoc | + + @level_2_technical_details @nominal_case @valid + Scenario: get result of a search + Given we have those directories in the database + | id | name | label | description | order | relativePath | path | branchId | + | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | + | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | + | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | + And we have those pages in the database + | id | name | label | description | order | relativePath | path | markdown | directoryId | + | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | + | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | + | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | + | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | + And the lucene index is loaded from the database + And I perform a "GET" on following URL "/api/pages/search?keyword=context" + Then I get a response with status "200" + Then I get the following json response body + """ +{ + "items": [ + { + "page": { + "id": "/suggestion/suggestionsWS>master>/context", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/context", + "breadcrumb": "Suggestion system / Suggestions WebServices / The context", + "project": "Suggestions WebServices", + "branch": "master", + "label": "The context", + "description": "Why providing suggestions", + "pageContent": "**Feature**: Provide book suggestions" + }, + "highlights": [ + { + "fragment": "Suggestion system / Suggestions WebServices / The context", + "word": "context" + }, + { + "fragment": "The context", + "word": "context" + } + ] + } + ] +} +""" + + @level_2_technical_details @nominal_case @valid + Scenario: get result of a search with multiple results + Given we have those directories in the database + | id | name | label | description | order | relativePath | path | branchId | + | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | + | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | + | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | + | 4 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 2 | + And we have those pages in the database + | id | name | label | description | order | relativePath | path | markdown | directoryId | + | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | + | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | + | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | + | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | + | 5 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>feature/654-simple-full-text-search-on-pages>/context | **Feature**: Provide book suggestions context | 4 | + And the lucene index is loaded from the database + When I perform a "GET" on following URL "/api/pages/search?keyword=suggestions" + Then I get a response with status "200" + Then I get the following json response body + """ +{ + "items": [ + { + "page": { + "id": "/suggestion/suggestionsWS>master>/suggestions/suggestion", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/suggestions/suggestion", + "breadcrumb": "Suggestion system / Suggestions WebServices / suggestions / The suggestions", + "project": "Suggestions WebServices", + "branch": "master", + "label": "The suggestions", + "description": "The suggestions...", + "pageContent": "**What's a suggestion ?**" + }, + "highlights": [ + { + "fragment": "Suggestion system / Suggestions WebServices / suggestions / The suggestions", + "word": "Suggestions" + }, + { + "fragment": "The suggestions", + "word": "suggestions" + }, + { + "fragment": "The suggestions...", + "word": "suggestions" + } + ] + }, + { + "page": { + "id": "/suggestion/suggestionsWS>master>/context", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/context", + "breadcrumb": "Suggestion system / Suggestions WebServices / The context", + "project": "Suggestions WebServices", + "branch": "master", + "label": "The context", + "description": "Why providing suggestions", + "pageContent": "**Feature**: Provide book suggestions" + }, + "highlights": [ + { + "fragment": "Suggestion system / Suggestions WebServices / The context", + "word": "Suggestions" + }, + { + "fragment": "Why providing suggestions", + "word": "suggestions" + }, + { + "fragment": "**Feature**: Provide book suggestions", + "word": "suggestions" + } + ] + }, + { + "page": { + "id": "/suggestion/suggestionsWS>master>/suggestions/examples", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/suggestions/examples", + "breadcrumb": "Suggestion system / Suggestions WebServices / suggestions / examples", + "project": "Suggestions WebServices", + "branch": "master", + "label": "examples", + "description": "Some examples", + "pageContent": "**Some suggestion examples**" + }, + "highlights": [ + { + "fragment": "Suggestion system / Suggestions WebServices / suggestions / examples", + "word": "Suggestions" + } + ] + }, + { + "page": { + "id": "/suggestion/suggestionsWS>master>/admin/admin", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/admin/admin", + "breadcrumb": "Suggestion system / Suggestions WebServices / admin / admin", + "project": "Suggestions WebServices", + "branch": "master", + "label": "admin", + "description": "admin", + "pageContent": "**Page for the admin users**" + }, + "highlights": [ + { + "fragment": "Suggestion system / Suggestions WebServices / admin / admin", + "word": "Suggestions" + } + ] + } + ] +} +""" + + @level_2_technical_details @nominal_case @valid + Scenario: only pages from stable branches are indexed in the search + Given we have those directories in the database + | id | name | label | description | order | relativePath | path | branchId | + | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | + | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | + | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | + | 4 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 2 | + And we have those pages in the database + | id | name | label | description | order | relativePath | path | markdown | directoryId | + | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | + | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | + | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | + | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | + | 5 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>feature/654-simple-full-text-search-on-pages>/context | **Feature**: Provide book suggestions context | 4 | + And the lucene index is loaded from the database + When I perform a "GET" on following URL "/api/pages/search?keyword=context" + Then I get a response with status "200" + Then I get the following json response body + """ +{ + "items": [ + { + "page": { + "id": "/suggestion/suggestionsWS>master>/context", + "hierarchy": "/suggestion", + "path": "suggestionsWS>master>/context", + "breadcrumb": "Suggestion system / Suggestions WebServices / The context", + "project": "Suggestions WebServices", + "branch": "master", + "label": "The context", + "description": "Why providing suggestions", + "pageContent": "**Feature**: Provide book suggestions" + }, + "highlights": [ + { + "fragment": "Suggestion system / Suggestions WebServices / The context", + "word": "context" + }, + { + "fragment": "The context", + "word": "context" + } + ] + } + ] +} +""" + + @level_2_technical_details @nominal_case @valid @ongoing + Scenario: get result of a search with pages only attached to the hierarchy by the node.directoryPath + Given we have those directories in the database + | id | name | label | description | order | relativePath | path | branchId | + | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | + | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | + | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | + | 4 | libraryDoc | libraryDoc | library system group documentation | 0 | / | libraryDoc>master>/ | 3 | + And we have those pages in the database + | id | name | label | description | order | relativePath | path | markdown | directoryId | + | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | + | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | + | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | + | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | + | 5 | libDocOverview | Overview library | Overview library documentation | 0 | /overview | libraryDoc>master>/overview | **Documentation on the library** | 4 | + And the lucene index is loaded from the database + And I perform a "GET" on following URL "/api/pages/search?keyword=library" + Then I get a response with status "200" + Then I get the following json response body + """ +{ + "items": [ + { + "page": { + "id": "/lib/libraryDoc>master>/overview", + "hierarchy": "/lib", + "path": "libraryDoc>master>/overview", + "breadcrumb": "Library / library system group documentation / Overview library", + "project": "library system group documentation", + "branch": "master", + "label": "Overview library", + "description": "Overview library documentation", + "pageContent": "**Documentation on the library**" + }, + "highlights": [ + { + "fragment": "Library / library system group documentation / Overview library", + "word": "Library" + }, + { + "fragment": "Overview library", + "word": "library" + }, + { + "fragment": "Overview library documentation", + "word": "library" + }, + { + "fragment": "**Documentation on the library**", + "word": "library" + } + ] + } + ] +} + """ + diff --git a/test/features/search/search_through_name_pages.feature b/test/features/search/search_through_name_pages.feature deleted file mode 100644 index b890b26fa..000000000 --- a/test/features/search/search_through_name_pages.feature +++ /dev/null @@ -1,142 +0,0 @@ -Feature: search through pages by name - As a user, - I want to be able to search a page by its name to find more easily what i need - - Background: - Given the database is empty - And the cache is empty - And No project is checkout - And the remote projects are empty - And the hierarchy nodes are - | id | slugName | name | childrenLabel | childLabel | - | . | root | Hierarchy root | Views | View | - | .01. | suggestion | Suggestion system | Projects | Project | - And we have the following projects - | id | name | repositoryUrl | stableBranch | featuresRootPath | documentationRootPath | - | suggestionsWS | Suggestions WebServices | target/remote/data/GetFeatures/library/suggestionsWS/ | master | test/features | doc | - And the links between hierarchy nodes are - | projectId | hierarchyId | - | suggestionsWS | .01. | - And we have those branches in the database - | id | name | isStable | projectId | - | 1 | master | true | suggestionsWS | - | 2 | feature/654-simple-full-text-search-on-pages | false | suggestionsWS | - - @level_2_technical_details @nominal_case @valid - Scenario: get result of a search - Given we have those directories in the database - | id | name | label | description | order | relativePath | path | branchId | - | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | - | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | - | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | - And we have those pages in the database - | id | name | label | description | order | relativePath | path | markdown | directoryId | - | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | - | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | - | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | - | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | - And the lucene index is loaded from the database - And I perform a "GET" on following URL "/api/pages/search?keywords=context" - Then I get the following json response body - """ -[ - { - "hierarchy":"/Suggestion system/master/context", - "path":"suggestionsWS>master>/context", - "branch":"master", - "label":"The context", - "description":"Why providing suggestions", - "pageContent":"**Feature**: Provide book suggestions" - } -] - - """ - - @level_2_technical_details @nominal_case @valid - Scenario: get result of a search with multiple results - Given we have those directories in the database - | id | name | label | description | order | relativePath | path | branchId | - | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | - | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | - | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | - | 4 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 2 | - And we have those pages in the database - | id | name | label | description | order | relativePath | path | markdown | directoryId | - | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | - | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | - | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | - | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | - | 5 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>feature/654-simple-full-text-search-on-pages>/context | **Feature**: Provide book suggestions context | 4 | - And the lucene index is loaded from the database - When I perform a "GET" on following URL "/api/pages/search?keywords=context" - Then I get the following json response body - """ -[ - { - "hierarchy":"/Suggestion system/feature/654-simple-full-text-search-on-pages/context", - "path":"suggestionsWS>feature/654-simple-full-text-search-on-pages>/context", - "branch":"feature/654-simple-full-text-search-on-pages", - "label":"The context", - "description":"Why providing suggestions", - "pageContent":"**Feature**: Provide book suggestions context" - }, - { - "hierarchy":"/Suggestion system/master/context", - "path":"suggestionsWS>master>/context", - "branch":"master", - "label":"The context", - "description":"Why providing suggestions", - "pageContent":"**Feature**: Provide book suggestions" - } -] -""" - - @level_2_technical_details @nominal_case @valid - Scenario: get result of a search with multiple results - Given we have those directories in the database - | id | name | label | description | order | relativePath | path | branchId | - | 1 | root | SuggestionsWS | Suggestions WebServices | 0 | / | suggestionsWS>master>/ | 1 | - | 2 | suggestions | Suggestions | Suggestions... | 0 | /suggestions/ | suggestionsWS>master>/suggestions/ | 1 | - | 3 | admin | Admin | Administration... | 1 | /admin/ | suggestionsWS>master>/admin/ | 1 | - And we have those pages in the database - | id | name | label | description | order | relativePath | path | markdown | directoryId | - | 1 | context | The context | Why providing suggestions | 0 | /context | suggestionsWS>master>/context | **Feature**: Provide book suggestions | 1 | - | 2 | suggestion | The suggestions | The suggestions... | 0 | /suggestions/suggestion | suggestionsWS>master>/suggestions/suggestion | **What's a suggestion ?** | 2 | - | 3 | examples | examples | Some examples | 1 | /suggestions/examples | suggestionsWS>master>/suggestions/examples | **Some suggestion examples** | 2 | - | 4 | admin | admin | admin | 0 | /admin/admin | suggestionsWS>master>/admin/admin | **Page for the admin users** | 3 | - And the lucene index is loaded from the database - When I perform a "GET" on following URL "/api/pages/search?keywords=suggestion" - Then I get the following json response body - """ -[ - { - "hierarchy":"/Suggestion system/master/suggestions/suggestion", - "path":"suggestionsWS>master>/suggestions/suggestion", - "branch":"master", - "label":"The suggestions", - "description":"The suggestions...", - "pageContent":"**What's a suggestion ?**" - }, - { - "hierarchy":"/Suggestion system/master/context", - "path":"suggestionsWS>master>/context", - "branch":"master", - "label":"The context", - "description":"Why providing suggestions", - "pageContent":"**Feature**: Provide book suggestions" - }, - { - "hierarchy":"/Suggestion system/master/suggestions/examples", - "path":"suggestionsWS>master>/suggestions/examples", - "branch":"master", - "label":"examples", - "description":"Some examples", - "pageContent":"**Some suggestion examples**" - } -] -""" - - - - - diff --git a/test/services/SearchServiceTest.scala b/test/services/SearchServiceTest.scala index 5a40eadfd..d4e281a47 100644 --- a/test/services/SearchServiceTest.scala +++ b/test/services/SearchServiceTest.scala @@ -14,31 +14,67 @@ class SearchServiceTest extends WordSpec with MustMatchers with BeforeAndAfter w val pageIndex = new IndexService() val searchService = new SearchService(pageIndex) - val pageIndex1 = PageIndexDocument("hierarchy1", "path1", "branch1", "Doeco", "this is a test for markdown", "") - val pageIndex2 = PageIndexDocument("hierarchy2", "path2", "branch2", "Superstore", "", "") - val pageIndex3 = PageIndexDocument("hierarchy3", "path3", "branch3", "Doe co", "this is a text for markdown doe", "") - val pageIndex4 = PageIndexDocument("hierarchy4", "path4", "branch4", "co", "doe", "") - val pageIndex5 = PageIndexDocument("hierarchy5", "path5", "branch5", "Buymore", "this is a test for markdown this is a text for markdown", "") - val pageIndex6 = PageIndexDocument("hierarchy6", "path6", "branch6", "Do-Lots-Co", "", "") + val pageIndex1 = PageIndexDocument("id1", "hierarchy1", "path1", "breadcrum1", "project1", "branch1", "Doeco", "this is a test for markdown", "") + val pageIndex2 = PageIndexDocument("id2", "hierarchy2", "path2", "breadcrum2", "project2", "branch2", "Superstore", "page two", "") + val pageIndex3 = PageIndexDocument("id3", "hierarchy3", "path3", "breadcrum3", "project3", "branch3", "Doe co", "this is a text for markdown doe", "") + val pageIndex4 = PageIndexDocument("id4", "hierarchy4", "path4", "breadcrum4", "project4", "branch4", "co", "doe", "") + val pageIndex5 = PageIndexDocument("id5", "hierarchy5", "path5", "breadcrum5", "project5", "branch5", "Buymore", "this is a test for markdown this is a text for markdown", "") + val pageIndex6 = PageIndexDocument("id6", "hierarchy6", "path6", "breadcrum6", "project6", "branch6", "Do-Lots-Co", "", "") before { Mockito.reset(pageRepository) - pageIndex.addDocument(PageIndexDocument("hierarchy1", "path1", "branch1", "Doeco", "this is a test for markdown", "")) - pageIndex.addDocument(PageIndexDocument("hierarchy2", "path2", "branch2", "Superstore", "", "")) - pageIndex.addDocument(PageIndexDocument("hierarchy3", "path3", "branch3", "Doe co", "this is a text for markdown doe", "")) - pageIndex.addDocument(PageIndexDocument("hierarchy4", "path4", "branch4", "co", "doe", "")) - pageIndex.addDocument(PageIndexDocument("hierarchy5", "path5", "branch5", "Buymore", "this is a test for markdown this is a text for markdown", "")) - pageIndex.addDocument(PageIndexDocument("hierarchy6", "path6", "branch6", "Do-Lots-Co", "", "")) + pageIndex.reset() + pageIndex.insertOrUpdateDocument(PageIndexDocument("id1", "hierarchy1", "path1", "breadcrum1", "project1", "branch1", "Doeco", "this is a test for markdown", "")) + pageIndex.insertOrUpdateDocument(PageIndexDocument("id2", "hierarchy2", "path2", "breadcrum2", "project2", "branch2", "Superstore", "page two", "")) + pageIndex.insertOrUpdateDocument(PageIndexDocument("id3", "hierarchy3", "path3", "breadcrum3", "project3", "branch3", "Doe co", "this is a text for markdown doe", "")) + pageIndex.insertOrUpdateDocument(PageIndexDocument("id4", "hierarchy4", "path4", "breadcrum4", "project4", "branch4", "co", "doe", "")) + pageIndex.insertOrUpdateDocument(PageIndexDocument("id5", "hierarchy5", "path5", "breadcrum5", "project5", "branch5", "Buymore", "this is a test for markdown this is a text for markdown", "")) + pageIndex.insertOrUpdateDocument(PageIndexDocument("id6", "hierarchy6", "path6", "breadcrum6", "project6", "branch6", "Do-Lots-Co", "", "")) } "PageService" should { "search in lucene" in { - searchService.searchForPage("branch test") must contain theSameElementsAs Seq(pageIndex1, pageIndex5) - searchService.searchForPage("branch1") must contain theSameElementsAs Seq(pageIndex1) - searchService.searchForPage(" text") must contain theSameElementsAs Seq(pageIndex3, pageIndex5) - searchService.searchForPage("doe") must contain theSameElementsAs Seq(pageIndex3, pageIndex1, pageIndex4) + var items = searchService.searchForPage("branch test").items + items.length must equal(2) + items(0).page.id must equal("id1") + items(1).page.id must equal("id5") + + items = searchService.searchForPage("branch1").items + items.length must equal(1) + items(0).page.id must equal("id1") + + items = searchService.searchForPage(" text").items + items.length must equal(2) + items(0).page.id must equal("id3") + items(1).page.id must equal("id5") + + items = searchService.searchForPage("doe").items + items.length must equal(3) + items(0).page.id must equal("id3") + items(1).page.id must equal("id1") + items(2).page.id must equal("id4") + } + + + "insert or update in lucene" in { + var result = searchService.searchForPage("Superstore").items + + result.size must equal(1) + result.head.page.id must equal("id2") + result.head.page.description must equal("page two") + + + for(pageNumber <- 1 to 10){ + pageIndex.insertOrUpdateDocument(PageIndexDocument("id2", "hierarchy2", "path2", "breadcrum2", "branch2", "project2", "Superstore", s"page ${pageNumber}", "")) + } + + result = searchService.searchForPage("Superstore").items + + result.size must equal(1) + result.head.page.id must equal("id2") + result.head.page.description must equal(s"page 10") } diff --git a/test/steps/CommonSteps.scala b/test/steps/CommonSteps.scala index 1bb9234dc..03c5405e2 100644 --- a/test/steps/CommonSteps.scala +++ b/test/steps/CommonSteps.scala @@ -175,7 +175,7 @@ case class Configuration(path: String, value: String) case class PageRow(id: Long, name: String, label: String, description: String, order: Int, markdown: String, relativePath: String, path: String, directoryId: Long, dependOnOpenApi: Boolean) -case class LuceneDoc(hierarchy: String, path: String, branch: String, label: String, description: String, pageContent: String) +case class LuceneDoc(id: String, hierarchy: String, path: String, breadcrumb: String, project: String, branch: String, label: String, description: String, pageContent: String) class CommonSteps extends ScalaDsl with EN with MockitoSugar with Logging with JacksonDefaultDataTableEntryTransformer { @@ -301,7 +301,7 @@ Scenario: providing several book suggestions Given("""^we have the following document in the lucene index$""") { docs: util.List[LuceneDoc] => docs.asScala.map(doc => - pageIndex.addDocument(PageIndexDocument(doc.hierarchy, doc.path, doc.branch, doc.label, doc.description, doc.pageContent)) + pageIndex.insertOrUpdateDocument(PageIndexDocument(doc.id, doc.hierarchy, doc.path, doc.breadcrumb, doc.project, doc.branch, doc.label, doc.description, doc.pageContent)) ) } From 02fe0895b54ead1e2722040a54412c12122ba46a Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Tue, 1 Sep 2020 16:43:05 +0000 Subject: [PATCH 04/15] [ci-skip]prepare release theGardener-1.4.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index af9483bd0..56e67a1e5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.3.3-SNAPSHOT + 1.4.0 kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - HEAD + theGardener-1.4.0 From 0d921b96e537ce2acd6143029071c9d6af103afc Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Tue, 1 Sep 2020 16:43:07 +0000 Subject: [PATCH 05/15] [ci-skip]prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 56e67a1e5..ae5e5abbd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.4.0 + 1.4.1-SNAPSHOT kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - theGardener-1.4.0 + HEAD From 73d1d888b6da44008362208b7526ff8f02d3ed3e Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Thu, 3 Sep 2020 18:35:17 +0200 Subject: [PATCH 06/15] refactoring lucene --- app/services/PageService.scala | 2 +- app/services/SearchService.scala | 104 ++++++++++-------- frontend/package-lock.json | 11 +- .../page/header/header.component.ts | 16 +-- 4 files changed, 78 insertions(+), 55 deletions(-) diff --git a/app/services/PageService.scala b/app/services/PageService.scala index 3c5cfb036..9b89ea0a9 100644 --- a/app/services/PageService.scala +++ b/app/services/PageService.scala @@ -277,7 +277,7 @@ class PageService @Inject()(config: Configuration, projectRepository: ProjectRep private def insertOrUpdateIndex(pageJoinProject: PageJoinProject) = { - hierarchyService.getHierarchyPath(pageJoinProject).map{ hierarchy => + hierarchyService.getHierarchyPath(pageJoinProject).map { hierarchy => val document = PageIndexDocument(id = hierarchy + "/" + pageJoinProject.page.path, hierarchy = hierarchy, path = pageJoinProject.page.path, diff --git a/app/services/SearchService.scala b/app/services/SearchService.scala index d0fa535cb..3b0ca37e7 100644 --- a/app/services/SearchService.scala +++ b/app/services/SearchService.scala @@ -1,7 +1,9 @@ package services -import com.outr.lucene4s.query.{Sort, TermSearchTerm} -import com.outr.lucene4s.{DirectLucene, parse} +import com.outr.lucene4s.field.Field +import com.outr.lucene4s.mapper.Searchable +import com.outr.lucene4s.query.{SearchTerm, Sort} +import com.outr.lucene4s.{DirectLucene, parse, _} import javax.inject.{Inject, Singleton} import play.api.libs.json.Json @@ -13,6 +15,12 @@ case class PageIndexDocument(id: String, hierarchy: String, path: String, breadc case class HighlightedFragment(fragment: String, word: String) +case class PageIndexLucene(id: Id[PageIndexLucene], hierarchy: String, path: String, breadcrumb: String, project: String, branch: String, label: String, description: String, pageContent: String) + +case class Id[T](value: String) { + override def toString: String = value +} + object HighlightedFragment { implicit val format = Json.format[HighlightedFragment] } @@ -30,49 +38,51 @@ object SearchResult { } -@Singleton -class IndexService { +trait SearchablePageIndexLucene extends Searchable[PageIndexLucene] { + // We must implement the criteria for updating and deleting + override def idSearchTerms(t: PageIndexLucene): List[SearchTerm] = List(exact(id(t.id))) - val luceneSearchIndex = new DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = true) + implicit def stringifyId[T]: Stringify[Id[T]] = Stringify[Id[T]]((s: String) => Id[T](s)) - private val id = luceneSearchIndex.create.field[String]("id") - private val hierarchy = luceneSearchIndex.create.field[String]("hierarchy") - private val path = luceneSearchIndex.create.field[String]("path", fullTextSearchable = false) - private val breadcrumb = luceneSearchIndex.create.field[String]("breadcrumb") - private val project = luceneSearchIndex.create.field[String]("project") - private val branch = luceneSearchIndex.create.field[String]("branch") - private val label = luceneSearchIndex.create.field[String]("label") - private val description = luceneSearchIndex.create.field[String]("description") - private val pageContent = luceneSearchIndex.create.field[String]("pageContent", sortable = false) + // We can create custom / explicit configurations for each field + val id: Field[Id[PageIndexLucene]] = lucene.create.stringifiedField[Id[PageIndexLucene]]("id", fullTextSearchable = false) - def insertOrUpdateDocument(document: PageIndexDocument): Unit = { + def hierarchy: Field[String] - var i = 0 - while (exists(document) && i < 10) { - luceneSearchIndex.delete(new TermSearchTerm(Some(id), document.id)) - luceneSearchIndex.commit() - i = i + 1 - } - - luceneSearchIndex.doc().fields(id(document.id), - hierarchy(document.hierarchy), - path(document.path), - breadcrumb(document.breadcrumb), - project(document.project), - branch(document.branch), - label(document.label), - description(document.description), - pageContent(document.pageContent) - ).index() - luceneSearchIndex.commit() - } + def path: Field[String] + + def breadcrumb: Field[String] + + def project: Field[String] + + def branch: Field[String] + + def label: Field[String] + + def description: Field[String] - private def exists(document: PageIndexDocument) = { - luceneSearchIndex.query().filter(new TermSearchTerm(Some(id), document.id)).search().results.length > 0 + def pageContent: Field[String] + +} + + +object lucene extends DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = true) { + val page: SearchablePageIndexLucene = create.searchable[SearchablePageIndexLucene] +} + + +@Singleton +class IndexService { + + def insertOrUpdateDocument(document: PageIndexDocument): Unit = { + val docLucene = PageIndexLucene(Id[PageIndexLucene](document.id), document.hierarchy, document.path, document.breadcrumb, document.project, document.branch, document.label, document.description, document.pageContent) + lucene.page.delete(docLucene) + lucene.page.insert(docLucene) + () } def query(keywords: String): SearchResult = { - val results = luceneSearchIndex.query().sort(Sort.Score).filter(parse("id:" + keywords + "*^20 " + " | " + val results = lucene.page.query().sort(Sort.Score).filter(parse("id:" + keywords + "*^20 " + " | " + "breadcrumb:" + keywords + "*^50 " + " | " + "project:" + keywords + "*^30 " + " | " + "branch:" + keywords + "*^30" + " | " @@ -82,16 +92,24 @@ class IndexService { )).highlight().limit(50).search() SearchResult(results.results.map { result => - val highlighting = result.highlighting(breadcrumb).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: - result.highlighting(label).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: - result.highlighting(description).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: - result.highlighting(pageContent).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: Nil - SearchResultItem(PageIndexDocument(result(id), result(hierarchy), result(path), result(breadcrumb), result(project), result(branch), result(label), result(description), result(pageContent)), highlighting.flatten) + val highlighting = result.highlighting(lucene.page.breadcrumb).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(lucene.page.label).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(lucene.page.description).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(lucene.page.pageContent).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: Nil + SearchResultItem(PageIndexDocument(result(lucene.page.id).value, + result(lucene.page.hierarchy), + result(lucene.page.path), + result(lucene.page.breadcrumb), + result(lucene.page.project), + result(lucene.page.branch), + result(lucene.page.label), + result(lucene.page.description), + result(lucene.page.pageContent)), highlighting.flatten) }) } def reset(): Unit = { - luceneSearchIndex.deleteAll() + lucene.deleteAll() } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd447c7fb..faa5a2ba8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5556,12 +5556,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5580,6 +5582,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5759,7 +5762,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5865,7 +5869,8 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } } diff --git a/frontend/src/app/_components/page/header/header.component.ts b/frontend/src/app/_components/page/header/header.component.ts index b79f3335f..23729e478 100644 --- a/frontend/src/app/_components/page/header/header.component.ts +++ b/frontend/src/app/_components/page/header/header.component.ts @@ -45,18 +45,18 @@ export class HeaderComponent implements OnInit { }); } - getCurrentRoute(): NavigationRoute{ - let params : NavigationParams; - if (this.activatedRoute.firstChild && this.activatedRoute.firstChild.snapshot){ + getCurrentRoute(): NavigationRoute { + let params: NavigationParams; + if (this.activatedRoute.firstChild && this.activatedRoute.firstChild.snapshot) { params = this.activatedRoute.firstChild.snapshot.params; - if (this.activatedRoute.firstChild.snapshot.url){ - this.url = this.activatedRoute.firstChild.snapshot.url.join('/'); + if (this.activatedRoute.firstChild.snapshot.url) { + this.url = this.activatedRoute.firstChild.snapshot.url.join('/'); } } - if (!params && this.activatedRoute && this.activatedRoute.snapshot){ + if (!params && this.activatedRoute && this.activatedRoute.snapshot) { params = this.activatedRoute.snapshot.params; - if (this.activatedRoute.firstChild.snapshot.url){ - this.url = this.activatedRoute.firstChild.snapshot.url.join('/'); + if (this.activatedRoute.snapshot.url) { + this.url = this.activatedRoute.snapshot.url.join('/'); } } return this.routeService.navigationParamsToNavigationRoute(params); From 4efd65e3dcdb44b2c8c6b0cd69d43b9685e4baec Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Thu, 3 Sep 2020 18:54:08 +0200 Subject: [PATCH 07/15] rollback refactoring lucene --- app/services/SearchService.scala | 105 +++++++++++++------------------ 1 file changed, 44 insertions(+), 61 deletions(-) diff --git a/app/services/SearchService.scala b/app/services/SearchService.scala index 3b0ca37e7..f55231035 100644 --- a/app/services/SearchService.scala +++ b/app/services/SearchService.scala @@ -1,9 +1,7 @@ package services -import com.outr.lucene4s.field.Field -import com.outr.lucene4s.mapper.Searchable -import com.outr.lucene4s.query.{SearchTerm, Sort} -import com.outr.lucene4s.{DirectLucene, parse, _} +import com.outr.lucene4s.query.{Sort, TermSearchTerm} +import com.outr.lucene4s.{DirectLucene, parse} import javax.inject.{Inject, Singleton} import play.api.libs.json.Json @@ -15,12 +13,6 @@ case class PageIndexDocument(id: String, hierarchy: String, path: String, breadc case class HighlightedFragment(fragment: String, word: String) -case class PageIndexLucene(id: Id[PageIndexLucene], hierarchy: String, path: String, breadcrumb: String, project: String, branch: String, label: String, description: String, pageContent: String) - -case class Id[T](value: String) { - override def toString: String = value -} - object HighlightedFragment { implicit val format = Json.format[HighlightedFragment] } @@ -38,51 +30,50 @@ object SearchResult { } -trait SearchablePageIndexLucene extends Searchable[PageIndexLucene] { - // We must implement the criteria for updating and deleting - override def idSearchTerms(t: PageIndexLucene): List[SearchTerm] = List(exact(id(t.id))) - - implicit def stringifyId[T]: Stringify[Id[T]] = Stringify[Id[T]]((s: String) => Id[T](s)) - - // We can create custom / explicit configurations for each field - val id: Field[Id[PageIndexLucene]] = lucene.create.stringifiedField[Id[PageIndexLucene]]("id", fullTextSearchable = false) - - def hierarchy: Field[String] - - def path: Field[String] - - def breadcrumb: Field[String] - - def project: Field[String] - - def branch: Field[String] - - def label: Field[String] - - def description: Field[String] - - def pageContent: Field[String] - -} - - -object lucene extends DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = true) { - val page: SearchablePageIndexLucene = create.searchable[SearchablePageIndexLucene] -} - - @Singleton class IndexService { + val luceneSearchIndex = new DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = true) + + private val id = luceneSearchIndex.create.field[String]("id") + private val hierarchy = luceneSearchIndex.create.field[String]("hierarchy") + private val path = luceneSearchIndex.create.field[String]("path", fullTextSearchable = false) + private val breadcrumb = luceneSearchIndex.create.field[String]("breadcrumb") + private val project = luceneSearchIndex.create.field[String]("project") + private val branch = luceneSearchIndex.create.field[String]("branch") + private val label = luceneSearchIndex.create.field[String]("label") + private val description = luceneSearchIndex.create.field[String]("description") + private val pageContent = luceneSearchIndex.create.field[String]("pageContent", sortable = false) + def insertOrUpdateDocument(document: PageIndexDocument): Unit = { - val docLucene = PageIndexLucene(Id[PageIndexLucene](document.id), document.hierarchy, document.path, document.breadcrumb, document.project, document.branch, document.label, document.description, document.pageContent) - lucene.page.delete(docLucene) - lucene.page.insert(docLucene) + + var i = 0 + while (exists(document) && i < 10) { + luceneSearchIndex.delete(new TermSearchTerm(Some(id), document.id)) + luceneSearchIndex.commit() + i = i + 1 + } + + luceneSearchIndex.doc().fields(id(document.id), + hierarchy(document.hierarchy), + path(document.path), + breadcrumb(document.breadcrumb), + project(document.project), + branch(document.branch), + label(document.label), + description(document.description), + pageContent(document.pageContent) + ).index() + luceneSearchIndex.commit() () } + private def exists(document: PageIndexDocument) = { + luceneSearchIndex.query().filter(new TermSearchTerm(Some(id), document.id)).search().results.length > 0 + } + def query(keywords: String): SearchResult = { - val results = lucene.page.query().sort(Sort.Score).filter(parse("id:" + keywords + "*^20 " + " | " + val results = luceneSearchIndex.query().sort(Sort.Score).filter(parse("id:" + keywords + "*^20 " + " | " + "breadcrumb:" + keywords + "*^50 " + " | " + "project:" + keywords + "*^30 " + " | " + "branch:" + keywords + "*^30" + " | " @@ -92,24 +83,16 @@ class IndexService { )).highlight().limit(50).search() SearchResult(results.results.map { result => - val highlighting = result.highlighting(lucene.page.breadcrumb).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: - result.highlighting(lucene.page.label).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: - result.highlighting(lucene.page.description).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: - result.highlighting(lucene.page.pageContent).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: Nil - SearchResultItem(PageIndexDocument(result(lucene.page.id).value, - result(lucene.page.hierarchy), - result(lucene.page.path), - result(lucene.page.breadcrumb), - result(lucene.page.project), - result(lucene.page.branch), - result(lucene.page.label), - result(lucene.page.description), - result(lucene.page.pageContent)), highlighting.flatten) + val highlighting = result.highlighting(breadcrumb).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(label).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(description).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: + result.highlighting(pageContent).map(frg => HighlightedFragment(frg.fragment, frg.word)) +: Nil + SearchResultItem(PageIndexDocument(result(id), result(hierarchy), result(path), result(breadcrumb), result(project), result(branch), result(label), result(description), result(pageContent)), highlighting.flatten) }) } def reset(): Unit = { - lucene.deleteAll() + luceneSearchIndex.deleteAll() } } From cc7d74d954ac64caf22345c683242d0ae4831d63 Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Thu, 3 Sep 2020 21:39:17 +0200 Subject: [PATCH 08/15] reset index every batch --- app/services/ProjectService.scala | 3 ++- app/services/SearchService.scala | 2 +- local-conf/application.conf | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/services/ProjectService.scala b/app/services/ProjectService.scala index 05ff2a73d..f954bc6ef 100644 --- a/app/services/ProjectService.scala +++ b/app/services/ProjectService.scala @@ -17,7 +17,7 @@ import scala.concurrent.duration._ @Singleton class ProjectService @Inject()(projectRepository: ProjectRepository, gitService: GitService, featureService: FeatureService, featureRepository: FeatureRepository, branchRepository: BranchRepository, directoryRepository: DirectoryRepository, - pageRepository: PageRepository, menuService: MenuService, pageService: PageService, + pageRepository: PageRepository, menuService: MenuService, pageService: PageService, indexService: IndexService, config: Configuration, environment: Environment, actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends Logging { val projectsRootDirectory = config.get[String]("projects.root.directory") val synchronizeInterval = config.get[Int]("projects.synchronize.interval") @@ -288,6 +288,7 @@ class ProjectService @Inject()(projectRepository: ProjectRepository, gitService: def refreshAllPagesFromAllProjects(): Unit = { val startTime = System.currentTimeMillis() logger.info("Start refreshing pages not in cache") + indexService.reset() projectRepository.findAll().foreach(p => refreshAllPages(p, forceRefresh = false)) val endTime = System.currentTimeMillis() val duration = DurationUtil.durationFromMillisToHumanReadable( endTime-startTime ) diff --git a/app/services/SearchService.scala b/app/services/SearchService.scala index f55231035..d453a4e95 100644 --- a/app/services/SearchService.scala +++ b/app/services/SearchService.scala @@ -33,7 +33,7 @@ object SearchResult { @Singleton class IndexService { - val luceneSearchIndex = new DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = true) + val luceneSearchIndex = new DirectLucene(uniqueFields = List("id"), defaultFullTextSearchable = true, appendIfExists = false, autoCommit = false) private val id = luceneSearchIndex.create.field[String]("id") private val hierarchy = luceneSearchIndex.create.field[String]("hierarchy") diff --git a/local-conf/application.conf b/local-conf/application.conf index 4b1c135b4..4db021e44 100644 --- a/local-conf/application.conf +++ b/local-conf/application.conf @@ -78,7 +78,7 @@ play.evolutions.autoApply = true # db.default.jndiName=DefaultDS projects.root.directory = "target/data/git/" -projects.synchronize.interval = 1800 +projects.synchronize.interval = 600 projects.synchronize.initial.delay = 5 projects.synchronize.from.remote.enabled = true From 48bc4e5d8acc0dc2939350dc8d9f7cd457bad4d7 Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Thu, 3 Sep 2020 22:04:37 +0200 Subject: [PATCH 09/15] reset index every batch --- frontend/package-lock.json | 5 +---- test/services/ProjectServiceTest.scala | 3 ++- test/steps/CommonSteps.scala | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index faa5a2ba8..b55d5eef0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5556,14 +5556,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5582,7 +5580,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } diff --git a/test/services/ProjectServiceTest.scala b/test/services/ProjectServiceTest.scala index b11f2d968..e0967396b 100644 --- a/test/services/ProjectServiceTest.scala +++ b/test/services/ProjectServiceTest.scala @@ -38,10 +38,11 @@ class ProjectServiceTest extends WordSpec with MustMatchers with BeforeAndAfter val featureService = mock[FeatureService] val menuService = mock[MenuService] val pageService = mock[PageService] + val indexService = mock[IndexService] val environment = mock[Environment] - val projectService = new ProjectService(projectRepository, gitService, featureService, featureRepository, branchRepository, directoryRepository, pageRepository, menuService, pageService, Configuration.load(Environment.simple()), environment, ActorSystem()) + val projectService = new ProjectService(projectRepository, gitService, featureService, featureRepository, branchRepository, directoryRepository, pageRepository, menuService, pageService, indexService, Configuration.load(Environment.simple()), environment, ActorSystem()) val project = Project("suggestionsWS", "Suggestions WebServices", "git@github.com:library/suggestionsWS.git", Some("http://github.com:library/suggestionsWS/blob/${branch}/${path}"), "master", Some("^(^master$)|(^feature\\/.*$)"), Some("test/features")) val masterDirectory = projectService.getLocalRepository(project.id, project.stableBranch) diff --git a/test/steps/CommonSteps.scala b/test/steps/CommonSteps.scala index 03c5405e2..8a2c05ab3 100644 --- a/test/steps/CommonSteps.scala +++ b/test/steps/CommonSteps.scala @@ -113,7 +113,7 @@ object CommonSteps extends MockitoSugar with MustMatchers { val spyMenuService = spy(menuService) val replicaService = new ReplicaClient(conf, wsClient) val spyReplicaService = spy(replicaService) - val projectService = new ProjectService(projectRepository, gitService, featureService, featureRepository, branchRepository, directoryRepository, pageRepository, menuService, pageService, conf, environment, actorSystem) + val projectService = new ProjectService(projectRepository, gitService, featureService, featureRepository, branchRepository, directoryRepository, pageRepository, menuService, pageService, pageIndex, conf, environment, actorSystem) val spyProjectService = spy(projectService) From d6dc38d4d813c668171d4f97146fe17f2205c6ba Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Thu, 3 Sep 2020 20:28:39 +0000 Subject: [PATCH 10/15] [ci-skip]prepare release theGardener-1.4.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ae5e5abbd..af5127df0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.4.1-SNAPSHOT + 1.4.1 kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - HEAD + theGardener-1.4.1 From 0ca3e09e415160e2fde74c51b53fb52c678c3656 Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Thu, 3 Sep 2020 20:28:42 +0000 Subject: [PATCH 11/15] [ci-skip]prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index af5127df0..954f50ffa 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.4.1 + 1.4.2-SNAPSHOT kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - theGardener-1.4.1 + HEAD From f42b262a2e8bffeb7bcb188577ee3fa362c78841 Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Fri, 11 Sep 2020 14:12:39 +0200 Subject: [PATCH 12/15] fix relative links --- frontend/package-lock.json | 5 ++++- frontend/src/app/_services/route.service.spec.ts | 6 ++++-- frontend/src/app/_services/route.service.ts | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b55d5eef0..faa5a2ba8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5556,12 +5556,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5580,6 +5582,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } diff --git a/frontend/src/app/_services/route.service.spec.ts b/frontend/src/app/_services/route.service.spec.ts index 1f723b50f..812deef8a 100644 --- a/frontend/src/app/_services/route.service.spec.ts +++ b/frontend/src/app/_services/route.service.spec.ts @@ -159,10 +159,12 @@ describe('RouteService', () => { }); it('relativeUrlToFullFrontEndUrl ', () => { - + expect(service.relativeUrlToFullFrontEndUrl('Public/Features/OfferFeeds/Feature',{nodes: '_publisher_systems_services_shopping',project: 'shoppingAPI',branch: 'qa',directories: '_',page: 'Why'} )) + .toEqual("app/documentation/navigate/_publisher_systems_services_shopping/shoppingAPI/qa/_Public_Features_OfferFeeds/Feature"); + expect(service.relativeUrlToFullFrontEndUrl('../../Guides/Feeds/UseCases',{nodes: '_publisher_systems_services_shopping',project: 'shoppingAPI',branch: 'qa',directories: '_Public_Features_OfferFeeds',page: 'Feature'} )) + .toEqual("app/documentation/navigate/_publisher_systems_services_shopping/shoppingAPI/qa/_Public_Guides_Feeds/UseCases"); expect(service.relativeUrlToFullFrontEndUrl('../OfferSearch/Feature',{nodes: '_platform_publisher_systems_public',project: 'shoppingAPIPublic',branch: 'feature_PUB-4629-review-doc',directories: '_Features_OfferFeeds',page: 'Feature'} )) .toEqual("app/documentation/navigate/_platform_publisher_systems_public/shoppingAPIPublic/feature_PUB-4629-review-doc/_Features_OfferSearch/Feature"); - expect(service.relativeUrlToFullFrontEndUrl('https://thegardener.kelkoogroup.com',{nodes: 'Tools',project: 'theGardener',branch: 'master',directories: '_Guide_Write',page: 'Basics'} )) .toEqual("https://thegardener.kelkoogroup.com"); expect(service.relativeUrlToFullFrontEndUrl('OpenApi',{nodes: 'Tools',project: 'theGardener',branch: 'master',directories: '_Guide_Write',page: 'Basics'} )) diff --git a/frontend/src/app/_services/route.service.ts b/frontend/src/app/_services/route.service.ts index 7424d0930..b53a5114e 100644 --- a/frontend/src/app/_services/route.service.ts +++ b/frontend/src/app/_services/route.service.ts @@ -107,7 +107,8 @@ export class RouteService { } else { directoryNavigationForward = directoriesAndPageArray.join(EMPTY_CHAR); } - targetDirectoriesPath += directoryNavigationForward; + targetDirectoriesPath += EMPTY_CHAR + directoryNavigationForward; + targetDirectoriesPath = targetDirectoriesPath.replace('__','_') } @@ -125,6 +126,7 @@ export class RouteService { } else { const subDirectories = subDirectoriesAndPage.slice(0, subDirectoriesAndPage.length - 1); targetDirectoriesPath = navigationParams.directories + EMPTY_CHAR + subDirectories.join(EMPTY_CHAR); + targetDirectoriesPath = targetDirectoriesPath.replace('__','_') } return `${NAVIGATE_PATH}${navigationParams.nodes}/${navigationParams.project}/${navigationParams.branch}/${targetDirectoriesPath}/${page}`; } From 0a9a768f31b1865e56f3d3afb4c77e99b06a1472 Mon Sep 17 00:00:00 2001 From: GeReinhart Date: Tue, 15 Sep 2020 15:16:13 +0200 Subject: [PATCH 13/15] redirect brocken pages to search --- frontend/src/app/_services/route.service.spec.ts | 7 ++++++- frontend/src/app/_services/route.service.ts | 5 +++++ .../output/page-content/page-content.component.ts | 15 ++++++++------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/_services/route.service.spec.ts b/frontend/src/app/_services/route.service.spec.ts index 812deef8a..1e4eb42d7 100644 --- a/frontend/src/app/_services/route.service.spec.ts +++ b/frontend/src/app/_services/route.service.spec.ts @@ -200,7 +200,6 @@ describe('RouteService', () => { expect(service.directoryPathSimilar(navigationRoute, { nodes: ["publisher"], project: "ecs", branch: "master", directories: [] as Array, page: "Meta" })).toBeTrue(); }); - it('pagePathSimilar ', () => { const navigationRoute = { nodes: ["publisher"], project: "ecs", branch: "_", directories: [] as Array, page: "Meta" }; @@ -220,4 +219,10 @@ describe('RouteService', () => { expect(service.urlToRelativePath('_features_foo_bar')).toEqual('/features/foo/bar'); expect(service.urlToRelativePath('_features_foo.foo_bar')).toEqual('/features/foo_foo/bar'); }); + + it('extractKeyword ', () => { + expect(service.extractKeyword({ nodes: ["publisher","services","shopping"], project: "shoppingAPI", branch: "_", directories: ["Public","Guides"], page: "AuthenticationWithSignedUrlGuide" })) + .toEqual('publisher services shopping shopping A P I Authentication With Signed Url Guide'); + }); + }); diff --git a/frontend/src/app/_services/route.service.ts b/frontend/src/app/_services/route.service.ts index b53a5114e..4477117a8 100644 --- a/frontend/src/app/_services/route.service.ts +++ b/frontend/src/app/_services/route.service.ts @@ -330,4 +330,9 @@ export class RouteService { } return hierarchyFrontEndPath + '/' + navigationRoute.project + '/' + navigationRoute.branch + '/' + directories + '/' + navigationRoute.page; } + + extractKeyword(targetedRoute: NavigationRoute) { + const join = targetedRoute.nodes.join(" ")+ " " + targetedRoute.project+ " " + targetedRoute.page; + return join.replace(/([a-zA-Z])(?=[A-Z])/g, '$1 '); + } } diff --git a/frontend/src/app/output/page-content/page-content.component.ts b/frontend/src/app/output/page-content/page-content.component.ts index 05465f0b8..994fee376 100644 --- a/frontend/src/app/output/page-content/page-content.component.ts +++ b/frontend/src/app/output/page-content/page-content.component.ts @@ -12,8 +12,8 @@ import { PagePart, ScenarioPart } from '../../_models/page'; -import {NotificationService} from '../../_services/notification.service'; -import {RouteService} from "../../_services/route.service"; +import {RouteService, SEARCH_PATH} from "../../_services/route.service"; +import {NavigationRoute} from "../../_models/route"; @Component({ @@ -28,11 +28,11 @@ export class PageContentComponent implements OnInit, OnDestroy, AfterViewChecked private routerSubscription: Subscription; private fragment: string; private canScroll: boolean = true; + private targetedRoute: NavigationRoute; constructor(private activatedRoute: ActivatedRoute, private pageService: PageService, private routeService: RouteService, - private notificationService: NotificationService, private router: Router, private ngZone: NgZone) { } @@ -62,16 +62,17 @@ export class PageContentComponent implements OnInit, OnDestroy, AfterViewChecked return params; }), switchMap(params => { - const navigationRoute = this.routeService.navigationParamsToNavigationRoute(params); - if ( navigationRoute.page == undefined ){ + this.targetedRoute = this.routeService.navigationParamsToNavigationRoute(params); + if ( this.targetedRoute.page == undefined ){ return of(); }else{ - const backEndPath = this.routeService.navigationRouteToBackEndPath(navigationRoute); + const backEndPath = this.routeService.navigationRouteToBackEndPath(this.targetedRoute); return this.pageService.getPage(backEndPath.pathFromProject); } }), catchError(err => { - this.notificationService.showError(`Error while loading page`, err); + const keyword = this.routeService.extractKeyword(this.targetedRoute); + this.router.navigateByUrl(SEARCH_PATH + `?keyword=${keyword.trim()}`); return of(); }) ).subscribe(page => { From 698ec25ebba2d17df44794c4a93dcd873e562f6d Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Tue, 15 Sep 2020 13:41:38 +0000 Subject: [PATCH 14/15] [ci-skip]prepare release theGardener-1.4.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 954f50ffa..5721299a8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.4.2-SNAPSHOT + 1.4.2 kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - HEAD + theGardener-1.4.2 From 7e4f6aa0074442ad8ad3c691b5e7d43125dbde19 Mon Sep 17 00:00:00 2001 From: Kelkoo applications user Date: Tue, 15 Sep 2020 13:41:40 +0000 Subject: [PATCH 15/15] [ci-skip]prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5721299a8..bce933759 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kelkoo.gardener theGardener - 1.4.2 + 1.4.3-SNAPSHOT kelkoo-rpm theGardener @@ -21,7 +21,7 @@ scm:git|git@gitlab.corp.kelkoo.net:syndication/theGardener.git http://gitlab.corp.kelkoo.net/syndication/theGardener - theGardener-1.4.2 + HEAD