From 16e62ac47a516ac8c42f48e3d6e169f123939b1c Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 24 Jan 2024 17:13:38 +0100 Subject: [PATCH] Finish building block search logic :sparkles: --- src/Client/Init.fs | 1 - src/Client/Messages.fs | 17 +- src/Client/Model.fs | 103 ++- .../Pages/BuildingBlock/BuildingBlockView.fs | 26 +- src/Client/Pages/BuildingBlock/Dropdown.fs | 158 ++--- src/Client/Pages/BuildingBlock/Helper.fs | 64 +- .../Pages/BuildingBlock/SearchComponent.fs | 589 ++++-------------- src/Client/Pages/TermSearch/TermSearchView.fs | 4 +- src/Client/SharedComponents/AdvancedSearch.fs | 21 +- .../SharedComponents/TermSearchInput.fs | 114 ++-- src/Client/Spreadsheet/Sidebar.Controller.fs | 2 +- src/Client/Update.fs | 21 + src/Shared/ARCtrl.Helper.fs | 8 + 13 files changed, 420 insertions(+), 708 deletions(-) diff --git a/src/Client/Init.fs b/src/Client/Init.fs index 94a68113..99e233b2 100644 --- a/src/Client/Init.fs +++ b/src/Client/Init.fs @@ -17,7 +17,6 @@ let initializeModel () = PersistentStorageState = PersistentStorageState .init () DevState = DevState .init () TermSearchState = TermSearch.Model .init () - AdvancedSearchState = AdvancedSearch.Model .init () ExcelState = OfficeInterop.Model .init () ApiState = ApiState .init () FilePickerState = FilePicker.Model .init () diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index 29f89e39..e20df903 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -15,6 +15,7 @@ open OfficeInteropTypes open Model open Routing open ARCtrl.ISA +open Fable.Core type System.Exception with member this.GetPropagatedError() = @@ -36,6 +37,12 @@ module TermSearch = | UpdateSelectedTerm of OntologyAnnotation option | UpdateParentTerm of OntologyAnnotation option + +module AdvancedSearch = + + type Msg = + | GetSearchResults of {| config:AdvancedSearchTypes.AdvancedSearchOptions; responseSetter: Term [] -> unit |} + type DevMsg = | LogTableMetadata | GenericLog of Cmd * (string*string) @@ -81,9 +88,10 @@ module BuildingBlock = open TermSearch type Msg = - | SelectHeader of CompositeHeader - /// Returns all child terms - | SelectBodyCell of CompositeCell option + | UpdateHeaderCellType of BuildingBlock.HeaderCellType + | UpdateHeaderArg of U2 option + | UpdateBodyCellType of BuildingBlock.BodyCellType + | UpdateBodyArg of U2 option // Below everything is more or less deprecated // Is still used for unit update in office | SearchUnitTermTextChange of searchString:string @@ -131,7 +139,6 @@ type Model = { DevState : DevState ///States regarding term search TermSearchState : TermSearch.Model - AdvancedSearchState : AdvancedSearch.Model ///Use this in the future to model excel stuff like table data ExcelState : OfficeInterop.Model /// This should be removed. Overhead making maintainance more difficult @@ -171,6 +178,7 @@ type Msg = | Api of ApiMsg | DevMsg of DevMsg | TermSearchMsg of TermSearch.Msg +| AdvancedSearchMsg of AdvancedSearch.Msg | OfficeInteropMsg of OfficeInterop.Msg | PersistentStorage of PersistentStorageMsg | FilePickerMsg of FilePicker.Msg @@ -189,6 +197,7 @@ type Msg = | UpdatePageState of Routing.Route option | UpdateIsExpert of bool | Batch of seq +| Run of (unit -> unit) | UpdateHistory of LocalHistory.Model /// Top level msg to test specific api interactions, only for dev. | TestMyAPI diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 4825d8ba..12273014 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -195,57 +195,90 @@ module FilePicker = } open OfficeInteropTypes +open Fable.Core module BuildingBlock = open ARCtrl.ISA + type [] HeaderCellType = + | Component + | Characteristic + | Factor + | Parameter + | ProtocolType + | ProtocolDescription + | ProtocolUri + | ProtocolVersion + | ProtocolREF + | Performer + | Date + | Input + | Output + with + /// + /// Returns true if the Building Block is a term column + /// + member this.IsTermColumn() = + match this with + | Component + | Characteristic + | Factor + | Parameter + | ProtocolType -> true + | _ -> false + member this.HasOA() = + match this with + | Component + | Characteristic + | Factor + | Parameter -> true + | _ -> false + + member this.HasIOType() = + match this with + | Input + | Output -> true + | _ -> false + + type [] BodyCellType = + | Term + | Unitized + | Text + [] type DropdownPage = | Main | More - | IOTypes of ((IOType -> CompositeHeader)*string) + | IOTypes of HeaderCellType member this.toString = match this with | Main -> "Main Page" | More -> "More" - | IOTypes (_,name) -> name + | IOTypes (t) -> t.ToString() member this.toTooltip = match this with | More -> "More" - | IOTypes (_,name) -> $"Per table only one {name} is allowed. The value of this column must be a unique identifier." + | IOTypes (t) -> $"Per table only one {t} is allowed. The value of this column must be a unique identifier." | _ -> "" - type [] BodyCellType = - | Term - | Unitized - | Text - type BuildingBlockUIState = { DropdownIsActive : bool DropdownPage : DropdownPage - BodyCellType : BodyCellType } with static member init() = { DropdownIsActive = false DropdownPage = DropdownPage.Main - BodyCellType = BodyCellType.Term } type Model = { - Header : CompositeHeader - BodyCell : CompositeCell option - /// This can refer to directly inserted terms as values for the body or to unit terms applied to all body cells. - HeaderSearchText : string - /// This always referrs to any term applied to the header. - HeaderSearchResults : Term [] - /// This always referrs to any term applied to the header. - BodySearchText : string - /// This can refer to directly inserted terms as values for the body or to unit terms applied to all body cells. - BodySearchResults : Term [] + HeaderCellType : HeaderCellType + HeaderArg : U2 option + BodyCellType : BodyCellType + BodyArg : U2 option // Below everything is more or less deprecated // This section is used to add a unit directly to an already existing building block @@ -258,12 +291,10 @@ module BuildingBlock = } with static member init () = { - HeaderSearchText = "" - HeaderSearchResults = [||] - Header = CompositeHeader.ParameterEmpty - BodySearchText = "" - BodySearchResults = [||] - BodyCell = None + HeaderCellType = HeaderCellType.Parameter + HeaderArg = None + BodyCellType = BodyCellType.Term + BodyArg = None // Below everything is more or less deprecated // This section is used to add a unit directly to an already existing building block @@ -274,6 +305,26 @@ module BuildingBlock = HasUnit2TermSuggestionsLoading = false } + member this.TryHeaderOA() = + match this.HeaderArg with + | Some (U2.Case1 oa) -> Some oa + | _ -> None + + member this.TryHeaderIO() = + match this.HeaderArg with + | Some (U2.Case2 io) -> Some io + | _ -> None + + member this.TryBodyOA() = + match this.BodyArg with + | Some (U2.Case2 oa) -> Some oa + | _ -> None + + member this.TryBodyString() = + match this.BodyArg with + | Some (U2.Case1 s) -> Some s + | _ -> None + /// Validation scheme for Table module Validation = type Model = { diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index 428f39ea..2b35fe26 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -17,11 +17,29 @@ open Elmish let update (addBuildingBlockMsg:BuildingBlock.Msg) (state: BuildingBlock.Model) : BuildingBlock.Model * Cmd = match addBuildingBlockMsg with - | SelectHeader header -> - let nextState = { state with Header = header } + | UpdateBodyArg next -> + let nextState = { state with BodyArg = next } nextState, Cmd.none - | SelectBodyCell (cell) -> - let nextState = { state with BodyCell = cell} + | UpdateHeaderArg next -> + let nextState = { state with HeaderArg = next} + nextState, Cmd.none + | UpdateHeaderCellType next -> + let nextState = + if Helper.isSameMajorHeaderCellType state.HeaderCellType next then + { state with + HeaderCellType = next + } + else + let nextBodyCellType = if next.IsTermColumn() then BuildingBlock.BodyCellType.Term else BuildingBlock.BodyCellType.Text + { state with + HeaderCellType = next + BodyCellType = nextBodyCellType + HeaderArg = None + BodyArg = None + } + nextState, Cmd.none + | UpdateBodyCellType next -> + let nextState = { state with BodyCellType = next } nextState, Cmd.none | SearchUnitTermTextChange (newTerm) -> diff --git a/src/Client/Pages/BuildingBlock/Dropdown.fs b/src/Client/Pages/BuildingBlock/Dropdown.fs index c58e2a17..f2bd479c 100644 --- a/src/Client/Pages/BuildingBlock/Dropdown.fs +++ b/src/Client/Pages/BuildingBlock/Dropdown.fs @@ -13,6 +13,7 @@ open Model open Messages open ARCtrl.ISA open BuildingBlock.Helper +open Fable.Core [] @@ -48,14 +49,6 @@ module private DropdownElements = style.justifyContent.spaceBetween ] prop.children [ - //Html.span [ - // prop.style itemTooltipStyle - // prop.className "has-tooltip-multiline" - // prop.custom("data-tooltip", subpage.toTooltip) - // prop.children (Bulma.icon [ - // Html.i [prop.className "fa-solid fa-circle-info"] - // ]) - //] Html.span subpage.toString @@ -67,7 +60,7 @@ module private DropdownElements = ] /// Navigation element back to main page - let backToMainDropdownButton (state: BuildingBlockUIState) setState = + let backToMainDropdownButton setState = Bulma.dropdownItem.div [ prop.style [style.textAlign.right] prop.children [ @@ -76,7 +69,7 @@ module private DropdownElements = prop.onClick(fun e -> e.preventDefault() e.stopPropagation() - setState {state with DropdownPage = BuildingBlock.DropdownPage.Main} + setState {DropdownPage = BuildingBlock.DropdownPage.Main; DropdownIsActive = true} ) Bulma.button.isInverted Bulma.color.isBlack @@ -90,67 +83,51 @@ module private DropdownElements = ] ] - let createBuildingBlockDropdownItem (model: Model) dispatch uiState setUiState (header: CompositeHeader) = - let isDeepFreeText = - match header with - | CompositeHeader.FreeText _ - | CompositeHeader.Input (IOType.FreeText _) - | CompositeHeader.Output (IOType.FreeText _) -> - true - | _ -> - false + let createBuildingBlockDropdownItem (model: Model) dispatch setUiState (headerType: BuildingBlock.HeaderCellType) = + Bulma.dropdownItem.a [ + prop.onClick (fun e -> + e.stopPropagation() + Helper.selectHeaderCellType headerType setUiState dispatch + ) + prop.onKeyDown(fun k -> + if (int k.which) = 13 then Helper.selectHeaderCellType headerType setUiState dispatch + ) + prop.text (headerType.ToString()) + ] + + let createIOTypeDropdownItem (model: Model) dispatch setUiState (headerType: BuildingBlock.HeaderCellType) (iotype: IOType) = + let setIO (ioType) = + Helper.selectHeaderCellType headerType setUiState dispatch + U2.Case2 ioType |> Some |> BuildingBlock.UpdateHeaderArg |> BuildingBlockMsg |> dispatch Bulma.dropdownItem.a [ - if not isDeepFreeText then //disable clicking on freetext elements - let nextHeader = - if header.IsTermColumn && not header.IsFeaturedColumn then - header.UpdateDeepWith model.AddBuildingBlockState.Header - else - header - prop.onClick (fun e -> - e.stopPropagation() - selectHeader uiState setUiState nextHeader |> dispatch - ) - prop.onKeyDown(fun k -> - if (int k.which) = 13 then selectHeader uiState setUiState nextHeader |> dispatch - ) prop.children [ - //Html.span [ - // prop.style itemTooltipStyle - // prop.className "has-tooltip-multiline" - // prop.custom("data-tooltip", header.GetUITooltip()) - // prop.children (Bulma.icon [ - // Html.i [prop.className "fa-solid fa-circle-info"] - // ]) - //] - match header with - | CompositeHeader.Output io | CompositeHeader.Input io -> - match io with - | IOType.FreeText str -> - let ch = if header.isOutput then CompositeHeader.Output else CompositeHeader.Input - let onSubmit = fun (v: string) -> - let header = IOType.FreeText v |> ch - selectHeader uiState setUiState header |> dispatch - FreeTextInputElement onSubmit - | otherIO -> - Html.span (string otherIO) - | CompositeHeader.Component _ | CompositeHeader.Parameter _ | CompositeHeader.Factor _ | CompositeHeader.Characteristic _ -> - Html.span header.AsButtonName - | _ -> Html.span (string header) + match iotype with + | IOType.FreeText s -> + let onSubmit = fun (v: string) -> + let header = IOType.FreeText v + setIO header + FreeTextInputElement onSubmit + | _ -> + Html.div [ + prop.onClick (fun e -> e.stopPropagation(); setIO iotype) + prop.onKeyDown(fun k -> if (int k.which) = 13 then setIO iotype) + prop.text (iotype.ToString()) + ] ] ] /// Main column types subpage for dropdown let dropdownContentMain state setState (model:Model) dispatch = [ - DropdownPage.IOTypes (CompositeHeader.Input, CompositeHeader.InputEmpty.AsButtonName) |> createSubBuildingBlockDropdownLink state setState + DropdownPage.IOTypes BuildingBlock.HeaderCellType.Input |> createSubBuildingBlockDropdownLink state setState Bulma.dropdownDivider [] - CompositeHeader.ParameterEmpty |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.FactorEmpty |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.CharacteristicEmpty |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.ComponentEmpty |> createBuildingBlockDropdownItem model dispatch state setState - Model.BuildingBlock.DropdownPage.More |> createSubBuildingBlockDropdownLink state setState + BuildingBlock.HeaderCellType.Parameter |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.Factor |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.Characteristic |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.Component |> createBuildingBlockDropdownItem model dispatch setState + Model.BuildingBlock.DropdownPage.More |> createSubBuildingBlockDropdownLink state setState Bulma.dropdownDivider [] - DropdownPage.IOTypes (CompositeHeader.Output, CompositeHeader.OutputEmpty.AsButtonName) |> createSubBuildingBlockDropdownLink state setState + DropdownPage.IOTypes BuildingBlock.HeaderCellType.Output |> createSubBuildingBlockDropdownLink state setState Bulma.dropdownItem.div [ prop.style [style.textAlign.right] prop.children annotationsPrinciplesUrl @@ -158,33 +135,21 @@ module private DropdownElements = ] /// Protocol Type subpage for dropdown - let dropdownContentProtocolTypeColumns state setState state_search setState_search (model:Model) dispatch = + let dropdownContentProtocolTypeColumns state setState (model:Model) dispatch = [ - // Heading - //Bulma.dropdownItem.div [ - // prop.style [style.textAlign.center] - // prop.children [ - // Html.h6 [ - // prop.className "subtitle" - // prop.style [style.fontWeight.bold] - // prop.text BuildingBlock.DropdownPage.More.toString - // ] - // ] - //] - //Bulma.dropdownDivider [] - CompositeHeader.Date |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.Performer |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.ProtocolDescription |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.ProtocolREF |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.ProtocolType |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.ProtocolUri |> createBuildingBlockDropdownItem model dispatch state setState - CompositeHeader.ProtocolVersion |> createBuildingBlockDropdownItem model dispatch state setState + BuildingBlock.HeaderCellType.Date |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.Performer |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.ProtocolDescription |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.ProtocolREF |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.ProtocolType |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.ProtocolUri |> createBuildingBlockDropdownItem model dispatch setState + BuildingBlock.HeaderCellType.ProtocolVersion |> createBuildingBlockDropdownItem model dispatch setState // Navigation element back to main page - backToMainDropdownButton state setState + backToMainDropdownButton setState ] /// Output columns subpage for dropdown - let dropdownContentIOTypeColumns (createHeaderFunc: IOType -> CompositeHeader) state setState (model:Model) dispatch = + let dropdownContentIOTypeColumns header state setState (model:Model) dispatch = [ // Heading //Bulma.dropdownItem.div [ @@ -198,19 +163,18 @@ module private DropdownElements = // ] //] //Bulma.dropdownDivider [] - createHeaderFunc IOType.Source |> createBuildingBlockDropdownItem model dispatch state setState - createHeaderFunc IOType.Sample |> createBuildingBlockDropdownItem model dispatch state setState - createHeaderFunc IOType.Material |> createBuildingBlockDropdownItem model dispatch state setState - createHeaderFunc IOType.RawDataFile |> createBuildingBlockDropdownItem model dispatch state setState - createHeaderFunc IOType.DerivedDataFile |> createBuildingBlockDropdownItem model dispatch state setState - createHeaderFunc IOType.ImageFile |> createBuildingBlockDropdownItem model dispatch state setState - createHeaderFunc (IOType.FreeText "") |> createBuildingBlockDropdownItem model dispatch state setState + IOType.Source |> createIOTypeDropdownItem model dispatch setState header + IOType.Sample |> createIOTypeDropdownItem model dispatch setState header + IOType.Material |> createIOTypeDropdownItem model dispatch setState header + IOType.RawDataFile |> createIOTypeDropdownItem model dispatch setState header + IOType.DerivedDataFile |> createIOTypeDropdownItem model dispatch setState header + IOType.ImageFile |> createIOTypeDropdownItem model dispatch setState header + IOType.FreeText "" |> createIOTypeDropdownItem model dispatch setState header // Navigation element back to main page - backToMainDropdownButton state setState + backToMainDropdownButton setState ] -let Main state setState state_search setState_search (model: Model) dispatch = - let state_bb = model.AddBuildingBlockState +let Main state setState (model: Model) dispatch = Bulma.control.div [ Bulma.dropdown [ if state.DropdownIsActive then Bulma.dropdown.isActive //Dropdown.IsActive model.AddBuildingBlockState.ShowBuildingBlockSelection @@ -221,7 +185,7 @@ let Main state setState state_search setState_search (model: Model) dispatch = prop.children [ Html.span [ prop.style [style.marginRight 5] - prop.text (state_bb.Header.AsButtonName) + prop.text (model.AddBuildingBlockState.HeaderCellType.ToString()) ] Bulma.icon [ Html.i [ prop.className "fa-solid fa-angle-down" @@ -234,9 +198,9 @@ let Main state setState state_search setState_search (model: Model) dispatch = | Model.BuildingBlock.DropdownPage.Main -> DropdownElements.dropdownContentMain state setState model dispatch | Model.BuildingBlock.DropdownPage.More -> - DropdownElements.dropdownContentProtocolTypeColumns state setState state_search setState_search model dispatch - | Model.BuildingBlock.DropdownPage.IOTypes (createHeader,_) -> - DropdownElements.dropdownContentIOTypeColumns createHeader state setState model dispatch + DropdownElements.dropdownContentProtocolTypeColumns state setState model dispatch + | Model.BuildingBlock.DropdownPage.IOTypes iotype -> + DropdownElements.dropdownContentIOTypeColumns iotype state setState model dispatch |> fun content -> Bulma.dropdownContent [ prop.children content ] ] ] diff --git a/src/Client/Pages/BuildingBlock/Helper.fs b/src/Client/Pages/BuildingBlock/Helper.fs index a9769053..b23d4f83 100644 --- a/src/Client/Pages/BuildingBlock/Helper.fs +++ b/src/Client/Pages/BuildingBlock/Helper.fs @@ -7,38 +7,42 @@ open Messages open ARCtrl.ISA open Model.BuildingBlock -let createCellFromUiStateAndOA (uiState: BuildingBlockUIState) (oa:OntologyAnnotation) = - match uiState.BodyCellType with - | BodyCellType.Text -> CompositeCell.createFreeText oa.NameText - | BodyCellType.Term -> CompositeCell.createTerm oa - | BodyCellType.Unitized -> CompositeCell.createUnitized("", oa) +let isSameMajorHeaderCellType (hct1: BuildingBlock.HeaderCellType) (hct2: BuildingBlock.HeaderCellType) = + (hct1.IsTermColumn() = hct2.IsTermColumn()) + && (hct1.HasIOType() = hct2.HasIOType()) + +let selectHeaderCellType (hct: BuildingBlock.HeaderCellType) setUiState dispatch = + BuildingBlock.UpdateHeaderCellType hct |> BuildingBlockMsg |> dispatch + { DropdownPage = DropdownPage.Main; DropdownIsActive = false }|> setUiState + +open Fable.Core -let selectHeader (uiState: BuildingBlockUIState) (setUiState: BuildingBlockUIState -> unit) (nextHeader: CompositeHeader) = - let nextState, updateBodyCellMsg = - match nextHeader.IsTermColumn, uiState.BodyCellType with - | true, BodyCellType.Term | true, BodyCellType.Unitized -> - { uiState with DropdownPage = DropdownPage.Main; DropdownIsActive = false }, - Msg.DoNothing - | true, BodyCellType.Text -> - { BodyCellType = BodyCellType.Term; DropdownPage = DropdownPage.Main; DropdownIsActive = false }, - Msg.DoNothing - | false, _ -> - { BodyCellType = BodyCellType.Text; DropdownPage = DropdownPage.Main; DropdownIsActive = false }, - Msg.DoNothing - setUiState nextState - Msg.Batch [ - BuildingBlock.Msg.SelectHeader nextHeader |> BuildingBlockMsg - updateBodyCellMsg - ] - -let selectBody (body: CompositeCell) = - BuildingBlock.Msg.SelectBodyCell (Some body) |> BuildingBlockMsg - -let hasVerifiedTermHeader (header: CompositeHeader) = header.IsTermColumn && header.ToTerm().TermAccessionShort <> "" - -let hasVerifiedCell (cell: CompositeCell) = (cell.isTerm || cell.isUnitized) && cell.ToOA().TermAccessionShort <> "" +let createCompositeHeaderFromState (state: BuildingBlock.Model) = + let getOA() = state.TryHeaderOA() |> Option.defaultValue OntologyAnnotation.empty + let getIOType() = state.TryHeaderIO() |> Option.defaultValue (IOType.FreeText "") + match state.HeaderCellType with + | HeaderCellType.Component -> CompositeHeader.Component <| getOA() + | HeaderCellType.Characteristic -> CompositeHeader.Characteristic <| getOA() + | HeaderCellType.Factor -> CompositeHeader.Factor <| getOA() + | HeaderCellType.Parameter -> CompositeHeader.Parameter <| getOA() + | HeaderCellType.ProtocolType -> CompositeHeader.ProtocolType + | HeaderCellType.ProtocolDescription -> CompositeHeader.ProtocolDescription + | HeaderCellType.ProtocolUri -> CompositeHeader.ProtocolUri + | HeaderCellType.ProtocolVersion -> CompositeHeader.ProtocolVersion + | HeaderCellType.ProtocolREF -> CompositeHeader.ProtocolREF + | HeaderCellType.Performer -> CompositeHeader.Performer + | HeaderCellType.Date -> CompositeHeader.Date + | HeaderCellType.Input -> CompositeHeader.Input <| getIOType() + | HeaderCellType.Output -> CompositeHeader.Output <| getIOType() + +let tryCreateCompositeCellFromState (state: BuildingBlock.Model) = + match state.BodyCellType, state.BodyArg with + | BodyCellType.Term, Some (U2.Case2 oa) -> CompositeCell.createTerm (oa) |> Some + | BodyCellType.Unitized, Some (U2.Case2 oa) -> CompositeCell.createUnitized ("", oa) |> Some + | BodyCellType.Text, Some (U2.Case1 s) -> CompositeCell.createFreeText s |> Some + | _ -> None let isValidColumn (header : CompositeHeader) = header.IsFeaturedColumn || (header.IsTermColumn && header.ToTerm().NameText.Length > 0) - || header.IsSingleColumn \ No newline at end of file + || header.IsSingleColumn \ No newline at end of file diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index 789d63e4..d4f47d3f 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -14,487 +14,115 @@ open Messages open ARCtrl.ISA open BuildingBlock.Helper -module private AutocompleteComponents = +let private termOrUnitizedSwitch (model:Messages.Model) dispatch = + + let state = model.AddBuildingBlockState + Bulma.buttons [ + Bulma.buttons.hasAddons + prop.style [style.flexWrap.nowrap; style.marginBottom 0; style.marginRight (length.rem 1)] + prop.children [ + Bulma.button.a [ + let isActive = state.BodyCellType = BodyCellType.Term + if isActive then Bulma.color.isSuccess + prop.onClick (fun _ -> BuildingBlock.UpdateBodyCellType BodyCellType.Term |> BuildingBlockMsg |> dispatch) + prop.classes ["pr-2 pl-2 mb-0"; if isActive then "is-selected"] + prop.text "Term" + ] + Bulma.button.a [ + let isActive = state.BodyCellType = BodyCellType.Unitized + if isActive then Bulma.color.isSuccess + prop.onClick (fun _ -> BuildingBlock.UpdateBodyCellType BodyCellType.Unitized |> BuildingBlockMsg |> dispatch) + prop.classes ["pr-2 pl-2 mb-0"; if isActive then "is-selected"] + prop.text "Unit" + ] + ] + ] - /// inputId is used to set the value to the input field after selecting term. - let createTermElement_Main (term:Term) selectMsg dispatch = - let id = term.Accession - let hiddenId = sprintf "isHidden_%s" id - let main = - Html.tr [ - prop.key id - prop.onClick (fun _ -> selectMsg()) - prop.onKeyDown (fun k -> if k.key = "Enter" then selectMsg()) - prop.tabIndex 0 - prop.className "suggestion" - prop.children [ - Html.td [ Html.b term.Name ] - Html.td [ - if term.IsObsolete then - Bulma.color.hasTextDanger - prop.text ("obsolete") - ] - Html.td [ - prop.onClick ( fun e -> e.stopPropagation()) - prop.style [style.fontWeight.lighter] - //prop.children [SidebarComponents.AdvancedSearch.createLinkOfAccession term.Accession] - ] - // Cytoscape graph tree view - Html.td [ - Bulma.buttons [ - Bulma.buttons.isRight - prop.children [ - Bulma.button.a [ - prop.title "Show Term Tree" - Bulma.button.isSmall - Bulma.color.isSuccess - Bulma.button.isInverted - prop.onClick(fun e -> - e.preventDefault() - e.stopPropagation() - Cytoscape.Msg.GetTermTree term.Accession |> CytoscapeMsg |> dispatch - ) - prop.children [ - Bulma.icon [ - Html.i [prop.className "fa-solid fa-tree"] - ] - ] - ] - Bulma.button.a [ - Bulma.button.isSmall - Bulma.color.isBlack - Bulma.button.isInverted - prop.onClick(fun e -> - e.preventDefault() - e.stopPropagation() - let ele = Browser.Dom.document.getElementById(hiddenId) - let isCollapsed = - let vis = string ele?style?display - vis = "none" || vis = "" - if isCollapsed then - ele?style?display <- "table-row" - else - ele?style?display <- "none" - () - ) - prop.children [ - Bulma.icon [ - Html.i [prop.className "fa-solid fa-chevron-down"] - ] - ] - ] - ] - ] - ] - ] - ] - let hidden = - Html.tr [ - prop.onClick (fun e -> e.stopPropagation()) - prop.id hiddenId - prop.key hiddenId - prop.className "suggestion-details" - prop.children [ - Html.td [ - prop.colSpan 4 - prop.children [ - Bulma.content [ - Html.b "Definition: " - Html.p (if term.Description = "" then "No definition found" else term.Description) +open Fable.Core + +let private SearchBuildingBlockBodyElement (model: Messages.Model) dispatch = + let id = "SearchBuildingBlockBodyElementID" + let element = React.useElementRef() + React.useEffectOnce(fun _ -> element.current <- Some <| Browser.Dom.document.getElementById(id)) + let width = element.current |> Option.map (fun ele -> length.px ele.clientWidth) + Bulma.field.div [ + prop.id id + prop.style [ style.display.flex; style.justifyContent.spaceBetween ] + prop.children [ + termOrUnitizedSwitch model dispatch + let setter (oaOpt: OntologyAnnotation option) = + let case = oaOpt |> Option.map (fun oa -> !^oa) + BuildingBlock.UpdateBodyArg case |> BuildingBlockMsg |> dispatch + let parent = model.AddBuildingBlockState.TryHeaderOA() + let input = model.AddBuildingBlockState.TryBodyOA() + Components.TermSearch.Input(setter, dispatch, fullwidth=true, ?input=input, ?parent'=parent, displayParent=false, ?dropdownWidth=width, alignRight=true) + ] + ] + +let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState) setUi (model: Model) dispatch = + let state = model.AddBuildingBlockState + let id = "SearchBuildingBlockHeaderElementID" + let element = React.useElementRef() + React.useEffectOnce(fun _ -> element.current <- Some <| Browser.Dom.document.getElementById(id)) + let width = element.current |> Option.map (fun ele -> length.px ele.clientWidth) + Bulma.field.div [ + prop.id id + Bulma.field.hasAddons + prop.style [style.position.relative] + // Choose building block type dropdown element + prop.children [ + // Dropdown building block type choice + Dropdown.Main ui setUi model dispatch + // Term search field + if state.HeaderCellType.HasOA() then + let setter (oaOpt: OntologyAnnotation option) = + let case = oaOpt |> Option.map (fun oa -> !^oa) + BuildingBlock.UpdateHeaderArg case |> BuildingBlockMsg |> dispatch + //selectHeader ui setUi h |> dispatch + let input = model.AddBuildingBlockState.TryHeaderOA() + Components.TermSearch.Input(setter, dispatch, ?input=input, isExpanded=true, fullwidth=true, ?dropdownWidth=width, alignRight=true) + elif state.HeaderCellType.HasIOType() then + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + Bulma.control.p [ + Bulma.input.text [ + Bulma.color.hasBackgroundGreyLighter + prop.readOnly true + prop.valueOrDefault ( + state.TryHeaderIO() + |> Option.get + |> _.ToString() + ) ] ] ] ] - ] - [|main; hidden|] - -// let createAutocompleteSuggestions (termSuggestions: Term []) (selectMsg: Term -> unit) (state: TermSearchUIState) setState (dispatch: Msg -> unit) = -// let suggestions = -// if termSuggestions.Length > 0 then -// termSuggestions -// |> Array.distinctBy (fun t -> t.Accession) -// |> Array.collect (fun t -> -// let msg = fun e -> -// setState {state with SearchIsActive = false} -// selectMsg t -// createTermElement_Main t msg dispatch -// ) -// |> List.ofArray -// else -// [ Html.tr [ Html.td "No terms found matching your input." ] ] - -// let alternative_advancedSearch = -// Html.tr [ -// prop.className "suggestion" -// prop.children [ -// Html.td [ -// prop.colSpan 4 -// prop.children [ -// Html.span "Cant find the Term you are looking for? Try " -// Html.a [ -// prop.onClick (fun _ -> failwith "Not implemented in createAutocompleteSuggestions!"(*AdvancedSearch.ToggleModal autocompleteParams.ModalId |> AdvancedSearchMsg |> dispatch*)) -// prop.text "Advanced Search" -// ] -// Html.span "!" -// ] -// ] -// ] -// ] - -// let alternative_getInContact = -// Html.tr [ -// prop.className "suggestion" -// prop.children [ -// Html.td [ -// prop.colSpan 4 -// prop.children [ -// Html.span "Still can't find what you need? Get in " -// Html.a [ -// prop.href Shared.URLs.Helpdesk.UrlOntologyTopic; -// prop.target "_Blank" -// prop.text "contact" -// ] -// Html.span " with us!" -// ] -// ] -// ] -// ] - -// suggestions @ [alternative_advancedSearch; alternative_getInContact] - -// let closeElement state setState = -// Html.div [ -// prop.style [ -// style.position.fixedRelativeToWindow -// style.left 0 -// style.top 0 -// style.width(length.percent 100) -// style.height(length.percent 100) -// style.zIndex 19 -// style.backgroundColor.transparent -// ] -// prop.onClick(fun _ -> -// setState {state with SearchIsActive = false} -// ) -// ] - -// let autocompleteDropdownComponent (termSuggestions: Term []) selectMsg (state: TermSearchUIState) setState (model:Model) (dispatch: Msg -> unit) = -// let searchResults = createAutocompleteSuggestions termSuggestions selectMsg state setState dispatch -// Html.div [ -// prop.style [style.position.relative] -// prop.children [ -// closeElement state setState -// Html.div [ -// prop.style [ -// style.zIndex 20 -// style.width(length.percent 100) -// style.maxHeight 400 -// style.position.absolute -// style.marginTop(length.rem -0.5) -// style.overflowY.auto -// style.custom("borderWidth", "0 0.5px 0.5px 0.5px") -// style.borderStyle.solid -// ] -// prop.children [ -// Bulma.table [ -// Bulma.table.isFullWidth -// prop.children [ -// if state.SearchIsLoading then -// Html.tbody [ -// prop.style [style.height 75] -// prop.children [ -// Html.tr [ -// Html.td [ -// prop.style [style.textAlign.center] -// prop.children [ -// Modals.Loading.loadingComponent -// Html.br [] -// ] -// ] -// ] -// ] -// ] -// else -// Html.tbody searchResults -// ] -// ] -// ] -// ] -// ] -// ] - -///// if inputId ends with Main we apply autofocus to the element -//let private basicTermSearchElement (inputId: string) (onChangeMsg: string -> unit) (onDoubleClickMsg: string -> unit) (isVerified:bool) (state: TermSearchUIState) setState (valueOrDefault: string) = -// Bulma.control.p [ -// Bulma.control.hasIconsRight -// prop.children [ -// Bulma.input.text [ -// if isVerified then Bulma.color.isSuccess -// prop.id inputId -// prop.key inputId -// prop.autoFocus (inputId.EndsWith "Main") -// prop.placeholder "Start typing to search" -// prop.valueOrDefault valueOrDefault -// prop.onDoubleClick (fun (e: Browser.Types.MouseEvent) -> -// let v: string = e.target?value -// v |> onDoubleClickMsg -// ) -// prop.onKeyDown(fun (e: Browser.Types.KeyboardEvent) -> -// match e.which with -// | 27. -> // Escape -// setState {state with SearchIsActive = false} -// | _ -> () -// ) -// prop.onChange (fun (e: string) -> -// onChangeMsg e -// ) -// ] -// if isVerified then -// Bulma.icon [ -// Bulma.icon.isRight -// Bulma.icon.isSmall -// prop.className "fas fa-check" -// ] -// ] -// ] - -//let header_searchElement inputId (ui: BuildingBlockUIState) setUi (ui_search:TermSearchUIState) setUi_search (state: BuildingBlock.Model) dispatch = -// let onChangeMsg = fun (v:string) -> -// let triggerNewSearch = v.Length > 2 -// let h = state.Header.UpdateWithOA (OntologyAnnotation.fromString v) -// selectHeader ui setUi h |> dispatch -// if triggerNewSearch then -// setUi_search { SearchIsActive = true; SearchIsLoading = true } -// let msg = BuildingBlock.Msg.GetHeaderSuggestions (v, {state = ui_search; setState = setUi_search}) |> BuildingBlockMsg -// let bounce_msg = Bounce (System.TimeSpan.FromSeconds 0.5,nameof(msg), msg) -// bounce_msg |> dispatch -// Bulma.control.div [ -// Bulma.control.isExpanded -// prop.children [ -// let valueOrDefault = state.Header.ToTerm().NameText -// basicTermSearchElement inputId onChangeMsg onChangeMsg (hasVerifiedTermHeader state.Header) ui_search setUi_search valueOrDefault -// ] -// ] - -//let private chooseBuildingBlock_element (ui: BuildingBlockUIState) setUi (ui_search:TermSearchUIState) setUi_search (model: Model) dispatch = -// let state = model.AddBuildingBlockState -// let inputId = "BuildingBlock_InputMain" -// Bulma.field.div [ -// Bulma.field.div [ -// Bulma.field.hasAddons -// // Choose building block type dropdown element -// prop.children [ -// // Dropdown building block type choice -// Dropdown.Main ui setUi ui_search setUi_search model dispatch -// // Term search field -// if state.Header.IsTermColumn && not state.Header.IsFeaturedColumn then -// header_searchElement inputId ui setUi ui_search setUi_search state dispatch -// elif state.Header.IsIOType then -// Bulma.control.div [ -// Bulma.control.isExpanded -// prop.children [ -// Bulma.control.p [ -// Bulma.input.text [ -// Bulma.color.hasBackgroundGreyLighter -// prop.readOnly true -// prop.valueOrDefault ( -// state.Header.TryIOType() -// |> Option.get -// |> _.ToString() -// ) -// ] -// ] -// ] -// ] -// ] -// ] -// // Ontology Term search preview -// if ui_search.SearchIsActive then -// let selectMsg = fun (term:Term) -> -// let oa = OntologyAnnotation.fromTerm term -// let h = model.AddBuildingBlockState.Header.UpdateWithOA oa -// selectHeader ui setUi h |> dispatch -// AutocompleteComponents.autocompleteDropdownComponent model.AddBuildingBlockState.HeaderSearchResults selectMsg ui_search setUi_search model dispatch -// ] - -//module private BodyTerm = - -// let private termOrUnit_switch (uiState: BuildingBlockUIState) setUiState = - -// Bulma.buttons [ -// Bulma.buttons.hasAddons -// prop.style [style.flexWrap.nowrap; style.marginBottom 0; style.marginRight (length.rem 1)] -// prop.children [ -// Bulma.button.a [ -// let isActive = uiState.BodyCellType = BodyCellType.Term -// if isActive then Bulma.color.isSuccess -// prop.onClick (fun _ -> {uiState with BodyCellType = BodyCellType.Term } |> setUiState) -// prop.classes ["pr-2 pl-2 mb-0"; if isActive then "is-selected"] -// prop.text "Term" -// ] -// Bulma.button.a [ -// let isActive = uiState.BodyCellType = BodyCellType.Unitized -// if isActive then Bulma.color.isSuccess -// prop.onClick (fun _ -> {uiState with BodyCellType = BodyCellType.Unitized } |> setUiState) -// prop.classes ["pr-2 pl-2 mb-0"; if isActive then "is-selected"] -// prop.text "Unit" -// ] -// ] -// ] - -// let private isDirectedSearch_toggle (state: bool) setState = -// Bulma.control.div [ -// Bulma.button.a [ -// prop.title "Toggle child search" -// if state then Bulma.color.isSuccess -// prop.onClick (fun _ -> not state |> setState) -// prop.children [ -// Bulma.icon [Html.i [prop.className "fa-solid fa-diagram-project"]] -// ] -// ] -// ] - -// [] -// let private body_searchElement inputId state setState (model: Model) dispatch = -// let state_isDirectedSearchMode, setState_isDirectedSearchMode = React.useState(true) -// let bodyCell = -// model.AddBuildingBlockState.BodyCell -// |> Option.defaultValue CompositeCell.emptyFreeText -// let onChangeMsg = fun (isClicked: bool) (v:string) -> -// let updateSearchState msg = -// setState {SearchIsActive = true; SearchIsLoading = true} -// let bounce_msg = Bounce (System.TimeSpan.FromSeconds 0.5,nameof(msg), msg) -// bounce_msg |> dispatch -// if not isClicked then // only trigger the body cell reset on typing. When clicking to display terms we will not want to loose TAN information -// bodyCell.UpdateWithOA(OntologyAnnotation.fromString(v)) |> selectBody |> dispatch -// match isClicked, state_isDirectedSearchMode, model.AddBuildingBlockState.Header.IsTermColumn, v with -// // only execute this on onDoubleClick event. If executed on onChange event it will trigger when deleting term. -// | true, true, true, "" -> // Search all children -// let parent = model.AddBuildingBlockState.Header.ToTerm().ToTermMinimal() -// BuildingBlock.Msg.GetBodyTermsByParent (parent, {state = state; setState = setState}) |> BuildingBlockMsg -// |> updateSearchState -// | _, true, true, any when any.Length > 2 -> -// let parent = model.AddBuildingBlockState.Header.ToTerm().ToTermMinimal() -// BuildingBlock.Msg.GetBodySuggestionsByParent (v,parent,{state = state; setState = setState}) |> BuildingBlockMsg -// |> updateSearchState -// | _,_,_, any when any.Length > 2 -> -// BuildingBlock.Msg.GetBodySuggestions (v, {state = state; setState = setState}) |> BuildingBlockMsg -// |> updateSearchState -// | _ -> -// () -// Bulma.field.div [ -// Bulma.field.hasAddons -// prop.style [style.flexGrow 1 ] -// // Choose building block type dropdown element -// prop.children [ -// // Dropdown building block type choice -// isDirectedSearch_toggle state_isDirectedSearchMode setState_isDirectedSearchMode -// // Term search field -// Bulma.control.div [ -// Bulma.control.isExpanded -// if state_isDirectedSearchMode && not model.AddBuildingBlockState.Header.IsTermColumn then -// prop.title "No parent term selected" -// elif state_isDirectedSearchMode && hasVerifiedTermHeader model.AddBuildingBlockState.Header && model.AddBuildingBlockState.BodySearchText = "" then -// prop.title "Double click to show all children" -// prop.style [ -// // display box-shadow if term search is fully activated -// if state_isDirectedSearchMode && hasVerifiedTermHeader model.AddBuildingBlockState.Header then style.boxShadow(2,2,NFDIColors.Mint.Lighter20) -// ] -// prop.children [ -// let valueOrDefault = model.AddBuildingBlockState.BodyCell |> Option.map (fun cc -> cc.ToTerm().NameText) |> Option.defaultValue "" -// let hasVerified = model.AddBuildingBlockState.BodyCell |> Option.map (fun cc -> hasVerifiedCell cc) |> Option.defaultValue false -// basicTermSearchElement inputId (onChangeMsg false) (onChangeMsg true) hasVerified state setState valueOrDefault -// ] -// ] -// ] -// ] - -// let Main uiState setState searchState setSearchState model dispatch = -// let inputId = "BuildingBlock_BodyInput" -// Bulma.field.div [ -// Bulma.field.div [ -// prop.style [ style.display.flex; style.justifyContent.spaceBetween ] -// prop.children [ -// termOrUnit_switch uiState setState -// body_searchElement inputId searchState setSearchState model dispatch -// ] -// ] -// // Ontology Term search preview -// if searchState.SearchIsActive then -// let selectMsg = fun (term:Term) -> -// let oa = OntologyAnnotation.fromTerm term -// let cell = createCellFromUiStateAndOA uiState oa -// BuildingBlock.SelectBodyCell (Some cell) |> BuildingBlockMsg |> dispatch -// AutocompleteComponents.autocompleteDropdownComponent model.AddBuildingBlockState.BodySearchResults selectMsg searchState setSearchState model dispatch -// ] - -//let private add_button (ui: BuildingBlockUIState) (model: Model) dispatch = -// let state = model.AddBuildingBlockState -// Bulma.field.div [ -// Bulma.button.button [ -// let header = state.Header -// let body = state.BodyCell -// let isValid = Helper.isValidColumn header -// if isValid then -// Bulma.color.isSuccess -// Bulma.button.isActive -// else -// Bulma.color.isDanger -// prop.disabled true -// Bulma.button.isFullWidth -// prop.onClick (fun _ -> -// let column = CompositeColumn.create(header, [|if body.IsSome then body.Value|]) -// SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch -// ) -// prop.text "Add Column" -// ] -// ] - -//module private AdvancedSearch = - -// let modal_container (uiState: BuildingBlockUIState) setUiState (model:Model) dispatch = -// Html.span [ -// let selectHeader = fun (term:Term) -> -// let h = model.AddBuildingBlockState.Header.UpdateWithOA(OntologyAnnotation.fromTerm term) -// Msg.Batch [ -// BuildingBlock.UpdateHeaderSearchText term.Name |> BuildingBlockMsg -// selectHeader uiState setUiState h -// ] -// let selectBody = fun (term:Term) -> -// let oa = OntologyAnnotation.fromTerm term -// let cell = createCellFromUiStateAndOA uiState oa -// Msg.Batch [ -// BuildingBlock.UpdateBodySearchText term.Name |> BuildingBlockMsg -// BuildingBlock.SelectBodyCell (Some cell) |> BuildingBlockMsg -// ] -// // added edge case to modal, where relatedInputId = "" is ignored -// SidebarComponents.AdvancedSearch.advancedSearchModal model AdvancedSearch.Model.BuildingBlockHeaderId "" dispatch selectHeader -// SidebarComponents.AdvancedSearch.advancedSearchModal model AdvancedSearch.Model.BuildingBlockBodyId "" dispatch selectBody -// ] + ] + ] -// let links_container (bb_type: CompositeHeader) dispatch = -// Html.div [ -// if not bb_type.IsFeaturedColumn then -// Bulma.help [ -// prop.style [style.display.inlineElement] -// prop.children [ -// Html.a [ -// prop.onClick (fun _ -> AdvancedSearch.ToggleModal AdvancedSearch.Model.BuildingBlockHeaderId |> AdvancedSearchMsg |> dispatch) -// prop.text "Use advanced search header" -// ] -// ] -// ] -// Bulma.help [ -// prop.style [style.display.inlineElement; style.float'.right] -// prop.children [ -// Html.a [ -// prop.onClick (fun _ -> AdvancedSearch.ToggleModal AdvancedSearch.Model.BuildingBlockBodyId |> AdvancedSearchMsg |> dispatch) -// prop.text "Use advanced search body" -// ] -// ] -// ] -// ] +let private addBuildingBlockButton (model: Model) dispatch = + let state = model.AddBuildingBlockState + Bulma.field.div [ + Bulma.button.button [ + let header = Helper.createCompositeHeaderFromState state + let body = Helper.tryCreateCompositeCellFromState state + let isValid = Helper.isValidColumn header + if isValid then + Bulma.color.isSuccess + Bulma.button.isActive + else + Bulma.color.isDanger + prop.disabled true + Bulma.button.isFullWidth + prop.onClick (fun _ -> + let column = CompositeColumn.create(header, [|if body.IsSome then body.Value|]) + SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch + ) + prop.text "Add Column" + ] + ] [] let Main (model: Model) dispatch = @@ -502,11 +130,10 @@ let Main (model: Model) dispatch = //let state_searchHeader, setState_searchHeader = React.useState(TermSearchUIState.init) //let state_searchBody, setState_searchBody = React.useState(TermSearchUIState.init) mainFunctionContainer [ - Html.span "not implemented" - //chooseBuildingBlock_element state_bb setState_bb state_searchHeader setState_searchHeader model dispatch - //if model.AddBuildingBlockState.Header.IsTermColumn then - // BodyTerm.Main state_bb setState_bb state_searchBody setState_searchBody model dispatch - // AdvancedSearch.modal_container state_bb setState_bb model dispatch - // AdvancedSearch.links_container model.AddBuildingBlockState.Header dispatch - //add_button state_bb model dispatch + SearchBuildingBlockHeaderElement state_bb setState_bb model dispatch + if model.AddBuildingBlockState.HeaderCellType.IsTermColumn() then + SearchBuildingBlockBodyElement model dispatch + //AdvancedSearch.modal_container state_bb setState_bb model dispatch + //AdvancedSearch.links_container model.AddBuildingBlockState.Header dispatch + addBuildingBlockButton model dispatch ] diff --git a/src/Client/Pages/TermSearch/TermSearchView.fs b/src/Client/Pages/TermSearch/TermSearchView.fs index 6661d4da..c8cf860c 100644 --- a/src/Client/Pages/TermSearch/TermSearchView.fs +++ b/src/Client/Pages/TermSearch/TermSearchView.fs @@ -108,7 +108,9 @@ let Main (model:Messages.Model, dispatch) = Bulma.label "Search for an ontology term to fill into the selected field(s)" mainFunctionContainer [ - Components.TermSearch.Input(setTerm, fullwidth=true, size=Bulma.input.isLarge, ?parent'=model.TermSearchState.ParentTerm, showAdvancedSearch=true) + Bulma.field.div [ + Components.TermSearch.Input(setTerm, dispatch, fullwidth=true, size=Bulma.input.isLarge, ?parent'=model.TermSearchState.ParentTerm, showAdvancedSearch=true) + ] addButton(model, dispatch) ] diff --git a/src/Client/SharedComponents/AdvancedSearch.fs b/src/Client/SharedComponents/AdvancedSearch.fs index 8d9e94f9..608b7a34 100644 --- a/src/Client/SharedComponents/AdvancedSearch.fs +++ b/src/Client/SharedComponents/AdvancedSearch.fs @@ -1,4 +1,4 @@ -module SharedComponents.AdvancedSearch +module Components.AdvancedSearch open Fable.React open Fable.React.Props @@ -18,15 +18,14 @@ open AdvancedSearch open Messages -let private StartAdvancedSearch (state: AdvancedSearch.Model) setState = - async { - let! terms = Api.api.getTermsForAdvancedSearch state.AdvancedSearchOptions +let private StartAdvancedSearch (state: AdvancedSearch.Model) setState dispatch = + let setter (terms: Term []) = setState { state with AdvancedSearchTermResults = terms Subpage = AdvancedSearch.AdvancedSearchSubpages.ResultsSubpage } - } + AdvancedSearch.Msg.GetSearchResults {|config=state.AdvancedSearchOptions; responseSetter = setter|} |> AdvancedSearchMsg |> dispatch let private createLinkOfAccession (accession:string) = a [ @@ -277,7 +276,7 @@ let private keepObsoleteCheckradioElement (state:AdvancedSearch.Model) setState ] ] -let private inputFormPage (state:AdvancedSearch.Model) (setState:AdvancedSearch.Model -> unit) = +let private inputFormPage (state:AdvancedSearch.Model) (setState:AdvancedSearch.Model -> unit) dispatch = Html.div [ Bulma.field.div [ Bulma.label "Term name keywords:" @@ -303,7 +302,7 @@ let private inputFormPage (state:AdvancedSearch.Model) (setState:AdvancedSearch Subpage = AdvancedSearchSubpages.ResultsSubpage HasAdvancedSearchResultsLoading = true } - StartAdvancedSearch state setState |> Async.StartImmediate + StartAdvancedSearch state setState dispatch | _ -> () ) ] @@ -326,7 +325,7 @@ let private inputFormPage (state:AdvancedSearch.Model) (setState:AdvancedSearch e.stopPropagation(); let isValid = isValidAdancedSearchOptions state.AdvancedSearchOptions if isValid then - StartAdvancedSearch state setState |> Async.StartImmediate + StartAdvancedSearch state setState dispatch | _ -> () ) prop.valueOrDefault state.AdvancedSearchOptions.TermDefinition @@ -391,7 +390,7 @@ let private resultsPage (resultHandler: Term -> unit) (state: AdvancedSearch.Mod ] [] -let Main (isActive: bool, setIsActive: bool -> unit, resultHandler: Term -> unit) = +let Main (isActive: bool, setIsActive: bool -> unit, resultHandler: Term -> unit, dispatch) = let state, setState = React.useState(AdvancedSearch.Model.init) React.useEffect( (fun _ -> AdvancedSearch.Model.init() |> setState), @@ -423,7 +422,7 @@ let Main (isActive: bool, setIsActive: bool -> unit, resultHandler: Term -> unit match state.Subpage with | AdvancedSearchSubpages.InputFormSubpage -> // we need to propagate the modal id here, so we can use meaningful and UNIQUE ids to the checkradio id's - inputFormPage state setState + inputFormPage state setState dispatch | AdvancedSearchSubpages.ResultsSubpage -> resultsPage resultHandler state setState ] @@ -458,7 +457,7 @@ let Main (isActive: bool, setIsActive: bool -> unit, resultHandler: Term -> unit prop.onClick (fun e -> e.preventDefault() e.stopPropagation(); - StartAdvancedSearch state setState |> Async.StartImmediate + StartAdvancedSearch state setState dispatch ) prop.text "Start advanced search" ] diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 07762f92..0df72acb 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -230,7 +230,7 @@ type TermSearch = ] ] - static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool) = + static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool, width: Styles.ICssUnit, alignRight) = let searchesAreComplete = searchNameState.SearchIs = SearchIs.Done && searchTreeState.SearchIs = SearchIs.Done let foundInBoth (term:TermTypes.Term) = (searchTreeState.Results |> Array.contains term) @@ -260,6 +260,7 @@ type TermSearch = Html.div [ prop.id id prop.classes ["term-select-area"; if not show then "is-hidden";] + prop.style [style.width width; if alignRight then style.right 0] prop.children [ yield! matchSearchState searchNameState false yield! matchSearchState searchTreeState true @@ -267,7 +268,16 @@ type TermSearch = ] [] - static member Input (setter: OntologyAnnotation option -> unit, ?input: OntologyAnnotation, ?parent': OntologyAnnotation, ?label: string, ?fullwidth: bool, ?size: IReactProperty, ?showAdvancedSearch: bool) = + static member Input ( + setter: OntologyAnnotation option -> unit, dispatch, + ?input: OntologyAnnotation, ?parent': OntologyAnnotation, + ?showAdvancedSearch: bool, + ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?dropdownWidth: Styles.ICssUnit, ?alignRight: bool, ?displayParent: bool) + = + let displayParent = defaultArg displayParent true + let alignRight = defaultArg alignRight false + let dropdownWidth = defaultArg dropdownWidth (length.perc 100) + let isExpanded = defaultArg isExpanded false let showAdvancedSearch = defaultArg showAdvancedSearch false let advancedSearchActive, setAdvancedSearchActive = React.useState(false) let fullwidth = defaultArg fullwidth false @@ -297,64 +307,64 @@ type TermSearch = setSearchTreeState <| SearchState.init() setIsSearching true React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. - Bulma.field.div [ - prop.style [if fullwidth then style.flexGrow 1] + Bulma.control.div [ + if isExpanded then Bulma.control.isExpanded + if size.IsSome then size.Value + Bulma.control.hasIconsLeft + Bulma.control.hasIconsRight + prop.style [ + if fullwidth then style.flexGrow 1; + ] + if loading then Bulma.control.isLoading prop.children [ - if label.IsSome then Bulma.label label.Value - Bulma.control.div [ + Bulma.input.text [ if size.IsSome then size.Value - Bulma.control.hasIconsLeft - Bulma.control.hasIconsRight - prop.style [if fullwidth then style.flexGrow 1; style.position.relative] - if loading then Bulma.control.isLoading + if state.IsSome then prop.valueOrDefault state.Value.NameText + prop.onDoubleClick(fun e -> + let s : string = e.target?value + if s.Trim() = "" && parent.IsSome && parent.Value.TermAccessionShort <> "" then // trigger get all by parent search + startSearch(None) + allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) + elif s.Trim() <> "" then + startSearch (Some s) + mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) + else + () + ) + prop.onChange(fun (s: string) -> + if s.Trim() = "" then + startSearch(None) + stopSearch() + else + startSearch (Some s) + mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 1000) + ) + prop.onKeyDown(key.escape, fun _ -> stopSearch()) + ] + TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching, dropdownWidth, alignRight) + Components.searchIcon + if state.IsSome && state.Value.Name.IsSome && state.Value.TermAccessionNumber.IsSome && not isSearching then Components.verifiedIcon + // Optional elements + Html.div [ + prop.classes ["is-flex"] prop.children [ - Bulma.input.text [ - if size.IsSome then size.Value - if state.IsSome then prop.valueOrDefault state.Value.NameText - prop.onDoubleClick(fun e -> - let s : string = e.target?value - if s.Trim() = "" && parent.IsSome && parent.Value.TermAccessionShort <> "" then // trigger get all by parent search - startSearch(None) - allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) - elif s.Trim() <> "" then - startSearch (Some s) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) - else - () - ) - prop.onChange(fun (s: string) -> - if s.Trim() = "" then - startSearch(None) - stopSearch() - else - startSearch (Some s) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 1000) + if parent.IsSome && displayParent then + Bulma.help [ + Html.span "Parent: " + Html.span $"{parent.Value.NameText}, {parent.Value.TermAccessionShort}" + ] + if showAdvancedSearch then + Components.AdvancedSearch.Main(advancedSearchActive, setAdvancedSearchActive, (fun t -> + setAdvancedSearchActive false + Some t |> selectTerm), + dispatch ) - ] - TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching) - Components.searchIcon - if state.IsSome && state.Value.Name.IsSome && state.Value.TermAccessionNumber.IsSome && not isSearching then Components.verifiedIcon - ] - ] - if parent.IsSome then - Bulma.help [ - Html.span "Parent: " - Html.span $"{parent.Value.NameText}, {parent.Value.TermAccessionShort}" - ] - if showAdvancedSearch then - SharedComponents.AdvancedSearch.Main(advancedSearchActive, setAdvancedSearchActive, fun t -> - setAdvancedSearchActive false - Some t |> selectTerm - ) - Html.div [ - prop.classes ["is-flex"] - prop.children [ Html.a [ prop.onClick(fun e -> e.preventDefault(); e.stopPropagation(); setAdvancedSearchActive true) prop.style [style.custom("marginLeft","auto")] prop.text "Use advanced search" - ] + ] ] - ] + ] ] ] diff --git a/src/Client/Spreadsheet/Sidebar.Controller.fs b/src/Client/Spreadsheet/Sidebar.Controller.fs index e3747bbd..7d7d9ce2 100644 --- a/src/Client/Spreadsheet/Sidebar.Controller.fs +++ b/src/Client/Spreadsheet/Sidebar.Controller.fs @@ -29,7 +29,7 @@ module SidebarControllerAux = let getNextColumnIndex (state: Spreadsheet.Model) = // if cell is selected get column of selected cell we want to insert AFTER if not state.SelectedCells.IsEmpty then - let indexNextToSelected = state.SelectedCells |> Set.toArray |> Array.head |> fst + let indexNextToSelected = state.SelectedCells |> Set.toArray |> Array.head |> fst |> (+) 1 indexNextToSelected else state.ActiveTable.ColumnCount diff --git a/src/Client/Update.fs b/src/Client/Update.fs index f3df6bd2..7f614aeb 100644 --- a/src/Client/Update.fs +++ b/src/Client/Update.fs @@ -37,6 +37,20 @@ let urlUpdate (route: Route option) (currentModel:Model) : Model * Cmd = + match msg with + | AdvancedSearch.GetSearchResults content -> + let cmd = + Cmd.OfAsync.either + Api.api.getTermsForAdvancedSearch + content.config + (fun terms -> Run (fun _ -> content.responseSetter terms)) + (curry GenericError Cmd.none >> DevMsg) + + model, cmd + module Dev = let update (devMsg: DevMsg) (currentState:DevState) : DevState * Cmd = @@ -425,6 +439,9 @@ let update (msg : Msg) (model : Model) : Model * Cmd = let innerUpdate (msg: Msg) (currentModel: Model) = match msg with | DoNothing -> currentModel,Cmd.none + | Run callback -> + callback() + model, Cmd.none | UpdateHistory next -> {model with History = next}, Cmd.none | TestMyAPI -> let cmd = @@ -524,6 +541,10 @@ let update (msg : Msg) (model : Model) : Model * Cmd = TermSearchState = nextTermSearchState } nextModel,nextCmd + | AdvancedSearchMsg msg -> + let nextModel, cmd = AdvancedSearch.update msg model + + nextModel, cmd | DevMsg msg -> let nextDevState,nextCmd = currentModel.DevState |> Dev.update msg diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index f9c15a52..b05637a8 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -75,6 +75,14 @@ module Extensions = | _ -> this + member this.TryOA() = + match this with + | CompositeHeader.Component oa -> Some oa + | CompositeHeader.Parameter oa -> Some oa + | CompositeHeader.Characteristic oa -> Some oa + | CompositeHeader.Factor oa -> Some oa + | _ -> None + type CompositeCell with member this.UpdateWithOA(oa:OntologyAnnotation) = match this with