From d8c8be2ea8bc84dc82933ccff2bc81b470b97858 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Mon, 11 Mar 2024 13:07:06 +0000 Subject: [PATCH 01/10] add context menu item to workspace view --- frontend/src/js/components/workspace/Workspaces.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/components/workspace/Workspaces.tsx b/frontend/src/js/components/workspace/Workspaces.tsx index 89d21cd4..ffeb0700 100644 --- a/frontend/src/js/components/workspace/Workspaces.tsx +++ b/frontend/src/js/components/workspace/Workspaces.tsx @@ -542,7 +542,7 @@ class WorkspacesUnconnected extends React.Component { {key : "copyFilePath", content: copyFilePathContent, icon: "copy"}, // or 'pencil alternate' { key: "rename", content: "Rename", icon: "pen square" }, - { key: "remove", content: "Remove from workspace", icon: "trash" } + { key: "remove", content: "Remove from workspace", icon: "trash" }, ]; if (entry.data.addedBy.username === currentUser.username && isWorkspaceLeaf(entry.data)) { @@ -551,6 +551,8 @@ class WorkspacesUnconnected extends React.Component { if (isWorkspaceLeaf(entry.data)){ items.push({ key: "reprocess", content: "Reprocess source file", icon: "redo" }); + } else { + items.push({ key: "search", content: "Search in folder", icon: "search" }) } return @@ -594,6 +596,11 @@ class WorkspacesUnconnected extends React.Component { if (menuItemProps.content === 'Reprocess source file' && (isWorkspaceLeaf(entry.data))) { this.props.reprocessBlob(workspaceId, entry.data.uri) } + + if (menuItemProps.content === "Search in folder"){ + window.location.replace(`/search?filters.workspace[]=${workspace.id}`) + } + setTimeout(() => this.closeContextMenu(), closeMenuDelay); }} /> From 7f0923ab4d4be6ec894585a6fca6fd9d358f1300 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Wed, 13 Mar 2024 16:53:10 +0000 Subject: [PATCH 02/10] search endpoint handles workspace_folder chip --- .../app/controllers/api/PagesController.scala | 4 ++-- backend/app/controllers/api/Resource.scala | 10 ++++----- backend/app/controllers/api/Search.scala | 20 +++-------------- backend/app/model/frontend/Chip.scala | 22 ++++++++++++++++--- .../app/model/index/SearchParameters.scala | 6 ++++- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/backend/app/controllers/api/PagesController.scala b/backend/app/controllers/api/PagesController.scala index 7e53dc4e..ee78a1fc 100644 --- a/backend/app/controllers/api/PagesController.scala +++ b/backend/app/controllers/api/PagesController.scala @@ -49,7 +49,7 @@ class PagesController(val controllerComponents: AuthControllerComponents, manife // Get language and highlight data for a given page def getPageData(uri: Uri, pageNumber: Int, sq: Option[String], fq: Option[String]) = ApiAction.attempt { req => for { - response <- frontendPageFromQuery(uri, pageNumber, req.user.username, sq.map(Chips.parseQueryString), fq) + response <- frontendPageFromQuery(uri, pageNumber, req.user.username, sq.map(Chips.parseQueryString(_).query), fq) } yield { Ok(Json.toJson(response)) } @@ -150,7 +150,7 @@ class PagesController(val controllerComponents: AuthControllerComponents, manife // It behaves identically to the findInDocument endpoint, except that it expects its query to be in // a JSON format that may contain chips, and it returns highlight ids with a different prefix. def searchInDocument(uri: Uri, q: String) = ApiAction.attempt { req => - getHighlights(uri, Chips.parseQueryString(q), req.user.username, isSearch = true).map(highlights => + getHighlights(uri, Chips.parseQueryString(q).query, req.user.username, isSearch = true).map(highlights => Ok(Json.toJson(highlights)) ) } diff --git a/backend/app/controllers/api/Resource.scala b/backend/app/controllers/api/Resource.scala index 5f029df5..4f0651f2 100644 --- a/backend/app/controllers/api/Resource.scala +++ b/backend/app/controllers/api/Resource.scala @@ -34,11 +34,11 @@ class Resource(val controllerComponents: AuthControllerComponents, manifest: Man index: Index, pages: Pages, annotations: Annotations, previewStorage: ObjectStorage) extends AuthApiController { def getResource(uri: Uri, basic: Boolean, q: Option[String]) = ApiAction.attempt { implicit req => - val query = q.map(Chips.parseQueryString) + val parsedChips = q.map(Chips.parseQueryString) - val resourceFetchMode = (basic, query) match { + val resourceFetchMode = (basic, parsedChips) match { case (true, _) => ResourceFetchMode.Basic - case (false, text) => ResourceFetchMode.WithData(text) + case (false, pt) => ResourceFetchMode.WithData(pt.map(_.query)) } val decodedUri = Uri(UriEncoding.decodePath(uri.value, StandardCharsets.UTF_8)) @@ -75,12 +75,12 @@ class Resource(val controllerComponents: AuthControllerComponents, manifest: Man } def getTextPages(uri: Uri, top: Double, bottom: Double, q: Option[String], language: Option[Language]) = ApiAction.attempt { req => - val query = q.map(Chips.parseQueryString) + val parsedChips = q.map(Chips.parseQueryString) for { // Check we have permission to see this file _ <- GetResource(uri, ResourceFetchMode.Basic, req.user.username, manifest, index, annotations, controllerComponents.users).process() - response <- new GetPages(uri, top, bottom, query, language, pages, previewStorage).process() + response <- new GetPages(uri, top, bottom, parsedChips.map(_.query), language, pages, previewStorage).process() } yield { Ok(Json.toJson(response)) } diff --git a/backend/app/controllers/api/Search.scala b/backend/app/controllers/api/Search.scala index 17490a17..f0b4975a 100644 --- a/backend/app/controllers/api/Search.scala +++ b/backend/app/controllers/api/Search.scala @@ -22,10 +22,9 @@ class Search(override val controllerComponents: AuthControllerComponents, userMa def search() = ApiAction.attempt { req: UserIdentityRequest[_] => val q = req.queryString.getOrElse("q", Seq("")).head - val maybeWorkspaceContextParams = Search.parseWorkspaceContextParams(req) val proposedParams = Search.buildSearchParameters(q, req) - buildSearch(req.user, proposedParams, maybeWorkspaceContextParams).flatMap { case (verifiedParams, context) => + buildSearch(req.user, proposedParams, proposedParams.workspaceContext).flatMap { case (verifiedParams, context) => val returnEmptyResult = Search.shouldReturnEmptyResult(proposedParams, verifiedParams, context) if(returnEmptyResult) { @@ -96,9 +95,9 @@ object Search extends Logging { val createdAtFilters = req.queryString.getOrElse("createdAt[]", Seq()).toList val (start, end) = parseCreatedAt(createdAtFilters) - val query = Chips.parseQueryString(q) + val parsedChips = Chips.parseQueryString(q) - SearchParameters(query, mimeFilters, ingestionFilters, workspaceFilters, start, end, page, pageSize, sortBy) + SearchParameters(parsedChips.query, mimeFilters, ingestionFilters, workspaceFilters, start, end, page, pageSize, sortBy, parsedChips.workspace) } def verifyParameters(user: User, params: SearchParameters, context: DefaultSearchContext): SearchParameters = { @@ -183,19 +182,6 @@ object Search extends Logging { } } - def parseWorkspaceContextParams(req: RequestHeader): Option[WorkspaceSearchContextParams] = { - val maybeWorkspaceId = req.getQueryString("workspaceId") - val maybeWorkspaceFolderId = req.getQueryString("workspaceFolderId") - - (maybeWorkspaceId, maybeWorkspaceFolderId) match { - case (Some(workspaceId), Some(workspaceFolderId)) => - Some(WorkspaceSearchContextParams(workspaceId, workspaceFolderId)) - - case _ => - None - } - } - private def epochTs(instant: LocalDateTime): Long = { instant.toInstant(ZoneOffset.UTC).toEpochMilli } diff --git a/backend/app/model/frontend/Chip.scala b/backend/app/model/frontend/Chip.scala index 9c6595f6..6ed1e46b 100644 --- a/backend/app/model/frontend/Chip.scala +++ b/backend/app/model/frontend/Chip.scala @@ -1,6 +1,7 @@ package model.frontend import play.api.libs.json._ +import services.index.WorkspaceSearchContextParams case class DropdownOption(label: String, value: String) @@ -46,14 +47,28 @@ object Chip { } } +case class ParsedChips (query: String, workspace: Option[WorkspaceSearchContextParams]) + object Chips { // TODO - when the index supports attempts we can uncomment these lines to make this attempty //def parseQueryString(q: String): Attempt[String] = { - def parseQueryString(q: String): String = { + def parseQueryString(q: String): ParsedChips = { //Attempt.catchNonFatal { + val parsedQ = Json.parse(q) + // find WorkspaceSearchContextParams + val workspaceFolder = parsedQ match { + case JsArray(value) => value.collectFirst { + case JsObject(o) => WorkspaceSearchContextParams(o.get("workspaceId").map(_.validate[String].get).get, o.get("folderId").map(_.validate[String].get).get) + } + case _ => None + } - Json.parse(q) match { - case JsArray(v) => v.toList.map { + val query = parsedQ match { + case JsArray(v) => v.toList.filter { + // remove workspace_folder chips + case JsObject(o) if o.get("t").map(_.validate[String].get).get == "workspace_folder" => false + case _ => true + }.map { // When typing a new chip, we end up with a dangling + which is illegal in the ES query syntax. // This doesn't matter if you start a chip before an existing term or in between two existing. // In that case it will be parsed as the boolean operator attached to the subsequent term. @@ -78,6 +93,7 @@ object Chips { }.mkString(" ") case _ => throw new UnsupportedOperationException("Outer json type must be an array") } + ParsedChips(query, workspaceFolder) // } { // case s: Exception => ClientFailure(s"Invalid query: ${s.getMessage}") //} diff --git a/backend/app/model/index/SearchParameters.scala b/backend/app/model/index/SearchParameters.scala index f1afbdc8..74e8caa4 100644 --- a/backend/app/model/index/SearchParameters.scala +++ b/backend/app/model/index/SearchParameters.scala @@ -1,5 +1,7 @@ package model.index +import services.index.WorkspaceSearchContextParams + trait SortBy case object Relevance extends SortBy case object SizeAsc extends SortBy @@ -15,7 +17,9 @@ case class SearchParameters(q: String, end: Option[Long], page: Int, pageSize: Int, - sortBy: SortBy) { + sortBy: SortBy, + workspaceContext: Option[WorkspaceSearchContextParams] = None + ) { def from: Int = (page - 1) * pageSize def size: Int = pageSize } From 40802646b9b2803de8b8db21af95be68ba1a8eae Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Wed, 13 Mar 2024 17:02:47 +0000 Subject: [PATCH 03/10] only take workspace info from folder chip --- backend/app/model/frontend/Chip.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/model/frontend/Chip.scala b/backend/app/model/frontend/Chip.scala index 6ed1e46b..08214a84 100644 --- a/backend/app/model/frontend/Chip.scala +++ b/backend/app/model/frontend/Chip.scala @@ -58,7 +58,8 @@ object Chips { // find WorkspaceSearchContextParams val workspaceFolder = parsedQ match { case JsArray(value) => value.collectFirst { - case JsObject(o) => WorkspaceSearchContextParams(o.get("workspaceId").map(_.validate[String].get).get, o.get("folderId").map(_.validate[String].get).get) + case JsObject(o) if o.get("t").map(_.validate[String].get).get == "workspace_folder" => + WorkspaceSearchContextParams(o.get("workspaceId").map(_.validate[String].get).get, o.get("folderId").map(_.validate[String].get).get) } case _ => None } From c75e30e1070b53f8bce4e670cfe64ba3d1fb1f90 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Thu, 14 Mar 2024 13:42:45 +0000 Subject: [PATCH 04/10] add WorkspaceFolderChip component --- .../UtilComponents/InputSupper/Chip.js | 53 ++++++++++++++++++- .../UtilComponents/InputSupper/index.js | 8 ++- .../js/components/workspace/Workspaces.tsx | 20 ++++++- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/frontend/src/js/components/UtilComponents/InputSupper/Chip.js b/frontend/src/js/components/UtilComponents/InputSupper/Chip.js index cb03d9e3..7aa61be7 100644 --- a/frontend/src/js/components/UtilComponents/InputSupper/Chip.js +++ b/frontend/src/js/components/UtilComponents/InputSupper/Chip.js @@ -84,7 +84,8 @@ export default class Chip extends React.Component { } onNegateClicked = () => { - this.props.onNegateClicked(this.props.index); + if (this.props.type !== 'workspace_folder') + this.props.onNegateClicked(this.props.index); } refHandler = (element) => { @@ -95,6 +96,8 @@ export default class Chip extends React.Component { switch (this.props.type) { case 'text': return ; + case 'workspace_folder': + return case 'date': return ; case 'date_ex': @@ -159,6 +162,54 @@ class UnknownChip extends React.Component { } } +class WorkspaceFolderChip extends React.Component { + static propTypes = { + value: PropTypes.string.isRequired, + onFocus: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyDown: PropTypes.func.isRequired + } + + focus = () => { + this.textInput.focus(); + } + + focusEnd = () => { + this.textInput.focus(); + this.textInput.input.selectionStart = this.textInput.input.selectionEnd = this.textInput.input.value.length; + } + + select = () => { + this.textInput.select(); + } + + onChange = (e) => { + this.props.onChange(e.target.value); + } + + onKeyDown = (e) => { + const inputStart = this.textInput.input.selectionStart; + const inputEnd = this.textInput.input.selectionEnd; + + this.props.onKeyDown(e, inputStart, inputEnd); + } + + render() { + return ( + this.textInput = i} + type='text' + inputClassName='input-supper__inline-input input-supper__inline-input--chip' + value={this.props.value} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onFocus={this.props.onFocus} + disabled={true} + /> + ); + } + +} + class InputChip extends React.Component { static propTypes = { value: PropTypes.string.isRequired, diff --git a/frontend/src/js/components/UtilComponents/InputSupper/index.js b/frontend/src/js/components/UtilComponents/InputSupper/index.js index 782ea703..9c32ed38 100644 --- a/frontend/src/js/components/UtilComponents/InputSupper/index.js +++ b/frontend/src/js/components/UtilComponents/InputSupper/index.js @@ -71,7 +71,9 @@ export default class InputSupper extends React.Component { name: e.n, value: e.v, negate: e.op === '-', - chipType: e.t + chipType: e.t, + workspaceId: e.workspaceId, + folderId: e.folderId, }; } @@ -125,7 +127,9 @@ export default class InputSupper extends React.Component { n: e.name, v: e.value, op: e.negate ? '-' : '+', - t: e.chipType + t: e.chipType, + workspaceId: e.workspaceId, + folderId: e.folderId }; } return ''; diff --git a/frontend/src/js/components/workspace/Workspaces.tsx b/frontend/src/js/components/workspace/Workspaces.tsx index ffeb0700..efd5d6a5 100644 --- a/frontend/src/js/components/workspace/Workspaces.tsx +++ b/frontend/src/js/components/workspace/Workspaces.tsx @@ -48,6 +48,8 @@ import { reprocessBlob } from '../../actions/workspaces/reprocessBlob'; import { DeleteModal, DeleteStatus } from './DeleteModal'; import { PartialUser } from '../../types/User'; import { getMyPermissions } from '../../actions/users/getMyPermissions'; +import buildLink from '../../util/buildLink'; +import history from '../../util/history'; type Props = ReturnType @@ -598,7 +600,23 @@ class WorkspacesUnconnected extends React.Component { } if (menuItemProps.content === "Search in folder"){ - window.location.replace(`/search?filters.workspace[]=${workspace.id}`) + history.push( + buildLink("/search", { + q: JSON.stringify([ + "", + { + n: "Workspace Folder", + v: entry.name, + op: "+", + t: "workspace_folder", + workspaceId: workspace.id, + folderId: entry.id, + }, + "*" + ]), + page: 1 + }), + ) } setTimeout(() => this.closeContextMenu(), closeMenuDelay); From a135b4af07e58a773900a970a9c6254a43db876f Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Thu, 14 Mar 2024 15:36:59 +0000 Subject: [PATCH 05/10] fix tests --- backend/app/model/frontend/Chip.scala | 8 ++++++++ backend/test/controllers/api/WorkspaceSearchITest.scala | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/app/model/frontend/Chip.scala b/backend/app/model/frontend/Chip.scala index 08214a84..0dab54be 100644 --- a/backend/app/model/frontend/Chip.scala +++ b/backend/app/model/frontend/Chip.scala @@ -17,6 +17,12 @@ case class DateChip(name: String, template: String) extends Chip case class ExclusiveDateChip(name: String, template: String) extends Chip case class DropdownChip(name: String, options: List[DropdownOption], template: String) extends Chip +case class WorkspaceFolderChip(name: String, template: String, workspaceId: String, folderId: String) extends Chip + +object WorkspaceFolderChip { + implicit val workspaceFolderChip = Json.format[WorkspaceFolderChip] +} + object Chip { private val plainChipFormat = Json.format[TextChip] private val dateChipFormat = Json.format[DateChip] @@ -31,6 +37,7 @@ object Chip { case JsString("dropdown") => dropdownChipFormat.reads(json) case JsString("date") => dateChipFormat.reads(json) case JsString("date_ex") => exclusiveDateChipFormat.reads(json) + case JsString("workspace_folder") => WorkspaceFolderChip.workspaceFolderChip.reads(json) case other => JsError(s"Unexpected type in chip $other") } } @@ -41,6 +48,7 @@ object Chip { case r: DateChip => dateChipFormat.writes(r) + ("type", JsString("date")) - "template" case r: ExclusiveDateChip => exclusiveDateChipFormat.writes(r) + ("type", JsString("date_ex")) - "template" case r: DropdownChip => dropdownChipFormat.writes(r) + ("type", JsString("dropdown")) - "template" + case r: WorkspaceFolderChip => WorkspaceFolderChip.workspaceFolderChip.writes(r) + ("type", JsString("workspace_folder")) - "template" case other => throw new UnsupportedOperationException(s"Unable to serialize chip of type ${other.getClass.toString}") } } diff --git a/backend/test/controllers/api/WorkspaceSearchITest.scala b/backend/test/controllers/api/WorkspaceSearchITest.scala index 40e7bb6d..f552277c 100644 --- a/backend/test/controllers/api/WorkspaceSearchITest.scala +++ b/backend/test/controllers/api/WorkspaceSearchITest.scala @@ -14,6 +14,9 @@ import test.integration.{ElasticsearchTestService, ItemIds, Neo4jTestService} import scala.concurrent.ExecutionContext import scala.concurrent.duration._ import org.scalatest.funsuite.AnyFunSuite +import model.frontend.WorkspaceFolderChip._ +import model.frontend.WorkspaceFolderChip +import play.api.libs.json._ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with ElasticsearchTestService with BeforeAndAfterEach { final implicit override def patience: PatienceConfig = PatienceConfig(Span(30, Seconds), Span(250, Millis)) @@ -78,7 +81,8 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti test("Paul can search in a folder in his workspace") { asUser("paul") { controllers => val folderId = itemIds.`f/h` - val request = FakeRequest("GET", s"""/query?q=["*"]&workspaceId=${paulWorkspace.id}&workspaceFolderId=${folderId}""") + val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) + val request = FakeRequest("GET", s"""/query?q=[${q}]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results results should have length(1) @@ -90,7 +94,8 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti test("Paul can see results from nested folders in his workspace") { asUser("paul") { controllers => val folderId = itemIds.`f` - val request = FakeRequest("GET", s"""/query?q=["*"]&workspaceId=${paulWorkspace.id}&workspaceFolderId=${folderId}""") + val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) + val request = FakeRequest("GET", s"""/query?q=[${q}]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results From dc946cc9f2ec74ae731c402a3272e470f2beaf32 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Thu, 14 Mar 2024 15:53:57 +0000 Subject: [PATCH 06/10] fix tests some more --- backend/app/model/frontend/Chip.scala | 2 +- .../test/controllers/api/WorkspaceSearchITest.scala | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/app/model/frontend/Chip.scala b/backend/app/model/frontend/Chip.scala index 0dab54be..fea220b4 100644 --- a/backend/app/model/frontend/Chip.scala +++ b/backend/app/model/frontend/Chip.scala @@ -48,7 +48,7 @@ object Chip { case r: DateChip => dateChipFormat.writes(r) + ("type", JsString("date")) - "template" case r: ExclusiveDateChip => exclusiveDateChipFormat.writes(r) + ("type", JsString("date_ex")) - "template" case r: DropdownChip => dropdownChipFormat.writes(r) + ("type", JsString("dropdown")) - "template" - case r: WorkspaceFolderChip => WorkspaceFolderChip.workspaceFolderChip.writes(r) + ("type", JsString("workspace_folder")) - "template" + case r: WorkspaceFolderChip => WorkspaceFolderChip.workspaceFolderChip.writes(r) + ("t", JsString("workspace_folder")) - "template" case other => throw new UnsupportedOperationException(s"Unable to serialize chip of type ${other.getClass.toString}") } } diff --git a/backend/test/controllers/api/WorkspaceSearchITest.scala b/backend/test/controllers/api/WorkspaceSearchITest.scala index f552277c..e6ed0443 100644 --- a/backend/test/controllers/api/WorkspaceSearchITest.scala +++ b/backend/test/controllers/api/WorkspaceSearchITest.scala @@ -68,7 +68,8 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti test("Barry can't search inside a folder in Paul's workspace") { asUser("barry") { controllers => val folderId = itemIds.f - val request = FakeRequest("GET", s"""/query?q=["*"]&workspaceId=${paulWorkspace.id}&workspaceFolderId=${folderId}""") + val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) + val request = FakeRequest("GET", s"""/query?q=[${q}]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults] // TODO: This is only 0 because Barry cannot see anything at all (he has no workspaces or collections of his own). @@ -110,8 +111,8 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("paul") { controllers => val folderId = itemIds.`f/g` - val request = FakeRequest("GET", s"""/query?q=["*"]&workspaceId=${paulWorkspace.id}&workspaceFolderId=${folderId}""") - + val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) + val request = FakeRequest("GET", s"""/query?q=[${q}]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results results.map(_.uri) should contain only( @@ -130,7 +131,8 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti itemId = itemIds.`f/h/1.txt`.nodeId ) - val request = FakeRequest("GET", s"""/query?q=["*"]&workspaceId=${paulWorkspace.id}&workspaceFolderId=${folderId}""") + val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) + val request = FakeRequest("GET", s"""/query?q=[${q}]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults] results.hits should be(0) @@ -144,7 +146,8 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("barry") { implicit controllers => val folderId = itemIds.`f/h` - val request = FakeRequest("GET", s"""/query?q=["*"]&workspaceId=${paulWorkspace.id}&workspaceFolderId=${folderId}""") + val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) + val request = FakeRequest("GET", s"""/query?q=[${q}]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results results should have length(1) From 26a6c649389f8e5be5610fe39c25cc2b13a99120 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Thu, 14 Mar 2024 16:20:56 +0000 Subject: [PATCH 07/10] fix --- backend/app/model/frontend/Chip.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/app/model/frontend/Chip.scala b/backend/app/model/frontend/Chip.scala index fea220b4..ff99e94b 100644 --- a/backend/app/model/frontend/Chip.scala +++ b/backend/app/model/frontend/Chip.scala @@ -17,7 +17,7 @@ case class DateChip(name: String, template: String) extends Chip case class ExclusiveDateChip(name: String, template: String) extends Chip case class DropdownChip(name: String, options: List[DropdownOption], template: String) extends Chip -case class WorkspaceFolderChip(name: String, template: String, workspaceId: String, folderId: String) extends Chip +case class WorkspaceFolderChip(name: String, t: String, workspaceId: String, folderId: String) object WorkspaceFolderChip { implicit val workspaceFolderChip = Json.format[WorkspaceFolderChip] @@ -37,7 +37,6 @@ object Chip { case JsString("dropdown") => dropdownChipFormat.reads(json) case JsString("date") => dateChipFormat.reads(json) case JsString("date_ex") => exclusiveDateChipFormat.reads(json) - case JsString("workspace_folder") => WorkspaceFolderChip.workspaceFolderChip.reads(json) case other => JsError(s"Unexpected type in chip $other") } } @@ -48,7 +47,6 @@ object Chip { case r: DateChip => dateChipFormat.writes(r) + ("type", JsString("date")) - "template" case r: ExclusiveDateChip => exclusiveDateChipFormat.writes(r) + ("type", JsString("date_ex")) - "template" case r: DropdownChip => dropdownChipFormat.writes(r) + ("type", JsString("dropdown")) - "template" - case r: WorkspaceFolderChip => WorkspaceFolderChip.workspaceFolderChip.writes(r) + ("t", JsString("workspace_folder")) - "template" case other => throw new UnsupportedOperationException(s"Unable to serialize chip of type ${other.getClass.toString}") } } From 8982eae8d82d88e9da689d0d3433499a104a4530 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Thu, 14 Mar 2024 16:38:59 +0000 Subject: [PATCH 08/10] fix --- .../test/controllers/api/WorkspaceSearchITest.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/test/controllers/api/WorkspaceSearchITest.scala b/backend/test/controllers/api/WorkspaceSearchITest.scala index e6ed0443..e08c266f 100644 --- a/backend/test/controllers/api/WorkspaceSearchITest.scala +++ b/backend/test/controllers/api/WorkspaceSearchITest.scala @@ -69,7 +69,7 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("barry") { controllers => val folderId = itemIds.f val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) - val request = FakeRequest("GET", s"""/query?q=[${q}]""") + val request = FakeRequest("GET", s"""/query?q=[${q},"*"]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults] // TODO: This is only 0 because Barry cannot see anything at all (he has no workspaces or collections of his own). @@ -83,7 +83,7 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("paul") { controllers => val folderId = itemIds.`f/h` val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) - val request = FakeRequest("GET", s"""/query?q=[${q}]""") + val request = FakeRequest("GET", s"""/query?q=[${q},"*"]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results results should have length(1) @@ -96,7 +96,7 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("paul") { controllers => val folderId = itemIds.`f` val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) - val request = FakeRequest("GET", s"""/query?q=[${q}]""") + val request = FakeRequest("GET", s"""/query?q=[${q},"*"]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results @@ -112,7 +112,7 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("paul") { controllers => val folderId = itemIds.`f/g` val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) - val request = FakeRequest("GET", s"""/query?q=[${q}]""") + val request = FakeRequest("GET", s"""/query?q=[${q},"*"]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results results.map(_.uri) should contain only( @@ -132,7 +132,7 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti ) val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) - val request = FakeRequest("GET", s"""/query?q=[${q}]""") + val request = FakeRequest("GET", s"""/query?q=[${q},"*"]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults] results.hits should be(0) @@ -147,7 +147,7 @@ class WorkspaceSearchITest extends AnyFunSuite with Neo4jTestService with Elasti asUser("barry") { implicit controllers => val folderId = itemIds.`f/h` val q = Json.toJson(WorkspaceFolderChip("workspace", "workspace_folder", paulWorkspace.id, folderId)) - val request = FakeRequest("GET", s"""/query?q=[${q}]""") + val request = FakeRequest("GET", s"""/query?q=[${q},"*"]""") val results = contentAsJson(controllers.search.search().apply(request)).as[SearchResults].results results should have length(1) From 5ef34f4e56d4296c81f775e82dc542b853c4c7b9 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Fri, 15 Mar 2024 10:01:59 +0000 Subject: [PATCH 09/10] fix typo --- backend/app/controllers/api/Resource.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/controllers/api/Resource.scala b/backend/app/controllers/api/Resource.scala index 4f0651f2..a9a69d6c 100644 --- a/backend/app/controllers/api/Resource.scala +++ b/backend/app/controllers/api/Resource.scala @@ -38,7 +38,7 @@ class Resource(val controllerComponents: AuthControllerComponents, manifest: Man val resourceFetchMode = (basic, parsedChips) match { case (true, _) => ResourceFetchMode.Basic - case (false, pt) => ResourceFetchMode.WithData(pt.map(_.query)) + case (false, pc) => ResourceFetchMode.WithData(pc.map(_.query)) } val decodedUri = Uri(UriEncoding.decodePath(uri.value, StandardCharsets.UTF_8)) From ce3ec85ffd542ab22fcf8015c56c18577ad06936 Mon Sep 17 00:00:00 2001 From: Zeke Hunter-Green Date: Mon, 18 Mar 2024 13:18:45 +0000 Subject: [PATCH 10/10] Add comment about folder chip to Chips --- backend/app/model/frontend/Chip.scala | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/app/model/frontend/Chip.scala b/backend/app/model/frontend/Chip.scala index ff99e94b..bd0cea7c 100644 --- a/backend/app/model/frontend/Chip.scala +++ b/backend/app/model/frontend/Chip.scala @@ -56,12 +56,13 @@ object Chip { case class ParsedChips (query: String, workspace: Option[WorkspaceSearchContextParams]) object Chips { - // TODO - when the index supports attempts we can uncomment these lines to make this attempty - //def parseQueryString(q: String): Attempt[String] = { + // TODO - when the index supports attempts, make this function attempty def parseQueryString(q: String): ParsedChips = { - //Attempt.catchNonFatal { val parsedQ = Json.parse(q) - // find WorkspaceSearchContextParams + // workspace_folder chips need to be handled separately from the rest. + // Instead of transforming the chips to a query string, we need to use + // workspace and folder IDs to find a list of blob so filter for. Look for + // those IDs here, then skip workspace_folder when building the query. val workspaceFolder = parsedQ match { case JsArray(value) => value.collectFirst { case JsObject(o) if o.get("t").map(_.validate[String].get).get == "workspace_folder" => @@ -101,9 +102,6 @@ object Chips { case _ => throw new UnsupportedOperationException("Outer json type must be an array") } ParsedChips(query, workspaceFolder) - // } { - // case s: Exception => ClientFailure(s"Invalid query: ${s.getMessage}") - //} } val all: List[Chip] = List(