From 45966378a3f4bf9e7dc75434fe48d7cc16ef57ab Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 15:27:14 +0100 Subject: [PATCH 001/135] Add term search to all ontology annotation forms :sparkles: #345 --- build/Build.fs | 3 + src/Client/Client.fs | 2 +- src/Client/Helper.fs | 50 +- src/Client/MainComponents/Metadata/Assay.fs | 19 +- src/Client/MainComponents/Metadata/Forms.fs | 53 +- src/Client/Messages.fs | 1 + .../Pages/BuildingBlock/BuildingBlockView.fs | 9 + src/Client/Pages/BuildingBlock/Dropdown.fs | 30 +- .../Pages/BuildingBlock/SearchComponent.fs | 26 +- src/Client/Pages/TermSearch/TermSearchView.fs | 2 +- .../SharedComponents/TermSearchInput.fs | 42 +- src/Client/Views/MainWindowView.fs | 4 +- src/Client/Views/SplitWindowView.fs | 10 +- src/Client/Views/XlsxFileView.fs | 3 +- src/Client/style.scss | 922 +++++++++--------- 15 files changed, 632 insertions(+), 544 deletions(-) diff --git a/build/Build.fs b/build/Build.fs index 499b22c4..f8f5619c 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -422,6 +422,9 @@ let main args = match a with | "create-file" :: version :: a -> ReleaseNoteTasks.createVersionFile(version); 0 | _ -> runOrDefault args + | "cmdtest" :: a -> + Git.Commit.exec "." (sprintf "Release v%s" ProjectInfo.prereleaseTag) + 0 | _ -> runOrDefault args \ No newline at end of file diff --git a/src/Client/Client.fs b/src/Client/Client.fs index f6bb79bc..b8692740 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -17,7 +17,7 @@ let sayHello name = $"Hello {name}" open Feliz let private split_container model dispatch = - let mainWindow = Seq.singleton <| MainWindowView.Main model dispatch + let mainWindow = Seq.singleton <| MainWindowView.Main (model, dispatch) let sideWindow = Seq.singleton <| SidebarView.SidebarView model dispatch SplitWindowView.Main mainWindow diff --git a/src/Client/Helper.fs b/src/Client/Helper.fs index 9803ffb3..79e17b01 100644 --- a/src/Client/Helper.fs +++ b/src/Client/Helper.fs @@ -15,8 +15,8 @@ let debounce<'T> (storage:Dictionary) (key: string) (timeout: int) let key = key // fn.ToString() // Cancel previous debouncer match storage.TryGetValue(key) with - | true, timeoutId -> printfn "CLEAR"; Fable.Core.JS.clearTimeout timeoutId - | _ -> printfn "Not clear";() + | true, timeoutId -> Fable.Core.JS.clearTimeout timeoutId + | _ -> () // Create a new timeout and memoize it let timeoutId = @@ -50,4 +50,48 @@ let debouncel<'T> (storage:Dictionary) (key: string) (timeout: int) timeout storage.[key] <- timeoutId -let newDebounceStorage = fun () -> Dictionary(HashIdentity.Structural) \ No newline at end of file +let newDebounceStorage = fun () -> Dictionary(HashIdentity.Structural) + +module React = + open Fable.Core + open Browser.Types + open Feliz + open Fable.Core.JsInterop + + type ElementSize = { + Width: float + Height: float + } with + static member init() = { + Width = 0. + Height = 0. + } + + [] + let (!?) (opt: 't option) (property: string) : obj = nativeOnly + + let useElementSize () = + let initialValue : HTMLElement option = None + let ref, setRef = React.useState(initialValue) + // https://usehooks-ts.com/react-hook/use-element-size + // Mutable values like 'ref.current' aren't valid dependencies + // because mutating them doesn't re-render the component. + // Instead, we use a state as a ref to be reactive. + let size, setSize = React.useState(ElementSize.init()) + // Prevent too many rendering using useCallback + let handleSize = + React.useCallback( + (fun () -> + setSize { + Width = ref |> Option.map (fun r -> r.offsetWidth) |> Option.defaultValue 0. + Height = ref |> Option.map (fun r -> r.offsetHeight) |> Option.defaultValue 0. + } + ), + [| !? ref "offsetWidth"; !? ref "offsetHeight" |] + ) + React.useLayoutEffect((fun () -> handleSize()), [|!? ref "offsetWidth"; !? ref "offsetHeight"|]) + size, + (fun (ele: Element) -> + setRef (ele :?> HTMLElement |> Some) + ) + diff --git a/src/Client/MainComponents/Metadata/Assay.fs b/src/Client/MainComponents/Metadata/Assay.fs index 99878715..33a405f0 100644 --- a/src/Client/MainComponents/Metadata/Assay.fs +++ b/src/Client/MainComponents/Metadata/Assay.fs @@ -6,6 +6,7 @@ open Messages open ARCtrl.ISA open Shared +[] let Main(assay: ArcAssay, model: Messages.Model, dispatch: Msg -> unit) = Bulma.section [ FormComponents.TextInput ( @@ -18,27 +19,27 @@ let Main(assay: ArcAssay, model: Messages.Model, dispatch: Msg -> unit) = ) FormComponents.OntologyAnnotationInput( assay.MeasurementType |> Option.defaultValue OntologyAnnotation.empty, - "Measurement Type", - fun oa -> + (fun oa -> let oa = if oa = OntologyAnnotation.empty then None else Some oa assay.MeasurementType <- oa - assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), + "Measurement Type" ) FormComponents.OntologyAnnotationInput( assay.TechnologyType |> Option.defaultValue OntologyAnnotation.empty, - "Technology Type", - fun oa -> + (fun oa -> let oa = if oa = OntologyAnnotation.empty then None else Some oa assay.TechnologyType <- oa - assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), + "Technology Type" ) FormComponents.OntologyAnnotationInput( assay.TechnologyPlatform |> Option.defaultValue OntologyAnnotation.empty, - "Technology Platform", - fun oa -> + (fun oa -> let oa = if oa = OntologyAnnotation.empty then None else Some oa assay.TechnologyPlatform <- oa - assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), + "Technology Platform" ) FormComponents.PersonsInput( assay.Performers, diff --git a/src/Client/MainComponents/Metadata/Forms.fs b/src/Client/MainComponents/Metadata/Forms.fs index 5ee4244c..76dd1011 100644 --- a/src/Client/MainComponents/Metadata/Forms.fs +++ b/src/Client/MainComponents/Metadata/Forms.fs @@ -176,7 +176,7 @@ type FormComponents = let fullwidth = defaultArg fullwidth false let loading, setLoading = React.useState(false) let state, setState = React.useState(input) - let debounceStorage, setdebounceStorage = React.useState(newDebounceStorage) + let debounceStorage = React.useRef(newDebounceStorage()) React.useEffect((fun () -> setState input), dependencies=[|box input|]) Bulma.field.div [ prop.style [if fullwidth then style.flexGrow 1] @@ -193,7 +193,7 @@ type FormComponents = prop.valueOrDefault state prop.onChange(fun (e: string) -> setState e - debouncel debounceStorage label 1000 setLoading setter e + debouncel debounceStorage.current label 1000 setLoading setter e ) ] ] @@ -337,28 +337,43 @@ type FormComponents = ] [] - static member OntologyAnnotationInput (input: OntologyAnnotation, label: string, setter: OntologyAnnotation -> unit, ?showTextLabels: bool, ?removebutton: MouseEvent -> unit) = + static member OntologyAnnotationInput (input: OntologyAnnotation, setter: OntologyAnnotation -> unit, ?label: string, ?showTextLabels: bool, ?removebutton: MouseEvent -> unit) = let showTextLabels = defaultArg showTextLabels true let state, setState = React.useState(Helper.OntologyAnnotationMutable.fromOntologyAnnotation input) + let element = React.useElementRef() React.useEffect((fun () -> setState <| Helper.OntologyAnnotationMutable.fromOntologyAnnotation input), dependencies=[|box input|]) - let hasLabel = label <> "" Bulma.field.div [ - if hasLabel then Bulma.label label + if label.IsSome then Bulma.label label.Value Bulma.field.div [ + //prop.ref element + prop.style [style.position.relative] prop.classes ["is-flex"; "is-flex-direction-row"; "is-justify-content-space-between"] prop.children [ Html.div [ prop.classes ["form-container"; if removebutton.IsSome then "pr-2"] prop.children [ - FormComponents.TextInput( - Option.defaultValue "" state.Name, - (if showTextLabels then $"Term Name" else ""), - (fun s -> - let s = if s = "" then None else Some s - state.Name <- s - state.ToOntologyAnnotation() |> setter), - fullwidth = true - ) + Bulma.field.div [ + prop.style [style.flexGrow 1] + prop.children [ + if showTextLabels then Bulma.label $"Term Name" + let innersetter = + fun (oaOpt: OntologyAnnotation option) -> + if oaOpt.IsSome then + setter oaOpt.Value + setState (Helper.OntologyAnnotationMutable.fromOntologyAnnotation oaOpt.Value) + Components.TermSearch.Input( + innersetter, + input=state.ToOntologyAnnotation(), + fullwidth=true, + ?portalTermSelectArea=element.current, + debounceSetter=1000 + ) + ] + ] + Html.div [ + prop.classes ["form-input-term-search-positioner"] + prop.ref element + ] FormComponents.TextInput( Option.defaultValue "" state.TSR, (if showTextLabels then $"TSR" else ""), @@ -397,7 +412,7 @@ type FormComponents = static member OntologyAnnotationsInput (oas: OntologyAnnotation [], label: string, setter: OntologyAnnotation [] -> unit, ?showTextLabels: bool) = FormComponents.InputSequence( oas, OntologyAnnotation.empty, label, setter, - (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,b,c,removebutton=d,?showTextLabels=showTextLabels)) + (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,c,label=b,removebutton=d,?showTextLabels=showTextLabels)) ) [] @@ -629,11 +644,11 @@ type FormComponents = createPersonFieldTextInput(state.Authors, "Authors", fun s -> state.Authors <- s) FormComponents.OntologyAnnotationInput( Option.defaultValue OntologyAnnotation.empty state.Status, - "Status", (fun s -> state.Status <- if s = OntologyAnnotation.empty then None else Some s state.ToPublication() |> setter - ) + ), + "Status" ) FormComponents.CommentsInput( Option.defaultValue [||] state.Comments, @@ -713,11 +728,11 @@ type FormComponents = createFieldTextInput(state.Name, "Name", fun s -> state.Name <- s) FormComponents.OntologyAnnotationInput( Option.defaultValue OntologyAnnotation.empty state.FactorType, - "Status", (fun s -> state.FactorType <- if s = OntologyAnnotation.empty then None else Some s state.ToFactor() |> setter - ) + ), + "Status" ) FormComponents.CommentsInput( Option.defaultValue [||] state.Comments, diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index e20df903..93c4f134 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -88,6 +88,7 @@ module BuildingBlock = open TermSearch type Msg = + | UpdateHeaderWithIO of BuildingBlock.HeaderCellType * IOType | UpdateHeaderCellType of BuildingBlock.HeaderCellType | UpdateHeaderArg of U2 option | UpdateBodyCellType of BuildingBlock.BodyCellType diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index 2b35fe26..f480bea2 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -38,6 +38,15 @@ let update (addBuildingBlockMsg:BuildingBlock.Msg) (state: BuildingBlock.Model) BodyArg = None } nextState, Cmd.none + | UpdateHeaderWithIO (hct, iotype) -> + let nextState = { + state with + HeaderCellType = hct + HeaderArg = Some (Fable.Core.U2.Case2 iotype) + BodyArg = None + BodyCellType = BuildingBlock.BodyCellType.Text + } + nextState, Cmd.none | UpdateBodyCellType next -> let nextState = { state with BodyCellType = next } nextState, Cmd.none diff --git a/src/Client/Pages/BuildingBlock/Dropdown.fs b/src/Client/Pages/BuildingBlock/Dropdown.fs index f2bd479c..bb72454e 100644 --- a/src/Client/Pages/BuildingBlock/Dropdown.fs +++ b/src/Client/Pages/BuildingBlock/Dropdown.fs @@ -97,23 +97,21 @@ module private DropdownElements = 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 + { DropdownPage = DropdownPage.Main; DropdownIsActive = false } |> setUiState + (headerType,ioType) |> BuildingBlock.UpdateHeaderWithIO |> BuildingBlockMsg |> dispatch Bulma.dropdownItem.a [ - prop.children [ - 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()) - ] - ] + match iotype with + | IOType.FreeText s -> + let onSubmit = fun (v: string) -> + let header = IOType.FreeText v + setIO header + prop.children [FreeTextInputElement onSubmit] + | _ -> + prop.onClick (fun e -> e.stopPropagation(); setIO iotype) + prop.onKeyDown(fun k -> if (int k.which) = 13 then setIO iotype) + prop.children [ + Html.div [prop.text (iotype.ToString())] + ] ] /// Main column types subpage for dropdown diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index d4f47d3f..d25414d7 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -41,14 +41,12 @@ let private termOrUnitizedSwitch (model:Messages.Model) dispatch = open Fable.Core -let private SearchBuildingBlockBodyElement (model: Messages.Model) dispatch = - let id = "SearchBuildingBlockBodyElementID" +[] +let private SearchBuildingBlockBodyElement (model: Messages.Model, dispatch) = 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.ref element + prop.style [ style.display.flex; style.justifyContent.spaceBetween; style.position.relative ] prop.children [ termOrUnitizedSwitch model dispatch let setter (oaOpt: OntologyAnnotation option) = @@ -56,18 +54,16 @@ let private SearchBuildingBlockBodyElement (model: Messages.Model) dispatch = 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) + Components.TermSearch.Input(setter, fullwidth=true, ?input=input, ?parent'=parent, displayParent=false, ?portalTermSelectArea=element.current, debounceSetter=1000) ] ] -let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState) setUi (model: Model) dispatch = +[] +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 + prop.ref element Bulma.field.hasAddons prop.style [style.position.relative] // Choose building block type dropdown element @@ -81,7 +77,7 @@ let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState) setUi (m 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) + Components.TermSearch.Input(setter, ?input=input, isExpanded=true, fullwidth=true, ?portalTermSelectArea=element.current, debounceSetter=1000) elif state.HeaderCellType.HasIOType() then Bulma.control.div [ Bulma.control.isExpanded @@ -130,9 +126,9 @@ let Main (model: Model) dispatch = //let state_searchHeader, setState_searchHeader = React.useState(TermSearchUIState.init) //let state_searchBody, setState_searchBody = React.useState(TermSearchUIState.init) mainFunctionContainer [ - SearchBuildingBlockHeaderElement state_bb setState_bb model dispatch + SearchBuildingBlockHeaderElement (state_bb, setState_bb, model, dispatch) if model.AddBuildingBlockState.HeaderCellType.IsTermColumn() then - SearchBuildingBlockBodyElement model dispatch + 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 c8cf860c..eb636017 100644 --- a/src/Client/Pages/TermSearch/TermSearchView.fs +++ b/src/Client/Pages/TermSearch/TermSearchView.fs @@ -109,7 +109,7 @@ let Main (model:Messages.Model, dispatch) = mainFunctionContainer [ Bulma.field.div [ - Components.TermSearch.Input(setTerm, dispatch, fullwidth=true, size=Bulma.input.isLarge, ?parent'=model.TermSearchState.ParentTerm, showAdvancedSearch=true) + Components.TermSearch.Input(setTerm, fullwidth=true, size=Bulma.input.isLarge, ?parent'=model.TermSearchState.ParentTerm, advancedSearchDispatch=dispatch) ] addButton(model, dispatch) ] diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 0df72acb..c97ecaf1 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -230,7 +230,8 @@ type TermSearch = ] ] - static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool, width: Styles.ICssUnit, alignRight) = + [] + static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool) = let searchesAreComplete = searchNameState.SearchIs = SearchIs.Done && searchTreeState.SearchIs = SearchIs.Done let foundInBoth (term:TermTypes.Term) = (searchTreeState.Results |> Array.contains term) @@ -260,7 +261,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.style [style.width (length.perc 100); style.top (length.perc 100)] prop.children [ yield! matchSearchState searchNameState false yield! matchSearchState searchTreeState true @@ -269,27 +270,28 @@ type TermSearch = [] static member Input ( - setter: OntologyAnnotation option -> unit, dispatch, + setter: OntologyAnnotation option -> unit, ?input: OntologyAnnotation, ?parent': OntologyAnnotation, - ?showAdvancedSearch: bool, - ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?dropdownWidth: Styles.ICssUnit, ?alignRight: bool, ?displayParent: bool) + ?advancedSearchDispatch: Messages.Msg -> unit, + ?debounceSetter: int, ?portalTermSelectArea: HTMLElement, + ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: 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 let loading, setLoading = React.useState(false) let state, setState = React.useState(input) + let parent, setParent = React.useState(parent') let searchNameState, setSearchNameState = React.useState(SearchState.init) let searchTreeState, setSearchTreeState = React.useState(SearchState.init) let isSearching, setIsSearching = React.useState(false) - let debounceStorage, setdebounceStorage = React.useState(newDebounceStorage) - let parent, setParent = React.useState(parent') + let debounceStorage = React.useRef(newDebounceStorage()) + let setter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp + React.useEffect((fun () -> setState input), dependencies=[|box input|]) + React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. let stopSearch() = - debounceStorage.Clear() + debounceStorage.current.Remove("TermSearch") |> ignore setLoading false setIsSearching false setSearchTreeState {searchTreeState with SearchIs = SearchIs.Idle} @@ -306,7 +308,6 @@ type TermSearch = setSearchNameState <| SearchState.init() setSearchTreeState <| SearchState.init() setIsSearching true - React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. Bulma.control.div [ if isExpanded then Bulma.control.isExpanded if size.IsSome then size.Value @@ -324,10 +325,10 @@ type TermSearch = 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) + allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) elif s.Trim() <> "" then startSearch (Some s) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 0) + mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) else () ) @@ -337,11 +338,16 @@ type TermSearch = stopSearch() else startSearch (Some s) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage, 1000) + mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) ) prop.onKeyDown(key.escape, fun _ -> stopSearch()) ] - TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching, dropdownWidth, alignRight) + let TermSelectArea = + TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching) + if portalTermSelectArea.IsSome then + ReactDOM.createPortal(TermSelectArea,portalTermSelectArea.Value) + else + TermSelectArea Components.searchIcon if state.IsSome && state.Value.Name.IsSome && state.Value.TermAccessionNumber.IsSome && not isSearching then Components.verifiedIcon // Optional elements @@ -353,11 +359,11 @@ type TermSearch = Html.span "Parent: " Html.span $"{parent.Value.NameText}, {parent.Value.TermAccessionShort}" ] - if showAdvancedSearch then + if advancedSearchDispatch.IsSome then Components.AdvancedSearch.Main(advancedSearchActive, setAdvancedSearchActive, (fun t -> setAdvancedSearchActive false Some t |> selectTerm), - dispatch + advancedSearchDispatch.Value ) Html.a [ prop.onClick(fun e -> e.preventDefault(); e.stopPropagation(); setAdvancedSearchActive true) diff --git a/src/Client/Views/MainWindowView.fs b/src/Client/Views/MainWindowView.fs index 396c6925..ea017851 100644 --- a/src/Client/Views/MainWindowView.fs +++ b/src/Client/Views/MainWindowView.fs @@ -43,7 +43,7 @@ let private spreadsheetSelectionFooter (model: Messages.Model) dispatch = open Shared [] -let Main (model: Messages.Model) dispatch = +let Main (model: Messages.Model, dispatch) = let state = model.SpreadsheetModel Html.div [ prop.id "MainWindow" @@ -73,7 +73,7 @@ let Main (model: Messages.Model) dispatch = | Some (ArcFiles.Study _) | Some (ArcFiles.Investigation _) | Some (ArcFiles.Template _) -> - XlsxFileView.Main {|model = model; dispatch = dispatch|} + XlsxFileView.Main (model , dispatch) if state.Tables.TableCount > 0 && state.ActiveTable.ColumnCount > 0 && state.ActiveView <> Spreadsheet.ActiveView.Metadata then MainComponents.AddRows.Main dispatch ] diff --git a/src/Client/Views/SplitWindowView.fs b/src/Client/Views/SplitWindowView.fs index 379b3d9c..697b7dba 100644 --- a/src/Client/Views/SplitWindowView.fs +++ b/src/Client/Views/SplitWindowView.fs @@ -20,10 +20,12 @@ let private calculateNewSideBarSize (model:SplitWindow) (pos:float) = let private onResize_event (model:SplitWindow) (setModel: SplitWindow -> unit) = (fun (e: Event) -> /// must get width like this, cannot propagate model correctly. - let sidebarWindow = Browser.Dom.document.getElementById(sidebarId).clientWidth - let windowWidth = Browser.Dom.window.innerWidth - let new_sidebarWidth = calculateNewSideBarSize model (windowWidth - sidebarWindow) - { model with RightWindowWidth = new_sidebarWidth } |> setModel + let ele = Browser.Dom.document.getElementById(sidebarId) + if isNull ele |> not then + let sidebarWindow = ele.clientWidth + let windowWidth = Browser.Dom.window.innerWidth + let new_sidebarWidth = calculateNewSideBarSize model (windowWidth - sidebarWindow) + { model with RightWindowWidth = new_sidebarWidth } |> setModel ) /// This event changes the size of main window and sidebar diff --git a/src/Client/Views/XlsxFileView.fs b/src/Client/Views/XlsxFileView.fs index c46e32ef..c45bffed 100644 --- a/src/Client/Views/XlsxFileView.fs +++ b/src/Client/Views/XlsxFileView.fs @@ -7,8 +7,7 @@ open Spreadsheet open Shared [] -let Main(x: {| model: Messages.Model; dispatch: Messages.Msg -> unit |}) = - let model, dispatch = x.model, x.dispatch +let Main(model: Messages.Model, dispatch: Messages.Msg -> unit) = match model.SpreadsheetModel.ActiveView with | ActiveView.Table _ -> MainComponents.SpreadsheetView.Main model dispatch diff --git a/src/Client/style.scss b/src/Client/style.scss index cc76da6d..7f151300 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -81,561 +81,575 @@ $colors: map-merge($colors, $addColors); flex-wrap: wrap; column-gap: 10px; flex-grow: 1; + + .form-input-term-search-positioner { + position: absolute; + top: calc(100% - 10px); + z-index: 20; + width: 100%; + } + + @media only screen and (max-width: 694px) { + .form-input-term-search-positioner { + position: relative; + z-index: 20; + top: -10px + } + } } .term-select-area { - position: absolute; - width: 100%; - max-height: 400px; - overflow: auto; - border: 1px solid $border-color; - background-color: $scheme-main; - border-top: unset; - z-index: 20; - - .term-select-item { - - > * { - padding: .5rem 1rem; - } + position: absolute; + width: 100%; + max-height: 400px; + overflow: auto; + border: 1px solid $border-color; + background-color: $scheme-main; + border-top: unset; + z-index: 20; + + .term-select-item { + + > * { + padding: .5rem 1rem; + } - .term-select-item-main { - cursor: pointer; + .term-select-item-main { + cursor: pointer; - > *:not(:last-child) { - padding-right: .5rem; - } + > *:not(:last-child) { + padding-right: .5rem; + } - a:hover { - text-decoration: underline + a:hover { + text-decoration: underline + } } - } - .term-select-item-more { - border-top: .5px solid $border-color; - box-shadow: 0px 4px 8px $border-color; + .term-select-item-more { + border-top: .5px solid $border-color; + box-shadow: 0px 4px 8px $border-color; - td { + td { + border: unset; + padding: 0.15em 0.25em; + } + } + + .term-select-item-toggle-button { + padding: calc(0.5em - 1px) .8em; + display: flex; + background-color: transparent; border: unset; - padding: 0.15em 0.25em; + cursor: pointer; + color: $scheme-invert; + justify-content: center; + align-items: center; + border-radius: 2px; + white-space: nowrap } - } - .term-select-item-toggle-button { - padding: calc(0.5em - 1px) .8em; - display: flex; - background-color: transparent; - border: unset; - cursor: pointer; - color: $scheme-invert; - justify-content: center; - align-items: center; - border-radius: 2px; - white-space: nowrap + .term-select-item-toggle-button:hover { + background-color: $content-blockquote-background-color + } } - .term-select-item-toggle-button:hover { - background-color: $content-blockquote-background-color + .term-select-item:not(:last-child) { + border-bottom: 1px solid $border-color; } } - .term-select-item:not(:last-child) { - border-bottom: 1px solid $border-color; + .modal-background { + opacity: 0.5 } -} -.modal-background { - opacity: 0.5 -} - -.modal-card { - box-shadow: 2px 2px black; -} + .modal-card { + box-shadow: 2px 2px black; + } + /*https: //stackoverflow.com/questions/54044479/table-with-sticky-header-and-resizable-columns-without-using-jquery*/ + .fixed_headers { + border-collapse: collapse; + width: max-content; + border-style: hidden + } -/*https: //stackoverflow.com/questions/54044479/table-with-sticky-header-and-resizable-columns-without-using-jquery*/ -.fixed_headers { - border-collapse: collapse; - width: max-content; - border-style: hidden -} + .fixed_headers td, + .fixed_headers thead th { + text-align: left; + } -.fixed_headers td, -.fixed_headers thead th { - text-align: left; -} + .fixed_headers thead tr { + position: relative; + } -.fixed_headers thead tr { - position: relative; -} + .fixed_headers tbody tr:nth-child(even) { + background-color: #DDD; + } -.fixed_headers tbody tr:nth-child(even) { - background-color: #DDD; -} + .fixed_headers thead th { + position: sticky; + top: 0; /* REQUIRED: https://stackoverflow.com/a/43707215 */ + resize: horizontal; + overflow: auto; + z-index: 2 + } -.fixed_headers thead th { - position: sticky; - top: 0; /* REQUIRED: https://stackoverflow.com/a/43707215 */ - resize: horizontal; - overflow: auto; - z-index: 2 -} + html, body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-size: 14px; + min-width: 400px; + height: 100vh; + overflow: auto + } -html, body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - font-size: 14px; - min-width: 400px; - height: 100vh; - overflow: auto -} + a { + color: $nfdi-blue-light + } -a { - color: $nfdi-blue-light -} + a:hover { + color: $nfdi-blue-lighter-20 + } -a:hover { - color: $nfdi-blue-lighter-20 -} + .dragover-footertab { + position: relative + } -.dragover-footertab { - position: relative -} + .dragover-footertab:before { + position: absolute; + content: ""; + margin: 0 3px; + display: inline-block; + border: 7px solid transparent; + border-top: 8px solid black; + border-bottom: 0 none; + top: -3px; + left: -9px; + } -.dragover-footertab:before { - position: absolute; - content: ""; - margin: 0 3px; - display: inline-block; - border: 7px solid transparent; - border-top: 8px solid black; - border-bottom: 0 none; - top: -3px; - left: -9px; -} + [type=checkbox]:checked.switch.is-primary + label::before, [type=checkbox]:checked.switch.is-primary + label::before { + background: $nfdi-mint + } -[type=checkbox]:checked.switch.is-primary + label::before, [type=checkbox]:checked.switch.is-primary + label::before { - background: $nfdi-mint -} + [data-tooltip]:not(.is-loading)::before, [data-tooltip]:not(.is-loading)::after, [data-tooltip]:not(.is-disabled)::before, [data-tooltip]:not(.is-disabled)::after, [data-tooltip]:not([disabled])::before, [data-tooltip]:not([disabled])::after { + box-sizing: border-box; + color: #fff; + left: 50px; + display: none; + font-family: BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif; + font-size: .75rem; + hyphens: auto; + opacity: 0; + overflow: hidden; + pointer-events: none; + position: absolute; + visibility: hidden; + z-index: 1020; + } -[data-tooltip]:not(.is-loading)::before, [data-tooltip]:not(.is-loading)::after, [data-tooltip]:not(.is-disabled)::before, [data-tooltip]:not(.is-disabled)::after, [data-tooltip]:not([disabled])::before, [data-tooltip]:not([disabled])::after { - box-sizing: border-box; - color: #fff; - left: 50px; - display: none; - font-family: BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif; - font-size: .75rem; - hyphens: auto; - opacity: 0; - overflow: hidden; - pointer-events: none; - position: absolute; - visibility: hidden; - z-index: 1020; -} + [data-tooltip]:hover::before { + display: inline-block + } -[data-tooltip]:hover::before { - display: inline-block -} + #cy { + height: 300px; + display: block + } -#cy { - height: 300px; - display: block -} + .myNavbarSticky { + position: sticky; + position: -webkit-sticky; + min-height: min-content; + top: 0; + background-color: $primary; + } -.myNavbarSticky { - position: sticky; - position: -webkit-sticky; - min-height: min-content; - top: 0; - background-color: $primary; -} + .myNavbarSticky .navbar-item#logo { + position: relative; + overflow: hidden; + + &::before { + content: ""; + border-radius: 90px; + position: absolute; + top: 50%; + left: 50%; + box-shadow: 0 0 15px 5px #fff, /* inner white */ + 0 0 30px 15px $success; + } + } -.myNavbarSticky .navbar-item#logo { - position: relative; - overflow:hidden; + .myNavbarButton { + height: 100% !important; + width: 100% !important; + background-color: transparent; + border-radius: unset; + border-color: transparent !important; + align-items: center; + justify-content: center; + color: white; + display: inline-flex; + padding: unset; + box-shadow: unset !important + } - &::before { - content: ""; - border-radius: 90px; - position: absolute; - top: 50%; - left: 50%; - box-shadow: 0 0 15px 5px #fff, /* inner white */ - 0 0 30px 15px $success; + .myNavbarButton:not([disabled]):active { + background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)) !important } -} -.myNavbarButton { - height: 100% !important; - width: 100% !important; - background-color: transparent; - border-radius: unset; - border-color: transparent !important; - align-items: center; - justify-content: center; - color: white; - display: inline-flex; - padding: unset; - box-shadow: unset !important -} + .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within, .myNavbarButton:not([disabled]):hover { + background-color: transparent; + color: $success + } -.myNavbarButton:not([disabled]):active { - background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)) !important -} + .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within { + border-color: white !important; + } -.myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within, .myNavbarButton:not([disabled]):hover { - background-color: transparent; - color: $success -} + .myNavbarButton:not([disabled]):focus:not(:focus-visible), .myNavbarButton:not([disabled]):focus-within:not(:focus-visible) { + border-color: transparent !important; + color: white + } -.myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within { - border-color: white !important; -} + .myNavbarButton[disabled] { + filter: brightness(85%); + background-color: transparent; + color: white; + } -.myNavbarButton:not([disabled]):focus:not(:focus-visible), .myNavbarButton:not([disabled]):focus-within:not(:focus-visible) { - border-color: transparent !important; - color: white -} + .myNavbarButton[disabled]:focus, .myNavbarButton[disabled]:focus-within { + box-shadow: unset + } -.myNavbarButton[disabled] { - filter: brightness(85%); - background-color: transparent; - color: white; -} + .help { + @extend .help; + font-size: 0.85rem; + } -.myNavbarButton[disabled]:focus, .myNavbarButton[disabled]:focus-within { - box-shadow: unset -} -.help { - @extend .help; - font-size: 0.85rem; -} + .mainFunctionContainer { + border-width: 0px 0px 0px 5px; + border-style: none none none solid; + padding: 0.25rem 1rem; + margin-bottom: 1rem; + border-image-slice: 1 + } + .wrapFlexBox { + flex-wrap: wrap; + flex-shrink: 1; + } -.mainFunctionContainer { - border-width: 0px 0px 0px 5px; - border-style: none none none solid; - padding: 0.25rem 1rem; - margin-bottom: 1rem; - border-image-slice: 1 -} + .button { + @extend .button; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + } -.wrapFlexBox { - flex-wrap: wrap; - flex-shrink: 1; -} + .textarea { + @extend .textarea; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; + border: 3px solid lightgrey; + } -.button { - @extend .button; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; -} + .textarea:focus { + border-color: $nfdi-mint + } -.textarea { - @extend .textarea; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; - border: 3px solid lightgrey; -} + .button:disabled, .button[disabled] { + opacity: 0.5 + } -.textarea:focus { - border-color: $nfdi-mint -} + .input { + @extend .input; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + } -.button:disabled, .button[disabled] { - opacity: 0.5 -} + .clone * { + pointer-events: none + } -.input { - @extend .input; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; -} + .delete { + @extend .delete + } -.clone * { - pointer-events: none -} + a.navbar-item:hover { + background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)) !important; + } -.delete { - @extend .delete -} + .delete:hover { + @extend .delete; + background-color: $danger + } -a.navbar-item:hover { - background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)) !important; -} + .hoverTableEle { + } -.delete:hover { - @extend .delete; - background-color: $danger -} + .hoverTableEle:hover { + /*background-color: #E8E8E8 !important*/ + background-color: rgba(0, 0, 0, 0.2) + } -.hoverTableEle { -} + .clickableTag { + cursor: pointer + } -.hoverTableEle:hover { - /*background-color: #E8E8E8 !important*/ - background-color: rgba(0, 0, 0, 0.2) -} + .clickableTag:hover { + /*background-color: $lightBlueLighter20 !important;*/ + box-shadow: inset 0 0 0 10em rgba(255, 255, 255, 0.3); + } -.clickableTag { - cursor: pointer -} + .clickableTagDelete { + cursor: pointer; + border: 0.2px solid $info + } -.clickableTag:hover { - /*background-color: $lightBlueLighter20 !important;*/ - box-shadow: inset 0 0 0 10em rgba(255, 255, 255, 0.3); -} + .clickableTagDelete:hover { + background-color: $danger !important; + border-color: $danger !important; + color: white + } -.clickableTagDelete { - cursor: pointer; - border: 0.2px solid $info -} + .toExcelColor { + color: $primarye !important + } + /////////// Custom simple checkbox, due to issue #54 /////////////////// + .checkbox-label { + display: inline-block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 1rem; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: auto; + } + /* Hide the browser's default checkbox */ + .checkbox-input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + } + /* Create a custom checkbox */ + .checkbox-checkmark { + position: absolute; + top: 3px; + left: 3px; + height: 20px; + width: 20px; + background-color: #eee; + border: 0.1px solid #dbdbdb; + border-radius: 50%; + cursor: pointer; + } -.clickableTagDelete:hover { - background-color: $danger !important; - border-color: $danger !important; - color: white -} + .checkbox-input[disabled] ~ .checkbox-label, .checkbox-input[disabled] ~ .checkbox-checkmark { + color: lightgrey; + cursor: not-allowed; + } + /* On mouse-over, add a grey background color */ + .checkbox-label:hover ~ .checkbox-checkmark, .checkbox-checkmark:hover { + background-color: #ccc; + } + /* On mouse-over on a disabled input, add a grey background color */ + .checkbox-input[disabled] ~ .checkbox-checkmark:hover, .checkbox-input[disabled] ~ .checkbox-label:hover ~ .checkbox-checkmark { + background-color: unset; + } + /* When the checkbox is checked, add a green background */ + .checkbox-input:checked ~ .checkbox-checkmark { + background-color: $success; + border: none; + } + /* Create the checkmark/indicator (hidden when not checked) */ + .checkbox-checkmark:after { + content: ""; + position: absolute; + display: none; + } + /* Show the indicator when checked */ + .checkbox-input:checked ~ .checkbox-checkmark:after { + display: block; + } + /* Style the checkmark/indicator */ + .checkbox-checkmark:after { + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 50%; + background: white; + } + ////////////////// End of custom checkbox ///////////////////////// -.toExcelColor { - color: $primarye !important -} -/////////// Custom simple checkbox, due to issue #54 /////////////////// -.checkbox-label { - display: inline-block; - position: relative; - padding-left: 35px; - margin-bottom: 12px; - cursor: pointer; - font-size: 1rem; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - width: auto; -} -/* Hide the browser's default checkbox */ -.checkbox-input { - position: absolute; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; -} -/* Create a custom checkbox */ -.checkbox-checkmark { - position: absolute; - top: 3px; - left: 3px; - height: 20px; - width: 20px; - background-color: #eee; - border: 0.1px solid #dbdbdb; - border-radius: 50%; - cursor: pointer; -} + .niceBkgrnd { + background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); + background-size: 400% 400%; + animation: gradient 5s ease infinite; + } -.checkbox-input[disabled] ~ .checkbox-label, .checkbox-input[disabled] ~ .checkbox-checkmark { - color: lightgrey; - cursor: not-allowed; -} -/* On mouse-over, add a grey background color */ -.checkbox-label:hover ~ .checkbox-checkmark, .checkbox-checkmark:hover { - background-color: #ccc; -} -/* On mouse-over on a disabled input, add a grey background color */ -.checkbox-input[disabled] ~ .checkbox-checkmark:hover, .checkbox-input[disabled] ~ .checkbox-label:hover ~ .checkbox-checkmark { - background-color: unset; -} -/* When the checkbox is checked, add a green background */ -.checkbox-input:checked ~ .checkbox-checkmark { - background-color: $success; - border: none; -} -/* Create the checkmark/indicator (hidden when not checked) */ -.checkbox-checkmark:after { - content: ""; - position: absolute; - display: none; -} -/* Show the indicator when checked */ -.checkbox-input:checked ~ .checkbox-checkmark:after { - display: block; -} -/* Style the checkmark/indicator */ -.checkbox-checkmark:after { - top: 6px; - left: 6px; - width: 8px; - height: 8px; - border-radius: 50%; - background: white; -} -////////////////// End of custom checkbox ///////////////////////// + @keyframes gradient { + 0% { + background-position: 0% 50%; + } -.niceBkgrnd { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 5s ease infinite; -} + 50% { + background-position: 100% 50%; + } -@keyframes gradient { - 0% { - background-position: 0% 50%; + 100% { + background-position: 0% 50%; + } } - 50% { - background-position: 100% 50%; + tr.suggestion :hover { + cursor: pointer; } - 100% { - background-position: 0% 50%; + tr.suggestion-details { + /*visibility: collapse;*/ + display: none; + box-shadow: 1px 2px 2px darkgrey } -} -tr.suggestion :hover { - cursor: pointer; -} -tr.suggestion-details { - /*visibility: collapse;*/ - display: none; - box-shadow: 1px 2px 2px darkgrey -} + .delete :hover { + cursor: pointer; + } + .nonSelectText { + user-select: none; /* standard syntax */ + -webkit-user-select: none; /* webkit (safari, chrome) browsers */ + -moz-user-select: none; /* mozilla browsers */ + -khtml-user-select: none; /* webkit (konqueror) browsers */ + -ms-user-select: none; /* IE10+ */ + } -.delete :hover { - cursor: pointer; -} + input::-ms-clear { + display: none; + } -.nonSelectText { - user-select: none; /* standard syntax */ - -webkit-user-select: none; /* webkit (safari, chrome) browsers */ - -moz-user-select: none; /* mozilla browsers */ - -khtml-user-select: none; /* webkit (konqueror) browsers */ - -ms-user-select: none; /* IE10+ */ -} + .hideOver575px { + display: none !important + } -input::-ms-clear { - display: none; -} + .hideOver775px { + display: none + } -.hideOver575px { - display: none !important -} + .hideUnder575px { + display: flex + } -.hideOver775px { - display: none -} + .hideUnder775px { + display: flex + } -.hideUnder575px { - display: flex -} + .flexToCentered { + } -.hideUnder775px { - display: flex -} + .myFlexText { + text-align: justify; + font-size: 1.2rem; + } + /// Brands -.flexToCentered { -} + .nfdiIcon { + transition: 0.2s opacity; + } -.myFlexText { - text-align: justify; - font-size: 1.2rem; -} -/// Brands + .nfdiIcon:hover { + opacity: 0.7 + } -.nfdiIcon { - transition: 0.2s opacity; -} + .myFaBrand { + transition: 0.2s opacity; + padding: 10px; + cursor: pointer + } -.nfdiIcon:hover { - opacity: 0.7 -} + .myFaBrand:hover { + opacity: 0.7; + } -.myFaBrand { - transition: 0.2s opacity; - padding: 10px; - cursor: pointer -} + .myFaTwitter { + background: #55ACEE; + color: white; + } -.myFaBrand:hover { - opacity: 0.7; -} + .myFaCSB { + background: #ed7d31; + color: white; + } -.myFaTwitter { - background: #55ACEE; - color: white; -} + .myFaGithub { + background: #24292e; + color: white + } -.myFaCSB { - background: #ed7d31; - color: white; -} + @media only screen and (max-width: 768px) { -.myFaGithub { - background: #24292e; - color: white -} + .myFlexText { + font-size: 1rem; + width: 90%; + margin: auto; + margin-bottom: 0.5rem; + } -@media only screen and (max-width: 768px) { + .hideUnder775px { + display: none + } - .myFlexText { - font-size: 1rem; - width: 90%; - margin: auto; - margin-bottom: 0.5rem; - } + .hideOver775px { + display: flex + } - .hideUnder775px { - display: none - } + .flexToCentered { + text-align: center + } - .hideOver775px { - display: flex + .reverseCols { + flex-direction: column-reverse; + display: flex; + } } - .flexToCentered { - text-align: center - } + @media only screen and (max-width: 575px) { + .hideOver575px { + display: flex !important + } - .reverseCols { - flex-direction: column-reverse; - display: flex; - } -} + .hideUnder575px { + display: none !important + } -@media only screen and (max-width: 575px) { - .hideOver575px { - display: flex !important + .myFlexText { + margin-bottom: 0.5rem; + font-size: 0.8rem; + } } - .hideUnder575px { - display: none !important + .danger_important { + color: $danger !important } - .myFlexText { - margin-bottom: 0.5rem; - font-size: 0.8rem; + kbd { + margin: 0px 0.1em; + padding: 0.1em 0.6em; + border-radius: 3px; + border: 1px solid rgb(204, 204, 204); + color: rgb(51, 51, 51); + line-height: 1.4; + font-family: Arial,Helvetica,sans-serif; + font-size: 10px; + display: inline-block; + box-shadow: 0px 1px 0px rgba(0,0,0,0.2), inset 0px 0px 0px 2px #ffffff; + background-color: rgb(247, 247, 247); + -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; + -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-shadow: 0 1px 0 #fff; } -} - -.danger_important { - color: $danger !important -} - -kbd { - margin: 0px 0.1em; - padding: 0.1em 0.6em; - border-radius: 3px; - border: 1px solid rgb(204, 204, 204); - color: rgb(51, 51, 51); - line-height: 1.4; - font-family: Arial,Helvetica,sans-serif; - font-size: 10px; - display: inline-block; - box-shadow: 0px 1px 0px rgba(0,0,0,0.2), inset 0px 0px 0px 2px #ffffff; - background-color: rgb(247, 247, 247); - -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; - -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - text-shadow: 0 1px 0 #fff; -} From 3e7fec2a916178a6904a2c7d91a341ed14cda46a Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 15:32:08 +0100 Subject: [PATCH 002/135] Fix build ci --- build/Build.fs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/build/Build.fs b/build/Build.fs index f8f5619c..849b24cd 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -223,22 +223,22 @@ module Release = open System.Diagnostics - let private executeCommand (command: string) : string = - let p = new Process() - p.StartInfo.FileName <- "git" - p.StartInfo.Arguments <- command - p.StartInfo.RedirectStandardOutput <- true - p.StartInfo.UseShellExecute <- false - p.StartInfo.CreateNoWindow <- true - p.Start() |> ignore + let GetLatestGitTag () : string = + let executeCommand (command: string) : string = + let p = new Process() + p.StartInfo.FileName <- "git" + p.StartInfo.Arguments <- command + p.StartInfo.RedirectStandardOutput <- true + p.StartInfo.UseShellExecute <- false + p.StartInfo.CreateNoWindow <- true - let output = p.StandardOutput.ReadToEnd() - p.WaitForExit() + p.Start() |> ignore - output + let output = p.StandardOutput.ReadToEnd() + p.WaitForExit() - let GetLatestGitTag () : string = + output executeCommand "describe --abbrev=0 --tags" |> String.trim @@ -265,7 +265,8 @@ module Release = let ForcePushNightly() = if promptYesNo "Ready to force push release to nightly branch?" then - Git.Commit.exec "." (sprintf "Release v%s" ProjectInfo.prereleaseTag) + run git ["add"; "."] __SOURCE_DIRECTORY__ + run git ["commit"; "-m"; (sprintf "Release v%s" ProjectInfo.prereleaseTag)] __SOURCE_DIRECTORY__ run git ["push"; "-f"; "origin"; "HEAD:nightly"] __SOURCE_DIRECTORY__ else failwith "aborted" @@ -423,7 +424,8 @@ let main args = | "create-file" :: version :: a -> ReleaseNoteTasks.createVersionFile(version); 0 | _ -> runOrDefault args | "cmdtest" :: a -> - Git.Commit.exec "." (sprintf "Release v%s" ProjectInfo.prereleaseTag) + run git ["add"; "."] "" + run git ["commit"; "-m"; (sprintf "Release v%s" ProjectInfo.prereleaseTag)] "" 0 | _ -> runOrDefault args From c0571ff5f504871d7b8b8987ce9b3538b860f14d Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 15:42:55 +0100 Subject: [PATCH 003/135] improve term search controle :sparkles: --- src/Client/SharedComponents/TermSearchInput.fs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index c97ecaf1..7f922d80 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -287,7 +287,7 @@ type TermSearch = let searchTreeState, setSearchTreeState = React.useState(SearchState.init) let isSearching, setIsSearching = React.useState(false) let debounceStorage = React.useRef(newDebounceStorage()) - let setter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp + let dsetter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp React.useEffect((fun () -> setState input), dependencies=[|box input|]) React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. let stopSearch() = @@ -301,10 +301,11 @@ type TermSearch = setState oaOpt setter oaOpt setIsSearching false - let startSearch(queryString: string option) = + let startSearch(queryString: string option, isOnChange: bool) = let oaOpt = queryString |> Option.map (fun s -> OntologyAnnotation.fromString(s) ) - setter oaOpt - setState oaOpt + if isOnChange then + dsetter oaOpt + setState oaOpt setSearchNameState <| SearchState.init() setSearchTreeState <| SearchState.init() setIsSearching true @@ -324,20 +325,20 @@ type TermSearch = 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) + startSearch(None, false) allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) elif s.Trim() <> "" then - startSearch (Some s) + startSearch (Some s, false) mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) else () ) prop.onChange(fun (s: string) -> if s.Trim() = "" then - startSearch(None) + startSearch(None, true) stopSearch() else - startSearch (Some s) + startSearch (Some s, true) mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) ) prop.onKeyDown(key.escape, fun _ -> stopSearch()) From 4d7f4b8f0ac160ecafbae5890177df60994e43cd Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 15:43:22 +0100 Subject: [PATCH 004/135] Add history control buttons for ARCitect :sparkles: --- src/Client/MainComponents/Navbar.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index 1f897290..81e1c09c 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -119,6 +119,7 @@ let Main (model: Messages.Model) dispatch = (fun e -> ()), false ).toReactElement() + quickAccessButtonListStart (model.History: LocalHistory.Model) dispatch ] ] ] From 5b62fb63bc3a3199613e638ec7a729e7751e48dc Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 15:46:06 +0100 Subject: [PATCH 005/135] Minor git commit improvement for ci --- build/Build.fs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build/Build.fs b/build/Build.fs index 849b24cd..707400d8 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -38,7 +38,7 @@ module ReleaseNoteTasks = open Fake.Extensions.Release - let createVersionFile(version: string) = + let createVersionFile(version: string, commit: bool) = let releaseDate = System.DateTime.UtcNow.ToShortDateString() Fake.DotNet.AssemblyInfoFile.createFSharp "src/Server/Version.fs" [ Fake.DotNet.AssemblyInfo.Title "Swate" @@ -46,6 +46,9 @@ module ReleaseNoteTasks = Fake.DotNet.AssemblyInfo.Metadata ("Version",version) Fake.DotNet.AssemblyInfo.Metadata ("ReleaseDate",releaseDate) ] + if commit then + run git ["add"; "."] __SOURCE_DIRECTORY__ + run git ["commit"; "-m"; (sprintf "Release v%s :bookmark:" ProjectInfo.prereleaseTag)] __SOURCE_DIRECTORY__ let updateReleaseNotes = Target.create "releasenotes" (fun config -> ReleaseNotes.ensure() @@ -53,7 +56,7 @@ module ReleaseNoteTasks = ReleaseNotes.update(ProjectInfo.gitOwner, ProjectInfo.project, config) let newRelease = ReleaseNotes.load "RELEASE_NOTES.md" - createVersionFile(newRelease.AssemblyVersion) + createVersionFile(newRelease.AssemblyVersion, false) Trace.trace "Update Version.fs done!" @@ -223,7 +226,6 @@ module Release = open System.Diagnostics - let GetLatestGitTag () : string = let executeCommand (command: string) : string = let p = new Process() @@ -265,8 +267,6 @@ module Release = let ForcePushNightly() = if promptYesNo "Ready to force push release to nightly branch?" then - run git ["add"; "."] __SOURCE_DIRECTORY__ - run git ["commit"; "-m"; (sprintf "Release v%s" ProjectInfo.prereleaseTag)] __SOURCE_DIRECTORY__ run git ["push"; "-f"; "origin"; "HEAD:nightly"] __SOURCE_DIRECTORY__ else failwith "aborted" @@ -405,7 +405,7 @@ let main args = Release.SetPrereleaseTag() Release.CreatePrereleaseTag() let version = Release.GetLatestGitTag() - ReleaseNoteTasks.createVersionFile(version) + ReleaseNoteTasks.createVersionFile(version, true) Release.ForcePushNightly() 0 | _ -> @@ -421,7 +421,7 @@ let main args = | _ -> runOrDefault args | "version" :: a -> match a with - | "create-file" :: version :: a -> ReleaseNoteTasks.createVersionFile(version); 0 + | "create-file" :: version :: a -> ReleaseNoteTasks.createVersionFile(version, false); 0 | _ -> runOrDefault args | "cmdtest" :: a -> run git ["add"; "."] "" From 14ecc0a72918ac1ba591e8c44506e97486b8e6f5 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 15:56:53 +0100 Subject: [PATCH 006/135] Release v1.0.0-alpha.03 :bookmark: --- build/Build.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Build.fs b/build/Build.fs index 707400d8..cef25e76 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -260,7 +260,7 @@ module Release = let CreatePrereleaseTag() = if promptYesNo (sprintf "Tagging branch with %s OK?" ProjectInfo.prereleaseTag ) then - Git.Branches.tag "" ProjectInfo.prereleaseTag + run git ["tag"; "-f"; ProjectInfo.prereleaseTag; ] __SOURCE_DIRECTORY__ Git.Branches.pushTag "" ProjectInfo.projectRepo ProjectInfo.prereleaseTag else failwith "aborted" From 32b8ffce033b2e01eb26c7a1143e06cc81c1522f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 16:04:54 +0100 Subject: [PATCH 007/135] Release v1.0.0-alpha.04 :bookmark: --- build/Build.fs | 8 ++++---- src/Server/Version.fs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/Build.fs b/build/Build.fs index cef25e76..6fa8f331 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -47,8 +47,8 @@ module ReleaseNoteTasks = Fake.DotNet.AssemblyInfo.Metadata ("ReleaseDate",releaseDate) ] if commit then - run git ["add"; "."] __SOURCE_DIRECTORY__ - run git ["commit"; "-m"; (sprintf "Release v%s :bookmark:" ProjectInfo.prereleaseTag)] __SOURCE_DIRECTORY__ + run git ["add"; "."] "" + run git ["commit"; "-m"; (sprintf "Release v%s :bookmark:" ProjectInfo.prereleaseTag)] "" let updateReleaseNotes = Target.create "releasenotes" (fun config -> ReleaseNotes.ensure() @@ -260,14 +260,14 @@ module Release = let CreatePrereleaseTag() = if promptYesNo (sprintf "Tagging branch with %s OK?" ProjectInfo.prereleaseTag ) then - run git ["tag"; "-f"; ProjectInfo.prereleaseTag; ] __SOURCE_DIRECTORY__ + run git ["tag"; "-f"; ProjectInfo.prereleaseTag; ] "" Git.Branches.pushTag "" ProjectInfo.projectRepo ProjectInfo.prereleaseTag else failwith "aborted" let ForcePushNightly() = if promptYesNo "Ready to force push release to nightly branch?" then - run git ["push"; "-f"; "origin"; "HEAD:nightly"] __SOURCE_DIRECTORY__ + run git ["push"; "-f"; "origin"; "HEAD:nightly"] "" else failwith "aborted" diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 80a6293a..04be7877 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "1.0.0-alpha.02" - let [] AssemblyMetadata_ReleaseDate = "24.01.2024" + let [] AssemblyMetadata_Version = "1.0.0-alpha.03" + let [] AssemblyMetadata_ReleaseDate = "25.01.2024" From 043c105a139ccc001630a93995a5e59ccfc32a88 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 25 Jan 2024 16:35:01 +0100 Subject: [PATCH 008/135] try to improve github packages release --- .github/workflows/PublishDocker.yaml | 2 +- build/Build.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/PublishDocker.yaml b/.github/workflows/PublishDocker.yaml index 0c4f50b9..28b78692 100644 --- a/.github/workflows/PublishDocker.yaml +++ b/.github/workflows/PublishDocker.yaml @@ -39,7 +39,7 @@ jobs: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref, event=branch - type=semver,pattern={{version}} + type=semver,pattern={{raw}} # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. diff --git a/build/Build.fs b/build/Build.fs index 6fa8f331..f8fc2a00 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -248,7 +248,7 @@ module Release = printfn "Please enter pre-release package suffix" let suffix = System.Console.ReadLine() ProjectInfo.prereleaseSuffix <- suffix - ProjectInfo.prereleaseTag <- (sprintf "%i.%i.%i-%s" ProjectInfo.release.SemVer.Major ProjectInfo.release.SemVer.Minor ProjectInfo.release.SemVer.Patch suffix) + ProjectInfo.prereleaseTag <- (sprintf "v%i.%i.%i-%s" ProjectInfo.release.SemVer.Major ProjectInfo.release.SemVer.Minor ProjectInfo.release.SemVer.Patch suffix) ProjectInfo.isPrerelease <- true let CreateTag() = From c86c3b95a5ff3bdc13d5320a2ca278fc6790a7ed Mon Sep 17 00:00:00 2001 From: Kevin F Date: Mon, 29 Jan 2024 15:34:27 +0100 Subject: [PATCH 009/135] Fix missing cells issue #346 --- paket.dependencies | 4 +- paket.lock | 64 ++--- src/Client/MainComponents/Cells.fs | 266 ++++++++++--------- src/Client/MainComponents/SpreadsheetView.fs | 4 +- 4 files changed, 181 insertions(+), 157 deletions(-) diff --git a/paket.dependencies b/paket.dependencies index 7ef8ff1e..42e97157 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -2,12 +2,12 @@ source https://api.nuget.org/v3/index.json framework: net8.0 storage: none -nuget ARCtrl 1.0.5 +nuget ARCtrl 1.0.6 nuget Feliz.Bulma.Checkradio nuget Feliz.Bulma.Switch nuget Fsharp.Core ~> 8 nuget Fable.Remoting.Giraffe ~> 5 -nuget FsSpreadsheet.Exceljs ~> 5.0.2 +nuget FsSpreadsheet.Exceljs ~> 5.1.1 nuget Saturn ~> 0 nuget Fable.Core ~> 4 diff --git a/paket.lock b/paket.lock index f622e315..af4d3e42 100644 --- a/paket.lock +++ b/paket.lock @@ -2,38 +2,38 @@ STORAGE: NONE RESTRICTION: == net8.0 NUGET remote: https://api.nuget.org/v3/index.json - ARCtrl (1.0.5) - ARCtrl.Contract (>= 1.0.5) - ARCtrl.CWL (>= 1.0.5) - ARCtrl.FileSystem (>= 1.0.5) - ARCtrl.ISA (>= 1.0.5) - ARCtrl.ISA.Json (>= 1.0.5) - ARCtrl.ISA.Spreadsheet (>= 1.0.5) + ARCtrl (1.0.6) + ARCtrl.Contract (>= 1.0.6) + ARCtrl.CWL (>= 1.0.6) + ARCtrl.FileSystem (>= 1.0.6) + ARCtrl.ISA (>= 1.0.6) + ARCtrl.ISA.Json (>= 1.0.6) + ARCtrl.ISA.Spreadsheet (>= 1.0.6) Fable.Fetch (>= 2.6) Fable.SimpleHttp (>= 3.5) - FSharp.Core (>= 6.0.7) - ARCtrl.Contract (1.0.5) - ARCtrl.ISA (>= 1.0.5) - FSharp.Core (>= 6.0.7) - ARCtrl.CWL (1.0.5) - FSharp.Core (>= 6.0.7) - ARCtrl.FileSystem (1.0.5) + FSharp.Core (>= 6.0.6) + ARCtrl.Contract (1.0.6) + ARCtrl.ISA (>= 1.0.6) + FSharp.Core (>= 6.0.6) + ARCtrl.CWL (1.0.6) + FSharp.Core (>= 6.0.6) + ARCtrl.FileSystem (1.0.6) Fable.Core (>= 4.2) - FSharp.Core (>= 6.0.7) - ARCtrl.ISA (1.0.5) - ARCtrl.FileSystem (>= 1.0.5) - FSharp.Core (>= 6.0.7) - ARCtrl.ISA.Json (1.0.5) - ARCtrl.ISA (>= 1.0.5) - FSharp.Core (>= 6.0.7) + FSharp.Core (>= 6.0.6) + ARCtrl.ISA (1.0.6) + ARCtrl.FileSystem (>= 1.0.6) + FSharp.Core (>= 6.0.6) + ARCtrl.ISA.Json (1.0.6) + ARCtrl.ISA (>= 1.0.6) + FSharp.Core (>= 6.0.6) NJsonSchema (>= 10.8) Thoth.Json (>= 10.1) Thoth.Json.Net (>= 11.0) - ARCtrl.ISA.Spreadsheet (1.0.5) - ARCtrl.FileSystem (>= 1.0.5) - ARCtrl.ISA (>= 1.0.5) - FSharp.Core (>= 6.0.7) - FsSpreadsheet (>= 5.0.1) + ARCtrl.ISA.Spreadsheet (1.0.6) + ARCtrl.FileSystem (>= 1.0.6) + ARCtrl.ISA (>= 1.0.6) + FSharp.Core (>= 6.0.6) + FsSpreadsheet (>= 5.1.1) ExcelJS.Fable (0.3) Fable.Core (>= 3.2.8) Fable.React (>= 7.4.1) @@ -72,7 +72,7 @@ NUGET Fable.Browser.Event (>= 1.5) Fable.Core (>= 3.0) FSharp.Core (>= 4.7.2) - Fable.Core (4.2) + Fable.Core (4.3) Fable.Elmish (4.1) Fable.Core (>= 3.7.1) FSharp.Core (>= 4.7.2) @@ -186,14 +186,14 @@ NUGET FSharp.Core (>= 6.0) Microsoft.IO.RecyclableMemoryStream (>= 2.2.1) FSharp.Core (8.0.101) - FsSpreadsheet (5.0.2) + FsSpreadsheet (5.1.1) Fable.Core (>= 4.0) - FSharp.Core (>= 6.0.7) - FsSpreadsheet.Exceljs (5.0.2) + FSharp.Core (>= 6.0.6) + FsSpreadsheet.Exceljs (5.1.1) Fable.Exceljs (>= 1.6) Fable.Promise (>= 3.2) - FSharp.Core (>= 6.0.7) - FsSpreadsheet (>= 5.0.2) + FSharp.Core (>= 6.0.6) + FsSpreadsheet (>= 5.1.1) Giraffe (6.2) FSharp.Core (>= 6.0) Giraffe.ViewEngine (>= 1.4) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 6753f765..d8fe13f6 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -87,6 +87,29 @@ let private basicValueDisplayCell (v: string) = prop.text v ] +let private compositeCellDisplay (cc: CompositeCell) = + let ccType = match cc with | CompositeCell.Term oa -> "T" | CompositeCell.Unitized _ -> "U" | CompositeCell.FreeText _ -> "F" + let v = cc.ToString() + Html.div [ + prop.classes ["is-flex"] + prop.style [ + style.flexGrow 1 + style.padding(length.em 0.5,length.em 0.75) + ] + prop.children [ + Html.span [ + prop.style [ + style.flexGrow 1 + ] + prop.text v + ] + Html.span [ + prop.style [style.custom("marginLeft", "auto")] + prop.text ccType + ] + ] + ] + module private EventPresets = open Shared @@ -262,131 +285,132 @@ module private EventPresets = // ] // ] -//let private extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = -// let isExtended = state_extend.Contains(columnIndex) -// Bulma.icon [ -// prop.style [ -// style.cursor.pointer -// ] -// prop.onDoubleClick(fun e -> -// e.stopPropagation() -// e.preventDefault() -// () -// ) -// prop.onClick(fun e -> -// e.stopPropagation() -// e.preventDefault() -// let nextState = if isExtended then state_extend.Remove(columnIndex) else state_extend.Add(columnIndex) -// setState_extend nextState -// ) -// prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] -// ] - -[] -let HeaderCell(columnIndex: int, state_extend: Set, setState_extend, model: Model, dispatch) = - let state = model.SpreadsheetModel - let header = state.ActiveTable.Headers.[columnIndex] - let cellValue = header.ToString() - let state_cell, setState_cell = React.useState(CellState.init(cellValue)) - Html.th [ - prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}" - cellStyle [ - //if isHeader && cell.Header.isInputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.LightBlue.Base) - //elif isHeader && cell.Header.isOutputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.Red.Lighter30) - //elif isHeader then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.DarkBlue.Base) - //if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) +let private extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = + let isExtended = state_extend.Contains(columnIndex) + Bulma.icon [ + prop.style [ + style.cursor.pointer ] - //prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) - prop.children [ - Html.div [ - cellInnerContainerStyle [] - prop.onDoubleClick(fun e -> - e.preventDefault() - e.stopPropagation() - UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - if not state_cell.Active then setState_cell {state_cell with Active = true} - ) - //prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) - prop.children [ - if state_cell.Active then - /// Update change to mainState and exit active input. - let updateMainStateTable() = - // Only update if changed - if state_cell.Value <> cellValue then - let nextHeader = CompositeHeader.OfHeaderString state_cell.Value - Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch - setState_cell {state_cell with Active = false} - cellInputElement(true, updateMainStateTable, setState_cell, state_cell, cellValue) - else - basicValueDisplayCell cellValue - //if isHeader && cell.Header.isTermColumn then - // extendHeaderButton(state_extend, columnIndex, setState_extend) + prop.onDoubleClick(fun e -> + e.stopPropagation() + e.preventDefault() + () + ) + prop.onClick(fun e -> + e.stopPropagation() + e.preventDefault() + let nextState = if isExtended then state_extend.Remove(columnIndex) else state_extend.Add(columnIndex) + setState_extend nextState + ) + prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] + ] + +type Cell = + + [] + static member Header(columnIndex: int, state_extend: Set, setState_extend, model: Model, dispatch) = + let state = model.SpreadsheetModel + let header = state.ActiveTable.Headers.[columnIndex] + let cellValue = header.ToString() + let state_cell, setState_cell = React.useState(CellState.init(cellValue)) + Html.th [ + prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}" + cellStyle [ + //if isHeader && cell.Header.isInputColumn then + // style.color(NFDIColors.white) + // style.backgroundColor(NFDIColors.LightBlue.Base) + //elif isHeader && cell.Header.isOutputColumn then + // style.color(NFDIColors.white) + // style.backgroundColor(NFDIColors.Red.Lighter30) + //elif isHeader then + // style.color(NFDIColors.white) + // style.backgroundColor(NFDIColors.DarkBlue.Base) + //if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) + ] + //prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + prop.children [ + Html.div [ + cellInnerContainerStyle [] + prop.onDoubleClick(fun e -> + e.preventDefault() + e.stopPropagation() + UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch + if not state_cell.Active then setState_cell {state_cell with Active = true} + ) + //prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) + prop.children [ + if state_cell.Active then + /// Update change to mainState and exit active input. + let updateMainStateTable() = + // Only update if changed + if state_cell.Value <> cellValue then + let nextHeader = CompositeHeader.OfHeaderString state_cell.Value + Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch + setState_cell {state_cell with Active = false} + cellInputElement(true, updateMainStateTable, setState_cell, state_cell, cellValue) + else + basicValueDisplayCell cellValue + extendHeaderButton(state_extend, columnIndex, setState_extend) + ] ] ] ] - ] -[] -let BodyCell(index: (int*int), state_extend: Set, setState_extend, model: Model, dispatch) = - let columnIndex, rowIndex = index - let state = model.SpreadsheetModel - let cell = state.ActiveTable.TryGetCellAt index |> Option.defaultValue CompositeCell.emptyFreeText - let cellValue = cell.GetContent().[0] - let state_cell, setState_cell = React.useState(CellState.init(cellValue)) - let isSelected = state.SelectedCells.Contains index - Html.td [ - prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" - cellStyle [ - //if isHeader && cell.Header.isInputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.LightBlue.Base) - //elif isHeader && cell.Header.isOutputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.Red.Lighter30) - //elif isHeader then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.DarkBlue.Base) - if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) - ] - prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) - prop.children [ - Html.div [ - cellInnerContainerStyle [] - prop.onDoubleClick(fun e -> - e.preventDefault() - e.stopPropagation() - UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - if not state_cell.Active then setState_cell {state_cell with Active = true} - ) - prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) - prop.children [ - if state_cell.Active then - /// Update change to mainState and exit active input. - let updateMainStateTable() = - // Only update if changed - if state_cell.Value <> cellValue then - let nextCell = cell.UpdateMainField state_cell.Value - Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - setState_cell {state_cell with Active = false} - cellInputElement(false, updateMainStateTable, setState_cell, state_cell, cellValue) - else - let displayName = - if cell.isUnitized then - let oaName = cell.ToOA().NameText - let unitizedValue = if cellValue = "" then "" else cellValue + " " + oaName - unitizedValue - else - cellValue - basicValueDisplayCell displayName - //if isHeader && cell.Header.isTermColumn then - // extendHeaderButton(state_extend, columnIndex, setState_extend) + [] + static member Body(index: (int*int), state_extend: Set, setState_extend, model: Model, dispatch) = + let columnIndex, rowIndex = index + let state = model.SpreadsheetModel + let cell = state.ActiveTable.Values.[index] // This creates huge issues + let cellValue = cell.GetContent().[0] + let state_cell, setState_cell = React.useState(CellState.init(cellValue)) + let isSelected = state.SelectedCells.Contains index + Html.td [ + prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" + cellStyle [ + //if isHeader && cell.Header.isInputColumn then + // style.color(NFDIColors.white) + // style.backgroundColor(NFDIColors.LightBlue.Base) + //elif isHeader && cell.Header.isOutputColumn then + // style.color(NFDIColors.white) + // style.backgroundColor(NFDIColors.Red.Lighter30) + //elif isHeader then + // style.color(NFDIColors.white) + // style.backgroundColor(NFDIColors.DarkBlue.Base) + if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) + ] + prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + prop.children [ + Html.div [ + cellInnerContainerStyle [] + prop.onDoubleClick(fun e -> + e.preventDefault() + e.stopPropagation() + UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch + if not state_cell.Active then setState_cell {state_cell with Active = true} + ) + prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) + prop.children [ + if state_cell.Active then + /// Update change to mainState and exit active input. + let updateMainStateTable() = + // Only update if changed + if state_cell.Value <> cellValue then + let nextCell = cell.UpdateMainField state_cell.Value + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + setState_cell {state_cell with Active = false} + cellInputElement(false, updateMainStateTable, setState_cell, state_cell, cellValue) + else + let displayName = + if cell.isUnitized then + let oaName = cell.ToOA().NameText + let unitizedValue = if cellValue = "" then "" else cellValue + " " + oaName + unitizedValue + else + cellValue + compositeCellDisplay cell + //if isHeader && cell.Header.isTermColumn then + // extendHeaderButton(state_extend, columnIndex, setState_extend) + ] ] ] - ] - ] \ No newline at end of file + ] \ No newline at end of file diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index 0831d964..4d435214 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -45,7 +45,7 @@ let private bodyRow (rowIndex: int) (state:Set) setState (model:Model) (dis Html.tr [ for columnIndex in 0 .. (table.ColumnCount-1) do let index = columnIndex, rowIndex - Cells.BodyCell(index, state, setState, model, dispatch) + Cells.Cell.Body (index, state, setState, model, dispatch) //Cell((columnIndex,rowIndex), state, setState, model, dispatch) //yield! referenceColumns(state, header, (column,row), model, dispatch) ] @@ -62,7 +62,7 @@ let private headerRow (state:Set) setState (model:Model) (dispatch: Msg -> Html.tr [ for columnIndex in 0 .. (table.ColumnCount-1) do yield - Cells.HeaderCell(columnIndex, state, setState, model, dispatch) + Cells.Cell.Header(columnIndex, state, setState, model, dispatch) //yield! referenceColumns(state, cell, (column,row), model, dispatch) ] From 900bbb383bc14089b6eb3cf3968af5689a9389c0 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 30 Jan 2024 11:03:50 +0100 Subject: [PATCH 010/135] restore reference columns for header :sparkles: --- package-lock.json | 7308 +++++++++--------- src/Client/MainComponents/Cells.fs | 107 +- src/Client/MainComponents/SpreadsheetView.fs | 29 +- 3 files changed, 3733 insertions(+), 3711 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63fd122d..89cd1e39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3658 +1,3658 @@ { - "name": "Swate", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@creativebulma/bulma-tooltip": "^1.2.0", - "@nfdi4plants/exceljs": "^0.3.0", - "bulma": "^0.9.4", - "bulma-checkradio": "^2.1.3", - "bulma-slider": "^2.0.5", - "bulma-switch": "^2.0.4", - "cytoscape": "^3.27.0", - "human-readable-ids": "^1.0.4", - "isomorphic-fetch": "^3.0.0", - "jsonschema": "^1.4.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "use-sync-external-store": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^20.10.3", - "@vitejs/plugin-basic-ssl": "^1.0.2", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "core-js": "^3.33.3", - "postcss": "^8.4.32", - "remotedev": "^0.2.9", - "sass": "^1.69.5", - "selfsigned": "^2.4.1", - "tailwindcss": "^3.3.6", - "vite": "^5.0.5" - }, - "engines": { - "node": "~18 || ~20", - "npm": "~9 || ~10" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", - "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.5", - "@babel/parser": "^7.23.5", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", - "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", - "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", - "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@creativebulma/bulma-tooltip": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", - "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", - "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", - "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", - "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", - "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", - "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", - "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", - "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", - "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", - "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", - "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", - "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", - "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", - "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", - "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", - "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", - "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", - "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", - "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", - "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", - "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", - "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fast-csv/format": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", - "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", - "dependencies": { - "@types/node": "^14.0.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isboolean": "^3.0.3", - "lodash.isequal": "^4.5.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0" - } - }, - "node_modules/@fast-csv/format/node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" - }, - "node_modules/@fast-csv/parse": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", - "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", - "dependencies": { - "@types/node": "^14.0.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.groupby": "^4.6.0", - "lodash.isfunction": "^3.0.9", - "lodash.isnil": "^4.0.0", - "lodash.isundefined": "^3.0.1", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@fast-csv/parse/node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nfdi4plants/exceljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nfdi4plants/exceljs/-/exceljs-0.3.0.tgz", - "integrity": "sha512-/IvHS3ozGyZ2jG1pYpMoUn2vz+GMzkdo8zUnhsfnn2175ajnjlQKQi7qVhp8Kgpvt/FtthcysrloOjlttbyJQQ==", - "dependencies": { - "archiver": "^5.0.0", - "dayjs": "^1.8.34", - "fast-csv": "^4.3.1", - "jszip": "^3.10.1", - "readable-stream": "^3.6.0", - "saxes": "^5.0.1", - "tmp": "^0.2.0", - "unzipper": "^0.10.11", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz", - "integrity": "sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz", - "integrity": "sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz", - "integrity": "sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz", - "integrity": "sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz", - "integrity": "sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz", - "integrity": "sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz", - "integrity": "sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", - "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz", - "integrity": "sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz", - "integrity": "sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz", - "integrity": "sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz", - "integrity": "sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.7", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", - "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", - "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/node": { - "version": "20.10.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", - "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", - "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", - "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", - "dev": true, - "engines": { - "node": ">=14.6.0" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", - "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.5", - "@babel/plugin-transform-react-jsx-self": "^7.23.3", - "@babel/plugin-transform-react-jsx-source": "^7.23.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" - } - }, - "node_modules/acorn": { - "version": "8.6.0", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/anymatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "engines": { - "node": ">=0.2.0" - } - }, - "node_modules/bulma": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", - "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" - }, - "node_modules/bulma-checkradio": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bulma-checkradio/-/bulma-checkradio-2.1.3.tgz", - "integrity": "sha512-8OmZ7PURyftNLGXSTNAYNTJHIe0OkoH/8z9iWfSXGxiv3AlrKneMtiVpBKofXsvc9ZHBUI1YjefiW5WFhgFgAQ==", - "dependencies": { - "bulma": "^0.9.3" - } - }, - "node_modules/bulma-slider": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/bulma-slider/-/bulma-slider-2.0.5.tgz", - "integrity": "sha512-6woD/1E7q1o5bfEaQjNqpWZaCItC1oHe9bN15WYB2ELqz2gDaJYZkf+rlozGpAYOXQGDQGCCv3y+QuKjx6sQuw==" - }, - "node_modules/bulma-switch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.4.tgz", - "integrity": "sha512-kMu4H0Pr0VjvfsnT6viRDCgptUq0Rvy7y7PX6q+IHg1xUynsjszPjhAdal5ysAlCG5HNO+5YXxeiu92qYGQolw==" - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001566", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", - "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", - "dev": true - }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/core-js": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.3.tgz", - "integrity": "sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cytoscape": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.27.0.tgz", - "integrity": "sha512-pPZJilfX9BxESwujODz5pydeGi+FBrXq1rcaB1mfhFXXFJ9GjE6CNndAk+8jPzoXGD+16LtSS4xlYEIUiW4Abg==", - "dependencies": { - "heap": "^0.2.6", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.488", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", - "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/esbuild": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", - "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.8", - "@esbuild/android-arm64": "0.19.8", - "@esbuild/android-x64": "0.19.8", - "@esbuild/darwin-arm64": "0.19.8", - "@esbuild/darwin-x64": "0.19.8", - "@esbuild/freebsd-arm64": "0.19.8", - "@esbuild/freebsd-x64": "0.19.8", - "@esbuild/linux-arm": "0.19.8", - "@esbuild/linux-arm64": "0.19.8", - "@esbuild/linux-ia32": "0.19.8", - "@esbuild/linux-loong64": "0.19.8", - "@esbuild/linux-mips64el": "0.19.8", - "@esbuild/linux-ppc64": "0.19.8", - "@esbuild/linux-riscv64": "0.19.8", - "@esbuild/linux-s390x": "0.19.8", - "@esbuild/linux-x64": "0.19.8", - "@esbuild/netbsd-x64": "0.19.8", - "@esbuild/openbsd-x64": "0.19.8", - "@esbuild/sunos-x64": "0.19.8", - "@esbuild/win32-arm64": "0.19.8", - "@esbuild/win32-ia32": "0.19.8", - "@esbuild/win32-x64": "0.19.8" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fast-csv": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", - "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", - "dependencies": { - "@fast-csv/format": "4.3.5", - "@fast-csv/parse": "4.3.6" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/heap": { - "version": "0.2.7", - "license": "MIT" - }, - "node_modules/human-readable-ids": { - "version": "1.0.4", - "license": "Apache2", - "dependencies": { - "knuth-shuffle": "^1.0.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/immutable": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/jsan": { - "version": "3.1.13", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonschema": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", - "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/knuth-shuffle": { - "version": "1.0.8", - "license": "(MIT OR Apache-2.0)" - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/linked-list": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", - "integrity": "sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg==", - "dev": true - }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, - "node_modules/lodash.groupby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", - "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.isfunction": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", - "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" - }, - "node_modules/lodash.isnil": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", - "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isundefined": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", - "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/querystring": { - "version": "0.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/remotedev": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.9.tgz", - "integrity": "sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA==", - "dev": true, - "dependencies": { - "jsan": "^3.1.3", - "querystring": "^0.2.0", - "rn-host-detect": "^1.0.1", - "socketcluster-client": "^13.0.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rn-host-detect": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.1.tgz", - "integrity": "sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.6.1", - "@rollup/rollup-android-arm64": "4.6.1", - "@rollup/rollup-darwin-arm64": "4.6.1", - "@rollup/rollup-darwin-x64": "4.6.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.6.1", - "@rollup/rollup-linux-arm64-gnu": "4.6.1", - "@rollup/rollup-linux-arm64-musl": "4.6.1", - "@rollup/rollup-linux-x64-gnu": "4.6.1", - "@rollup/rollup-linux-x64-musl": "4.6.1", - "@rollup/rollup-win32-arm64-msvc": "4.6.1", - "@rollup/rollup-win32-ia32-msvc": "4.6.1", - "@rollup/rollup-win32-x64-msvc": "4.6.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sc-channel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", - "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", - "dev": true, - "dependencies": { - "component-emitter": "1.2.1" - } - }, - "node_modules/sc-errors": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", - "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", - "dev": true - }, - "node_modules/sc-formatter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.3.tgz", - "integrity": "sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ==", - "dev": true - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/socketcluster-client": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-13.0.1.tgz", - "integrity": "sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw==", - "dev": true, - "dependencies": { - "base-64": "0.1.0", - "clone": "2.1.1", - "component-emitter": "1.2.1", - "linked-list": "0.1.0", - "querystring": "0.2.0", - "sc-channel": "^1.2.0", - "sc-errors": "^1.4.0", - "sc-formatter": "^3.0.1", - "uuid": "3.2.1", - "ws": "5.1.1" - } - }, - "node_modules/socketcluster-client/node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/socketcluster-client/node_modules/uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz", - "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.14.2", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "engines": { - "node": "*" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/unzipper/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/unzipper/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vite": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.5.tgz", - "integrity": "sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==", - "dev": true, - "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.1.tgz", - "integrity": "sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw==", - "dev": true, - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } + "name": "Swate", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@creativebulma/bulma-tooltip": "^1.2.0", + "@nfdi4plants/exceljs": "^0.3.0", + "bulma": "^0.9.4", + "bulma-checkradio": "^2.1.3", + "bulma-slider": "^2.0.5", + "bulma-switch": "^2.0.4", + "cytoscape": "^3.27.0", + "human-readable-ids": "^1.0.4", + "isomorphic-fetch": "^3.0.0", + "jsonschema": "^1.4.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "use-sync-external-store": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^20.10.3", + "@vitejs/plugin-basic-ssl": "^1.0.2", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "core-js": "^3.33.3", + "postcss": "^8.4.32", + "remotedev": "^0.2.9", + "sass": "^1.69.5", + "selfsigned": "^2.4.1", + "tailwindcss": "^3.3.6", + "vite": "^5.0.5" + }, + "engines": { + "node": "~18 || ~20", + "npm": "~9 || ~10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@creativebulma/bulma-tooltip": { + "version": "1.2.0", + "license": "MIT" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", + "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", + "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", + "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", + "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", + "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", + "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", + "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", + "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", + "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", + "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", + "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", + "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", + "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", + "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", + "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", + "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", + "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", + "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", + "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", + "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", + "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", + "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nfdi4plants/exceljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nfdi4plants/exceljs/-/exceljs-0.3.0.tgz", + "integrity": "sha512-/IvHS3ozGyZ2jG1pYpMoUn2vz+GMzkdo8zUnhsfnn2175ajnjlQKQi7qVhp8Kgpvt/FtthcysrloOjlttbyJQQ==", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz", + "integrity": "sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz", + "integrity": "sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz", + "integrity": "sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz", + "integrity": "sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz", + "integrity": "sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz", + "integrity": "sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz", + "integrity": "sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", + "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz", + "integrity": "sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz", + "integrity": "sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz", + "integrity": "sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz", + "integrity": "sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.7", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.7.tgz", + "integrity": "sha512-6Sfsq+EaaLrw4RmdFWE9Onp63TOUue71AWb4Gpa6JxzgTYtimbM086WnYTy2U67AofR++QKCo08ZP6pwx8YFHQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.4", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.4.tgz", + "integrity": "sha512-mSM/iKUk5fDDrEV/e83qY+Cr3I1+Q3qqTuEn++HAWYjEa1+NxZr6CNrcJGf2ZTnq4HoFGC3zaTPZTobCzCFukA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/node": { + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", + "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", + "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.6.0", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/bulma": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", + "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" + }, + "node_modules/bulma-checkradio": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bulma-checkradio/-/bulma-checkradio-2.1.3.tgz", + "integrity": "sha512-8OmZ7PURyftNLGXSTNAYNTJHIe0OkoH/8z9iWfSXGxiv3AlrKneMtiVpBKofXsvc9ZHBUI1YjefiW5WFhgFgAQ==", + "dependencies": { + "bulma": "^0.9.3" + } + }, + "node_modules/bulma-slider": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/bulma-slider/-/bulma-slider-2.0.5.tgz", + "integrity": "sha512-6woD/1E7q1o5bfEaQjNqpWZaCItC1oHe9bN15WYB2ELqz2gDaJYZkf+rlozGpAYOXQGDQGCCv3y+QuKjx6sQuw==" + }, + "node_modules/bulma-switch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.4.tgz", + "integrity": "sha512-kMu4H0Pr0VjvfsnT6viRDCgptUq0Rvy7y7PX6q+IHg1xUynsjszPjhAdal5ysAlCG5HNO+5YXxeiu92qYGQolw==" + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha512-h5FLmEMFHeuzqmpVRcDayNlVZ+k4uK1niyKQN6oUMe7ieJihv44Vc3dY/kDnnWX4PDQSwes48s965PG/D4GntQ==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", + "dev": true + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js": { + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.3.tgz", + "integrity": "sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cytoscape": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.27.0.tgz", + "integrity": "sha512-pPZJilfX9BxESwujODz5pydeGi+FBrXq1rcaB1mfhFXXFJ9GjE6CNndAk+8jPzoXGD+16LtSS4xlYEIUiW4Abg==", + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.488", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", + "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", + "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.8", + "@esbuild/android-arm64": "0.19.8", + "@esbuild/android-x64": "0.19.8", + "@esbuild/darwin-arm64": "0.19.8", + "@esbuild/darwin-x64": "0.19.8", + "@esbuild/freebsd-arm64": "0.19.8", + "@esbuild/freebsd-x64": "0.19.8", + "@esbuild/linux-arm": "0.19.8", + "@esbuild/linux-arm64": "0.19.8", + "@esbuild/linux-ia32": "0.19.8", + "@esbuild/linux-loong64": "0.19.8", + "@esbuild/linux-mips64el": "0.19.8", + "@esbuild/linux-ppc64": "0.19.8", + "@esbuild/linux-riscv64": "0.19.8", + "@esbuild/linux-s390x": "0.19.8", + "@esbuild/linux-x64": "0.19.8", + "@esbuild/netbsd-x64": "0.19.8", + "@esbuild/openbsd-x64": "0.19.8", + "@esbuild/sunos-x64": "0.19.8", + "@esbuild/win32-arm64": "0.19.8", + "@esbuild/win32-ia32": "0.19.8", + "@esbuild/win32-x64": "0.19.8" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/heap": { + "version": "0.2.7", + "license": "MIT" + }, + "node_modules/human-readable-ids": { + "version": "1.0.4", + "license": "Apache2", + "dependencies": { + "knuth-shuffle": "^1.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/immutable": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jsan": { + "version": "3.1.13", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/knuth-shuffle": { + "version": "1.0.8", + "license": "(MIT OR Apache-2.0)" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linked-list": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", + "integrity": "sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg==", + "dev": true + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/querystring": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/remotedev": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.9.tgz", + "integrity": "sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA==", + "dev": true, + "dependencies": { + "jsan": "^3.1.3", + "querystring": "^0.2.0", + "rn-host-detect": "^1.0.1", + "socketcluster-client": "^13.0.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rn-host-detect": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.1.tgz", + "integrity": "sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.6.1", + "@rollup/rollup-android-arm64": "4.6.1", + "@rollup/rollup-darwin-arm64": "4.6.1", + "@rollup/rollup-darwin-x64": "4.6.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.6.1", + "@rollup/rollup-linux-arm64-gnu": "4.6.1", + "@rollup/rollup-linux-arm64-musl": "4.6.1", + "@rollup/rollup-linux-x64-gnu": "4.6.1", + "@rollup/rollup-linux-x64-musl": "4.6.1", + "@rollup/rollup-win32-arm64-msvc": "4.6.1", + "@rollup/rollup-win32-ia32-msvc": "4.6.1", + "@rollup/rollup-win32-x64-msvc": "4.6.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sc-channel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", + "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", + "dev": true, + "dependencies": { + "component-emitter": "1.2.1" + } + }, + "node_modules/sc-errors": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "dev": true + }, + "node_modules/sc-formatter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.3.tgz", + "integrity": "sha512-lYI/lTs1u1c0geKElcj+bmEUfcP/HuKg2iDeTijPSjiTNFzN3Cf8Qh6tVd65oi7Qn+2/oD7LP4s6GC13v/9NiQ==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/socketcluster-client": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-13.0.1.tgz", + "integrity": "sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw==", + "dev": true, + "dependencies": { + "base-64": "0.1.0", + "clone": "2.1.1", + "component-emitter": "1.2.1", + "linked-list": "0.1.0", + "querystring": "0.2.0", + "sc-channel": "^1.2.0", + "sc-errors": "^1.4.0", + "sc-formatter": "^3.0.1", + "uuid": "3.2.1", + "ws": "5.1.1" + } + }, + "node_modules/socketcluster-client/node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/socketcluster-client/node_modules/uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz", + "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.14.2", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.5.tgz", + "integrity": "sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.1.tgz", + "integrity": "sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } } + } } diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index d8fe13f6..61f77a71 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -43,8 +43,9 @@ let private cellInnerContainerStyle (specificStyle: IStyleAttribute list) = prop yield! specificStyle ] -let private cellInputElement (isHeader: bool, updateMainStateTable: unit -> unit, setState_cell, state_cell, cell_value) = +let private cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: unit -> unit, setState_cell, state_cell, cell_value) = Bulma.input.text [ + prop.readOnly isReadOnly prop.autoFocus true prop.style [ if isHeader then style.fontWeight.bold @@ -305,77 +306,104 @@ let private extendHeaderButton (state_extend: Set, columnIndex, setState_ex prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] ] + +type ColumnType = +| Main +| Unit +| TSR +| TAN +with + member this.IsMainColumn = match this with | Main -> true | _ -> false + member this.IsRefColumn = match this with | Unit | TSR | TAN -> true | _ -> false + +open Shared + type Cell = [] - static member Header(columnIndex: int, state_extend: Set, setState_extend, model: Model, dispatch) = + static member HeaderBase(columnType: ColumnType, setter: string -> unit, cellValue: string, columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = let state = model.SpreadsheetModel - let header = state.ActiveTable.Headers.[columnIndex] - let cellValue = header.ToString() let state_cell, setState_cell = React.useState(CellState.init(cellValue)) + let isReadOnly = columnType = Unit Html.th [ + if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}" - cellStyle [ - //if isHeader && cell.Header.isInputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.LightBlue.Base) - //elif isHeader && cell.Header.isOutputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.Red.Lighter30) - //elif isHeader then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.DarkBlue.Base) - //if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) - ] - //prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + cellStyle [] prop.children [ Html.div [ cellInnerContainerStyle [] - prop.onDoubleClick(fun e -> + if not isReadOnly then prop.onDoubleClick(fun e -> e.preventDefault() e.stopPropagation() UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch if not state_cell.Active then setState_cell {state_cell with Active = true} ) - //prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) prop.children [ if state_cell.Active then /// Update change to mainState and exit active input. let updateMainStateTable() = // Only update if changed if state_cell.Value <> cellValue then - let nextHeader = CompositeHeader.OfHeaderString state_cell.Value - Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch + setter state_cell.Value setState_cell {state_cell with Active = false} - cellInputElement(true, updateMainStateTable, setState_cell, state_cell, cellValue) + cellInputElement(true, isReadOnly, updateMainStateTable, setState_cell, state_cell, cellValue) else + let cellValue = // shadow cell value for tsr and tan to add columnType + match columnType with + | TSR | TAN -> $"{columnType} ({cellValue})" + | _ -> cellValue basicValueDisplayCell cellValue - extendHeaderButton(state_extend, columnIndex, setState_extend) + if columnType = Main && not header.IsSingleColumn then + extendHeaderButton(state_extend, columnIndex, setState_extend) ] ] ] ] + static member Header(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = header.ToString() + let setter = + fun (s: string) -> + let mutable nextHeader = CompositeHeader.OfHeaderString s + // update header with ref columns if term column + if header.IsTermColumn && not header.IsFeaturedColumn then + nextHeader <- nextHeader.UpdateDeepWith header + Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch + Cell.HeaderBase(Main, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + + static member HeaderUnit(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = "Unit" + let setter = fun (s: string) -> () + Cell.HeaderBase(Unit, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + + static member private HeaderTANSetter (columnIndex: int, s: string, header: CompositeHeader, dispatch) = + let oa = header.TryOA() + let tan = if s = "" then None else Some s + oa + |> Option.map (fun x -> {x with TermAccessionNumber = tan}) + |> Option.map header.UpdateWithOA + |> Option.iter (fun nextHeader -> Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch) + + static member HeaderTSR(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" + let setter = fun (s: string) -> Cell.HeaderTANSetter(columnIndex, s, header, dispatch) + Cell.HeaderBase(TSR, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + + static member HeaderTAN(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" + let setter = fun (s: string) -> Cell.HeaderTANSetter(columnIndex, s, header, dispatch) + Cell.HeaderBase(TAN, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + [] - static member Body(index: (int*int), state_extend: Set, setState_extend, model: Model, dispatch) = + static member Body(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let columnIndex, rowIndex = index let state = model.SpreadsheetModel - let cell = state.ActiveTable.Values.[index] // This creates huge issues let cellValue = cell.GetContent().[0] let state_cell, setState_cell = React.useState(CellState.init(cellValue)) let isSelected = state.SelectedCells.Contains index Html.td [ prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" cellStyle [ - //if isHeader && cell.Header.isInputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.LightBlue.Base) - //elif isHeader && cell.Header.isOutputColumn then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.Red.Lighter30) - //elif isHeader then - // style.color(NFDIColors.white) - // style.backgroundColor(NFDIColors.DarkBlue.Base) if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) ] prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) @@ -398,18 +426,9 @@ type Cell = let nextCell = cell.UpdateMainField state_cell.Value Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch setState_cell {state_cell with Active = false} - cellInputElement(false, updateMainStateTable, setState_cell, state_cell, cellValue) + cellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue) else - let displayName = - if cell.isUnitized then - let oaName = cell.ToOA().NameText - let unitizedValue = if cellValue = "" then "" else cellValue + " " + oaName - unitizedValue - else - cellValue compositeCellDisplay cell - //if isHeader && cell.Header.isTermColumn then - // extendHeaderButton(state_extend, columnIndex, setState_extend) ] ] ] diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index 4d435214..508339be 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -10,16 +10,16 @@ open ARCtrl.ISA open Shared -//let private referenceColumns (state:Set, header:SwateCell, (columnIndex: int, rowIndex:int), model, dispatch) = -// if header.Header.isTermColumn then -// [ -// let isExtended = state.Contains(columnIndex) -// if isExtended then -// if header.Header.HasUnit then -// yield UnitCell((columnIndex,rowIndex), model, dispatch) -// yield TANCell((columnIndex,rowIndex), model, dispatch) -// ] -// else [] +let private referenceColumns (columnIndex, header: CompositeHeader, state: Set, setState, model, dispatch) = + if header.IsTermColumn then + [ + let isExtended = state.Contains columnIndex + if isExtended then + Cell.HeaderUnit(columnIndex, header, state, setState, model, dispatch) + Cell.HeaderTSR(columnIndex, header, state, setState, model, dispatch) + Cell.HeaderTAN(columnIndex, header, state, setState, model, dispatch) + ] + else [] let cellPlaceholder (c_opt: CompositeCell option) = let tableCell (children: ReactElement list) = Html.td [ @@ -45,7 +45,9 @@ let private bodyRow (rowIndex: int) (state:Set) setState (model:Model) (dis Html.tr [ for columnIndex in 0 .. (table.ColumnCount-1) do let index = columnIndex, rowIndex - Cells.Cell.Body (index, state, setState, model, dispatch) + let state = model.SpreadsheetModel + let cell = state.ActiveTable.Values.[index] + Cells.Cell.Body (index, cell, model, dispatch) //Cell((columnIndex,rowIndex), state, setState, model, dispatch) //yield! referenceColumns(state, header, (column,row), model, dispatch) ] @@ -61,9 +63,10 @@ let private headerRow (state:Set) setState (model:Model) (dispatch: Msg -> let table = model.SpreadsheetModel.ActiveTable Html.tr [ for columnIndex in 0 .. (table.ColumnCount-1) do + let header = table.Headers.[columnIndex] yield - Cells.Cell.Header(columnIndex, state, setState, model, dispatch) - //yield! referenceColumns(state, cell, (column,row), model, dispatch) + Cells.Cell.Header(columnIndex, header, state, setState, model, dispatch) + yield! referenceColumns(columnIndex, header, state, setState, model, dispatch) ] [] From 70d07ca28029f06cf4b22b6bfed70b8f7edf6765 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 30 Jan 2024 14:43:22 +0100 Subject: [PATCH 011/135] fix wrong automatic file read :bug:#340 --- src/Client/Spreadsheet/IO.fs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Client/Spreadsheet/IO.fs b/src/Client/Spreadsheet/IO.fs index c191ec0b..fb9552a1 100644 --- a/src/Client/Spreadsheet/IO.fs +++ b/src/Client/Spreadsheet/IO.fs @@ -36,15 +36,19 @@ let private converters = [tryToConvertAssay; tryToConvertStudy; tryToConvertInve // TODO: Can this be done better? If we want to allow upload of any isa.xlsx file? let readFromBytes (bytes: byte []) = // Try each conversion function and return the first successful result - let rec tryConvert (converters: ('a -> 'b option) list) (json: 'a) : 'b = - match converters with - | [] -> failwith "Unable to parse json to supported isa file." - | convert :: rest -> - match convert json with - | Some result -> result - | None -> tryConvert rest json promise { let! fswb = FsSpreadsheet.Exceljs.Xlsx.fromBytes bytes - let arcFile = tryConvert converters fswb - return arcFile + let ws = fswb.GetWorksheets() + let arcfile = + match ws with + | _ when ws.Exists (fun ws -> ARCtrl.ISA.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name ) -> + ArcAssay.fromFsWorkbook fswb |> Assay |> Some + | _ when ws.Exists (fun ws -> ARCtrl.ISA.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name ) -> + ArcStudy.fromFsWorkbook fswb |> Study |> Some + | _ when ws.Exists (fun ws -> ARCtrl.ISA.Spreadsheet.ArcInvestigation.metaDataSheetName = ws.Name ) -> + ArcInvestigation.fromFsWorkbook fswb |> Investigation |> Some + | _ when ws.Exists (fun ws -> ARCtrl.Template.Spreadsheet.Template.metaDataSheetName = ws.Name ) -> + ARCtrl.Template.Spreadsheet.Template.fromFsWorkbook fswb |> Template |> Some + | _ -> None + return arcfile } \ No newline at end of file From 46d8a21a1fc18c83fb60f1f563e6e068b0e8e021 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 30 Jan 2024 14:52:42 +0100 Subject: [PATCH 012/135] restore body reference cells :sparkles: --- paket.dependencies | 4 +- paket.lock | 44 +-- src/Client/MainComponents/AddRows.fs | 2 +- src/Client/MainComponents/Cells.fs | 333 +++++++++++-------- src/Client/MainComponents/SpreadsheetView.fs | 22 +- src/Client/Spreadsheet/IO.fs | 37 +-- 6 files changed, 245 insertions(+), 197 deletions(-) diff --git a/paket.dependencies b/paket.dependencies index 42e97157..0cffb9b6 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -2,12 +2,12 @@ source https://api.nuget.org/v3/index.json framework: net8.0 storage: none -nuget ARCtrl 1.0.6 +nuget ARCtrl 1.0.7 nuget Feliz.Bulma.Checkradio nuget Feliz.Bulma.Switch nuget Fsharp.Core ~> 8 nuget Fable.Remoting.Giraffe ~> 5 -nuget FsSpreadsheet.Exceljs ~> 5.1.1 +nuget FsSpreadsheet.Exceljs ~> 5.1.2 nuget Saturn ~> 0 nuget Fable.Core ~> 4 diff --git a/paket.lock b/paket.lock index af4d3e42..06a2c0e8 100644 --- a/paket.lock +++ b/paket.lock @@ -2,38 +2,38 @@ STORAGE: NONE RESTRICTION: == net8.0 NUGET remote: https://api.nuget.org/v3/index.json - ARCtrl (1.0.6) - ARCtrl.Contract (>= 1.0.6) - ARCtrl.CWL (>= 1.0.6) - ARCtrl.FileSystem (>= 1.0.6) - ARCtrl.ISA (>= 1.0.6) - ARCtrl.ISA.Json (>= 1.0.6) - ARCtrl.ISA.Spreadsheet (>= 1.0.6) + ARCtrl (1.0.7) + ARCtrl.Contract (>= 1.0.7) + ARCtrl.CWL (>= 1.0.7) + ARCtrl.FileSystem (>= 1.0.7) + ARCtrl.ISA (>= 1.0.7) + ARCtrl.ISA.Json (>= 1.0.7) + ARCtrl.ISA.Spreadsheet (>= 1.0.7) Fable.Fetch (>= 2.6) Fable.SimpleHttp (>= 3.5) FSharp.Core (>= 6.0.6) - ARCtrl.Contract (1.0.6) - ARCtrl.ISA (>= 1.0.6) + ARCtrl.Contract (1.0.7) + ARCtrl.ISA (>= 1.0.7) FSharp.Core (>= 6.0.6) - ARCtrl.CWL (1.0.6) + ARCtrl.CWL (1.0.7) FSharp.Core (>= 6.0.6) - ARCtrl.FileSystem (1.0.6) + ARCtrl.FileSystem (1.0.7) Fable.Core (>= 4.2) FSharp.Core (>= 6.0.6) - ARCtrl.ISA (1.0.6) - ARCtrl.FileSystem (>= 1.0.6) + ARCtrl.ISA (1.0.7) + ARCtrl.FileSystem (>= 1.0.7) FSharp.Core (>= 6.0.6) - ARCtrl.ISA.Json (1.0.6) - ARCtrl.ISA (>= 1.0.6) + ARCtrl.ISA.Json (1.0.7) + ARCtrl.ISA (>= 1.0.7) FSharp.Core (>= 6.0.6) NJsonSchema (>= 10.8) Thoth.Json (>= 10.1) Thoth.Json.Net (>= 11.0) - ARCtrl.ISA.Spreadsheet (1.0.6) - ARCtrl.FileSystem (>= 1.0.6) - ARCtrl.ISA (>= 1.0.6) + ARCtrl.ISA.Spreadsheet (1.0.7) + ARCtrl.FileSystem (>= 1.0.7) + ARCtrl.ISA (>= 1.0.7) FSharp.Core (>= 6.0.6) - FsSpreadsheet (>= 5.1.1) + FsSpreadsheet (>= 5.1.2) ExcelJS.Fable (0.3) Fable.Core (>= 3.2.8) Fable.React (>= 7.4.1) @@ -186,14 +186,14 @@ NUGET FSharp.Core (>= 6.0) Microsoft.IO.RecyclableMemoryStream (>= 2.2.1) FSharp.Core (8.0.101) - FsSpreadsheet (5.1.1) + FsSpreadsheet (5.1.2) Fable.Core (>= 4.0) FSharp.Core (>= 6.0.6) - FsSpreadsheet.Exceljs (5.1.1) + FsSpreadsheet.Exceljs (5.1.2) Fable.Exceljs (>= 1.6) Fable.Promise (>= 3.2) FSharp.Core (>= 6.0.6) - FsSpreadsheet (>= 5.1.1) + FsSpreadsheet (>= 5.1.2) Giraffe (6.2) FSharp.Core (>= 6.0) Giraffe.ViewEngine (>= 1.4) diff --git a/src/Client/MainComponents/AddRows.fs b/src/Client/MainComponents/AddRows.fs index 07857199..fc55b862 100644 --- a/src/Client/MainComponents/AddRows.fs +++ b/src/Client/MainComponents/AddRows.fs @@ -12,7 +12,6 @@ let Main (dispatch: Messages.Msg -> unit) = let state_rows, setState_rows = React.useState(init_RowsToAdd) Html.div [ prop.id "ExpandTable" - prop.title "Add rows" prop.style [ style.flexGrow 1; style.justifyContent.center; style.display.inheritFromParent; style.padding(length.rem 1) style.position.sticky; style.left 0 @@ -30,6 +29,7 @@ let Main (dispatch: Messages.Msg -> unit) = ] Bulma.button.a [ Bulma.button.isRounded + prop.title "Add rows" prop.onClick(fun _ -> let inp = Browser.Dom.document.getElementById "n_row_input" inp?Value <- init_RowsToAdd diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 61f77a71..6187b121 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -9,116 +9,171 @@ open Messages open Shared open ARCtrl.ISA +type private CellMode = +| Active +| Idle +| Search + type private CellState = { - Active: bool + CellMode: CellMode /// This value is used to show during input cell editing. After confirming edit it will be used to push update Value: string } with static member init() = { - Active = false + CellMode = Idle Value = "" } static member init(v: string) = { - Active = false + CellMode = Idle Value = v } + member this.IsActive = this.CellMode = Active + member this.IsSearch = this.CellMode = Search + member this.IsIdle = this.CellMode = Idle + +type private ColumnType = +| Main +| Unit +| TSR +| TAN +with + member this.IsMainColumn = match this with | Main -> true | _ -> false + member this.IsRefColumn = match this with | Unit | TSR | TAN -> true | _ -> false + + +module private CellComponents = + -let private cellStyle (specificStyle: IStyleAttribute list) = prop.style [ - style.minWidth 100 - style.height 22 - style.border(length.px 1, borderStyle.solid, "darkgrey") - yield! specificStyle - ] - -let private cellInnerContainerStyle (specificStyle: IStyleAttribute list) = prop.style [ - style.display.flex; - style.justifyContent.spaceBetween; - style.height(length.percent 100); - style.minHeight(35) - style.width(length.percent 100) - style.alignItems.center - yield! specificStyle - ] - -let private cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: unit -> unit, setState_cell, state_cell, cell_value) = - Bulma.input.text [ - prop.readOnly isReadOnly - prop.autoFocus true - prop.style [ - if isHeader then style.fontWeight.bold + let cellStyle (specificStyle: IStyleAttribute list) = prop.style [ + style.minWidth 100 + style.height 22 + style.border(length.px 1, borderStyle.solid, "darkgrey") + yield! specificStyle + ] + + let cellInnerContainerStyle (specificStyle: IStyleAttribute list) = prop.style [ + style.display.flex; + style.justifyContent.spaceBetween; + style.height(length.percent 100); + style.minHeight(35) style.width(length.percent 100) - style.height.unset - style.borderRadius(0) - style.border(0,borderStyle.none,"") - style.backgroundColor.transparent - style.margin (0) - style.padding(length.em 0.5,length.em 0.75) - //if isHeader then - // style.color(NFDIColors.white) + style.alignItems.center + yield! specificStyle ] - // Update main spreadsheet state when leaving focus or... - prop.onBlur(fun _ -> - updateMainStateTable() - ) - // .. when pressing "ENTER". "ESCAPE" will negate changes. - prop.onKeyDown(fun e -> - match e.which with - | 13. -> //enter + + let cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: unit -> unit, setState_cell, state_cell, cell_value) = + Bulma.input.text [ + prop.readOnly isReadOnly + prop.autoFocus true + prop.style [ + if isHeader then style.fontWeight.bold + style.width(length.percent 100) + style.height.unset + style.borderRadius(0) + style.border(0,borderStyle.none,"") + style.backgroundColor.transparent + style.margin (0) + style.padding(length.em 0.5,length.em 0.75) + //if isHeader then + // style.color(NFDIColors.white) + ] + // Update main spreadsheet state when leaving focus or... + prop.onBlur(fun _ -> updateMainStateTable() - | 27. -> //escape - setState_cell {Active = false; Value = cell_value} - | _ -> () - ) - // Only change cell value while typing to increase performance. - prop.onChange(fun e -> - setState_cell {state_cell with Value = e} - ) - prop.defaultValue cell_value - ] - -let private basicValueDisplayCell (v: string) = - Html.span [ - prop.style [ - style.flexGrow 1 - style.padding(length.em 0.5,length.em 0.75) + ) + // .. when pressing "ENTER". "ESCAPE" will negate changes. + prop.onKeyDown(fun e -> + match e.which with + | 13. -> //enter + updateMainStateTable() + | 27. -> //escape + setState_cell {CellMode = Idle; Value = cell_value} + | _ -> () + ) + // Only change cell value while typing to increase performance. + prop.onChange(fun e -> + setState_cell {state_cell with Value = e} + ) + prop.defaultValue cell_value ] - prop.text v - ] - -let private compositeCellDisplay (cc: CompositeCell) = - let ccType = match cc with | CompositeCell.Term oa -> "T" | CompositeCell.Unitized _ -> "U" | CompositeCell.FreeText _ -> "F" - let v = cc.ToString() - Html.div [ - prop.classes ["is-flex"] - prop.style [ - style.flexGrow 1 - style.padding(length.em 0.5,length.em 0.75) + + let basicValueDisplayCell (v: string) = + Html.span [ + prop.style [ + style.flexGrow 1 + style.padding(length.em 0.5,length.em 0.75) + ] + prop.text v ] - prop.children [ - Html.span [ - prop.style [ - style.flexGrow 1 + + let compositeCellDisplay (cc: CompositeCell) = + let ccType = match cc with | CompositeCell.Term oa -> "T" | CompositeCell.Unitized _ -> "U" | CompositeCell.FreeText _ -> "F" + let v = cc.ToString() + Html.div [ + prop.classes ["is-flex"] + prop.style [ + style.flexGrow 1 + style.padding(length.em 0.5,length.em 0.75) + ] + prop.children [ + Html.span [ + prop.style [ + style.flexGrow 1 + ] + prop.text v + ] + Html.span [ + prop.style [style.custom("marginLeft", "auto")] + prop.text ccType ] - prop.text v ] - Html.span [ - prop.style [style.custom("marginLeft", "auto")] - prop.text ccType + ] + + let extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = + let isExtended = state_extend.Contains(columnIndex) + Bulma.icon [ + prop.style [ + style.cursor.pointer ] + prop.onDoubleClick(fun e -> + e.stopPropagation() + e.preventDefault() + () + ) + prop.onClick(fun e -> + e.stopPropagation() + e.preventDefault() + let nextState = if isExtended then state_extend.Remove(columnIndex) else state_extend.Add(columnIndex) + setState_extend nextState + ) + prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] ] - ] + + +module private CellAux = + + let headerTANSetter (columnIndex: int, s: string, header: CompositeHeader, dispatch) = + let oa = header.TryOA() + let tan = if s = "" then None else Some s + oa + |> Option.map (fun x -> {x with TermAccessionNumber = tan}) + |> Option.map header.UpdateWithOA + |> Option.iter (fun nextHeader -> Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch) + +open CellComponents +open CellAux module private EventPresets = open Shared - let onClickSelect (index: int*int, state_cell, selectedCells: Set, model:Messages.Model, dispatch)= + let onClickSelect (index: int*int, state_cell: CellState, selectedCells: Set, model:Messages.Model, dispatch)= fun (e: Browser.Types.MouseEvent) -> // don't select cell if active(editable) - if not state_cell.Active then + if state_cell.IsIdle then let set = match e.ctrlKey with | true -> @@ -286,42 +341,13 @@ module private EventPresets = // ] // ] -let private extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = - let isExtended = state_extend.Contains(columnIndex) - Bulma.icon [ - prop.style [ - style.cursor.pointer - ] - prop.onDoubleClick(fun e -> - e.stopPropagation() - e.preventDefault() - () - ) - prop.onClick(fun e -> - e.stopPropagation() - e.preventDefault() - let nextState = if isExtended then state_extend.Remove(columnIndex) else state_extend.Add(columnIndex) - setState_extend nextState - ) - prop.children [Html.i [prop.classes ["fa-sharp"; "fa-solid"; "fa-angles-up"; if isExtended then "fa-rotate-270" else "fa-rotate-90"]; prop.style [style.fontSize(length.em 1)]]] - ] - - -type ColumnType = -| Main -| Unit -| TSR -| TAN -with - member this.IsMainColumn = match this with | Main -> true | _ -> false - member this.IsRefColumn = match this with | Unit | TSR | TAN -> true | _ -> false open Shared type Cell = [] - static member HeaderBase(columnType: ColumnType, setter: string -> unit, cellValue: string, columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + static member private HeaderBase(columnType: ColumnType, setter: string -> unit, cellValue: string, columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = let state = model.SpreadsheetModel let state_cell, setState_cell = React.useState(CellState.init(cellValue)) let isReadOnly = columnType = Unit @@ -336,16 +362,16 @@ type Cell = e.preventDefault() e.stopPropagation() UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - if not state_cell.Active then setState_cell {state_cell with Active = true} + if state_cell.IsIdle then setState_cell {state_cell with CellMode = Active} ) prop.children [ - if state_cell.Active then + if state_cell.IsActive then /// Update change to mainState and exit active input. let updateMainStateTable() = // Only update if changed if state_cell.Value <> cellValue then setter state_cell.Value - setState_cell {state_cell with Active = false} + setState_cell {state_cell with CellMode = Idle} cellInputElement(true, isReadOnly, updateMainStateTable, setState_cell, state_cell, cellValue) else let cellValue = // shadow cell value for tsr and tan to add columnType @@ -376,29 +402,23 @@ type Cell = let setter = fun (s: string) -> () Cell.HeaderBase(Unit, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) - static member private HeaderTANSetter (columnIndex: int, s: string, header: CompositeHeader, dispatch) = - let oa = header.TryOA() - let tan = if s = "" then None else Some s - oa - |> Option.map (fun x -> {x with TermAccessionNumber = tan}) - |> Option.map header.UpdateWithOA - |> Option.iter (fun nextHeader -> Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch) - static member HeaderTSR(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" - let setter = fun (s: string) -> Cell.HeaderTANSetter(columnIndex, s, header, dispatch) + let setter = fun (s: string) -> headerTANSetter(columnIndex, s, header, dispatch) Cell.HeaderBase(TSR, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) static member HeaderTAN(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" - let setter = fun (s: string) -> Cell.HeaderTANSetter(columnIndex, s, header, dispatch) + let setter = fun (s: string) -> headerTANSetter(columnIndex, s, header, dispatch) Cell.HeaderBase(TAN, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + static member Empty() = + Html.td [ cellStyle []; prop.readOnly true ] + [] - static member Body(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch) = let columnIndex, rowIndex = index let state = model.SpreadsheetModel - let cellValue = cell.GetContent().[0] let state_cell, setState_cell = React.useState(CellState.init(cellValue)) let isSelected = state.SelectedCells.Contains index Html.td [ @@ -414,22 +434,69 @@ type Cell = e.preventDefault() e.stopPropagation() UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - if not state_cell.Active then setState_cell {state_cell with Active = true} + let mode = if e.ctrlKey && columnType = Main then Search else Active + if state_cell.IsIdle then setState_cell {state_cell with CellMode = mode} ) prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) prop.children [ - if state_cell.Active then + match state_cell.CellMode with + | Active -> /// Update change to mainState and exit active input. let updateMainStateTable() = // Only update if changed if state_cell.Value <> cellValue then - let nextCell = cell.UpdateMainField state_cell.Value - Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - setState_cell {state_cell with Active = false} + setter state_cell.Value + setState_cell {state_cell with CellMode = Idle} cellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue) - else - compositeCellDisplay cell + | Search -> + let setter = fun oa -> log oa + Components.TermSearch.Input(setter, fullwidth=true) + | Idle -> + if columnType = Main then + compositeCellDisplay cell + else + basicValueDisplayCell cellValue ] ] ] - ] \ No newline at end of file + ] + + static member Body(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let cellValue = cell.GetContent().[0] + let setter = fun (s: string) -> + let nextCell = cell.UpdateMainField s + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(Main, cellValue, setter, index, cell, model, dispatch) + + static member BodyUnit(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let cellValue = cell.GetContent().[1] + let setter = fun (s: string) -> + let oa = cell.ToOA() + let newName = if s = "" then None else Some s + let nextOA = {oa with Name = newName } + let nextCell = cell.UpdateWithOA nextOA + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(Unit, cellValue, setter, index, cell, model, dispatch) + + static member BodyTSR(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let contentIndex = if cell.isUnitized then 2 else 1 + let cellValue = cell.GetContent().[contentIndex] + let setter = fun (s: string) -> + let oa = cell.ToOA() + let newTSR = if s = "" then None else Some s + let nextOA = {oa with TermSourceREF = newTSR} + let nextCell = cell.UpdateWithOA nextOA + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(TSR, cellValue, setter, index, cell, model, dispatch) + + static member BodyTAN(index: (int*int), cell: CompositeCell, model: Model, dispatch) = + let contentIndex = if cell.isUnitized then 3 else 2 + let cellValue = cell.GetContent().[contentIndex] + let setter = fun (s: string) -> + let oa = cell.ToOA() + let newTAN = if s = "" then None else Some s + let nextOA = {oa with TermAccessionNumber = newTAN} + let nextCell = cell.UpdateWithOA nextOA + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(TAN, cellValue, setter, index, cell, model, dispatch) + \ No newline at end of file diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index 508339be..f0d86b96 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -45,11 +45,16 @@ let private bodyRow (rowIndex: int) (state:Set) setState (model:Model) (dis Html.tr [ for columnIndex in 0 .. (table.ColumnCount-1) do let index = columnIndex, rowIndex - let state = model.SpreadsheetModel - let cell = state.ActiveTable.Values.[index] + let cell = model.SpreadsheetModel.ActiveTable.Values.[index] Cells.Cell.Body (index, cell, model, dispatch) - //Cell((columnIndex,rowIndex), state, setState, model, dispatch) - //yield! referenceColumns(state, header, (column,row), model, dispatch) + let isExtended = state.Contains columnIndex + if isExtended && (cell.isTerm || cell.isUnitized) then + if cell.isUnitized then + Cell.BodyUnit(index, cell, model, dispatch) + else + Cell.Empty() + Cell.BodyTSR(index, cell, model, dispatch) + Cell.BodyTAN(index, cell, model, dispatch) ] let private bodyRows (state:Set) setState (model:Model) (dispatch: Msg -> unit) = @@ -64,9 +69,12 @@ let private headerRow (state:Set) setState (model:Model) (dispatch: Msg -> Html.tr [ for columnIndex in 0 .. (table.ColumnCount-1) do let header = table.Headers.[columnIndex] - yield - Cells.Cell.Header(columnIndex, header, state, setState, model, dispatch) - yield! referenceColumns(columnIndex, header, state, setState, model, dispatch) + Cells.Cell.Header(columnIndex, header, state, setState, model, dispatch) + let isExtended = state.Contains columnIndex + if isExtended then + Cell.HeaderUnit(columnIndex, header, state, setState, model, dispatch) + Cell.HeaderTSR(columnIndex, header, state, setState, model, dispatch) + Cell.HeaderTAN(columnIndex, header, state, setState, model, dispatch) ] [] diff --git a/src/Client/Spreadsheet/IO.fs b/src/Client/Spreadsheet/IO.fs index fb9552a1..169b2668 100644 --- a/src/Client/Spreadsheet/IO.fs +++ b/src/Client/Spreadsheet/IO.fs @@ -6,33 +6,6 @@ open FsSpreadsheet.Exceljs open Shared open FsSpreadsheet -let private tryToConvertAssay (fswb: FsWorkbook) = - try - ArcAssay.fromFsWorkbook fswb |> Assay |> Some - with - | _ -> None - -let private tryToConvertStudy (fswb: FsWorkbook) = - try - ArcStudy.fromFsWorkbook fswb |> Study |> Some - with - | _ -> None - -let private tryToConvertInvestigation (fswb: FsWorkbook) = - try - ArcInvestigation.fromFsWorkbook fswb |> Investigation |> Some - with - | _ -> None - -let private tryToConvertTemplate (fswb: FsWorkbook) = - try - ARCtrl.Template.Spreadsheet.Template.fromFsWorkbook fswb |> Template |> Some - with - | _ -> None - -// List of conversion functions -let private converters = [tryToConvertAssay; tryToConvertStudy; tryToConvertInvestigation; tryToConvertTemplate] - // TODO: Can this be done better? If we want to allow upload of any isa.xlsx file? let readFromBytes (bytes: byte []) = // Try each conversion function and return the first successful result @@ -42,13 +15,13 @@ let readFromBytes (bytes: byte []) = let arcfile = match ws with | _ when ws.Exists (fun ws -> ARCtrl.ISA.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name ) -> - ArcAssay.fromFsWorkbook fswb |> Assay |> Some + ArcAssay.fromFsWorkbook fswb |> Assay | _ when ws.Exists (fun ws -> ARCtrl.ISA.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name ) -> - ArcStudy.fromFsWorkbook fswb |> Study |> Some + ArcStudy.fromFsWorkbook fswb |> Study | _ when ws.Exists (fun ws -> ARCtrl.ISA.Spreadsheet.ArcInvestigation.metaDataSheetName = ws.Name ) -> - ArcInvestigation.fromFsWorkbook fswb |> Investigation |> Some + ArcInvestigation.fromFsWorkbook fswb |> Investigation | _ when ws.Exists (fun ws -> ARCtrl.Template.Spreadsheet.Template.metaDataSheetName = ws.Name ) -> - ARCtrl.Template.Spreadsheet.Template.fromFsWorkbook fswb |> Template |> Some - | _ -> None + ARCtrl.Template.Spreadsheet.Template.fromFsWorkbook fswb |> Template + | _ -> failwith "Unable to identify given file. Missing metadata sheet with correct name." return arcfile } \ No newline at end of file From 126dab369300727be3694aab77dbd62a9e5a795f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 30 Jan 2024 15:46:21 +0100 Subject: [PATCH 013/135] Improve term search in table cell :sparkles: --- src/Client/MainComponents/Cells.fs | 33 +++++++++++++------ .../SharedComponents/TermSearchInput.fs | 19 ++++++++--- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 6187b121..f9d7ca2e 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -175,8 +175,10 @@ module private EventPresets = // don't select cell if active(editable) if state_cell.IsIdle then let set = - match e.ctrlKey with - | true -> + match e.ctrlKey, selectedCells.Count with + | true, 0 -> + selectedCells + | true, _ -> let createSetOfIndex (columnMin:int, columnMax, rowMin:int, rowMax: int) = [ for c in columnMin .. columnMax do @@ -189,7 +191,7 @@ module private EventPresets = let rowMin, rowMax = System.Math.Min(snd source, snd target), System.Math.Max(snd source, snd target) let set = createSetOfIndex (columnMin,columnMax,rowMin,rowMax) set - | false -> + | false, _ -> let next = if selectedCells = Set([index]) then Set.empty else Set([index]) next UpdateSelectedCells set |> SpreadsheetMsg |> dispatch @@ -416,11 +418,12 @@ type Cell = Html.td [ cellStyle []; prop.readOnly true ] [] - static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch) = + static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation option -> unit) = let columnIndex, rowIndex = index let state = model.SpreadsheetModel let state_cell, setState_cell = React.useState(CellState.init(cellValue)) let isSelected = state.SelectedCells.Contains index + let makeIdle() = setState_cell {state_cell with CellMode = Idle} Html.td [ prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" cellStyle [ @@ -433,11 +436,11 @@ type Cell = prop.onDoubleClick(fun e -> e.preventDefault() e.stopPropagation() - UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch let mode = if e.ctrlKey && columnType = Main then Search else Active if state_cell.IsIdle then setState_cell {state_cell with CellMode = mode} + UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch ) - prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) + if state_cell.IsIdle then prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) prop.children [ match state_cell.CellMode with | Active -> @@ -446,11 +449,17 @@ type Cell = // Only update if changed if state_cell.Value <> cellValue then setter state_cell.Value - setState_cell {state_cell with CellMode = Idle} + makeIdle() cellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue) | Search -> - let setter = fun oa -> log oa - Components.TermSearch.Input(setter, fullwidth=true) + if oasetter.IsSome then + let headerOA = state.ActiveTable.Headers.[columnIndex].TryOA() + let oa = cell.ToOA() + let onBlur = fun e -> makeIdle() + let onEscape = fun e -> makeIdle() + Components.TermSearch.Input(oasetter.Value, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, autofocus=true, borderRadius=0) + else + printfn "No setter for OntologyAnnotation given for table cell term search." | Idle -> if columnType = Main then compositeCellDisplay cell @@ -466,7 +475,11 @@ type Cell = let setter = fun (s: string) -> let nextCell = cell.UpdateMainField s Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - Cell.BodyBase(Main, cellValue, setter, index, cell, model, dispatch) + let oaSetter = fun (oa:OntologyAnnotation option) -> + let nextCell = oa |> Option.map cell.UpdateWithOA + if nextCell.IsSome then + Msg.UpdateCell (index, nextCell.Value) |> SpreadsheetMsg |> dispatch + Cell.BodyBase(Main, cellValue, setter, index, cell, model, dispatch, oaSetter) static member BodyUnit(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let cellValue = cell.GetContent().[1] diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 7f922d80..b9963361 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -140,7 +140,7 @@ module TermSearchAux = let termSelectItemMain (term: TermTypes.Term, show, setShow, setTerm, isDirectedSearchResult: bool) = Html.div [ prop.classes ["is-flex"; "is-flex-direction-row"; "term-select-item-main"] - prop.onClick setTerm + prop.onMouseDown setTerm prop.style [style.position.relative] prop.children [ Html.i [ @@ -272,10 +272,13 @@ type TermSearch = static member Input ( setter: OntologyAnnotation option -> unit, ?input: OntologyAnnotation, ?parent': OntologyAnnotation, + ?debounceSetter: int, ?advancedSearchDispatch: Messages.Msg -> unit, - ?debounceSetter: int, ?portalTermSelectArea: HTMLElement, - ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool) + ?portalTermSelectArea: HTMLElement, + ?onBlur: FocusEvent -> unit, ?onEscape: KeyboardEvent -> unit, + ?autofocus: bool, ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool, ?borderRadius: int) = + let autofocus = defaultArg autofocus false let displayParent = defaultArg displayParent true let isExpanded = defaultArg isExpanded false let advancedSearchActive, setAdvancedSearchActive = React.useState(false) @@ -320,8 +323,13 @@ type TermSearch = if loading then Bulma.control.isLoading prop.children [ Bulma.input.text [ + prop.autoFocus autofocus + prop.style [ + if borderRadius.IsSome then style.borderRadius borderRadius.Value + ] if size.IsSome then size.Value if state.IsSome then prop.valueOrDefault state.Value.NameText + if onBlur.IsSome then prop.onBlur onBlur.Value 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 @@ -341,7 +349,10 @@ type TermSearch = startSearch (Some s, true) mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) ) - prop.onKeyDown(key.escape, fun _ -> stopSearch()) + prop.onKeyDown(key.escape, fun e -> + if onEscape.IsSome then onEscape.Value e + stopSearch() + ) ] let TermSelectArea = TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching) From 6573b56bd69589fb68dffbf332d59dd49d243eff Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 30 Jan 2024 15:49:38 +0100 Subject: [PATCH 014/135] display terms with accession --- src/Client/MainComponents/Cells.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index f9d7ca2e..fcbdb0f7 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -110,7 +110,7 @@ module private CellComponents = ] let compositeCellDisplay (cc: CompositeCell) = - let ccType = match cc with | CompositeCell.Term oa -> "T" | CompositeCell.Unitized _ -> "U" | CompositeCell.FreeText _ -> "F" + let hasValidOA = match cc with | CompositeCell.Term oa -> oa.TermAccessionShort <> "" | CompositeCell.Unitized (v, oa) -> oa.TermAccessionShort <> "" | CompositeCell.FreeText _ -> false let v = cc.ToString() Html.div [ prop.classes ["is-flex"] @@ -125,9 +125,9 @@ module private CellComponents = ] prop.text v ] - Html.span [ + if hasValidOA then Html.i [ prop.style [style.custom("marginLeft", "auto")] - prop.text ccType + prop.className ["fa-solid"; "fa-check"] ] ] ] From 95b1ff454f931bc19c196572ec6de198db287c8e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 30 Jan 2024 16:20:59 +0100 Subject: [PATCH 015/135] improve styling :art: --- src/Client/MainComponents/Cells.fs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index fcbdb0f7..6ded632a 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -125,10 +125,11 @@ module private CellComponents = ] prop.text v ] - if hasValidOA then Html.i [ - prop.style [style.custom("marginLeft", "auto")] - prop.className ["fa-solid"; "fa-check"] - ] + if hasValidOA then + Bulma.icon [Html.i [ + prop.style [style.custom("marginLeft", "auto")] + prop.className ["fa-solid"; "fa-check"] + ]] ] ] From 2b6f5c822164b0ea43408af4ffaad490447d4465 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 31 Jan 2024 13:38:52 +0100 Subject: [PATCH 016/135] increase cell search robustness :sparkles: --- src/Client/MainComponents/Cells.fs | 177 +++--------------- .../SharedComponents/ClickOutsideHandler.fs | 12 +- .../SharedComponents/TermSearchInput.fs | 8 +- src/Client/Views/SplitWindowView.fs | 2 + 4 files changed, 44 insertions(+), 155 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 6ded632a..87a11b01 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -64,7 +64,9 @@ module private CellComponents = yield! specificStyle ] - let cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: unit -> unit, setState_cell, state_cell, cell_value) = + let cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: string -> unit, setState_cell, state_cell, cell_value) = + log "cell_value" + log cell_value Bulma.input.text [ prop.readOnly isReadOnly prop.autoFocus true @@ -82,13 +84,13 @@ module private CellComponents = ] // Update main spreadsheet state when leaving focus or... prop.onBlur(fun _ -> - updateMainStateTable() + updateMainStateTable cell_value ) // .. when pressing "ENTER". "ESCAPE" will negate changes. prop.onKeyDown(fun e -> match e.which with | 13. -> //enter - updateMainStateTable() + updateMainStateTable cell_value | 27. -> //escape setState_cell {CellMode = Idle; Value = cell_value} | _ -> () @@ -205,145 +207,6 @@ module private EventPresets = else None TermSearch.UpdateParentTerm oa |> TermSearchMsg |> dispatch -///// Only apply this element to SwateCell if header has term. -//[] -//let TANCell(index: (int*int), model: Model, dispatch) = -// let columnIndex = fst index -// let rowIndex = snd index -// let state = model.SpreadsheetModel -// let cell = state.ActiveTable.[index] -// let isHeader = cell.isHeader -// let cellValue = -// if isHeader then -// let tan = cell.Header.Term |> Option.map (fun x -> x.TermAccession) |> Option.defaultValue "" -// tan -// elif cell.isUnit then -// cell.Unit.Unit.TermAccession -// elif cell.isTerm then -// cell.Term.Term.TermAccession -// else "" -// let state_cell, setState_cell = React.useState(CellState.init(cellValue)) -// let isSelected = state.SelectedCells.Contains index -// let cell_element : IReactProperty list -> ReactElement = if isHeader then Html.th else Html.td -// cell_element [ -// prop.key $"Cell_{state.ActiveTableIndex}-{columnIndex}-{rowIndex}_TAN" -// cellStyle [ -// if isHeader then -// style.color(NFDIColors.white) -// style.backgroundColor(NFDIColors.DarkBlue.Lighter20) -// if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) -// ] -// prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) -// prop.children [ -// Html.div [ -// cellInnerContainerStyle [] -// prop.onDoubleClick(fun e -> -// e.preventDefault() -// e.stopPropagation() -// UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch -// if not state_cell.Active then setState_cell {state_cell with Active = true} -// ) -// prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) -// prop.children [ -// if state_cell.Active then -// let updateMainStateTable() = -// // Only update if changed -// if state_cell.Value <> cellValue then -// // Updating unit name should remove unit tsr/tan -// let nextTerm = -// match cell with -// | IsHeader header -> -// let nextTerm = header.Term |> Option.map (fun t -> {t with TermAccession = state_cell.Value}) |> Option.defaultValue (TermMinimal.create "" state_cell.Value ) -// let nextHeader = {header with Term = Some nextTerm} -// IsHeader nextHeader -// | IsTerm t_cell -> -// let nextTermCell = -// let term = { t_cell.Term with TermAccession = state_cell.Value } -// { t_cell with Term = term } -// IsTerm nextTermCell -// | IsUnit u_cell -> -// let nextUnitCell = -// let unit = { u_cell.Unit with TermAccession = state_cell.Value } -// { u_cell with Unit = unit } -// IsUnit nextUnitCell -// | IsFreetext _ -> -// let t_cell = cell.toTermCell().Term -// let term = {t_cell.Term with TermAccession = state_cell.Value} -// let nextCell = {t_cell with Term = term} -// IsTerm nextCell -// Msg.UpdateTable (index, nextTerm) |> SpreadsheetMsg |> dispatch -// setState_cell {state_cell with Active = false} -// cellInputElement(isHeader, updateMainStateTable, setState_cell, state_cell, cellValue) -// else -// let displayValue = -// if isHeader then -// $"{ColumnCoreNames.TermAccessionNumber.toString} ({cellValue})" -// else -// cellValue -// Html.span [ -// prop.style [ -// style.flexGrow 1 -// ] -// prop.text displayValue -// ] -// ] -// ] -// ] -// ] - -//[] -//let UnitCell(index: (int*int), model: Model, dispatch) = -// let columnIndex = fst index -// let rowIndex = snd index -// let state = model.SpreadsheetModel -// let cell = state.ActiveTable.[index] -// let isHeader = cell.isHeader -// let cellValue = if isHeader then ColumnCoreNames.Unit.toString elif cell.isUnit then cell.Unit.Unit.Name else "Unknown" -// let state_cell, setState_cell = React.useState(CellState.init(cellValue)) -// let isSelected = state.SelectedCells.Contains index -// let cell_element : IReactProperty list -> ReactElement = if isHeader then Html.th else Html.td -// cell_element [ -// prop.key $"Cell_{state.ActiveTableIndex}-{columnIndex}-{rowIndex}_Unit" -// cellStyle [ -// if isHeader then -// style.color(NFDIColors.white) -// style.backgroundColor(NFDIColors.DarkBlue.Lighter20) -// if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) -// ] -// prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) -// prop.children [ -// Html.div [ -// cellInnerContainerStyle [] -// prop.onDoubleClick(fun e -> -// e.preventDefault() -// e.stopPropagation() -// UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch -// if not state_cell.Active then setState_cell {state_cell with Active = true} -// ) -// prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, dispatch) -// prop.children [ -// if not isHeader && state_cell.Active then -// let updateMainStateTable() = -// // Only update if changed -// if state_cell.Value <> cellValue then -// // This column only exists for unit cells -// let nextTerm = {cell.Unit.Unit with Name = state_cell.Value} -// let nextBody = IsUnit { cell.Unit with Unit = nextTerm } -// Msg.UpdateTable (index, nextBody) |> SpreadsheetMsg |> dispatch -// setState_cell {state_cell with Active = false} -// cellInputElement(isHeader, updateMainStateTable, setState_cell, state_cell, cellValue) -// else -// Html.span [ -// prop.style [ -// style.flexGrow 1 -// ] -// prop.text cellValue -// ] -// ] -// ] -// ] -// ] - open Shared @@ -370,11 +233,12 @@ type Cell = prop.children [ if state_cell.IsActive then /// Update change to mainState and exit active input. - let updateMainStateTable() = + let updateMainStateTable = + fun (s: string) -> // Only update if changed - if state_cell.Value <> cellValue then - setter state_cell.Value - setState_cell {state_cell with CellMode = Idle} + if s <> cellValue then + setter s + setState_cell {state_cell with CellMode = Idle} cellInputElement(true, isReadOnly, updateMainStateTable, setState_cell, state_cell, cellValue) else let cellValue = // shadow cell value for tsr and tan to add columnType @@ -416,7 +280,15 @@ type Cell = Cell.HeaderBase(TAN, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) static member Empty() = - Html.td [ cellStyle []; prop.readOnly true ] + Html.td [ cellStyle []; prop.readOnly true; prop.children [ + Html.div [ + prop.style [style.height (length.perc 100)] + prop.className "is-flex is-align-items-center is-justify-content-center" + prop.children [ + Html.div "-" + ] + ] + ]] [] static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation option -> unit) = @@ -446,11 +318,12 @@ type Cell = match state_cell.CellMode with | Active -> /// Update change to mainState and exit active input. - let updateMainStateTable() = + let updateMainStateTable = + fun (s: string) -> // Only update if changed - if state_cell.Value <> cellValue then - setter state_cell.Value - makeIdle() + if s <> cellValue then + setter s + makeIdle() cellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue) | Search -> if oasetter.IsSome then @@ -474,6 +347,8 @@ type Cell = static member Body(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let cellValue = cell.GetContent().[0] let setter = fun (s: string) -> + log "SETTER" + log s let nextCell = cell.UpdateMainField s Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch let oaSetter = fun (oa:OntologyAnnotation option) -> diff --git a/src/Client/SharedComponents/ClickOutsideHandler.fs b/src/Client/SharedComponents/ClickOutsideHandler.fs index db8b5743..035f2c3b 100644 --- a/src/Client/SharedComponents/ClickOutsideHandler.fs +++ b/src/Client/SharedComponents/ClickOutsideHandler.fs @@ -1,5 +1,6 @@ namespace Components +open Feliz open Fable.Core open Browser.Types open Fable.Core.JsInterop @@ -13,4 +14,13 @@ type ClickOutsideHandler = if not isClickedInsideDropdown then clickedOutsideEvent e Browser.Dom.document.removeEventListener("click", closeEvent) - Browser.Dom.document.addEventListener("click", closeEvent) \ No newline at end of file + Browser.Dom.document.addEventListener("click", closeEvent) + + static member AddListener(element: IRefValue, clickedOutsideEvent: Event -> unit) = + let rec closeEvent = fun (e: Event) -> + let dropdown = element.current + let isClickedInsideDropdown : bool = dropdown?contains(e.target) + if not isClickedInsideDropdown then + clickedOutsideEvent e + Browser.Dom.document.removeEventListener("click", closeEvent) + Browser.Dom.document.addEventListener("click", closeEvent) diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index b9963361..d1a74bba 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -167,7 +167,7 @@ module TermSearchAux = ] ] Html.button [ - prop.onClick(fun e -> + prop.onMouseDown(fun e -> e.stopPropagation() setShow (not show) ) @@ -275,7 +275,7 @@ type TermSearch = ?debounceSetter: int, ?advancedSearchDispatch: Messages.Msg -> unit, ?portalTermSelectArea: HTMLElement, - ?onBlur: FocusEvent -> unit, ?onEscape: KeyboardEvent -> unit, + ?onBlur: Event -> unit, ?onEscape: KeyboardEvent -> unit, ?autofocus: bool, ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool, ?borderRadius: int) = let autofocus = defaultArg autofocus false @@ -291,6 +291,8 @@ type TermSearch = let isSearching, setIsSearching = React.useState(false) let debounceStorage = React.useRef(newDebounceStorage()) let dsetter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp + let ref = React.useElementRef() + if onBlur.IsSome then React.useLayoutEffectOnce(fun _ -> ClickOutsideHandler.AddListener (ref, onBlur.Value) ) React.useEffect((fun () -> setState input), dependencies=[|box input|]) React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. let stopSearch() = @@ -317,6 +319,7 @@ type TermSearch = if size.IsSome then size.Value Bulma.control.hasIconsLeft Bulma.control.hasIconsRight + prop.ref ref prop.style [ if fullwidth then style.flexGrow 1; ] @@ -329,7 +332,6 @@ type TermSearch = ] if size.IsSome then size.Value if state.IsSome then prop.valueOrDefault state.Value.NameText - if onBlur.IsSome then prop.onBlur onBlur.Value 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 diff --git a/src/Client/Views/SplitWindowView.fs b/src/Client/Views/SplitWindowView.fs index 697b7dba..13e2ed46 100644 --- a/src/Client/Views/SplitWindowView.fs +++ b/src/Client/Views/SplitWindowView.fs @@ -82,6 +82,8 @@ let exampleTerm = [] let Main (left:seq) (right:seq) (mainModel:Messages.Model) (dispatch: Messages.Msg -> unit) = let (model, setModel) = React.useState(SplitWindow.init) + /// Used to show/hide the sidebar. + let show, setShow = React.useState(true) React.useEffect(model.WriteToLocalStorage, [|box model|]) React.useEffectOnce(fun _ -> Browser.Dom.window.addEventListener("resize", onResize_event model setModel)) Html.div [ From a64b7bde0592460c3709703fd86c861c8177bc6e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 31 Jan 2024 15:56:56 +0100 Subject: [PATCH 017/135] make sidebar toggle :sparkles: --- src/Client/Client.fs | 2 +- src/Client/Client.fsproj | 1 + src/Client/MainComponents/FooterTabs.fs | 42 +++++++++++---- .../MainComponents/MainViewContainer.fs | 20 +++++++ src/Client/Messages.fs | 6 ++- src/Client/Model.fs | 2 + src/Client/Update.fs | 14 ++--- src/Client/Views/MainWindowView.fs | 14 ++--- src/Client/Views/SplitWindowView.fs | 52 ++++++++----------- 9 files changed, 97 insertions(+), 56 deletions(-) create mode 100644 src/Client/MainComponents/MainViewContainer.fs diff --git a/src/Client/Client.fs b/src/Client/Client.fs index b8692740..61d30e96 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -15,6 +15,7 @@ let _ = importSideEffects "./style.scss" let sayHello name = $"Hello {name}" open Feliz +open Feliz.Bulma let private split_container model dispatch = let mainWindow = Seq.singleton <| MainWindowView.Main (model, dispatch) @@ -31,7 +32,6 @@ let View (model : Model) (dispatch : Msg -> unit) = let v = {colorstate with SetTheme = setColorstate} React.contextProvider(LocalStorage.Darkmode.themeContext, v, Html.div [ - Html.div [prop.id "modal-container"] match model.PersistentStorageState.Host with | Some Swatehost.Excel -> SidebarView.SidebarView model dispatch diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 46d78fe7..f1f10d2b 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -62,6 +62,7 @@ + diff --git a/src/Client/MainComponents/FooterTabs.fs b/src/Client/MainComponents/FooterTabs.fs index a8bb4cc5..a815d860 100644 --- a/src/Client/MainComponents/FooterTabs.fs +++ b/src/Client/MainComponents/FooterTabs.fs @@ -95,11 +95,9 @@ let private dragleave_handler(state, setState) = fun (e: Browser.Types.DragEvent setState {state with IsDraggedOver = false} [] -let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatch: Messages.Msg -> unit|}) = - let index = input.index - let table = input.tables.GetTableAt(index) +let Main (index: int, tables: ArcTables, model: Messages.Model, dispatch: Messages.Msg -> unit) = + let table = tables.GetTableAt(index) let state, setState = React.useState(FooterTab.init(table.Name)) - let dispatch = input.dispatch let id = $"ReorderMe_{index}_{table.Name}" Bulma.tab [ if state.IsDraggedOver then prop.className "dragover-footertab" @@ -120,7 +118,7 @@ let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatc // Use this to ensure updating reactelement correctly prop.key id prop.id id - if input.model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Table index then Bulma.tab.isActive + if model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Table index then Bulma.tab.isActive prop.onClick (fun _ -> Spreadsheet.UpdateActiveView (Spreadsheet.ActiveView.Table index) |> Messages.SpreadsheetMsg |> dispatch) prop.onContextMenu(fun e -> e.stopPropagation() @@ -162,13 +160,12 @@ let Main (input: {|index: int; tables: ArcTables; model: Messages.Model; dispatc ] [] -let MainMetadata(input:{|model: Messages.Model; dispatch: Messages.Msg -> unit|}) = - let dispatch = input.dispatch +let MainMetadata(model: Messages.Model, dispatch: Messages.Msg -> unit) = let order = 0 let id = "Metadata-Tab" let nav = Spreadsheet.ActiveView.Metadata Bulma.tab [ - if input.model.SpreadsheetModel.ActiveView = nav then Bulma.tab.isActive + if model.SpreadsheetModel.ActiveView = nav then Bulma.tab.isActive prop.key id prop.id id prop.onClick (fun _ -> Spreadsheet.UpdateActiveView nav |> Messages.SpreadsheetMsg |> dispatch) @@ -179,10 +176,9 @@ let MainMetadata(input:{|model: Messages.Model; dispatch: Messages.Msg -> unit|} ] [] -let MainPlus(input:{|dispatch: Messages.Msg -> unit|}) = - let dispatch = input.dispatch +let MainPlus(dispatch: Messages.Msg -> unit) = let state, setState = React.useState(FooterTab.init()) - let order = System.Int32.MaxValue + let order = System.Int32.MaxValue-1 // MaxValue will be sidebar toggle let id = "Add-Spreadsheet-Button" Bulma.tab [ prop.key id @@ -207,4 +203,28 @@ let MainPlus(input:{|dispatch: Messages.Msg -> unit|}) = ] ] ] + ] + +let ToggleSidebar(model: Messages.Model, dispatch: Messages.Msg -> unit) = + let show = model.PersistentStorageState.ShowSideBar + let order = System.Int32.MaxValue + let id = "Toggle-Sidebar-Button" + Bulma.tab [ + prop.key id + prop.id id + prop.onClick (fun e -> Messages.PersistentStorage.UpdateShowSidebar (not show) |> Messages.PersistentStorageMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer; style.marginLeft length.auto] + prop.children [ + Html.a [ + prop.style [style.height.inheritFromParent; style.pointerEvents.none] + prop.children [ + Bulma.icon [ + Bulma.icon.isSmall + prop.children [ + Html.i [prop.className ["fa-solid"; if show then "fa-chevron-right" else "fa-chevron-left"]] + ] + ] + ] + ] + ] ] \ No newline at end of file diff --git a/src/Client/MainComponents/MainViewContainer.fs b/src/Client/MainComponents/MainViewContainer.fs new file mode 100644 index 00000000..edc99f22 --- /dev/null +++ b/src/Client/MainComponents/MainViewContainer.fs @@ -0,0 +1,20 @@ +module MainComponents.MainViewContainer + + +open Feliz +open Feliz.Bulma +open Spreadsheet +open ARCtrl.ISA +open Messages + +let Main(minWidth: int, left: ReactElement seq) = + Html.div [ + prop.style [ + style.minWidth(minWidth) + style.flexGrow 1 + style.flexShrink 1 + style.height(length.vh 100) + style.width(length.perc 100) + ] + prop.children left + ] \ No newline at end of file diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index 93c4f134..50390b2e 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -74,9 +74,11 @@ type ApiMsg = type StyleChangeMsg = | UpdateColorMode of ColorMode -type PersistentStorageMsg = +module PersistentStorage = + type Msg = | NewSearchableOntologies of Ontology [] | UpdateAppVersion of string + | UpdateShowSidebar of bool module FilePicker = type Msg = @@ -181,7 +183,7 @@ type Msg = | TermSearchMsg of TermSearch.Msg | AdvancedSearchMsg of AdvancedSearch.Msg | OfficeInteropMsg of OfficeInterop.Msg -| PersistentStorage of PersistentStorageMsg +| PersistentStorageMsg of PersistentStorage.Msg | FilePickerMsg of FilePicker.Msg | BuildingBlockMsg of BuildingBlock.Msg | ProtocolMsg of Protocol.Msg diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 12273014..0b691223 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -143,12 +143,14 @@ type PersistentStorageState = { SearchableOntologies : (Set*Ontology) [] AppVersion : string Host : Swatehost option + ShowSideBar : bool HasOntologiesLoaded : bool } with static member init () = { SearchableOntologies = [||] Host = None AppVersion = "" + ShowSideBar = false HasOntologiesLoaded = false } diff --git a/src/Client/Update.fs b/src/Client/Update.fs index 7f614aeb..ffa008dc 100644 --- a/src/Client/Update.fs +++ b/src/Client/Update.fs @@ -291,7 +291,7 @@ let handleApiResponseMsg (resMsg: ApiResponseMsg) (currentState: ApiState) : Api let cmds = Cmd.batch [ ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - onts |> NewSearchableOntologies |> PersistentStorage |> Cmd.ofMsg + onts |> PersistentStorage.NewSearchableOntologies |> PersistentStorageMsg |> Cmd.ofMsg ] nextState, cmds @@ -325,7 +325,7 @@ let handleApiResponseMsg (resMsg: ApiResponseMsg) (currentState: ApiState) : Api let cmds = Cmd.batch [ ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - appVersion |> UpdateAppVersion |> PersistentStorage |> Cmd.ofMsg + appVersion |>PersistentStorage. UpdateAppVersion |> PersistentStorageMsg |> Cmd.ofMsg ] nextState, cmds @@ -362,9 +362,9 @@ let handleApiMsg (apiMsg:ApiMsg) (currentState:ApiState) : ApiState * Cmd handleApiResponseMsg res currentState -let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorageMsg) (currentState:PersistentStorageState) : PersistentStorageState * Cmd = +let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorage.Msg) (currentState:PersistentStorageState) : PersistentStorageState * Cmd = match persistentStorageMsg with - | NewSearchableOntologies onts -> + | PersistentStorage.NewSearchableOntologies onts -> let nextState = { currentState with SearchableOntologies = onts |> Array.map (fun ont -> ont.Name |> SorensenDice.createBigrams, ont) @@ -372,12 +372,14 @@ let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorageMsg) (curr } nextState,Cmd.none - | UpdateAppVersion appVersion -> + | PersistentStorage.UpdateAppVersion appVersion -> let nextState = { currentState with AppVersion = appVersion } nextState,Cmd.none + | PersistentStorage.UpdateShowSidebar show -> + {currentState with ShowSideBar = show}, Cmd.none let handleBuildingBlockDetailsMsg (topLevelMsg:BuildingBlockDetailsMsg) (currentState: BuildingBlockDetailsState) : BuildingBlockDetailsState * Cmd = match topLevelMsg with @@ -564,7 +566,7 @@ let update (msg : Msg) (model : Model) : Model * Cmd = } nextModel,nextCmd - | PersistentStorage persistentStorageMsg -> + | PersistentStorageMsg persistentStorageMsg -> let nextPersistentStorageState,nextCmd = currentModel.PersistentStorageState |> handlePersistenStorageMsg persistentStorageMsg diff --git a/src/Client/Views/MainWindowView.fs b/src/Client/Views/MainWindowView.fs index ea017851..3ff1eaf5 100644 --- a/src/Client/Views/MainWindowView.fs +++ b/src/Client/Views/MainWindowView.fs @@ -19,19 +19,19 @@ let private spreadsheetSelectionFooter (model: Messages.Model) dispatch = Bulma.tabs.isBoxed prop.children [ Html.ul [ - yield Bulma.tab [ + Bulma.tab [ prop.style [style.width (length.px 20)] ] - yield - MainComponents.FooterTabs.MainMetadata {| model=model; dispatch = dispatch |} + + MainComponents.FooterTabs.MainMetadata (model, dispatch) for index in 0 .. (model.SpreadsheetModel.Tables.TableCount-1) do - yield - MainComponents.FooterTabs.Main {| index = index; tables = model.SpreadsheetModel.Tables; model = model; dispatch = dispatch |} + MainComponents.FooterTabs.Main (index, model.SpreadsheetModel.Tables, model, dispatch) match model.SpreadsheetModel.ArcFile with | Some (ArcFiles.Template _) | Some (ArcFiles.Investigation _) -> - yield Html.none + Html.none | _ -> - yield MainComponents.FooterTabs.MainPlus {| dispatch = dispatch |} + MainComponents.FooterTabs.MainPlus dispatch + MainComponents.FooterTabs.ToggleSidebar(model, dispatch) ] ] ] diff --git a/src/Client/Views/SplitWindowView.fs b/src/Client/Views/SplitWindowView.fs index 13e2ed46..7378388e 100644 --- a/src/Client/Views/SplitWindowView.fs +++ b/src/Client/Views/SplitWindowView.fs @@ -1,6 +1,7 @@ module SplitWindowView open Feliz +open Feliz.Bulma open Elmish open Browser.Types open LocalStorage.SplitWindow @@ -76,14 +77,31 @@ let exampleTerm = false "MS" +let private sidebarCombinedElement(sidebarId: string, model: SplitWindow, setModel, dispatch, right) = + Html.div [ + prop.id sidebarId + prop.style [ + style.float'.right; + style.minWidth(minWidth); + style.flexBasis(length.px model.RightWindowWidth); style.flexShrink 0; style.flexGrow 0 + style.height(length.vh 100) + style.width(length.perc 100) + style.overflow.hidden + style.display.flex + ] + prop.children [ + dragbar model setModel dispatch + yield! right + ] + ] + // https://jsfiddle.net/gaby/Bek9L/ // https://stackoverflow.com/questions/6219031/how-can-i-resize-a-div-by-dragging-just-one-side-of-it /// Splits screen into two parts. Left and right, with a dragbar in between to change size of right side. [] let Main (left:seq) (right:seq) (mainModel:Messages.Model) (dispatch: Messages.Msg -> unit) = let (model, setModel) = React.useState(SplitWindow.init) - /// Used to show/hide the sidebar. - let show, setShow = React.useState(true) + let isNotMetadataSheet = not (mainModel.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata) React.useEffect(model.WriteToLocalStorage, [|box model|]) React.useEffectOnce(fun _ -> Browser.Dom.window.addEventListener("resize", onResize_event model setModel)) Html.div [ @@ -91,32 +109,8 @@ let Main (left:seq) (right:seq Date: Wed, 31 Jan 2024 17:19:34 +0100 Subject: [PATCH 018/135] Add movable building block short cut dialogue :sparkles: --- src/Client/Client.fsproj | 30 +-- src/Client/LocalStorage/ModalPositions.fs | 40 ++++ .../MainComponents/BuildingBlockModal.fs | 66 ++++++ src/Client/MainComponents/Navbar.fs | 43 +++- .../Pages/BuildingBlock/BuildingBlockView.fs | 218 ------------------ src/Client/Pages/BuildingBlock/Dropdown.fs | 12 - .../Pages/BuildingBlock/SearchComponent.fs | 2 - 7 files changed, 161 insertions(+), 250 deletions(-) create mode 100644 src/Client/LocalStorage/ModalPositions.fs create mode 100644 src/Client/MainComponents/BuildingBlockModal.fs diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index f1f10d2b..e625f37a 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -13,6 +13,7 @@ + @@ -62,20 +63,6 @@ - - - - - - - - - - - - - - @@ -99,6 +86,21 @@ + + + + + + + + + + + + + + + diff --git a/src/Client/LocalStorage/ModalPositions.fs b/src/Client/LocalStorage/ModalPositions.fs new file mode 100644 index 00000000..67a663ac --- /dev/null +++ b/src/Client/LocalStorage/ModalPositions.fs @@ -0,0 +1,40 @@ +module LocalStorage.ModalPositions + +open Feliz +open Fable.Core.JsInterop + +type Position = { + X: int + Y: int +} with + static member init () = { + X = 0 + Y = 0 + } + +open Fable.SimpleJson + +[] +module LocalStorage = + + open Browser + + let [] private DataTheme_Key_Prefix = "ModalPosition_" + + let write(modalName:string, dt: Position) = + let s = Json.serialize dt + WebStorage.localStorage.setItem(DataTheme_Key_Prefix + modalName, s) + + let load(modalName:string) = + let key = DataTheme_Key_Prefix + modalName + try + WebStorage.localStorage.getItem(key) + |> Json.parseAs + |> Some + with + |_ -> + WebStorage.localStorage.removeItem(key) + printfn "Could not find %s" key + None + +let [] BuildingBlockModal = "BuildingBlock" diff --git a/src/Client/MainComponents/BuildingBlockModal.fs b/src/Client/MainComponents/BuildingBlockModal.fs new file mode 100644 index 00000000..dc34673a --- /dev/null +++ b/src/Client/MainComponents/BuildingBlockModal.fs @@ -0,0 +1,66 @@ +namespace MainComponents + +open Feliz +open Feliz.Bulma +open Browser.Types + +open LocalStorage.ModalPositions + +module InitExtensions = + + type Position with + static member initBuildingBlock() = + match LocalStorage.load LocalStorage.ModalPositions.BuildingBlockModal with + | Some p -> p + | None -> Position.init() + +open InitExtensions + +type Modals = + + [] + static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = + let position, setPosition = React.useState(Position.initBuildingBlock) + let startPosition, setStartPosition = React.useState(None) + let isMoving, setIsMoving = React.useState false + let element = React.useElementRef() + Bulma.modalCard [ + prop.ref element + prop.style [style.overflow.visible; style.top position.Y; style.left position.X; style.position.absolute] + prop.children [ + Bulma.modalCardHead [ + prop.onMouseDown(fun e -> + let x = e.clientX - element.current.Value.offsetLeft + let y = e.clientY - element.current.Value.offsetTop; + setStartPosition <| Some {X = int x; Y = int y} + setIsMoving true + ) + prop.onMouseMove(fun e -> + if isMoving then + log "move" + let maxX = Browser.Dom.window.innerWidth - element.current.Value.offsetWidth; + let tempX = int e.clientX - startPosition.Value.X + let newX = System.Math.Min(System.Math.Max(tempX,0),int maxX) + let maxY = Browser.Dom.window.innerHeight - element.current.Value.offsetHeight; + let tempY = int e.clientY - startPosition.Value.Y + let newY = System.Math.Min(System.Math.Max(tempY,0),int maxY) + setPosition {X = newX; Y = newY} + ) + prop.onMouseUp(fun _ -> + setStartPosition None + LocalStorage.write(BuildingBlockModal,position) + setIsMoving false + ) + prop.style [style.cursor.move] + prop.children [ + Bulma.modalCardTitle Html.none + Bulma.delete [ prop.onClick rmv ] + ] + ] + Bulma.modalCardBody [ + BuildingBlock.SearchComponent.Main model dispatch + ] + ] + ] + + diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index 81e1c09c..1ba30567 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -8,8 +8,12 @@ open LocalHistory open Messages open Components.QuickAccessButton +[] +type private Modal = +| BuildingBlock +| Template -let quickAccessButtonListStart (state: LocalHistory.Model) dispatch = +let private quickAccessButtonListStart (state: LocalHistory.Model) (setModal: Modal option -> unit) dispatch = Html.div [ prop.style [ style.display.flex; style.flexDirection.row @@ -44,10 +48,30 @@ let quickAccessButtonListStart (state: LocalHistory.Model) dispatch = ), isActive = (state.NextPositionIsValid(state.HistoryCurrentPosition - 1)) ).toReactElement() + QuickAccessButton.create( + "Add Building Block", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table-columns" ] + ] + ], + (fun _ -> setModal (Some Modal.BuildingBlock)) + ).toReactElement() + //QuickAccessButton.create( + // "Add Template", + // [ + // Bulma.icon [ + // Html.i [prop.className "fa-solid fa-circle-plus" ] + // Html.i [prop.className "fa-solid fa-table" ] + // ] + // ], + // (fun _ -> setModal (Some Modal.Template)) + //).toReactElement() ] ] -let quickAccessButtonListEnd (model: Model) dispatch = +let private quickAccessButtonListEnd (model: Model) dispatch = Html.div [ prop.style [ style.display.flex; style.flexDirection.row @@ -71,9 +95,18 @@ let quickAccessButtonListEnd (model: Model) dispatch = ] ] +let private modalDisplay (modal: Modal option, model, dispatch, setModal) = + let rmv = fun _ -> setModal (None) + match modal with + | None -> Html.none + | Some Modal.BuildingBlock -> + MainComponents.Modals.BuildingBlock (model, dispatch, rmv) + | Some Modal.Template -> + MainComponents.Modals.BuildingBlock (model, dispatch, rmv) [] let Main (model: Messages.Model) dispatch = + let modal, setModal = React.useState(None) Bulma.navbar [ prop.className "myNavbarSticky" prop.id "swate-mainNavbar" @@ -84,6 +117,7 @@ let Main (model: Messages.Model) dispatch = style.minHeight(length.rem 3.25) ] prop.children [ + modalDisplay (modal, model, dispatch, setModal) Bulma.navbarBrand.div [ ] @@ -119,7 +153,8 @@ let Main (model: Messages.Model) dispatch = (fun e -> ()), false ).toReactElement() - quickAccessButtonListStart (model.History: LocalHistory.Model) dispatch + quickAccessButtonListStart (model.History: LocalHistory.Model) setModal dispatch + ] ] ] @@ -128,7 +163,7 @@ let Main (model: Messages.Model) dispatch = Bulma.navbarStart.div [ prop.style [style.display.flex; style.alignItems.stretch; style.justifyContent.flexStart; style.custom("marginRight", "auto")] prop.children [ - quickAccessButtonListStart model.History dispatch + quickAccessButtonListStart model.History setModal dispatch ] ] Bulma.navbarEnd.div [ diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index f480bea2..4629ca16 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -97,225 +97,12 @@ let update (addBuildingBlockMsg:BuildingBlock.Msg) (state: BuildingBlock.Model) } nextState, Cmd.none -//let addBuildingBlockFooterComponent (model:Model) (dispatch:Messages.Msg -> unit) = -// Content.content [ ] [ -// Label.label [Label.Props [Style [Color model.SiteStyleState.ColorMode.Accent]]] [ -// str (sprintf "More about %s:" (model.AddBuildingBlockState.CurrentBuildingBlock.Type.toString )) -// ] -// Text.p [Props [Style [TextAlign TextAlignOptions.Justify]]] [ -// span [] [model.AddBuildingBlockState.CurrentBuildingBlock.Type.toLongExplanation |> str] -// span [] [str " You can find more information on our "] -// a [Href Shared.URLs.AnnotationPrinciplesUrl; Target "_blank"] [str "website"] -// span [] [str "."] -// ] -// ] - open SidebarComponents - -//let addBuildingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = -// let autocompleteParamsTerm = AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockState model.AddBuildingBlockState -// let autocompleteParamsUnit = AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnitState model.AddBuildingBlockState - -// mainFunctionContainer [ -// AdvancedSearch.advancedSearchModal model autocompleteParamsTerm.ModalId autocompleteParamsTerm.InputId dispatch autocompleteParamsTerm.OnAdvancedSearch -// AdvancedSearch.advancedSearchModal model autocompleteParamsUnit.ModalId autocompleteParamsUnit.InputId dispatch autocompleteParamsUnit.OnAdvancedSearch -// Field.div [] [ -// let autocompleteParams = (AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockState model.AddBuildingBlockState) -// Field.div [Field.HasAddons] [ -// Choose building block type dropdown element -// Control.p [] [ -// Dropdown.dropdown [ -// Dropdown.IsActive model.AddBuildingBlockState.ShowBuildingBlockSelection -// ] [ -// Dropdown.trigger [] [ -// Button.a [Button.OnClick (fun e -> e.stopPropagation(); ToggleSelectionDropdown |> BuildingBlockMsg |> dispatch)] [ -// span [Style [MarginRight "5px"]] [str model.AddBuildingBlockState.CurrentBuildingBlock.Type.toString] -// Fa.i [Fa.Solid.AngleDown] [] -// ] -// ] -// Dropdown.menu [ ] [ -// match model.AddBuildingBlockState.DropdownPage with -// | Model.BuildingBlock.DropdownPage.Main -> -// Helper.DropdownElements.dropdownContentMain model dispatch -// | Model.BuildingBlock.DropdownPage.ProtocolTypes -> -// Helper.DropdownElements.dropdownContentProtocolTypeColumns model dispatch -// | Model.BuildingBlock.DropdownPage.Output -> -// Helper.DropdownElements.dropdownContentOutputColumns model dispatch -// |> fun content -> Dropdown.content [Props [Style [yield! colorControlInArray model.SiteStyleState.ColorMode]] ] content -// ] -// ] -// ] -// Ontology Term search field -// if model.AddBuildingBlockState.CurrentBuildingBlock.Type.isTermColumn && model.AddBuildingBlockState.CurrentBuildingBlock.Type.isFeaturedColumn |> not then -// AutocompleteSearch.autocompleteTermSearchComponentInputComponent -// dispatch -// false // isDisabled -// "Start typing to search" -// None // No input size specified -// autocompleteParams - -// ] -// Ontology Term search preview -// AutocompleteSearch.autocompleteDropdownComponent -// dispatch -// model.SiteStyleState.ColorMode -// autocompleteParams.DropDownIsVisible -// autocompleteParams.DropDownIsLoading -// (AutocompleteSearch.createAutocompleteSuggestions dispatch autocompleteParams model) -// ] -// Ontology Unit Term search field -// if model.AddBuildingBlockState.CurrentBuildingBlock.Type.isTermColumn && model.AddBuildingBlockState.CurrentBuildingBlock.Type.isFeaturedColumn |> not then -// let unitAutoCompleteParams = AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnitState model.AddBuildingBlockState -// Field.div [] [ -// Field.div [Field.HasAddons] [ -// Control.p [] [ -// Button.a [ -// Button.Props [Style [ -// if model.AddBuildingBlockState.BuildingBlockHasUnit then Color NFDIColors.Mint.Base else Color NFDIColors.Red.Base -// ]] -// Button.OnClick (fun _ -> -// let inputId = (AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnitState model.AddBuildingBlockState).InputId -// if model.AddBuildingBlockState.BuildingBlockHasUnit = true then -// let e = Browser.Dom.document.getElementById inputId -// e?value <- null -// ToggleBuildingBlockHasUnit |> BuildingBlockMsg |> dispatch -// ) -// ] [ -// Fa.i [ -// Fa.Size Fa.FaLarge; -// Fa.Props [Style [AlignSelf AlignSelfOptions.Center; Transform "translateY(1px)"]] -// if model.AddBuildingBlockState.BuildingBlockHasUnit then -// Fa.Solid.Check -// else -// Fa.Solid.Ban -// ] [ ] -// ] -// ] -// Control.p [] [ -// Button.button [Button.IsStatic true; Button.Props [Style [BackgroundColor ExcelColors.Colorfull.white]]] [ -// str (sprintf "This %s has a unit:" (model.AddBuildingBlockState.CurrentBuildingBlock.Type.toString)) -// ] -// ] -// AutocompleteSearch.autocompleteTermSearchComponentInputComponent -// dispatch -// if BuildingBlockHasUnit = false then disabled = true -// (model.AddBuildingBlockState.BuildingBlockHasUnit |> not) -// "Start typing to search" -// None // No input size specified -// unitAutoCompleteParams -// ] -// Ontology Unit Term search preview -// AutocompleteSearch.autocompleteDropdownComponent -// dispatch -// model.SiteStyleState.ColorMode -// unitAutoCompleteParams.DropDownIsVisible -// unitAutoCompleteParams.DropDownIsLoading -// (AutocompleteSearch.createAutocompleteSuggestions dispatch unitAutoCompleteParams model) -// ] - -// div [] [ -// Help.help [Help.Props [Style [Display DisplayOptions.Inline]]] [ -// a [OnClick (fun _ -> AdvancedSearch.ToggleModal (AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockState model.AddBuildingBlockState).ModalId |> AdvancedSearchMsg |> dispatch)] [ -// str "Use advanced search building block" -// ] -// ] -// if model.AddBuildingBlockState.CurrentBuildingBlock.Type.isTermColumn then -// Help.help [Help.Props [Style [Display DisplayOptions.Inline; Float FloatOptions.Right]]] [ -// a [OnClick (fun _ -> AdvancedSearch.ToggleModal (AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnitState model.AddBuildingBlockState).ModalId |> AdvancedSearchMsg |> dispatch)] [ -// str "Use advanced search unit" -// ] -// ] -// ] - -// Field.div [] [ -// Button.button [ -// let isValid = model.AddBuildingBlockState.CurrentBuildingBlock |> Helper.isValidBuildingBlock -// if isValid then -// Button.Color Color.IsSuccess -// Button.IsActive true -// else -// Button.Color Color.IsDanger -// Button.Props [Disabled true] -// Button.IsFullWidth -// Button.OnClick (fun e -> -// let colName = model.AddBuildingBlockState.CurrentBuildingBlock -// let colTerm = -// if colName.isFeaturedColumn then -// TermMinimal.create colName.Type.toString colName.Type.getFeaturedColumnAccession |> Some -// elif model.AddBuildingBlockState.BuildingBlockSelectedTerm.IsSome && not colName.isSingleColumn then -// TermMinimal.ofTerm model.AddBuildingBlockState.BuildingBlockSelectedTerm.Value |> Some -// else -// None -// let unitTerm = if model.AddBuildingBlockState.UnitSelectedTerm.IsSome && colName.isTermColumn && not colName.isFeaturedColumn then TermMinimal.ofTerm model.AddBuildingBlockState.UnitSelectedTerm.Value |> Some else None -// let newBuildingBlock = InsertBuildingBlock.create colName colTerm unitTerm Array.empty -// SpreadsheetInterface.AddAnnotationBlock newBuildingBlock |> InterfaceMsg |> dispatch -// ) -// ] [ -// str "Add building block" -// ] -// ] -// ] - open Feliz open Feliz.Bulma let addUnitToExistingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = - // /// advanced unit term search 2 - //let autocompleteParamsUnit2 = AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnit2State model.AddBuildingBlockState mainFunctionContainer [ - // advanced unit term search 2 - //AdvancedSearch.advancedSearchModal model autocompleteParamsUnit2.ModalId autocompleteParamsUnit2.InputId dispatch autocompleteParamsUnit2.OnAdvancedSearch - //Bulma.field.div [ - // Bulma.help [ - // b [] [str "Adds a unit to a complete building block." ] - // str " If the building block already has a unit assigned, the new unit is only applied to selected rows of the selected column." - // ] - //] - //Bulma.field.div [ - // let changeUnitAutoCompleteParams = AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnit2State model.AddBuildingBlockState - // Bulma.field.div [ - // Bulma.field.hasAddons - // prop.children [ - // Bulma.control.p [ - // Bulma.button.button [ - // Bulma.button.isStatic - // Bulma.color.hasBackgroundWhite - // prop.text "Add unit" - // ] - // ] - // // Add/Update unit ontology term search field - // AutocompleteSearch.autocompleteTermSearchComponentInputComponent - // dispatch - // false // isDisabled - // "Start typing to search" - // None // No input size specified - // changeUnitAutoCompleteParams - // ] - // ] - // // Add/Update Ontology Unit Term search preview - // AutocompleteSearch.autocompleteDropdownComponent - // dispatch - // changeUnitAutoCompleteParams.DropDownIsVisible - // changeUnitAutoCompleteParams.DropDownIsLoading - // (AutocompleteSearch.createAutocompleteSuggestions dispatch changeUnitAutoCompleteParams model) - - //] - //Bulma.help [ - // prop.style [style.display.inlineElement] - // prop.children [ - // Html.a [ - // prop.onClick(fun e -> - // e.preventDefault() - // AdvancedSearch.ToggleModal ( - // AutocompleteSearch.AutocompleteParameters.ofAddBuildingBlockUnit2State model.AddBuildingBlockState).ModalId - // |> AdvancedSearchMsg - // |> dispatch - // ) - // prop.text "Use advanced search" - // ] - // ] - //] - Bulma.field.div [ Bulma.button.button [ @@ -353,11 +140,6 @@ let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = // Input forms, etc related to add building block. Bulma.label "Add annotation building blocks (columns) to the annotation table." - //match model.PersistentStorageState.Host with - //| Swatehost.Excel _ -> - // addBuildingBlockElements model dispatch - //| _ -> - // () SearchComponent.Main model dispatch match model.PersistentStorageState.Host with diff --git a/src/Client/Pages/BuildingBlock/Dropdown.fs b/src/Client/Pages/BuildingBlock/Dropdown.fs index bb72454e..941ee8c5 100644 --- a/src/Client/Pages/BuildingBlock/Dropdown.fs +++ b/src/Client/Pages/BuildingBlock/Dropdown.fs @@ -149,18 +149,6 @@ module private DropdownElements = /// Output columns subpage for dropdown let dropdownContentIOTypeColumns header 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 name - // ] - // ] - //] - //Bulma.dropdownDivider [] IOType.Source |> createIOTypeDropdownItem model dispatch setState header IOType.Sample |> createIOTypeDropdownItem model dispatch setState header IOType.Material |> createIOTypeDropdownItem model dispatch setState header diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index d25414d7..229cb55e 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -129,7 +129,5 @@ let Main (model: 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 ] From 5c83cfbcce49705ed9a58d6264f3364427ebcf0a Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 31 Jan 2024 17:26:37 +0100 Subject: [PATCH 019/135] Release vv1.0.0-alpha.05 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 04be7877..9739f209 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "1.0.0-alpha.03" - let [] AssemblyMetadata_ReleaseDate = "25.01.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.05" + let [] AssemblyMetadata_ReleaseDate = "31.01.2024" From 8581dcc76511484fdd22bb2440fe0617d46c9167 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 31 Jan 2024 17:40:55 +0100 Subject: [PATCH 020/135] =?UTF-8?q?hotfix=20modal=20overflow=20:bug:?= =?UTF-8?q?=F0=9F=98=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Client/MainComponents/BuildingBlockModal.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Client/MainComponents/BuildingBlockModal.fs b/src/Client/MainComponents/BuildingBlockModal.fs index dc34673a..241083fa 100644 --- a/src/Client/MainComponents/BuildingBlockModal.fs +++ b/src/Client/MainComponents/BuildingBlockModal.fs @@ -58,7 +58,10 @@ type Modals = ] ] Bulma.modalCardBody [ - BuildingBlock.SearchComponent.Main model dispatch + prop.style [style.overflow.inheritFromParent] + prop.children [ + BuildingBlock.SearchComponent.Main model dispatch + ] ] ] ] From 755e07fb4216ce48d3eeafe5e9fbe55f3b45bed4 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 31 Jan 2024 17:41:23 +0100 Subject: [PATCH 021/135] Release vv1.0.0-alpha.06 :bookmark: --- src/Server/Version.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 9739f209..cd78b760 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] +[] [] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha.05" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.06" let [] AssemblyMetadata_ReleaseDate = "31.01.2024" From dc06391e4f43751fd38dbbc64170ca1bc94b14c8 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 1 Feb 2024 10:55:29 +0100 Subject: [PATCH 022/135] Improve utility robustness :sparkles:#347 --- src/Client/Client.fsproj | 2 +- src/Client/LocalStorage/ModalPositions.fs | 44 +- .../MainComponents/BuildingBlockModal.fs | 69 -- src/Client/MainComponents/Modals.fs | 134 +++ .../Pages/BuildingBlock/BuildingBlockView.fs | 4 +- .../Pages/BuildingBlock/SearchComponent.fs | 2 +- .../SharedComponents/TermSearchInput.fs | 1 + src/Client/style.scss | 901 +++++++++--------- 8 files changed, 624 insertions(+), 533 deletions(-) delete mode 100644 src/Client/MainComponents/BuildingBlockModal.fs create mode 100644 src/Client/MainComponents/Modals.fs diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index e625f37a..296e8486 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -86,7 +86,7 @@ - + diff --git a/src/Client/LocalStorage/ModalPositions.fs b/src/Client/LocalStorage/ModalPositions.fs index 67a663ac..f9b7e3a5 100644 --- a/src/Client/LocalStorage/ModalPositions.fs +++ b/src/Client/LocalStorage/ModalPositions.fs @@ -1,9 +1,12 @@ -module LocalStorage.ModalPositions +module LocalStorage.Modal open Feliz open Fable.Core.JsInterop -type Position = { +/// +/// Is not only used to store position but also size. +/// +type Rect = { X: int Y: int } with @@ -14,22 +17,24 @@ type Position = { open Fable.SimpleJson +let [] BuildingBlockModal = "BuildingBlock" + [] -module LocalStorage = +module Position = open Browser - let [] private DataTheme_Key_Prefix = "ModalPosition_" + let [] private Key_Prefix = "ModalPosition_" - let write(modalName:string, dt: Position) = + let write(modalName:string, dt: Rect) = let s = Json.serialize dt - WebStorage.localStorage.setItem(DataTheme_Key_Prefix + modalName, s) + WebStorage.localStorage.setItem(Key_Prefix + modalName, s) let load(modalName:string) = - let key = DataTheme_Key_Prefix + modalName + let key = Key_Prefix + modalName try WebStorage.localStorage.getItem(key) - |> Json.parseAs + |> Json.parseAs |> Some with |_ -> @@ -37,4 +42,25 @@ module LocalStorage = printfn "Could not find %s" key None -let [] BuildingBlockModal = "BuildingBlock" + +[] +module Size = + open Browser + + let [] private Key_Prefix = "ModalSize_" + + let write(modalName:string, dt: Rect) = + let s = Json.serialize dt + WebStorage.localStorage.setItem(Key_Prefix + modalName, s) + + let load(modalName:string) = + let key = Key_Prefix + modalName + try + WebStorage.localStorage.getItem(key) + |> Json.parseAs + |> Some + with + |_ -> + WebStorage.localStorage.removeItem(key) + printfn "Could not find %s" key + None diff --git a/src/Client/MainComponents/BuildingBlockModal.fs b/src/Client/MainComponents/BuildingBlockModal.fs deleted file mode 100644 index 241083fa..00000000 --- a/src/Client/MainComponents/BuildingBlockModal.fs +++ /dev/null @@ -1,69 +0,0 @@ -namespace MainComponents - -open Feliz -open Feliz.Bulma -open Browser.Types - -open LocalStorage.ModalPositions - -module InitExtensions = - - type Position with - static member initBuildingBlock() = - match LocalStorage.load LocalStorage.ModalPositions.BuildingBlockModal with - | Some p -> p - | None -> Position.init() - -open InitExtensions - -type Modals = - - [] - static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = - let position, setPosition = React.useState(Position.initBuildingBlock) - let startPosition, setStartPosition = React.useState(None) - let isMoving, setIsMoving = React.useState false - let element = React.useElementRef() - Bulma.modalCard [ - prop.ref element - prop.style [style.overflow.visible; style.top position.Y; style.left position.X; style.position.absolute] - prop.children [ - Bulma.modalCardHead [ - prop.onMouseDown(fun e -> - let x = e.clientX - element.current.Value.offsetLeft - let y = e.clientY - element.current.Value.offsetTop; - setStartPosition <| Some {X = int x; Y = int y} - setIsMoving true - ) - prop.onMouseMove(fun e -> - if isMoving then - log "move" - let maxX = Browser.Dom.window.innerWidth - element.current.Value.offsetWidth; - let tempX = int e.clientX - startPosition.Value.X - let newX = System.Math.Min(System.Math.Max(tempX,0),int maxX) - let maxY = Browser.Dom.window.innerHeight - element.current.Value.offsetHeight; - let tempY = int e.clientY - startPosition.Value.Y - let newY = System.Math.Min(System.Math.Max(tempY,0),int maxY) - setPosition {X = newX; Y = newY} - ) - prop.onMouseUp(fun _ -> - setStartPosition None - LocalStorage.write(BuildingBlockModal,position) - setIsMoving false - ) - prop.style [style.cursor.move] - prop.children [ - Bulma.modalCardTitle Html.none - Bulma.delete [ prop.onClick rmv ] - ] - ] - Bulma.modalCardBody [ - prop.style [style.overflow.inheritFromParent] - prop.children [ - BuildingBlock.SearchComponent.Main model dispatch - ] - ] - ] - ] - - diff --git a/src/Client/MainComponents/Modals.fs b/src/Client/MainComponents/Modals.fs new file mode 100644 index 00000000..3b5230b3 --- /dev/null +++ b/src/Client/MainComponents/Modals.fs @@ -0,0 +1,134 @@ +namespace MainComponents + +open Feliz +open Feliz.Bulma +open Browser.Types + +open LocalStorage.Modal + +module InitExtensions = + + type Rect with + static member initBuildingBlockPosition() = + match Position.load BuildingBlockModal with + | Some p -> Some p + | None -> None + + static member initBuildingBlockSize() = + match Size.load BuildingBlockModal with + | Some p -> Some p + | None -> None + +open InitExtensions + +open Fable.Core +open Fable.Core.JsInterop + +module MoveEventListener = + + open Fable.Core.JsInterop + + let onmousemove (element:IRefValue) (startPosition: Rect) setPosition = fun (e: Event) -> + let e : MouseEvent = !!e + let maxX = Browser.Dom.window.innerWidth - element.current.Value.offsetWidth; + let tempX = int e.clientX - startPosition.X + let newX = System.Math.Min(System.Math.Max(tempX,0),int maxX) + let maxY = Browser.Dom.window.innerHeight - element.current.Value.offsetHeight; + let tempY = int e.clientY - startPosition.Y + let newY = System.Math.Min(System.Math.Max(tempY,0),int maxY) + setPosition (Some {X = newX; Y = newY}) + + let onmouseup onmousemove = + Browser.Dom.document.removeEventListener("mousemove", onmousemove) + +module ResizeEventListener = + + open Fable.Core.JsInterop + + let onmousemove (startPosition: Rect) (startSize: Rect) setSize = fun (e: Event) -> + let e : MouseEvent = !!e + let width = int e.clientX - startPosition.X + startSize.X + //let height = int e.clientY - startPosition.Y + startSize.Y + setSize (Some {X = width; Y = startSize.Y}) + + let onmouseup onmousemove = + Browser.Dom.document.removeEventListener("mousemove", onmousemove) + +module Elements = + let resizeElement (content: ReactElement) = + Html.div [ + prop.style [style.cursor.northWestSouthEastResize; style.border(1, borderStyle.solid, "black")] + prop.children content + ] + +type Modals = + + [] + static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = + let position, setPosition = React.useState(Rect.initBuildingBlockPosition) + let size, setSize = React.useState(Rect.initBuildingBlockSize) + let element = React.useElementRef() + let resizeElement (content: ReactElement) = + Bulma.modalCard [ + prop.ref element + prop.onMouseDown(fun e -> + e.preventDefault() + e.stopPropagation() + let startPosition = {X = int e.clientX; Y = int e.clientY} + let startSize = {X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight} + let onmousemove = ResizeEventListener.onmousemove startPosition startSize setSize + let onmouseup = fun e -> + ResizeEventListener.onmouseup onmousemove + if size.IsSome then + Size.write(BuildingBlockModal,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) + Browser.Dom.document.addEventListener("mousemove", onmousemove) + let config = createEmpty + config.once <- true + Browser.Dom.document.addEventListener("mouseup", onmouseup, config) + ) + prop.style [ + style.cursor.eastWestResize; style.display.flex + style.padding(2); style.overflow.visible + style.position.absolute + if size.IsSome then + style.width size.Value.X + if position.IsSome then + style.top position.Value.Y; style.left position.Value.X; + ] + prop.children content + ] + resizeElement <| Html.div [ + prop.children [ + Bulma.modalCardHead [ + prop.onMouseDown(fun e -> + e.preventDefault() + e.stopPropagation() + let x = e.clientX - element.current.Value.offsetLeft + let y = e.clientY - element.current.Value.offsetTop; + let startPosition = {X = int x; Y = int y} + let onmousemove = MoveEventListener.onmousemove element startPosition setPosition + let onmouseup = fun e -> + MoveEventListener.onmouseup onmousemove + if position.IsSome then + Position.write(BuildingBlockModal,position.Value) + Browser.Dom.document.addEventListener("mousemove", onmousemove) + let config = createEmpty + config.once <- true + Browser.Dom.document.addEventListener("mouseup", onmouseup, config) + ) + prop.style [style.cursor.move] + prop.children [ + Bulma.modalCardTitle Html.none + Bulma.delete [ prop.onClick rmv ] + ] + ] + Bulma.modalCardBody [ + prop.style [style.overflow.inheritFromParent] + prop.children [ + BuildingBlock.SearchComponent.Main model dispatch + ] + ] + ] + ] + + diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index 4629ca16..d85f9391 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -140,7 +140,9 @@ let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = // Input forms, etc related to add building block. Bulma.label "Add annotation building blocks (columns) to the annotation table." - SearchComponent.Main model dispatch + mainFunctionContainer [ + SearchComponent.Main model dispatch + ] match model.PersistentStorageState.Host with | Some Swatehost.Excel -> diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index 229cb55e..f6dc9893 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -125,7 +125,7 @@ let Main (model: Model) dispatch = let state_bb, setState_bb = React.useState(BuildingBlockUIState.init) //let state_searchHeader, setState_searchHeader = React.useState(TermSearchUIState.init) //let state_searchBody, setState_searchBody = React.useState(TermSearchUIState.init) - mainFunctionContainer [ + Html.div [ SearchBuildingBlockHeaderElement (state_bb, setState_bb, model, dispatch) if model.AddBuildingBlockState.HeaderCellType.IsTermColumn() then SearchBuildingBlockBodyElement (model, dispatch) diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index d1a74bba..89f9e4b3 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -332,6 +332,7 @@ type TermSearch = ] if size.IsSome then size.Value if state.IsSome then prop.valueOrDefault state.Value.NameText + prop.onMouseDown(fun e -> e.stopPropagation()) 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 diff --git a/src/Client/style.scss b/src/Client/style.scss index 7f151300..d31aaad9 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -99,557 +99,554 @@ $colors: map-merge($colors, $addColors); } .term-select-area { - position: absolute; - width: 100%; - max-height: 400px; - overflow: auto; - border: 1px solid $border-color; - background-color: $scheme-main; - border-top: unset; - z-index: 20; + position: absolute; + width: 100%; + max-height: 400px; + overflow: auto; + border: 1px solid $border-color; + background-color: $scheme-main; + border-top: unset; + z-index: 20; + + .term-select-item { + + > * { + padding: .5rem 1rem; + } - .term-select-item { + .term-select-item-main { + cursor: pointer; - > * { - padding: .5rem 1rem; + > *:not(:last-child) { + padding-right: .5rem; } - .term-select-item-main { - cursor: pointer; - - > *:not(:last-child) { - padding-right: .5rem; - } - - a:hover { - text-decoration: underline - } + a:hover { + text-decoration: underline } + } - .term-select-item-more { - border-top: .5px solid $border-color; - box-shadow: 0px 4px 8px $border-color; - - td { - border: unset; - padding: 0.15em 0.25em; - } - } + .term-select-item-more { + border-top: .5px solid $border-color; + box-shadow: 0px 4px 8px $border-color; - .term-select-item-toggle-button { - padding: calc(0.5em - 1px) .8em; - display: flex; - background-color: transparent; + td { border: unset; - cursor: pointer; - color: $scheme-invert; - justify-content: center; - align-items: center; - border-radius: 2px; - white-space: nowrap + padding: 0.15em 0.25em; } + } - .term-select-item-toggle-button:hover { - background-color: $content-blockquote-background-color - } + .term-select-item-toggle-button { + padding: calc(0.5em - 1px) .8em; + display: flex; + background-color: transparent; + border: unset; + cursor: pointer; + color: $scheme-invert; + justify-content: center; + align-items: center; + border-radius: 2px; + white-space: nowrap } - .term-select-item:not(:last-child) { - border-bottom: 1px solid $border-color; + .term-select-item-toggle-button:hover { + background-color: $content-blockquote-background-color } } - .modal-background { - opacity: 0.5 + .term-select-item:not(:last-child) { + border-bottom: 1px solid $border-color; } +} - .modal-card { - box-shadow: 2px 2px black; - } - /*https: //stackoverflow.com/questions/54044479/table-with-sticky-header-and-resizable-columns-without-using-jquery*/ - .fixed_headers { - border-collapse: collapse; - width: max-content; - border-style: hidden - } +.modal-background { + opacity: 0.5 +} - .fixed_headers td, - .fixed_headers thead th { - text-align: left; - } +/*https: //stackoverflow.com/questions/54044479/table-with-sticky-header-and-resizable-columns-without-using-jquery*/ +.fixed_headers { + border-collapse: collapse; + width: max-content; + border-style: hidden +} - .fixed_headers thead tr { - position: relative; - } +.fixed_headers td, +.fixed_headers thead th { + text-align: left; +} - .fixed_headers tbody tr:nth-child(even) { - background-color: #DDD; - } +.fixed_headers thead tr { + position: relative; +} - .fixed_headers thead th { - position: sticky; - top: 0; /* REQUIRED: https://stackoverflow.com/a/43707215 */ - resize: horizontal; - overflow: auto; - z-index: 2 - } +.fixed_headers tbody tr:nth-child(even) { + background-color: #DDD; +} - html, body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - font-size: 14px; - min-width: 400px; - height: 100vh; - overflow: auto - } +.fixed_headers thead th { + position: sticky; + top: 0; /* REQUIRED: https://stackoverflow.com/a/43707215 */ + resize: horizontal; + overflow: auto; + z-index: 2 +} - a { - color: $nfdi-blue-light - } +html, body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-size: 14px; + min-width: 400px; + height: 100vh; + overflow: auto +} - a:hover { - color: $nfdi-blue-lighter-20 - } +a { + color: $nfdi-blue-light +} - .dragover-footertab { - position: relative - } +a:hover { + color: $nfdi-blue-lighter-20 +} - .dragover-footertab:before { - position: absolute; - content: ""; - margin: 0 3px; - display: inline-block; - border: 7px solid transparent; - border-top: 8px solid black; - border-bottom: 0 none; - top: -3px; - left: -9px; - } +.dragover-footertab { + position: relative +} - [type=checkbox]:checked.switch.is-primary + label::before, [type=checkbox]:checked.switch.is-primary + label::before { - background: $nfdi-mint - } +.dragover-footertab:before { + position: absolute; + content: ""; + margin: 0 3px; + display: inline-block; + border: 7px solid transparent; + border-top: 8px solid black; + border-bottom: 0 none; + top: -3px; + left: -9px; +} - [data-tooltip]:not(.is-loading)::before, [data-tooltip]:not(.is-loading)::after, [data-tooltip]:not(.is-disabled)::before, [data-tooltip]:not(.is-disabled)::after, [data-tooltip]:not([disabled])::before, [data-tooltip]:not([disabled])::after { - box-sizing: border-box; - color: #fff; - left: 50px; - display: none; - font-family: BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif; - font-size: .75rem; - hyphens: auto; - opacity: 0; - overflow: hidden; - pointer-events: none; - position: absolute; - visibility: hidden; - z-index: 1020; - } +[type=checkbox]:checked.switch.is-primary + label::before, [type=checkbox]:checked.switch.is-primary + label::before { + background: $nfdi-mint +} - [data-tooltip]:hover::before { - display: inline-block - } +[data-tooltip]:not(.is-loading)::before, [data-tooltip]:not(.is-loading)::after, [data-tooltip]:not(.is-disabled)::before, [data-tooltip]:not(.is-disabled)::after, [data-tooltip]:not([disabled])::before, [data-tooltip]:not([disabled])::after { + box-sizing: border-box; + color: #fff; + left: 50px; + display: none; + font-family: BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif; + font-size: .75rem; + hyphens: auto; + opacity: 0; + overflow: hidden; + pointer-events: none; + position: absolute; + visibility: hidden; + z-index: 1020; +} - #cy { - height: 300px; - display: block - } +[data-tooltip]:hover::before { + display: inline-block +} - .myNavbarSticky { - position: sticky; - position: -webkit-sticky; - min-height: min-content; - top: 0; - background-color: $primary; - } +#cy { + height: 300px; + display: block +} - .myNavbarSticky .navbar-item#logo { - position: relative; - overflow: hidden; - - &::before { - content: ""; - border-radius: 90px; - position: absolute; - top: 50%; - left: 50%; - box-shadow: 0 0 15px 5px #fff, /* inner white */ - 0 0 30px 15px $success; - } - } +.myNavbarSticky { + position: sticky; + position: -webkit-sticky; + min-height: min-content; + top: 0; + background-color: $primary; +} - .myNavbarButton { - height: 100% !important; - width: 100% !important; - background-color: transparent; - border-radius: unset; - border-color: transparent !important; - align-items: center; - justify-content: center; - color: white; - display: inline-flex; - padding: unset; - box-shadow: unset !important - } +.myNavbarSticky .navbar-item#logo { + position: relative; + overflow: hidden; - .myNavbarButton:not([disabled]):active { - background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)) !important + &::before { + content: ""; + border-radius: 90px; + position: absolute; + top: 50%; + left: 50%; + box-shadow: 0 0 15px 5px #fff, /* inner white */ + 0 0 30px 15px $success; } +} - .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within, .myNavbarButton:not([disabled]):hover { - background-color: transparent; - color: $success - } +.myNavbarButton { + height: 100% !important; + width: 100% !important; + background-color: transparent; + border-radius: unset; + border-color: transparent !important; + align-items: center; + justify-content: center; + color: white; + display: inline-flex; + padding: unset; + box-shadow: unset !important +} - .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within { - border-color: white !important; - } +.myNavbarButton:not([disabled]):active { + background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)) !important +} - .myNavbarButton:not([disabled]):focus:not(:focus-visible), .myNavbarButton:not([disabled]):focus-within:not(:focus-visible) { - border-color: transparent !important; - color: white - } +.myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within, .myNavbarButton:not([disabled]):hover { + background-color: transparent; + color: $success +} - .myNavbarButton[disabled] { - filter: brightness(85%); - background-color: transparent; - color: white; - } +.myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within { + border-color: white !important; +} - .myNavbarButton[disabled]:focus, .myNavbarButton[disabled]:focus-within { - box-shadow: unset - } +.myNavbarButton:not([disabled]):focus:not(:focus-visible), .myNavbarButton:not([disabled]):focus-within:not(:focus-visible) { + border-color: transparent !important; + color: white +} - .help { - @extend .help; - font-size: 0.85rem; - } +.myNavbarButton[disabled] { + filter: brightness(85%); + background-color: transparent; + color: white; +} +.myNavbarButton[disabled]:focus, .myNavbarButton[disabled]:focus-within { + box-shadow: unset +} - .mainFunctionContainer { - border-width: 0px 0px 0px 5px; - border-style: none none none solid; - padding: 0.25rem 1rem; - margin-bottom: 1rem; - border-image-slice: 1 - } +.help { + @extend .help; + font-size: 0.85rem; +} - .wrapFlexBox { - flex-wrap: wrap; - flex-shrink: 1; - } - .button { - @extend .button; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - } +.mainFunctionContainer { + border-width: 0px 0px 0px 5px; + border-style: none none none solid; + padding: 0.25rem 1rem; + margin-bottom: 1rem; + border-image-slice: 1 +} - .textarea { - @extend .textarea; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; - border: 3px solid lightgrey; - } +.wrapFlexBox { + flex-wrap: wrap; + flex-shrink: 1; +} - .textarea:focus { - border-color: $nfdi-mint - } +.button { + @extend .button; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} - .button:disabled, .button[disabled] { - opacity: 0.5 - } +.textarea { + @extend .textarea; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important; + border: 3px solid lightgrey; +} - .input { - @extend .input; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - } +.textarea:focus { + border-color: $nfdi-mint +} - .clone * { - pointer-events: none - } +.button:disabled, .button[disabled] { + opacity: 0.5 +} - .delete { - @extend .delete - } +.input { + @extend .input; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} - a.navbar-item:hover { - background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)) !important; - } +.clone * { + pointer-events: none +} - .delete:hover { - @extend .delete; - background-color: $danger - } +.delete { + @extend .delete +} - .hoverTableEle { - } +a.navbar-item:hover { + background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)) !important; +} - .hoverTableEle:hover { - /*background-color: #E8E8E8 !important*/ - background-color: rgba(0, 0, 0, 0.2) - } +.delete:hover { + @extend .delete; + background-color: $danger +} - .clickableTag { - cursor: pointer - } +.hoverTableEle { +} - .clickableTag:hover { - /*background-color: $lightBlueLighter20 !important;*/ - box-shadow: inset 0 0 0 10em rgba(255, 255, 255, 0.3); - } +.hoverTableEle:hover { + /*background-color: #E8E8E8 !important*/ + background-color: rgba(0, 0, 0, 0.2) +} - .clickableTagDelete { - cursor: pointer; - border: 0.2px solid $info - } +.clickableTag { + cursor: pointer +} - .clickableTagDelete:hover { - background-color: $danger !important; - border-color: $danger !important; - color: white - } +.clickableTag:hover { + /*background-color: $lightBlueLighter20 !important;*/ + box-shadow: inset 0 0 0 10em rgba(255, 255, 255, 0.3); +} - .toExcelColor { - color: $primarye !important - } - /////////// Custom simple checkbox, due to issue #54 /////////////////// - .checkbox-label { - display: inline-block; - position: relative; - padding-left: 35px; - margin-bottom: 12px; - cursor: pointer; - font-size: 1rem; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - width: auto; - } - /* Hide the browser's default checkbox */ - .checkbox-input { - position: absolute; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; - } - /* Create a custom checkbox */ - .checkbox-checkmark { - position: absolute; - top: 3px; - left: 3px; - height: 20px; - width: 20px; - background-color: #eee; - border: 0.1px solid #dbdbdb; - border-radius: 50%; - cursor: pointer; - } +.clickableTagDelete { + cursor: pointer; + border: 0.2px solid $info +} - .checkbox-input[disabled] ~ .checkbox-label, .checkbox-input[disabled] ~ .checkbox-checkmark { - color: lightgrey; - cursor: not-allowed; - } - /* On mouse-over, add a grey background color */ - .checkbox-label:hover ~ .checkbox-checkmark, .checkbox-checkmark:hover { - background-color: #ccc; - } - /* On mouse-over on a disabled input, add a grey background color */ - .checkbox-input[disabled] ~ .checkbox-checkmark:hover, .checkbox-input[disabled] ~ .checkbox-label:hover ~ .checkbox-checkmark { - background-color: unset; - } - /* When the checkbox is checked, add a green background */ - .checkbox-input:checked ~ .checkbox-checkmark { - background-color: $success; - border: none; - } - /* Create the checkmark/indicator (hidden when not checked) */ - .checkbox-checkmark:after { - content: ""; - position: absolute; - display: none; - } - /* Show the indicator when checked */ - .checkbox-input:checked ~ .checkbox-checkmark:after { - display: block; - } - /* Style the checkmark/indicator */ - .checkbox-checkmark:after { - top: 6px; - left: 6px; - width: 8px; - height: 8px; - border-radius: 50%; - background: white; - } - ////////////////// End of custom checkbox ///////////////////////// +.clickableTagDelete:hover { + background-color: $danger !important; + border-color: $danger !important; + color: white +} - .niceBkgrnd { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 5s ease infinite; - } +.toExcelColor { + color: $primarye !important +} +/////////// Custom simple checkbox, due to issue #54 /////////////////// +.checkbox-label { + display: inline-block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 1rem; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: auto; +} +/* Hide the browser's default checkbox */ +.checkbox-input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} +/* Create a custom checkbox */ +.checkbox-checkmark { + position: absolute; + top: 3px; + left: 3px; + height: 20px; + width: 20px; + background-color: #eee; + border: 0.1px solid #dbdbdb; + border-radius: 50%; + cursor: pointer; +} - @keyframes gradient { - 0% { - background-position: 0% 50%; - } +.checkbox-input[disabled] ~ .checkbox-label, .checkbox-input[disabled] ~ .checkbox-checkmark { + color: lightgrey; + cursor: not-allowed; +} +/* On mouse-over, add a grey background color */ +.checkbox-label:hover ~ .checkbox-checkmark, .checkbox-checkmark:hover { + background-color: #ccc; +} +/* On mouse-over on a disabled input, add a grey background color */ +.checkbox-input[disabled] ~ .checkbox-checkmark:hover, .checkbox-input[disabled] ~ .checkbox-label:hover ~ .checkbox-checkmark { + background-color: unset; +} +/* When the checkbox is checked, add a green background */ +.checkbox-input:checked ~ .checkbox-checkmark { + background-color: $success; + border: none; +} +/* Create the checkmark/indicator (hidden when not checked) */ +.checkbox-checkmark:after { + content: ""; + position: absolute; + display: none; +} +/* Show the indicator when checked */ +.checkbox-input:checked ~ .checkbox-checkmark:after { + display: block; +} +/* Style the checkmark/indicator */ +.checkbox-checkmark:after { + top: 6px; + left: 6px; + width: 8px; + height: 8px; + border-radius: 50%; + background: white; +} +////////////////// End of custom checkbox ///////////////////////// - 50% { - background-position: 100% 50%; - } +.niceBkgrnd { + background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); + background-size: 400% 400%; + animation: gradient 5s ease infinite; +} - 100% { - background-position: 0% 50%; - } +@keyframes gradient { + 0% { + background-position: 0% 50%; } - tr.suggestion :hover { - cursor: pointer; + 50% { + background-position: 100% 50%; } - tr.suggestion-details { - /*visibility: collapse;*/ - display: none; - box-shadow: 1px 2px 2px darkgrey + 100% { + background-position: 0% 50%; } +} +tr.suggestion :hover { + cursor: pointer; +} - .delete :hover { - cursor: pointer; - } +tr.suggestion-details { + /*visibility: collapse;*/ + display: none; + box-shadow: 1px 2px 2px darkgrey +} - .nonSelectText { - user-select: none; /* standard syntax */ - -webkit-user-select: none; /* webkit (safari, chrome) browsers */ - -moz-user-select: none; /* mozilla browsers */ - -khtml-user-select: none; /* webkit (konqueror) browsers */ - -ms-user-select: none; /* IE10+ */ - } - input::-ms-clear { - display: none; - } +.delete :hover { + cursor: pointer; +} - .hideOver575px { - display: none !important - } +.nonSelectText { + user-select: none; /* standard syntax */ + -webkit-user-select: none; /* webkit (safari, chrome) browsers */ + -moz-user-select: none; /* mozilla browsers */ + -khtml-user-select: none; /* webkit (konqueror) browsers */ + -ms-user-select: none; /* IE10+ */ +} - .hideOver775px { - display: none - } +input::-ms-clear { + display: none; +} - .hideUnder575px { - display: flex - } +.hideOver575px { + display: none !important +} - .hideUnder775px { - display: flex - } +.hideOver775px { + display: none +} - .flexToCentered { - } +.hideUnder575px { + display: flex +} - .myFlexText { - text-align: justify; - font-size: 1.2rem; - } - /// Brands +.hideUnder775px { + display: flex +} - .nfdiIcon { - transition: 0.2s opacity; - } +.flexToCentered { +} - .nfdiIcon:hover { - opacity: 0.7 - } +.myFlexText { + text-align: justify; + font-size: 1.2rem; +} +/// Brands - .myFaBrand { - transition: 0.2s opacity; - padding: 10px; - cursor: pointer - } +.nfdiIcon { + transition: 0.2s opacity; +} - .myFaBrand:hover { - opacity: 0.7; - } +.nfdiIcon:hover { + opacity: 0.7 +} - .myFaTwitter { - background: #55ACEE; - color: white; - } +.myFaBrand { + transition: 0.2s opacity; + padding: 10px; + cursor: pointer +} - .myFaCSB { - background: #ed7d31; - color: white; - } +.myFaBrand:hover { + opacity: 0.7; +} - .myFaGithub { - background: #24292e; - color: white - } +.myFaTwitter { + background: #55ACEE; + color: white; +} - @media only screen and (max-width: 768px) { +.myFaCSB { + background: #ed7d31; + color: white; +} - .myFlexText { - font-size: 1rem; - width: 90%; - margin: auto; - margin-bottom: 0.5rem; - } +.myFaGithub { + background: #24292e; + color: white +} - .hideUnder775px { - display: none - } +@media only screen and (max-width: 768px) { - .hideOver775px { - display: flex - } + .myFlexText { + font-size: 1rem; + width: 90%; + margin: auto; + margin-bottom: 0.5rem; + } - .flexToCentered { - text-align: center - } + .hideUnder775px { + display: none + } - .reverseCols { - flex-direction: column-reverse; - display: flex; - } + .hideOver775px { + display: flex } - @media only screen and (max-width: 575px) { - .hideOver575px { - display: flex !important - } + .flexToCentered { + text-align: center + } - .hideUnder575px { - display: none !important - } + .reverseCols { + flex-direction: column-reverse; + display: flex; + } +} - .myFlexText { - margin-bottom: 0.5rem; - font-size: 0.8rem; - } +@media only screen and (max-width: 575px) { + .hideOver575px { + display: flex !important } - .danger_important { - color: $danger !important + .hideUnder575px { + display: none !important } - kbd { - margin: 0px 0.1em; - padding: 0.1em 0.6em; - border-radius: 3px; - border: 1px solid rgb(204, 204, 204); - color: rgb(51, 51, 51); - line-height: 1.4; - font-family: Arial,Helvetica,sans-serif; - font-size: 10px; - display: inline-block; - box-shadow: 0px 1px 0px rgba(0,0,0,0.2), inset 0px 0px 0px 2px #ffffff; - background-color: rgb(247, 247, 247); - -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; - -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - text-shadow: 0 1px 0 #fff; + .myFlexText { + margin-bottom: 0.5rem; + font-size: 0.8rem; } +} + +.danger_important { + color: $danger !important +} + +kbd { + margin: 0px 0.1em; + padding: 0.1em 0.6em; + border-radius: 3px; + border: 1px solid rgb(204, 204, 204); + color: rgb(51, 51, 51); + line-height: 1.4; + font-family: Arial,Helvetica,sans-serif; + font-size: 10px; + display: inline-block; + box-shadow: 0px 1px 0px rgba(0,0,0,0.2), inset 0px 0px 0px 2px #ffffff; + background-color: rgb(247, 247, 247); + -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; + -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-shadow: 0 1px 0 #fff; +} From a0b0ad1ac60ea99960884a55a609987a36fcae46 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 1 Feb 2024 11:02:45 +0100 Subject: [PATCH 023/135] Improve search cell robustness :bug:#348 --- .../SharedComponents/ClickOutsideHandler.fs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Client/SharedComponents/ClickOutsideHandler.fs b/src/Client/SharedComponents/ClickOutsideHandler.fs index 035f2c3b..7f635c1f 100644 --- a/src/Client/SharedComponents/ClickOutsideHandler.fs +++ b/src/Client/SharedComponents/ClickOutsideHandler.fs @@ -9,18 +9,26 @@ type ClickOutsideHandler = static member AddListener(containerId: string, clickedOutsideEvent: Event -> unit) = let rec closeEvent = fun (e: Event) -> + let rmv = fun () -> Browser.Dom.document.removeEventListener("click", closeEvent) let dropdown = Browser.Dom.document.getElementById(containerId) - let isClickedInsideDropdown : bool = dropdown?contains(e.target) - if not isClickedInsideDropdown then - clickedOutsideEvent e - Browser.Dom.document.removeEventListener("click", closeEvent) + if isNull dropdown then + rmv() + else + let isClickedInsideDropdown : bool = dropdown?contains(e.target) + if not isClickedInsideDropdown then + clickedOutsideEvent e + rmv() Browser.Dom.document.addEventListener("click", closeEvent) static member AddListener(element: IRefValue, clickedOutsideEvent: Event -> unit) = let rec closeEvent = fun (e: Event) -> + let rmv = fun () -> Browser.Dom.document.removeEventListener("click", closeEvent) let dropdown = element.current - let isClickedInsideDropdown : bool = dropdown?contains(e.target) - if not isClickedInsideDropdown then - clickedOutsideEvent e - Browser.Dom.document.removeEventListener("click", closeEvent) + if dropdown.IsNone then + rmv() + else + let isClickedInsideDropdown : bool = dropdown?contains(e.target) + if not isClickedInsideDropdown then + clickedOutsideEvent e + rmv() Browser.Dom.document.addEventListener("click", closeEvent) From 36e65dd85b820f645e267bdbd982d9c05049404e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 1 Feb 2024 15:37:18 +0100 Subject: [PATCH 024/135] fix cell default cell input issue :bug: #350 --- src/Client/MainComponents/Cells.fs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 87a11b01..5f38e5c8 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -65,8 +65,6 @@ module private CellComponents = ] let cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: string -> unit, setState_cell, state_cell, cell_value) = - log "cell_value" - log cell_value Bulma.input.text [ prop.readOnly isReadOnly prop.autoFocus true @@ -90,13 +88,14 @@ module private CellComponents = prop.onKeyDown(fun e -> match e.which with | 13. -> //enter - updateMainStateTable cell_value + updateMainStateTable state_cell.Value | 27. -> //escape setState_cell {CellMode = Idle; Value = cell_value} | _ -> () ) // Only change cell value while typing to increase performance. prop.onChange(fun e -> + setState_cell {state_cell with Value = e} ) prop.defaultValue cell_value @@ -347,8 +346,6 @@ type Cell = static member Body(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let cellValue = cell.GetContent().[0] let setter = fun (s: string) -> - log "SETTER" - log s let nextCell = cell.UpdateMainField s Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch let oaSetter = fun (oa:OntologyAnnotation option) -> From c5d41d504f3b88957a8a898921e2da8b447544da Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 1 Feb 2024 15:42:15 +0100 Subject: [PATCH 025/135] Fix cell select not removed when switching active view :bug:#349 --- src/Client/Update/SpreadsheetUpdate.fs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index e81fb52c..ad34b490 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -103,7 +103,11 @@ module Spreadsheet = {state with ArcFile = state.ArcFile} nextState, model, Cmd.none | UpdateActiveView nextView -> - let nextState = { state with ActiveView = nextView } + let nextState = { + state with + ActiveView = nextView + SelectedCells = Set.empty + } nextState, model, Cmd.none | RemoveTable removeIndex -> let nextState = Controller.removeTable removeIndex state From dedd7f4d9eb6d4fc495321cac6c9da427bd45948 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 1 Feb 2024 15:42:53 +0100 Subject: [PATCH 026/135] Release vv1.0.0-alpha.07 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index cd78b760..30576c20 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha.06" - let [] AssemblyMetadata_ReleaseDate = "31.01.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.07" + let [] AssemblyMetadata_ReleaseDate = "01.02.2024" From e03cee155e3b378b0d463ca11fbbfe3bd23905eb Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 09:49:42 +0100 Subject: [PATCH 027/135] Improve start position for widgets :art: --- src/Client/Client.fsproj | 2 +- src/Client/LocalStorage/ModalPositions.fs | 8 ++++---- src/Client/MainComponents/Cells.fs | 3 +-- src/Client/MainComponents/Navbar.fs | 4 ++-- .../MainComponents/{Modals.fs => Widgets.fs} | 17 ++++++++++------- 5 files changed, 18 insertions(+), 16 deletions(-) rename src/Client/MainComponents/{Modals.fs => Widgets.fs} (88%) diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 296e8486..8b33ea12 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -86,7 +86,7 @@ - + diff --git a/src/Client/LocalStorage/ModalPositions.fs b/src/Client/LocalStorage/ModalPositions.fs index f9b7e3a5..2d4fa505 100644 --- a/src/Client/LocalStorage/ModalPositions.fs +++ b/src/Client/LocalStorage/ModalPositions.fs @@ -1,4 +1,4 @@ -module LocalStorage.Modal +module LocalStorage.Widgets open Feliz open Fable.Core.JsInterop @@ -17,14 +17,14 @@ type Rect = { open Fable.SimpleJson -let [] BuildingBlockModal = "BuildingBlock" +let [] BuildingBlockWidgets = "BuildingBlock" [] module Position = open Browser - let [] private Key_Prefix = "ModalPosition_" + let [] private Key_Prefix = "WidgetsPosition_" let write(modalName:string, dt: Rect) = let s = Json.serialize dt @@ -47,7 +47,7 @@ module Position = module Size = open Browser - let [] private Key_Prefix = "ModalSize_" + let [] private Key_Prefix = "WidgetsSize_" let write(modalName:string, dt: Rect) = let s = Json.serialize dt diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 5f38e5c8..a4dd75e9 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -43,7 +43,6 @@ with member this.IsMainColumn = match this with | Main -> true | _ -> false member this.IsRefColumn = match this with | Unit | TSR | TAN -> true | _ -> false - module private CellComponents = @@ -308,7 +307,7 @@ type Cell = prop.onDoubleClick(fun e -> e.preventDefault() e.stopPropagation() - let mode = if e.ctrlKey && columnType = Main then Search else Active + let mode = if (e.ctrlKey || e.metaKey )&& columnType = Main then Search else Active if state_cell.IsIdle then setState_cell {state_cell with CellMode = mode} UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch ) diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index 1ba30567..4f46c6eb 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -100,9 +100,9 @@ let private modalDisplay (modal: Modal option, model, dispatch, setModal) = match modal with | None -> Html.none | Some Modal.BuildingBlock -> - MainComponents.Modals.BuildingBlock (model, dispatch, rmv) + MainComponents.Widgets.BuildingBlock (model, dispatch, rmv) | Some Modal.Template -> - MainComponents.Modals.BuildingBlock (model, dispatch, rmv) + MainComponents.Widgets.BuildingBlock (model, dispatch, rmv) [] let Main (model: Messages.Model) dispatch = diff --git a/src/Client/MainComponents/Modals.fs b/src/Client/MainComponents/Widgets.fs similarity index 88% rename from src/Client/MainComponents/Modals.fs rename to src/Client/MainComponents/Widgets.fs index 3b5230b3..2fc25140 100644 --- a/src/Client/MainComponents/Modals.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -4,18 +4,18 @@ open Feliz open Feliz.Bulma open Browser.Types -open LocalStorage.Modal +open LocalStorage.Widgets module InitExtensions = type Rect with static member initBuildingBlockPosition() = - match Position.load BuildingBlockModal with + match Position.load BuildingBlockWidgets with | Some p -> Some p | None -> None static member initBuildingBlockSize() = - match Size.load BuildingBlockModal with + match Size.load BuildingBlockWidgets with | Some p -> Some p | None -> None @@ -61,7 +61,7 @@ module Elements = prop.children content ] -type Modals = +type Widgets = [] static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = @@ -80,7 +80,7 @@ type Modals = let onmouseup = fun e -> ResizeEventListener.onmouseup onmousemove if size.IsSome then - Size.write(BuildingBlockModal,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) + Size.write(BuildingBlockWidgets,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) Browser.Dom.document.addEventListener("mousemove", onmousemove) let config = createEmpty config.once <- true @@ -89,9 +89,12 @@ type Modals = prop.style [ style.cursor.eastWestResize; style.display.flex style.padding(2); style.overflow.visible - style.position.absolute + style.position.fixedRelativeToWindow if size.IsSome then style.width size.Value.X + if position.IsNone then + style.transform.translate (length.perc -50,length.perc -50) + style.top (length.perc 50); style.left (length.perc 50); if position.IsSome then style.top position.Value.Y; style.left position.Value.X; ] @@ -110,7 +113,7 @@ type Modals = let onmouseup = fun e -> MoveEventListener.onmouseup onmousemove if position.IsSome then - Position.write(BuildingBlockModal,position.Value) + Position.write(BuildingBlockWidgets,position.Value) Browser.Dom.document.addEventListener("mousemove", onmousemove) let config = createEmpty config.once <- true From 9f86f7e91e41da2f1d745398cb9623fb7953729f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 11:44:34 +0100 Subject: [PATCH 028/135] Add button to go to search state --- src/Client/MainComponents/Cells.fs | 124 +++++++++++------- .../SharedComponents/TermSearchInput.fs | 21 ++- 2 files changed, 92 insertions(+), 53 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index a4dd75e9..cc85729a 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -8,6 +8,7 @@ open MainComponents open Messages open Shared open ARCtrl.ISA +open Components type private CellMode = | Active @@ -63,41 +64,69 @@ module private CellComponents = yield! specificStyle ] - let cellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: string -> unit, setState_cell, state_cell, cell_value) = - Bulma.input.text [ - prop.readOnly isReadOnly - prop.autoFocus true - prop.style [ - if isHeader then style.fontWeight.bold - style.width(length.percent 100) - style.height.unset - style.borderRadius(0) - style.border(0,borderStyle.none,"") - style.backgroundColor.transparent - style.margin (0) - style.padding(length.em 0.5,length.em 0.75) - //if isHeader then - // style.color(NFDIColors.white) + [] + let CellInputElement (isHeader: bool, isReadOnly: bool, updateMainStateTable: string -> unit, setState_cell, state_cell, cell_value, columnType) = + let ref = React.useElementRef() + React.useLayoutEffectOnce(fun _ -> ClickOutsideHandler.AddListener (ref, fun _ -> updateMainStateTable state_cell.Value)) + let input = + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + Bulma.input.text [ + prop.readOnly isReadOnly + prop.autoFocus true + prop.style [ + if isHeader then style.fontWeight.bold + style.width(length.percent 100) + style.height.unset + style.borderRadius(0) + style.border(0,borderStyle.none,"") + style.backgroundColor.transparent + style.margin (0) + style.padding(length.em 0.5,length.em 0.75) + //if isHeader then + // style.color(NFDIColors.white) + ] + // .. when pressing "ENTER". "ESCAPE" will negate changes. + prop.onKeyDown(fun e -> + match e.which with + | 13. -> //enter + updateMainStateTable state_cell.Value + | 27. -> //escape + setState_cell {CellMode = Idle; Value = cell_value} + | _ -> () + ) + // Only change cell value while typing to increase performance. + prop.onChange(fun e -> + setState_cell {state_cell with Value = e} + ) + prop.defaultValue cell_value + ] + ] ] - // Update main spreadsheet state when leaving focus or... - prop.onBlur(fun _ -> - updateMainStateTable cell_value - ) - // .. when pressing "ENTER". "ESCAPE" will negate changes. - prop.onKeyDown(fun e -> - match e.which with - | 13. -> //enter - updateMainStateTable state_cell.Value - | 27. -> //escape - setState_cell {CellMode = Idle; Value = cell_value} - | _ -> () - ) - // Only change cell value while typing to increase performance. - prop.onChange(fun e -> - - setState_cell {state_cell with Value = e} - ) - prop.defaultValue cell_value + let switchToSearchButton = + Bulma.control.div [ + Bulma.button.button [ + prop.className "is-ghost" + prop.style [style.borderWidth 0; style.borderRadius 0] + prop.onClick(fun e -> + e.stopPropagation() + setState_cell {state_cell with CellMode = Search} + ) + prop.children [ + Bulma.icon [Html.i [prop.className "fa-solid fa-search"]] + ] + ] + ] + Bulma.field.div [ + Bulma.field.hasAddons + prop.ref ref + prop.className "is-flex-grow-1 m-0" + prop.children [ + input + if not isHeader && columnType = Main then + switchToSearchButton + ] ] let basicValueDisplayCell (v: string) = @@ -214,6 +243,7 @@ type Cell = static member private HeaderBase(columnType: ColumnType, setter: string -> unit, cellValue: string, columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = let state = model.SpreadsheetModel let state_cell, setState_cell = React.useState(CellState.init(cellValue)) + React.useEffect((fun _ -> setState_cell {state_cell with Value = cellValue}), [|box cellValue|]) let isReadOnly = columnType = Unit Html.th [ if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter @@ -231,13 +261,12 @@ type Cell = prop.children [ if state_cell.IsActive then /// Update change to mainState and exit active input. - let updateMainStateTable = - fun (s: string) -> + let updateMainStateTable = fun (s: string) -> // Only update if changed - if s <> cellValue then - setter s - setState_cell {state_cell with CellMode = Idle} - cellInputElement(true, isReadOnly, updateMainStateTable, setState_cell, state_cell, cellValue) + if s <> cellValue then + setter s + setState_cell {state_cell with CellMode = Idle} + CellInputElement(true, isReadOnly, updateMainStateTable, setState_cell, state_cell, cellValue, columnType) else let cellValue = // shadow cell value for tsr and tan to add columnType match columnType with @@ -293,6 +322,7 @@ type Cell = let columnIndex, rowIndex = index let state = model.SpreadsheetModel let state_cell, setState_cell = React.useState(CellState.init(cellValue)) + React.useEffect((fun _ -> setState_cell {state_cell with Value = cellValue}), [|box cellValue|]) let isSelected = state.SelectedCells.Contains index let makeIdle() = setState_cell {state_cell with CellMode = Idle} Html.td [ @@ -316,20 +346,20 @@ type Cell = match state_cell.CellMode with | Active -> /// Update change to mainState and exit active input. - let updateMainStateTable = - fun (s: string) -> + let updateMainStateTable = fun (s: string) -> // Only update if changed - if s <> cellValue then - setter s - makeIdle() - cellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue) + if s <> cellValue then + setter s + makeIdle() + CellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue, columnType) | Search -> if oasetter.IsSome then let headerOA = state.ActiveTable.Headers.[columnIndex].TryOA() let oa = cell.ToOA() let onBlur = fun e -> makeIdle() let onEscape = fun e -> makeIdle() - Components.TermSearch.Input(oasetter.Value, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, autofocus=true, borderRadius=0) + let onEnter = fun e -> makeIdle() + Components.TermSearch.Input(oasetter.Value, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, onEnter=onEnter, autofocus=true, borderRadius=0, border="unset") else printfn "No setter for OntologyAnnotation given for table cell term search." | Idle -> diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 89f9e4b3..4a124b9f 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -275,8 +275,8 @@ type TermSearch = ?debounceSetter: int, ?advancedSearchDispatch: Messages.Msg -> unit, ?portalTermSelectArea: HTMLElement, - ?onBlur: Event -> unit, ?onEscape: KeyboardEvent -> unit, - ?autofocus: bool, ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool, ?borderRadius: int) + ?onBlur: Event -> unit, ?onEscape: KeyboardEvent -> unit, ?onEnter: KeyboardEvent -> unit, + ?autofocus: bool, ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool, ?borderRadius: int, ?border: string) = let autofocus = defaultArg autofocus false let displayParent = defaultArg displayParent true @@ -292,7 +292,7 @@ type TermSearch = let debounceStorage = React.useRef(newDebounceStorage()) let dsetter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp let ref = React.useElementRef() - if onBlur.IsSome then React.useLayoutEffectOnce(fun _ -> ClickOutsideHandler.AddListener (ref, onBlur.Value) ) + if onBlur.IsSome then React.useLayoutEffectOnce(fun _ -> ClickOutsideHandler.AddListener (ref, onBlur.Value)) React.useEffect((fun () -> setState input), dependencies=[|box input|]) React.useEffect((fun () -> setParent parent'), dependencies=[|box parent'|]) // careful, check console. might result in maximum dependency depth error. let stopSearch() = @@ -329,6 +329,7 @@ type TermSearch = prop.autoFocus autofocus prop.style [ if borderRadius.IsSome then style.borderRadius borderRadius.Value + if border.IsSome then style.custom("border", border.Value) ] if size.IsSome then size.Value if state.IsSome then prop.valueOrDefault state.Value.NameText @@ -352,9 +353,17 @@ type TermSearch = startSearch (Some s, true) mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) ) - prop.onKeyDown(key.escape, fun e -> - if onEscape.IsSome then onEscape.Value e - stopSearch() + prop.onKeyDown(fun e -> + match e.which with + | 27. -> //escape + if onEscape.IsSome then onEscape.Value e + stopSearch() + | 13. -> //enter + log "enter" + if onEnter.IsSome then onEnter.Value e + setter state + | _ -> () + ) ] let TermSelectArea = From ef0732b337d953c3e96d70b39c742e79b1bbe0f8 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 12:08:05 +0100 Subject: [PATCH 029/135] Add scroll into view logic for new building blocks #351 --- src/Client/MainComponents/Cells.fs | 3 ++- .../Pages/BuildingBlock/SearchComponent.fs | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index cc85729a..d4671d28 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -247,7 +247,8 @@ type Cell = let isReadOnly = columnType = Unit Html.th [ if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter - prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}" + prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}-{columnType}" + prop.id $"Header_{columnIndex}_{columnType}" cellStyle [] prop.children [ Html.div [ diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index f6dc9893..8e185f15 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -98,6 +98,24 @@ let private SearchBuildingBlockHeaderElement (ui: BuildingBlockUIState, setUi, m ] ] +let private scrollIntoViewRetry (id: string) = + let rec loop (iteration: int) = + let headerelement = Browser.Dom.document.getElementById(id) + if isNull headerelement then + if iteration < 5 then + Fable.Core.JS.setTimeout (fun _ -> loop (iteration+1)) 100 |> ignore + else + () + else + let config = createEmpty + config.behavior <- Browser.Types.ScrollBehavior.Smooth + config.block <- Browser.Types.ScrollAlignment.End + config.``inline`` <- Browser.Types.ScrollAlignment.End + log headerelement + headerelement.scrollIntoView(config) + loop 0 + + let private addBuildingBlockButton (model: Model) dispatch = let state = model.AddBuildingBlockState Bulma.field.div [ @@ -114,7 +132,10 @@ let private addBuildingBlockButton (model: Model) dispatch = Bulma.button.isFullWidth prop.onClick (fun _ -> let column = CompositeColumn.create(header, [|if body.IsSome then body.Value|]) + let index = Spreadsheet.Sidebar.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch + let id = $"Header_{index}_Main" + scrollIntoViewRetry id ) prop.text "Add Column" ] From 47dab426990748429519c9cb7e599dba99354a2f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 15:26:15 +0100 Subject: [PATCH 030/135] Add cell context menu option to switch between term and unit cell :sparkles: --- src/Client/MainComponents/ContextMenu.fs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Client/MainComponents/ContextMenu.fs b/src/Client/MainComponents/ContextMenu.fs index c89af432..dd866ed0 100644 --- a/src/Client/MainComponents/ContextMenu.fs +++ b/src/Client/MainComponents/ContextMenu.fs @@ -13,12 +13,16 @@ type private ContextFunctions = { Cut : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit Paste : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit FillColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + TransformCell : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit //EditColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit RowIndex : int ColumnIndex : int } -let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (selectedCell: CompositeCell option ) (rmv: _ -> unit) = +let private isUnitOrTermCell (cell: CompositeCell option) = + cell.IsSome && not cell.Value.isFreeText + +let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (contextCell: CompositeCell option) (selectedCell: CompositeCell option ) (rmv: _ -> unit) = /// This element will remove the contextmenu when clicking anywhere else let rmv_element = Html.div [ prop.onClick rmv @@ -54,6 +58,9 @@ let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (sel let buttonList = [ //button ("Edit Column", "fa-solid fa-table-columns", funcs.EditColumn rmv, []) button ("Fill Column", "fa-solid fa-file-signature", funcs.FillColumn rmv, []) + if isUnitOrTermCell contextCell then + let text = if contextCell.Value.isTerm then "As Unit Cell" else "As Term Cell" + button (text, "fa-solid fa-arrow-right-arrow-left", funcs.TransformCell rmv, []) divider button ("Copy", "fa-solid fa-copy", funcs.Copy rmv, []) button ("Cut", "fa-solid fa-scissors", funcs.Cut rmv, []) @@ -90,6 +97,7 @@ let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Typ Spreadsheet.DeleteRows indexArr |> Messages.SpreadsheetMsg |> dispatch else Spreadsheet.DeleteRow (snd index) |> Messages.SpreadsheetMsg |> dispatch + let cell = model.SpreadsheetModel.ActiveTable.TryGetCellAt(fst index, snd index) //let editColumnEvent _ = Modals.Controller.renderModal("EditColumn_Modal", Modals.EditColumn.Main (fst index) model dispatch) let funcs = { DeleteRow = fun rmv e -> rmv e; deleteRowEvent e @@ -98,10 +106,14 @@ let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Typ Cut = fun rmv e -> rmv e; Spreadsheet.CutCell index |> Messages.SpreadsheetMsg |> dispatch Paste = fun rmv e -> rmv e; Spreadsheet.PasteCell index |> Messages.SpreadsheetMsg |> dispatch FillColumn = fun rmv e -> rmv e; Spreadsheet.FillColumnWithTerm index |> Messages.SpreadsheetMsg |> dispatch + TransformCell = fun rmv e -> + if cell.IsSome && (cell.Value.isTerm || cell.Value.isUnitized) then + let nextCell = if cell.Value.isTerm then cell.Value.ToUnitizedCell() else cell.Value.ToTermCell() + rmv e; Spreadsheet.UpdateCell (index, nextCell) |> Messages.SpreadsheetMsg |> dispatch //EditColumn = fun rmv e -> rmv e; editColumnEvent e RowIndex = snd index ColumnIndex = fst index } - let child = contextmenu mousePosition funcs model.SpreadsheetModel.Clipboard.Cell + let child = contextmenu mousePosition funcs cell model.SpreadsheetModel.Clipboard.Cell let name = $"context_{mousePosition}" Modals.Controller.renderModal(name, child) \ No newline at end of file From 93542ffc51cc42b9221187c6ec42e6a4b009803b Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 15:48:20 +0100 Subject: [PATCH 031/135] more improvements for widgets --- src/Client/MainComponents/Widgets.fs | 40 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index 2fc25140..b449833a 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -28,7 +28,7 @@ module MoveEventListener = open Fable.Core.JsInterop - let onmousemove (element:IRefValue) (startPosition: Rect) setPosition = fun (e: Event) -> + let calculatePosition (element:IRefValue) (startPosition: Rect) = fun (e: Event) -> let e : MouseEvent = !!e let maxX = Browser.Dom.window.innerWidth - element.current.Value.offsetWidth; let tempX = int e.clientX - startPosition.X @@ -36,10 +36,18 @@ module MoveEventListener = let maxY = Browser.Dom.window.innerHeight - element.current.Value.offsetHeight; let tempY = int e.clientY - startPosition.Y let newY = System.Math.Min(System.Math.Max(tempY,0),int maxY) - setPosition (Some {X = newX; Y = newY}) + {X = newX; Y = newY} + + let onmousemove (element:IRefValue) (startPosition: Rect) setPosition = fun (e: Event) -> + let nextPosition = calculatePosition element startPosition e + setPosition (Some nextPosition) - let onmouseup onmousemove = + let onmouseup (element:IRefValue) onmousemove = Browser.Dom.document.removeEventListener("mousemove", onmousemove) + if element.current.IsSome then + let rect = element.current.Value.getBoundingClientRect() + let position = {X = int rect.left; Y = int rect.top} + Position.write(BuildingBlockWidgets,position) module ResizeEventListener = @@ -51,8 +59,10 @@ module ResizeEventListener = //let height = int e.clientY - startPosition.Y + startSize.Y setSize (Some {X = width; Y = startSize.Y}) - let onmouseup onmousemove = + let onmouseup (element: IRefValue) onmousemove = Browser.Dom.document.removeEventListener("mousemove", onmousemove) + if element.current.IsSome then + Size.write(BuildingBlockWidgets,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) module Elements = let resizeElement (content: ReactElement) = @@ -71,16 +81,13 @@ type Widgets = let resizeElement (content: ReactElement) = Bulma.modalCard [ prop.ref element - prop.onMouseDown(fun e -> + prop.onMouseDown(fun e -> // resize e.preventDefault() e.stopPropagation() let startPosition = {X = int e.clientX; Y = int e.clientY} let startSize = {X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight} let onmousemove = ResizeEventListener.onmousemove startPosition startSize setSize - let onmouseup = fun e -> - ResizeEventListener.onmouseup onmousemove - if size.IsSome then - Size.write(BuildingBlockWidgets,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) + let onmouseup = fun e -> ResizeEventListener.onmouseup element onmousemove Browser.Dom.document.addEventListener("mousemove", onmousemove) let config = createEmpty config.once <- true @@ -93,27 +100,26 @@ type Widgets = if size.IsSome then style.width size.Value.X if position.IsNone then - style.transform.translate (length.perc -50,length.perc -50) - style.top (length.perc 50); style.left (length.perc 50); - if position.IsSome then + //style.transform.translate (length.perc -50,length.perc -50) + style.top (length.perc 20); style.left (length.perc 20); + else style.top position.Value.Y; style.left position.Value.X; ] prop.children content ] resizeElement <| Html.div [ + prop.onMouseDown(fun e -> e.stopPropagation()) + prop.style [style.cursor.defaultCursor] prop.children [ Bulma.modalCardHead [ - prop.onMouseDown(fun e -> + prop.onMouseDown(fun e -> // move e.preventDefault() e.stopPropagation() let x = e.clientX - element.current.Value.offsetLeft let y = e.clientY - element.current.Value.offsetTop; let startPosition = {X = int x; Y = int y} let onmousemove = MoveEventListener.onmousemove element startPosition setPosition - let onmouseup = fun e -> - MoveEventListener.onmouseup onmousemove - if position.IsSome then - Position.write(BuildingBlockWidgets,position.Value) + let onmouseup = fun e -> MoveEventListener.onmouseup element onmousemove Browser.Dom.document.addEventListener("mousemove", onmousemove) let config = createEmpty config.once <- true From 2e6690981bdbbfd6001e1dde3ec9ed11310e499b Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 15:49:10 +0100 Subject: [PATCH 032/135] Release vv1.0.0-alpha-08 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 30576c20..0e58e27e 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha.07" - let [] AssemblyMetadata_ReleaseDate = "01.02.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha-08" + let [] AssemblyMetadata_ReleaseDate = "02.02.2024" From 690edf4dc8d09221aa6b8ec2ac5fd5ce95df8505 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 2 Feb 2024 15:50:06 +0100 Subject: [PATCH 033/135] fix typo --- build/Build.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Build.fs b/build/Build.fs index f8fc2a00..d59c3b94 100644 --- a/build/Build.fs +++ b/build/Build.fs @@ -48,7 +48,7 @@ module ReleaseNoteTasks = ] if commit then run git ["add"; "."] "" - run git ["commit"; "-m"; (sprintf "Release v%s :bookmark:" ProjectInfo.prereleaseTag)] "" + run git ["commit"; "-m"; (sprintf "Release %s :bookmark:" ProjectInfo.prereleaseTag)] "" let updateReleaseNotes = Target.create "releasenotes" (fun config -> ReleaseNotes.ensure() From d88530211d33ee91e877aeb27fb0d6d9ab244c1f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 8 Feb 2024 14:49:26 +0100 Subject: [PATCH 034/135] Update, restyle templates and add template widget #356 --- src/Client/LocalStorage/ModalPositions.fs | 1 + src/Client/MainComponents/Navbar.fs | 22 +- src/Client/MainComponents/Widgets.fs | 106 +- src/Client/Messages.fs | 4 +- src/Client/Model.fs | 27 +- .../ProtocolTemplates/ProtocolSearchView.fs | 22 +- .../ProtocolSearchViewComponent.fs | 1035 +++++++++-------- .../Pages/ProtocolTemplates/ProtocolState.fs | 57 +- .../Pages/ProtocolTemplates/ProtocolView.fs | 65 +- .../SharedComponents/TermSearchInput.fs | 2 +- src/Client/Views/SidebarView.fs | 2 +- src/Client/style.scss | 37 +- 12 files changed, 795 insertions(+), 585 deletions(-) diff --git a/src/Client/LocalStorage/ModalPositions.fs b/src/Client/LocalStorage/ModalPositions.fs index 2d4fa505..eafcf74f 100644 --- a/src/Client/LocalStorage/ModalPositions.fs +++ b/src/Client/LocalStorage/ModalPositions.fs @@ -18,6 +18,7 @@ type Rect = { open Fable.SimpleJson let [] BuildingBlockWidgets = "BuildingBlock" +let [] TemplatesWidgets = "Templates" [] module Position = diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index 4f46c6eb..13c94edb 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -58,16 +58,16 @@ let private quickAccessButtonListStart (state: LocalHistory.Model) (setModal: Mo ], (fun _ -> setModal (Some Modal.BuildingBlock)) ).toReactElement() - //QuickAccessButton.create( - // "Add Template", - // [ - // Bulma.icon [ - // Html.i [prop.className "fa-solid fa-circle-plus" ] - // Html.i [prop.className "fa-solid fa-table" ] - // ] - // ], - // (fun _ -> setModal (Some Modal.Template)) - //).toReactElement() + QuickAccessButton.create( + "Add Template", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table" ] + ] + ], + (fun _ -> setModal (Some Modal.Template)) + ).toReactElement() ] ] @@ -102,7 +102,7 @@ let private modalDisplay (modal: Modal option, model, dispatch, setModal) = | Some Modal.BuildingBlock -> MainComponents.Widgets.BuildingBlock (model, dispatch, rmv) | Some Modal.Template -> - MainComponents.Widgets.BuildingBlock (model, dispatch, rmv) + MainComponents.Widgets.Templates (model, dispatch, rmv) [] let Main (model: Messages.Model) dispatch = diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index b449833a..5a4da9c9 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -9,16 +9,18 @@ open LocalStorage.Widgets module InitExtensions = type Rect with - static member initBuildingBlockPosition() = - match Position.load BuildingBlockWidgets with + + static member initSizeFromPrefix(prefix: string) = + match Size.load prefix with | Some p -> Some p - | None -> None - - static member initBuildingBlockSize() = - match Size.load BuildingBlockWidgets with + | None -> None + + static member initPositionFromPrefix(prefix: string) = + match Position.load prefix with | Some p -> Some p | None -> None + open InitExtensions open Fable.Core @@ -42,12 +44,12 @@ module MoveEventListener = let nextPosition = calculatePosition element startPosition e setPosition (Some nextPosition) - let onmouseup (element:IRefValue) onmousemove = + let onmouseup (prefix,element:IRefValue) onmousemove = Browser.Dom.document.removeEventListener("mousemove", onmousemove) if element.current.IsSome then let rect = element.current.Value.getBoundingClientRect() let position = {X = int rect.left; Y = int rect.top} - Position.write(BuildingBlockWidgets,position) + Position.write(prefix,position) module ResizeEventListener = @@ -59,10 +61,10 @@ module ResizeEventListener = //let height = int e.clientY - startPosition.Y + startSize.Y setSize (Some {X = width; Y = startSize.Y}) - let onmouseup (element: IRefValue) onmousemove = + let onmouseup (prefix, element: IRefValue) onmousemove = Browser.Dom.document.removeEventListener("mousemove", onmousemove) if element.current.IsSome then - Size.write(BuildingBlockWidgets,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) + Size.write(prefix,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) module Elements = let resizeElement (content: ReactElement) = @@ -71,12 +73,25 @@ module Elements = prop.children content ] + let helpExtendButton (extendToggle: unit -> unit) = + Bulma.help [ + prop.className "is-flex" + prop.children [ + Html.a [ + prop.text "Help"; + prop.style [style.marginLeft length.auto; style.userSelect.none] + prop.onClick (fun e -> e.preventDefault(); e.stopPropagation(); extendToggle()) + ] + ] + ] + type Widgets = [] - static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = - let position, setPosition = React.useState(Rect.initBuildingBlockPosition) - let size, setSize = React.useState(Rect.initBuildingBlockSize) + static member Base(content: ReactElement, prefix: string, rmv: MouseEvent -> unit, ?help: ReactElement) = + let position, setPosition = React.useState(fun _ -> Rect.initPositionFromPrefix prefix) + let size, setSize = React.useState(fun _ -> Rect.initSizeFromPrefix prefix) + let helpIsActive, setHelpIsActive = React.useState(false) let element = React.useElementRef() let resizeElement (content: ReactElement) = Bulma.modalCard [ @@ -87,7 +102,7 @@ type Widgets = let startPosition = {X = int e.clientX; Y = int e.clientY} let startSize = {X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight} let onmousemove = ResizeEventListener.onmousemove startPosition startSize setSize - let onmouseup = fun e -> ResizeEventListener.onmouseup element onmousemove + let onmouseup = fun e -> ResizeEventListener.onmouseup (prefix, element) onmousemove Browser.Dom.document.addEventListener("mousemove", onmousemove) let config = createEmpty config.once <- true @@ -119,7 +134,7 @@ type Widgets = let y = e.clientY - element.current.Value.offsetTop; let startPosition = {X = int x; Y = int y} let onmousemove = MoveEventListener.onmousemove element startPosition setPosition - let onmouseup = fun e -> MoveEventListener.onmouseup element onmousemove + let onmouseup = fun e -> MoveEventListener.onmouseup (prefix, element) onmousemove Browser.Dom.document.addEventListener("mousemove", onmousemove) let config = createEmpty config.once <- true @@ -134,10 +149,67 @@ type Widgets = Bulma.modalCardBody [ prop.style [style.overflow.inheritFromParent] prop.children [ - BuildingBlock.SearchComponent.Main model dispatch + content + if help.IsSome then Elements.helpExtendButton (fun _ -> setHelpIsActive (not helpIsActive)) ] ] + Bulma.modalCardFoot [ + prop.style [style.padding 5] + if help.IsSome then + prop.children [ + Bulma.content [ + prop.className "widget-help-container" + prop.style [style.overflow.hidden; if not helpIsActive then style.display.none; ] + prop.children [ + help.Value + ] + ] + ] + ] ] ] + + static member BuildingBlock (model, dispatch, rmv: MouseEvent -> unit) = + let content = BuildingBlock.SearchComponent.Main model dispatch + let help = Html.div [ + Html.p "Add a new Building Block." + Html.ul [ + Html.li "If a cell is selected, a new Building Block is added to the right of the selected cell." + Html.li "If no cell is selected, a new Building Block is appended at the right end of the table." + ] + ] + let prefix = BuildingBlockWidgets + Widgets.Base(content, prefix, rmv, help) + - + [] + static member Templates (model: Messages.Model, dispatch, rmv: MouseEvent -> unit) = + let templates, setTemplates = React.useState(model.ProtocolState.Templates) + React.useEffectOnce(fun _ -> Messages.Protocol.GetAllProtocolsRequest |> Messages.ProtocolMsg |> dispatch) + React.useEffect((fun _ -> setTemplates model.ProtocolState.Templates), [|box model.ProtocolState.Templates|]) + let selectContent() = + [ + Protocol.Search.FileSortElement(templates, setTemplates, model, dispatch) + Protocol.Search.Component (templates, model, dispatch, length.px 300) + ] + let insertContent() = + [ + Bulma.field.div [ + Protocol.Core.TemplateFromDB.addFromDBToTableButton model dispatch + ] + Bulma.field.div [ + prop.style [style.maxHeight (length.px 300); style.overflow.auto] + prop.children [ + Protocol.Core.TemplateFromDB.displaySelectedProtocolEle model dispatch + ] + ] + ] + let content = + let switchContent = if model.ProtocolState.TemplateSelected.IsNone then selectContent() else insertContent() + Html.div [ + prop.children switchContent + ] + + let help = Protocol.Search.InfoField() + let prefix = TemplatesWidgets + Widgets.Base(content, prefix, rmv, help) diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index 50390b2e..e0b14544 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -108,15 +108,17 @@ module Protocol = | ParseUploadedFileRequest of raw: byte [] | ParseUploadedFileResponse of (string * InsertBuildingBlock []) [] // Client + | UpdateTemplates of ARCtrl.Template.Template [] + | UpdateLoading of bool | RemoveUploadedFileParsed // // ------ Protocol from Database ------ + | GetAllProtocolsForceRequest | GetAllProtocolsRequest | GetAllProtocolsResponse of string [] | SelectProtocol of ARCtrl.Template.Template | ProtocolIncreaseTimesUsed of protocolName:string // Client | RemoveSelectedProtocol - | UpdateLoading of bool type BuildingBlockDetailsMsg = | GetSelectedBuildingBlockTermsRequest of TermSearchable [] diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 0b691223..cec2d7ea 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -344,29 +344,44 @@ module Validation = module Protocol = [] - type CuratedCommunityFilter = - | Both + type CommunityFilter = + | All | OnlyCurated | OnlyCommunity + member this.ToStringRdb() = + match this with + | All -> "All" + | OnlyCurated -> "Curated" + | OnlyCommunity -> "Community" + + static member fromString(str:string) = + match str.ToLower() with + | "all" -> All + | "curated" -> OnlyCurated + | "community" -> OnlyCommunity + | anyElse -> printfn "Unable to parse %s to CommunityFilter. Default to 'All'." anyElse; All + /// This model is used for both protocol insert and protocol search type Model = { // Client Loading : bool + LastUpdated : System.DateTime option // // ------ Process from file ------ UploadedFileParsed : (string*InsertBuildingBlock []) [] // ------ Protocol from Database ------ - ProtocolSelected : ARCtrl.Template.Template option - ProtocolsAll : ARCtrl.Template.Template [] + TemplateSelected : ARCtrl.Template.Template option + Templates : ARCtrl.Template.Template [] } with static member init () = { // Client Loading = false - ProtocolSelected = None + LastUpdated = None + TemplateSelected = None // // ------ Process from file ------ UploadedFileParsed = [||] // ------ Protocol from Database ------ - ProtocolsAll = [||] + Templates = [||] } type RequestBuildingBlockInfoStates = diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs index b958e884..39c1bc04 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs @@ -7,7 +7,7 @@ open Messages open Feliz open Feliz.Bulma -let breadcrumbEle (model:Model) dispatch = +let private breadcrumbEle (model:Model) dispatch = Bulma.breadcrumb [ Bulma.breadcrumb.hasArrowSeparator prop.children [ @@ -30,11 +30,11 @@ let breadcrumbEle (model:Model) dispatch = open Fable.Core [] -let ProtocolSearchView (model:Model) dispatch = - React.useEffectOnce(fun () -> - Messages.Protocol.GetAllProtocolsRequest |> ProtocolMsg |> dispatch - ) - let isEmpty = model.ProtocolState.ProtocolsAll |> isNull || model.ProtocolState.ProtocolsAll |> Array.isEmpty +let Main (model:Model) dispatch = + let templates, setTemplates = React.useState(model.ProtocolState.Templates) + React.useEffectOnce(fun _ -> Messages.Protocol.GetAllProtocolsRequest |> Messages.ProtocolMsg |> dispatch) + React.useEffect((fun _ -> setTemplates model.ProtocolState.Templates), [|box model.ProtocolState.Templates|]) + let isEmpty = model.ProtocolState.Templates |> isNull || model.ProtocolState.Templates |> Array.isEmpty let isLoading = model.ProtocolState.Loading div [ OnSubmit (fun e -> e.preventDefault()) @@ -46,11 +46,11 @@ let ProtocolSearchView (model:Model) dispatch = if isEmpty && not isLoading then Bulma.help [Bulma.color.isDanger; prop.text "No templates were found. This can happen if connection to the server was lost. You can try reload this site or contact a developer."] - if isLoading then - Modals.Loading.loadingModal - Bulma.label "Search the database for protocol templates." - if not isEmpty then - Protocol.Component.ProtocolContainer model dispatch + mainFunctionContainer [ + Protocol.Search.InfoField() + Protocol.Search.FileSortElement(templates, setTemplates, model, dispatch) + Protocol.Search.Component (templates, model, dispatch) + ] ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs index 9cb14133..9da3ded6 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs @@ -1,4 +1,4 @@ -module Protocol.Component +namespace Protocol open Shared open TemplateTypes @@ -9,545 +9,622 @@ open Messages open Feliz open Feliz.Bulma -let private curatedOrganisationNames = [ - "dataplant" - "nfdi4plants" -] - -/// Fields of Template that can be searched -[] -type private SearchFields = -| Name -| Organisation -| Authors - - static member private ofFieldString (str:string) = - let str = str.ToLower() - match str with - | "/o" | "/org" -> Some Organisation - | "/a" | "/authors" -> Some Authors - | "/n" | "/reset" | "/e" -> Some Name - | _ -> None - - member this.toStr = - match this with - | Name -> "/name" - | Organisation -> "/org" - | Authors -> "/auth" - - member this.toNameRdb = - match this with - | Name -> "template name" - | Organisation -> "organisation" - | Authors -> "authors" - - static member GetOfQuery(query:string) = - SearchFields.ofFieldString query - -open ARCtrl.ISA - -type private ProtocolViewState = { - DisplayedProtDetailsId : int option - ProtocolSearchQuery : string - ProtocolTagSearchQuery : string - ProtocolFilterTags : OntologyAnnotation list - ProtocolFilterErTags : OntologyAnnotation list - CuratedCommunityFilter : Model.Protocol.CuratedCommunityFilter - TagFilterIsAnd : bool - Searchfield : SearchFields -} with - static member init () = { - ProtocolSearchQuery = "" - ProtocolTagSearchQuery = "" - ProtocolFilterTags = [] - ProtocolFilterErTags = [] - CuratedCommunityFilter = Model.Protocol.CuratedCommunityFilter.Both - TagFilterIsAnd = true - DisplayedProtDetailsId = None - Searchfield = SearchFields.Name - } - -[] -let private SearchFieldId = "template_searchfield_main" - -let private queryField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.column [ - Bulma.label $"Search by {state.Searchfield.toNameRdb}" - let hasSearchAddon = state.Searchfield <> SearchFields.Name - Bulma.field.div [ - if hasSearchAddon then Bulma.field.hasAddons - prop.children [ - if hasSearchAddon then +module ComponentAux = + + let curatedOrganisationNames = [ + "dataplant" + "nfdi4plants" + ] + + /// Fields of Template that can be searched + [] + type SearchFields = + | Name + | Organisation + | Authors + + static member private ofFieldString (str:string) = + let str = str.ToLower() + match str with + | "/o" | "/org" -> Some Organisation + | "/a" | "/authors" -> Some Authors + | "/n" | "/reset" | "/e" -> Some Name + | _ -> None + + member this.toStr = + match this with + | Name -> "/name" + | Organisation -> "/org" + | Authors -> "/auth" + + member this.toNameRdb = + match this with + | Name -> "template name" + | Organisation -> "organisation" + | Authors -> "authors" + + static member GetOfQuery(query:string) = + SearchFields.ofFieldString query + + open ARCtrl.ISA + + type ProtocolViewState = { + ProtocolSearchQuery : string + ProtocolTagSearchQuery : string + ProtocolFilterTags : OntologyAnnotation list + ProtocolFilterErTags : OntologyAnnotation list + CommunityFilter : Model.Protocol.CommunityFilter + TagFilterIsAnd : bool + Searchfield : SearchFields + } with + static member init () = { + ProtocolSearchQuery = "" + ProtocolTagSearchQuery = "" + ProtocolFilterTags = [] + ProtocolFilterErTags = [] + CommunityFilter = Model.Protocol.CommunityFilter.All + TagFilterIsAnd = true + Searchfield = SearchFields.Name + } + + [] + let SearchFieldId = "template_searchfield_main" + + let queryField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = + Html.div [ + Bulma.label $"Search by {state.Searchfield.toNameRdb}" + let hasSearchAddon = state.Searchfield <> SearchFields.Name + Bulma.field.div [ + if hasSearchAddon then Bulma.field.hasAddons + prop.children [ + if hasSearchAddon then + Bulma.control.div [ + Bulma.button.a [ Bulma.button.isStatic; prop.text state.Searchfield.toStr] + ] Bulma.control.div [ - Bulma.button.a [ Bulma.button.isStatic; prop.text state.Searchfield.toStr] - ] - Bulma.control.div [ - Bulma.control.hasIconsRight - prop.children [ - Bulma.input.text [ - prop.placeholder $".. {state.Searchfield.toNameRdb}" - prop.id SearchFieldId - Bulma.color.isPrimary - prop.valueOrDefault state.ProtocolSearchQuery - prop.onChange (fun (e: string) -> - let query = e - // if query starts with "/" expect intend to search by different field - if query.StartsWith "/" then - let searchField = SearchFields.GetOfQuery query - if searchField.IsSome then - {state with Searchfield = searchField.Value; ProtocolSearchQuery = ""} |> setState - //let inp = Browser.Dom.document.getElementById SearchFieldId - // if query starts NOT with "/" update query - else - { - state with - ProtocolSearchQuery = query - DisplayedProtDetailsId = None - } - |> setState - ) + Bulma.control.hasIconsRight + prop.children [ + Bulma.input.text [ + prop.style [style.minWidth 200] + prop.placeholder $".. {state.Searchfield.toNameRdb}" + prop.id SearchFieldId + Bulma.color.isPrimary + prop.valueOrDefault state.ProtocolSearchQuery + prop.onChange (fun (e: string) -> + let query = e + // if query starts with "/" expect intend to search by different field + if query.StartsWith "/" then + let searchField = SearchFields.GetOfQuery query + if searchField.IsSome then + {state with Searchfield = searchField.Value; ProtocolSearchQuery = ""} |> setState + //let inp = Browser.Dom.document.getElementById SearchFieldId + // if query starts NOT with "/" update query + else + { + state with + ProtocolSearchQuery = query + } + |> setState + ) + ] + Bulma.icon [Bulma.icon.isSmall; Bulma.icon.isRight; prop.children (Html.i [prop.className "fa-solid fa-search"])] ] - Bulma.icon [Bulma.icon.isSmall; Bulma.icon.isRight; prop.children (Html.i [prop.className "fa-solid fa-search"])] ] ] ] ] - ] -let private tagQueryField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - let allTags = model.ProtocolState.ProtocolsAll |> Array.collect (fun x -> x.Tags) |> Array.distinct |> Array.filter (fun x -> state.ProtocolFilterTags |> List.contains x |> not ) - let allErTags = model.ProtocolState.ProtocolsAll |> Array.collect (fun x -> x.EndpointRepositories) |> Array.distinct |> Array.filter (fun x -> state.ProtocolFilterErTags |> List.contains x |> not ) - let hitTagList, hitErTagList = - if state.ProtocolTagSearchQuery <> "" - then - let queryBigram = state.ProtocolTagSearchQuery |> Shared.SorensenDice.createBigrams - let getMatchingTags (allTags: OntologyAnnotation []) = - allTags - |> Array.map (fun x -> - x.NameText - |> Shared.SorensenDice.createBigrams - |> Shared.SorensenDice.calculateDistance queryBigram - , x - ) - |> Array.filter (fun x -> fst x >= 0.3 || (snd x).TermAccessionShort = state.ProtocolTagSearchQuery) - |> Array.sortByDescending fst - |> Array.map snd - let sortedTags = getMatchingTags allTags - let sortedErTags = getMatchingTags allErTags - sortedTags, sortedErTags - else - [||], [||] - Bulma.column [ - Bulma.label "Search for tags" - Bulma.control.div [ - Bulma.control.hasIconsRight - prop.children [ - Bulma.input.text [ - prop.placeholder ".. protocol tag" - Bulma.color.isPrimary - prop.valueOrDefault state.ProtocolTagSearchQuery - prop.onChange (fun (e:string) -> - {state with ProtocolTagSearchQuery = e} |> setState + let tagQueryField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = + let allTags = model.ProtocolState.Templates |> Array.collect (fun x -> x.Tags) |> Array.distinct |> Array.filter (fun x -> state.ProtocolFilterTags |> List.contains x |> not ) + let allErTags = model.ProtocolState.Templates |> Array.collect (fun x -> x.EndpointRepositories) |> Array.distinct |> Array.filter (fun x -> state.ProtocolFilterErTags |> List.contains x |> not ) + let hitTagList, hitErTagList = + if state.ProtocolTagSearchQuery <> "" + then + let queryBigram = state.ProtocolTagSearchQuery |> Shared.SorensenDice.createBigrams + let getMatchingTags (allTags: OntologyAnnotation []) = + allTags + |> Array.map (fun x -> + x.NameText + |> Shared.SorensenDice.createBigrams + |> Shared.SorensenDice.calculateDistance queryBigram + , x ) - ] - Bulma.icon [ - Bulma.icon.isSmall; Bulma.icon.isRight - Html.i [prop.className "fa-solid fa-search"] |> prop.children - ] - // Pseudo dropdown - Bulma.box [ - prop.style [ - style.position.absolute - style.width(length.perc 100) - style.zIndex 10 - if hitTagList |> Array.isEmpty && hitErTagList |> Array.isEmpty then style.display.none + |> Array.filter (fun x -> fst x >= 0.3 || (snd x).TermAccessionShort = state.ProtocolTagSearchQuery) + |> Array.sortByDescending fst + |> Array.map snd + let sortedTags = getMatchingTags allTags + let sortedErTags = getMatchingTags allErTags + sortedTags, sortedErTags + else + [||], [||] + Html.div [ + Bulma.label "Search for tags" + Bulma.control.div [ + Bulma.control.hasIconsRight + prop.children [ + Bulma.input.text [ + prop.style [style.minWidth 150] + prop.placeholder ".. protocol tag" + Bulma.color.isPrimary + prop.valueOrDefault state.ProtocolTagSearchQuery + prop.onChange (fun (e:string) -> + {state with ProtocolTagSearchQuery = e} |> setState + ) ] - prop.children [ - if hitErTagList <> [||] then - Bulma.label "Endpoint Repositories" - Bulma.tags [ - for tagSuggestion in hitErTagList do - yield - Bulma.tag [ - prop.className "clickableTag" - Bulma.color.isLink - prop.onClick (fun _ -> - let nextState = { - state with - ProtocolFilterErTags = tagSuggestion::state.ProtocolFilterErTags - ProtocolTagSearchQuery = "" - DisplayedProtDetailsId = None - } - setState nextState - ) - prop.title tagSuggestion.TermAccessionShort - prop.text tagSuggestion.NameText - ] - ] - if hitTagList <> [||] then - Bulma.label "Tags" - Bulma.tags [ - for tagSuggestion in hitTagList do - yield - Bulma.tag [ - prop.className "clickableTag" - Bulma.color.isInfo - prop.onClick (fun _ -> - let nextState = { + Bulma.icon [ + Bulma.icon.isSmall; Bulma.icon.isRight + Html.i [prop.className "fa-solid fa-search"] |> prop.children + ] + // Pseudo dropdown + Bulma.box [ + prop.style [ + style.position.absolute + style.width(length.perc 100) + style.zIndex 10 + if hitTagList |> Array.isEmpty && hitErTagList |> Array.isEmpty then style.display.none + ] + prop.children [ + if hitErTagList <> [||] then + Bulma.label "Endpoint Repositories" + Bulma.tags [ + for tagSuggestion in hitErTagList do + yield + Bulma.tag [ + prop.className "clickableTag" + Bulma.color.isLink + prop.onClick (fun _ -> + let nextState = { state with - ProtocolFilterTags = tagSuggestion::state.ProtocolFilterTags + ProtocolFilterErTags = tagSuggestion::state.ProtocolFilterErTags ProtocolTagSearchQuery = "" - DisplayedProtDetailsId = None } - setState nextState - //AddProtocolTag tagSuggestion |> ProtocolMsg |> dispatch - ) - prop.title tagSuggestion.TermAccessionShort - prop.text tagSuggestion.NameText - ] - ] + setState nextState + ) + prop.title tagSuggestion.TermAccessionShort + prop.text tagSuggestion.NameText + ] + ] + if hitTagList <> [||] then + Bulma.label "Tags" + Bulma.tags [ + for tagSuggestion in hitTagList do + yield + Bulma.tag [ + prop.className "clickableTag" + Bulma.color.isInfo + prop.onClick (fun _ -> + let nextState = { + state with + ProtocolFilterTags = tagSuggestion::state.ProtocolFilterTags + ProtocolTagSearchQuery = "" + } + setState nextState + //AddProtocolTag tagSuggestion |> ProtocolMsg |> dispatch + ) + prop.title tagSuggestion.TermAccessionShort + prop.text tagSuggestion.NameText + ] + ] + ] ] ] ] ] - ] -let private tagDisplayField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.columns [ - Bulma.columns.isMobile - prop.children [ - Bulma.column [ - Bulma.field.div [ - Bulma.field.isGroupedMultiline - prop.children [ - for selectedTag in state.ProtocolFilterErTags do - yield Bulma.control.div [ - Bulma.tags [ - Bulma.tags.hasAddons - prop.children [ - Bulma.tag [Bulma.color.isLink; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] - Bulma.delete [ - prop.className "clickableTagDelete" - prop.onClick (fun _ -> - {state with ProtocolFilterErTags = state.ProtocolFilterErTags |> List.except [selectedTag]} |> setState - //RemoveProtocolErTag selectedTag |> ProtocolMsg |> dispatch - ) + open Fable.Core.JsInterop + + let communitySelectField (model) (state: ProtocolViewState) setState = + let options = [ + Model.Protocol.CommunityFilter.All + Model.Protocol.CommunityFilter.OnlyCurated + Model.Protocol.CommunityFilter.OnlyCommunity + ] + Html.div [ + Bulma.label "Search for tags" + Bulma.control.div [ + Bulma.control.isExpanded + prop.children [ + Bulma.select [ + prop.onChange(fun (e: Browser.Types.Event) -> + let filter = Model.Protocol.CommunityFilter.fromString e.target?value + {state with CommunityFilter = filter} |> setState + ) + prop.children [ + for option in options do + Html.option [ + prop.selected (state.CommunityFilter = option) + prop.value (option.ToStringRdb()) + prop.text (option.ToStringRdb()) + ] + ] + ] + ] + ] + ] + + let tagDisplayField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = + Bulma.columns [ + Bulma.columns.isMobile + prop.children [ + Bulma.column [ + Bulma.field.div [ + Bulma.field.isGroupedMultiline + prop.children [ + for selectedTag in state.ProtocolFilterErTags do + yield Bulma.control.div [ + Bulma.tags [ + Bulma.tags.hasAddons + prop.children [ + Bulma.tag [Bulma.color.isLink; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] + Bulma.delete [ + prop.className "clickableTagDelete" + prop.onClick (fun _ -> + {state with ProtocolFilterErTags = state.ProtocolFilterErTags |> List.except [selectedTag]} |> setState + //RemoveProtocolErTag selectedTag |> ProtocolMsg |> dispatch + ) + ] ] ] ] - ] - for selectedTag in state.ProtocolFilterTags do - yield Bulma.control.div [ - Bulma.tags [ - Bulma.tags.hasAddons - prop.children [ - Bulma.tag [Bulma.color.isInfo; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] - Bulma.delete [ - prop.className "clickableTagDelete" - //Tag.Color IsWarning; - prop.onClick (fun _ -> - {state with ProtocolFilterTags = state.ProtocolFilterTags |> List.except [selectedTag]} |> setState - //RemoveProtocolTag selectedTag |> ProtocolMsg |> dispatch - ) + for selectedTag in state.ProtocolFilterTags do + yield Bulma.control.div [ + Bulma.tags [ + Bulma.tags.hasAddons + prop.children [ + Bulma.tag [Bulma.color.isInfo; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] + Bulma.delete [ + prop.className "clickableTagDelete" + //Tag.Color IsWarning; + prop.onClick (fun _ -> + {state with ProtocolFilterTags = state.ProtocolFilterTags |> List.except [selectedTag]} |> setState + //RemoveProtocolTag selectedTag |> ProtocolMsg |> dispatch + ) + ] ] ] ] - ] + ] ] ] - ] - // tag filter (AND or OR) - Bulma.column [ - Bulma.column.isNarrow - prop.title (if state.TagFilterIsAnd then "Templates contain all tags." else "Templates contain at least one tag.") - Switch.checkbox [ - Bulma.color.isDark - prop.style [style.userSelect.none] - switch.isOutlined - switch.isSmall - prop.id "switch-2" - prop.isChecked state.TagFilterIsAnd - prop.onChange (fun (e:bool) -> - {state with TagFilterIsAnd = not state.TagFilterIsAnd} |> setState - //UpdateTagFilterIsAnd (not state.TagFilterIsAnd) |> ProtocolMsg |> dispatch - ) - prop.children (if state.TagFilterIsAnd then Html.b "And" else Html.b "Or") - ] |> prop.children + // tag filter (AND or OR) + Bulma.column [ + Bulma.column.isNarrow + prop.title (if state.TagFilterIsAnd then "Templates contain all tags." else "Templates contain at least one tag.") + Switch.checkbox [ + Bulma.color.isDark + prop.style [style.userSelect.none] + switch.isOutlined + switch.isSmall + prop.id "switch-2" + prop.isChecked state.TagFilterIsAnd + prop.onChange (fun (e:bool) -> + {state with TagFilterIsAnd = not state.TagFilterIsAnd} |> setState + //UpdateTagFilterIsAnd (not state.TagFilterIsAnd) |> ProtocolMsg |> dispatch + ) + prop.children (if state.TagFilterIsAnd then Html.b "And" else Html.b "Or") + ] |> prop.children + ] ] ] - ] -let private fileSortElements (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = + let fileSortElements (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - Html.div [ - prop.style [style.marginBottom(length.rem 0.75)] - prop.children [ - Bulma.columns [ - Bulma.columns.isMobile; prop.style [style.marginBottom 0] - prop.children [ - queryField model state setState - tagQueryField model state setState + Html.div [ + prop.style [style.marginBottom(length.rem 0.75); style.display.flex] + prop.children [ + Html.div [ + prop.className "template-filter-container" + prop.children [ + queryField model state setState + tagQueryField model state setState + communitySelectField model state setState + ] ] + // Only show the tag list and tag filter (AND or OR) if any tag exists + if state.ProtocolFilterErTags <> [] || state.ProtocolFilterTags <> [] then + tagDisplayField model state setState ] - // Only show the tag list and tag filter (AND or OR) if any tag exists - if state.ProtocolFilterErTags <> [] || state.ProtocolFilterTags <> [] then - tagDisplayField model state setState ] - ] -let private curatedTag = Bulma.tag [prop.text "curated"; Bulma.color.isSuccess] -let private communitytag = Bulma.tag [prop.text "community"; Bulma.color.isWarning] -let private curatedCommunityTag = - Bulma.tag [ - prop.style [style.custom("background", "linear-gradient(90deg, rgba(31,194,167,1) 50%, rgba(255,192,0,1) 50%)")] - Bulma.color.isSuccess - prop.children [ - Html.span [prop.style [style.marginRight (length.em 0.75)]; prop.text "cur"] - Html.span [prop.style [style.marginLeft (length.em 0.75); style.color "rgba(0, 0, 0, 0.7)"]; prop.text "com"] - ] - ] - -let createAuthorStringHelper (author: Person) = - let mi = if author.MidInitials.IsSome then author.MidInitials.Value else "" - $"{author.FirstName} {mi} {author.LastName}" -let createAuthorsStringHelper (authors: Person []) = authors |> Array.map createAuthorStringHelper |> String.concat ", " - -let private protocolElement i (template:ARCtrl.Template.Template) (model:Model) (state:ProtocolViewState) dispatch (setState: ProtocolViewState -> unit) = - let isActive = - match state.DisplayedProtDetailsId with - | Some id when id = i -> - true - | _ -> - false - [ - Html.tr [ - prop.key $"{i}_{template.Id}" - prop.classes [ "nonSelectText"; if isActive then "hoverTableEle"] - prop.style [ - style.cursor.pointer; style.userSelect.none; - ] - prop.onClick (fun e -> - e.preventDefault() - { state with - DisplayedProtDetailsId = if isActive then None else Some i } - |> setState - //if isActive then - // UpdateDisplayedProtDetailsId None |> ProtocolMsg |> dispatch - //else - // UpdateDisplayedProtDetailsId (Some i) |> ProtocolMsg |> dispatch - ) + let curatedTag = Bulma.tag [prop.text "curated"; Bulma.color.isSuccess] + let communitytag = Bulma.tag [prop.text "community"; Bulma.color.isWarning] + let curatedCommunityTag = + Bulma.tag [ + prop.style [style.custom("background", "linear-gradient(90deg, rgba(31,194,167,1) 50%, rgba(255,192,0,1) 50%)")] + Bulma.color.isSuccess prop.children [ - Html.td template.Name - Html.td ( - if curatedOrganisationNames |> List.contains (template.Organisation.ToString().ToLower()) then - curatedTag - else - communitytag - ) - //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ a [ OnClick (fun e -> e.stopPropagation()); Href prot.DocsLink; Target "_Blank"; Title "docs" ] [Fa.i [Fa.Size Fa.Fa2x ; Fa.Regular.FileAlt] []] ] - Html.td [ prop.style [style.textAlign.center; style.verticalAlign.middle]; prop.text template.Version ] - //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ str (string template.Used) ] - Html.td ( - Bulma.icon [Html.i [prop.className "fa-solid fa-chevron-down"]] - ) + Html.span [prop.style [style.marginRight (length.em 0.75)]; prop.text "cur"] + Html.span [prop.style [style.marginLeft (length.em 0.75); style.color "rgba(0, 0, 0, 0.7)"]; prop.text "com"] ] ] - Html.tr [ - Html.td [ + + let createAuthorStringHelper (author: Person) = + let mi = if author.MidInitials.IsSome then author.MidInitials.Value else "" + $"{author.FirstName} {mi} {author.LastName}" + let createAuthorsStringHelper (authors: Person []) = authors |> Array.map createAuthorStringHelper |> String.concat ", " + + let protocolElement i (template:ARCtrl.Template.Template) (isShown:bool) (setIsShown: bool -> unit) (model:Model) dispatch = + [ + Html.tr [ + prop.key $"{i}_{template.Id}" + prop.classes [ "nonSelectText"; if isShown then "hoverTableEle"] prop.style [ - style.padding 0 - if isActive then - style.borderBottom (2, borderStyle.solid, "black") - else - style.display.none + style.cursor.pointer; style.userSelect.none; ] - prop.colSpan 4 + prop.onClick (fun e -> + e.preventDefault() + setIsShown (not isShown) + //if isActive then + // UpdateDisplayedProtDetailsId None |> ProtocolMsg |> dispatch + //else + // UpdateDisplayedProtDetailsId (Some i) |> ProtocolMsg |> dispatch + ) prop.children [ - Bulma.box [ - prop.style [style.borderRadius 0] - prop.children [ - Html.div [ - Html.div template.Description + Html.td template.Name + Html.td ( + if curatedOrganisationNames |> List.contains (template.Organisation.ToString().ToLower()) then + curatedTag + else + communitytag + ) + //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ a [ OnClick (fun e -> e.stopPropagation()); Href prot.DocsLink; Target "_Blank"; Title "docs" ] [Fa.i [Fa.Size Fa.Fa2x ; Fa.Regular.FileAlt] []] ] + Html.td [ prop.style [style.textAlign.center; style.verticalAlign.middle]; prop.text template.Version ] + //td [ Style [TextAlign TextAlignOptions.Center; VerticalAlign "middle"] ] [ str (string template.Used) ] + Html.td ( + Bulma.icon [Html.i [prop.className "fa-solid fa-chevron-down"]] + ) + ] + ] + Html.tr [ + Html.td [ + prop.style [ + style.padding 0 + if isShown then + style.borderBottom (2, borderStyle.solid, "black") + else + style.display.none + ] + prop.colSpan 4 + prop.children [ + Bulma.box [ + prop.style [style.borderRadius 0] + prop.children [ Html.div [ - Html.div [ Html.b "Author: "; Html.span (createAuthorsStringHelper template.Authors) ] - Html.div [ Html.b "Created: "; Html.span (template.LastUpdated.ToString("yyyy/MM/dd")) ] + Html.div template.Description + Html.div [ + Html.div [ Html.b "Author: "; Html.span (createAuthorsStringHelper template.Authors) ] + Html.div [ Html.b "Created: "; Html.span (template.LastUpdated.ToString("yyyy/MM/dd")) ] + ] + Html.div [ + Html.div [ Html.b "Organisation: "; Html.span (template.Organisation.ToString()) ] + ] ] - Html.div [ - Html.div [ Html.b "Organisation: "; Html.span (template.Organisation.ToString()) ] + Bulma.tags [ + for tag in template.EndpointRepositories do + yield + Bulma.tag [Bulma.color.isLink; prop.text tag.NameText; prop.title tag.TermAccessionShort] + ] + Bulma.tags [ + for tag in template.Tags do + yield + Bulma.tag [Bulma.color.isInfo; prop.text tag.NameText; prop.title tag.TermAccessionShort] + ] + Bulma.button.a [ + prop.onClick (fun _ -> SelectProtocol template |> ProtocolMsg |> dispatch) + Bulma.button.isFullWidth; Bulma.color.isSuccess + prop.text "select" ] - ] - Bulma.tags [ - for tag in template.EndpointRepositories do - yield - Bulma.tag [Bulma.color.isLink; prop.text tag.NameText; prop.title tag.TermAccessionShort] - ] - Bulma.tags [ - for tag in template.Tags do - yield - Bulma.tag [Bulma.color.isInfo; prop.text tag.NameText; prop.title tag.TermAccessionShort] - ] - Bulma.button.a [ - prop.onClick (fun _ -> SelectProtocol template |> ProtocolMsg |> dispatch) - Bulma.button.isFullWidth; Bulma.color.isSuccess - prop.text "select" ] ] ] ] ] ] - ] - -let private curatedCommunityFilterDropdownItem (filter:Protocol.CuratedCommunityFilter) (child: ReactElement) (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.dropdownItem.a [ - prop.onClick(fun e -> - e.preventDefault(); - {state with CuratedCommunityFilter = filter} |> setState - //UpdateCuratedCommunityFilter filter |> ProtocolMsg |> dispatch - ) - prop.children child - ] -let private curatedCommunityFilterElement (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.dropdown [ - Bulma.dropdown.isHoverable - prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.button [ - Bulma.button.isSmall; Bulma.button.isOutlined; Bulma.color.isWhite; - prop.style [style.padding 0] - match state.CuratedCommunityFilter with - | Protocol.CuratedCommunityFilter.Both -> curatedCommunityTag - | Protocol.CuratedCommunityFilter.OnlyCommunity -> communitytag - | Protocol.CuratedCommunityFilter.OnlyCurated -> curatedTag - |> prop.children - ] - ] - Bulma.dropdownMenu [ - prop.style [style.minWidth.unset; style.fontWeight.normal] - Bulma.dropdownContent [ - curatedCommunityFilterDropdownItem Protocol.CuratedCommunityFilter.Both curatedCommunityTag state setState - curatedCommunityFilterDropdownItem Protocol.CuratedCommunityFilter.OnlyCurated curatedTag state setState - curatedCommunityFilterDropdownItem Protocol.CuratedCommunityFilter.OnlyCommunity communitytag state setState - ] |> prop.children + let RefreshButton (model:Messages.Model) dispatch = + Bulma.button.button [ + Bulma.button.isSmall + prop.onClick (fun _ -> Messages.Protocol.GetAllProtocolsForceRequest |> ProtocolMsg |> dispatch) + prop.children [ + Bulma.icon [Html.i [prop.className "fa-solid fa-arrows-rotate"]] ] ] - ] + + //let CommunityFilterDropdownItem (filter:Protocol.CommunityFilter) (child: ReactElement) (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = + // Bulma.dropdownItem.a [ + // prop.onClick(fun e -> + // e.preventDefault(); + // {state with CommunityFilter = filter} |> setState + // //UpdateCommunityFilter filter |> ProtocolMsg |> dispatch + // ) + // prop.children child + // ] + + //let CommunityFilterElement (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = + // Bulma.dropdown [ + // Bulma.dropdown.isHoverable + // prop.children [ + // Bulma.dropdownTrigger [ + // Bulma.button.button [ + // Bulma.button.isSmall; Bulma.button.isOutlined; Bulma.color.isWhite; + // prop.style [style.padding 0] + // match state.CommunityFilter with + // | Protocol.CommunityFilter.All -> curatedCommunityTag + // | Protocol.CommunityFilter.OnlyCommunity -> communitytag + // | Protocol.CommunityFilter.OnlyCurated -> curatedTag + // |> prop.children + // ] + // ] + // Bulma.dropdownMenu [ + // prop.style [style.minWidth.unset; style.fontWeight.normal] + // Bulma.dropdownContent [ + // CommunityFilterDropdownItem Protocol.CommunityFilter.All curatedCommunityTag state setState + // CommunityFilterDropdownItem Protocol.CommunityFilter.OnlyCurated curatedTag state setState + // CommunityFilterDropdownItem Protocol.CommunityFilter.OnlyCommunity communitytag state setState + // ] |> prop.children + // ] + // ] + // ] open Feliz open System +open ComponentAux -[] -let ProtocolContainer (model:Model) dispatch = - - let state, setState = React.useState(ProtocolViewState.init) - - let sortTableBySearchQuery (protocol: ARCtrl.Template.Template []) = - let query = state.ProtocolSearchQuery.Trim() - // Only search if field is not empty and does not start with "/". - // If it starts with "/" and does not match SearchFields then it will never trigger search - // As soon as it matches SearchFields it will be removed and can become 'query <> ""' - if query <> "" && query.StartsWith("/") |> not - then - let queryBigram = query |> Shared.SorensenDice.createBigrams - let createScore (str:string) = - str - |> Shared.SorensenDice.createBigrams - |> Shared.SorensenDice.calculateDistance queryBigram - let scoredTemplate = - protocol - |> Array.map (fun template -> - let score = - match state.Searchfield with - | SearchFields.Name -> - createScore template.Name - | SearchFields.Organisation -> - createScore (template.Organisation.ToString()) - | SearchFields.Authors -> - let query' = query.ToLower() - let scores = template.Authors |> Array.filter (fun author -> - (createAuthorStringHelper author).ToLower().Contains query' - || (author.ORCID.IsSome && author.ORCID.Value = query) - ) - if Array.isEmpty scores then 0.0 else 1.0 - score, template - ) - |> Array.filter (fun (score,_) -> score > 0.1) - |> Array.sortByDescending fst - |> Array.map snd - scoredTemplate - else - protocol - let filterTableByTags (protocol:ARCtrl.Template.Template []) = - if state.ProtocolFilterTags <> [] || state.ProtocolFilterErTags <> [] then - protocol |> Array.filter (fun x -> - let tags = Array.append x.Tags x.EndpointRepositories |> Array.distinct - let filterTags = state.ProtocolFilterTags@state.ProtocolFilterErTags |> List.distinct - Seq.except filterTags tags - |> fun filteredTags -> - // if we want to filter by tag with AND, all tags must match - if state.TagFilterIsAnd then - Seq.length filteredTags = tags.Length - filterTags.Length - // if we want to filter by tag with OR, at least one tag must match - else - Seq.length filteredTags < tags.Length - ) - else - protocol - let filterTableByCuratedCommunityFilter (protocol:ARCtrl.Template.Template []) = - match state.CuratedCommunityFilter with - | Protocol.CuratedCommunityFilter.Both -> protocol - | Protocol.CuratedCommunityFilter.OnlyCurated -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames) - | Protocol.CuratedCommunityFilter.OnlyCommunity -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames |> not) - - let sortedTable = - model.ProtocolState.ProtocolsAll - |> filterTableByTags - |> filterTableByCuratedCommunityFilter - |> sortTableBySearchQuery - |> Array.sortBy (fun template -> template.Name, template.Organisation) - - mainFunctionContainer [ + +type Search = + + static member InfoField() = Bulma.field.div [ - Bulma.help [ - Html.b "Search for templates." - Html.span " For more information you can look " - Html.a [ prop.href Shared.URLs.SwateWiki; prop.target "_Blank"; prop.text "here"] - Html.span ". If you find any problems with a template or have other suggestions you can contact us " - Html.a [ prop.href URLs.Helpdesk.UrlTemplateTopic; prop.target "_Blank"; prop.text "here"] - Html.span "." - ] - Bulma.help [ - Html.span "You can search by template name, organisation and authors. Just type:" - Bulma.content [ - Html.ul [ - Html.li [Html.code "/a"; Html.span " to search authors."] - Html.li [Html.code "/o"; Html.span " to search organisations."] - Html.li [Html.code "/n"; Html.span " to search template names."] + Bulma.help [ + Html.b "Search for templates." + Html.span " For more information you can look " + Html.a [ prop.href Shared.URLs.SwateWiki; prop.target "_Blank"; prop.text "here"] + Html.span ". If you find any problems with a template or have other suggestions you can contact us " + Html.a [ prop.href URLs.Helpdesk.UrlTemplateTopic; prop.target "_Blank"; prop.text "here"] + Html.span "." + ] + Bulma.help [ + Html.span "You can search by template name, organisation and authors. Just type:" + Bulma.content [ + Html.ul [ + Html.li [Html.code "/a"; Html.span " to search authors."] + Html.li [Html.code "/o"; Html.span " to search organisations."] + Html.li [Html.code "/n"; Html.span " to search template names."] + ] ] ] ] - ] + + [] + static member FileSortElement(templates, setter, model, dispatch) = + let templates = model.ProtocolState.Templates + let sortTableBySearchQuery searchfield (searchQuery: string) (protocol: ARCtrl.Template.Template []) = + let query = searchQuery.Trim() + // Only search if field is not empty and does not start with "/". + // If it starts with "/" and does not match SearchFields then it will never trigger search + // As soon as it matches SearchFields it will be removed and can become 'query <> ""' + if query <> "" && query.StartsWith("/") |> not + then + let queryBigram = query |> Shared.SorensenDice.createBigrams + let createScore (str:string) = + str + |> Shared.SorensenDice.createBigrams + |> Shared.SorensenDice.calculateDistance queryBigram + let scoredTemplate = + protocol + |> Array.map (fun template -> + let score = + match searchfield with + | SearchFields.Name -> + createScore template.Name + | SearchFields.Organisation -> + createScore (template.Organisation.ToString()) + | SearchFields.Authors -> + let query' = query.ToLower() + let scores = template.Authors |> Array.filter (fun author -> + (createAuthorStringHelper author).ToLower().Contains query' + || (author.ORCID.IsSome && author.ORCID.Value = query) + ) + if Array.isEmpty scores then 0.0 else 1.0 + score, template + ) + |> Array.filter (fun (score,_) -> score > 0.2) + |> Array.sortByDescending fst + |> Array.map snd + scoredTemplate + else + protocol + let filterTableByTags tags ertags tagfilter (protocol:ARCtrl.Template.Template []) = + if tags <> [] || ertags <> [] then + protocol |> Array.filter (fun x -> + let tags' = Array.append x.Tags x.EndpointRepositories |> Array.distinct + let filterTags = tags@ertags |> List.distinct + Seq.except filterTags tags' + |> fun filteredTags -> + // if we want to filter by tag with AND, all tags must match + if tagfilter then + Seq.length filteredTags = tags'.Length - filterTags.Length + // if we want to filter by tag with OR, at least one tag must match + else + Seq.length filteredTags < tags'.Length + ) + else + protocol + let filterTableByCommunityFilter communityfilter (protocol:ARCtrl.Template.Template []) = + match communityfilter with + | Protocol.CommunityFilter.All -> protocol + | Protocol.CommunityFilter.OnlyCurated -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames) + | Protocol.CommunityFilter.OnlyCommunity -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames |> not) + + let state, setState = React.useState(ProtocolViewState.init) + let propagateOutside = fun () -> + let sortedTable = + if templates.Length = 0 then [||] else + model.ProtocolState.Templates + |> filterTableByTags state.ProtocolFilterTags state.ProtocolFilterErTags state.TagFilterIsAnd + |> filterTableByCommunityFilter state.CommunityFilter + |> sortTableBySearchQuery state.Searchfield state.ProtocolSearchQuery + |> Array.sortBy (fun template -> template.Name, template.Organisation) + setter sortedTable + React.useEffect(propagateOutside, [|box state|]) + fileSortElements model state setState - Bulma.table [ - Bulma.table.isFullWidth - Bulma.table.isStriped + + [] + static member Component (templates, model:Model, dispatch, ?maxheight: Styles.ICssUnit) = + let maxheight = defaultArg maxheight (length.px 600) + let isEmpty = templates |> isNull || templates |> Array.isEmpty + let showIds, setShowIds = React.useState(fun _ -> []) + Html.div [ + prop.style [style.overflow.auto; style.maxHeight maxheight] prop.children [ - Html.thead [ - Html.tr [ - Html.th "Template Name" - //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Documentation" ] - Html.th [curatedCommunityFilterElement state setState] - Html.th "Template Version" - //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Uses" ] - Html.th Html.none + Bulma.table [ + Bulma.table.isFullWidth + Bulma.table.isStriped + prop.className "tableFixHead" + prop.children [ + Html.thead [ + Html.tr [ + Html.th "Template Name" + //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Documentation" ] + Html.th "Community"//[CommunityFilterElement state setState] + Html.th "Template Version" + //th [ Style [ Color model.SiteStyleState.ColorMode.Text; TextAlign TextAlignOptions.Center] ] [ str "Uses" ] + Html.th [ + RefreshButton model dispatch + ] + ] + ] + Html.tbody [ + match model.ProtocolState.Loading with + | true -> + Html.tr [ + Html.td [ + prop.colSpan 4 + prop.style [style.textAlign.center] + prop.children [ + Bulma.icon [ + Bulma.icon.isMedium + prop.children [ + Html.i [prop.className "fa-solid fa-spinner fa-spin fa-lg"] + ] + ] + ] + ] + ] + | false -> + match templates with + | [||] -> + Html.tr [ Html.td "Empty" ] + | _ -> + for i in 0 .. templates.Length-1 do + let isShown = showIds |> List.contains i + let setIsShown (show: bool) = + if show then i::showIds |> setShowIds else showIds |> List.filter (fun x -> x <> i) |> setShowIds + yield! + protocolElement i templates.[i] isShown setIsShown model dispatch + ] ] ] - Html.tbody [ - for i in 0 .. sortedTable.Length-1 do - yield! - protocolElement i sortedTable.[i] model state dispatch setState - ] ] - ] - ] \ No newline at end of file + ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs index 5f4dc290..ee331f5a 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs @@ -10,12 +10,13 @@ module Protocol = open Shared open Fable.Core - let update (fujMsg:Protocol.Msg) (currentState: Protocol.Model) : Protocol.Model * Cmd = + let update (fujMsg:Protocol.Msg) (state: Protocol.Model) : Protocol.Model * Cmd = match fujMsg with + | UpdateLoading next -> + {state with Loading = next}, Cmd.none // // ------ Process from file ------ | ParseUploadedFileRequest bytes -> - let nextModel = { currentState with Loading = true } failwith "ParseUploadedFileRequest IS NOT IMPLEMENTED YET" //let cmd = // Cmd.OfAsync.either @@ -23,32 +24,51 @@ module Protocol = // bytes // (ParseUploadedFileResponse >> ProtocolMsg) // (curry GenericError (UpdateLoading false |> ProtocolMsg |> Cmd.ofMsg) >> DevMsg) - nextModel, Cmd.none + state, Cmd.none | ParseUploadedFileResponse buildingBlockTables -> - let nextState = { currentState with UploadedFileParsed = buildingBlockTables; Loading = false } + let nextState = { state with UploadedFileParsed = buildingBlockTables } nextState, Cmd.none // ------ Protocol from Database ------ | GetAllProtocolsRequest -> - let nextState = {currentState with Loading = true} + let now = System.DateTime.UtcNow + let olderThanOneHour = state.LastUpdated |> Option.map (fun last -> (now - last) > System.TimeSpan(1,0,0)) + let cmd = + if olderThanOneHour.IsNone || olderThanOneHour.Value then GetAllProtocolsForceRequest |> ProtocolMsg |> Cmd.ofMsg else Cmd.none + state, cmd + | GetAllProtocolsForceRequest -> + let nextState = {state with Loading = true} let cmd = + let updateRequestStateOnErrorCmd = UpdateLoading false |> ProtocolMsg |> Cmd.ofMsg Cmd.OfAsync.either Api.templateApi.getTemplates () (GetAllProtocolsResponse >> ProtocolMsg) - (curry GenericError Cmd.none >> DevMsg) + (curry GenericError updateRequestStateOnErrorCmd >> DevMsg) nextState, cmd | GetAllProtocolsResponse protocolsJson -> - let protocols = protocolsJson |> Array.map (ARCtrl.Template.Json.Template.fromJsonString) + let state = {state with Loading = false} + let templates = + try + protocolsJson |> Array.map (ARCtrl.Template.Json.Template.fromJsonString) |> Ok + with + | e -> Result.Error e + let nextState, cmd = + match templates with + | Ok t -> + let nextState = { state with LastUpdated = Some System.DateTime.UtcNow } + nextState, UpdateTemplates t |> ProtocolMsg |> Cmd.ofMsg + | Result.Error e -> state, GenericError (Cmd.none,e) |> DevMsg |> Cmd.ofMsg + nextState, cmd + | UpdateTemplates templates -> let nextState = { - currentState with - ProtocolsAll = protocols - Loading = false + state with + Templates = templates } nextState, Cmd.none | SelectProtocol prot -> let nextState = { - currentState with - ProtocolSelected = Some prot + state with + TemplateSelected = Some prot } nextState, Cmd.ofMsg (UpdatePageState <| Some Routing.Route.Protocol) | ProtocolIncreaseTimesUsed templateId -> @@ -58,20 +78,15 @@ module Protocol = // Api.templateApi.increaseTimesUsedById // templateId // (curry GenericError Cmd.none >> DevMsg) - currentState, Cmd.none + state, Cmd.none // Client - | UpdateLoading nextLoadingState -> - let nextState = { - currentState with Loading = nextLoadingState - } - nextState, Cmd.none | RemoveSelectedProtocol -> let nextState = { - currentState with - ProtocolSelected = None + state with + TemplateSelected = None } nextState, Cmd.none | RemoveUploadedFileParsed -> - let nextState = {currentState with UploadedFileParsed = Array.empty} + let nextState = {state with UploadedFileParsed = Array.empty} nextState, Cmd.none diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index 59925ed7..4b3a5649 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -148,19 +148,19 @@ module TemplateFromDB = prop.children [ Bulma.button.a [ Bulma.color.isSuccess - if model.ProtocolState.ProtocolSelected.IsSome then + if model.ProtocolState.TemplateSelected.IsSome then Bulma.button.isActive else Bulma.color.isDanger prop.disabled true Bulma.button.isFullWidth prop.onClick (fun _ -> - if model.ProtocolState.ProtocolSelected.IsNone then + if model.ProtocolState.TemplateSelected.IsNone then failwith "No template selected!" // Remove existing columns let mutable columnsToRemove = [] // find duplicate columns - let tablecopy = model.ProtocolState.ProtocolSelected.Value.Table.Copy() + let tablecopy = model.ProtocolState.TemplateSelected.Value.Table.Copy() for header in tablecopy.Headers do let containsAtIndex = model.SpreadsheetModel.ActiveTable.Headers.FindIndex(fun h -> h = header) if containsAtIndex >= 0 then @@ -173,7 +173,7 @@ module TemplateFromDB = ] ] ] - if model.ProtocolState.ProtocolSelected.IsSome then + if model.ProtocolState.TemplateSelected.IsSome then Bulma.column [ Bulma.column.isNarrow Bulma.button.a [ @@ -185,38 +185,36 @@ module TemplateFromDB = ] ] + + let displaySelectedProtocolEle (model:Model) dispatch = - [ - div [Style [OverflowX OverflowOptions.Auto; MarginBottom "1rem"]] [ - Bulma.table [ - Bulma.table.isFullWidth; - Bulma.table.isBordered - prop.children [ - thead [] [ - Html.tr [ - Html.th "Column" - Html.th "Column TAN" - //Html.th "Unit" - //Html.th "Unit TAN" - ] - ] - tbody [] [ - for column in model.ProtocolState.ProtocolSelected.Value.Table.Columns do - //let unitOption = column.TryGetColumnUnits() - yield - Html.tr [ - td [] [str (column.Header.ToString())] - td [] [str (if column.Header.IsTermColumn then column.Header.ToTerm().TermAccessionShort else "-")] - //td [] [str (if unitOption.IsSome then insertBB.UnitTerm.Value.Name else "-")] - //td [] [str (if insertBB.HasUnit then insertBB.UnitTerm.Value.TermAccession else "-")] - ] + div [Style [OverflowX OverflowOptions.Auto; MarginBottom "1rem"]] [ + Bulma.table [ + Bulma.table.isFullWidth; + Bulma.table.isBordered + prop.children [ + thead [] [ + Html.tr [ + Html.th "Column" + Html.th "Column TAN" + //Html.th "Unit" + //Html.th "Unit TAN" ] ] + tbody [] [ + for column in model.ProtocolState.TemplateSelected.Value.Table.Columns do + //let unitOption = column.TryGetColumnUnits() + yield + Html.tr [ + td [] [str (column.Header.ToString())] + td [] [str (if column.Header.IsTermColumn then column.Header.ToTerm().TermAccessionShort else "-")] + //td [] [str (if unitOption.IsSome then insertBB.UnitTerm.Value.Name else "-")] + //td [] [str (if insertBB.HasUnit then insertBB.UnitTerm.Value.TermAccession else "-")] + ] + ] ] ] - addFromDBToTableButton model dispatch ] - let showDatabaseProtocolTemplate (model:Messages.Model) dispatch = mainFunctionContainer [ @@ -234,9 +232,12 @@ module TemplateFromDB = Bulma.field.div [ addFromDBToTableButton model dispatch ] - if model.ProtocolState.ProtocolSelected.IsSome then + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.field.div [ + displaySelectedProtocolEle model dispatch + ] Bulma.field.div [ - yield! displaySelectedProtocolEle model dispatch + addFromDBToTableButton model dispatch ] ] diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 4a124b9f..e158353c 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -28,7 +28,7 @@ module TermSearchAux = let searchByName(query: string, setResults: TermTypes.Term [] -> unit) = async { - let! terms = Api.ontology.searchTerms {|limit = 5; ontologies = []; query=query|} + let! terms = Api.ontology.searchTerms {|limit = 10; ontologies = []; query=query|} setResults terms } diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index 37284b30..4b06e1ae 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -169,7 +169,7 @@ module private Content = TemplateMetadata.Core.newNameMainElement model dispatch | Routing.Route.ProtocolSearch -> - Protocol.Search.ProtocolSearchView model dispatch + Protocol.Search.Main model dispatch | Routing.Route.ActivityLog -> ActivityLog.activityLogComponent model dispatch diff --git a/src/Client/style.scss b/src/Client/style.scss index d31aaad9..37a037d5 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -1,10 +1,5 @@ @charset "utf-8"; -// OpenSans -$FontPathOpenSans: "~open-sans-fonts/open-sans"; -$family-primary: 'Open Sans'; - - // 1. Import the initial variables @import "../../node_modules/bulma/sass/utilities/initial-variables.sass"; @import "../../node_modules/bulma/sass/utilities/functions.sass"; @@ -74,6 +69,38 @@ $colors: map-merge($colors, $addColors); @import "../../node_modules/bulma/bulma.sass"; /*@import "../../node_modules/bulma-checkradio/src/sass/index.sass";*/ +.template-filter-container { + flex-grow: 1; + display: flex; + flex-wrap: wrap; + gap: 1rem; + + > div { + flex-grow: 1 + } +} + +.widget-help-container { + padding: 5px; + + > * { + font-size: 0.85rem; // Set the specific font size for all direct children + } +} + +.tableFixHead { + overflow: auto; + max-height: 600px; + border-collapse: collapse; + + > thead th { + position: sticky; + top: 0; + z-index: 1; + background-color: var(--scheme-main) + } +} + .form-container { display: flex; flex-direction: row; From 4a85d48db329cde94ade2570e76ba3dc87e470cc Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 8 Feb 2024 15:15:26 +0100 Subject: [PATCH 035/135] Make widgets auto reposition inside window #358 --- src/Client/MainComponents/Widgets.fs | 35 +++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index 5a4da9c9..c7979015 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -30,16 +30,22 @@ module MoveEventListener = open Fable.Core.JsInterop - let calculatePosition (element:IRefValue) (startPosition: Rect) = fun (e: Event) -> - let e : MouseEvent = !!e + let ensurePositionInsideWindow (element:IRefValue) (position: Rect) = let maxX = Browser.Dom.window.innerWidth - element.current.Value.offsetWidth; - let tempX = int e.clientX - startPosition.X + let tempX = position.X let newX = System.Math.Min(System.Math.Max(tempX,0),int maxX) let maxY = Browser.Dom.window.innerHeight - element.current.Value.offsetHeight; - let tempY = int e.clientY - startPosition.Y + let tempY = position.Y let newY = System.Math.Min(System.Math.Max(tempY,0),int maxY) {X = newX; Y = newY} + let calculatePosition (element:IRefValue) (startPosition: Rect) = fun (e: Event) -> + let e : MouseEvent = !!e + let tempX = int e.clientX - startPosition.X + let tempY = int e.clientY - startPosition.Y + let tempPosition = {X = tempX; Y = tempY} + ensurePositionInsideWindow element tempPosition + let onmousemove (element:IRefValue) (startPosition: Rect) setPosition = fun (e: Event) -> let nextPosition = calculatePosition element startPosition e setPosition (Some nextPosition) @@ -58,7 +64,10 @@ module ResizeEventListener = let onmousemove (startPosition: Rect) (startSize: Rect) setSize = fun (e: Event) -> let e : MouseEvent = !!e let width = int e.clientX - startPosition.X + startSize.X - //let height = int e.clientY - startPosition.Y + startSize.Y + // I did not enable this, as it creates issues with overlays such as the term search dropdown. + // The widget card itself has overflow: visible, which makes a set height impossible, + // but wihout the visible overflow term search results might require scrolling. + // // let height = int e.clientY - startPosition.Y + startSize.Y setSize (Some {X = width; Y = startSize.Y}) let onmouseup (prefix, element: IRefValue) onmousemove = @@ -67,11 +76,6 @@ module ResizeEventListener = Size.write(prefix,{X = int element.current.Value.offsetWidth; Y = int element.current.Value.offsetHeight}) module Elements = - let resizeElement (content: ReactElement) = - Html.div [ - prop.style [style.cursor.northWestSouthEastResize; style.border(1, borderStyle.solid, "black")] - prop.children content - ] let helpExtendButton (extendToggle: unit -> unit) = Bulma.help [ @@ -93,6 +97,7 @@ type Widgets = let size, setSize = React.useState(fun _ -> Rect.initSizeFromPrefix prefix) let helpIsActive, setHelpIsActive = React.useState(false) let element = React.useElementRef() + React.useLayoutEffectOnce(fun _ -> MoveEventListener.ensurePositionInsideWindow element position.Value |> Some |> setPosition) // Reposition widget inside window let resizeElement (content: ReactElement) = Bulma.modalCard [ prop.ref element @@ -109,11 +114,13 @@ type Widgets = Browser.Dom.document.addEventListener("mouseup", onmouseup, config) ) prop.style [ - style.cursor.eastWestResize; style.display.flex + style.cursor.northWestSouthEastResize //style.cursor.eastWestResize; + style.display.flex style.padding(2); style.overflow.visible style.position.fixedRelativeToWindow if size.IsSome then style.width size.Value.X + //style.height size.Value.Y if position.IsNone then //style.transform.translate (length.perc -50,length.perc -50) style.top (length.perc 20); style.left (length.perc 20); @@ -124,7 +131,7 @@ type Widgets = ] resizeElement <| Html.div [ prop.onMouseDown(fun e -> e.stopPropagation()) - prop.style [style.cursor.defaultCursor] + prop.style [style.cursor.defaultCursor; style.display.flex; style.flexDirection.column; style.flexGrow 1] prop.children [ Bulma.modalCardHead [ prop.onMouseDown(fun e -> // move @@ -190,7 +197,7 @@ type Widgets = let selectContent() = [ Protocol.Search.FileSortElement(templates, setTemplates, model, dispatch) - Protocol.Search.Component (templates, model, dispatch, length.px 300) + Protocol.Search.Component (templates, model, dispatch, length.px 350) ] let insertContent() = [ @@ -198,7 +205,7 @@ type Widgets = Protocol.Core.TemplateFromDB.addFromDBToTableButton model dispatch ] Bulma.field.div [ - prop.style [style.maxHeight (length.px 300); style.overflow.auto] + prop.style [style.maxHeight (length.px 350); style.overflow.auto] prop.children [ Protocol.Core.TemplateFromDB.displaySelectedProtocolEle model dispatch ] From 6a5ffb21a0e1513b7ccb1059d3314d8d6d2ef875 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 8 Feb 2024 15:27:33 +0100 Subject: [PATCH 036/135] Fix bug in widget reposition :bug: --- src/Client/MainComponents/Widgets.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index c7979015..86b2eff3 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -97,7 +97,7 @@ type Widgets = let size, setSize = React.useState(fun _ -> Rect.initSizeFromPrefix prefix) let helpIsActive, setHelpIsActive = React.useState(false) let element = React.useElementRef() - React.useLayoutEffectOnce(fun _ -> MoveEventListener.ensurePositionInsideWindow element position.Value |> Some |> setPosition) // Reposition widget inside window + React.useLayoutEffectOnce(fun _ -> position |> Option.iter (fun position -> MoveEventListener.ensurePositionInsideWindow element position |> Some |> setPosition)) // Reposition widget inside window let resizeElement (content: ReactElement) = Bulma.modalCard [ prop.ref element From 10999a1cf37aa79437077494802850a21de575b1 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 8 Feb 2024 15:28:02 +0100 Subject: [PATCH 037/135] Improve ui state according to current state #354 --- src/Client/MainComponents/FooterTabs.fs | 76 +++++++++++++------------ src/Client/MainComponents/Navbar.fs | 52 ++++++++++------- src/Client/States/Spreadsheet.fs | 8 +++ src/Client/Views/MainWindowView.fs | 10 ++-- 4 files changed, 84 insertions(+), 62 deletions(-) diff --git a/src/Client/MainComponents/FooterTabs.fs b/src/Client/MainComponents/FooterTabs.fs index a815d860..361c66be 100644 --- a/src/Client/MainComponents/FooterTabs.fs +++ b/src/Client/MainComponents/FooterTabs.fs @@ -176,55 +176,61 @@ let MainMetadata(model: Messages.Model, dispatch: Messages.Msg -> unit) = ] [] -let MainPlus(dispatch: Messages.Msg -> unit) = +let MainPlus(model: Messages.Model, dispatch: Messages.Msg -> unit) = let state, setState = React.useState(FooterTab.init()) let order = System.Int32.MaxValue-1 // MaxValue will be sidebar toggle let id = "Add-Spreadsheet-Button" - Bulma.tab [ - prop.key id - prop.id id - if state.IsDraggedOver then prop.className "dragover-footertab" - prop.onDragEnter <| dragenter_handler(state, setState) - prop.onDragLeave <| dragleave_handler (state, setState) - prop.onDragOver drag_preventdefault - prop.onDrop <| drop_handler (order, state, setState, dispatch) - prop.onClick (fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) - prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] - prop.children [ - Html.a [ - prop.style [style.height.inheritFromParent; style.pointerEvents.none] - prop.children [ - Bulma.icon [ - Bulma.icon.isSmall - prop.children [ - Html.i [prop.className "fa-solid fa-plus"] + if model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata then + Html.none + else + Bulma.tab [ + prop.key id + prop.id id + if state.IsDraggedOver then prop.className "dragover-footertab" + prop.onDragEnter <| dragenter_handler(state, setState) + prop.onDragLeave <| dragleave_handler (state, setState) + prop.onDragOver drag_preventdefault + prop.onDrop <| drop_handler (order, state, setState, dispatch) + prop.onClick (fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] + prop.children [ + Html.a [ + prop.style [style.height.inheritFromParent; style.pointerEvents.none] + prop.children [ + Bulma.icon [ + Bulma.icon.isSmall + prop.children [ + Html.i [prop.className "fa-solid fa-plus"] + ] ] ] ] ] ] - ] let ToggleSidebar(model: Messages.Model, dispatch: Messages.Msg -> unit) = let show = model.PersistentStorageState.ShowSideBar let order = System.Int32.MaxValue let id = "Toggle-Sidebar-Button" - Bulma.tab [ - prop.key id - prop.id id - prop.onClick (fun e -> Messages.PersistentStorage.UpdateShowSidebar (not show) |> Messages.PersistentStorageMsg |> dispatch) - prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer; style.marginLeft length.auto] - prop.children [ - Html.a [ - prop.style [style.height.inheritFromParent; style.pointerEvents.none] - prop.children [ - Bulma.icon [ - Bulma.icon.isSmall - prop.children [ - Html.i [prop.className ["fa-solid"; if show then "fa-chevron-right" else "fa-chevron-left"]] + if model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata then + Html.none + else + Bulma.tab [ + prop.key id + prop.id id + prop.onClick (fun e -> Messages.PersistentStorage.UpdateShowSidebar (not show) |> Messages.PersistentStorageMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer; style.marginLeft length.auto] + prop.children [ + Html.a [ + prop.style [style.height.inheritFromParent; style.pointerEvents.none] + prop.children [ + Bulma.icon [ + Bulma.icon.isSmall + prop.children [ + Html.i [prop.className ["fa-solid"; if show then "fa-chevron-right" else "fa-chevron-left"]] + ] ] ] ] ] - ] - ] \ No newline at end of file + ] \ No newline at end of file diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index 13c94edb..a6cf3733 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -48,26 +48,6 @@ let private quickAccessButtonListStart (state: LocalHistory.Model) (setModal: Mo ), isActive = (state.NextPositionIsValid(state.HistoryCurrentPosition - 1)) ).toReactElement() - QuickAccessButton.create( - "Add Building Block", - [ - Bulma.icon [ - Html.i [prop.className "fa-solid fa-circle-plus" ] - Html.i [prop.className "fa-solid fa-table-columns" ] - ] - ], - (fun _ -> setModal (Some Modal.BuildingBlock)) - ).toReactElement() - QuickAccessButton.create( - "Add Template", - [ - Bulma.icon [ - Html.i [prop.className "fa-solid fa-circle-plus" ] - Html.i [prop.className "fa-solid fa-table" ] - ] - ], - (fun _ -> setModal (Some Modal.Template)) - ).toReactElement() ] ] @@ -95,6 +75,35 @@ let private quickAccessButtonListEnd (model: Model) dispatch = ] ] +let WidgetNavbarList (model, dispatch, setModal) = + Html.div [ + prop.style [ + style.display.flex; style.flexDirection.row + ] + prop.children [ + QuickAccessButton.create( + "Add Building Block", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table-columns" ] + ] + ], + (fun _ -> setModal (Some Modal.BuildingBlock)) + ).toReactElement() + QuickAccessButton.create( + "Add Template", + [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table" ] + ] + ], + (fun _ -> setModal (Some Modal.Template)) + ).toReactElement() + ] + ] + let private modalDisplay (modal: Modal option, model, dispatch, setModal) = let rmv = fun _ -> setModal (None) match modal with @@ -154,7 +163,7 @@ let Main (model: Messages.Model) dispatch = false ).toReactElement() quickAccessButtonListStart (model.History: LocalHistory.Model) setModal dispatch - + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, setModal) ] ] ] @@ -164,6 +173,7 @@ let Main (model: Messages.Model) dispatch = prop.style [style.display.flex; style.alignItems.stretch; style.justifyContent.flexStart; style.custom("marginRight", "auto")] prop.children [ quickAccessButtonListStart model.History setModal dispatch + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, setModal) ] ] Bulma.navbarEnd.div [ diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 6eabcc64..4451e388 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -63,6 +63,14 @@ type Model = { match this.ArcFile with | Some (Assay a) -> a | _ -> ArcAssay.init("ASSAY_NULL") member this.headerIsSelected = not this.SelectedCells.IsEmpty && this.SelectedCells |> Seq.exists (fun (c,r) -> r = 0) + member this.CanHaveTables() = + match this.ArcFile with + | Some (ArcFiles.Assay _) | Some (ArcFiles.Study _) -> true + | _ -> false + member this.TableViewIsActive() = + match this.ActiveView with + | ActiveView.Table i -> true + | _ -> false type Msg = // <--> UI <--> diff --git a/src/Client/Views/MainWindowView.fs b/src/Client/Views/MainWindowView.fs index 3ff1eaf5..d1da1793 100644 --- a/src/Client/Views/MainWindowView.fs +++ b/src/Client/Views/MainWindowView.fs @@ -26,12 +26,10 @@ let private spreadsheetSelectionFooter (model: Messages.Model) dispatch = MainComponents.FooterTabs.MainMetadata (model, dispatch) for index in 0 .. (model.SpreadsheetModel.Tables.TableCount-1) do MainComponents.FooterTabs.Main (index, model.SpreadsheetModel.Tables, model, dispatch) - match model.SpreadsheetModel.ArcFile with - | Some (ArcFiles.Template _) | Some (ArcFiles.Investigation _) -> - Html.none - | _ -> - MainComponents.FooterTabs.MainPlus dispatch - MainComponents.FooterTabs.ToggleSidebar(model, dispatch) + if model.SpreadsheetModel.CanHaveTables() then + MainComponents.FooterTabs.MainPlus (model, dispatch) + if model.SpreadsheetModel.TableViewIsActive() then + MainComponents.FooterTabs.ToggleSidebar(model, dispatch) ] ] ] From 1f3b65408bec84ee53b9543f91aaa01b3059e8af Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 8 Feb 2024 16:28:11 +0100 Subject: [PATCH 038/135] add table "+" not shown on metadata sheet :bug: --- src/Client/MainComponents/FooterTabs.fs | 74 ++++++++++++------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/src/Client/MainComponents/FooterTabs.fs b/src/Client/MainComponents/FooterTabs.fs index 361c66be..0f8c12bc 100644 --- a/src/Client/MainComponents/FooterTabs.fs +++ b/src/Client/MainComponents/FooterTabs.fs @@ -180,57 +180,51 @@ let MainPlus(model: Messages.Model, dispatch: Messages.Msg -> unit) = let state, setState = React.useState(FooterTab.init()) let order = System.Int32.MaxValue-1 // MaxValue will be sidebar toggle let id = "Add-Spreadsheet-Button" - if model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata then - Html.none - else - Bulma.tab [ - prop.key id - prop.id id - if state.IsDraggedOver then prop.className "dragover-footertab" - prop.onDragEnter <| dragenter_handler(state, setState) - prop.onDragLeave <| dragleave_handler (state, setState) - prop.onDragOver drag_preventdefault - prop.onDrop <| drop_handler (order, state, setState, dispatch) - prop.onClick (fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) - prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] - prop.children [ - Html.a [ - prop.style [style.height.inheritFromParent; style.pointerEvents.none] - prop.children [ - Bulma.icon [ - Bulma.icon.isSmall - prop.children [ - Html.i [prop.className "fa-solid fa-plus"] - ] + Bulma.tab [ + prop.key id + prop.id id + if state.IsDraggedOver then prop.className "dragover-footertab" + prop.onDragEnter <| dragenter_handler(state, setState) + prop.onDragLeave <| dragleave_handler (state, setState) + prop.onDragOver drag_preventdefault + prop.onDrop <| drop_handler (order, state, setState, dispatch) + prop.onClick (fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] + prop.children [ + Html.a [ + prop.style [style.height.inheritFromParent; style.pointerEvents.none] + prop.children [ + Bulma.icon [ + Bulma.icon.isSmall + prop.children [ + Html.i [prop.className "fa-solid fa-plus"] ] ] ] ] ] + ] let ToggleSidebar(model: Messages.Model, dispatch: Messages.Msg -> unit) = let show = model.PersistentStorageState.ShowSideBar let order = System.Int32.MaxValue let id = "Toggle-Sidebar-Button" - if model.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata then - Html.none - else - Bulma.tab [ - prop.key id - prop.id id - prop.onClick (fun e -> Messages.PersistentStorage.UpdateShowSidebar (not show) |> Messages.PersistentStorageMsg |> dispatch) - prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer; style.marginLeft length.auto] - prop.children [ - Html.a [ - prop.style [style.height.inheritFromParent; style.pointerEvents.none] - prop.children [ - Bulma.icon [ - Bulma.icon.isSmall - prop.children [ - Html.i [prop.className ["fa-solid"; if show then "fa-chevron-right" else "fa-chevron-left"]] - ] + Bulma.tab [ + prop.key id + prop.id id + prop.onClick (fun e -> Messages.PersistentStorage.UpdateShowSidebar (not show) |> Messages.PersistentStorageMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer; style.marginLeft length.auto] + prop.children [ + Html.a [ + prop.style [style.height.inheritFromParent; style.pointerEvents.none] + prop.children [ + Bulma.icon [ + Bulma.icon.isSmall + prop.children [ + Html.i [prop.className ["fa-solid"; if show then "fa-chevron-right" else "fa-chevron-left"]] ] ] ] ] - ] \ No newline at end of file + ] + ] \ No newline at end of file From 6c52f374c7c443b42bd48ce59a828cc28cf5cb3a Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 9 Feb 2024 13:02:29 +0100 Subject: [PATCH 039/135] Fix template search #360 --- src/Client/MainComponents/Navbar.fs | 2 +- src/Client/MainComponents/Widgets.fs | 4 +- .../ProtocolSearchViewComponent.fs | 164 ++++++------------ 3 files changed, 56 insertions(+), 114 deletions(-) diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index a6cf3733..cb7268ce 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -75,7 +75,7 @@ let private quickAccessButtonListEnd (model: Model) dispatch = ] ] -let WidgetNavbarList (model, dispatch, setModal) = +let private WidgetNavbarList (model, dispatch, setModal) = Html.div [ prop.style [ style.display.flex; style.flexDirection.row diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index 86b2eff3..9b0d196f 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -114,9 +114,9 @@ type Widgets = Browser.Dom.document.addEventListener("mouseup", onmouseup, config) ) prop.style [ - style.cursor.northWestSouthEastResize //style.cursor.eastWestResize; + style.cursor.eastWestResize//style.cursor.northWestSouthEastResize ; style.display.flex - style.padding(2); style.overflow.visible + style.paddingRight(2); style.overflow.visible style.position.fixedRelativeToWindow if size.IsSome then style.width size.Value.X diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs index 9da3ded6..65804475 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs @@ -242,77 +242,62 @@ module ComponentAux = ] ] - let tagDisplayField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = - Bulma.columns [ - Bulma.columns.isMobile + let TagRemovableElement (tag:OntologyAnnotation) (color: IReactProperty) (rmv: unit -> unit) = + Bulma.control.div [ + Bulma.tags [ + prop.style [style.flexWrap.nowrap] + Bulma.tags.hasAddons + prop.children [ + Bulma.tag [color; prop.style [style.borderWidth 0]; prop.text tag.NameText; prop.title tag.TermAccessionShort] + Bulma.tag [ + Bulma.tag.isDelete + prop.onClick (fun _ -> rmv()) + ] + ] + ] + ] + + let SwitchElement (tagIsFilterAnd: bool) (setFilter: bool -> unit) = + Html.div [ + prop.style [style.marginLeft length.auto] prop.children [ - Bulma.column [ - Bulma.field.div [ - Bulma.field.isGroupedMultiline - prop.children [ - for selectedTag in state.ProtocolFilterErTags do - yield Bulma.control.div [ - Bulma.tags [ - Bulma.tags.hasAddons - prop.children [ - Bulma.tag [Bulma.color.isLink; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] - Bulma.delete [ - prop.className "clickableTagDelete" - prop.onClick (fun _ -> - {state with ProtocolFilterErTags = state.ProtocolFilterErTags |> List.except [selectedTag]} |> setState - //RemoveProtocolErTag selectedTag |> ProtocolMsg |> dispatch - ) - ] - ] - ] - ] - for selectedTag in state.ProtocolFilterTags do - yield Bulma.control.div [ - Bulma.tags [ - Bulma.tags.hasAddons - prop.children [ - Bulma.tag [Bulma.color.isInfo; prop.style [style.borderWidth 0]; prop.text selectedTag.NameText] - Bulma.delete [ - prop.className "clickableTagDelete" - //Tag.Color IsWarning; - prop.onClick (fun _ -> - {state with ProtocolFilterTags = state.ProtocolFilterTags |> List.except [selectedTag]} |> setState - //RemoveProtocolTag selectedTag |> ProtocolMsg |> dispatch - ) - ] - ] - ] - ] - ] + Bulma.button.button [ + Bulma.button.isSmall + prop.onClick (fun _ -> setFilter (not tagIsFilterAnd)) + prop.title (if tagIsFilterAnd then "Templates contain all tags." else "Templates contain at least one tag.") + prop.text (if tagIsFilterAnd then "And" else "Or") + ] + ] + ] + + let TagDisplayField (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = + Html.div [ + prop.className "is-flex" + prop.children [ + Bulma.field.div [ + Bulma.field.isGroupedMultiline + prop.style [style.display.flex; style.flexGrow 1; style.gap (length.rem 0.5); style.flexWrap.wrap; style.flexDirection.row] + prop.children [ + for selectedTag in state.ProtocolFilterErTags do + let rmv = fun () -> {state with ProtocolFilterErTags = state.ProtocolFilterErTags |> List.except [selectedTag]} |> setState + TagRemovableElement selectedTag Bulma.color.isLink rmv + for selectedTag in state.ProtocolFilterTags do + let rmv = fun () -> {state with ProtocolFilterTags = state.ProtocolFilterTags |> List.except [selectedTag]} |> setState + TagRemovableElement selectedTag Bulma.color.isInfo rmv ] ] // tag filter (AND or OR) - Bulma.column [ - Bulma.column.isNarrow - prop.title (if state.TagFilterIsAnd then "Templates contain all tags." else "Templates contain at least one tag.") - Switch.checkbox [ - Bulma.color.isDark - prop.style [style.userSelect.none] - switch.isOutlined - switch.isSmall - prop.id "switch-2" - prop.isChecked state.TagFilterIsAnd - prop.onChange (fun (e:bool) -> - {state with TagFilterIsAnd = not state.TagFilterIsAnd} |> setState - //UpdateTagFilterIsAnd (not state.TagFilterIsAnd) |> ProtocolMsg |> dispatch - ) - prop.children (if state.TagFilterIsAnd then Html.b "And" else Html.b "Or") - ] |> prop.children - ] + let filtersetter = fun b -> setState {state with TagFilterIsAnd = b} + SwitchElement state.TagFilterIsAnd filtersetter ] ] let fileSortElements (model:Model) (state: ProtocolViewState) (setState: ProtocolViewState -> unit) = Html.div [ - prop.style [style.marginBottom(length.rem 0.75); style.display.flex] + prop.style [style.marginBottom(length.rem 0.75); style.display.flex; style.flexDirection.column] prop.children [ - Html.div [ + Bulma.field.div [ prop.className "template-filter-container" prop.children [ queryField model state setState @@ -322,7 +307,9 @@ module ComponentAux = ] // Only show the tag list and tag filter (AND or OR) if any tag exists if state.ProtocolFilterErTags <> [] || state.ProtocolFilterTags <> [] then - tagDisplayField model state setState + Bulma.field.div [ + TagDisplayField model state setState + ] ] ] @@ -430,42 +417,6 @@ module ComponentAux = ] ] - //let CommunityFilterDropdownItem (filter:Protocol.CommunityFilter) (child: ReactElement) (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = - // Bulma.dropdownItem.a [ - // prop.onClick(fun e -> - // e.preventDefault(); - // {state with CommunityFilter = filter} |> setState - // //UpdateCommunityFilter filter |> ProtocolMsg |> dispatch - // ) - // prop.children child - // ] - - //let CommunityFilterElement (state:ProtocolViewState) (setState: ProtocolViewState -> unit) = - // Bulma.dropdown [ - // Bulma.dropdown.isHoverable - // prop.children [ - // Bulma.dropdownTrigger [ - // Bulma.button.button [ - // Bulma.button.isSmall; Bulma.button.isOutlined; Bulma.color.isWhite; - // prop.style [style.padding 0] - // match state.CommunityFilter with - // | Protocol.CommunityFilter.All -> curatedCommunityTag - // | Protocol.CommunityFilter.OnlyCommunity -> communitytag - // | Protocol.CommunityFilter.OnlyCurated -> curatedTag - // |> prop.children - // ] - // ] - // Bulma.dropdownMenu [ - // prop.style [style.minWidth.unset; style.fontWeight.normal] - // Bulma.dropdownContent [ - // CommunityFilterDropdownItem Protocol.CommunityFilter.All curatedCommunityTag state setState - // CommunityFilterDropdownItem Protocol.CommunityFilter.OnlyCurated curatedTag state setState - // CommunityFilterDropdownItem Protocol.CommunityFilter.OnlyCommunity communitytag state setState - // ] |> prop.children - // ] - // ] - // ] - open Feliz open System open ComponentAux @@ -534,22 +485,13 @@ type Search = scoredTemplate else protocol - let filterTableByTags tags ertags tagfilter (protocol:ARCtrl.Template.Template []) = + let filterTableByTags tags ertags tagfilter (templates:ARCtrl.Template.Template []) = if tags <> [] || ertags <> [] then - protocol |> Array.filter (fun x -> - let tags' = Array.append x.Tags x.EndpointRepositories |> Array.distinct - let filterTags = tags@ertags |> List.distinct - Seq.except filterTags tags' - |> fun filteredTags -> - // if we want to filter by tag with AND, all tags must match - if tagfilter then - Seq.length filteredTags = tags'.Length - filterTags.Length - // if we want to filter by tag with OR, at least one tag must match - else - Seq.length filteredTags < tags'.Length - ) + let tagArray = tags@ertags |> Array.ofList + let filteredTemplates = templates |> ARCtrl.Template.Templates.filterByOntologyAnnotation(tagArray, tagfilter) + filteredTemplates else - protocol + templates let filterTableByCommunityFilter communityfilter (protocol:ARCtrl.Template.Template []) = match communityfilter with | Protocol.CommunityFilter.All -> protocol From b7134f47e7a49605f17a635755e72ce5742746e9 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 9 Feb 2024 13:14:20 +0100 Subject: [PATCH 040/135] Release v1.0.0-alpha.09 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 0e58e27e..e668ca0f 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha-08" - let [] AssemblyMetadata_ReleaseDate = "02.02.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.09" + let [] AssemblyMetadata_ReleaseDate = "09.02.2024" From 0c038b88ba846803ae0e4a63460bd7bc55eb1eab Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 13 Feb 2024 09:46:00 +0100 Subject: [PATCH 041/135] force lightmode in ARCitect on load :bug: #361 --- src/Client/Client.fs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Client/Client.fs b/src/Client/Client.fs index 61d30e96..9d280b5f 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -29,6 +29,12 @@ let private split_container model dispatch = [] let View (model : Model) (dispatch : Msg -> unit) = let (colorstate, setColorstate) = React.useState(LocalStorage.Darkmode.State.init) + // Make ARCitect always use lighttheme + let makeColorSchemeLight = fun _ -> + if model.PersistentStorageState.Host.IsSome && model.PersistentStorageState.Host.Value = Swatehost.ARCitect then + setColorstate {colorstate with Theme = LocalStorage.Darkmode.DataTheme.Light} + LocalStorage.Darkmode.DataTheme.SET LocalStorage.Darkmode.DataTheme.Light + React.useEffect(makeColorSchemeLight, [|box model.PersistentStorageState.Host|]) let v = {colorstate with SetTheme = setColorstate} React.contextProvider(LocalStorage.Darkmode.themeContext, v, Html.div [ From d8f466871b1e66ecec7ab6efef2352c2b6c258b3 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 13 Feb 2024 13:25:28 +0100 Subject: [PATCH 042/135] Support multiple widgets :sparkles: #364 --- src/Client/MainComponents/Navbar.fs | 49 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index cb7268ce..a742353a 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -9,11 +9,11 @@ open Messages open Components.QuickAccessButton [] -type private Modal = +type private Widget = | BuildingBlock | Template -let private quickAccessButtonListStart (state: LocalHistory.Model) (setModal: Modal option -> unit) dispatch = +let private quickAccessButtonListStart (state: LocalHistory.Model) dispatch = Html.div [ prop.style [ style.display.flex; style.flexDirection.row @@ -75,7 +75,7 @@ let private quickAccessButtonListEnd (model: Model) dispatch = ] ] -let private WidgetNavbarList (model, dispatch, setModal) = +let private WidgetNavbarList (model, dispatch, addWidget: Widget -> unit) = Html.div [ prop.style [ style.display.flex; style.flexDirection.row @@ -89,7 +89,7 @@ let private WidgetNavbarList (model, dispatch, setModal) = Html.i [prop.className "fa-solid fa-table-columns" ] ] ], - (fun _ -> setModal (Some Modal.BuildingBlock)) + (fun _ -> addWidget Widget.BuildingBlock) ).toReactElement() QuickAccessButton.create( "Add Template", @@ -99,23 +99,34 @@ let private WidgetNavbarList (model, dispatch, setModal) = Html.i [prop.className "fa-solid fa-table" ] ] ], - (fun _ -> setModal (Some Modal.Template)) + (fun _ -> addWidget Widget.Template) ).toReactElement() ] ] -let private modalDisplay (modal: Modal option, model, dispatch, setModal) = - let rmv = fun _ -> setModal (None) - match modal with - | None -> Html.none - | Some Modal.BuildingBlock -> - MainComponents.Widgets.BuildingBlock (model, dispatch, rmv) - | Some Modal.Template -> - MainComponents.Widgets.Templates (model, dispatch, rmv) +let private modalDisplay (widgets: Widget list, rmvWidget: Widget -> unit, model, dispatch) = + let rmv (widget: Widget) = fun _ -> rmvWidget widget + let displayWidget (widget: Widget) (widgetComponent) = + if widgets |> List.contains widget then + widgetComponent (model, dispatch, rmv widget) + else + Html.none + match widgets.Length with + | 0 -> + Html.none + | _ -> + Html.div [ + displayWidget Widget.BuildingBlock MainComponents.Widgets.BuildingBlock + displayWidget Widget.Template MainComponents.Widgets.Templates + ] [] let Main (model: Messages.Model) dispatch = - let modal, setModal = React.useState(None) + let widgets, setWidgets = React.useState([]) + let rmvWidget (widget: Widget) = widgets |> List.except [widget] |> setWidgets + let addWidget (widget: Widget) = + if widgets |> List.contains widget then () else + widget::widgets |> setWidgets Bulma.navbar [ prop.className "myNavbarSticky" prop.id "swate-mainNavbar" @@ -126,7 +137,7 @@ let Main (model: Messages.Model) dispatch = style.minHeight(length.rem 3.25) ] prop.children [ - modalDisplay (modal, model, dispatch, setModal) + modalDisplay (widgets, rmvWidget, model, dispatch) Bulma.navbarBrand.div [ ] @@ -162,8 +173,8 @@ let Main (model: Messages.Model) dispatch = (fun e -> ()), false ).toReactElement() - quickAccessButtonListStart (model.History: LocalHistory.Model) setModal dispatch - if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, setModal) + quickAccessButtonListStart (model.History: LocalHistory.Model) dispatch + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, addWidget) ] ] ] @@ -172,8 +183,8 @@ let Main (model: Messages.Model) dispatch = Bulma.navbarStart.div [ prop.style [style.display.flex; style.alignItems.stretch; style.justifyContent.flexStart; style.custom("marginRight", "auto")] prop.children [ - quickAccessButtonListStart model.History setModal dispatch - if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, setModal) + quickAccessButtonListStart model.History dispatch + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, addWidget) ] ] Bulma.navbarEnd.div [ From ec1dcd37261964251ffa65dd71fdfd45cec95a2f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 13 Feb 2024 14:24:07 +0100 Subject: [PATCH 043/135] Restore filepicker #362 --- src/Client/Pages/FilePicker/FilePickerView.fs | 50 +++++++++---------- src/Client/SidebarComponents/ResponsiveFA.fs | 22 ++++---- src/Client/States/Spreadsheet.fs | 1 + src/Client/Update/InterfaceUpdate.fs | 20 ++++++-- src/Client/Update/SpreadsheetUpdate.fs | 5 ++ src/Shared/ARCtrl.Helper.fs | 16 ++++++ 6 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/Client/Pages/FilePicker/FilePickerView.fs b/src/Client/Pages/FilePicker/FilePickerView.fs index e5b69535..4dc273e4 100644 --- a/src/Client/Pages/FilePicker/FilePickerView.fs +++ b/src/Client/Pages/FilePicker/FilePickerView.fs @@ -122,31 +122,31 @@ let sortButton icon msg = let fileSortElements (model:Messages.Model) dispatch = Bulma.field.div [ Bulma.buttons [ - Bulma.button.a [ - prop.title "Copy to Clipboard" - prop.onClick(fun e -> - CustomComponents.ResponsiveFA.triggerResponsiveReturnEle "clipboard_filepicker" - let txt = model.FilePickerState.FileNames |> List.map snd |> String.concat System.Environment.NewLine - let textArea = Browser.Dom.document.createElement "textarea" - textArea?value <- txt - textArea?style?top <- "0" - textArea?style?left <- "0" - textArea?style?position <- "fixed" - - Browser.Dom.document.body.appendChild textArea |> ignore - - textArea.focus() - // Can't belive this actually worked - textArea?select() - - let t = Browser.Dom.document.execCommand("copy") - Browser.Dom.document.body.removeChild(textArea) |> ignore - () - ) - prop.children [ - CustomComponents.ResponsiveFA.responsiveReturnEle "clipboard_filepicker" "fa-regular fa-clipboard" "fa-solid fa-check" - ] - ] + //Bulma.button.a [ + // prop.title "Copy to Clipboard" + // prop.onClick(fun e -> + // CustomComponents.ResponsiveFA.triggerResponsiveReturnEle "clipboard_filepicker" + // let txt = model.FilePickerState.FileNames |> List.map snd |> String.concat System.Environment.NewLine + // let textArea = Browser.Dom.document.createElement "textarea" + // textArea?value <- txt + // textArea?style?top <- "0" + // textArea?style?left <- "0" + // textArea?style?position <- "fixed" + + // Browser.Dom.document.body.appendChild textArea |> ignore + + // textArea.focus() + // // Can't belive this actually worked + // textArea?select() + + // let t = Browser.Dom.document.execCommand("copy") + // Browser.Dom.document.body.removeChild(textArea) |> ignore + // () + // ) + // prop.children [ + // CustomComponents.ResponsiveFA.responsiveReturnEle "clipboard_filepicker" "fa-solid fa-copy" "fa-solid fa-check" + // ] + //] Bulma.buttons [ Bulma.buttons.hasAddons diff --git a/src/Client/SidebarComponents/ResponsiveFA.fs b/src/Client/SidebarComponents/ResponsiveFA.fs index 32bd50bb..a14d1faf 100644 --- a/src/Client/SidebarComponents/ResponsiveFA.fs +++ b/src/Client/SidebarComponents/ResponsiveFA.fs @@ -65,18 +65,16 @@ let triggerResponsiveReturnEle id = let responsiveReturnEle id (fa: string) (faToggled: string) = let notTriggeredId = createNonTriggeredId id let triggeredId = createTriggeredId id - div [Style [ - Position PositionOptions.Relative - ]] [ + Bulma.icon [ Html.i [ prop.style [ - style.position.absolute - style.top 0 - style.left 0 - style.display.block - style.custom("transition", "opacity 0.25s, transform 0.25s") - style.opacity 1 - ] + style.position.absolute + //style.top 0 + //style.left 0 + style.display.block + style.custom("transition", "opacity 0.25s, transform 0.25s") + style.opacity 1 + ] prop.id notTriggeredId prop.onTransitionEnd (fun e -> Fable.Core.JS.setTimeout (fun () -> @@ -89,8 +87,8 @@ let responsiveReturnEle id (fa: string) (faToggled: string) = Html.i [ prop.style [ style.position.absolute - style.top 0 - style.left 0 + //style.top 0 + //style.left 0 style.display.block style.custom("transition", "opacity 0.25s, transform 0.25s") style.opacity 0 diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 4451e388..cec8a7a5 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -75,6 +75,7 @@ type Model = { type Msg = // <--> UI <--> | UpdateCell of (int*int) * CompositeCell +| UpdateCells of ((int*int) * CompositeCell) [] | UpdateHeader of columIndex: int * CompositeHeader | UpdateActiveView of ActiveView | UpdateSelectedCells of Set diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 7e07c787..06b90743 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -103,13 +103,23 @@ module Interface = | _ -> failwith "not implemented" | InsertFileNames fileNames -> match host with - | Some Swatehost.Excel | Some Swatehost.ARCitect -> + | Some Swatehost.Excel -> let cmd = OfficeInterop.InsertFileNames fileNames |> OfficeInteropMsg |> Cmd.ofMsg model, cmd - //| Swatehost.Browser -> - // let arr = fileNames |> List.toArray |> Array.map (fun x -> TermTypes.TermMinimal.create x "") - // let cmd = Spreadsheet.InsertOntologyTerms arr |> SpreadsheetMsg |> Cmd.ofMsg - // model, cmd + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + if model.SpreadsheetModel.SelectedCells.IsEmpty then + model, Cmd.ofMsg (DevMsg.GenericError (Cmd.none, exn("No cell(s) selected.")) |> DevMsg) + else + let columnIndex, rowIndex = model.SpreadsheetModel.SelectedCells.MinimumElement + let mutable rowIndex = rowIndex + let cells = [| + for name in fileNames do + let cell = ARCtrl.ISA.CompositeCell.createFreeText name + (columnIndex, rowIndex), cell + rowIndex <- rowIndex + 1 + |] + let cmd = Spreadsheet.UpdateCells cells |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd | _ -> failwith "not implemented" | RemoveBuildingBlock -> match host with diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index ad34b490..d5d738e9 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -97,6 +97,11 @@ module Spreadsheet = state.ActiveTable.UpdateCellAt(fst index,snd index, cell) {state with ArcFile = state.ArcFile} nextState, model, Cmd.none + | UpdateCells arr -> + let nextState = + state.ActiveTable.SetCellsAt arr + {state with ArcFile = state.ArcFile} + nextState, model, Cmd.none | UpdateHeader (index, header) -> let nextState = state.ActiveTable.UpdateHeader(index, header) diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index b05637a8..a84cb3b5 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -25,6 +25,22 @@ module ARCtrlHelper = module Extensions = open ARCtrl.Template + open ArcTableAux + + type ArcTable with + member this.SetCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell) = + SanityChecks.validateColumn <| CompositeColumn.create(this.Headers.[columnIndex],[|cell|]) + Unchecked.setCellAt(columnIndex, rowIndex,cell) this.Values + Unchecked.fillMissingCells this.Headers this.Values + + member this.SetCellsAt (cells: ((int*int)*CompositeCell) []) = + let columns = cells |> Array.groupBy (fun (index, cell) -> fst index) + for columnIndex, items in columns do + SanityChecks.validateColumn <| CompositeColumn.create(this.Headers.[columnIndex], items |> Array.map snd) + for index, cell in cells do + Unchecked.setCellAt(fst index, snd index, cell) this.Values + Unchecked.fillMissingCells this.Headers this.Values + type Template with member this.FileName From 996accca1ba3d8bda7681c6e7bba574b30480959 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 13 Feb 2024 14:42:37 +0100 Subject: [PATCH 044/135] Improve community select for templates :sparkles: --- src/Client/Model.fs | 25 +++++++++++------ .../ProtocolSearchViewComponent.fs | 28 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/Client/Model.fs b/src/Client/Model.fs index cec2d7ea..84ca8e28 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -347,20 +347,27 @@ module Protocol = type CommunityFilter = | All | OnlyCurated - | OnlyCommunity + | OnlyCommunities + | Community of string member this.ToStringRdb() = match this with - | All -> "All" - | OnlyCurated -> "Curated" - | OnlyCommunity -> "Community" + | All -> "All" + | OnlyCurated -> "DataPLANT official" + | OnlyCommunities -> "All Communities" + | Community name -> name static member fromString(str:string) = - match str.ToLower() with - | "all" -> All - | "curated" -> OnlyCurated - | "community" -> OnlyCommunity - | anyElse -> printfn "Unable to parse %s to CommunityFilter. Default to 'All'." anyElse; All + match str with + | "All" -> All + | "All Curated" -> OnlyCurated + | "All Community" -> OnlyCommunities + | anyElse -> Community anyElse + + static member CommunityFromOrganisation(org:ARCtrl.Template.Organisation) = + match org with + | ARCtrl.Template.Organisation.DataPLANT -> None + | ARCtrl.Template.Other name -> Some <| Community name /// This model is used for both protocol insert and protocol search type Model = { diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs index 65804475..33b550b8 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs @@ -213,14 +213,19 @@ module ComponentAux = open Fable.Core.JsInterop - let communitySelectField (model) (state: ProtocolViewState) setState = - let options = [ - Model.Protocol.CommunityFilter.All - Model.Protocol.CommunityFilter.OnlyCurated - Model.Protocol.CommunityFilter.OnlyCommunity - ] + let communitySelectField (model: Messages.Model) (state: ProtocolViewState) setState = + let communityNames = + model.ProtocolState.Templates + |> Array.choose (fun t -> Model.Protocol.CommunityFilter.CommunityFromOrganisation t.Organisation) + |> Array.distinct |> List.ofArray + let options = + [ + Model.Protocol.CommunityFilter.All + Model.Protocol.CommunityFilter.OnlyCurated + Model.Protocol.CommunityFilter.OnlyCommunities + ]@communityNames Html.div [ - Bulma.label "Search for tags" + Bulma.label "Select community" Bulma.control.div [ Bulma.control.isExpanded prop.children [ @@ -493,10 +498,13 @@ type Search = else templates let filterTableByCommunityFilter communityfilter (protocol:ARCtrl.Template.Template []) = + log communityfilter match communityfilter with - | Protocol.CommunityFilter.All -> protocol - | Protocol.CommunityFilter.OnlyCurated -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames) - | Protocol.CommunityFilter.OnlyCommunity -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames |> not) + | Protocol.CommunityFilter.All -> protocol + | Protocol.CommunityFilter.OnlyCurated -> protocol |> Array.filter (fun x -> log (x.Organisation.ToString().ToLower()) ; List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames) + | Protocol.CommunityFilter.OnlyCommunities -> protocol |> Array.filter (fun x -> List.contains (x.Organisation.ToString().ToLower()) curatedOrganisationNames |> not) + | Protocol.CommunityFilter.Community name -> protocol |> Array.filter (fun x -> x.Organisation.ToString() = name) + let state, setState = React.useState(ProtocolViewState.init) let propagateOutside = fun () -> From b799f291a765d2a95baa728423fb2434b7f53094 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 13 Feb 2024 16:14:21 +0100 Subject: [PATCH 045/135] Restore isa json export #365 --- src/Client/Client.fsproj | 2 +- .../{ModalPositions.fs => Widgets.fs} | 0 src/Client/MainComponents/Navbar.fs | 2 +- src/Client/Model.fs | 2 +- src/Client/Pages/JsonExporter/JsonExporter.fs | 559 ++++++++++-------- src/Client/Routing.fs | 2 +- src/Client/SidebarComponents/Navbar.fs | 7 + src/Client/Views/SidebarView.fs | 5 +- 8 files changed, 341 insertions(+), 238 deletions(-) rename src/Client/LocalStorage/{ModalPositions.fs => Widgets.fs} (100%) diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 8b33ea12..afe7ab9d 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -13,7 +13,7 @@ - + diff --git a/src/Client/LocalStorage/ModalPositions.fs b/src/Client/LocalStorage/Widgets.fs similarity index 100% rename from src/Client/LocalStorage/ModalPositions.fs rename to src/Client/LocalStorage/Widgets.fs diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index a742353a..a4c7b412 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -58,7 +58,7 @@ let private quickAccessButtonListEnd (model: Model) dispatch = ] prop.children [ QuickAccessButton.create( - "Save as xlsx", + "Save", [ Bulma.icon [Html.i [prop.className "fa-solid fa-floppy-disk";]] ], diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 84ca8e28..c89612e2 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -360,7 +360,7 @@ module Protocol = static member fromString(str:string) = match str with | "All" -> All - | "All Curated" -> OnlyCurated + | "DataPLANT official" -> OnlyCurated | "All Community" -> OnlyCommunities | anyElse -> Community anyElse diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index e0f2d8fc..3071a72d 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -25,7 +25,8 @@ let download(filename, text) = element.click(); - document.body.removeChild(element); + document.body.removeChild(element) |> ignore + () let update (msg:JsonExporter.Msg) (currentModel: Messages.Model) : Messages.Model * Cmd = match msg with @@ -196,251 +197,347 @@ let update (msg:JsonExporter.Msg) (currentModel: Messages.Model) : Messages.Mode currentModel.updateByJsonExporterModel nextModel, Cmd.none -open Messages +//open Messages +//open Feliz +//open Feliz.Bulma + +//let dropdownItem (exportType:JsonExportType) (model:Model) msg (isActive:bool) = +// Bulma.dropdownItem.a [ +// prop.tabIndex 0 +// prop.onClick (fun e -> +// e.stopPropagation() +// exportType |> msg +// ) +// prop.onKeyDown (fun k -> if (int k.which) = 13 then exportType |> msg) +// prop.children [ +// Html.span [ +// prop.className "has-tooltip-right has-tooltip-multiline" +// prop.custom ("data-tooltip", exportType.toExplanation) +// prop.style [style.fontSize(length.rem 1.1); style.paddingRight 10; style.textAlign.center; style.color NFDIColors.Yellow.Darker20] +// Html.i [prop.className "fa-solid fa-circle-info"] |> prop.children +// ] + +// Html.span (exportType.ToString()) +// ] +// ] + +//let parseTableToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = +// mainFunctionContainer [ +// Bulma.field.div [ +// Bulma.field.hasAddons +// prop.children [ +// Bulma.control.div [ +// Bulma.dropdown [ +// if model.JsonExporterModel.ShowTableExportTypeDropdown then Bulma.dropdown.isActive +// prop.children [ +// Bulma.dropdownTrigger [ +// Bulma.button.a [ +// prop.onClick(fun e -> e.stopPropagation(); UpdateShowTableExportTypeDropdown (not model.JsonExporterModel.ShowTableExportTypeDropdown) |> JsonExporterMsg |> dispatch ) +// prop.children [ +// span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.TableJsonExportType.ToString())] +// Html.i [prop.className "fa-solid fa-angle-down"] +// ] +// ] +// ] +// Bulma.dropdownMenu [ +// Bulma.dropdownContent [ +// let msg = (UpdateTableJsonExportType >> JsonExporterMsg >> dispatch) +// dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.Assay) +// dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.ProcessSeq) +// ] +// ] +// ] +// ] +// ] +// Bulma.control.div [ +// Bulma.control.isExpanded +// Bulma.button.a [ +// Bulma.color.isInfo +// Bulma.button.isFullWidth +// prop.onClick(fun _ -> +// InterfaceMsg SpreadsheetInterface.ExportJsonTable |> dispatch +// ) +// prop.text "Download as isa json" +// ] |> prop.children +// ] +// ] +// ] +// ] + +//let parseTablesToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = +// mainFunctionContainer [ +// Bulma.field.div [ +// Bulma.field.hasAddons +// prop.children [ +// Bulma.control.div [ +// Bulma.dropdown [ +// if model.JsonExporterModel.ShowWorkbookExportTypeDropdown then Bulma.dropdown.isActive +// prop.children [ +// Bulma.dropdownTrigger [ +// Bulma.button.a [ +// prop.onClick (fun e -> e.stopPropagation(); UpdateShowWorkbookExportTypeDropdown (not model.JsonExporterModel.ShowWorkbookExportTypeDropdown) |> JsonExporterMsg |> dispatch ) +// prop.children [ +// span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.WorkbookJsonExportType.ToString())] +// Html.i [prop.className "fa-solid fa-angle-down"] +// ] +// ] +// ] +// Bulma.dropdownMenu [ +// Bulma.dropdownContent [ +// let msg = (UpdateWorkbookJsonExportType >> JsonExporterMsg >> dispatch) +// dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.Assay) +// dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.ProcessSeq) +// ] +// ] +// ] +// ] +// ] +// Bulma.control.div [ +// Bulma.control.isExpanded +// Bulma.button.a [ +// Bulma.color.isInfo +// Bulma.button.isFullWidth +// prop.onClick(fun _ -> +// InterfaceMsg SpreadsheetInterface.ExportJsonTables |> dispatch +// ) +// prop.text "Download as isa json" +// ] +// |> prop.children +// ] +// ] +// ] +// ] + +//// SND ELEMENT +//open Browser.Types + +//let fileUploadButton (model:Model) dispatch (id: string) = +// Bulma.label [ +// prop.className "mb-2 has-text-weight-normal" +// prop.children [ +// Bulma.fileInput [ +// prop.id id +// prop.type' "file"; +// prop.style [style.display.none] +// prop.onChange (fun (ev: File list) -> +// let files = ev//: Browser.Types.FileList = ev.target?files + +// let blobs = +// files +// |> List.map (fun f -> f.slice() ) + +// let reader = Browser.Dom.FileReader.Create() + +// reader.onload <- fun evt -> +// let byteArr = +// let arraybuffer : Fable.Core.JS.ArrayBuffer = evt.target?result +// let uintArr = Fable.Core.JS.Constructors.Uint8Array.Create arraybuffer +// uintArr.ToString().Split([|","|], System.StringSplitOptions.RemoveEmptyEntries) +// |> Array.map (fun byteStr -> byte byteStr) + +// StoreXLSXByteArray byteArr |> JsonExporterMsg |> dispatch + +// reader.onerror <- fun evt -> +// curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch + +// reader.readAsArrayBuffer(blobs |> List.head) + +// let picker = Browser.Dom.document.getElementById(id) +// // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 +// picker?value <- null +// ) +// ] +// Bulma.button.a [ +// Bulma.color.isInfo; +// Bulma.button.isFullWidth +// prop.text "Upload Excel file" +// ] +// ] +// ] + + +//let xlsxUploadAndParsingMainElement (model:Model) (dispatch: Msg -> unit) = +// let inputId = "xlsxConverter_uploadButton" +// mainFunctionContainer [ +// // Upload xlsx file to byte [] +// fileUploadButton model dispatch inputId +// // Request parsing +// Bulma.field.div [ +// Bulma.field.hasAddons +// prop.children [ +// Bulma.control.div [ +// Bulma.dropdown [ +// if model.JsonExporterModel.ShowXLSXExportTypeDropdown then Bulma.dropdown.isActive +// prop.children [ +// Bulma.dropdownTrigger [ +// Bulma.button.a [ +// prop.onClick (fun e -> e.stopPropagation(); UpdateShowXLSXExportTypeDropdown (not model.JsonExporterModel.ShowXLSXExportTypeDropdown) |> JsonExporterMsg |> dispatch ) +// prop.children [ +// span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.XLSXParsingExportType.ToString())] +// Html.i [prop.className "fa-solid fa-angle-down"] +// ] +// ] +// ] +// Bulma.dropdownMenu [ +// Bulma.dropdownContent [ +// let msg = (UpdateXLSXParsingExportType >> JsonExporterMsg >> dispatch) +// dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.Assay) +// dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProcessSeq) +// dropdownItem JsonExportType.ProtocolTemplate model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProtocolTemplate) +// ] +// ] +// ] +// ] +// ] +// Bulma.control.div [ +// Bulma.control.isExpanded +// Bulma.button.a [ +// let hasContent = model.JsonExporterModel.XLSXByteArray <> Array.empty +// Bulma.color.isInfo +// if hasContent then +// Bulma.button.isActive +// else +// Bulma.color.isDanger +// prop.disabled true +// Bulma.button.isFullWidth +// prop.onClick(fun _ -> +// if hasContent then +// ParseXLSXToJsonRequest model.JsonExporterModel.XLSXByteArray |> JsonExporterMsg |> dispatch +// ) +// prop.text "Download as isa json" +// ] +// |> prop.children +// ] +// ] +// ] +// ] + +//let jsonExporterMainElement (model:Messages.Model) (dispatch: Messages.Msg -> unit) = + + //Bulma.content [ + + // prop.onSubmit (fun e -> e.preventDefault()) + // prop.onKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) + // prop.onClick (fun e -> CloseAllDropdowns |> JsonExporterMsg |> dispatch) + // prop.style [style.minHeight(length.vh 100)] + // prop.children [ + // Bulma.label "Json Exporter" + + // Bulma.help [ + // str "Export swate annotation tables to " + // a [Href @"https://en.wikipedia.org/wiki/JSON"] [str "JSON"] + // str " format. Official ISA-JSON types can be found " + // a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"] [str "here"] + // str "." + // ] + + // Bulma.label "Export active table" + + // parseTableToISAJsonEle model dispatch + + // Bulma.label "Export workbook" + + // parseTablesToISAJsonEle model dispatch + + // Bulma.label "Export Swate conform xlsx file." + + // xlsxUploadAndParsingMainElement model dispatch + // ] + //] + open Feliz open Feliz.Bulma -let dropdownItem (exportType:JsonExportType) (model:Model) msg (isActive:bool) = - Bulma.dropdownItem.a [ - prop.tabIndex 0 - prop.onClick (fun e -> - e.stopPropagation() - exportType |> msg - ) - prop.onKeyDown (fun k -> if (int k.which) = 13 then exportType |> msg) - prop.children [ - Html.span [ - prop.className "has-tooltip-right has-tooltip-multiline" - prop.custom ("data-tooltip", exportType.toExplanation) - prop.style [style.fontSize(length.rem 1.1); style.paddingRight 10; style.textAlign.center; style.color NFDIColors.Yellow.Darker20] - Html.i [prop.className "fa-solid fa-circle-info"] |> prop.children - ] +module FileExporterAux = - Html.span (exportType.ToString()) - ] - ] - -let parseTableToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.field.hasAddons - prop.children [ - Bulma.control.div [ - Bulma.dropdown [ - if model.JsonExporterModel.ShowTableExportTypeDropdown then Bulma.dropdown.isActive - prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.a [ - prop.onClick(fun e -> e.stopPropagation(); UpdateShowTableExportTypeDropdown (not model.JsonExporterModel.ShowTableExportTypeDropdown) |> JsonExporterMsg |> dispatch ) - prop.children [ - span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.TableJsonExportType.ToString())] - Html.i [prop.className "fa-solid fa-angle-down"] - ] - ] - ] - Bulma.dropdownMenu [ - Bulma.dropdownContent [ - let msg = (UpdateTableJsonExportType >> JsonExporterMsg >> dispatch) - dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.Assay) - dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.TableJsonExportType = JsonExportType.ProcessSeq) - ] - ] + open ARCtrl.ISA + open ARCtrl.ISA.Json + + [] + type ISA = + | Assay + | Study + | Investigation + + static member initFromArcfile(arcfile: ArcFiles) = + match arcfile with + | ArcFiles.Assay _ -> Some Assay + | ArcFiles.Study _ -> Some Study + | ArcFiles.Investigation _ -> Some Investigation + | ArcFiles.Template _ -> None + + let toISAJsonString (arcfile: ArcFiles option) = + let timed = fun s -> System.DateTime.Now.ToString("yyyyMMdd_hhmm_") + s + match arcfile with + | None | Some (ArcFiles.Template _) -> None + | Some (ArcFiles.Assay a) -> (timed "assay.json",ArcAssay.toJsonString a) |> Some + | Some (ArcFiles.Study (s,as')) -> (timed "study.json", ArcStudy.toJsonString s (ResizeArray as')) |> Some + | Some (ArcFiles.Investigation i) -> (timed "investigation.json", ArcInvestigation.toJsonString i) |> Some + +open FileExporterAux + +type FileExporter = + + [] + static member JsonExport(model: Messages.Model, dispatch) = + let isa, setIsa = React.useState(model.SpreadsheetModel.ArcFile |> Option.bind ISA.initFromArcfile) + Html.div [ + Bulma.field.div [ + Bulma.field.hasAddons + prop.children [ + Bulma.control.p [ + //Html.span [ + // prop.className "select" + // prop.children [ + // Html.select [ + // Html.option "Test" + // Html.option "Test2" + // Html.option "Test3" + // ] + // ] + //] + Bulma.button.a [ + prop.text "ISA" + Bulma.button.isStatic ] ] - ] - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.onClick(fun _ -> - InterfaceMsg SpreadsheetInterface.ExportJsonTable |> dispatch - ) - prop.text "Download as isa json" - ] |> prop.children - ] - ] - ] - ] - -let parseTablesToISAJsonEle (model:Model) (dispatch:Messages.Msg -> unit) = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.field.hasAddons - prop.children [ - Bulma.control.div [ - Bulma.dropdown [ - if model.JsonExporterModel.ShowWorkbookExportTypeDropdown then Bulma.dropdown.isActive + Bulma.control.p [ + Bulma.control.isExpanded prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.a [ - prop.onClick (fun e -> e.stopPropagation(); UpdateShowWorkbookExportTypeDropdown (not model.JsonExporterModel.ShowWorkbookExportTypeDropdown) |> JsonExporterMsg |> dispatch ) - prop.children [ - span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.WorkbookJsonExportType.ToString())] - Html.i [prop.className "fa-solid fa-angle-down"] - ] - ] - ] - Bulma.dropdownMenu [ - Bulma.dropdownContent [ - let msg = (UpdateWorkbookJsonExportType >> JsonExporterMsg >> dispatch) - dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.Assay) - dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.WorkbookJsonExportType = JsonExportType.ProcessSeq) - ] + Bulma.button.button [ + Bulma.button.isFullWidth + prop.text "Download" + if isa.IsNone then + prop.disabled true + prop.onClick (fun _ -> + let r = toISAJsonString model.SpreadsheetModel.ArcFile + r |> Option.iter (fun r -> download r) + ) ] ] ] ] - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.onClick(fun _ -> - InterfaceMsg SpreadsheetInterface.ExportJsonTables |> dispatch - ) - prop.text "Download as isa json" - ] - |> prop.children - ] - ] - ] - ] - -// SND ELEMENT -open Browser.Types - -let fileUploadButton (model:Model) dispatch (id: string) = - Bulma.label [ - prop.className "mb-2 has-text-weight-normal" - prop.children [ - Bulma.fileInput [ - prop.id id - prop.type' "file"; - prop.style [style.display.none] - prop.onChange (fun (ev: File list) -> - let files = ev//: Browser.Types.FileList = ev.target?files - - let blobs = - files - |> List.map (fun f -> f.slice() ) - - let reader = Browser.Dom.FileReader.Create() - - reader.onload <- fun evt -> - let byteArr = - let arraybuffer : Fable.Core.JS.ArrayBuffer = evt.target?result - let uintArr = Fable.Core.JS.Constructors.Uint8Array.Create arraybuffer - uintArr.ToString().Split([|","|], System.StringSplitOptions.RemoveEmptyEntries) - |> Array.map (fun byteStr -> byte byteStr) - - StoreXLSXByteArray byteArr |> JsonExporterMsg |> dispatch - - reader.onerror <- fun evt -> - curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch - - reader.readAsArrayBuffer(blobs |> List.head) - - let picker = Browser.Dom.document.getElementById(id) - // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 - picker?value <- null - ) - ] - Bulma.button.a [ - Bulma.color.isInfo; - Bulma.button.isFullWidth - prop.text "Upload Excel file" ] - ] - ] - - -let xlsxUploadAndParsingMainElement (model:Model) (dispatch: Msg -> unit) = - let inputId = "xlsxConverter_uploadButton" - mainFunctionContainer [ - // Upload xlsx file to byte [] - fileUploadButton model dispatch inputId - // Request parsing - Bulma.field.div [ - Bulma.field.hasAddons - prop.children [ - Bulma.control.div [ - Bulma.dropdown [ - if model.JsonExporterModel.ShowXLSXExportTypeDropdown then Bulma.dropdown.isActive - prop.children [ - Bulma.dropdownTrigger [ - Bulma.button.a [ - prop.onClick (fun e -> e.stopPropagation(); UpdateShowXLSXExportTypeDropdown (not model.JsonExporterModel.ShowXLSXExportTypeDropdown) |> JsonExporterMsg |> dispatch ) - prop.children [ - span [Style [MarginRight "5px"]] [str (model.JsonExporterModel.XLSXParsingExportType.ToString())] - Html.i [prop.className "fa-solid fa-angle-down"] - ] - ] - ] - Bulma.dropdownMenu [ - Bulma.dropdownContent [ - let msg = (UpdateXLSXParsingExportType >> JsonExporterMsg >> dispatch) - dropdownItem JsonExportType.Assay model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.Assay) - dropdownItem JsonExportType.ProcessSeq model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProcessSeq) - dropdownItem JsonExportType.ProtocolTemplate model msg (model.JsonExporterModel.XLSXParsingExportType = JsonExportType.ProtocolTemplate) - ] - ] - ] - ] + if isa.IsNone then + Bulma.help [ + Bulma.color.isDanger + prop.text "Unable to convert Template to ISA-JSON" ] - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - let hasContent = model.JsonExporterModel.XLSXByteArray <> Array.empty - Bulma.color.isInfo - if hasContent then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick(fun _ -> - if hasContent then - ParseXLSXToJsonRequest model.JsonExporterModel.XLSXByteArray |> JsonExporterMsg |> dispatch - ) - prop.text "Download as isa json" - ] - |> prop.children - ] - ] ] - ] - -let jsonExporterMainElement (model:Messages.Model) (dispatch: Messages.Msg -> unit) = - - Bulma.content [ - - prop.onSubmit (fun e -> e.preventDefault()) - prop.onKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) - prop.onClick (fun e -> CloseAllDropdowns |> JsonExporterMsg |> dispatch) - prop.style [style.minHeight(length.vh 100)] - prop.children [ - Bulma.label "Json Exporter" - - Bulma.help [ - str "Export swate annotation tables to " - a [Href @"https://en.wikipedia.org/wiki/JSON"] [str "JSON"] - str " format. Official ISA-JSON types can be found " - a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"] [str "here"] - str "." + static member Main(model:Messages.Model, dispatch: Messages.Msg -> unit) = + Html.div [ + pageHeader "File Export" + + Bulma.label "Export to Json" + mainFunctionContainer [ + Bulma.field.div [Bulma.help [ + str "Export Swate annotation tables to official ISA-JSON (" + a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"] [str "more"] + str ")." + ]] + FileExporter.JsonExport(model, dispatch) ] + ] - Bulma.label "Export active table" - - parseTableToISAJsonEle model dispatch - - Bulma.label "Export workbook" - - parseTablesToISAJsonEle model dispatch - - Bulma.label "Export Swate conform xlsx file." - xlsxUploadAndParsingMainElement model dispatch - ] - ] \ No newline at end of file diff --git a/src/Client/Routing.fs b/src/Client/Routing.fs index 6985fae3..b02f29e9 100644 --- a/src/Client/Routing.fs +++ b/src/Client/Routing.fs @@ -71,7 +71,7 @@ type Route = | Route.TemplateMetadata -> createElem [ Html.i [prop.className "fa-solid fa-circle-plus" ];Html.i [prop.className "fa-solid fa-table" ]] p.toStringRdbl | Route.FilePicker -> - createElem [ Html.i [prop.className "fa-solid fa-upload" ]] p.toStringRdbl + createElem [ Html.i [prop.className "fa-solid fa-file-signature" ]] p.toStringRdbl | Route.ActivityLog -> createElem [ Html.i [prop.className "fa-solid fa-timeline" ]] p.toStringRdbl | Route.Info -> diff --git a/src/Client/SidebarComponents/Navbar.fs b/src/Client/SidebarComponents/Navbar.fs index 9ae30c8b..d5bb9bf8 100644 --- a/src/Client/SidebarComponents/Navbar.fs +++ b/src/Client/SidebarComponents/Navbar.fs @@ -214,6 +214,13 @@ let NavbarComponent (model : Model) (dispatch : Msg -> unit) (sidebarsize: Model Html.i [prop.className "fa-brands fa-twitter"; prop.style [style.color "#1DA1F2"; style.marginLeft 2]] ] ] + Bulma.navbarItem.a [ + prop.onClick (fun e -> + setState {state with BurgerActive = not state.BurgerActive} + UpdatePageState (Some Routing.Route.Info) |> dispatch + ) + prop.text Routing.Route.Info.toStringRdbl + ] Bulma.navbarItem.a [ prop.href Shared.URLs.SwateWiki ; prop.target "_Blank"; diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index 4b06e1ae..fa796640 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -62,9 +62,8 @@ let private tabs (model:Model) dispatch (sidebarsize: Model.WindowSize) = //if not isIEBrowser then // docsrc attribute not supported in iframe in IE //createNavigationTab Routing.Route.Dag model dispatch sidebarsize - createNavigationTab Routing.Route.Info model dispatch sidebarsize - else createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize + else createNavigationTab Routing.Route.TemplateMetadata model dispatch sidebarsize //createNavigationTab Routing.Route.Validation model dispatch sidebarsize createNavigationTab Routing.Route.Info model dispatch sidebarsize @@ -163,7 +162,7 @@ module private Content = Protocol.Core.fileUploadViewComponent model dispatch | Routing.Route.JsonExport -> - JsonExporter.Core.jsonExporterMainElement model dispatch + JsonExporter.Core.FileExporter.Main(model, dispatch) | Routing.Route.TemplateMetadata -> TemplateMetadata.Core.newNameMainElement model dispatch From 6ff2d2a82c59b1818e3123895600ebe6bf88ce7d Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 13 Feb 2024 16:15:20 +0100 Subject: [PATCH 046/135] Release v1.0.0-alpha.10 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index e668ca0f..fd0a7287 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha.09" - let [] AssemblyMetadata_ReleaseDate = "09.02.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.10" + let [] AssemblyMetadata_ReleaseDate = "13.02.2024" From 3da74c713e4b9c3562c062b9d53c0321f952da7c Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 15 Feb 2024 14:23:39 +0100 Subject: [PATCH 047/135] Greatly improve copy/paste logic for excel interop. Utilizing clipboard :sparkles: #367#368 --- src/Client/Helper.fs | 58 ++++------ src/Client/MainComponents/Cells.fs | 5 +- src/Client/MainComponents/ContextMenu.fs | 35 +++++- .../Spreadsheet/Clipboard.Controller.fs | 101 ++++++++++++------ src/Client/Spreadsheet/Table.Controller.fs | 16 +++ src/Client/States/Spreadsheet.fs | 10 +- src/Client/Update/SpreadsheetUpdate.fs | 78 +++++++++++--- src/Shared/ARCtrl.Helper.fs | 34 ++++++ 8 files changed, 242 insertions(+), 95 deletions(-) diff --git a/src/Client/Helper.fs b/src/Client/Helper.fs index 79e17b01..588305f1 100644 --- a/src/Client/Helper.fs +++ b/src/Client/Helper.fs @@ -52,46 +52,26 @@ let debouncel<'T> (storage:Dictionary) (key: string) (timeout: int) let newDebounceStorage = fun () -> Dictionary(HashIdentity.Structural) -module React = - open Fable.Core - open Browser.Types - open Feliz - open Fable.Core.JsInterop +type Clipboard = + abstract member writeText: string -> JS.Promise + abstract member readText: unit -> JS.Promise - type ElementSize = { - Width: float - Height: float - } with - static member init() = { - Width = 0. - Height = 0. - } +type Navigator = + abstract member clipboard: Clipboard - [] - let (!?) (opt: 't option) (property: string) : obj = nativeOnly +[] +let navigator : Navigator = jsNative - let useElementSize () = - let initialValue : HTMLElement option = None - let ref, setRef = React.useState(initialValue) - // https://usehooks-ts.com/react-hook/use-element-size - // Mutable values like 'ref.current' aren't valid dependencies - // because mutating them doesn't re-render the component. - // Instead, we use a state as a ref to be reactive. - let size, setSize = React.useState(ElementSize.init()) - // Prevent too many rendering using useCallback - let handleSize = - React.useCallback( - (fun () -> - setSize { - Width = ref |> Option.map (fun r -> r.offsetWidth) |> Option.defaultValue 0. - Height = ref |> Option.map (fun r -> r.offsetHeight) |> Option.defaultValue 0. - } - ), - [| !? ref "offsetWidth"; !? ref "offsetHeight" |] - ) - React.useLayoutEffect((fun () -> handleSize()), [|!? ref "offsetWidth"; !? ref "offsetHeight"|]) - size, - (fun (ele: Element) -> - setRef (ele :?> HTMLElement |> Some) - ) +let takeFromArray (count: int) (array: 'a []) = + let exit (acc: 'a list) = List.rev acc |> Array.ofList + let rec takeRec (l2: 'a list) (acc: 'a list) index = + if index >= count then + exit acc + else + match l2 with + | [] -> exit acc + | item::tail -> + let newAcc = item::acc + takeRec tail newAcc (index+1) + takeRec (Array.toList array) [] 0 \ No newline at end of file diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index d4671d28..2fd5d69e 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -205,7 +205,7 @@ module private EventPresets = // don't select cell if active(editable) if state_cell.IsIdle then let set = - match e.ctrlKey, selectedCells.Count with + match e.shiftKey, selectedCells.Count with | true, 0 -> selectedCells | true, _ -> @@ -338,11 +338,12 @@ type Cell = prop.onDoubleClick(fun e -> e.preventDefault() e.stopPropagation() - let mode = if (e.ctrlKey || e.metaKey )&& columnType = Main then Search else Active + let mode = if (e.ctrlKey || e.metaKey) && columnType = Main then Search else Active if state_cell.IsIdle then setState_cell {state_cell with CellMode = mode} UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch ) if state_cell.IsIdle then prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) + prop.onMouseDown(fun e -> if state_cell.IsIdle && e.shiftKey then e.preventDefault()) prop.children [ match state_cell.CellMode with | Active -> diff --git a/src/Client/MainComponents/ContextMenu.fs b/src/Client/MainComponents/ContextMenu.fs index dd866ed0..cc8dfd72 100644 --- a/src/Client/MainComponents/ContextMenu.fs +++ b/src/Client/MainComponents/ContextMenu.fs @@ -12,7 +12,9 @@ type private ContextFunctions = { Copy : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit Cut : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit Paste : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + PasteAll : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit FillColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + Clear : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit TransformCell : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit //EditColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit RowIndex : int @@ -22,7 +24,7 @@ type private ContextFunctions = { let private isUnitOrTermCell (cell: CompositeCell option) = cell.IsSome && not cell.Value.isFreeText -let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (contextCell: CompositeCell option) (selectedCell: CompositeCell option ) (rmv: _ -> unit) = +let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (contextCell: CompositeCell option) (rmv: _ -> unit) = /// This element will remove the contextmenu when clicking anywhere else let rmv_element = Html.div [ prop.onClick rmv @@ -61,10 +63,12 @@ let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (con if isUnitOrTermCell contextCell then let text = if contextCell.Value.isTerm then "As Unit Cell" else "As Term Cell" button (text, "fa-solid fa-arrow-right-arrow-left", funcs.TransformCell rmv, []) + button ("Clear", "fa-solid fa-eraser", funcs.Clear rmv, []) divider button ("Copy", "fa-solid fa-copy", funcs.Copy rmv, []) button ("Cut", "fa-solid fa-scissors", funcs.Cut rmv, []) - button ("Paste", "fa-solid fa-paste", funcs.Paste rmv, [prop.disabled selectedCell.IsNone]) + button ("Paste", "fa-solid fa-paste", funcs.Paste rmv, []) + button ("Paste All", "fa-solid fa-paste", funcs.PasteAll rmv, []) divider button ("Delete Row", "fa-solid fa-delete-left", funcs.DeleteRow rmv, []) button ("Delete Column", "fa-solid fa-delete-left fa-rotate-270", funcs.DeleteColumn rmv, []) @@ -85,6 +89,8 @@ let private contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (con ] ] +open Shared + let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Types.MouseEvent) -> e.stopPropagation() e.preventDefault() @@ -98,14 +104,33 @@ let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Typ else Spreadsheet.DeleteRow (snd index) |> Messages.SpreadsheetMsg |> dispatch let cell = model.SpreadsheetModel.ActiveTable.TryGetCellAt(fst index, snd index) + let isSelectedCell = model.SpreadsheetModel.SelectedCells.Contains index //let editColumnEvent _ = Modals.Controller.renderModal("EditColumn_Modal", Modals.EditColumn.Main (fst index) model dispatch) let funcs = { DeleteRow = fun rmv e -> rmv e; deleteRowEvent e DeleteColumn = fun rmv e -> rmv e; Spreadsheet.DeleteColumn (fst index) |> Messages.SpreadsheetMsg |> dispatch - Copy = fun rmv e -> rmv e; Spreadsheet.CopyCell index |> Messages.SpreadsheetMsg |> dispatch + Copy = fun rmv e -> + rmv e; + if isSelectedCell then + log "Copy Cells" + Spreadsheet.CopySelectedCells |> Messages.SpreadsheetMsg |> dispatch + else + log "Copy Cell" + Spreadsheet.CopyCell index |> Messages.SpreadsheetMsg |> dispatch Cut = fun rmv e -> rmv e; Spreadsheet.CutCell index |> Messages.SpreadsheetMsg |> dispatch - Paste = fun rmv e -> rmv e; Spreadsheet.PasteCell index |> Messages.SpreadsheetMsg |> dispatch + Paste = fun rmv e -> + rmv e; + if isSelectedCell then + log "Paste Cells" + Spreadsheet.PasteSelectedCells |> Messages.SpreadsheetMsg |> dispatch + else + log "Paste Cell" + Spreadsheet.PasteCell index |> Messages.SpreadsheetMsg |> dispatch + PasteAll = fun rmv e -> + rmv e; + Spreadsheet.PasteCellsExtend index |> Messages.SpreadsheetMsg |> dispatch FillColumn = fun rmv e -> rmv e; Spreadsheet.FillColumnWithTerm index |> Messages.SpreadsheetMsg |> dispatch + Clear = fun rmv e -> rmv e; if isSelectedCell then Spreadsheet.ClearSelected |> Messages.SpreadsheetMsg |> dispatch else Spreadsheet.Clear [|index|] |> Messages.SpreadsheetMsg |> dispatch TransformCell = fun rmv e -> if cell.IsSome && (cell.Value.isTerm || cell.Value.isUnitized) then let nextCell = if cell.Value.isTerm then cell.Value.ToUnitizedCell() else cell.Value.ToTermCell() @@ -114,6 +139,6 @@ let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Typ RowIndex = snd index ColumnIndex = fst index } - let child = contextmenu mousePosition funcs cell model.SpreadsheetModel.Clipboard.Cell + let child = contextmenu mousePosition funcs cell let name = $"context_{mousePosition}" Modals.Controller.renderModal(name, child) \ No newline at end of file diff --git a/src/Client/Spreadsheet/Clipboard.Controller.fs b/src/Client/Spreadsheet/Clipboard.Controller.fs index cb37cbef..b3280bb0 100644 --- a/src/Client/Spreadsheet/Clipboard.Controller.fs +++ b/src/Client/Spreadsheet/Clipboard.Controller.fs @@ -1,56 +1,87 @@ module Spreadsheet.Clipboard.Controller +open Fable.Core open ARCtrl.ISA open Shared -module ClipboardAux = - let setClipboardCell (state: Spreadsheet.Model) (cell: CompositeCell option) = - let nextState = {state with Clipboard = { state.Clipboard with Cell = cell}} - nextState +let copyCell (cell: CompositeCell) : JS.Promise = + let tab = cell.ToTabStr() + navigator.clipboard.writeText(tab) -open ClipboardAux +let copyCells (cells: CompositeCell []) : JS.Promise = + let tab = CompositeCell.ToTabTxt cells + navigator.clipboard.writeText(tab) -let copyCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - let cell = state.ActiveTable.TryGetCellAt(index) - let nextState = {state with Clipboard = { state.Clipboard with Cell = cell}} - nextState +let copyCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise = + let cell = state.ActiveTable.Values.[index] + copyCell cell -let copySelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = +let copyCellsByIndex (indices: (int*int) []) (state: Spreadsheet.Model) : JS.Promise = + let cells = [|for index in indices do yield state.ActiveTable.Values.[index] |] + log cells + copyCells cells + +let copySelectedCell (state: Spreadsheet.Model) : JS.Promise = /// Array.head is used until multiple cells are supported, should this ever be intended let index = state.SelectedCells |> Set.toArray |> Array.min - copyCell index state + copyCellByIndex index state + +let copySelectedCells (state: Spreadsheet.Model) : JS.Promise = + /// Array.head is used until multiple cells are supported, should this ever be intended + let indices = state.SelectedCells |> Set.toArray + copyCellsByIndex indices state -let cutCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - let cell = state.ActiveTable.TryGetCellAt(index) +let cutCellByIndex (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = + let cell = state.ActiveTable.Values.[index] // Remove selected cell value - - let emptyCell = if cell.IsSome then cell.Value.GetEmptyCell() else state.ActiveTable.GetColumn(fst index).GetDefaultEmptyCell() - state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) - let nextState = setClipboardCell state cell - nextState + let emptyCell = cell.GetEmptyCell() + state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) + copyCell cell |> Promise.start + state let cutSelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = /// Array.min is used until multiple cells are supported, should this ever be intended let index = state.SelectedCells |> Set.toArray |> Array.min - cutCell index state + cutCellByIndex index state + +let pasteCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise = + promise { + let! tab = navigator.clipboard.readText() + let cell = CompositeCell.fromTabTxt tab |> Array.head + state.ActiveTable.UpdateCellAt(fst index, snd index, cell) + return state + } -let pasteCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - match state.Clipboard.Cell with - // Don't update if no cell in saved - | None -> state - | Some c -> - state.ActiveTable.UpdateCellAt(fst index, snd index, c) - state +let pasteCellsByIndexExtend (index: int*int) (state: Spreadsheet.Model) : JS.Promise = + promise { + let! tab = navigator.clipboard.readText() + let cells = CompositeCell.fromTabTxt tab + let columnIndex, rowIndex = fst index, snd index + let indexedCells = cells |> Array.indexed |> Array.map (fun (i,c) -> (columnIndex, rowIndex + i), c) + state.ActiveTable.SetCellsAt indexedCells + return state + } -let pasteSelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = +let pasteCellIntoSelected (state: Spreadsheet.Model) : JS.Promise = if state.SelectedCells.IsEmpty then - state + promise {return state} else - // TODO: - //let arr = state.SelectedCells |> Set.toArray - //let isOneColumn = - // let c = fst arr.[0] // can just use head of selected cells as all must be same column - // arr |> Array.forall (fun x -> fst x = c) - //if not isOneColumn then failwith "Can only paste cells in one column at a time!" let minIndex = state.SelectedCells |> Set.toArray |> Array.min - pasteCell minIndex state \ No newline at end of file + pasteCellByIndex minIndex state + +let pasteCellsIntoSelected (state: Spreadsheet.Model) : JS.Promise = + if state.SelectedCells.IsEmpty then + promise {return state} + else + let columnIndex = state.SelectedCells |> Set.toArray |> Array.minBy fst |> fst + let selectedSingleColumnCells = state.SelectedCells |> Set.filter (fun index -> fst index = columnIndex) + promise { + let! tab = navigator.clipboard.readText() + let cells = CompositeCell.fromTabTxt tab + let rowCount = selectedSingleColumnCells.Count + let cellsTrimmed = cells |> takeFromArray rowCount + let indicesTrimmed = (Set.toArray selectedSingleColumnCells).[0..cellsTrimmed.Length-1] + let indexedCellsTrimmed = Array.zip indicesTrimmed cellsTrimmed + state.ActiveTable.SetCellsAt indexedCellsTrimmed + return state + } \ No newline at end of file diff --git a/src/Client/Spreadsheet/Table.Controller.fs b/src/Client/Spreadsheet/Table.Controller.fs index 58c4a35d..2df153c3 100644 --- a/src/Client/Spreadsheet/Table.Controller.fs +++ b/src/Client/Spreadsheet/Table.Controller.fs @@ -97,6 +97,22 @@ let fillColumnWithCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet ) {state with ArcFile = state.ArcFile} +/// +/// Transform cells of given indices to their empty equivalents +/// +/// +/// +let clearCells (indexArr: (int*int) []) (state: Spreadsheet.Model) : Spreadsheet.Model = + let table = state.ActiveTable + let newCells = [| + for index in indexArr do + let cell = table.Values.[index] + let emptyCell = cell.GetEmptyCell() + index, emptyCell + |] + table.SetCellsAt newCells + state + // Ui depends on main column name, maybe change this to depends on BuildingBlockType? // Header main column name must be updated diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index cec8a7a5..23973370 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -30,14 +30,12 @@ type Model = { ActiveView: ActiveView SelectedCells: Set ArcFile: ArcFiles option - Clipboard: TableClipboard } with static member init() = { ActiveView = ActiveView.Metadata SelectedCells = Set.empty ArcFile = None - Clipboard = TableClipboard.init() } member this.Tables with get() = @@ -74,6 +72,7 @@ type Model = { type Msg = // <--> UI <--> +| UpdateState of Model | UpdateCell of (int*int) * CompositeCell | UpdateCells of ((int*int) * CompositeCell) [] | UpdateHeader of columIndex: int * CompositeHeader @@ -88,11 +87,18 @@ type Msg = | DeleteRows of int [] | DeleteColumn of int | CopySelectedCell +| CopySelectedCells | CutSelectedCell | PasteSelectedCell +| PasteSelectedCells | CopyCell of index:(int*int) +| CopyCells of indices:(int*int) [] | CutCell of index:(int*int) | PasteCell of index:(int*int) +/// This Msg will paste all cell from clipboard into column starting from index. It will extend the table if necessary. +| PasteCellsExtend of index:(int*int) +| Clear of index:(int*int) [] +| ClearSelected | FillColumnWithTerm of index:(int*int) // /// Update column of index to new column type defined by given SwateCell.emptyXXX // | EditColumn of index: int * newType: SwateCell * b_type: BuildingBlockType option diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index d5d738e9..e4826453 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -17,6 +17,7 @@ open ARCtrl.ISA open ARCtrl.ISA.Spreadsheet open Spreadsheet.Sidebar.Controller + module Spreadsheet = module Helper = @@ -66,6 +67,8 @@ module Spreadsheet = let innerUpdate (state: Spreadsheet.Model) (model: Messages.Model) (msg: Spreadsheet.Msg) = match msg with + | UpdateState nextState -> + nextState, model, Cmd.none | CreateAnnotationTable usePrevOutput -> let nextState = Controller.createTable usePrevOutput state nextState, model, Cmd.none @@ -156,15 +159,35 @@ module Spreadsheet = let nextState = {state with SelectedCells = nextSelectedCells} nextState, model, Cmd.none | CopyCell index -> - let nextState = Controller.copyCell index state - nextState, model, Cmd.none + let cmd = + Cmd.OfPromise.attempt + (Controller.copyCellByIndex index) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | CopyCells indices -> + let cmd = + Cmd.OfPromise.attempt + (Controller.copyCellsByIndex indices) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd | CopySelectedCell -> - let nextState = - if state.SelectedCells.IsEmpty then state else - Controller.copySelectedCell state - nextState, model, Cmd.none + let cmd = + Cmd.OfPromise.attempt + (Controller.copySelectedCell) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | CopySelectedCells -> + let cmd = + Cmd.OfPromise.attempt + (Controller.copySelectedCells) + state + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd | CutCell index -> - let nextState = Controller.cutCell index state + let nextState = Controller.cutCellByIndex index state nextState, model, Cmd.none | CutSelectedCell -> let nextState = @@ -172,12 +195,43 @@ module Spreadsheet = Controller.cutSelectedCell state nextState, model, Cmd.none | PasteCell index -> - let nextState = if state.Clipboard.Cell.IsNone then state else Controller.pasteCell index state - nextState, model, Cmd.none + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellByIndex index) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | PasteCellsExtend index -> + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellsByIndexExtend index) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd | PasteSelectedCell -> - let nextState = - if state.SelectedCells.IsEmpty || state.Clipboard.Cell.IsNone then state else - Controller.pasteSelectedCell state + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellIntoSelected) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | PasteSelectedCells -> + let cmd = + Cmd.OfPromise.either + (Clipboard.Controller.pasteCellsIntoSelected) + state + (UpdateState >> SpreadsheetMsg) + (curry GenericError Cmd.none >> DevMsg) + state, model, cmd + | Clear indices -> + let nextState = Controller.clearCells indices state + nextState, model, Cmd.none + | ClearSelected -> + let indices = state.SelectedCells |> Set.toArray + let nextState = Controller.clearCells indices state nextState, model, Cmd.none | FillColumnWithTerm index -> let nextState = Controller.fillColumnWithCell index state diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index a84cb3b5..55e2b7fd 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -99,7 +99,41 @@ module Extensions = | CompositeHeader.Factor oa -> Some oa | _ -> None + let internal tryFromContent' (content: string []) = + match content with + | [|freetext|] -> CompositeCell.createFreeText freetext |> Ok + | [|name; tsr; tan|] -> CompositeCell.createTermFromString(name, tsr, tan) |> Ok + | [|value; name; tsr; tan|] -> CompositeCell.createUnitizedFromString(value, name, tsr, tan) |> Ok + | anyElse -> sprintf "Unable to convert \"%A\" to CompositeCell." anyElse |> Error + type CompositeCell with + + static member tryFromContent (content: string []) = + match tryFromContent' content with + | Ok r -> Some r + | Error _ -> None + + static member fromContent (content: string []) = + match tryFromContent' content with + | Ok r -> r + | Error msg -> raise (exn msg) + + member this.ToTabStr() = this.GetContent() |> String.concat "\t" + + static member fromTabStr (str:string) = + let content = str.Split('\t', System.StringSplitOptions.TrimEntries) + CompositeCell.fromContent content + + static member ToTabTxt (cells: CompositeCell []) = + cells + |> Array.map (fun c -> c.ToTabStr()) + |> String.concat (System.Environment.NewLine) + + static member fromTabTxt (tabTxt: string) = + let lines = tabTxt.Split(System.Environment.NewLine, System.StringSplitOptions.TrimEntries) + let cells = lines |> Array.map (fun line -> CompositeCell.fromTabStr line) + cells + member this.UpdateWithOA(oa:OntologyAnnotation) = match this with | CompositeCell.Term _ -> CompositeCell.createTerm oa From 0cf3093cd1e8d0a25a4150576026fd3a69e1a6a9 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 15 Feb 2024 15:31:45 +0100 Subject: [PATCH 048/135] make cell search handling more robust --- src/Client/MainComponents/Cells.fs | 38 +++++++++----- .../Pages/BuildingBlock/BuildingBlockView.fs | 2 + .../SharedComponents/TermSearchInput.fs | 49 +++++++++++++++---- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 2fd5d69e..a33d43b1 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -318,6 +318,29 @@ type Cell = ] ]] + [] + static member private SearchCellSubcomponent(setter, headerOA: OntologyAnnotation option, cell: CompositeCell, makeIdle, cellState: CellState) = + let oa = cell.ToOA() + let onBlur = fun e -> makeIdle() + let onEscape = fun e -> makeIdle() + let onEnter = fun e -> makeIdle() + let nosearch, setNoSearch= React.useState(true) + Bulma.field.div [ + prop.style [style.flexGrow 1] + Bulma.field.hasAddons + prop.children [ + Bulma.control.p [ + Bulma.button.a [ + prop.style [style.borderWidth 0] + if nosearch then Bulma.color.hasTextGreyLight + Bulma.button.isInverted + prop.onClick(fun _ -> setNoSearch (not nosearch)) + prop.children [Bulma.icon [Html.i [prop.className "fa-solid fa-magnifying-glass"]]] + ] + ] + ] + ] + [] static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation option -> unit) = let columnIndex, rowIndex = index @@ -346,22 +369,15 @@ type Cell = prop.onMouseDown(fun e -> if state_cell.IsIdle && e.shiftKey then e.preventDefault()) prop.children [ match state_cell.CellMode with - | Active -> - /// Update change to mainState and exit active input. - let updateMainStateTable = fun (s: string) -> - // Only update if changed - if s <> cellValue then - setter s - makeIdle() - CellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue, columnType) - | Search -> + | Active | Search -> + // Update change to mainState and exit active input. if oasetter.IsSome then - let headerOA = state.ActiveTable.Headers.[columnIndex].TryOA() let oa = cell.ToOA() let onBlur = fun e -> makeIdle() let onEscape = fun e -> makeIdle() let onEnter = fun e -> makeIdle() - Components.TermSearch.Input(oasetter.Value, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, onEnter=onEnter, autofocus=true, borderRadius=0, border="unset") + let headerOA = state.ActiveTable.Headers.[columnIndex].TryOA() + Components.TermSearch.Input(oasetter.Value, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, onEnter=onEnter, autofocus=true, borderRadius=0, border="unset", searchableToggle=true) else printfn "No setter for OntologyAnnotation given for table cell term search." | Idle -> diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index d85f9391..f95f3a7a 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -130,6 +130,8 @@ let addUnitToExistingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) ] ] + + let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = div [ OnSubmit (fun e -> e.preventDefault()) diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index e158353c..0d9cb909 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -211,12 +211,31 @@ module TermSearchAux = ] ] ] - + open TermSearchAux open Fable.Core.JsInterop type TermSearch = + static member ToggleSearchContainer (element: ReactElement, ref: IRefValue, searchable: bool, searchableSetter: bool -> unit) = + Bulma.field.div [ + prop.style [style.flexGrow 1] + prop.ref ref + Bulma.field.hasAddons + prop.children [ + element + Bulma.control.p [ + Bulma.button.a [ + prop.style [style.borderWidth 0] + if not searchable then Bulma.color.hasTextGreyLight + Bulma.button.isInverted + prop.onClick(fun _ -> searchableSetter (not searchable)) + prop.children [Bulma.icon [Html.i [prop.className "fa-solid fa-magnifying-glass"]]] + ] + ] + ] + ] + [] static member TermSelectItem (term: TermTypes.Term, setTerm, ?isDirectedSearchResult: bool) = let isDirectedSearchResult = defaultArg isDirectedSearchResult false @@ -272,12 +291,13 @@ type TermSearch = static member Input ( setter: OntologyAnnotation option -> unit, ?input: OntologyAnnotation, ?parent': OntologyAnnotation, - ?debounceSetter: int, + ?debounceSetter: int, ?searchableToggle: bool, ?advancedSearchDispatch: Messages.Msg -> unit, ?portalTermSelectArea: HTMLElement, ?onBlur: Event -> unit, ?onEscape: KeyboardEvent -> unit, ?onEnter: KeyboardEvent -> unit, ?autofocus: bool, ?fullwidth: bool, ?size: IReactProperty, ?isExpanded: bool, ?displayParent: bool, ?borderRadius: int, ?border: string) = + let searchableToggle = defaultArg searchableToggle false let autofocus = defaultArg autofocus false let displayParent = defaultArg displayParent true let isExpanded = defaultArg isExpanded false @@ -286,11 +306,12 @@ type TermSearch = let loading, setLoading = React.useState(false) let state, setState = React.useState(input) let parent, setParent = React.useState(parent') + let searchable, setSearchable= React.useState(not searchableToggle) let searchNameState, setSearchNameState = React.useState(SearchState.init) let searchTreeState, setSearchTreeState = React.useState(SearchState.init) let isSearching, setIsSearching = React.useState(false) let debounceStorage = React.useRef(newDebounceStorage()) - let dsetter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp + let dsetter = fun inp -> if debounceSetter.IsSome then debounce debounceStorage.current "setter_debounce" debounceSetter.Value setter inp else setter inp let ref = React.useElementRef() if onBlur.IsSome then React.useLayoutEffectOnce(fun _ -> ClickOutsideHandler.AddListener (ref, onBlur.Value)) React.useEffect((fun () -> setState input), dependencies=[|box input|]) @@ -317,9 +338,9 @@ type TermSearch = Bulma.control.div [ if isExpanded then Bulma.control.isExpanded if size.IsSome then size.Value - Bulma.control.hasIconsLeft + if searchable then Bulma.control.hasIconsLeft Bulma.control.hasIconsRight - prop.ref ref + if not searchableToggle then prop.ref ref prop.style [ if fullwidth then style.flexGrow 1; ] @@ -338,10 +359,10 @@ type TermSearch = let s : string = e.target?value if s.Trim() = "" && parent.IsSome && parent.Value.TermAccessionShort <> "" then // trigger get all by parent search startSearch(None, false) - allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) + if searchable then allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) elif s.Trim() <> "" then startSearch (Some s, false) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) + if searchable then mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) else () ) @@ -351,7 +372,7 @@ type TermSearch = stopSearch() else startSearch (Some s, true) - mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) + if searchable then mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) ) prop.onKeyDown(fun e -> match e.which with @@ -359,9 +380,12 @@ type TermSearch = if onEscape.IsSome then onEscape.Value e stopSearch() | 13. -> //enter - log "enter" if onEnter.IsSome then onEnter.Value e setter state + | 9. -> //tab + if searchableToggle then + e.preventDefault() + setSearchable (not searchable) | _ -> () ) @@ -372,7 +396,7 @@ type TermSearch = ReactDOM.createPortal(TermSelectArea,portalTermSelectArea.Value) else TermSelectArea - Components.searchIcon + if searchable then Components.searchIcon if state.IsSome && state.Value.Name.IsSome && state.Value.TermAccessionNumber.IsSome && not isSearching then Components.verifiedIcon // Optional elements Html.div [ @@ -398,3 +422,8 @@ type TermSearch = ] ] ] + |> fun main -> + if searchableToggle then + TermSearch.ToggleSearchContainer(main, ref, searchable, setSearchable) + else + main From 36e57f32caf4d041eed3e64b87ade5db1249b2b9 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 15 Feb 2024 16:12:04 +0100 Subject: [PATCH 049/135] fix some cell search issues :bug: --- src/Client/MainComponents/Cells.fs | 58 +++++++------------ .../SharedComponents/TermSearchInput.fs | 16 ++--- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index a33d43b1..2dfdeec9 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -13,7 +13,6 @@ open Components type private CellMode = | Active | Idle -| Search type private CellState = { CellMode: CellMode @@ -32,7 +31,6 @@ type private CellState = { } member this.IsActive = this.CellMode = Active - member this.IsSearch = this.CellMode = Search member this.IsIdle = this.CellMode = Idle type private ColumnType = @@ -111,7 +109,7 @@ module private CellComponents = prop.style [style.borderWidth 0; style.borderRadius 0] prop.onClick(fun e -> e.stopPropagation() - setState_cell {state_cell with CellMode = Search} + //setState_cell {state_cell with CellMode = Search} ) prop.children [ Bulma.icon [Html.i [prop.className "fa-solid fa-search"]] @@ -319,30 +317,7 @@ type Cell = ]] [] - static member private SearchCellSubcomponent(setter, headerOA: OntologyAnnotation option, cell: CompositeCell, makeIdle, cellState: CellState) = - let oa = cell.ToOA() - let onBlur = fun e -> makeIdle() - let onEscape = fun e -> makeIdle() - let onEnter = fun e -> makeIdle() - let nosearch, setNoSearch= React.useState(true) - Bulma.field.div [ - prop.style [style.flexGrow 1] - Bulma.field.hasAddons - prop.children [ - Bulma.control.p [ - Bulma.button.a [ - prop.style [style.borderWidth 0] - if nosearch then Bulma.color.hasTextGreyLight - Bulma.button.isInverted - prop.onClick(fun _ -> setNoSearch (not nosearch)) - prop.children [Bulma.icon [Html.i [prop.className "fa-solid fa-magnifying-glass"]]] - ] - ] - ] - ] - - [] - static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation option -> unit) = + static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation -> unit) = let columnIndex, rowIndex = index let state = model.SpreadsheetModel let state_cell, setState_cell = React.useState(CellState.init(cellValue)) @@ -360,16 +335,15 @@ type Cell = cellInnerContainerStyle [] prop.onDoubleClick(fun e -> e.preventDefault() - e.stopPropagation() - let mode = if (e.ctrlKey || e.metaKey) && columnType = Main then Search else Active - if state_cell.IsIdle then setState_cell {state_cell with CellMode = mode} + e.stopPropagation() + if state_cell.IsIdle then setState_cell {state_cell with CellMode = Active} UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch ) if state_cell.IsIdle then prop.onClick <| EventPresets.onClickSelect(index, state_cell, state.SelectedCells, model, dispatch) prop.onMouseDown(fun e -> if state_cell.IsIdle && e.shiftKey then e.preventDefault()) prop.children [ match state_cell.CellMode with - | Active | Search -> + | Active -> // Update change to mainState and exit active input. if oasetter.IsSome then let oa = cell.ToOA() @@ -377,9 +351,16 @@ type Cell = let onEscape = fun e -> makeIdle() let onEnter = fun e -> makeIdle() let headerOA = state.ActiveTable.Headers.[columnIndex].TryOA() - Components.TermSearch.Input(oasetter.Value, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, onEnter=onEnter, autofocus=true, borderRadius=0, border="unset", searchableToggle=true) + let setter = fun (oa: OntologyAnnotation option) -> + if oa.IsSome then oasetter.Value oa.Value else setter "" + Components.TermSearch.Input(setter, input=oa, fullwidth=true, ?parent'=headerOA, displayParent=false, debounceSetter=1000, onBlur=onBlur, onEscape=onEscape, onEnter=onEnter, autofocus=true, borderRadius=0, border="unset", searchableToggle=true) else - printfn "No setter for OntologyAnnotation given for table cell term search." + let updateMainStateTable = fun (s: string) -> + // Only update if changed + if s <> cellValue then + setter s + makeIdle() + CellInputElement(false, false, updateMainStateTable, setState_cell, state_cell, cellValue, columnType) | Idle -> if columnType = Main then compositeCellDisplay cell @@ -395,10 +376,13 @@ type Cell = let setter = fun (s: string) -> let nextCell = cell.UpdateMainField s Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - let oaSetter = fun (oa:OntologyAnnotation option) -> - let nextCell = oa |> Option.map cell.UpdateWithOA - if nextCell.IsSome then - Msg.UpdateCell (index, nextCell.Value) |> SpreadsheetMsg |> dispatch + let oaSetter = fun (oa:OntologyAnnotation) -> + let nextCell = + if oa.TermSourceREF.IsNone && oa.TermAccessionNumber.IsNone then // update only mainfield, if mainfield is the only field with value + cell.UpdateMainField oa.NameText + else + cell.UpdateWithOA oa + Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch Cell.BodyBase(Main, cellValue, setter, index, cell, model, dispatch, oaSetter) static member BodyUnit(index: (int*int), cell: CompositeCell, model: Model, dispatch) = diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 0d9cb909..e4b9636c 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -226,7 +226,7 @@ type TermSearch = element Bulma.control.p [ Bulma.button.a [ - prop.style [style.borderWidth 0] + prop.style [style.borderWidth 0; style.borderRadius 0] if not searchable then Bulma.color.hasTextGreyLight Bulma.button.isInverted prop.onClick(fun _ -> searchableSetter (not searchable)) @@ -291,7 +291,7 @@ type TermSearch = static member Input ( setter: OntologyAnnotation option -> unit, ?input: OntologyAnnotation, ?parent': OntologyAnnotation, - ?debounceSetter: int, ?searchableToggle: bool, + ?debounceSetter: int, ?searchableToggle: bool, ?advancedSearchDispatch: Messages.Msg -> unit, ?portalTermSelectArea: HTMLElement, ?onBlur: Event -> unit, ?onEscape: KeyboardEvent -> unit, ?onEnter: KeyboardEvent -> unit, @@ -306,7 +306,7 @@ type TermSearch = let loading, setLoading = React.useState(false) let state, setState = React.useState(input) let parent, setParent = React.useState(parent') - let searchable, setSearchable= React.useState(not searchableToggle) + let searchable, setSearchable = React.useState(not searchableToggle) let searchNameState, setSearchNameState = React.useState(SearchState.init) let searchTreeState, setSearchTreeState = React.useState(SearchState.init) let isSearching, setIsSearching = React.useState(false) @@ -338,7 +338,7 @@ type TermSearch = Bulma.control.div [ if isExpanded then Bulma.control.isExpanded if size.IsSome then size.Value - if searchable then Bulma.control.hasIconsLeft + if not searchableToggle then Bulma.control.hasIconsLeft Bulma.control.hasIconsRight if not searchableToggle then prop.ref ref prop.style [ @@ -359,10 +359,10 @@ type TermSearch = let s : string = e.target?value if s.Trim() = "" && parent.IsSome && parent.Value.TermAccessionShort <> "" then // trigger get all by parent search startSearch(None, false) - if searchable then allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) + if searchable then allByParentSearch(parent.Value, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) else stopSearch() elif s.Trim() <> "" then startSearch (Some s, false) - if searchable then mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) + if searchable then mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 0) else stopSearch() else () ) @@ -372,7 +372,7 @@ type TermSearch = stopSearch() else startSearch (Some s, true) - if searchable then mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) + if searchable then mainSearch(s, parent, setSearchNameState, setSearchTreeState, setLoading, stopSearch, debounceStorage.current, 1000) else stopSearch() ) prop.onKeyDown(fun e -> match e.which with @@ -396,7 +396,7 @@ type TermSearch = ReactDOM.createPortal(TermSelectArea,portalTermSelectArea.Value) else TermSelectArea - if searchable then Components.searchIcon + if not searchableToggle then Components.searchIcon if state.IsSome && state.Value.Name.IsSome && state.Value.TermAccessionNumber.IsSome && not isSearching then Components.verifiedIcon // Optional elements Html.div [ From 0260a81aa674b07b73f48a2e9b27067ef8dd9b60 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 15 Feb 2024 16:39:20 +0100 Subject: [PATCH 050/135] Start working on keyboard shortcuts --- src/Client/Client.fs | 8 +++++++ src/Client/Host.fs | 8 ++++--- .../MainComponents/KeyboardShortcuts.fs | 24 ++++++++++++++----- src/Client/Update/InterfaceUpdate.fs | 9 ++----- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/Client/Client.fs b/src/Client/Client.fs index 9d280b5f..8da6057b 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -52,10 +52,18 @@ let ARCitect_subscription (initial: Messages.Model) : (SubId * Subscribe unit) : System.IDisposable = + let rmv = Spreadsheet.KeyboardShortcuts.initEventListener dispatch + { new System.IDisposable with + member _.Dispose() = rmv() + } [ // Only subscribe to ARCitect messages when host is set correctly via query param. if initial.PersistentStorageState.Host = Some (Swatehost.ARCitect) then ["ARCitect"], subscription + //https://elmish.github.io/elmish/docs/subscription.html#aggregating-multiple-subscribers + //if initial.PersistentStorageState.Host.IsSome && initial.PersistentStorageState.Host.Value.IsStandalone then + // ["KeyboardShortCuts"], keyboardEventSubscription ] #if DEBUG diff --git a/src/Client/Host.fs b/src/Client/Host.fs index 3d84d091..33fd146f 100644 --- a/src/Client/Host.fs +++ b/src/Client/Host.fs @@ -5,11 +5,13 @@ module Host type Swatehost = | Browser | Excel -| ARCitect //WIP - +| ARCitect with static member ofQueryParam (queryInteger: int option) = match queryInteger with | Some 1 -> Swatehost.ARCitect | Some 2 -> Swatehost.Excel - | _ -> Browser \ No newline at end of file + | _ -> Browser + + member this.IsStandalone = + this = Swatehost.Browser || this = Swatehost.ARCitect \ No newline at end of file diff --git a/src/Client/MainComponents/KeyboardShortcuts.fs b/src/Client/MainComponents/KeyboardShortcuts.fs index 3ffeaf41..77f2f419 100644 --- a/src/Client/MainComponents/KeyboardShortcuts.fs +++ b/src/Client/MainComponents/KeyboardShortcuts.fs @@ -5,22 +5,34 @@ let private onKeydownEvent (dispatch: Messages.Msg -> unit) = //e.preventDefault() //e.stopPropagation() let e = e :?> Browser.Types.KeyboardEvent + log "KEY DOWN" match e.ctrlKey, e.which with | false, _ -> () // Ctrl + c | _, _ -> - match e.ctrlKey, e.which with + match (e.ctrlKey || e.metaKey), e.which with + // Ctrl + c | true, 67. -> + log 67 Spreadsheet.CopySelectedCell |> Messages.SpreadsheetMsg |> dispatch - // Ctrl + c + // Ctrl + x | true, 88. -> + log 88 Spreadsheet.CutSelectedCell |> Messages.SpreadsheetMsg |> dispatch // Ctrl + v | true, 86. -> + log 86 Spreadsheet.PasteSelectedCell |> Messages.SpreadsheetMsg |> dispatch + | _, 46. -> // del + Spreadsheet.ClearSelected |> Messages.SpreadsheetMsg |> dispatch | _, _ -> () -///These events only get reapplied on reload, not during hot reload -let addOnKeydownEvent dispatch = - Browser.Dom.document.body.removeEventListener("keydown", onKeydownEvent dispatch) - Browser.Dom.document.body.addEventListener("keydown", onKeydownEvent dispatch) +/// +/// Returns a function to remove the event listener +/// +/// +let initEventListener (dispatch) : unit -> unit = + log "INIT" + let handle = fun (e: Browser.Types.Event) -> onKeydownEvent dispatch e + Browser.Dom.window.addEventListener("message", handle) + fun () -> Browser.Dom.window.removeEventListener("keydown", handle) diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 06b90743..6defe0a3 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -39,14 +39,9 @@ module Interface = (OfficeInterop.AnnotationTableExists >> OfficeInteropMsg) (curry GenericError Cmd.none >> DevMsg) | Swatehost.Browser -> - Cmd.batch [ - Cmd.ofEffect (fun dispatch -> Spreadsheet.KeyboardShortcuts.addOnKeydownEvent dispatch) - ] + Cmd.none | Swatehost.ARCitect -> - Cmd.batch [ - Cmd.ofEffect (fun dispatch -> Spreadsheet.KeyboardShortcuts.addOnKeydownEvent dispatch) - Cmd.ofEffect (fun _ -> ARCitect.ARCitect.send ARCitect.Init) - ] + Cmd.ofEffect (fun _ -> ARCitect.ARCitect.send ARCitect.Init) ] model, cmd | CreateAnnotationTable usePrevOutput -> From 1e1156b5455cfa2a9fbbf8a3dab180cb16821ac1 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 15 Feb 2024 16:41:47 +0100 Subject: [PATCH 051/135] Release v1.0.0-alpha.11 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index fd0a7287..dd40af05 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha.10" - let [] AssemblyMetadata_ReleaseDate = "13.02.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.11" + let [] AssemblyMetadata_ReleaseDate = "15.02.2024" From 9b2c35be0636a636e51ba09bbb83b71cf127fb81 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 16 Feb 2024 10:26:01 +0100 Subject: [PATCH 052/135] Fix search view width :bug: #370 --- src/Client/SharedComponents/TermSearchInput.fs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index e4b9636c..4c60646a 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -219,7 +219,7 @@ type TermSearch = static member ToggleSearchContainer (element: ReactElement, ref: IRefValue, searchable: bool, searchableSetter: bool -> unit) = Bulma.field.div [ - prop.style [style.flexGrow 1] + prop.style [style.flexGrow 1; style.position.relative] prop.ref ref Bulma.field.hasAddons prop.children [ @@ -343,6 +343,7 @@ type TermSearch = if not searchableToggle then prop.ref ref prop.style [ if fullwidth then style.flexGrow 1; + style.minWidth 400 ] if loading then Bulma.control.isLoading prop.children [ @@ -394,6 +395,8 @@ type TermSearch = TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching) if portalTermSelectArea.IsSome then ReactDOM.createPortal(TermSelectArea,portalTermSelectArea.Value) + elif ref.current.IsSome then + ReactDOM.createPortal(TermSelectArea,ref.current.Value) else TermSelectArea if not searchableToggle then Components.searchIcon From aff241bcc1f7aed2244d0ba249d25069e7cdee10 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 16 Feb 2024 10:31:46 +0100 Subject: [PATCH 053/135] make external url open in new window/tab #371 --- src/Client/Pages/JsonExporter/JsonExporter.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index 3071a72d..52ba75cf 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -533,7 +533,7 @@ type FileExporter = mainFunctionContainer [ Bulma.field.div [Bulma.help [ str "Export Swate annotation tables to official ISA-JSON (" - a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"] [str "more"] + a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"; Target "_Blank"] [str "more"] str ")." ]] FileExporter.JsonExport(model, dispatch) From 63efcbea05dae642b2e85820b726a9a291521f6c Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 16 Feb 2024 10:32:51 +0100 Subject: [PATCH 054/135] Release v1.0.0-alpha.12 :bookmark: --- src/Server/Version.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Server/Version.fs b/src/Server/Version.fs index dd40af05..163844ba 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-alpha.11" - let [] AssemblyMetadata_ReleaseDate = "15.02.2024" + let [] AssemblyMetadata_Version = "v1.0.0-alpha.12" + let [] AssemblyMetadata_ReleaseDate = "16.02.2024" From 69e0f45bba83ee348abdedf5992743d43c9a77c6 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 16 Feb 2024 13:39:55 +0100 Subject: [PATCH 055/135] rmv ARCitect specific navabr buttons :fire: --- src/Client/ARCitect/ARCitect.fs | 8 +++++++- src/Client/MainComponents/Navbar.fs | 28 +++----------------------- src/Client/States/ARCitect.fs | 6 ++++-- src/Client/States/LocalHistory.fs | 2 -- src/Client/Update/SpreadsheetUpdate.fs | 2 ++ 5 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/Client/ARCitect/ARCitect.fs b/src/Client/ARCitect/ARCitect.fs index fd09db01..9fd929ce 100644 --- a/src/Client/ARCitect/ARCitect.fs +++ b/src/Client/ARCitect/ARCitect.fs @@ -22,6 +22,9 @@ let send (msg:ARCitect.Msg) = | StudyToARCitect study -> let json = ArcStudy.toArcJsonString study json + | InvestigationToARCitect inv -> + let json = ArcInvestigation.toArcJsonString inv + json | Error exn -> exn postMessageToARCitect(msg, data) @@ -36,7 +39,10 @@ let EventHandler (dispatch: Messages.Msg -> unit) : IEventHandler = let study = ArcStudy.fromArcJsonString data.ArcStudyJsonString Spreadsheet.InitFromArcFile (ArcFiles.Study (study, [])) |> SpreadsheetMsg |> dispatch log($"Received Study {study.Identifier} from ARCitect!") - Browser.Dom.console.log(study) + InvestigationToSwate = fun data -> + let inv = ArcInvestigation.fromArcJsonString data.ArcInvestigationJsonString + Spreadsheet.InitFromArcFile (ArcFiles.Investigation inv) |> SpreadsheetMsg |> dispatch + log($"Received Investigation {inv.Title} from ARCitect!") Error = fun exn -> GenericError (Cmd.none, exn) |> DevMsg |> dispatch } \ No newline at end of file diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index a4c7b412..9ad2fd89 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -149,34 +149,12 @@ let Main (model: Messages.Model) dispatch = prop.ariaLabel "menu" prop.children [ match model.PersistentStorageState.Host with - | Some Swatehost.ARCitect -> + | Some (Swatehost.ARCitect) -> Bulma.navbarStart.div [ prop.style [style.display.flex; style.alignItems.stretch; style.justifyContent.flexStart; style.custom("marginRight", "auto")] prop.children [ - Html.div [ - prop.style [ - style.display.flex; style.flexDirection.row - ] - prop.children [ - QuickAccessButton.create( - "Return to ARCitect", - [ - Bulma.icon [Html.i [prop.className "fa-solid fa-circle-left";]] - ], - (fun _ -> ARCitect.ARCitect.send Model.ARCitect.TriggerSwateClose) - ).toReactElement() - QuickAccessButton.create( - "Alpha State", - [ - Html.span "ALPHA STATE" - ], - (fun e -> ()), - false - ).toReactElement() - quickAccessButtonListStart (model.History: LocalHistory.Model) dispatch - if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, addWidget) - ] - ] + quickAccessButtonListStart model.History dispatch + if model.SpreadsheetModel.TableViewIsActive() then WidgetNavbarList(model, dispatch, addWidget) ] ] | Some _ -> diff --git a/src/Client/States/ARCitect.fs b/src/Client/States/ARCitect.fs index 54314cae..719420ce 100644 --- a/src/Client/States/ARCitect.fs +++ b/src/Client/States/ARCitect.fs @@ -7,10 +7,12 @@ type Msg = | Error of exn | AssayToARCitect of ArcAssay | StudyToARCitect of ArcStudy + | InvestigationToARCitect of ArcInvestigation | TriggerSwateClose type IEventHandler = { Error: exn -> unit - AssayToSwate: {| ArcAssayJsonString: string |} -> unit - StudyToSwate: {| ArcStudyJsonString: string |} -> unit + AssayToSwate : {| ArcAssayJsonString: string |} -> unit + StudyToSwate : {| ArcStudyJsonString: string |} -> unit + InvestigationToSwate : {| ArcInvestigationJsonString: string |} -> unit } \ No newline at end of file diff --git a/src/Client/States/LocalHistory.fs b/src/Client/States/LocalHistory.fs index d556a493..f31735ae 100644 --- a/src/Client/States/LocalHistory.fs +++ b/src/Client/States/LocalHistory.fs @@ -197,7 +197,6 @@ type Model = // if e.g at position 4 and we create new table state from position 4 we want to delete position 0 .. 3 and use 4 as new 0 let rebranchedList, toRemoveList1 = if this.HistoryCurrentPosition <> 0 then - printfn "[HISTORY] Rebranch to %i" this.HistoryCurrentPosition this.HistoryOrder |> List.splitAt this.HistoryCurrentPosition |> fun (remove, keep) -> keep, remove @@ -220,7 +219,6 @@ type Model = Browser.WebStorage.sessionStorage.setItem(Keys.swate_session_history_key, HistoryOrder.toJson(nextState.HistoryOrder)) // reset new table position to 0 Browser.WebStorage.sessionStorage.setItem(Keys.swate_session_history_position, "0") - printfn "[HISTORY] length: %i" nextState.HistoryOrder.Length nextState member this.ResetAll() = diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index e4826453..5c55506b 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -47,6 +47,8 @@ module Spreadsheet = ARCitect.ARCitect.send(ARCitect.AssayToARCitect assay) | Some (Study (study,_)) -> ARCitect.ARCitect.send(ARCitect.StudyToARCitect study) + | Some (Investigation inv) -> + ARCitect.ARCitect.send(ARCitect.InvestigationToARCitect inv) | _ -> () state, {model with History = nextHistory}, cmd From 1e07511f22b39c595a5fc4c18e7ffdb9569ecfb5 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 20 Feb 2024 15:10:35 +0100 Subject: [PATCH 056/135] add background color to table header cells #373 --- src/Client/MainComponents/Cells.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 2dfdeec9..97f924ca 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -248,6 +248,7 @@ type Cell = prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}-{columnType}" prop.id $"Header_{columnIndex}_{columnType}" cellStyle [] + Bulma.color.hasBackgroundWhite prop.children [ Html.div [ cellInnerContainerStyle [] From ec30ee2fa70e8437e5f4e538cdedf75121e77036 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 20 Feb 2024 15:20:50 +0100 Subject: [PATCH 057/135] fix style for darkmode :bug::art: --- src/Client/MainComponents/Cells.fs | 4 ++-- src/Client/style.scss | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 97f924ca..fdaf9db2 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -248,10 +248,10 @@ type Cell = prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}-{columnType}" prop.id $"Header_{columnIndex}_{columnType}" cellStyle [] - Bulma.color.hasBackgroundWhite + prop.className "main-contrast-bg" prop.children [ Html.div [ - cellInnerContainerStyle [] + cellInnerContainerStyle [style.custom("backgroundColor","inherit")] if not isReadOnly then prop.onDoubleClick(fun e -> e.preventDefault() e.stopPropagation() diff --git a/src/Client/style.scss b/src/Client/style.scss index 37a037d5..5271a0d0 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -69,6 +69,10 @@ $colors: map-merge($colors, $addColors); @import "../../node_modules/bulma/bulma.sass"; /*@import "../../node_modules/bulma-checkradio/src/sass/index.sass";*/ +.main-contrast-bg { + background-color: var(--scheme-main) +} + .template-filter-container { flex-grow: 1; display: flex; From 3cfb5da337046bde8aa2dc818a192508618a35a7 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 20 Feb 2024 15:52:32 +0100 Subject: [PATCH 058/135] Fix trimming away tab separation for empty strings #376 --- src/Client/Spreadsheet/Clipboard.Controller.fs | 2 +- src/Shared/ARCtrl.Helper.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/Spreadsheet/Clipboard.Controller.fs b/src/Client/Spreadsheet/Clipboard.Controller.fs index b3280bb0..bdbdfa03 100644 --- a/src/Client/Spreadsheet/Clipboard.Controller.fs +++ b/src/Client/Spreadsheet/Clipboard.Controller.fs @@ -48,7 +48,7 @@ let pasteCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise Array.head - state.ActiveTable.UpdateCellAt(fst index, snd index, cell) + state.ActiveTable.SetCellAt(fst index, snd index, cell) return state } diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 55e2b7fd..dabbfda2 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -130,7 +130,7 @@ module Extensions = |> String.concat (System.Environment.NewLine) static member fromTabTxt (tabTxt: string) = - let lines = tabTxt.Split(System.Environment.NewLine, System.StringSplitOptions.TrimEntries) + let lines = tabTxt.Split(System.Environment.NewLine, System.StringSplitOptions.None) let cells = lines |> Array.map (fun line -> CompositeCell.fromTabStr line) cells From 52e1cbcdd8e1ac22495da9ecf86302319cee2d82 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 20 Feb 2024 16:13:13 +0100 Subject: [PATCH 059/135] add automatic conversion for copy/paste should cell type not fit column type #379 --- src/Client/Spreadsheet/Clipboard.Controller.fs | 9 ++++++--- src/Shared/ARCtrl.Helper.fs | 16 +++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Client/Spreadsheet/Clipboard.Controller.fs b/src/Client/Spreadsheet/Clipboard.Controller.fs index bdbdfa03..fb365062 100644 --- a/src/Client/Spreadsheet/Clipboard.Controller.fs +++ b/src/Client/Spreadsheet/Clipboard.Controller.fs @@ -47,7 +47,8 @@ let cutSelectedCell (state: Spreadsheet.Model) : Spreadsheet.Model = let pasteCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise = promise { let! tab = navigator.clipboard.readText() - let cell = CompositeCell.fromTabTxt tab |> Array.head + let header = state.ActiveTable.Headers.[fst index] + let cell = CompositeCell.fromTabTxt tab |> Array.head |> _.ConvertToValidCell(header) state.ActiveTable.SetCellAt(fst index, snd index, cell) return state } @@ -55,7 +56,8 @@ let pasteCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise = promise { let! tab = navigator.clipboard.readText() - let cells = CompositeCell.fromTabTxt tab + let header = state.ActiveTable.Headers.[fst index] + let cells = CompositeCell.fromTabTxt tab |> Array.map _.ConvertToValidCell(header) let columnIndex, rowIndex = fst index, snd index let indexedCells = cells |> Array.indexed |> Array.map (fun (i,c) -> (columnIndex, rowIndex + i), c) state.ActiveTable.SetCellsAt indexedCells @@ -77,7 +79,8 @@ let pasteCellsIntoSelected (state: Spreadsheet.Model) : JS.Promise Set.filter (fun index -> fst index = columnIndex) promise { let! tab = navigator.clipboard.readText() - let cells = CompositeCell.fromTabTxt tab + let header = state.ActiveTable.Headers.[columnIndex] + let cells = CompositeCell.fromTabTxt tab |> Array.map _.ConvertToValidCell(header) let rowCount = selectedSingleColumnCells.Count let cellsTrimmed = cells |> takeFromArray rowCount let indicesTrimmed = (Set.toArray selectedSingleColumnCells).[0..cellsTrimmed.Length-1] diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index dabbfda2..f44bd1fc 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -47,15 +47,6 @@ module Extensions = with get() = this.Name.Replace(" ","_") + ".xlsx" type CompositeHeader with - member this.AsButtonName = - match this with - | CompositeHeader.Parameter _ -> "Parameter" - | CompositeHeader.Characteristic _ -> "Characteristic" - | CompositeHeader.Component _ -> "Component" - | CompositeHeader.Factor _ -> "Factor" - | CompositeHeader.Input _ -> "Input" - | CompositeHeader.Output _ -> "Output" - | anyElse -> anyElse.ToString() member this.UpdateWithOA (oa: OntologyAnnotation) = match this with @@ -134,6 +125,13 @@ module Extensions = let cells = lines |> Array.map (fun line -> CompositeCell.fromTabStr line) cells + member this.ConvertToValidCell (header: CompositeHeader) = + match header.IsTermColumn, this with + | true, CompositeCell.Term _ | true, CompositeCell.Unitized _ -> this + | true, CompositeCell.FreeText txt -> this.ToTermCell() + | false, CompositeCell.Term _ | false, CompositeCell.Unitized _ -> this.ToFreeTextCell() + | false, CompositeCell.FreeText _ -> this + member this.UpdateWithOA(oa:OntologyAnnotation) = match this with | CompositeCell.Term _ -> CompositeCell.createTerm oa From 86b0da4d2e2a95a4b866b4246f427e14e7ac366d Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 20 Feb 2024 16:16:32 +0100 Subject: [PATCH 060/135] increase scrollbar sizes! #378 --- src/Client/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/index.html b/src/Client/index.html index c550747b..e31ef3ab 100644 --- a/src/Client/index.html +++ b/src/Client/index.html @@ -15,8 +15,8 @@ """) |> ignore - // //newScript.AppendLine("""""") |> ignore - // //newScript.AppendLine("""""") |> ignore - // newScript.AppendLine("""
""") |> ignore - // newScript.AppendLine("") |> ignore - // newScript.ToString() - - ///// Converts a CyGraph to it HTML representation. The div layer has a default size of 600 if not specified otherwise. - //let toCytoHTML (cy:Cytoscape) = - // let guid = Guid.NewGuid().ToString() - // let id = sprintf "e%s" <| Guid.NewGuid().ToString().Replace("-","").Substring(0,10) - // cy.container <- PlainJsonString id - - // let userZoomingEnabled = - // match cy.TryGetTypedValue "zoom" with - // | Some z -> - // match z.TryGetTypedValue "zoomingEnabled" with - // | Some t -> t - // | None -> false - // | None -> false - // |> string - // |> fun s -> s.ToLower() - - // let strCANVAS = // DynamicObj.DynObj.tryGetValue cy "Dims" //tryGetLayoutSize gChart - // match cy.TryGetTypedValue "Canvas" with - // |Some c -> c - // |None -> Canvas.InitDefault() - // //|> fun c -> c?display <- "block" ; c - // |> fun c -> - // c.GetProperties(true) - // |> Seq.map (fun k -> sprintf "%s: %O" k.Key k.Value) - // |> String.concat "; " - // |> sprintf "{ %s }" - - // DynamicObj.DynObj.remove cy "Canvas" - - // /// Create asp.net core able settings - // let settings = - // let converter = PlainJsonStringConverter() :> JsonConverter - // let l = System.Collections.Immutable.ImmutableList.Create(converter) - // let n = new JsonSerializerSettings() - // n.ReferenceLoopHandling <- ReferenceLoopHandling.Serialize - // n.Converters <- l - // n - - // let jsonGraph = JsonConvert.SerializeObject (cy,settings) - - // let html = - // graphDoc - // .Replace("[CANVAS]", strCANVAS) - // .Replace("[ID]", id) - // .Replace("[ZOOMING]", userZoomingEnabled) - // .Replace("[SCRIPTID]", guid.Replace("-","")) - // .Replace("[GRAPHDATA]", jsonGraph) - // html - - ///// Converts a CyGraph to it HTML representation and embeds it into a html page. - //let toEmbeddedHTML (cy:Cytoscape) = - // let graph = - // toCytoHTML cy - // doc - // .Replace("[GRAPH]", graph) - // //.Replace("[DESCRIPTION]", "") diff --git a/src/Server/Database/Template.fs b/src/Server/Database/Template.fs index abea0e62..ecec9883 100644 --- a/src/Server/Database/Template.fs +++ b/src/Server/Database/Template.fs @@ -4,8 +4,6 @@ open Neo4j.Driver open System open Helper - -open ISADotNet open Newtonsoft.Json //type Author = { diff --git a/src/Server/Export.fs b/src/Server/Export.fs deleted file mode 100644 index f893d81c..00000000 --- a/src/Server/Export.fs +++ /dev/null @@ -1,68 +0,0 @@ -module Export - -open System -open ISADotNet -open Shared.OfficeInteropTypes - -//type Column with -// member this.toMatrixElement() = -// let header = this.Header.SwateColumnHeader -// this.Cells -// |> Array.map (fun cell -> -// (cell.Index, header), Option.defaultValue "" cell.Value -// ) -// member this.toMatrixElement(rebaseIndex:int) = -// let header = this.Header.SwateColumnHeader -// this.Cells -// |> Array.map (fun cell -> -// (cell.Index-rebaseIndex, header), Option.defaultValue "" cell.Value -// ) - -//let parseBuildingBlockToMatrix (buildingBlocks:BuildingBlock []) = -// let matrixHeaders = -// buildingBlocks -// |> Array.collect (fun bb -> [| -// bb.MainColumn.Header.SwateColumnHeader -// if bb.hasUnit then -// bb.Unit.Value.Header.SwateColumnHeader -// if bb.hasCompleteTSRTAN then -// bb.TSR.Value.Header.SwateColumnHeader -// bb.TAN.Value.Header.SwateColumnHeader -// |]) -// let rebaseindex = -// let getCellIndices (cellArr:Cell []) = cellArr |> Array.map (fun c -> c.Index) -// buildingBlocks -// |> Array.collect (fun bb -> [| -// yield! bb.MainColumn.Cells |> getCellIndices -// if bb.hasUnit then -// yield! bb.Unit.Value.Cells |> getCellIndices -// if bb.hasCompleteTSRTAN then -// yield! bb.TSR.Value.Cells |> getCellIndices -// yield! bb.TAN.Value.Cells |> getCellIndices -// |]) |> Array.min -// let matrixArr = -// buildingBlocks -// |> Array.collect (fun bb -> [| -// yield! bb.MainColumn.toMatrixElement(rebaseindex) -// if bb.hasUnit then -// yield! bb.Unit.Value.toMatrixElement(rebaseindex) -// if bb.hasCompleteTSRTAN then -// yield! bb.TSR.Value.toMatrixElement(rebaseindex) -// yield! bb.TAN.Value.toMatrixElement(rebaseindex) -// |]) -// let matrix = Collections.Generic.Dictionary<(int*string),string>(Map.ofArray matrixArr) -// matrixHeaders, matrix - -//let parseBuildingBlockToAssay (templateName:string) (buildingBlocks:BuildingBlock []) = -// let matrixHeaders, matrix = parseBuildingBlockToMatrix buildingBlocks -// //printfn "%A" matrixHeaders // contains "Component [instrument model]" -// ISADotNet.XLSX.AssayFile.Assay.fromSparseMatrix templateName matrixHeaders matrix - -//let parseBuildingBlockSeqsToAssay (worksheetNameBuildingBlocks: (string*BuildingBlock []) []) = -// let matrices = -// worksheetNameBuildingBlocks -// |> Array.map (fun (templateName, buildingBlocks) -> -// let matrixHeaders, matrix = parseBuildingBlockToMatrix buildingBlocks -// templateName, Seq.ofArray matrixHeaders, matrix -// ) -// ISADotNet.XLSX.AssayFile.Assay.fromSparseMatrices matrices diff --git a/src/Server/ISADotNet.fs b/src/Server/ISADotNet.fs deleted file mode 100644 index 6a088f6f..00000000 --- a/src/Server/ISADotNet.fs +++ /dev/null @@ -1,282 +0,0 @@ -module ISADotNet - -//open Shared -//open OfficeInteropTypes -//open TermTypes -//open ISADotNet -//open ISADotNet.Json - -//module Assay = - -// open FsSpreadsheet.ExcelIO - -// /// Reads an assay from an xlsx spreadsheetdocument -// /// -// /// As factors and protocols are used for the investigation file, they are returned individually -// /// -// /// The persons from the metadata sheet are returned independently as they are not a part of the assay object -// let fromTemplateSpreadsheet (doc:DocumentFormat.OpenXml.Packaging.SpreadsheetDocument,tableName:string) = - -// let sst = Spreadsheet.tryGetSharedStringTable doc - -// // All sheetnames in the spreadsheetDocument -// let sheetNames = -// Spreadsheet.getWorkbookPart doc -// |> Workbook.get -// |> Sheet.Sheets.get -// |> Sheet.Sheets.getSheets -// |> Seq.map Sheet.getName - -// let assay = -// sheetNames -// |> Seq.tryPick (fun sheetName -> -// Spreadsheet.tryGetWorksheetPartBySheetName sheetName doc -// |> Option.bind (fun wsp -> -// Table.tryGetByNameBy (fun s -> s = tableName) wsp -// |> Option.map (fun table -> -// let sheet = Worksheet.getSheetData wsp.Worksheet -// let headers = Table.getColumnHeaders table -// let m = Table.toSparseValueMatrix sst sheet table -// XLSX.AssayFile.Process.fromSparseMatrix sheetName headers m -// |> fun ps -> Assay.create(ProcessSequence = ps) -// ) -// ) -// ) -// assay - - -///// Only use this function for protocol templates from db -//let rowMajorOfTemplateJson jsonString = -// let assay = Assay.fromString jsonString -// let qAssay = QueryModel.QAssay.fromAssay assay -// if qAssay.Sheets.Length <> 1 then -// failwith "Swate was unable to identify the information from the requested template (). Please open an issue for the developers." -// let template = qAssay.Sheets.Head -// template //QAssay - -//let private ColumnPositionCommentName = "ValueIndex" - -//type OntologyAnnotation with -// member this.toTermMinimal = -// match this.Name, Option.bind Regex.parseTermAccession this.TermAccessionNumber with -// | Some name, Some tan -> TermMinimal.create (AnnotationValue.toString name) tan |> Some -// | Some name, None -> TermMinimal.create (AnnotationValue.toString name) "" |> Some -// | _,_ -> None - -//type ISADotNet.Value with -// member this.toTermMinimal = -// let nameOpt,tanUrlOpt,tsrOpt = ISADotNet.Value.toOptions this -// let name = nameOpt |> Option.defaultValue "" -// let tan = -// if tanUrlOpt.IsSome then -// Regex.parseTermAccession tanUrlOpt.Value |> Option.map (fun tan -> tan.Replace("_",":")) |> Option.defaultValue "" -// else -// "" -// TermMinimal.create name tan - -//let getColumnPosition (oa:OntologyAnnotation) = -// let c = oa.Comments |> Option.map (List.find (fun c -> c.Name = Some ColumnPositionCommentName)) -// c.Value.Value.Value |> int - -//open ISADotNet.QueryModel - -//type ISAValue with - -// member this.toInsertBuildingBlock : (int * InsertBuildingBlock) = -// let buildingBlockType = -// if this.IsFactorValue then -// BuildingBlockType.Factor -// elif this.IsParameterValue then -// BuildingBlockType.Parameter -// elif this.IsCharacteristicValue then -// BuildingBlockType.Characteristic -// elif this.IsComponent then -// BuildingBlockType.Component -// else -// failwithf "This function should only ever be used to parse Factor/Parameter/Characteristic/Component, instead parsed: %A" this -// let colHeaderTermName = -// if this.HasCategory |> not then -// None -// else -// this.Category.toTermMinimal -// let columnPosition = getColumnPosition this.Category -// let unitTerm = if this.HasUnit then this.Unit.toTermMinimal else None -// let headerPrePrint = OfficeInteropTypes.BuildingBlockNamePrePrint.create buildingBlockType colHeaderTermName.Value.Name -// let value = if this.HasValue then Array.singleton this.Value.toTermMinimal else [||] -// //printfn "%A" (if this.HasValue then box this.Value else box "") -// columnPosition, InsertBuildingBlock.create headerPrePrint colHeaderTermName unitTerm value - - -//type IOType with -// member this.toBuildingBlockType = -// match this with -// | Source -> BuildingBlockType.Source -// | Sample -> BuildingBlockType.Sample -// | Data -> BuildingBlockType.Data -// | RawData -> BuildingBlockType.RawDataFile -// | ProcessedData -> BuildingBlockType.DerivedDataFile -// | anyElse -> failwith $"Cannot parse {anyElse} IsaDotNet IOType to BuildingBlockType." - -//let createBuildingBlock_fromProtocolType (protocol: Protocol) = -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create BuildingBlockType.ProtocolType "" -// let columnTerm = Some BuildingBlockType.ProtocolType.getFeaturedColumnTermMinimal -// let rows = -// protocol -// |> fun protType -> -// // row range information is saved in comments and can be accessed + parsed by isadotnet function -// let rowStart, rowEnd = protType.GetRowRange() -// let tmEmpty = TermMinimal.create "" "" -// [| for _ in rowStart .. rowEnd do -// let hasValue = protType.ProtocolType.IsSome -// yield -// if hasValue then protType.ProtocolType.Value.toTermMinimal |> Option.defaultValue tmEmpty else tmEmpty -// |] -// let columnPosition = protocol.ProtocolType |> Option.map getColumnPosition |> Option.defaultValue 0 -// columnPosition, InsertBuildingBlock.create header columnTerm None rows - -///// extend existing ISADotNet.Json.AssayCommonAPI.RowWiseSheet from ISADotNet library with -///// static member to map it to the Swate InsertBuildingBlock type used as input for addBuildingBlock functions -//type QueryModel.QSheet with - -// /// This function is only used for Swate templates. -// /// This function looses values, input and output columns as well as Protocol REF -// member this.headerToInsertBuildingBlockList : InsertBuildingBlock list = -// let headerRow = this.Rows.Head -// if this.Protocols.Length <> 1 then failwith "Protocol template must contain exactly one template" - -// let protocolType = -// let protocol = this.Protocols.Head -// let hasProtocolType = protocol.ProtocolType.IsSome -// if hasProtocolType then -// createBuildingBlock_fromProtocolType protocol |> Some -// else -// None - -// let rawCols = headerRow.Values().Values -// let mutable cols = rawCols |> List.map (fun fv -> fv.toInsertBuildingBlock) -// match protocolType with -// | Some (pbb) -> -// cols <- pbb::cols -// | None -> () -// cols -// |> List.sortBy fst -// |> List.map snd - -// /// This function is the basic parser for all json/xlsx input, with values, input, output and all supported column types. -// member this.toInsertBuildingBlockList : InsertBuildingBlock list = -// let insertBuildingBlockRowList = -// this.Rows |> List.collect (fun r -> -// let cols = -// let cols = r.Values().Values -// cols |> List.map (fun fv -> fv.toInsertBuildingBlock) -// cols |> List.sortBy fst |> List.map snd -// ) -// // Check if protocolREF column exists in assay. because this column is not represented as other columns in isa we need to infer this. -// let protocolRef = -// let sheetName, _ = Process.decomposeName this.SheetName -// let protocolNames = this.Protocols |> List.map (fun x -> x.Name, x) -// // if sheetname and any protocol name differ then we need to create the protocol ref column -// let hasProtocolRef = protocolNames |> List.exists (fun (name, _) -> name.IsSome && name.Value <> sheetName) -// if hasProtocolRef then -// // header must be protocol ref -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create BuildingBlockType.ProtocolREF "" -// let rows = -// this.Protocols -// |> Array.ofList -// |> Array.collect (fun protRef -> -// // row range information is saved in comments and can be accessed + parsed by isadotnet function -// let rowStart, rowEnd = protRef.GetRowRange() -// [| for _ in rowStart .. rowEnd do -// yield TermMinimal.create protRef.Name.Value "" |] -// ) -// InsertBuildingBlock.create header None None rows |> Some -// else -// None - -// let protocolType = -// let hasProtocolType = -// let prots = this.Protocols |> List.choose (fun x -> x.ProtocolType) -// prots.Length > 0 -// if hasProtocolType then -// // header must be protocol type -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create BuildingBlockType.ProtocolType "" -// let columnTerm = Some BuildingBlockType.ProtocolType.getFeaturedColumnTermMinimal -// let rows = -// this.Protocols -// |> Array.ofList -// |> Array.collect (fun protType -> -// // row range information is saved in comments and can be accessed + parsed by isadotnet function -// let rowStart, rowEnd = protType.GetRowRange() -// let tmEmpty = TermMinimal.create "" "" -// [| for _ in rowStart .. rowEnd do -// let hasValue = protType.ProtocolType.IsSome -// yield -// if hasValue then protType.ProtocolType.Value.toTermMinimal |> Option.defaultValue tmEmpty else tmEmpty -// |] -// ) -// InsertBuildingBlock.create header columnTerm None rows |> Some -// else -// None - -// /// https://github.com/nfdi4plants/ISADotNet/issues/80 -// /// This needs to be fixed! For now we only have one input so we can assume "Source" but should this change we need to adapt. -// /// As Soon as this is fixed, create one function for both input and output with (string*IOType option) list as input. -// let input = -// if List.isEmpty this.Inputs then -// None -// else -// let names, types = this.Inputs |> List.unzip -// let inputType = -// //let distinct = (List.choose id >> List.distinct) types -// //try -// // distinct |> List.exactlyOne -// //with -// // | _ -> failwith $"Cannot have input of multiple types: {distinct}" -// IOType.Source.toBuildingBlockType -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create inputType "" -// let rows = names |> List.map (fun x -> TermMinimal.create x "") |> Array.ofList -// InsertBuildingBlock.create header None None rows |> Some - -// /// https://github.com/nfdi4plants/ISADotNet/issues/80 -// let output = -// if List.isEmpty this.Outputs then -// None -// else -// let names, types = this.Outputs |> List.unzip -// //printfn "[OUTPUTS]: %A" this.Outputs -// //printfn "[OUTUT_TYPES]: %A" types -// let inputType = -// // let distinct = (List.choose id >> List.distinct) types -// // try -// // distinct |> List.exactlyOne -// // with -// // | _ -> failwith $"Cannot have input of multiple types: {distinct}" -// // |> fun d -> d.toBuildingBlockType -// IOType.Sample.toBuildingBlockType -// let header = OfficeInteropTypes.BuildingBlockNamePrePrint.create inputType "" -// let rows = names |> List.map (fun x -> TermMinimal.create x "") |> Array.ofList -// InsertBuildingBlock.create header None None rows |> Some - -// // group building block values by "InsertBuildingBlock" information (column information without values) -// insertBuildingBlockRowList -// |> List.groupBy (fun buildingBlock -> -// buildingBlock.ColumnHeader,buildingBlock.ColumnTerm,buildingBlock.UnitTerm -// ) -// |> List.map (fun ((header,term,unit),buildingBlocks) -> -// let rows = buildingBlocks |> Array.ofList |> Array.collect (fun bb -> bb.Rows) -// InsertBuildingBlock.create header term unit rows -// ) -// |> fun l -> // add special columns -// match protocolRef, protocolType with -// | None, None -> l -// | Some ref, Some t -> ref::t::l -// | Some ref, None -> ref::l -// | None, Some t -> t::l -// |> fun l -> // add input -// match input with -// | Some i -> i::l -// | None -> l -// |> fun l -> // add output -// match output with -// | Some o -> l@[o] -// | None -> l \ No newline at end of file diff --git a/src/Server/Import.fs b/src/Server/Import.fs deleted file mode 100644 index c51ea394..00000000 --- a/src/Server/Import.fs +++ /dev/null @@ -1,46 +0,0 @@ -module Import - -//open ISADotNet -//open ISADotNet.Json - - -/////Input is json string. -//module Json = - -// // The following functions are used to unify all jsonInput to the table type. - -// let fromAssay jsonString = -// let assay = Assay.fromString jsonString -// let tables = QueryModel.QAssay.fromAssay assay -// tables - -// let fromProcessSeq jsonString = -// let processSeq = ProcessSequence.fromString jsonString -// let assay = Assay.create(ProcessSequence = List.ofSeq processSeq) -// let tables = QueryModel.QAssay.fromAssay assay -// tables - -/////Input is byte []. -//module Xlsx = - -// let fromAssay (byteArray: byte []) = -// let ms = new System.IO.MemoryStream(byteArray) -// let _,assay = ISADotNet.XLSX.AssayFile.Assay.fromStream ms -// let tables = QueryModel.QAssay.fromAssay assay -// tables - -///// This function tries to parse any possible json input to building blocks. -//let tryToTable (bytes: byte []) = -// let jsonString = System.Text.Encoding.ASCII.GetString(bytes) -// try -// Json.fromAssay jsonString -// with -// | _ -> -// try -// Json.fromProcessSeq jsonString -// with -// | _ -> -// try -// Xlsx.fromAssay bytes -// with -// | _ -> failwith "Could not match given file to supported file type." \ No newline at end of file diff --git a/src/Server/Server.fs b/src/Server/Server.fs index 4ed48790..3ad96451 100644 --- a/src/Server/Server.fs +++ b/src/Server/Server.fs @@ -13,11 +13,13 @@ open Fable.Remoting.Giraffe open Microsoft.Extensions.Logging open Microsoft.Extensions.Configuration -let serviceApi = { +open ARCtrl +open ARCtrl.Json + +let serviceApi: IServiceAPIv1 = { getAppVersion = fun () -> async { return System.AssemblyVersionInformation.AssemblyMetadata_Version } } -open ISADotNet open Microsoft.AspNetCore.Http let dagApiv1 = { @@ -169,20 +171,22 @@ let isaDotNetCommonAPIv1 : IISADotNetCommonAPIv1 = open Database -let templateApi credentials = { - getTemplates = fun () -> async { - let! templates = ARCtrl.Template.Web.getTemplates None - let templatesJson = templates |> Array.map (ARCtrl.Template.Json.Template.toJsonString 0) - return templatesJson - } +let templateApi credentials = + let templateUrl = @"https://github.com/nfdi4plants/Swate-templates/releases/download/latest/templates_v2.0.0.json" + { + getTemplates = fun () -> async { + let! templates = ARCtrl.Template.Web.getTemplates (None) + let templatesJson = ARCtrl.Json.Templates.toJsonString 0 (Array.ofSeq templates.Values) + return templatesJson + } - getTemplateById = fun id -> async { - let! templates = ARCtrl.Template.Web.getTemplates None - let template = templates |> Array.find (fun t -> t.Id = System.Guid(id)) - let templateJson = ARCtrl.Template.Json.Template.toJsonString 0 template - return templateJson + getTemplateById = fun id -> async { + let! templates = ARCtrl.Template.Web.getTemplates (None) + let template = templates.Values |> Seq.find (fun t -> t.Id = System.Guid(id)) + let templateJson = Template.toCompressedJsonString 0 template + return templateJson + } } -} let testApi (ctx: HttpContext): ITestAPI = { test = fun () -> async { diff --git a/src/Server/Server.fsproj b/src/Server/Server.fsproj index 13c8bca1..c5564071 100644 --- a/src/Server/Server.fsproj +++ b/src/Server/Server.fsproj @@ -8,16 +8,11 @@ - - - - - @@ -26,5 +21,8 @@ + + + \ No newline at end of file diff --git a/src/Server/Server.fsproj.user b/src/Server/Server.fsproj.user deleted file mode 100644 index f34b3fce..00000000 --- a/src/Server/Server.fsproj.user +++ /dev/null @@ -1,10 +0,0 @@ - - - - IIS - false - - - ProjectDebugger - - \ No newline at end of file diff --git a/src/Server/TemplateMetadata.fs b/src/Server/TemplateMetadata.fs deleted file mode 100644 index 73de4ae3..00000000 --- a/src/Server/TemplateMetadata.fs +++ /dev/null @@ -1,149 +0,0 @@ -module TemplateMetadata - -//open Newtonsoft.Json.Schema - -//let resolver = JSchemaUrlResolver() - -//let jsonSchemaPath = @"public/TemplateMetadataSchema.json" - -//let writeSettings = -// let s = JSchemaWriterSettings() -// s.ReferenceHandling <- JSchemaWriterReferenceHandling.Never -// s - -//let getJsonSchemaAsXml = -// System.IO.File.ReadAllText jsonSchemaPath -// |> fun x -> JSchema.Parse(x,resolver).ToString(writeSettings) - -//open System -//open System.IO -//open FsSpreadsheet.ExcelIO - -//open Shared -//open TemplateTypes -//open DynamicObj -//open Newtonsoft.Json - -//type TemplateMetadataJsonExport() = -// inherit DynamicObj() - -// static member init(?props) = -// let t = TemplateMetadataJsonExport() -// if props.IsSome then -// props.Value |> List.iter t.setProp -// t -// else -// t.setProp("", None) -// t - -// member this.setProp(key,value) = DynObj.setValueOpt this key value - -// member this.print() = DynObj.print this - -// member this.toJson() = this |> JsonConvert.SerializeObject - -//let private maxColumnByRows (rows:DocumentFormat.OpenXml.Spreadsheet.Row []) = -// rows -// |> Array.map (fun row -> (Row.Spans.toBoundaries >> snd) row.Spans) -// |> (Array.max >> int) - -//let private findRowByKey (key:string) (rowValues: string option [][])= -// rowValues -// |> Array.find (fun row -> -// row.[0].IsSome && row.[0].Value = key -// ) - -//let private findRowValuesByKey (key:string) (rowValues: string option [][])= -// findRowByKey key rowValues -// |> Array.tail - -///// Also gets rows from children -//let private getAllRelatedRowsValues (metadata:Metadata.MetadataField) (rows: string option [][]) = -// let rec collectRows (crMetadata:Metadata.MetadataField) = -// if crMetadata.Children.IsEmpty then -// let row = rows |> findRowValuesByKey crMetadata.ExtendedNameKey -// [|row|] -// else -// let childRows = crMetadata.Children |> Array.ofList |> Array.collect (fun child -> collectRows child) -// childRows -// collectRows metadata - -//let private convertToDynObject (sheetData:DocumentFormat.OpenXml.Spreadsheet.SheetData) sst (metadata:Metadata.MetadataField) = -// let rows = SheetData.getRows sheetData |> Array.ofSeq -// let rowValues = -// rows -// |> Array.mapi (fun i row -> -// let spans = row.Spans -// let leftB,rightB = Row.Spans.toBoundaries spans -// [| -// for i = leftB to rightB do -// yield -// Row.tryGetValueAt sst i row -// |] -// ) -// let rec convertDynamic (listIndex: int option) (forwardOutput:TemplateMetadataJsonExport option) (metadata:Metadata.MetadataField) = -// let output = -// if forwardOutput.IsSome then -// forwardOutput.Value -// else -// TemplateMetadataJsonExport.init() -// match metadata with -// /// hit leaves without children -// | isOutput when metadata.Children = [] -> -// let isList = listIndex.IsSome -// let rowValues = rowValues |> findRowValuesByKey metadata.ExtendedNameKey |> Array.map (fun x -> if x.IsSome then x.Value else "") //|> Array.choose id -// if isList then -// let v = -// let tryFromArr = Array.tryItem (listIndex.Value) rowValues -// Option.defaultValue "" tryFromArr -// //if v <> "" then -// output.setProp(metadata.Key, Some v) -// else -// let v = if Array.isEmpty >> not <| rowValues then rowValues.[0] else "" -// //if v <> "" then -// output.setProp(metadata.Key, Some v) -// output -// /// Treat nested lists as object, as nested lists cannot be represented in excel -// | isNestedObjectList when metadata.Children <> [] && metadata.List && metadata.Key <> "" && listIndex.IsSome -> -// /// "WARNING: Cannot parse nested list: metadata.Key, metadata.ExtendedNameKey. Treat by default as object." -// let noList = { metadata with List = false } -// convertDynamic listIndex forwardOutput noList -// /// children are represented by columns -// | isObjectList when metadata.Children <> [] && metadata.List && metadata.Key <> "" -> -// let childRows = getAllRelatedRowsValues metadata rowValues -// /// Only calculate max columns if cell still contains a value. Filter out None and "" -// let notEmptyChildRows = childRows |> Array.map (Array.choose id) |> Array.map (Array.filter (fun x -> x <> "")) -// let maxColumn = notEmptyChildRows |> Array.map Array.length |> Array.max -// let childObjects = -// [| for i = 0 to maxColumn-1 do -// let childOutput = TemplateMetadataJsonExport.init() -// let addChildObject = metadata.Children |> List.map (convertDynamic (Some i) (Some childOutput)) -// yield -// childOutput -// |] -// output.setProp(metadata.Key, Some childObjects) -// output -// /// hit if json object -// | isObject when metadata.Children <> [] && metadata.Key <> "" -> -// let childOutput = TemplateMetadataJsonExport.init() -// // Add key values from children to childOutput. childOutput will be added to output afterwards. -// let childObject = metadata.Children |> List.map (convertDynamic listIndex (Some childOutput)) -// output.setProp(metadata.Key, Some childOutput) -// output -// /// This hits only root objects without key -// | isRoot -> -// let addChildObject = -// metadata.Children -// |> List.map (fun childMetadata -> -// convertDynamic listIndex (Some output) childMetadata -// ) -// output -// convertDynamic None None metadata - -//let parseDynMetadataFromByteArr (byteArray:byte []) = -// let ms = new MemoryStream(byteArray) -// let spreadsheet = Spreadsheet.fromStream ms false -// let sst = Spreadsheet.tryGetSharedStringTable spreadsheet -// let sheetOpt = Spreadsheet.tryGetSheetBySheetName TemplateTypes.Metadata.TemplateMetadataWorksheetName spreadsheet -// if sheetOpt.IsNone then failwith $"Could not find template metadata worksheet: {TemplateTypes.Metadata.TemplateMetadataWorksheetName}" -// convertToDynObject sheetOpt.Value sst Metadata.root diff --git a/src/Server/public/TemplateMetadataSchema.json b/src/Server/public/TemplateMetadataSchema.json deleted file mode 100644 index 0abda3db..00000000 --- a/src/Server/public/TemplateMetadataSchema.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://raw.githubusercontent.com/nfdi4plants/SWATE_templates/FormattingDocs/templates/TemplatingSchema.json", - "title": "Template Metadata Schema", - "description": "The schema regarding Swate templating-related JSON files.", - "type": "object", - "properties": { - "templateId": { - "description": "The unique identifier of this template. It will be auto generated.", - "type": "string" - }, - "name": { - "description": "The name of the Swate template.", - "type": "string" - }, - "version": { - "description": "The current version of this template in SemVer notation.", - "type": "string" - }, - "description": { - "description": "The description of this template. Use few sentences for succinctness.", - "type": "string" - }, - "docslink": { - "description": "The URL to the documentation page.", - "type": "string" - }, - "organisation": { - "description": "The name of the template associated organisation.", - "type": "string" - }, - "table": { - "description": "The name of the Swate annotation table in the worksheet of the template's excel file.", - "type": "string" - }, - "er": { - "description": "A list of all ERs (endpoint repositories) targeted with this template. ERs are realized as Terms: ", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/ISA-tools/isa-api/master/isatools/resources/schemas/isa_model_version_1_0_schemas/core/ontology_annotation_schema.json" - }, - "minItems": 1, - "uniqueItems": true - }, - "tags": { - "description": "A list of all tags associated with this template. Tags are realized as Terms: ", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/ISA-tools/isa-api/master/isatools/resources/schemas/isa_model_version_1_0_schemas/core/ontology_annotation_schema.json" - }, - "minItems": 1, - "uniqueItems": true - }, - "authors": { - "description": "The author(s) of this template.", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/ISA-tools/isa-api/master/isatools/resources/schemas/isa_model_version_1_0_schemas/core/person_schema.json" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "required": [ - "name", - "version", - "description", - "docslink", - "organisation", - "table", - "er", - "tags", - "authors" - ] -} \ No newline at end of file diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 63a47903..26a0d097 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -1,6 +1,6 @@ namespace Shared -open ARCtrl.ISA +open ARCtrl open TermTypes open System.Collections.Generic @@ -9,7 +9,7 @@ open System.Collections.Generic module ARCtrlHelper = type ArcFiles = - | Template of ARCtrl.Template.Template + | Template of Template | Investigation of ArcInvestigation | Study of ArcStudy * ArcAssay list | Assay of ArcAssay @@ -57,6 +57,9 @@ module Extensions = open ARCtrl.Template open ArcTableAux + type OntologyAnnotation with + static member empty = OntologyAnnotation.create() + type ArcTable with member this.SetCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell) = SanityChecks.validateColumn <| CompositeColumn.create(this.Headers.[columnIndex],[|cell|]) @@ -192,11 +195,13 @@ module Extensions = match this with | CompositeCell.Term oa -> oa | CompositeCell.Unitized (v, oa) -> oa - | CompositeCell.FreeText t -> OntologyAnnotation.fromString t + | CompositeCell.FreeText t -> OntologyAnnotation.create t member this.UpdateMainField(s: string) = match this with - | CompositeCell.Term oa -> CompositeCell.Term ({oa with Name = Some s}) + | CompositeCell.Term oa -> + oa.Name <- Some s + CompositeCell.Term oa | CompositeCell.Unitized (_, oa) -> CompositeCell.Unitized (s, oa) | CompositeCell.FreeText _ -> CompositeCell.FreeText s @@ -205,7 +210,7 @@ module Extensions = /// /// member this.UpdateTSR(tsr: string) = - let updateTSR (oa: OntologyAnnotation) = {oa with TermSourceREF = tsr |> Some} + let updateTSR (oa: OntologyAnnotation) = oa.TermSourceREF <- Some tsr ;oa match this with | CompositeCell.Term oa -> CompositeCell.Term (updateTSR oa) | CompositeCell.Unitized (v, oa) -> CompositeCell.Unitized (v, updateTSR oa) @@ -216,12 +221,12 @@ module Extensions = /// /// member this.UpdateTAN(tan: string) = - let updateTAN (oa: OntologyAnnotation) = {oa with TermAccessionNumber = tan |> Some} + let updateTAN (oa: OntologyAnnotation) = oa.TermSourceREF <- Some tan ;oa match this with | CompositeCell.Term oa -> CompositeCell.Term (updateTAN oa) | CompositeCell.Unitized (v, oa) -> CompositeCell.Unitized (v, updateTAN oa) | _ -> this type OntologyAnnotation with - static member fromTerm (term:Term) = OntologyAnnotation.fromString(term.Name, term.FK_Ontology, term.Accession) + static member fromTerm (term:Term) = OntologyAnnotation(term.Name, term.FK_Ontology, term.Accession) member this.ToTermMinimal() = TermMinimal.create this.NameText this.TermAccessionShort diff --git a/src/Shared/OfficeInteropTypes.fs b/src/Shared/OfficeInteropTypes.fs index 82b0f100..de433843 100644 --- a/src/Shared/OfficeInteropTypes.fs +++ b/src/Shared/OfficeInteropTypes.fs @@ -1,7 +1,7 @@ namespace Shared open System -open ARCtrl.ISA +open ARCtrl module OfficeInteropTypes = diff --git a/src/Shared/Shared.fs b/src/Shared/Shared.fs index 7da320be..46805379 100644 --- a/src/Shared/Shared.fs +++ b/src/Shared/Shared.fs @@ -151,7 +151,7 @@ type IOntologyAPIv3 = { type ITemplateAPIv1 = { // must return template as string, fable remoting cannot do conversion automatically - getTemplates : unit -> Async + getTemplates : unit -> Async getTemplateById : string -> Async } diff --git a/src/Shared/StaticTermCollection.fs b/src/Shared/StaticTermCollection.fs index 10ffc560..e0e97d1b 100644 --- a/src/Shared/StaticTermCollection.fs +++ b/src/Shared/StaticTermCollection.fs @@ -1,9 +1,9 @@ module Term -open ARCtrl.ISA +open ARCtrl /// /// https://github.com/nfdi4plants/nfdi4plants_ontology/issues/85 /// -let Published = OntologyAnnotation.fromString("published","EFO","EFO:0001796") +let Published = OntologyAnnotation("published","EFO","EFO:0001796") diff --git a/src/Shared/TermTypes.fs b/src/Shared/TermTypes.fs index e94b7d98..51eb3f9d 100644 --- a/src/Shared/TermTypes.fs +++ b/src/Shared/TermTypes.fs @@ -1,6 +1,6 @@ namespace Shared -open ARCtrl.ISA +open ARCtrl module TermTypes = From dcf570afd053298c2c8420f068c5e621e31ec052 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 18 Jun 2024 12:10:27 +0200 Subject: [PATCH 108/135] finish update to ARCtrl v2 #427 --- paket.dependencies | 2 +- paket.lock | 41 ++++++++++--------- src/Client/MainComponents/Metadata/Assay.fs | 12 +++--- src/Client/MainComponents/Metadata/Forms.fs | 6 +-- src/Client/Pages/BuildingBlock/Helper.fs | 2 +- .../SharedComponents/TermSearchInput.fs | 2 +- src/Client/Spreadsheet/Table.Controller.fs | 1 + src/Client/Update/SpreadsheetUpdate.fs | 4 ++ src/Server/Server.fs | 4 +- src/Shared/ARCtrl.Helper.fs | 10 ++--- 10 files changed, 45 insertions(+), 39 deletions(-) diff --git a/paket.dependencies b/paket.dependencies index 952efc57..992566a2 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -2,7 +2,7 @@ source https://api.nuget.org/v3/index.json framework: net8.0 storage: none -nuget ARCtrl 2.0.0-alpha.6.swate alpha +nuget ARCtrl 2.0.0-alpha.7.swate alpha nuget Fable.Fetch 2.7.0 nuget Feliz.Bulma.Checkradio nuget Feliz.Bulma.Switch diff --git a/paket.lock b/paket.lock index 56b9c1c7..1ceec7ed 100644 --- a/paket.lock +++ b/paket.lock @@ -2,38 +2,39 @@ STORAGE: NONE RESTRICTION: == net8.0 NUGET remote: https://api.nuget.org/v3/index.json - ARCtrl (2.0.0-alpha.6.swate) - ARCtrl.Contract (>= 2.0.0-alpha.6.swate) - ARCtrl.CWL (>= 2.0.0-alpha.6.swate) - ARCtrl.FileSystem (>= 2.0.0-alpha.6.swate) - ARCtrl.Json (>= 2.0.0-alpha.6.swate) - ARCtrl.Spreadsheet (>= 2.0.0-alpha.6.swate) + ARCtrl (2.0.0-alpha.7.swate) + ARCtrl.Contract (>= 2.0.0-alpha.7.swate) + ARCtrl.CWL (>= 2.0.0-alpha.7.swate) + ARCtrl.FileSystem (>= 2.0.0-alpha.7.swate) + ARCtrl.Json (>= 2.0.0-alpha.7.swate) + ARCtrl.Spreadsheet (>= 2.0.0-alpha.7.swate) + Fable.Fetch (>= 2.6) Fable.SimpleHttp (>= 3.5) FSharp.Core (>= 7.0.401) - ARCtrl.Contract (2.0.0-alpha.6.swate) - ARCtrl.Core (>= 2.0.0-alpha.6.swate) - ARCtrl.Json (>= 2.0.0-alpha.6.swate) - ARCtrl.Spreadsheet (>= 2.0.0-alpha.6.swate) + ARCtrl.Contract (2.0.0-alpha.7.swate) + ARCtrl.Core (>= 2.0.0-alpha.7.swate) + ARCtrl.Json (>= 2.0.0-alpha.7.swate) + ARCtrl.Spreadsheet (>= 2.0.0-alpha.7.swate) FSharp.Core (>= 7.0.401) - ARCtrl.Core (2.0.0-alpha.6.swate) - ARCtrl.CWL (>= 2.0.0-alpha.6.swate) - ARCtrl.FileSystem (>= 2.0.0-alpha.6.swate) + ARCtrl.Core (2.0.0-alpha.7.swate) + ARCtrl.CWL (>= 2.0.0-alpha.7.swate) + ARCtrl.FileSystem (>= 2.0.0-alpha.7.swate) FSharp.Core (>= 7.0.401) - ARCtrl.CWL (2.0.0-alpha.6.swate) + ARCtrl.CWL (2.0.0-alpha.7.swate) FSharp.Core (>= 6.0.7) - ARCtrl.FileSystem (2.0.0-alpha.6.swate) + ARCtrl.FileSystem (2.0.0-alpha.7.swate) Fable.Core (>= 4.2) FSharp.Core (>= 6.0.7) - ARCtrl.Json (2.0.0-alpha.6.swate) - ARCtrl.Core (>= 2.0.0-alpha.6.swate) + ARCtrl.Json (2.0.0-alpha.7.swate) + ARCtrl.Core (>= 2.0.0-alpha.7.swate) FSharp.Core (>= 7.0.401) NJsonSchema (>= 10.8) Thoth.Json.Core (>= 0.2.1) Thoth.Json.JavaScript (>= 0.1) Thoth.Json.Newtonsoft (>= 0.1) - ARCtrl.Spreadsheet (2.0.0-alpha.6.swate) - ARCtrl.Core (>= 2.0.0-alpha.6.swate) - ARCtrl.FileSystem (>= 2.0.0-alpha.6.swate) + ARCtrl.Spreadsheet (2.0.0-alpha.7.swate) + ARCtrl.Core (>= 2.0.0-alpha.7.swate) + ARCtrl.FileSystem (>= 2.0.0-alpha.7.swate) FSharp.Core (>= 7.0.401) FsSpreadsheet (>= 6.1.2) ExcelJS.Fable (0.3) diff --git a/src/Client/MainComponents/Metadata/Assay.fs b/src/Client/MainComponents/Metadata/Assay.fs index 75bb1783..15064d6d 100644 --- a/src/Client/MainComponents/Metadata/Assay.fs +++ b/src/Client/MainComponents/Metadata/Assay.fs @@ -18,25 +18,25 @@ let Main(assay: ArcAssay, model: Messages.Model, dispatch: Msg -> unit) = fullwidth=true ) FormComponents.OntologyAnnotationInput( - assay.MeasurementType |> Option.defaultValue OntologyAnnotation.empty, + assay.MeasurementType |> Option.defaultValue (OntologyAnnotation.empty()), (fun oa -> - let oa = if oa = OntologyAnnotation.empty then None else Some oa + let oa = if oa = (OntologyAnnotation.empty()) then None else Some oa assay.MeasurementType <- oa assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), "Measurement Type" ) FormComponents.OntologyAnnotationInput( - assay.TechnologyType |> Option.defaultValue OntologyAnnotation.empty, + assay.TechnologyType |> Option.defaultValue (OntologyAnnotation.empty()), (fun oa -> - let oa = if oa = OntologyAnnotation.empty then None else Some oa + let oa = if oa = (OntologyAnnotation.empty()) then None else Some oa assay.TechnologyType <- oa assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), "Technology Type" ) FormComponents.OntologyAnnotationInput( - assay.TechnologyPlatform |> Option.defaultValue OntologyAnnotation.empty, + assay.TechnologyPlatform |> Option.defaultValue (OntologyAnnotation.empty()), (fun oa -> - let oa = if oa = OntologyAnnotation.empty then None else Some oa + let oa = if oa = (OntologyAnnotation.empty()) then None else Some oa assay.TechnologyPlatform <- oa assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch), "Technology Platform" diff --git a/src/Client/MainComponents/Metadata/Forms.fs b/src/Client/MainComponents/Metadata/Forms.fs index d609bbcf..e9768c52 100644 --- a/src/Client/MainComponents/Metadata/Forms.fs +++ b/src/Client/MainComponents/Metadata/Forms.fs @@ -778,7 +778,7 @@ type FormComponents = [] static member OntologyAnnotationsInput (oas: OntologyAnnotation [], label: string, setter: OntologyAnnotation [] -> unit, ?showTextLabels: bool) = FormComponents.InputSequence( - oas, OntologyAnnotation.empty, label, setter, + oas, (OntologyAnnotation.empty()), label, setter, (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,c,label=b,removebutton=d,?showTextLabels=showTextLabels)) ) @@ -1036,9 +1036,9 @@ type FormComponents = ] createPersonFieldTextInput(state.Authors, "Authors", fun s -> state.Authors <- s) FormComponents.OntologyAnnotationInput( - Option.defaultValue OntologyAnnotation.empty state.Status, + Option.defaultValue (OntologyAnnotation.empty()) state.Status, (fun s -> - state.Status <- if s = OntologyAnnotation.empty then None else Some s + state.Status <- if s = (OntologyAnnotation.empty()) then None else Some s state |> setter ), "Status" diff --git a/src/Client/Pages/BuildingBlock/Helper.fs b/src/Client/Pages/BuildingBlock/Helper.fs index bd9bb779..b56c058d 100644 --- a/src/Client/Pages/BuildingBlock/Helper.fs +++ b/src/Client/Pages/BuildingBlock/Helper.fs @@ -18,7 +18,7 @@ let selectHeaderCellType (hct: BuildingBlock.HeaderCellType) setUiState dispatch open Fable.Core let createCompositeHeaderFromState (state: BuildingBlock.Model) = - let getOA() = state.TryHeaderOA() |> Option.defaultValue OntologyAnnotation.empty + 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() diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index 5d23c403..ddaa4b8b 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -314,7 +314,7 @@ type TermSearch = let fullwidth = defaultArg fullwidth false let loading, setLoading = React.useState(false) let state, setState = React.useState(input) - let searchable, setSearchable = React.useState(not searchableToggle) + let searchable, setSearchable = React.useState(true) let searchNameState, setSearchNameState = React.useState(SearchState.init) let searchTreeState, setSearchTreeState = React.useState(SearchState.init) let isSearching, setIsSearching = React.useState(false) diff --git a/src/Client/Spreadsheet/Table.Controller.fs b/src/Client/Spreadsheet/Table.Controller.fs index 7127f578..01cd1180 100644 --- a/src/Client/Spreadsheet/Table.Controller.fs +++ b/src/Client/Spreadsheet/Table.Controller.fs @@ -105,6 +105,7 @@ let fillColumnWithCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet let cell = cell|> Option.defaultValue (column.GetDefaultEmptyCell()) if i = columnIndex then for cellRowIndex in 0 .. column.Cells.Length-1 do + let cell = cell state.ActiveTable.UpdateCellAt(columnIndex, cellRowIndex, cell) ) {state with ArcFile = state.ArcFile} diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index e4d00084..8d42f560 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -99,8 +99,12 @@ module Spreadsheet = let cmd = Cmd.none state, model, cmd | UpdateCell (index, cell) -> + log ("[UPDATECELL]", index, cell) let nextState = state.ActiveTable.UpdateCellAt(fst index,snd index, cell) + log "START" + log state.ActiveTable + log "END" {state with ArcFile = state.ArcFile} nextState, model, Cmd.none | UpdateCells arr -> diff --git a/src/Server/Server.fs b/src/Server/Server.fs index 3ad96451..10d479b5 100644 --- a/src/Server/Server.fs +++ b/src/Server/Server.fs @@ -176,13 +176,13 @@ let templateApi credentials = { getTemplates = fun () -> async { let! templates = ARCtrl.Template.Web.getTemplates (None) - let templatesJson = ARCtrl.Json.Templates.toJsonString 0 (Array.ofSeq templates.Values) + let templatesJson = ARCtrl.Json.Templates.toJsonString 0 (Array.ofSeq templates) return templatesJson } getTemplateById = fun id -> async { let! templates = ARCtrl.Template.Web.getTemplates (None) - let template = templates.Values |> Seq.find (fun t -> t.Id = System.Guid(id)) + let template = templates |> Seq.find (fun t -> t.Id = System.Guid(id)) let templateJson = Template.toCompressedJsonString 0 template return templateJson } diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 26a0d097..5cabdff5 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -58,7 +58,7 @@ module Extensions = open ArcTableAux type OntologyAnnotation with - static member empty = OntologyAnnotation.create() + static member empty() = OntologyAnnotation.create() type ArcTable with member this.SetCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell) = @@ -109,10 +109,10 @@ module Extensions = | CompositeHeader.Factor _ -> CompositeHeader.Factor oa | _ -> failwithf "Cannot update OntologyAnnotation on CompositeHeader without OntologyAnnotation: '%A'" this - static member ParameterEmpty = CompositeHeader.Parameter OntologyAnnotation.empty - static member CharacteristicEmpty = CompositeHeader.Characteristic OntologyAnnotation.empty - static member ComponentEmpty = CompositeHeader.Component OntologyAnnotation.empty - static member FactorEmpty = CompositeHeader.Factor OntologyAnnotation.empty + static member ParameterEmpty = CompositeHeader.Parameter <| OntologyAnnotation.empty() + static member CharacteristicEmpty = CompositeHeader.Characteristic <| OntologyAnnotation.empty() + static member ComponentEmpty = CompositeHeader.Component <| OntologyAnnotation.empty() + static member FactorEmpty = CompositeHeader.Factor <| OntologyAnnotation.empty() static member InputEmpty = CompositeHeader.Input <| IOType.FreeText "" static member OutputEmpty = CompositeHeader.Output <| IOType.FreeText "" From 759266947ba930bcdb6deec4e12fce95054659de Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 19 Jun 2024 13:50:25 +0200 Subject: [PATCH 109/135] =?UTF-8?q?Allow=20filepicker=20import=20into=20an?= =?UTF-8?q?y=20cell=20#420=20=F0=9F=8C=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 13 ++++++++++++- src/Client/Pages/ProtocolTemplates/ProtocolState.fs | 2 +- src/Client/Update/InterfaceUpdate.fs | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5f282702..30fa4c7b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1 +1,12 @@ - \ No newline at end of file +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false + +[*.fs] +fsharp_multiline_bracket_style = stroustrup +fsharp_newline_before_multiline_computation_expression = false \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs index bab5102a..5a156cab 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs @@ -55,7 +55,7 @@ module Protocol = let nextState, cmd = match templates with | Ok t0 -> - let t = Array.ofSeq t0.Values + let t = Array.ofSeq t0 let nextState = { state with LastUpdated = Some System.DateTime.UtcNow } nextState, UpdateTemplates t |> ProtocolMsg |> Cmd.ofMsg | Result.Error e -> state, GenericError (Cmd.none,e) |> DevMsg |> Cmd.ofMsg diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 1cd7f4c9..8aebcbce 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -12,6 +12,7 @@ open Elmish open Model open Shared open Fable.Core.JsInterop +open Shared.ARCtrlHelper module private Helper = open ExcelJS.Fable.GlobalBindings @@ -112,7 +113,8 @@ module Interface = let mutable rowIndex = rowIndex let cells = [| for name in fileNames do - let cell = ARCtrl.CompositeCell.createFreeText name + let c0 = model.SpreadsheetModel.ActiveTable.TryGetCellAt(columnIndex,rowIndex).Value + let cell = c0.UpdateMainField name (columnIndex, rowIndex), cell rowIndex <- rowIndex + 1 |] From d37df56d00f5ea0e6402d9168afa7b3520ad23bd Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 19 Jun 2024 13:53:56 +0200 Subject: [PATCH 110/135] Make filepicker widget scrollable! :bug: #434 --- src/Client/MainComponents/Widgets.fs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index d3a42ed6..34327e9f 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -1,4 +1,4 @@ -namespace MainComponents +namespace MainComponents open Feliz open Feliz.Bulma @@ -235,7 +235,12 @@ type Widget = if model.FilePickerState.FileNames <> [] then FilePicker.fileSortElements model dispatch - FilePicker.FileNameTable.table model dispatch + Bulma.field.div [ + prop.style [style.maxHeight (length.px 350); style.overflow.auto] + prop.children [ + FilePicker.FileNameTable.table model dispatch + ] + ] //fileNameElements model dispatch FilePicker.insertButton model dispatch ] From 512ea008ff60c979e5f16629d3dd7c81c498dc9d Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 19 Jun 2024 14:02:23 +0200 Subject: [PATCH 111/135] fix template name search :bug: #414 --- .../ProtocolTemplates/ProtocolSearchViewComponent.fs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs index be797c33..4eb25885 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearchViewComponent.fs @@ -463,8 +463,12 @@ module FilterHelper = if Seq.isEmpty scores then 0.0 else 1.0 score, template ) - |> Array.filter (fun (score,_) -> score > 0.2) + |> Array.filter (fun (score,_) -> score > 0.3) |> Array.sortByDescending fst + |> fun y -> + for score, x in y do + log (score, x.Name) + y |> Array.map snd scoredTemplate else @@ -515,10 +519,10 @@ type Search = if templates.Length = 0 then [||] else templates |> Array.ofSeq + |> Array.sortBy (fun template -> template.Name, template.Organisation) |> FilterHelper.filterTableByTags config.ProtocolFilterTags config.ProtocolFilterErTags config.TagFilterIsAnd |> FilterHelper.filterTableByCommunityFilter config.CommunityFilter |> FilterHelper.sortTableBySearchQuery config.Searchfield config.ProtocolSearchQuery - |> Array.sortBy (fun template -> template.Name, template.Organisation) [] static member FileSortElement(model, config, configSetter: TemplateFilterConfig -> unit) = From 08b9faec6db46d3d151a0202d5345cc74cb44455 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 19 Jun 2024 15:50:48 +0200 Subject: [PATCH 112/135] Update to bulma v1.0.0 :construction: #435 --- package-lock.json | 667 ++---------------- package.json | 2 +- src/Client/Client.fsproj | 1 + .../MainComponents/EmptyTableElement.fs | 28 + src/Client/MainComponents/Navbar.fs | 2 +- .../SharedComponents/QuickAccessButton.fs | 2 +- src/Client/Views/XlsxFileView.fs | 8 +- src/Client/style.scss | 110 +-- src/Shared/ARCtrl.Helper.fs | 6 +- 9 files changed, 131 insertions(+), 695 deletions(-) create mode 100644 src/Client/MainComponents/EmptyTableElement.fs diff --git a/package-lock.json b/package-lock.json index 4cc91bec..ce14329b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,10 +4,11 @@ "requires": true, "packages": { "": { + "name": "Swate", "dependencies": { "@creativebulma/bulma-tooltip": "^1.2.0", "@nfdi4plants/exceljs": "^0.3.0", - "bulma": "^0.9.4", + "bulma": "^1.0.1", "bulma-checkradio": "^2.1.3", "bulma-slider": "^2.0.5", "bulma-switch": "^2.0.4", @@ -399,358 +400,6 @@ "resolved": "https://registry.npmjs.org/@creativebulma/bulma-tooltip/-/bulma-tooltip-1.2.0.tgz", "integrity": "sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/win32-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", @@ -933,201 +582,6 @@ "node": ">=14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", @@ -1355,12 +809,6 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -1576,9 +1024,9 @@ } }, "node_modules/bulma": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", - "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.1.tgz", + "integrity": "sha512-+xv/BIAEQakHkR0QVz+s+RjNqfC53Mx9ZYexyaFNFo9wx5i76HXArNdwW7bccyJxa5mgV/T5DcVGqsAB19nBJQ==" }, "node_modules/bulma-checkradio": { "version": "2.1.3", @@ -1588,6 +1036,11 @@ "bulma": "^0.9.3" } }, + "node_modules/bulma-checkradio/node_modules/bulma": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", + "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" + }, "node_modules/bulma-slider": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/bulma-slider/-/bulma-slider-2.0.5.tgz", @@ -1710,9 +1163,9 @@ } }, "node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz", + "integrity": "sha512-U8EviusIm8Fc5vMbs9opNX8r/hAz8PFYOu003AR1OVkCnDSTaBHB8inMn97yIbkGlI+dcdsItTBjgiZkVVzxYg==", "dev": true }, "node_modules/compress-commons": { @@ -2052,20 +1505,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -3112,15 +2551,15 @@ } }, "node_modules/remotedev": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.9.tgz", - "integrity": "sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.7.tgz", + "integrity": "sha512-QomJ4+1A82zZGidaQ4ecRDMAeMT2CxvTvGzzw+OOsP+IfrvF3Pu8SCRezVksjH1WuajmJSzKvOKNRoF1MXFNrA==", "dev": true, "dependencies": { "jsan": "^3.1.3", "querystring": "^0.2.0", "rn-host-detect": "^1.0.1", - "socketcluster-client": "^13.0.0" + "socketcluster-client": "^5.0.0" } }, "node_modules/resolve": { @@ -3274,18 +2713,28 @@ } }, "node_modules/sc-channel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", - "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.0.6.tgz", + "integrity": "sha512-vXhuJ4GZeOulBjLrKpbVhxyBz4YSqgRdc9m1jaR1byZfwyexarb7xCSe5/A0V42XGjCJ3/FR7wa8UEBtL9xOxg==", "dev": true, "dependencies": { - "component-emitter": "1.2.1" + "sc-emitter": "1.x.x" + } + }, + "node_modules/sc-emitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sc-emitter/-/sc-emitter-1.1.0.tgz", + "integrity": "sha512-f8YiHF/LkRiyZ1iIrzwIkec1VfcNrKBTEJ8w26s/5TEJXcH024Y1V6u1CRl9OeQp8E0zLu+7u56rjWSaH3yePQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "dependencies": { + "component-emitter": "1.2.0" } }, "node_modules/sc-errors": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", - "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.3.3.tgz", + "integrity": "sha512-zJQxMxsQ4N5hnXND4VUwwUOJxANqidCRw7vygFe52+XVrYWERqkVlOhivBS2vt18eWVxUQrgxJXMA0x9Yuzn8A==", "dev": true }, "node_modules/sc-formatter": { @@ -3363,21 +2812,20 @@ } }, "node_modules/socketcluster-client": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-13.0.1.tgz", - "integrity": "sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-5.5.2.tgz", + "integrity": "sha512-ivgbsUvTOIvEvba2IrQvhn8xUJoKg0t6OpwIKPXh64zRLpnLxDZ2EZXpjdc8okGHjUArXWs+5MVK6BbQvnNHlw==", "dev": true, "dependencies": { "base-64": "0.1.0", "clone": "2.1.1", - "component-emitter": "1.2.1", "linked-list": "0.1.0", "querystring": "0.2.0", - "sc-channel": "^1.2.0", - "sc-errors": "^1.4.0", - "sc-formatter": "^3.0.1", - "uuid": "3.2.1", - "ws": "5.1.1" + "sc-channel": "~1.0.6", + "sc-emitter": "~1.1.0", + "sc-errors": "~1.3.0", + "sc-formatter": "~3.0.0", + "ws": "3.0.0" } }, "node_modules/socketcluster-client/node_modules/querystring": { @@ -3390,16 +2838,6 @@ "node": ">=0.4.x" } }, - "node_modules/socketcluster-client/node_modules/uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -3738,6 +3176,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4055,14 +3499,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.1.tgz", - "integrity": "sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.0.0.tgz", + "integrity": "sha512-sjCOvLIEgRVT+inhGpm/f/YeusxCEg5BENrIj31YcOR+GTLcqIJ029uTmLVFNDJBCBvCxhkWFZrR6iMppq/s2A==", "dev": true, "dependencies": { - "async-limiter": "~1.0.0" + "safe-buffer": "~5.0.1", + "ultron": "~1.1.0" } }, + "node_modules/ws/node_modules/safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha512-cr7dZWLwOeaFBLTIuZeYdkfO7UzGIKhjYENJFAxUOMKWGaWDm2nJM2rzxNRm5Owu0DH3ApwNo6kx5idXZfb/Iw==", + "dev": true + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index e6eecbc9..a48e3a1b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@creativebulma/bulma-tooltip": "^1.2.0", "@nfdi4plants/exceljs": "^0.3.0", - "bulma": "^0.9.4", + "bulma": "^1.0.1", "bulma-checkradio": "^2.1.3", "bulma-slider": "^2.0.5", "bulma-switch": "^2.0.4", diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 0f97d662..64b7a372 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -86,6 +86,7 @@ + diff --git a/src/Client/MainComponents/EmptyTableElement.fs b/src/Client/MainComponents/EmptyTableElement.fs new file mode 100644 index 00000000..97d93dcf --- /dev/null +++ b/src/Client/MainComponents/EmptyTableElement.fs @@ -0,0 +1,28 @@ +namespace MainComponents + +open Feliz +open Feliz.Bulma +open ARCtrl + +type EmptyTableElement = + static member Main() = + Html.div [ + prop.className "is-flex is-justify-content-center is-align-items-center" + prop.style [style.height (length.perc 100)] + prop.children [ + Bulma.box [ + Bulma.content [ + prop.children [ + Html.h1 [ + prop.className "title" + prop.text "No data to display" + ] + Html.h2 [ + prop.className "subtitle" + prop.text "Please upload a file or create a new table" + ] + ] + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/MainComponents/Navbar.fs b/src/Client/MainComponents/Navbar.fs index f0365fc0..419f0e0b 100644 --- a/src/Client/MainComponents/Navbar.fs +++ b/src/Client/MainComponents/Navbar.fs @@ -66,7 +66,7 @@ let private quickAccessButtonListEnd (model: Model) dispatch = Bulma.icon [Html.i [prop.className "fa-sharp fa-solid fa-trash";]] ], (fun _ -> Modals.Controller.renderModal("ResetTableWarning", Modals.ResetTable.Main dispatch)), - buttonProps = [Bulma.color.isDanger; Bulma.button.isInverted; Bulma.button.isOutlined] + buttonProps = [Bulma.color.isDanger] ).toReactElement() ] ] diff --git a/src/Client/SharedComponents/QuickAccessButton.fs b/src/Client/SharedComponents/QuickAccessButton.fs index 3a82add8..471ffe7b 100644 --- a/src/Client/SharedComponents/QuickAccessButton.fs +++ b/src/Client/SharedComponents/QuickAccessButton.fs @@ -30,9 +30,9 @@ type QuickAccessButton = { prop.children [ Bulma.button.a [ prop.tabIndex (if isDisabled then -1 else 0) + prop.className "myNavbarButton" yield! this.ButtonProps prop.disabled isDisabled - prop.className "myNavbarButton" prop.onClick this.Msg prop.children [ Html.div [ diff --git a/src/Client/Views/XlsxFileView.fs b/src/Client/Views/XlsxFileView.fs index c45bffed..adaa447b 100644 --- a/src/Client/Views/XlsxFileView.fs +++ b/src/Client/Views/XlsxFileView.fs @@ -1,4 +1,4 @@ -module XlsxFileView +module XlsxFileView open Feliz open Feliz.Bulma @@ -10,7 +10,11 @@ open Shared let Main(model: Messages.Model, dispatch: Messages.Msg -> unit) = match model.SpreadsheetModel.ActiveView with | ActiveView.Table _ -> - MainComponents.SpreadsheetView.Main model dispatch + match model.SpreadsheetModel.ActiveTable.ColumnCount with + | 0 -> + MainComponents.EmptyTableElement.Main() + | _ -> + MainComponents.SpreadsheetView.Main model dispatch | ActiveView.Metadata -> Bulma.section [ Bulma.container [ diff --git a/src/Client/style.scss b/src/Client/style.scss index 0e5b2837..121a5e3d 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -1,76 +1,24 @@ @charset "utf-8"; -// 1. Import the initial variables -@import "../../node_modules/bulma/sass/utilities/initial-variables.sass"; -@import "../../node_modules/bulma/sass/utilities/functions.sass"; - -// 2. Set your own initial variables -html[data-theme="dark"] { - --scheme-main: hsl(0, 0%, 4%); - --scheme-invert: hsl(0, 0%, 100%); - --text: hsl(0, 0%, 100%); - --text-light: hsl(0, 0%, 96%); - --text-strong: hsl(0, 0%, 100%); - --fs-background: #252529; - --fs-foreground: #1e1e20; - --scheme-invert-rgb: 255,255,255; - --table-alternate: #363636 -} - -html[data-theme="light"] { - --scheme-main: hsl(0, 0%, 100%); - --scheme-invert: hsl(0, 0%, 4%); - --text: hsl(0, 0%, 4%); - --text-light: hsl(0, 0%, 14%); - --text-strong: hsl(0, 0%, 4%); - --fs-background: #f6f6f7; - --fs-foreground: #e3e3e5; - --scheme-invert-rgb: 0,0,0; - --table-alternate: #f2f2f2 -} - +// Set your brand colors $excel-primary: #217346; $excel-primary30: #639d7d; -$nfdi-blue-dark: #2D3E50 ; +$nfdi-blue-dark: #2D3E50; $nfdi-mint: #1FC2A7; $nfdi-blue-light: #4fb3d9; $nfdi-blue-lighter-20: #72c2e1; -$nfdi-red: #c21f3a;//#e83151;//#E32746; +$nfdi-red: #c21f3a; //#e83151;//#E32746; $nfdi-yellow: #FFC000; - -// global bulma overrides -$primary: $nfdi-blue-dark; -$success: $nfdi-mint; -$info: $nfdi-blue-light; -$danger: $nfdi-red; -$warning: $nfdi-yellow; - -// 3. Set the derived variables -$button-static-background-color: var(--scheme-main); -$border-color: hsl(0, 0%, 86%); -$scheme-main: var(--scheme-main, hsl(0, 0%, 100%)); -$scheme-invert: var(--scheme-invert, hsl(0, 0%, 4%)); -$text: var(--text); -$text-light: var(--text-light); -$text-strong: var(--text-strong); -$modal-background-background-color: var(--fs-background, #FF0000); -$content-blockquote-background-color: var(--fs-background); -$modal-card-head-background-color: var(--fs-foreground); -$tabs-boxed-link-hover-background-color: var(--fs-background); -$dropdown-item-hover-background-color: var(--fs-background); -$shadow: 0 0.5em 1em -0.125em rgba(var(--scheme-invert-rgb), 0.1), 0 0px 0 1px rgba(var(--scheme-invert-rgb), 0.02); - -// 4. set up custom color $primarye: $excel-primary; -$primarye-invert: findColorInvert($excel-primary); +$primarye-invert: white; -// 5. Add new color variables to the color map. -@import "../../node_modules/bulma/sass/utilities/derived-variables"; -$addColors: ( "primarye":($primarye, $primarye-invert) ); -$colors: map-merge($colors, $addColors); +// 1. Import the initial variables +@use "bulma/sass" with ( $primary: $nfdi-blue-dark, $success: $nfdi-mint, $info: $nfdi-blue-light, $danger: $nfdi-red, $warning: $nfdi-yellow, $custom-colors: ( "primarye":($primarye, $primarye-invert)), ); +@forward "bulma/sass/themes"; -@import "../../node_modules/bulma/bulma.sass"; -/*@import "../../node_modules/bulma-checkradio/src/sass/index.sass";*/ +:root { + --bulma-duration: 0.1s; +} .main-contrast-bg { background-color: var(--scheme-main) @@ -137,8 +85,8 @@ $colors: map-merge($colors, $addColors); width: 100%; max-height: 400px; overflow: auto; - border: 1px solid $border-color; - background-color: $scheme-main; + border: 1px solid var(--bulma-border-color); + background-color: var(--bulma-scheme-main); border-top: unset; z-index: 20; @@ -161,8 +109,8 @@ $colors: map-merge($colors, $addColors); } .term-select-item-more { - border-top: .5px solid $border-color; - box-shadow: 0px 4px 8px $border-color; + border-top: .5px solid var(--bulma-border-color); + box-shadow: 0px 4px 8px var(--bulma-border-color); td { border: unset; @@ -176,7 +124,7 @@ $colors: map-merge($colors, $addColors); background-color: transparent; border: unset; cursor: pointer; - color: $scheme-invert; + color: var(--bulma-scheme-invert); justify-content: center; align-items: center; border-radius: 2px; @@ -184,12 +132,12 @@ $colors: map-merge($colors, $addColors); } .term-select-item-toggle-button:hover { - background-color: $content-blockquote-background-color + background-color: var(--bulma-content-blockquote-background-color) } } .term-select-item:not(:last-child) { - border-bottom: 1px solid $border-color; + border-bottom: 1px solid var(--bulma-border-color); } } @@ -291,7 +239,7 @@ a:hover { position: -webkit-sticky; min-height: min-content; top: 0; - background-color: $primary; + background-color: var(--bulma-primary); } .myNavbarSticky .navbar-item#logo { @@ -305,7 +253,7 @@ a:hover { top: 50%; left: 50%; box-shadow: 0 0 15px 5px #fff, /* inner white */ - 0 0 30px 15px $success; + 0 0 30px 15px var(--bulma-success); } } @@ -329,7 +277,13 @@ a:hover { .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within, .myNavbarButton:not([disabled]):hover { background-color: transparent; - color: $success + color: var(--bulma-success) +} + + +.myNavbarButton.is-danger:not([disabled]):focus, .myNavbarButton.is-danger:not([disabled]):focus-within, .myNavbarButton.is-danger:not([disabled]):hover { + background-color: transparent; + color: var(--bulma-danger) } .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within { @@ -408,7 +362,7 @@ a.navbar-item:hover { .delete:hover { @extend .delete; - background-color: $danger + background-color: var(--bulma-danger) } .hoverTableEle { @@ -430,12 +384,12 @@ a.navbar-item:hover { .clickableTagDelete { cursor: pointer; - border: 0.2px solid $info + border: 0.2px solid var(--bulma-info) } .clickableTagDelete:hover { - background-color: $danger !important; - border-color: $danger !important; + background-color: var(--bulma-danger) !important; + border-color: var(--bulma-anger) !important; color: white } @@ -491,7 +445,7 @@ a.navbar-item:hover { } /* When the checkbox is checked, add a green background */ .checkbox-input:checked ~ .checkbox-checkmark { - background-color: $success; + background-color: var(--bulma-success); border: none; } /* Create the checkmark/indicator (hidden when not checked) */ @@ -663,7 +617,7 @@ input::-ms-clear { } .danger_important { - color: $danger !important + color: var(--bulma-danger) !important } kbd { diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 5cabdff5..a4bfb418 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -59,6 +59,8 @@ module Extensions = type OntologyAnnotation with static member empty() = OntologyAnnotation.create() + static member fromTerm (term:Term) = OntologyAnnotation(term.Name, term.FK_Ontology, term.Accession) + member this.ToTermMinimal() = TermMinimal.create this.NameText this.TermAccessionShort type ArcTable with member this.SetCellAt(columnIndex: int, rowIndex: int, cell: CompositeCell) = @@ -226,7 +228,3 @@ module Extensions = | CompositeCell.Term oa -> CompositeCell.Term (updateTAN oa) | CompositeCell.Unitized (v, oa) -> CompositeCell.Unitized (v, updateTAN oa) | _ -> this - - type OntologyAnnotation with - static member fromTerm (term:Term) = OntologyAnnotation(term.Name, term.FK_Ontology, term.Accession) - member this.ToTermMinimal() = TermMinimal.create this.NameText this.TermAccessionShort From cab55633b8719b2ed277e34a034a460af8c63d85 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 19 Jun 2024 16:23:22 +0200 Subject: [PATCH 113/135] fix widget styling and add empt table placeholder --- .../MainComponents/EmptyTableElement.fs | 38 ++++++++++++++++--- src/Client/MainComponents/Widgets.fs | 20 ++++++---- src/Client/Views/MainWindowView.fs | 25 ++++++------ src/Client/Views/XlsxFileView.fs | 4 +- src/Client/style.scss | 16 ++++++++ 5 files changed, 77 insertions(+), 26 deletions(-) diff --git a/src/Client/MainComponents/EmptyTableElement.fs b/src/Client/MainComponents/EmptyTableElement.fs index 97d93dcf..56b74273 100644 --- a/src/Client/MainComponents/EmptyTableElement.fs +++ b/src/Client/MainComponents/EmptyTableElement.fs @@ -5,7 +5,7 @@ open Feliz.Bulma open ARCtrl type EmptyTableElement = - static member Main() = + static member Main(openBuildingBlockWidget: unit -> unit, openTemplateWidget: unit -> unit) = Html.div [ prop.className "is-flex is-justify-content-center is-align-items-center" prop.style [style.height (length.perc 100)] @@ -13,13 +13,39 @@ type EmptyTableElement = Bulma.box [ Bulma.content [ prop.children [ - Html.h1 [ + Html.h3 [ prop.className "title" - prop.text "No data to display" + prop.text "New Table!" ] - Html.h2 [ - prop.className "subtitle" - prop.text "Please upload a file or create a new table" + Bulma.field.div [ + prop.className "is-flex is-justify-content-space-between is-align-items-center gap-3" + prop.children [ + Html.text "Start from an existing template!" + Bulma.button.span [ + prop.onClick (fun _ -> openTemplateWidget()) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table" ] + ] + ] + ] + ] + ] + Bulma.field.div [ + prop.className "is-flex is-justify-content-space-between is-align-items-center gap-3" + prop.children [ + Html.text "Or start from scratch!" + Bulma.button.span [ + prop.onClick (fun _ -> openBuildingBlockWidget()) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-plus" ] + Html.i [prop.className "fa-solid fa-table-columns" ] + ] + ] + ] + ] ] ] ] diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index 34327e9f..e4c47e5d 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -104,7 +104,7 @@ type Widget = let element = React.useElementRef() React.useLayoutEffectOnce(fun _ -> position |> Option.iter (fun position -> MoveEventListener.ensurePositionInsideWindow element position |> Some |> setPosition)) // Reposition widget inside window let resizeElement (content: ReactElement) = - Bulma.modalCard [ + Bulma.card [ prop.ref element prop.onMouseDown(fun e -> // resize e.preventDefault() @@ -122,8 +122,10 @@ type Widget = style.zIndex 40 style.cursor.eastWestResize//style.cursor.northWestSouthEastResize ; style.display.flex - style.paddingRight(2); style.overflow.visible + style.paddingRight(2); + style.overflow.visible style.position.fixedRelativeToWindow + style.minWidth.minContent if size.IsSome then style.width size.Value.X //style.height size.Value.Y @@ -139,7 +141,7 @@ type Widget = prop.onMouseDown(fun e -> e.stopPropagation()) prop.style [style.cursor.defaultCursor; style.display.flex; style.flexDirection.column; style.flexGrow 1] prop.children [ - Bulma.modalCardHead [ + Bulma.cardHeader [ prop.onMouseDown(fun e -> // move e.preventDefault() e.stopPropagation() @@ -155,18 +157,22 @@ type Widget = ) prop.style [style.cursor.move] prop.children [ - Bulma.modalCardTitle Html.none - Bulma.delete [ prop.onClick (fun e -> e.stopPropagation(); rmv e) ] + Bulma.cardHeaderTitle.p Html.none + Bulma.cardHeaderIcon.a [ + Bulma.delete [ + prop.onClick (fun e -> e.stopPropagation(); rmv e) + ] + ] ] ] - Bulma.modalCardBody [ + Bulma.cardContent [ prop.style [style.overflow.inheritFromParent] prop.children [ content if help.IsSome then Elements.helpExtendButton (fun _ -> setHelpIsActive (not helpIsActive)) ] ] - Bulma.modalCardFoot [ + Bulma.cardFooter [ prop.style [style.padding 5] if help.IsSome then prop.children [ diff --git a/src/Client/Views/MainWindowView.fs b/src/Client/Views/MainWindowView.fs index 518320b5..795e8fbb 100644 --- a/src/Client/Views/MainWindowView.fs +++ b/src/Client/Views/MainWindowView.fs @@ -16,15 +16,8 @@ let private WidgetOrderContainer bringWidgetToFront (widget) = ] ] -let private ModalDisplay (widgets: Widget list, rmvWidget: Widget -> unit, bringWidgetToFront: Widget -> unit, model, dispatch) = - let rmv (widget: Widget) = fun _ -> rmvWidget widget - let displayWidget (widget: Widget) = - let bringWidgetToFront = fun _ -> bringWidgetToFront widget - match widget with - | Widget._BuildingBlock -> Widget.BuildingBlock (model, dispatch, rmv widget) - | Widget._Template -> Widget.Templates (model, dispatch, rmv widget) - | Widget._FilePicker -> Widget.FilePicker (model, dispatch, rmv widget) - |> WidgetOrderContainer bringWidgetToFront +let private ModalDisplay (widgets: Widget list, displayWidget: Widget -> ReactElement) = + match widgets.Length with | 0 -> Html.none @@ -73,6 +66,16 @@ let Main (model: Messages.Model, dispatch) = let bringWidgetToFront (widget: Widget) = let newList = widgets |> List.except [widget] |> fun x -> widget::x |> List.rev setWidgets newList + let displayWidget (widget: Widget) = + let rmv (widget: Widget) = fun _ -> rmvWidget widget + let bringWidgetToFront = fun _ -> bringWidgetToFront widget + match widget with + | Widget._BuildingBlock -> Widget.BuildingBlock (model, dispatch, rmv widget) + | Widget._Template -> Widget.Templates (model, dispatch, rmv widget) + | Widget._FilePicker -> Widget.FilePicker (model, dispatch, rmv widget) + |> WidgetOrderContainer bringWidgetToFront + let addWidget (widget: Widget) = + widget::widgets |> List.rev |> setWidgets let state = model.SpreadsheetModel Html.div [ prop.id "MainWindow" @@ -84,7 +87,7 @@ let Main (model: Messages.Model, dispatch) = ] prop.children [ MainComponents.Navbar.Main (model, dispatch, widgets, setWidgets) - ModalDisplay (widgets, rmvWidget, bringWidgetToFront, model, dispatch) + ModalDisplay (widgets, displayWidget) Html.div [ prop.id "TableContainer" prop.style [ @@ -104,7 +107,7 @@ let Main (model: Messages.Model, dispatch) = | Some (ArcFiles.Investigation _) | Some (ArcFiles.Template _) -> Html.none - XlsxFileView.Main (model , dispatch) + XlsxFileView.Main (model, dispatch, (fun () -> addWidget Widget._BuildingBlock), (fun () -> addWidget Widget._Template)) if state.Tables.TableCount > 0 && state.ActiveTable.ColumnCount > 0 && state.ActiveView <> Spreadsheet.ActiveView.Metadata then MainComponents.AddRows.Main dispatch ] diff --git a/src/Client/Views/XlsxFileView.fs b/src/Client/Views/XlsxFileView.fs index adaa447b..d530d332 100644 --- a/src/Client/Views/XlsxFileView.fs +++ b/src/Client/Views/XlsxFileView.fs @@ -7,12 +7,12 @@ open Spreadsheet open Shared [] -let Main(model: Messages.Model, dispatch: Messages.Msg -> unit) = +let Main(model: Messages.Model, dispatch: Messages.Msg -> unit, openBuildingBlockWidget, openTemplateWidget) = match model.SpreadsheetModel.ActiveView with | ActiveView.Table _ -> match model.SpreadsheetModel.ActiveTable.ColumnCount with | 0 -> - MainComponents.EmptyTableElement.Main() + MainComponents.EmptyTableElement.Main(openBuildingBlockWidget, openTemplateWidget) | _ -> MainComponents.SpreadsheetView.Main model dispatch | ActiveView.Metadata -> diff --git a/src/Client/style.scss b/src/Client/style.scss index 121a5e3d..a4365a5e 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -16,6 +16,22 @@ $primarye-invert: white; @use "bulma/sass" with ( $primary: $nfdi-blue-dark, $success: $nfdi-mint, $info: $nfdi-blue-light, $danger: $nfdi-red, $warning: $nfdi-yellow, $custom-colors: ( "primarye":($primarye, $primarye-invert)), ); @forward "bulma/sass/themes"; +.gap-1 { + gap: 0.25rem +} + +.gap-2 { + gap: 0.5rem +} + +.gap-3 { + gap: 1rem +} + +.gap-4 { + gap: 1.5rem +} + :root { --bulma-duration: 0.1s; } From 8d42b06a5c0c92c57cdf474f4e11df82f17b174e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 19 Jun 2024 16:26:14 +0200 Subject: [PATCH 114/135] Fix bulma "+" button on forms :art: --- src/Client/MainComponents/Metadata/Forms.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Client/MainComponents/Metadata/Forms.fs b/src/Client/MainComponents/Metadata/Forms.fs index e9768c52..82a909e7 100644 --- a/src/Client/MainComponents/Metadata/Forms.fs +++ b/src/Client/MainComponents/Metadata/Forms.fs @@ -1,4 +1,4 @@ -namespace MainComponents.Metadata +namespace MainComponents.Metadata open Feliz open Feliz.Bulma @@ -261,7 +261,6 @@ module private Helper = prop.classes ["is-flex"; "is-justify-content-center"] prop.children [ Bulma.button.button [ - prop.className "is-outlined" prop.text "+" prop.onClick clickEvent ] From 710afc4e7f5efcf5974e4a31522068d49c8bb1ff Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 11:24:59 +0200 Subject: [PATCH 115/135] complete update to bulma v1.0.0 #435 :art: --- src/Client/MainComponents/SpreadsheetView.fs | 27 +++++++--- .../Pages/BuildingBlock/SearchComponent.fs | 3 +- src/Client/SidebarComponents/Navbar.fs | 1 + src/Client/style.scss | 51 +++++++++++-------- 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index 739f7c54..2ccd674d 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -35,15 +35,26 @@ let private cellPlaceholder (c_opt: CompositeCell option) = let private RowLabel (rowIndex: int) = let t : IReactProperty list -> ReactElement = if rowIndex < 0 then Html.th else Html.td t [ - prop.style [style.resize.none; style.border(length.px 1, borderStyle.solid, "darkgrey")] + //prop.style [style.resize.none; style.border(length.px 1, borderStyle.solid, "darkgrey")] + //prop.children [ + // Bulma.button.button [ + // prop.className "px-2 py-1" + // prop.style [style.custom ("border", "unset"); style.borderRadius 0] + // Bulma.button.isFullWidth + // Bulma.button.isStatic + // prop.tabIndex -1 + // prop.text (if rowIndex < 0 then "" else $"{rowIndex+1}") + // ] + //] + prop.style [style.resize.none; style.border(length.px 1, borderStyle.solid, "darkgrey"); style.height(length.perc 100)] prop.children [ - Bulma.button.button [ - prop.className "px-2 py-1" - prop.style [style.custom ("border", "unset"); style.borderRadius 0] - Bulma.button.isFullWidth - Bulma.button.isStatic - prop.tabIndex -1 - prop.text (if rowIndex < 0 then "" else $"{rowIndex+1}") + Html.div [ + prop.style [style.height(length.perc 100);] + prop.className "is-flex is-justify-content-center is-align-items-center px-2 is-unselectable my-grey-out" + prop.disabled true + prop.children [ + Html.b (if rowIndex < 0 then "" else $"{rowIndex+1}") + ] ] ] ] diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index a1fb98f7..a818135f 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -1,4 +1,4 @@ -module BuildingBlock.SearchComponent +module BuildingBlock.SearchComponent open Feliz open Feliz.Bulma @@ -125,7 +125,6 @@ let private addBuildingBlockButton (model: Model) dispatch = let isValid = Helper.isValidColumn header if isValid then Bulma.color.isSuccess - Bulma.button.isActive else Bulma.color.isDanger prop.disabled true diff --git a/src/Client/SidebarComponents/Navbar.fs b/src/Client/SidebarComponents/Navbar.fs index d5bb9bf8..0bc9471f 100644 --- a/src/Client/SidebarComponents/Navbar.fs +++ b/src/Client/SidebarComponents/Navbar.fs @@ -195,6 +195,7 @@ let NavbarComponent (model : Model) (dispatch : Msg -> unit) (sidebarsize: Model span [AriaHidden true] [ ] span [AriaHidden true] [ ] span [AriaHidden true] [ ] + span [AriaHidden true] [ ] ] ] ] diff --git a/src/Client/style.scss b/src/Client/style.scss index a4365a5e..a9dc3f9f 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -15,6 +15,12 @@ $primarye-invert: white; // 1. Import the initial variables @use "bulma/sass" with ( $primary: $nfdi-blue-dark, $success: $nfdi-mint, $info: $nfdi-blue-light, $danger: $nfdi-red, $warning: $nfdi-yellow, $custom-colors: ( "primarye":($primarye, $primarye-invert)), ); @forward "bulma/sass/themes"; +@use "bulma/sass/utilities/css-variables" as cv; + +.my-grey-out { + background-color: cv.getVar("scheme-main-ter"); + color: cv.getVar("link") +} .gap-1 { gap: 0.25rem @@ -37,7 +43,7 @@ $primarye-invert: white; } .main-contrast-bg { - background-color: var(--scheme-main) + background-color: cv.getVar("scheme-main") } .template-filter-container { @@ -68,7 +74,7 @@ $primarye-invert: white; position: sticky; top: 0; z-index: 1; - background-color: var(--scheme-main) + background-color: cv.getVar("scheme-main") } } @@ -101,8 +107,8 @@ $primarye-invert: white; width: 100%; max-height: 400px; overflow: auto; - border: 1px solid var(--bulma-border-color); - background-color: var(--bulma-scheme-main); + border: 1px solid cv.getVar("border"); + background-color: cv.getVar("scheme-main"); border-top: unset; z-index: 20; @@ -125,8 +131,8 @@ $primarye-invert: white; } .term-select-item-more { - border-top: .5px solid var(--bulma-border-color); - box-shadow: 0px 4px 8px var(--bulma-border-color); + border-top: .5px solid cv.getVar("border"); + box-shadow: 0px 4px 8px cv.getVar("border"); td { border: unset; @@ -140,7 +146,7 @@ $primarye-invert: white; background-color: transparent; border: unset; cursor: pointer; - color: var(--bulma-scheme-invert); + color: cv.getVar("scheme-invert"); justify-content: center; align-items: center; border-radius: 2px; @@ -148,12 +154,12 @@ $primarye-invert: white; } .term-select-item-toggle-button:hover { - background-color: var(--bulma-content-blockquote-background-color) + background-color: cv.getVar("background") } } .term-select-item:not(:last-child) { - border-bottom: 1px solid var(--bulma-border-color); + border-bottom: 1px solid cv.getVar("border"); } } @@ -178,7 +184,7 @@ $primarye-invert: white; } .fixed_headers tbody tr:nth-child(even) { - background-color: var(--table-alternate); + background-color: cv.getVar("scheme-main-bis"); } .fixed_headers thead th { @@ -197,11 +203,16 @@ html, body { overflow: auto } -a { + +table { + height: fit-content; +} + +a:not([class]){ color: $nfdi-blue-light } -a:hover { +a:not([class]):hover { color: $nfdi-blue-lighter-20 } @@ -255,7 +266,7 @@ a:hover { position: -webkit-sticky; min-height: min-content; top: 0; - background-color: var(--bulma-primary); + background-color: cv.getVar("primary"); } .myNavbarSticky .navbar-item#logo { @@ -269,7 +280,7 @@ a:hover { top: 50%; left: 50%; box-shadow: 0 0 15px 5px #fff, /* inner white */ - 0 0 30px 15px var(--bulma-success); + 0 0 30px 15px cv.getVar("success"); } } @@ -293,13 +304,13 @@ a:hover { .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within, .myNavbarButton:not([disabled]):hover { background-color: transparent; - color: var(--bulma-success) + color: cv.getVar("success") } .myNavbarButton.is-danger:not([disabled]):focus, .myNavbarButton.is-danger:not([disabled]):focus-within, .myNavbarButton.is-danger:not([disabled]):hover { background-color: transparent; - color: var(--bulma-danger) + color: cv.getVar("danger") } .myNavbarButton:not([disabled]):focus, .myNavbarButton:not([disabled]):focus-within { @@ -404,8 +415,8 @@ a.navbar-item:hover { } .clickableTagDelete:hover { - background-color: var(--bulma-danger) !important; - border-color: var(--bulma-anger) !important; + background-color: cv.getVar("danger") !important; + border-color: cv.getVar("danger") !important; color: white } @@ -461,7 +472,7 @@ a.navbar-item:hover { } /* When the checkbox is checked, add a green background */ .checkbox-input:checked ~ .checkbox-checkmark { - background-color: var(--bulma-success); + background-color: cv.getVar("success"); border: none; } /* Create the checkmark/indicator (hidden when not checked) */ @@ -633,7 +644,7 @@ input::-ms-clear { } .danger_important { - color: var(--bulma-danger) !important + color: cv.getVar("danger") !important } kbd { From f6f2c043ac7c61667d082663c4f193f903ff9417 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 13:44:51 +0200 Subject: [PATCH 116/135] Fix template filter index issue #436 :bug: --- .../Pages/BuildingBlock/SearchComponent.fs | 16 ++++++++++------ .../Pages/ProtocolTemplates/ProtocolView.fs | 10 ++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index a818135f..2c12de5d 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -107,12 +107,16 @@ let private scrollIntoViewRetry (id: string) = else () else - let config = createEmpty - config.behavior <- Browser.Types.ScrollBehavior.Smooth - config.block <- Browser.Types.ScrollAlignment.End - config.``inline`` <- Browser.Types.ScrollAlignment.End - log headerelement - headerelement.scrollIntoView(config) + let rect = headerelement.getBoundingClientRect() + if rect.left >= 0 then + () + else + let config = createEmpty + config.behavior <- Browser.Types.ScrollBehavior.Smooth + config.block <- Browser.Types.ScrollAlignment.End + config.``inline`` <- Browser.Types.ScrollAlignment.End + //log headerelement + headerelement.scrollIntoView(config) loop 0 diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index d54a4e33..ebe2c8cd 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -161,12 +161,14 @@ module TemplateFromDB = let mutable columnsToRemove = [] // find duplicate columns let tablecopy = model.ProtocolState.TemplateSelected.Value.Table.Copy() - for header in tablecopy.Headers do - let containsAtIndex = model.SpreadsheetModel.ActiveTable.Headers.FindIndex(fun h -> h = header) - if containsAtIndex >= 0 then - columnsToRemove <- containsAtIndex::columnsToRemove + for header in model.SpreadsheetModel.ActiveTable.Headers do + let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) + if containsAtIndex.IsSome then + columnsToRemove <- containsAtIndex.Value::columnsToRemove + log columnsToRemove tablecopy.RemoveColumns (Array.ofList columnsToRemove) let index = Spreadsheet.Sidebar.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel + log index SpreadsheetInterface.JoinTable (tablecopy, Some index, Some ARCtrl.TableJoinOptions.WithUnit ) |> InterfaceMsg |> dispatch ) prop.text "Add template" From 7dcbc9a38708b1cbbef0ea8a247de43dc9127d9e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 13:50:12 +0200 Subject: [PATCH 117/135] Improve scrollIntoView behavior #437 --- src/Client/Pages/BuildingBlock/SearchComponent.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index 2c12de5d..83273690 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -108,7 +108,7 @@ let private scrollIntoViewRetry (id: string) = () else let rect = headerelement.getBoundingClientRect() - if rect.left >= 0 then + if rect.left >= 0 && ((rect.right <= Browser.Dom.window.innerWidth) || (rect.right <= Browser.Dom.document.documentElement.clientWidth)) then () else let config = createEmpty From e31b7154ec8c59c7bcf6e1c762cd1a90df10ea06 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 14:39:53 +0200 Subject: [PATCH 118/135] Fix table rename without name change :bug: --- src/Client/MainComponents/FooterTabs.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Client/MainComponents/FooterTabs.fs b/src/Client/MainComponents/FooterTabs.fs index cff9e46e..60a96953 100644 --- a/src/Client/MainComponents/FooterTabs.fs +++ b/src/Client/MainComponents/FooterTabs.fs @@ -134,7 +134,8 @@ let Main (index: int, tables: ArcTables, model: Messages.Model, dispatch: Messag prop.children [ if state.IsEditable then let updateName = fun e -> - Spreadsheet.RenameTable (index, state.Name) |> Messages.SpreadsheetMsg |> dispatch + if state.Name <> table.Name then + Spreadsheet.RenameTable (index, state.Name) |> Messages.SpreadsheetMsg |> dispatch setState {state with IsEditable = false} Bulma.input.text [ prop.autoFocus(true) From a3fd5818fab08d42b0d77352787373e616c49c8c Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 15:05:57 +0200 Subject: [PATCH 119/135] Fix not removing selected cell on table switch #439 :bug: --- src/Client/Client.fsproj | 2 +- .../Pages/BuildingBlock/SearchComponent.fs | 2 +- .../Pages/ProtocolTemplates/ProtocolView.fs | 3 +- ...roller.fs => BuildingBlocks.Controller.fs} | 45 +------------ src/Client/Spreadsheet/Table.Controller.fs | 65 +++++++++++++++---- src/Client/Update/SpreadsheetUpdate.fs | 5 +- 6 files changed, 56 insertions(+), 66 deletions(-) rename src/Client/Spreadsheet/{Sidebar.Controller.fs => BuildingBlocks.Controller.fs} (74%) diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 64b7a372..6766400b 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -59,7 +59,7 @@ - + diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index 83273690..4abd2a15 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -135,7 +135,7 @@ let private addBuildingBlockButton (model: Model) dispatch = Bulma.button.isFullWidth prop.onClick (fun _ -> let column = CompositeColumn.create(header, [|if body.IsSome then body.Value|]) - let index = Spreadsheet.Sidebar.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel + let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch let id = $"Header_{index}_Main" scrollIntoViewRetry id diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index ebe2c8cd..fa95decb 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -167,8 +167,7 @@ module TemplateFromDB = columnsToRemove <- containsAtIndex.Value::columnsToRemove log columnsToRemove tablecopy.RemoveColumns (Array.ofList columnsToRemove) - let index = Spreadsheet.Sidebar.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel - log index + let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.JoinTable (tablecopy, Some index, Some ARCtrl.TableJoinOptions.WithUnit ) |> InterfaceMsg |> dispatch ) prop.text "Add template" diff --git a/src/Client/Spreadsheet/Sidebar.Controller.fs b/src/Client/Spreadsheet/BuildingBlocks.Controller.fs similarity index 74% rename from src/Client/Spreadsheet/Sidebar.Controller.fs rename to src/Client/Spreadsheet/BuildingBlocks.Controller.fs index 445141dd..393ef38d 100644 --- a/src/Client/Spreadsheet/Sidebar.Controller.fs +++ b/src/Client/Spreadsheet/BuildingBlocks.Controller.fs @@ -1,4 +1,4 @@ -module Spreadsheet.Sidebar.Controller +module Spreadsheet.BuildingBlocks.Controller open System.Collections.Generic open Shared.TermTypes @@ -10,17 +10,6 @@ open Shared module SidebarControllerAux = - let rec createNewTableName (ind: int) names = - let name = "NewTable" + string ind - if Seq.contains name names then - createNewTableName (ind+1) names - else - name - - /// Uses current `ActiveTableIndex` to return next `ActiveTableIndex` whenever a new table is added and we want to - /// switch to the new table. - let getNextActiveTableIndex (state: Spreadsheet.Model) = - if state.Tables.TableCount = 0 then 0 else state.ActiveView.TableIndex + 1 /// /// Uses the first selected columnIndex from `state.SelectedCells` to determine if new column should be inserted or appended. @@ -45,38 +34,6 @@ module SanityChecks = open SidebarControllerAux -/// This is the basic function to create new Tables from an array of SwateBuildingBlocks -let addTable (newTable: ArcTable) (state: Spreadsheet.Model) : Spreadsheet.Model = - let tables = state.Tables - // calculate next index - let newIndex = getNextActiveTableIndex state - tables.AddTable(newTable, newIndex) - { state with - ArcFile = state.ArcFile - ActiveView = ActiveView.Table newIndex } - -/// This function is used to create multiple tables at once. -let addTables (tables: ArcTable []) (state: Spreadsheet.Model) : Spreadsheet.Model = - let newIndex = getNextActiveTableIndex state - state.Tables.AddTables(tables, newIndex) - { state with - ArcFile = state.ArcFile - ActiveView = ActiveView.Table (newIndex + tables.Length) } - - -/// Adds the most basic empty Swate table with auto generated name. -let createTable (usePrevOutput:bool) (state: Spreadsheet.Model) : Spreadsheet.Model = - let tables = state.ArcFile.Value.Tables() - let newName = createNewTableName 0 tables.TableNames - let newTable = ArcTable.init(newName) - if usePrevOutput && (tables.TableCount-1) >= state.ActiveView.TableIndex then - let table = tables.GetTableAt(state.ActiveView.TableIndex) - let output = table.GetOutputColumn() - let newInput = output.Header.TryOutput().Value |> CompositeHeader.Input - newTable.AddColumn(newInput,output.Cells,forceReplace=true) - let nextState = {state with ArcFile = state.ArcFile} - addTable newTable nextState - let addBuildingBlock(newColumn: CompositeColumn) (state: Spreadsheet.Model) : Spreadsheet.Model = let table = state.ActiveTable // add one to last column index OR to selected column index to append one to the right. diff --git a/src/Client/Spreadsheet/Table.Controller.fs b/src/Client/Spreadsheet/Table.Controller.fs index 01cd1180..99b1d39e 100644 --- a/src/Client/Spreadsheet/Table.Controller.fs +++ b/src/Client/Spreadsheet/Table.Controller.fs @@ -10,6 +10,13 @@ open Shared module ControllerTableAux = + let rec createNewTableName (ind: int) names = + let name = "NewTable" + string ind + if Seq.contains name names then + createNewTableName (ind+1) names + else + name + let findEarlierTable (tableIndex:int) (tables: ArcTables) = let indices = [ 0 .. tables.TableCount-1 ] let lower = indices |> Seq.tryFindBack (fun k -> k < tableIndex) @@ -25,6 +32,40 @@ module ControllerTableAux = open ControllerTableAux +let switchTable (nextIndex: int) (state: Spreadsheet.Model) : Spreadsheet.Model = + match state.ActiveView with + | ActiveView.Table i when i = nextIndex -> state + | _ -> + { state with + ActiveCell = None + SelectedCells = Set.empty + ActiveView = ActiveView.Table nextIndex } + +/// This is the basic function to create new Tables from an array of SwateBuildingBlocks +let addTable (newTable: ArcTable) (state: Spreadsheet.Model) : Spreadsheet.Model = + state.Tables.AddTable(newTable) + switchTable (state.Tables.TableCount - 1) state + + +/// This function is used to create multiple tables at once. +let addTables (tables: ArcTable []) (state: Spreadsheet.Model) : Spreadsheet.Model = + state.Tables.AddTables(tables) + switchTable (state.Tables.TableCount - 1) state + + +/// Adds the most basic empty Swate table with auto generated name. +let createTable (usePrevOutput:bool) (state: Spreadsheet.Model) : Spreadsheet.Model = + let tables = state.ArcFile.Value.Tables() + let newName = createNewTableName 0 tables.TableNames + let newTable = ArcTable.init(newName) + if usePrevOutput && ((tables.TableCount-1) >= state.ActiveView.TableIndex) then + let table = tables.GetTableAt(state.ActiveView.TableIndex) + let output = table.GetOutputColumn() + let newInput = output.Header.TryOutput().Value |> CompositeHeader.Input + newTable.AddColumn(newInput,output.Cells,forceReplace=true) + let nextState = {state with ArcFile = state.ArcFile} + addTable newTable nextState + let updateTableOrder (prevIndex:int, newIndex:int) (state:Spreadsheet.Model) = state.Tables.MoveTable(prevIndex, newIndex) {state with ArcFile = state.ArcFile} @@ -45,22 +86,18 @@ let removeTable (removeIndex: int) (state: Spreadsheet.Model) : Spreadsheet.Mode // if active table is removed get the next closest table and set it active match state.ActiveView with | ActiveView.Table i when i = removeIndex -> - let nextView = - let neighbors = findNeighborTables removeIndex state.Tables - match neighbors with - | Some (i, _), _ -> ActiveView.Table i - | None, Some (i, _) -> ActiveView.Table i - // This is a fallback option, which should never be hit - | _ -> ActiveView.Metadata - { state with - ArcFile = state.ArcFile - ActiveView = nextView } + let neighbors = findNeighborTables removeIndex state.Tables + match neighbors with + | Some (i, _), _ -> + switchTable i state + | None, Some (i, _) -> + switchTable i state + | _ -> { state with ActiveView = ActiveView.Metadata } | ActiveView.Table i -> // Tables still exist and an inactive one was removed. Just remove it. - let nextTable_Index = if i > removeIndex then i - 1 else i + let nextTableIndex = if i > removeIndex then i - 1 else i { state with - ArcFile = state.ArcFile - ActiveView = ActiveView.Table nextTable_Index } - | _ -> state + ActiveView = ActiveView.Table nextTableIndex } + | _ -> {state with ActiveView = ActiveView.Metadata } ///Add `n` rows to active table. let addRows (n: int) (state: Spreadsheet.Model) : Spreadsheet.Model = diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index 8d42f560..d32326a4 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -7,16 +7,13 @@ open LocalHistory open Model open Shared open Spreadsheet.Table -open Spreadsheet.Sidebar +open Spreadsheet.BuildingBlocks open Spreadsheet.Clipboard open Fable.Remoting.Client -open Fable.Remoting.Client.InternalUtilities open FsSpreadsheet open FsSpreadsheet.Js open ARCtrl open ARCtrl.Spreadsheet -open Spreadsheet.Sidebar.Controller -open Feliz module Spreadsheet = From 7db1d25047dc1e30109116adb5d4efe0a6b084f3 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 16:08:13 +0200 Subject: [PATCH 120/135] Fox replacing values for input/output columns while template import #416 :bug: --- .../Pages/ProtocolTemplates/ProtocolView.fs | 21 ++++++++++++++++++- src/Shared/ARCtrl.Helper.fs | 15 +------------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index fa95decb..f1b797e0 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -167,8 +167,27 @@ module TemplateFromDB = columnsToRemove <- containsAtIndex.Value::columnsToRemove log columnsToRemove tablecopy.RemoveColumns (Array.ofList columnsToRemove) + tablecopy.IteriColumns(fun i c0 -> + let c1 = {c0 with Cells = [||]} + let c2 = + if c1.Header.isInput then + match model.SpreadsheetModel.ActiveTable.TryGetInputColumn() with + | Some ic -> + {c1 with Cells = ic.Cells} + | _ -> c1 + elif c1.Header.isOutput then + match model.SpreadsheetModel.ActiveTable.TryGetOutputColumn() with + | Some oc -> + {c1 with Cells = oc.Cells} + | _ -> c1 + else + c1 + tablecopy.UpdateColumn(i, c2.Header, c2.Cells) + ) + log(tablecopy.RowCount, tablecopy.ColumnCount) let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel - SpreadsheetInterface.JoinTable (tablecopy, Some index, Some ARCtrl.TableJoinOptions.WithUnit ) |> InterfaceMsg |> dispatch + let joinConfig = Some ARCtrl.TableJoinOptions.WithValues // If changed to anything else we need different logic to keep input/output values + SpreadsheetInterface.JoinTable (tablecopy, Some index, joinConfig ) |> InterfaceMsg |> dispatch ) prop.text "Add template" ] diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index a4bfb418..4ab3a1c6 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -82,20 +82,7 @@ module Extensions = let updateBody = Helper.dictMoveColumn currentIndex nextIndex this.Values () - - member this.SetColumn(index: int, column: CompositeColumn) = - column.Validate(true) |> ignore - this.Headers.[index] <- column.Header - let cells = column.Cells - let keys = this.Values.Keys - for (ci, ri) in keys do - if ci = index then - let nextCell = cells |> Array.tryItem ri - match nextCell with - | Some c -> - this.Values.[(ci,ri)] <- c - | None -> - this.Values.[(ci,ri)] <- column.GetDefaultEmptyCell() + type Template with member this.FileName From f8d84dcdba389ee4c994fc886461a299f3968e5f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 20 Jun 2024 16:12:10 +0200 Subject: [PATCH 121/135] Fix minor issue introduced in last commit :bug: --- src/Client/Modals/UpdateColumn.fs | 2 +- src/Client/Spreadsheet/Table.Controller.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/Modals/UpdateColumn.fs b/src/Client/Modals/UpdateColumn.fs index abfb9690..709be490 100644 --- a/src/Client/Modals/UpdateColumn.fs +++ b/src/Client/Modals/UpdateColumn.fs @@ -1,4 +1,4 @@ -namespace Modals +namespace Modals open Feliz open Feliz.Bulma diff --git a/src/Client/Spreadsheet/Table.Controller.fs b/src/Client/Spreadsheet/Table.Controller.fs index 99b1d39e..4fa02a22 100644 --- a/src/Client/Spreadsheet/Table.Controller.fs +++ b/src/Client/Spreadsheet/Table.Controller.fs @@ -124,7 +124,7 @@ let deleteColumn (index: int) (state: Spreadsheet.Model) : Spreadsheet.Model = SelectedCells = Set.empty} let setColumn (index: int) (column: CompositeColumn) (state: Spreadsheet.Model) : Spreadsheet.Model = - state.ActiveTable.SetColumn (index, column) + state.ActiveTable.UpdateColumn (index, column.Header, column.Cells) {state with ArcFile = state.ArcFile SelectedCells = Set.empty} From fc5a803ece8fc265b3edad4829a94ac4cb37d89b Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 21 Jun 2024 11:21:07 +0200 Subject: [PATCH 122/135] Major code burn :fire: --- src/Client/Client.fs | 4 +- src/Client/Client.fsproj | 1 - src/Client/Init.fs | 5 - src/Client/Messages.fs | 49 +-- src/Client/Model.fs | 77 ---- .../Pages/BuildingBlock/BuildingBlockView.fs | 112 ++---- src/Client/Pages/JsonExporter/JsonExporter.fs | 242 +++---------- .../Pages/ProtocolTemplates/ProtocolView.fs | 8 +- src/Client/States/JsonExporterState.fs | 57 --- src/Client/States/Spreadsheet.fs | 4 +- src/Client/States/SpreadsheetInterface.fs | 4 +- src/Client/Update.fs | 328 +----------------- src/Client/Update/InterfaceUpdate.fs | 20 +- src/Client/Update/OfficeInteropUpdate.fs | 37 +- src/Client/Update/SpreadsheetUpdate.fs | 30 +- src/Client/Views/SidebarView.fs | 105 +++--- src/Shared/Shared.fs | 12 - 17 files changed, 185 insertions(+), 910 deletions(-) delete mode 100644 src/Client/States/JsonExporterState.fs diff --git a/src/Client/Client.fs b/src/Client/Client.fs index 9d280b5f..a3092a20 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -19,7 +19,7 @@ open Feliz.Bulma let private split_container model dispatch = let mainWindow = Seq.singleton <| MainWindowView.Main (model, dispatch) - let sideWindow = Seq.singleton <| SidebarView.SidebarView model dispatch + let sideWindow = Seq.singleton <| SidebarView.SidebarView.Main(model, dispatch) SplitWindowView.Main mainWindow sideWindow @@ -40,7 +40,7 @@ let View (model : Model) (dispatch : Msg -> unit) = Html.div [ match model.PersistentStorageState.Host with | Some Swatehost.Excel -> - SidebarView.SidebarView model dispatch + SidebarView.SidebarView.Main(model, dispatch) | _ -> split_container model dispatch ] diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 6766400b..3449f220 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -38,7 +38,6 @@ - diff --git a/src/Client/Init.fs b/src/Client/Init.fs index 9cf1346b..31295ed6 100644 --- a/src/Client/Init.fs +++ b/src/Client/Init.fs @@ -12,20 +12,15 @@ let initializeModel () = let dt = LocalStorage.Darkmode.DataTheme.GET() LocalStorage.Darkmode.DataTheme.SET dt { - DebouncerState = Debouncer .create () PageState = PageState .init () PersistentStorageState = PersistentStorageState .init () DevState = DevState .init () TermSearchState = TermSearch.Model .init () ExcelState = OfficeInterop.Model .init () - ApiState = ApiState .init () FilePickerState = FilePicker.Model .init () AddBuildingBlockState = BuildingBlock.Model .init () - ValidationState = Validation.Model .init () ProtocolState = Protocol.Model .init () BuildingBlockDetailsState = BuildingBlockDetailsState .init () - SettingsXmlState = SettingsXml.Model .init () - JsonExporterModel = JsonExporter.Model .init () DagModel = Dag.Model .init () CytoscapeModel = Cytoscape.Model .init () SpreadsheetModel = Spreadsheet.Model .fromLocalStorage() diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index f3d943f7..64eab759 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -42,33 +42,17 @@ module AdvancedSearch = type Msg = | GetSearchResults of {| config:AdvancedSearchTypes.AdvancedSearchOptions; responseSetter: Term [] -> unit |} +module Ontologies = + + type Msg = + | GetOntologies + type DevMsg = | LogTableMetadata | GenericLog of Cmd * (string*string) | GenericInteropLogs of Cmd * InteropLogging.Msg list | GenericError of Cmd * exn | UpdateDisplayLogList of LogItem list - -type ApiRequestMsg = - | GetNewUnitTermSuggestions of string - | FetchAllOntologies - /// TermSearchable [] is created by officeInterop and passed to server for db search. - | SearchForInsertTermsRequest of TermSearchable [] - // - | GetAppVersion - -type ApiResponseMsg = - | UnitTermSuggestionResponse of Term [] - | FetchAllOntologiesResponse of Ontology [] - | SearchForInsertTermsResponse of TermSearchable [] - // - | GetAppVersionResponse of string - -type ApiMsg = - | Request of ApiRequestMsg - | Response of ApiResponseMsg - | ApiError of exn - | ApiSuccess of (string*string) type StyleChangeMsg = | UpdateColorMode of ColorMode @@ -94,11 +78,6 @@ module BuildingBlock = | 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 - | UnitTermSuggestionUsed of unitTerm:Term - | NewUnitTermSuggestions of Term [] module Protocol = @@ -137,29 +116,19 @@ type Model = { PageState : PageState ///Data that needs to be persistent once loaded PersistentStorageState : PersistentStorageState - ///Debouncing - DebouncerState : Debouncer.State ///Error handling, Logging, etc. DevState : DevState ///States regarding term search TermSearchState : TermSearch.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 - /// "Use this to log Api calls and maybe handle them better" - ApiState : ApiState ///States regarding File picker functionality FilePickerState : FilePicker.Model ProtocolState : Protocol.Model ///Insert annotation columns AddBuildingBlockState : BuildingBlock.Model - ///Create Validation scheme for Table - ValidationState : Validation.Model ///Used to show selected building block information BuildingBlockDetailsState : BuildingBlockDetailsState - ///Used to manage all custom xml settings - SettingsXmlState : SettingsXml.Model - JsonExporterModel : JsonExporter.Model DagModel : Dag.Model CytoscapeModel : Cytoscape.Model /// Contains all information about spreadsheet view @@ -168,16 +137,12 @@ type Model = { } with member this.updateByExcelState (s:OfficeInterop.Model) = { this with ExcelState = s} - member this.updateByJsonExporterModel (m:JsonExporter.Model) = - { this with JsonExporterModel = m} member this.updateByDagModel (m:Dag.Model) = { this with DagModel = m} type Msg = -| Bounce of (System.TimeSpan*string*Msg) -| DebouncerSelfMsg of Debouncer.SelfMessage -| Api of ApiMsg | DevMsg of DevMsg +| OntologyMsg of Ontologies.Msg | TermSearchMsg of TermSearch.Msg | AdvancedSearchMsg of AdvancedSearch.Msg | OfficeInteropMsg of OfficeInterop.Msg @@ -185,7 +150,6 @@ type Msg = | FilePickerMsg of FilePicker.Msg | BuildingBlockMsg of BuildingBlock.Msg | ProtocolMsg of Protocol.Msg -| JsonExporterMsg of JsonExporter.Msg | BuildingBlockDetails of BuildingBlockDetailsMsg | CytoscapeMsg of Cytoscape.Msg | SpreadsheetMsg of Spreadsheet.Msg @@ -193,7 +157,6 @@ type Msg = /// This is used to forward Msg to SpreadsheetMsg/OfficeInterop | InterfaceMsg of SpreadsheetInterface.Msg //| SettingsProtocolMsg of SettingsProtocolMsg -| TopLevelMsg of TopLevelMsg | UpdatePageState of Routing.Route option | UpdateIsExpert of bool | Batch of seq diff --git a/src/Client/Model.fs b/src/Client/Model.fs index c9c92001..27128900 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -164,19 +164,6 @@ type ApiCallHistoryItem = { Status : ApiCallStatus } -type ApiState = { - currentCall : ApiCallHistoryItem - callHistory : ApiCallHistoryItem list -} with - static member init() = { - currentCall = ApiState.noCall - callHistory = [] - } - static member noCall = { - FunctionName = "None" - Status = IsNone - } - type PageState = { CurrentPage : Routing.Route IsExpert : bool @@ -281,14 +268,6 @@ module BuildingBlock = 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 - Unit2TermSearchText : string - Unit2SelectedTerm : Term option - Unit2TermSuggestions : Term [] - HasUnit2TermSuggestionsLoading : bool - ShowUnit2TermSuggestions : bool - } with static member init () = { @@ -296,14 +275,6 @@ module BuildingBlock = 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 - Unit2TermSearchText = "" - Unit2SelectedTerm = None - Unit2TermSuggestions = [||] - ShowUnit2TermSuggestions = false - HasUnit2TermSuggestionsLoading = false } member this.TryHeaderOA() = @@ -326,20 +297,6 @@ module BuildingBlock = | Some (U2.Case1 s) -> Some s | _ -> None -/// Validation scheme for Table -module Validation = - type Model = { - ActiveTableBuildingBlocks : BuildingBlock [] - TableValidationScheme : OfficeInterop.CustomXmlTypes.Validation.TableValidation - // Client view related - DisplayedOptionsId : int option - } with - static member init () = { - ActiveTableBuildingBlocks = [||] - TableValidationScheme = OfficeInterop.CustomXmlTypes.Validation.TableValidation.init() - DisplayedOptionsId = None - } - module Protocol = [] @@ -406,38 +363,4 @@ type BuildingBlockDetailsState = { BuildingBlockValues = [||] } -module SettingsXml = - type Model = { - // // Client // // - // Validation xml - ActiveSwateValidation : obj option //OfficeInterop.Types.Xml.ValidationTypes.TableValidation option - NextAnnotationTableForActiveValidation : string option - // Protocol group xml - ActiveProtocolGroup : obj option //OfficeInterop.Types.Xml.GroupTypes.ProtocolGroup option - NextAnnotationTableForActiveProtGroup : string option - // Protocol - ActiveProtocol : obj option //OfficeInterop.Types.Xml.GroupTypes.Protocol option - NextAnnotationTableForActiveProtocol : string option - // - RawXml : string option - NextRawXml : string option - FoundTables : string [] - ValidationXmls : obj [] //OfficeInterop.Types.Xml.ValidationTypes.TableValidation [] - } with - static member init () = { - // Client - ActiveSwateValidation = None - NextAnnotationTableForActiveValidation = None - ActiveProtocolGroup = None - NextAnnotationTableForActiveProtGroup = None - ActiveProtocol = None - // Unused - NextAnnotationTableForActiveProtocol = None - // - RawXml = None - NextRawXml = None - FoundTables = [||] - ValidationXmls = [||] - } - // The main MODEL was shifted to 'Messages.fs' to allow saving 'Msg' diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index f95f3a7a..2f47098f 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -51,84 +51,38 @@ let update (addBuildingBlockMsg:BuildingBlock.Msg) (state: BuildingBlock.Model) let nextState = { state with BodyCellType = next } nextState, Cmd.none - | SearchUnitTermTextChange (newTerm) -> - - let triggerNewSearch = - newTerm.Length > 2 - - let (delay, bounceId, msgToBounce) = - (System.TimeSpan.FromSeconds 0.5), - "GetNewUnitTermSuggestions", - ( - if triggerNewSearch then - (newTerm) |> (GetNewUnitTermSuggestions >> Request >> Api) - else - DoNothing - ) - - let nextState = { - state with - Unit2TermSearchText = newTerm - Unit2SelectedTerm = None - ShowUnit2TermSuggestions = triggerNewSearch - HasUnit2TermSuggestionsLoading = true - } - - nextState, ((delay, bounceId, msgToBounce) |> Bounce |> Cmd.ofMsg) - - | NewUnitTermSuggestions suggestions -> - - let nextState = { - state with - Unit2TermSuggestions = suggestions - ShowUnit2TermSuggestions = true - HasUnit2TermSuggestionsLoading = false - } - - nextState,Cmd.none - - | UnitTermSuggestionUsed suggestion -> - let nextState ={ - state with - Unit2TermSearchText = suggestion.Name - Unit2SelectedTerm = Some suggestion - ShowUnit2TermSuggestions = false - HasUnit2TermSuggestionsLoading = false - } - nextState, Cmd.none - open SidebarComponents open Feliz open Feliz.Bulma -let addUnitToExistingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.button.button [ +//let addUnitToExistingBlockElements (model:Model) (dispatch:Messages.Msg -> unit) = +// mainFunctionContainer [ +// Bulma.field.div [ +// Bulma.button.button [ - let isValid = model.AddBuildingBlockState.Unit2TermSearchText <> "" - Bulma.color.isSuccess - if isValid then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick (fun _ -> - let unitTerm = - if model.AddBuildingBlockState.Unit2SelectedTerm.IsSome then Some <| TermMinimal.ofTerm model.AddBuildingBlockState.Unit2SelectedTerm.Value else None - match model.AddBuildingBlockState.Unit2TermSearchText with - | "" -> - curry GenericLog Cmd.none ("Error", "Cannot execute function with empty unit input") |> DevMsg |> dispatch - | hasUnitTerm when model.AddBuildingBlockState.Unit2SelectedTerm.IsSome -> - OfficeInterop.UpdateUnitForCells unitTerm.Value |> OfficeInteropMsg |> dispatch - | freeText -> - OfficeInterop.UpdateUnitForCells (TermMinimal.create model.AddBuildingBlockState.Unit2TermSearchText "") |> OfficeInteropMsg |> dispatch - ) - prop.text "Update unit for cells" - ] - ] - ] +// let isValid = model.AddBuildingBlockState.Unit2TermSearchText <> "" +// Bulma.color.isSuccess +// if isValid then +// Bulma.button.isActive +// else +// Bulma.color.isDanger +// prop.disabled true +// Bulma.button.isFullWidth +// prop.onClick (fun _ -> +// let unitTerm = +// if model.AddBuildingBlockState.Unit2SelectedTerm.IsSome then Some <| TermMinimal.ofTerm model.AddBuildingBlockState.Unit2SelectedTerm.Value else None +// match model.AddBuildingBlockState.Unit2TermSearchText with +// | "" -> +// curry GenericLog Cmd.none ("Error", "Cannot execute function with empty unit input") |> DevMsg |> dispatch +// | hasUnitTerm when model.AddBuildingBlockState.Unit2SelectedTerm.IsSome -> +// OfficeInterop.UpdateUnitForCells unitTerm.Value |> OfficeInteropMsg |> dispatch +// | freeText -> +// OfficeInterop.UpdateUnitForCells (TermMinimal.create model.AddBuildingBlockState.Unit2TermSearchText "") |> OfficeInteropMsg |> dispatch +// ) +// prop.text "Update unit for cells" +// ] +// ] +// ] @@ -146,10 +100,10 @@ let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = SearchComponent.Main model dispatch ] - match model.PersistentStorageState.Host with - | Some Swatehost.Excel -> - Bulma.label "Add/Update unit reference to existing building block." - // Input forms, etc related to add unit to existing building block. - addUnitToExistingBlockElements model dispatch - | _ -> Html.none + //match model.PersistentStorageState.Host with + //| Some Swatehost.Excel -> + // Bulma.label "Add/Update unit reference to existing building block." + // // Input forms, etc related to add unit to existing building block. + // addUnitToExistingBlockElements model dispatch + //| _ -> Html.none ] \ No newline at end of file diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index ee8d05d1..54ef0c23 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -11,7 +11,6 @@ open ExcelColors open Model open Messages -open JsonExporter open Browser.Dom @@ -28,175 +27,6 @@ let download(filename, text) = document.body.removeChild(element) |> ignore () -let update (msg:JsonExporter.Msg) (currentModel: Messages.Model) : Messages.Model * Cmd = - match msg with - // Style - | UpdateLoading isLoading -> - let nextModel = { currentModel with Messages.Model.JsonExporterModel.Loading = isLoading } - nextModel, Cmd.none - | UpdateShowTableExportTypeDropdown nextVal -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = nextVal - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateShowWorkbookExportTypeDropdown nextVal -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = nextVal - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateShowXLSXExportTypeDropdown nextVal -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = nextVal - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateTableJsonExportType nextType -> - let nextModel = { - currentModel.JsonExporterModel with - TableJsonExportType = nextType - ShowTableExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateWorkbookJsonExportType nextType -> - let nextModel = { - currentModel.JsonExporterModel with - WorkbookJsonExportType = nextType - ShowWorkbookExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | UpdateXLSXParsingExportType nextType -> - let nextModel = { - currentModel.JsonExporterModel with - XLSXParsingExportType = nextType - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - | CloseAllDropdowns -> - let nextModel = { - currentModel.JsonExporterModel with - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = false - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - // - | ParseTableOfficeInteropRequest -> - let nextModel = { - currentModel.JsonExporterModel with - Loading = true - } - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getBuildingBlocksAndSheet - () - (ParseTableServerRequest >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByJsonExporterModel nextModel, cmd - | ParseTableServerRequest (worksheetName, buildingBlocks) -> - let nextModel = { - currentModel.JsonExporterModel with - CurrentExportType = Some currentModel.JsonExporterModel.TableJsonExportType - Loading = true - } - let api = - match currentModel.JsonExporterModel.TableJsonExportType with - | JsonExportType.Assay -> - Api.swateJsonAPIv1.parseAnnotationTableToAssayJson - | JsonExportType.ProcessSeq -> - Api.swateJsonAPIv1.parseAnnotationTableToProcessSeqJson - | anythingElse -> failwith $"Cannot parse \"{anythingElse.ToString()}\" with this endpoint." - let cmd = - Cmd.OfAsync.either - api - (worksheetName, buildingBlocks) - (ParseTableServerResponse >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByJsonExporterModel nextModel, cmd - // - | ParseTablesOfficeInteropRequest -> - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getBuildingBlocksAndSheets - () - (ParseTablesServerRequest >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel, cmd - | ParseTablesServerRequest (worksheetBuildingBlocksTuple) -> - let nextModel = { - currentModel.JsonExporterModel with - CurrentExportType = Some currentModel.JsonExporterModel.WorkbookJsonExportType - Loading = true - } - let api = - match currentModel.JsonExporterModel.WorkbookJsonExportType with - | JsonExportType.ProcessSeq -> - Api.swateJsonAPIv1.parseAnnotationTablesToProcessSeqJson - | JsonExportType.Assay -> - Api.swateJsonAPIv1.parseAnnotationTablesToAssayJson - | anythingElse -> failwith $"Cannot parse \"{anythingElse.ToString()}\" with this endpoint." - let cmd = - Cmd.OfAsync.either - api - worksheetBuildingBlocksTuple - (ParseTableServerResponse >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - - currentModel.updateByJsonExporterModel nextModel, cmd - // - | ParseTableServerResponse parsedJson -> - let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") - let jsonName = Option.bind (fun x -> Some <| "_" + x.ToString()) currentModel.JsonExporterModel.CurrentExportType |> Option.defaultValue "" - let _ = download ($"{n}{jsonName}.json",parsedJson) - let nextModel = { - currentModel.JsonExporterModel with - Loading = false - CurrentExportType = None - } - currentModel.updateByJsonExporterModel nextModel, Cmd.none - // - | StoreXLSXByteArray byteArr -> - let nextModel = { - currentModel.JsonExporterModel with - XLSXByteArray = byteArr - } - currentModel.updateByJsonExporterModel nextModel , Cmd.none - | ParseXLSXToJsonRequest byteArr -> - let nextModel = { - currentModel.JsonExporterModel with - CurrentExportType = Some currentModel.JsonExporterModel.XLSXParsingExportType - Loading = true - } - let apif = - match currentModel.JsonExporterModel.XLSXParsingExportType with - | JsonExportType.ProcessSeq -> Api.isaDotNetCommonApi.toProcessSeqJsonStr - | JsonExportType.Assay -> Api.isaDotNetCommonApi.toAssayJsonStr - | JsonExportType.ProtocolTemplate -> Api.isaDotNetCommonApi.toSwateTemplateJsonStr - let cmd = - Cmd.OfAsync.either - apif - byteArr - (ParseXLSXToJsonResponse >> JsonExporterMsg) - (curry GenericError (UpdateLoading false |> JsonExporterMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByJsonExporterModel nextModel, cmd - | ParseXLSXToJsonResponse jsonStr -> - let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") - let jsonName = Option.bind (fun x -> Some <| "_" + x.ToString()) currentModel.JsonExporterModel.CurrentExportType |> Option.defaultValue "" - let _ = download ($"{n}{jsonName}.json",jsonStr) - let nextModel = { - currentModel.JsonExporterModel with - Loading = false - } - - currentModel.updateByJsonExporterModel nextModel, Cmd.none - //open Messages //open Feliz //open Feliz.Bulma @@ -448,24 +278,28 @@ let update (msg:JsonExporter.Msg) (currentModel: Messages.Model) : Messages.Mode open Feliz open Feliz.Bulma -module FileExporterAux = +type private JsonExportFormat = + | ARCtrl + | ARCtrlCompressed + | ISA + | ROCrate + +type private JsonExportState = { + Error: exn option + Json: string + ExportFormat: JsonExportFormat +} with + static member init() = { + Error = None; + Json = "" + ExportFormat = ROCrate + } + +module private FileExporterAux = open ARCtrl open ARCtrl.Json - [] - type ISA = - | Assay - | Study - | Investigation - - static member initFromArcfile(arcfile: ArcFiles) = - match arcfile with - | ArcFiles.Assay _ -> Some Assay - | ArcFiles.Study _ -> Some Study - | ArcFiles.Investigation _ -> Some Investigation - | ArcFiles.Template _ -> None - let toISAJsonString (arcfile: ArcFiles option) = let timed = fun s -> System.DateTime.Now.ToString("yyyyMMdd_hhmm_") + s match arcfile with @@ -478,28 +312,34 @@ open FileExporterAux type FileExporter = + + static member private FileFormat(efm: JsonExportFormat, state: JsonExportState) = + Html.option [ + + ] + [] static member JsonExport(model: Messages.Model, dispatch) = - let isa, setIsa = React.useState(model.SpreadsheetModel.ArcFile |> Option.bind ISA.initFromArcfile) + let state, setState = React.useState JsonExportState.init Html.div [ Bulma.field.div [ Bulma.field.hasAddons prop.children [ Bulma.control.p [ - //Html.span [ - // prop.className "select" - // prop.children [ - // Html.select [ - // Html.option "Test" - // Html.option "Test2" - // Html.option "Test3" - // ] - // ] - //] - Bulma.button.a [ - prop.text "ISA" - Bulma.button.isStatic + Html.span [ + prop.className "select" + prop.children [ + Html.select [ + Html.option "ISA" + Html.option "Test2" + Html.option "Test3" + ] + ] ] + //Bulma.button.a [ + // prop.text "ISA" + // Bulma.button.isStatic + //] ] Bulma.control.p [ Bulma.control.isExpanded @@ -507,8 +347,6 @@ type FileExporter = Bulma.button.button [ Bulma.button.isFullWidth prop.text "Download" - if isa.IsNone then - prop.disabled true prop.onClick (fun _ -> let r = toISAJsonString model.SpreadsheetModel.ArcFile r |> Option.iter (fun r -> download r) @@ -518,10 +356,10 @@ type FileExporter = ] ] ] - if isa.IsNone then + if state.Error.IsSome then Bulma.help [ Bulma.color.isDanger - prop.text "Unable to convert Template to ISA-JSON" + prop.textf "Error trying to convert: %s" state.Error.Value.Message ] ] diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index f1b797e0..df466a9c 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -118,7 +118,7 @@ module TemplateFromJsonFile = Bulma.field.div [ Bulma.help [ b [] [str "Insert tables via ISA-JSON files."] - str " You can use Swate.Experts to create these files from existing Swate tables. " + str " You can use Settings --> Swate.Experts to create these files from existing Swate tables. " span [Style [Color NFDIColors.Red.Base]] [str "Only missing building blocks will be added."] ] ] @@ -276,8 +276,8 @@ let fileUploadViewComponent (model:Messages.Model) dispatch = TemplateFromDB.showDatabaseProtocolTemplate model dispatch - //// Box 2 - //Bulma.label "Add template(s) from file." + // Box 2 + Bulma.label "Add template(s) from file." - //TemplateFromJsonFile.protocolInsertElement model dispatch + TemplateFromJsonFile.protocolInsertElement model dispatch ] \ No newline at end of file diff --git a/src/Client/States/JsonExporterState.fs b/src/Client/States/JsonExporterState.fs deleted file mode 100644 index 2b474dfa..00000000 --- a/src/Client/States/JsonExporterState.fs +++ /dev/null @@ -1,57 +0,0 @@ -module Model.JsonExporter - -open Shared -open Shared.OfficeInteropTypes - -type Model = { - /// Use this value to determine on click which export value to use - CurrentExportType : JsonExportType option - // - TableJsonExportType : JsonExportType - WorkbookJsonExportType : JsonExportType - XLSXParsingExportType : JsonExportType - Loading : bool - ShowTableExportTypeDropdown : bool - ShowWorkbookExportTypeDropdown : bool - ShowXLSXExportTypeDropdown : bool - // XLSX upload with json parsing - XLSXByteArray : byte [] -} with - static member init() = { - - CurrentExportType = None - // - TableJsonExportType = JsonExportType.Assay - WorkbookJsonExportType = JsonExportType.Assay - XLSXParsingExportType = JsonExportType.Assay - Loading = false - ShowTableExportTypeDropdown = false - ShowWorkbookExportTypeDropdown = false - ShowXLSXExportTypeDropdown = false - - // XLSX upload with json parsing - XLSXByteArray = Array.empty - } - -type Msg = -// Style -| UpdateLoading of bool -| UpdateShowTableExportTypeDropdown of bool -| UpdateShowWorkbookExportTypeDropdown of bool -| UpdateShowXLSXExportTypeDropdown of bool -| CloseAllDropdowns -| UpdateTableJsonExportType of JsonExportType -| UpdateWorkbookJsonExportType of JsonExportType -| UpdateXLSXParsingExportType of JsonExportType -// -| ParseTableOfficeInteropRequest -/// parse active annotation table to building blocks -| ParseTableServerRequest of worksheetName:string * BuildingBlock [] -| ParseTableServerResponse of string -/// Parse all annotation tables to buildingblocks -| ParseTablesOfficeInteropRequest -| ParseTablesServerRequest of (string * BuildingBlock []) [] -// XLSX upload with json parsing -| StoreXLSXByteArray of byte [] -| ParseXLSXToJsonRequest of byte [] -| ParseXLSXToJsonResponse of string \ No newline at end of file diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 2c94dc66..9d50db22 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -152,9 +152,7 @@ type Msg = | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] /// Starts chain to export active table to isa json -| ExportJsonTable -/// Starts chain to export all tables to isa json -| ExportJsonTables +| ExportJson /// Starts chain to export all tables to xlsx swate tables. | ExportXlsx of ArcFiles | ExportXlsxDownload of filename: string * byte [] diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index ea3561da..26d1fc93 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -20,9 +20,7 @@ type Msg = | InsertOntologyAnnotation of OntologyAnnotation | InsertFileNames of string list /// Starts chain to export active table to isa json -| ExportJsonTable -/// Starts chain to export all tables to isa json -| ExportJsonTables +| ExportJson /// Starts chain to parse all tables to DAG | ParseTablesToDag | UpdateTermColumns diff --git a/src/Client/Update.fs b/src/Client/Update.fs index c3a8757e..d8ad1628 100644 --- a/src/Client/Update.fs +++ b/src/Client/Update.fs @@ -104,264 +104,6 @@ module Dev = (curry GenericError Cmd.none >> DevMsg) currentState, cmd -let handleApiRequestMsg (reqMsg: ApiRequestMsg) (currentState: ApiState) : ApiState * Cmd = - - let handleTermSuggestionRequest (apiFunctionname:string) (responseHandler: Term [] -> ApiMsg) queryString = - let currentCall = { - FunctionName = apiFunctionname - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - let nextCmd = - Cmd.OfAsync.either - Api.api.getTermSuggestions - {|n= 5; query = queryString; ontology = None|} - (responseHandler >> Api) - (ApiError >> Api) - - nextState,nextCmd - - let handleUnitTermSuggestionRequest (apiFunctionname:string) (responseHandler: (Term []) -> ApiMsg) queryString = - let currentCall = { - FunctionName = apiFunctionname - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - let nextCmd = - Cmd.OfAsync.either - Api.api.getUnitTermSuggestions - {|n= 5; query = queryString|} - (responseHandler >> Api) - (ApiError >> Api) - - nextState,nextCmd - - let handleTermSuggestionByParentTermRequest (apiFunctionname:string) (responseHandler: Term [] -> ApiMsg) queryString (parent:TermMinimal) = - let currentCall = { - FunctionName = apiFunctionname - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - let nextCmd = - Cmd.OfAsync.either - Api.api.getTermSuggestionsByParentTerm - {|n= 5; query = queryString; parent_term = parent|} - (responseHandler >> Api) - (ApiError >> Api) - - nextState,nextCmd - - match reqMsg with - - | GetNewUnitTermSuggestions (queryString) -> - handleUnitTermSuggestionRequest - "getUnitTermSuggestions" - (UnitTermSuggestionResponse >> Response) - queryString - - | FetchAllOntologies -> - let currentCall = { - FunctionName = "getAllOntologies" - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - - nextState, - Cmd.OfAsync.either - Api.api.getAllOntologies - () - (FetchAllOntologiesResponse >> Response >> Api) - (ApiError >> Api) - - | SearchForInsertTermsRequest (tableTerms) -> - let currentCall = { - FunctionName = "getTermsByNames" - Status = Pending - } - let nextState = { - currentState with - currentCall = currentCall - } - let cmd = - Cmd.OfAsync.either - Api.api.getTermsByNames - tableTerms - (SearchForInsertTermsResponse >> Response >> Api) - (fun e -> - Msg.Batch [ - OfficeInterop.UpdateFillHiddenColsState OfficeInterop.FillHiddenColsState.Inactive |> OfficeInteropMsg - ApiError e |> Api - ] ) - let stateCmd = OfficeInterop.UpdateFillHiddenColsState OfficeInterop.FillHiddenColsState.ServerSearchDatabase |> OfficeInteropMsg |> Cmd.ofMsg - let cmds = Cmd.batch [cmd; stateCmd] - nextState, cmds - // - | GetAppVersion -> - let currentCall = { - FunctionName = "getAppVersion" - Status = Pending - } - - let nextState = { - currentState with - currentCall = currentCall - } - - let cmd = - Cmd.OfAsync.either - Api.serviceApi.getAppVersion - () - (GetAppVersionResponse >> Response >> Api) - (ApiError >> Api) - - nextState, cmd - - -let handleApiResponseMsg (resMsg: ApiResponseMsg) (currentState: ApiState) : ApiState * Cmd = - - let handleTermSuggestionResponse (responseHandler: Term [] -> Msg) (suggestions: Term[]) = - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - suggestions |> responseHandler |> Cmd.ofMsg - ] - - nextState, cmds - - let handleUnitTermSuggestionResponse (responseHandler: Term [] -> Msg) (suggestions: Term[]) = - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - (suggestions) |> responseHandler |> Cmd.ofMsg - ] - - nextState, cmds - - match resMsg with - | UnitTermSuggestionResponse (suggestions) -> - - handleUnitTermSuggestionResponse - (BuildingBlock.Msg.NewUnitTermSuggestions >> BuildingBlockMsg) - suggestions - - | FetchAllOntologiesResponse onts -> - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - onts |> PersistentStorage.NewSearchableOntologies |> PersistentStorageMsg |> Cmd.ofMsg - ] - - nextState, cmds - - | SearchForInsertTermsResponse (termsWithSearchResult) -> - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - let cmd = - SpreadsheetInterface.UpdateTermColumnsResponse termsWithSearchResult |> InterfaceMsg |> Cmd.ofMsg - let loggingCmd = - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - nextState, Cmd.batch [cmd; loggingCmd] - - // - | GetAppVersionResponse appVersion -> - let finishedCall = { - currentState.currentCall with - Status = Successfull - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = finishedCall::currentState.callHistory - } - - let cmds = Cmd.batch [ - ("Debug",sprintf "[ApiSuccess]: Call %s successfull." finishedCall.FunctionName) |> ApiSuccess |> Api |> Cmd.ofMsg - appVersion |>PersistentStorage. UpdateAppVersion |> PersistentStorageMsg |> Cmd.ofMsg - ] - - nextState, cmds - -open Dev -open Messages - -let handleApiMsg (apiMsg:ApiMsg) (currentState:ApiState) : ApiState * Cmd = - match apiMsg with - | ApiError e -> - - let failedCall = { - currentState.currentCall with - Status = Failed (e.GetPropagatedError()) - } - - let nextState = { - currentCall = ApiState.noCall - callHistory = failedCall::currentState.callHistory - } - let batch = Cmd.batch [ - let modalName = "GenericError" - Cmd.ofEffect(fun _ -> Modals.Controller.renderModal(modalName, Modals.ErrorModal.errorModal(e))) - curry GenericLog Cmd.none ("Error",sprintf "[ApiError]: Call %s failed with: %s" failedCall.FunctionName (e.GetPropagatedError())) |> DevMsg |> Cmd.ofMsg - ] - - nextState, batch - - | ApiSuccess (level,logMsg) -> - currentState, curry GenericLog Cmd.none (level,logMsg) |> DevMsg |> Cmd.ofMsg - - | Request req -> - handleApiRequestMsg req currentState - | Response res -> - handleApiResponseMsg res currentState - let handlePersistenStorageMsg (persistentStorageMsg: PersistentStorage.Msg) (currentState:PersistentStorageState) : PersistentStorageState * Cmd = match persistentStorageMsg with | PersistentStorage.NewSearchableOntologies onts -> @@ -423,19 +165,18 @@ let handleBuildingBlockDetailsMsg (topLevelMsg:BuildingBlockDetailsMsg) (current Modals.Controller.renderModal("BuildingBlockDetails", Modals.BuildingBlockDetailsModal.buildingBlockDetailModal(nextState, dispatch)) ) nextState, cmd - -let handleTopLevelMsg (topLevelMsg:TopLevelMsg) (currentModel: Model) : Model * Cmd = - match topLevelMsg with - // Client - | CloseSuggestions -> - let nextModel = { - currentModel with - AddBuildingBlockState = { - currentModel.AddBuildingBlockState with - ShowUnit2TermSuggestions = false - } - } - nextModel, Cmd.none + +module Ontologies = + let update (omsg: Ontologies.Msg) (model: Model) = + match omsg with + | Ontologies.GetOntologies -> + let cmd = + Cmd.OfAsync.either + Api.api.getAllOntologies + () + (PersistentStorage.NewSearchableOntologies >> PersistentStorageMsg) + (curry GenericError Cmd.none >> DevMsg) + model, cmd let update (msg : Msg) (model : Model) : Model * Cmd = let innerUpdate (msg: Msg) (currentModel: Model) = @@ -498,28 +239,10 @@ let update (msg : Msg) (model : Model) : Model * Cmd = // https://stackoverflow.com/questions/42642863/office-js-nullifies-browser-history-functions-breaking-history-usage //| Navigate route -> // currentModel, Navigation.newUrl (Routing.Route.toRouteUrl route) - | Bounce (delay, bounceId, msgToBounce) -> - - let (debouncerModel, debouncerCmd) = - currentModel.DebouncerState - |> Debouncer.bounce delay bounceId msgToBounce - - let nextModel = { - currentModel with - DebouncerState = debouncerModel - } - - nextModel,Cmd.map DebouncerSelfMsg debouncerCmd - | DebouncerSelfMsg debouncerMsg -> - let nextDebouncerState, debouncerCmd = - Debouncer.update debouncerMsg currentModel.DebouncerState - - let nextModel = { - currentModel with - DebouncerState = nextDebouncerState - } - nextModel, debouncerCmd + | OntologyMsg msg -> + let nextModel, cmd = Ontologies.update msg model + nextModel, cmd | OfficeInteropMsg excelMsg -> let nextModel,nextCmd = Update.OfficeInterop.update currentModel excelMsg @@ -557,15 +280,6 @@ let update (msg : Msg) (model : Model) : Model * Cmd = } nextModel,nextCmd - | Api apiMsg -> - let nextApiState,nextCmd = currentModel.ApiState |> handleApiMsg apiMsg - - let nextModel = { - currentModel with - ApiState = nextApiState - } - nextModel,nextCmd - | PersistentStorageMsg persistentStorageMsg -> let nextPersistentStorageState,nextCmd = currentModel.PersistentStorageState @@ -641,25 +355,15 @@ let update (msg : Msg) (model : Model) : Model * Cmd = CytoscapeModel = nextState} nextModel, nextCmd - | JsonExporterMsg msg -> - let nextModel, nextCmd = currentModel |> JsonExporter.Core.update msg - nextModel, nextCmd - | DagMsg msg -> let nextModel, nextCmd = currentModel |> Dag.Core.update msg nextModel, nextCmd - | TopLevelMsg msg -> - let nextModel, nextCmd = - handleTopLevelMsg msg currentModel - - nextModel, nextCmd - /// This function is used to determine which msg should be logged to activity log. /// The function is exception based, so msg which should not be logged needs to be added here. let matchMsgToLog (msg: Msg) = match msg with - | Bounce _ | DevMsg _ | UpdatePageState _ -> false + | DevMsg _ | UpdatePageState _ -> false | _ -> true let logg (msg:Msg) (model: Model) : Model = diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 8aebcbce..038ed05a 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -30,8 +30,7 @@ module Interface = | Initialize host -> let cmd = Cmd.batch [ - Cmd.ofMsg (GetAppVersion |> Request |> Api) - Cmd.ofMsg (FetchAllOntologies |> Request |> Api) + Cmd.ofMsg (Ontologies.GetOntologies |> OntologyMsg) match host with | Swatehost.Excel -> Cmd.OfPromise.either @@ -138,22 +137,13 @@ module Interface = Spreadsheet.DeleteColumn (distinct.[0]) |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" - | ExportJsonTable -> + | ExportJson -> match host with | Some Swatehost.Excel -> - let cmd = JsonExporterMsg JsonExporter.ParseTableOfficeInteropRequest |> Cmd.ofMsg - model, cmd - | Some Swatehost.Browser -> - let cmd = SpreadsheetMsg Spreadsheet.ExportJsonTable |> Cmd.ofMsg - model, cmd - | _ -> failwith "not implemented" - | ExportJsonTables -> - match host with - | Some Swatehost.Excel -> - let cmd = JsonExporterMsg JsonExporter.ParseTablesOfficeInteropRequest |> Cmd.ofMsg - model, cmd + failwith "ExportJsonTable not implemented for Excel" + model, Cmd.none | Some Swatehost.Browser -> - let cmd = SpreadsheetMsg Spreadsheet.ExportJsonTables |> Cmd.ofMsg + let cmd = SpreadsheetMsg Spreadsheet.ExportJson |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" | ParseTablesToDag -> diff --git a/src/Client/Update/OfficeInteropUpdate.fs b/src/Client/Update/OfficeInteropUpdate.fs index 49fd44ea..10bdcd1f 100644 --- a/src/Client/Update/OfficeInteropUpdate.fs +++ b/src/Client/Update/OfficeInteropUpdate.fs @@ -115,24 +115,25 @@ module OfficeInterop = currentModel, cmd // | FillHiddenColsRequest -> - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getAllAnnotationBlockDetails - () - (fun (searchTerms,deprecationLogs) -> - // Push possible deprecation messages by piping through "GenericInteropLogs" - GenericInteropLogs ( - // This will be executed after "deprecationLogs" are handled by "GenericInteropLogs" - SearchForInsertTermsRequest searchTerms |> Request |> Api |> Cmd.ofMsg, - // This will be pushed to Activity logs, or as wanring modal to user in case of LogIdentifier.Warning - deprecationLogs - ) - |> DevMsg - ) - (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) - let stateCmd = UpdateFillHiddenColsState FillHiddenColsState.ExcelCheckHiddenCols |> OfficeInteropMsg |> Cmd.ofMsg - let cmds = Cmd.batch [cmd; stateCmd] - currentModel, cmds + failwith "FillHiddenColsRequest Not implemented yet" + //let cmd = + // Cmd.OfPromise.either + // OfficeInterop.Core.getAllAnnotationBlockDetails + // () + // (fun (searchTerms,deprecationLogs) -> + // // Push possible deprecation messages by piping through "GenericInteropLogs" + // GenericInteropLogs ( + // // This will be executed after "deprecationLogs" are handled by "GenericInteropLogs" + // SearchForInsertTermsRequest searchTerms |> Request |> Api |> Cmd.ofMsg, + // // This will be pushed to Activity logs, or as wanring modal to user in case of LogIdentifier.Warning + // deprecationLogs + // ) + // |> DevMsg + // ) + // (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) + //let stateCmd = UpdateFillHiddenColsState FillHiddenColsState.ExcelCheckHiddenCols |> OfficeInteropMsg |> Cmd.ofMsg + //let cmds = Cmd.batch [cmd; stateCmd] + currentModel, Cmd.none | FillHiddenColumns (termsWithSearchResult) -> let nextState = { diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index d32326a4..50458684 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -289,7 +289,7 @@ module Spreadsheet = (UpdateArcFile >> Messages.SpreadsheetMsg) (Messages.curry Messages.GenericError Cmd.none >> Messages.DevMsg) state, model, cmd - | ExportJsonTable -> + | ExportJson -> failwith "ExportsJsonTable is not implemented" //let exportJsonState = {model.JsonExporterModel with Loading = true} //let nextModel = model.updateByJsonExporterModel exportJsonState @@ -304,21 +304,6 @@ module Spreadsheet = // (Messages.curry Messages.GenericError (JsonExporter.State.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) //state, nextModel, cmd state, model, Cmd.none - | ExportJsonTables -> - failwith "ExportJsonTables is not implemented" - //let exportJsonState = {model.JsonExporterModel with Loading = true} - //let nextModel = model.updateByJsonExporterModel exportJsonState - //let func() = promise { - // return Controller.getTables state - //} - //let cmd = - // Cmd.OfPromise.either - // func - // () - // (JsonExporter.State.ParseTablesServerRequest >> Messages.JsonExporterMsg) - // (Messages.curry Messages.GenericError (JsonExporter.State.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) - //state, nextModel, cmd - state, model, Cmd.none | ParseTablesToDag -> failwith "ParseTablesToDag is not implemented" //let dagState = {model.DagModel with Loading = true} @@ -336,8 +321,6 @@ module Spreadsheet = state, model, Cmd.none | ExportXlsx arcfile-> // we highjack this loading function - let exportJsonState = {model.JsonExporterModel with Loading = true} - let nextModel = model.updateByJsonExporterModel exportJsonState let name, fswb = let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") match arcfile with @@ -354,16 +337,11 @@ module Spreadsheet = Xlsx.toXlsxBytes fswb (fun bytes -> ExportXlsxDownload (name,bytes) |> Messages.SpreadsheetMsg) - (Messages.curry Messages.GenericError (JsonExporter.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) - state, nextModel, cmd + (Messages.curry Messages.GenericError Cmd.none >> Messages.DevMsg) + state, model, cmd | ExportXlsxDownload (name,xlsxBytes) -> let _ = Helper.download (name ,xlsxBytes) - let nextJsonExporter = { - model.JsonExporterModel with - Loading = false - } - let nextModel = model.updateByJsonExporterModel nextJsonExporter - state, nextModel, Cmd.none + state, model, Cmd.none | UpdateTermColumns -> //let getUpdateTermColumns() = promise { // return Controller.getUpdateTermColumns state diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index 68f7554a..d82d1c2d 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -30,6 +30,7 @@ let private createNavigationTab (pageLink: Routing.Route) (model:Model) (dispatc Bulma.tab [ if isActive then Bulma.tab.isActive Html.a [ + prop.className "navigation" // this class does not do anything, but disables styling. prop.onClick (fun e -> e.preventDefault(); UpdatePageState (Some pageLink) |> dispatch) match sidebarsize with | Mini | MobileMini -> @@ -68,17 +69,6 @@ let private tabs (model:Model) dispatch (sidebarsize: Model.WindowSize) = createNavigationTab Routing.Route.Info model dispatch sidebarsize ] - -let private footer (model:Model) = - div [Style [Color "grey"; Position PositionOptions.Sticky; Width "inherit"; Bottom "0"; TextAlign TextAlignOptions.Center ]] [ - div [] [ - str "Swate Release Version " - a [Href "https://github.com/nfdi4plants/Swate/releases"; HTMLAttr.Target "_Blank"] [str model.PersistentStorageState.AppVersion] - str " Host " - Html.span [prop.style [style.color "#4fb3d9"]; prop.text (sprintf "%O" model.PersistentStorageState.Host)] - ] - ] - module private ResizeObserver = open Fable.Core @@ -129,11 +119,6 @@ let private viewContainer (model: Model) (dispatch: Msg -> unit) (state: Sidebar let ele = Browser.Dom.document.getElementById(Sidebar_Id) ResizeObserver.observer(state, setState).observe(ele) ) - OnClick (fun e -> - if model.AddBuildingBlockState.ShowUnit2TermSuggestions - then - TopLevelMsg.CloseSuggestions |> TopLevelMsg |> dispatch - ) Style [ Display DisplayOptions.Flex FlexGrow "1" @@ -144,9 +129,27 @@ let private viewContainer (model: Model) (dispatch: Msg -> unit) (state: Sidebar ] ] children +type SidebarView = -module private Content = - let main (model:Model) (dispatch: Msg -> unit) = + [] + static member private footer (model:Model, dispatch) = + React.useEffectOnce(fun () -> + async { + let! versionResponse = Api.serviceApi.getAppVersion() + PersistentStorage.UpdateAppVersion versionResponse |> PersistentStorageMsg |> dispatch + } + |> Async.StartImmediate + ) + div [Style [Color "grey"; Position PositionOptions.Sticky; Width "inherit"; Bottom "0"; TextAlign TextAlignOptions.Center ]] [ + div [] [ + str "Swate Release Version " + a [Href "https://github.com/nfdi4plants/Swate/releases"; HTMLAttr.Target "_Blank"] [str model.PersistentStorageState.AppVersion] + str " Host " + Html.span [prop.style [style.color "#4fb3d9"]; prop.text (sprintf "%O" model.PersistentStorageState.Host)] + ] + ] + + static member private content (model:Model) (dispatch: Msg -> unit) = match model.PageState.CurrentPage with | Routing.Route.BuildingBlock | Routing.Route.Home _ -> BuildingBlock.Core.addBuildingBlockComponent model dispatch @@ -184,37 +187,37 @@ module private Content = | Routing.Route.NotFound -> NotFoundView.notFoundComponent model dispatch -/// The base react component for the sidebar view in the app. contains the navbar and takes body and footer components to create the full view. -[] -let SidebarView (model: Model) (dispatch: Msg -> unit) = - let state, setState = React.useState(SidebarStyle.init) - viewContainer model dispatch state setState [ - SidebarComponents.Navbar.NavbarComponent model dispatch state.Size - - Bulma.container [ - Bulma.container.isFluid - prop.className "pl-4 pr-4" - prop.children [ - tabs model dispatch state.Size - - //str <| state.Size.ToString() - - //Button.button [ - // Button.OnClick (fun _ -> - // //Spreadsheet.Controller.deleteRow 2 model.SpreadsheetModel - // //() - // //Spreadsheet.DeleteColumn 1 |> SpreadsheetMsg |> dispatch - // () - // ) - //] [ str "Test button" ] - - match model.PersistentStorageState.Host, not model.ExcelState.HasAnnotationTable with - | Some Swatehost.Excel, true -> - SidebarComponents.AnnotationTableMissingWarning.annotationTableMissingWarningComponent model dispatch - | _ -> () - - Content.main model dispatch + /// The base react component for the sidebar view in the app. contains the navbar and takes body and footer components to create the full view. + [] + static member Main (model: Model, dispatch: Msg -> unit) = + let state, setState = React.useState(SidebarStyle.init) + viewContainer model dispatch state setState [ + SidebarComponents.Navbar.NavbarComponent model dispatch state.Size + + Bulma.container [ + Bulma.container.isFluid + prop.className "pl-4 pr-4" + prop.children [ + tabs model dispatch state.Size + + //str <| state.Size.ToString() + + //Button.button [ + // Button.OnClick (fun _ -> + // //Spreadsheet.Controller.deleteRow 2 model.SpreadsheetModel + // //() + // //Spreadsheet.DeleteColumn 1 |> SpreadsheetMsg |> dispatch + // () + // ) + //] [ str "Test button" ] + + match model.PersistentStorageState.Host, not model.ExcelState.HasAnnotationTable with + | Some Swatehost.Excel, true -> + SidebarComponents.AnnotationTableMissingWarning.annotationTableMissingWarningComponent model dispatch + | _ -> () + + SidebarView.content model dispatch + ] ] - ] - footer model - ] \ No newline at end of file + SidebarView.footer (model, dispatch) + ] \ No newline at end of file diff --git a/src/Shared/Shared.fs b/src/Shared/Shared.fs index 46805379..e1832539 100644 --- a/src/Shared/Shared.fs +++ b/src/Shared/Shared.fs @@ -32,18 +32,6 @@ module SorensenDice = calculateDistance resultSet searchSet ) -///This type is still used for JsonExporter page. -[] -type JsonExportType = -| ProcessSeq -| Assay -| ProtocolTemplate - member this.toExplanation = - match this with - | ProcessSeq -> "Sequence of ISA process.json." - | Assay -> "ISA assay.json" - | ProtocolTemplate -> "Schema for Swate protocol template, with template metadata and table json." - /// Development api type ITestAPI = { test : unit -> Async From 7fe31e8c89915a036112399d3f065d1638a8c514 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 21 Jun 2024 12:04:45 +0200 Subject: [PATCH 123/135] smaller code burn :fire: --- src/Client/Client.fsproj | 2 - src/Client/Init.fs | 1 - src/Client/Messages.fs | 4 - src/Client/Model.fs | 11 --- src/Client/Pages/Dag/Dag.fs | 101 ---------------------- src/Client/Routing.fs | 5 -- src/Client/States/DagState.fs | 22 ----- src/Client/States/SpreadsheetInterface.fs | 2 - src/Client/Update.fs | 4 - src/Client/Update/InterfaceUpdate.fs | 9 -- src/Client/Views/SidebarView.fs | 6 -- 11 files changed, 167 deletions(-) delete mode 100644 src/Client/Pages/Dag/Dag.fs delete mode 100644 src/Client/States/DagState.fs diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 3449f220..13e67923 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -37,7 +37,6 @@ - @@ -84,7 +83,6 @@ - diff --git a/src/Client/Init.fs b/src/Client/Init.fs index 31295ed6..a55faf06 100644 --- a/src/Client/Init.fs +++ b/src/Client/Init.fs @@ -21,7 +21,6 @@ let initializeModel () = AddBuildingBlockState = BuildingBlock.Model .init () ProtocolState = Protocol.Model .init () BuildingBlockDetailsState = BuildingBlockDetailsState .init () - DagModel = Dag.Model .init () CytoscapeModel = Cytoscape.Model .init () SpreadsheetModel = Spreadsheet.Model .fromLocalStorage() History = LocalHistory.Model .init().UpdateFromSessionStorage() diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index 64eab759..248098fb 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -129,7 +129,6 @@ type Model = { AddBuildingBlockState : BuildingBlock.Model ///Used to show selected building block information BuildingBlockDetailsState : BuildingBlockDetailsState - DagModel : Dag.Model CytoscapeModel : Cytoscape.Model /// Contains all information about spreadsheet view SpreadsheetModel : Spreadsheet.Model @@ -137,8 +136,6 @@ type Model = { } with member this.updateByExcelState (s:OfficeInterop.Model) = { this with ExcelState = s} - member this.updateByDagModel (m:Dag.Model) = - { this with DagModel = m} type Msg = | DevMsg of DevMsg @@ -153,7 +150,6 @@ type Msg = | BuildingBlockDetails of BuildingBlockDetailsMsg | CytoscapeMsg of Cytoscape.Msg | SpreadsheetMsg of Spreadsheet.Msg -| DagMsg of Dag.Msg /// This is used to forward Msg to SpreadsheetMsg/OfficeInterop | InterfaceMsg of SpreadsheetInterface.Msg //| SettingsProtocolMsg of SettingsProtocolMsg diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 27128900..84eebc8b 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -153,17 +153,6 @@ type PersistentStorageState = { HasOntologiesLoaded = false } -type ApiCallStatus = - | IsNone - | Pending - | Successfull - | Failed of string - -type ApiCallHistoryItem = { - FunctionName : string - Status : ApiCallStatus -} - type PageState = { CurrentPage : Routing.Route IsExpert : bool diff --git a/src/Client/Pages/Dag/Dag.fs b/src/Client/Pages/Dag/Dag.fs deleted file mode 100644 index 9ec7e525..00000000 --- a/src/Client/Pages/Dag/Dag.fs +++ /dev/null @@ -1,101 +0,0 @@ -[] -module Dag.Core - -open Fable.React -open Fable.React.Props -open Fable.Core.JsInterop -open Elmish - -open Shared - -open ExcelColors -open Model -open Messages - -open Dag - -let update (msg:Msg) (currentModel: Messages.Model) : Messages.Model * Cmd = - match msg with - | UpdateLoading loading -> - let nextModel = { - currentModel.DagModel with - Loading = loading - } - currentModel.updateByDagModel nextModel, Cmd.none - | ParseTablesOfficeInteropRequest -> - let nextModel = { - currentModel.DagModel with - Loading = true - } - let cmd = - Cmd.OfPromise.either - OfficeInterop.Core.getBuildingBlocksAndSheets - () - (ParseTablesDagServerRequest >> DagMsg) - (curry GenericError (Dag.UpdateLoading false |> DagMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByDagModel nextModel, cmd - | ParseTablesDagServerRequest (worksheetBuildingBlocksTuple) -> - let cmd = - Cmd.OfAsync.either - Api.dagApi.parseAnnotationTablesToDagHtml - worksheetBuildingBlocksTuple - (ParseTablesDagServerResponse >> DagMsg) - (curry GenericError (Dag.UpdateLoading false |> DagMsg |> Cmd.ofMsg) >> DevMsg) - currentModel, cmd - // - | ParseTablesDagServerResponse dagHtml -> - let nextModel = { - currentModel.DagModel with - Loading = false - DagHtml = Some dagHtml - } - currentModel.updateByDagModel nextModel, Cmd.none - -open Messages -open Feliz -open Feliz.Bulma - -let defaultMessageEle (model:Model) dispatch = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - str "A " - b [] [str "D"] - str "irected " - b [] [str "A"] - str "cyclic " - b [] [str "G"] - str "raph represents the chain of applied protocols to samples. Within are all intermediate products as well as protocols displayed." - ] - Bulma.help [ - str "This only works if your input and output columns have values." - ] - ] - - Bulma.field.div [ - Bulma.button.a [ - Bulma.button.isFullWidth - Bulma.color.isInfo - prop.onClick(fun _ -> SpreadsheetInterface.ParseTablesToDag |> InterfaceMsg |> dispatch) - prop.text "Display dag" - ] - ] - - if model.DagModel.DagHtml.IsSome then - Bulma.field.div [ - iframe [SrcDoc model.DagModel.DagHtml.Value; Style [Width "100%"; Height "400px"] ] [] - ] - ] - -let mainElement (model:Messages.Model) dispatch = - Bulma.content [ - prop.onSubmit (fun e -> e.preventDefault()) - prop.onKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) - prop.children [ - pageHeader "Visualize Protocol Flow" - - Bulma.label "Display directed acyclic graph" - - defaultMessageEle model dispatch - ] - ] \ No newline at end of file diff --git a/src/Client/Routing.fs b/src/Client/Routing.fs index f530d31f..af5375f1 100644 --- a/src/Client/Routing.fs +++ b/src/Client/Routing.fs @@ -14,7 +14,6 @@ type Route = | Info | Protocol | ProtocolSearch -| Dag /// Directed Acylclic Graph | JsonExport | ActivityLog | Settings @@ -27,7 +26,6 @@ type Route = | Route.FilePicker -> "File Picker" | Route.Protocol -> "Templates" | Route.ProtocolSearch -> "Template Search" - | Route.Dag -> "Directed Acylclic Graph" | Route.JsonExport -> "Json Export" | Route.Info -> "Info" | Route.ActivityLog -> "Activity Log" @@ -62,8 +60,6 @@ type Route = createElem [ Html.i [prop.className "fa-solid fa-circle-plus" ];Html.i [prop.className "fa-solid fa-table" ]] p.toStringRdbl | Route.ProtocolSearch -> createElem [ Html.i [prop.className "fa-solid fa-table" ]; Html.i [prop.className "fa-solid fa-magnifying-glass" ]] p.toStringRdbl - | Route.Dag -> - createElem [ Html.i [prop.className "fa-solid fa-diagram-project" ]] p.toStringRdbl | Route.JsonExport -> createElem [ Html.i [prop.className "fa-solid fa-file-export" ]] p.toStringRdbl | Route.FilePicker -> @@ -92,7 +88,6 @@ module Routing = map Route.Info (s "Info") map Route.Protocol (s "ProtocolInsert") map Route.ProtocolSearch (s "Protocol" s "Search") - map Route.Dag (s "Dag") map Route.JsonExport (s "Experts" s "JsonExport") map Route.ActivityLog (s "ActivityLog") map Route.Settings (s "Settings") diff --git a/src/Client/States/DagState.fs b/src/Client/States/DagState.fs deleted file mode 100644 index f348d80b..00000000 --- a/src/Client/States/DagState.fs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Dag - -open Shared.OfficeInteropTypes - -type HtmlString = string - -type Model = { - Loading : bool - DagHtml : HtmlString option -} with - static member init() = { - Loading = false - DagHtml = None - } - -type Msg = -//Client -| UpdateLoading of bool -// -| ParseTablesOfficeInteropRequest -| ParseTablesDagServerRequest of (string * BuildingBlock []) [] -| ParseTablesDagServerResponse of dagHtml:string \ No newline at end of file diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index 26d1fc93..45e7d625 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -21,7 +21,5 @@ type Msg = | InsertFileNames of string list /// Starts chain to export active table to isa json | ExportJson -/// Starts chain to parse all tables to DAG -| ParseTablesToDag | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] \ No newline at end of file diff --git a/src/Client/Update.fs b/src/Client/Update.fs index d8ad1628..c5efd4fc 100644 --- a/src/Client/Update.fs +++ b/src/Client/Update.fs @@ -355,10 +355,6 @@ let update (msg : Msg) (model : Model) : Model * Cmd = CytoscapeModel = nextState} nextModel, nextCmd - | DagMsg msg -> - let nextModel, nextCmd = currentModel |> Dag.Core.update msg - nextModel, nextCmd - /// This function is used to determine which msg should be logged to activity log. /// The function is exception based, so msg which should not be logged needs to be added here. let matchMsgToLog (msg: Msg) = diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 038ed05a..c346cd5a 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -146,15 +146,6 @@ module Interface = let cmd = SpreadsheetMsg Spreadsheet.ExportJson |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" - | ParseTablesToDag -> - match host with - | Some Swatehost.Excel -> - let cmd = DagMsg Dag.ParseTablesOfficeInteropRequest |> Cmd.ofMsg - model, cmd - | Some Swatehost.Browser | Some Swatehost.ARCitect -> - let cmd = SpreadsheetMsg Spreadsheet.ParseTablesToDag |> Cmd.ofMsg - model, cmd - | _ -> failwith "not implemented" | EditBuildingBlock -> match host with | Some Swatehost.Excel -> diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index d82d1c2d..603865e6 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -175,12 +175,6 @@ type SidebarView = | Routing.Route.Settings -> SettingsView.settingsViewComponent model dispatch - //| Routing.Route.SettingsXml -> - // SettingsXml.settingsXmlViewComponent model dispatch - - | Routing.Route.Dag -> - Dag.Core.mainElement model dispatch - | Routing.Route.Info -> InfoView.infoComponent model dispatch From 1b1a657dd5800b737c576d502541dd89427484d2 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 21 Jun 2024 13:33:20 +0200 Subject: [PATCH 124/135] Update JsonExporter --- src/Client/Client.fsproj | 2 +- src/Client/Pages/JsonExporter/JsonExporter.fs | 108 ++++++++++-------- src/Client/Pages/Settings/SettingsView.fs | 10 +- .../OfficeInteropState.fs | 0 src/Client/States/Spreadsheet.fs | 4 +- src/Client/States/SpreadsheetInterface.fs | 2 +- src/Client/Update/InterfaceUpdate.fs | 6 +- src/Client/Update/SpreadsheetUpdate.fs | 60 +++++----- src/Client/Views/SidebarView.fs | 16 +-- src/Shared/JsonExport.fs | 15 +++ src/Shared/Regex.fs | 1 + src/Shared/Shared.fsproj | 1 + src/Shared/TermTypes.fs | 1 - 13 files changed, 125 insertions(+), 101 deletions(-) rename src/Client/{OfficeInterop => States}/OfficeInteropState.fs (100%) create mode 100644 src/Shared/JsonExport.fs diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 13e67923..6a4766a3 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -31,9 +31,9 @@ - + diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index 54ef0c23..d33b2471 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -277,45 +277,23 @@ let download(filename, text) = open Feliz open Feliz.Bulma - -type private JsonExportFormat = - | ARCtrl - | ARCtrlCompressed - | ISA - | ROCrate +open Shared.JsonExport type private JsonExportState = { - Error: exn option - Json: string ExportFormat: JsonExportFormat } with static member init() = { - Error = None; - Json = "" ExportFormat = ROCrate } -module private FileExporterAux = - - open ARCtrl - open ARCtrl.Json - - let toISAJsonString (arcfile: ArcFiles option) = - let timed = fun s -> System.DateTime.Now.ToString("yyyyMMdd_hhmm_") + s - match arcfile with - | None | Some (ArcFiles.Template _) -> None - | Some (ArcFiles.Assay a) -> (timed "assay.json",ArcAssay.toISAJsonString 0 a) |> Some - | Some (ArcFiles.Study (s,as')) -> (timed "study.json", ArcStudy.toISAJsonString(as',0) s) |> Some - | Some (ArcFiles.Investigation i) -> (timed "investigation.json", ArcInvestigation.toISAJsonString 0 i) |> Some - -open FileExporterAux - type FileExporter = - static member private FileFormat(efm: JsonExportFormat, state: JsonExportState) = + static member private FileFormat(efm: JsonExportFormat, state: JsonExportState, setState) = + let isSelected = efm = state.ExportFormat Html.option [ - + prop.selected isSelected + prop.text (string efm) ] [] @@ -330,16 +308,21 @@ type FileExporter = prop.className "select" prop.children [ Html.select [ - Html.option "ISA" - Html.option "Test2" - Html.option "Test3" + prop.onChange (fun (e:Browser.Types.Event) -> + let jef: JsonExportFormat = JsonExportFormat.fromString (e.target?value) + { state with + ExportFormat = jef } + |> setState + ) + prop.children [ + FileExporter.FileFormat(ROCrate, state, setState) + FileExporter.FileFormat(ISA, state, setState) + FileExporter.FileFormat(ARCtrl, state, setState) + FileExporter.FileFormat(ARCtrlCompressed, state, setState) + ] ] ] ] - //Bulma.button.a [ - // prop.text "ISA" - // Bulma.button.isStatic - //] ] Bulma.control.p [ Bulma.control.isExpanded @@ -347,20 +330,15 @@ type FileExporter = Bulma.button.button [ Bulma.button.isFullWidth prop.text "Download" - prop.onClick (fun _ -> - let r = toISAJsonString model.SpreadsheetModel.ArcFile - r |> Option.iter (fun r -> download r) + prop.onClick (fun _ -> + if model.SpreadsheetModel.ArcFile.IsSome then + SpreadsheetInterface.ExportJson (model.SpreadsheetModel.ArcFile.Value, state.ExportFormat) |> InterfaceMsg |> dispatch ) ] ] ] ] ] - if state.Error.IsSome then - Bulma.help [ - Bulma.color.isDanger - prop.textf "Error trying to convert: %s" state.Error.Value.Message - ] ] static member Main(model:Messages.Model, dispatch: Messages.Msg -> unit) = @@ -369,11 +347,47 @@ type FileExporter = Bulma.label "Export to Json" mainFunctionContainer [ - Bulma.field.div [Bulma.help [ - str "Export Swate annotation tables to official ISA-JSON (" - a [Href @"https://isa-specs.readthedocs.io/en/latest/isajson.html#"; Target "_Blank"] [str "more"] - str ")." - ]] + Bulma.field.div [ + Bulma.content [ + Bulma.help "Export Swate annotation tables to official JSON." + Html.ul [ + Html.li [ + Html.b "ARCtrl" + str ": A simple ARCtrl specific format." + ] + Html.li [ + Html.b "ARCtrlCompressed" + str ": A compressed ARCtrl specific format." + ] + Html.li [ + Html.b "ISA" + str ": ISA-JSON format (" + Html.a [ + prop.target.blank + prop.href "https://isa-specs.readthedocs.io/en/latest/isajson.html#" + prop.text "ISA-JSON" + ] + str ")." + ] + Html.li [ + Html.b "ROCrate" + str ": ROCrate format (" + Html.a [ + prop.target.blank + prop.href "https://www.researchobject.org/ro-crate/" + prop.text "ROCrate" + ] + str ", " + Html.a [ + prop.target.blank + prop.href "https://github.com/nfdi4plants/isa-ro-crate-profile/blob/main/profile/isa_ro_crate.md" + prop.text "ISA-Profile" + ] + str ")." + ] + ] + ] + ] FileExporter.JsonExport(model, dispatch) ] ] diff --git a/src/Client/Pages/Settings/SettingsView.fs b/src/Client/Pages/Settings/SettingsView.fs index d60095f4..bea788a8 100644 --- a/src/Client/Pages/Settings/SettingsView.fs +++ b/src/Client/Pages/Settings/SettingsView.fs @@ -119,9 +119,9 @@ let settingsViewComponent (model:Model) dispatch = //Label.label [Label.Props [Style [Color model.SiteStyleState.ColorMode.Accent]]] [str "Advanced Settings"] //customXmlSettings model dispatch - Bulma.label "Advanced Settings" - if model.PageState.IsExpert then - swateCore model dispatch - else - swateExperts model dispatch + //Bulma.label "Advanced Settings" + //if model.PageState.IsExpert then + // swateCore model dispatch + //else + // swateExperts model dispatch ] \ No newline at end of file diff --git a/src/Client/OfficeInterop/OfficeInteropState.fs b/src/Client/States/OfficeInteropState.fs similarity index 100% rename from src/Client/OfficeInterop/OfficeInteropState.fs rename to src/Client/States/OfficeInteropState.fs diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 9d50db22..bac68e74 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -152,12 +152,10 @@ type Msg = | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] /// Starts chain to export active table to isa json -| ExportJson +| ExportJson of ArcFiles * JsonExport.JsonExportFormat /// Starts chain to export all tables to xlsx swate tables. | ExportXlsx of ArcFiles | ExportXlsxDownload of filename: string * byte [] -/// Starts chain to parse all tables to DAG -| ParseTablesToDag // <--> Result Messages <--> ///// This message will save `Model` to local storage and to session storage for history //| Success of Model diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index 45e7d625..3d4d00c6 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -20,6 +20,6 @@ type Msg = | InsertOntologyAnnotation of OntologyAnnotation | InsertFileNames of string list /// Starts chain to export active table to isa json -| ExportJson +| ExportJson of ArcFiles * JsonExport.JsonExportFormat | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] \ No newline at end of file diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index c346cd5a..f2a0e1f2 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -137,13 +137,13 @@ module Interface = Spreadsheet.DeleteColumn (distinct.[0]) |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" - | ExportJson -> + | ExportJson (arcfile, jef) -> match host with | Some Swatehost.Excel -> - failwith "ExportJsonTable not implemented for Excel" + failwith "ExportJson not implemented for Excel" model, Cmd.none | Some Swatehost.Browser -> - let cmd = SpreadsheetMsg Spreadsheet.ExportJson |> Cmd.ofMsg + let cmd = SpreadsheetMsg (Spreadsheet.ExportJson (arcfile, jef)) |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" | EditBuildingBlock -> diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index 50458684..8649fe8b 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -14,6 +14,7 @@ open FsSpreadsheet open FsSpreadsheet.Js open ARCtrl open ARCtrl.Spreadsheet +open ARCtrl.Json module Spreadsheet = @@ -25,6 +26,10 @@ module Spreadsheet = let download(filename, bytes:byte []) = bytes.SaveFileAs(filename) + let downloadFromString(filename, content:string) = + let bytes = System.Text.Encoding.UTF8.GetBytes(content) + bytes.SaveFileAs(filename) + /// /// This function will store the information correctly. /// Can return save information to local storage (persistent between browser sessions) and session storage. @@ -289,38 +294,33 @@ module Spreadsheet = (UpdateArcFile >> Messages.SpreadsheetMsg) (Messages.curry Messages.GenericError Cmd.none >> Messages.DevMsg) state, model, cmd - | ExportJson -> - failwith "ExportsJsonTable is not implemented" - //let exportJsonState = {model.JsonExporterModel with Loading = true} - //let nextModel = model.updateByJsonExporterModel exportJsonState - //let func() = promise { - // return Controller.getTable state - //} - //let cmd = - // Cmd.OfPromise.either - // func - // () - // (JsonExporter.State.ParseTableServerRequest >> Messages.JsonExporterMsg) - // (Messages.curry Messages.GenericError (JsonExporter.State.UpdateLoading false |> Messages.JsonExporterMsg |> Cmd.ofMsg) >> Messages.DevMsg) - //state, nextModel, cmd - state, model, Cmd.none - | ParseTablesToDag -> - failwith "ParseTablesToDag is not implemented" - //let dagState = {model.DagModel with Loading = true} - //let nextModel = model.updateByDagModel dagState - //let func() = promise { - // return Controller.getTables state - //} - //let cmd = - // Cmd.OfPromise.either - // func - // () - // (Dag.ParseTablesDagServerRequest >> Messages.DagMsg) - // (Messages.curry Messages.GenericError (Dag.UpdateLoading false |> Messages.DagMsg |> Cmd.ofMsg) >> Messages.DevMsg) - //state, nextModel, cmd + | ExportJson (arcfile,jef) -> + let name, jsonString = + let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") + let nameFromId (id: string) = (n + "_" + id + ".json") + match arcfile, jef with + | Investigation ai, JsonExport.ARCtrl -> nameFromId ai.Identifier, ArcInvestigation.toJsonString 0 ai + | Investigation ai, JsonExport.ARCtrlCompressed -> nameFromId ai.Identifier, ArcInvestigation.toCompressedJsonString 0 ai + | Investigation ai, JsonExport.ISA -> nameFromId ai.Identifier, ArcInvestigation.toISAJsonString 0 ai + | Investigation ai, JsonExport.ROCrate -> nameFromId ai.Identifier, ArcInvestigation.toROCrateJsonString 0 ai + + | Study (as',_), JsonExport.ARCtrl -> nameFromId as'.Identifier, ArcStudy.toJsonString 0 (as') + | Study (as',_), JsonExport.ARCtrlCompressed -> nameFromId as'.Identifier, ArcStudy.toCompressedJsonString 0 (as') + | Study (as',aaList), JsonExport.ISA -> nameFromId as'.Identifier, ArcStudy.toISAJsonString (aaList,0) (as') + | Study (as',aaList), JsonExport.ROCrate -> nameFromId as'.Identifier, ArcStudy.toROCrateJsonString (aaList,0) (as') + + | Assay aa, JsonExport.ARCtrl -> nameFromId aa.Identifier, ArcAssay.toJsonString 0 aa + | Assay aa, JsonExport.ARCtrlCompressed -> nameFromId aa.Identifier, ArcAssay.toCompressedJsonString 0 aa + | Assay aa, JsonExport.ISA -> nameFromId aa.Identifier, ArcAssay.toISAJsonString 0 aa + | Assay aa, JsonExport.ROCrate -> nameFromId aa.Identifier, ArcAssay.toROCrateJsonString () aa + + | Template t, JsonExport.ARCtrl -> nameFromId t.FileName, Template.toJsonString 0 t + | Template t, JsonExport.ARCtrlCompressed -> nameFromId t.FileName, Template.toCompressedJsonString 0 t + | Template _, anyElse -> failwithf "Error. It is not intended to parse Template to %s format." (string anyElse) + Helper.downloadFromString (name , jsonString) + state, model, Cmd.none | ExportXlsx arcfile-> - // we highjack this loading function let name, fswb = let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") match arcfile with diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index 603865e6..ca5143d5 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -56,17 +56,13 @@ let private tabs (model:Model) dispatch (sidebarsize: Model.WindowSize) = let isIEBrowser : bool = Browser.Dom.window.document?documentMode tabRow model [ if not model.PageState.IsExpert then - createNavigationTab Routing.Route.BuildingBlock model dispatch sidebarsize - createNavigationTab Routing.Route.TermSearch model dispatch sidebarsize - createNavigationTab Routing.Route.Protocol model dispatch sidebarsize - createNavigationTab Routing.Route.FilePicker model dispatch sidebarsize - //if not isIEBrowser then - // docsrc attribute not supported in iframe in IE - //createNavigationTab Routing.Route.Dag model dispatch sidebarsize - createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize + createNavigationTab Routing.Route.BuildingBlock model dispatch sidebarsize + createNavigationTab Routing.Route.TermSearch model dispatch sidebarsize + createNavigationTab Routing.Route.Protocol model dispatch sidebarsize + createNavigationTab Routing.Route.FilePicker model dispatch sidebarsize + createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize else - //createNavigationTab Routing.Route.Validation model dispatch sidebarsize - createNavigationTab Routing.Route.Info model dispatch sidebarsize + createNavigationTab Routing.Route.JsonExport model dispatch sidebarsize ] module private ResizeObserver = diff --git a/src/Shared/JsonExport.fs b/src/Shared/JsonExport.fs new file mode 100644 index 00000000..dc792a1e --- /dev/null +++ b/src/Shared/JsonExport.fs @@ -0,0 +1,15 @@ +module Shared.JsonExport + +type JsonExportFormat = + | ARCtrl + | ARCtrlCompressed + | ISA + | ROCrate + + static member fromString (str: string) = + match str.ToLower() with + | "arctrl" -> ARCtrl + | "arctrlcompressed" -> ARCtrlCompressed + | "isa" -> ISA + | "rocrate" -> ROCrate + | _ -> failwithf "Unknown JSON export format: %s" str diff --git a/src/Shared/Regex.fs b/src/Shared/Regex.fs index a1e4bd88..7fbaf42d 100644 --- a/src/Shared/Regex.fs +++ b/src/Shared/Regex.fs @@ -1,5 +1,6 @@ namespace Shared +[] module Regex = module Pattern = diff --git a/src/Shared/Shared.fsproj b/src/Shared/Shared.fsproj index 25d70a89..9cdbb1b7 100644 --- a/src/Shared/Shared.fsproj +++ b/src/Shared/Shared.fsproj @@ -4,6 +4,7 @@ net8.0 + diff --git a/src/Shared/TermTypes.fs b/src/Shared/TermTypes.fs index 51eb3f9d..27b5e130 100644 --- a/src/Shared/TermTypes.fs +++ b/src/Shared/TermTypes.fs @@ -4,7 +4,6 @@ open ARCtrl module TermTypes = - open Shared.Regex open System type Ontology = { From 337f684cf1a0cc789e37d34a8941cef1144cb58a Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 21 Jun 2024 16:59:16 +0200 Subject: [PATCH 125/135] Groundwork for json import #413 :construction: --- src/Client/Client.fsproj | 5 +- src/Client/MainComponents/NoTablesElement.fs | 16 +- src/Client/MainComponents/Widgets.fs | 4 +- src/Client/Messages.fs | 7 +- src/Client/Modals/SelectiveImportModal.fs | 42 +++ src/Client/Model.fs | 4 - src/Client/Pages/JsonExporter/JsonExporter.fs | 11 +- ...rotocolSearchView.fs => ProtocolSearch.fs} | 0 .../Pages/ProtocolTemplates/ProtocolState.fs | 16 - .../Pages/ProtocolTemplates/ProtocolView.fs | 274 ++---------------- .../Pages/ProtocolTemplates/TemplateFromDB.fs | 140 +++++++++ .../ProtocolTemplates/TemplateFromFile.fs | 157 ++++++++++ src/Client/Spreadsheet/IO.fs | 70 +++-- src/Client/States/Spreadsheet.fs | 4 +- src/Client/States/SpreadsheetInterface.fs | 5 +- src/Client/Update/InterfaceUpdate.fs | 11 +- src/Client/Update/SpreadsheetUpdate.fs | 32 +- src/Client/Views/SidebarView.fs | 2 +- src/Client/style.scss | 8 - src/Shared/ARCtrl.Helper.fs | 30 +- src/Shared/JsonExport.fs | 15 - src/Shared/Shared.fsproj | 1 - 22 files changed, 485 insertions(+), 369 deletions(-) create mode 100644 src/Client/Modals/SelectiveImportModal.fs rename src/Client/Pages/ProtocolTemplates/{ProtocolSearchView.fs => ProtocolSearch.fs} (100%) create mode 100644 src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs create mode 100644 src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs delete mode 100644 src/Shared/JsonExport.fs diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 6a4766a3..02614367 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -53,6 +53,7 @@ + @@ -76,7 +77,9 @@ - + + + diff --git a/src/Client/MainComponents/NoTablesElement.fs b/src/Client/MainComponents/NoTablesElement.fs index 69e799ef..4a1e82e1 100644 --- a/src/Client/MainComponents/NoTablesElement.fs +++ b/src/Client/MainComponents/NoTablesElement.fs @@ -3,7 +3,7 @@ module MainComponents.NoTablesElement open Feliz open Feliz.Bulma -open Spreadsheet +open SpreadsheetInterface open Messages open Browser.Types open Fable.Core.JsInterop @@ -22,7 +22,7 @@ module private UploadHandler = [] let id = "droparea" - let updateMsg = fun r -> r |> SetArcFileFromBytes |> SpreadsheetMsg + let updateMsg = fun r -> r |> ImportXlsx |> InterfaceMsg let setActive_DropArea() = styleCounter <- styleCounter + 1 @@ -69,7 +69,7 @@ let private uploadNewTable dispatch = reader.onload <- fun evt -> let (r: byte []) = evt.target?result - r |> SetArcFileFromBytes |> SpreadsheetMsg |> dispatch + r |> ImportXlsx |> InterfaceMsg |> dispatch reader.onerror <- fun evt -> curry GenericLog Cmd.none ("Error", evt?Value) |> DevMsg |> dispatch @@ -127,7 +127,7 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = let i = ArcInvestigation.init("New Investigation") ArcFiles.Investigation i |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Investigation" @@ -135,10 +135,10 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = Bulma.dropdownItem.a [ prop.onClick(fun _ -> let s = ArcStudy.init("New Study") - let newTable = s.InitTable("New Study Table") + let _ = s.InitTable("New Study Table") ArcFiles.Study (s, []) |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Study" @@ -149,7 +149,7 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = let newTable = a.InitTable("New Assay Table") ArcFiles.Assay a |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Assay" @@ -165,7 +165,7 @@ let private createNewTable isActive toggle (dispatch: Messages.Msg -> unit) = template.LastUpdated <- System.DateTime.Now ArcFiles.Template template |> UpdateArcFile - |> Messages.SpreadsheetMsg + |> InterfaceMsg |> dispatch ) prop.text "Template" diff --git a/src/Client/MainComponents/Widgets.fs b/src/Client/MainComponents/Widgets.fs index e4c47e5d..21a5b542 100644 --- a/src/Client/MainComponents/Widgets.fs +++ b/src/Client/MainComponents/Widgets.fs @@ -216,12 +216,12 @@ type Widget = let insertContent() = [ Bulma.field.div [ - Protocol.Core.TemplateFromDB.addFromDBToTableButton model dispatch + Protocol.TemplateFromDB.addFromDBToTableButton model dispatch ] Bulma.field.div [ prop.style [style.maxHeight (length.px 350); style.overflow.auto] prop.children [ - Protocol.Core.TemplateFromDB.displaySelectedProtocolEle model dispatch + Protocol.TemplateFromDB.displaySelectedProtocolEle model dispatch ] ] ] diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index 248098fb..aed5f8a1 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -82,21 +82,16 @@ module BuildingBlock = module Protocol = type Msg = - // // ------ Process from file ------ - | ParseUploadedFileRequest of raw: byte [] - | ParseUploadedFileResponse of (string * InsertBuildingBlock []) [] // Client | UpdateTemplates of Template [] | UpdateLoading of bool - | RemoveUploadedFileParsed + | RemoveSelectedProtocol // // ------ Protocol from Database ------ | GetAllProtocolsForceRequest | GetAllProtocolsRequest | GetAllProtocolsResponse of string | SelectProtocol of Template | ProtocolIncreaseTimesUsed of protocolName:string - // Client - | RemoveSelectedProtocol type BuildingBlockDetailsMsg = | GetSelectedBuildingBlockTermsRequest of TermSearchable [] diff --git a/src/Client/Modals/SelectiveImportModal.fs b/src/Client/Modals/SelectiveImportModal.fs new file mode 100644 index 00000000..79741ab6 --- /dev/null +++ b/src/Client/Modals/SelectiveImportModal.fs @@ -0,0 +1,42 @@ +namespace Modals + +open Feliz +open Feliz.Bulma +open Model +open Messages +open Shared + +open ARCtrl + +type SelectiveImportModal = + + [] + static member Main (import: ArcFiles) (rmv: _ -> unit) = + Bulma.modal [ + Bulma.modal.isActive + prop.children [ + Bulma.modalBackground [ prop.onClick rmv ] + Bulma.modalCard [ + prop.style [style.maxHeight(length.percent 70); style.overflowY.hidden] + prop.children [ + Bulma.modalCardHead [ + Bulma.modalCardTitle "Import" + Bulma.delete [ prop.onClick rmv ] + ] + Bulma.modalCardBody [ + Html.div "Placeholder1" + ] + Bulma.modalCardFoot [ + Bulma.button.button [ + color.isInfo + prop.style [style.marginLeft length.auto] + prop.text "Submit" + prop.onClick(fun e -> + rmv e + ) + ] + ] + ] + ] + ] + ] diff --git a/src/Client/Model.fs b/src/Client/Model.fs index 84eebc8b..4d8c6327 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -316,8 +316,6 @@ module Protocol = // Client Loading : bool LastUpdated : System.DateTime option - // // ------ Process from file ------ - UploadedFileParsed : (string*InsertBuildingBlock []) [] // ------ Protocol from Database ------ TemplateSelected : ARCtrl.Template option Templates : ARCtrl.Template [] @@ -327,8 +325,6 @@ module Protocol = Loading = false LastUpdated = None TemplateSelected = None - // // ------ Process from file ------ - UploadedFileParsed = [||] // ------ Protocol from Database ------ Templates = [||] } diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index d33b2471..4a73cf99 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -277,13 +277,12 @@ let download(filename, text) = open Feliz open Feliz.Bulma -open Shared.JsonExport type private JsonExportState = { ExportFormat: JsonExportFormat } with static member init() = { - ExportFormat = ROCrate + ExportFormat = JsonExportFormat.ROCrate } type FileExporter = @@ -315,10 +314,10 @@ type FileExporter = |> setState ) prop.children [ - FileExporter.FileFormat(ROCrate, state, setState) - FileExporter.FileFormat(ISA, state, setState) - FileExporter.FileFormat(ARCtrl, state, setState) - FileExporter.FileFormat(ARCtrlCompressed, state, setState) + FileExporter.FileFormat(JsonExportFormat.ROCrate, state, setState) + FileExporter.FileFormat(JsonExportFormat.ISA, state, setState) + FileExporter.FileFormat(JsonExportFormat.ARCtrl, state, setState) + FileExporter.FileFormat(JsonExportFormat.ARCtrlCompressed, state, setState) ] ] ] diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs similarity index 100% rename from src/Client/Pages/ProtocolTemplates/ProtocolSearchView.fs rename to src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs index 5a156cab..9182297f 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolState.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolState.fs @@ -15,19 +15,6 @@ module Protocol = match fujMsg with | UpdateLoading next -> {state with Loading = next}, Cmd.none - // // ------ Process from file ------ - | ParseUploadedFileRequest bytes -> - failwith "ParseUploadedFileRequest IS NOT IMPLEMENTED YET" - //let cmd = - // Cmd.OfAsync.either - // Api.templateApi.tryParseToBuildingBlocks - // bytes - // (ParseUploadedFileResponse >> ProtocolMsg) - // (curry GenericError (UpdateLoading false |> ProtocolMsg |> Cmd.ofMsg) >> DevMsg) - state, Cmd.none - | ParseUploadedFileResponse buildingBlockTables -> - let nextState = { state with UploadedFileParsed = buildingBlockTables } - nextState, Cmd.none // ------ Protocol from Database ------ | GetAllProtocolsRequest -> let now = System.DateTime.UtcNow @@ -88,6 +75,3 @@ module Protocol = TemplateSelected = None } nextState, Cmd.none - | RemoveUploadedFileParsed -> - let nextState = {state with UploadedFileParsed = Array.empty} - nextState, Cmd.none diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index df466a9c..3a298d5b 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -1,4 +1,4 @@ -module Protocol.Core +namespace Protocol open System @@ -13,271 +13,31 @@ open Fable.Core.JsInterop open Model open Messages open Browser.Types - -open Shared - -open OfficeInterop -open Protocol - +open SpreadsheetInterface open Messages open Elmish open Feliz open Feliz.Bulma -module TemplateFromJsonFile = - - let fileUploadButton (model:Model) dispatch = - let uploadId = "UploadFiles_ElementId" - Bulma.label [ - Bulma.fileInput [ - prop.id uploadId - prop.type' "file"; - prop.style [style.display.none] - prop.onChange (fun (ev: File list) -> - let fileList = ev //: FileList = ev.target?files - - if fileList.Length > 0 then - let file = fileList.Item 0 |> fun f -> f.slice() - - let reader = Browser.Dom.FileReader.Create() - - reader.onload <- fun evt -> - let (r: byte []) = evt.target?result - r |> ParseUploadedFileRequest |> ProtocolMsg |> dispatch - - reader.onerror <- fun evt -> - curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch - - reader.readAsArrayBuffer(file) - else - () - let picker = Browser.Dom.document.getElementById(uploadId) - // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 - picker?value <- null - ) - ] - Bulma.button.a [ - Bulma.color.isInfo; - Bulma.button.isFullWidth - prop.onClick(fun e -> - e.preventDefault() - let getUploadElement = Browser.Dom.document.getElementById uploadId - getUploadElement.click() - () - ) - prop.text "Upload protocol" - ] - ] - - let fileUploadEle (model:Model) dispatch = - let hasData = model.ProtocolState.UploadedFileParsed <> Array.empty - Bulma.columns [ - Bulma.columns.isMobile - prop.children [ - Bulma.column [ - fileUploadButton model dispatch - ] - if hasData then - Bulma.column [ - Bulma.column.isNarrow - Bulma.button.a [ - prop.onClick (fun e -> RemoveUploadedFileParsed |> ProtocolMsg |> dispatch) - Bulma.color.isDanger - prop.children (Html.i [prop.className "fa-solid fa-times"]) - ] |> prop.children - ] - ] - ] - - let importToTableEle (model:Model) (dispatch:Messages.Msg -> unit) = - let hasData = model.ProtocolState.UploadedFileParsed <> Array.empty - Bulma.field.div [ - Bulma.field.hasAddons - Bulma.control.div [ - Bulma.control.isExpanded - Bulma.button.a [ - Bulma.color.isInfo - if hasData then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick(fun _ -> - Browser.Dom.window.alert("'SpreadsheetInterface.ImportFile' is not implemented") - //SpreadsheetInterface.ImportFile model.ProtocolState.UploadedFileParsed |> InterfaceMsg |> dispatch - ) - prop.text "Insert json" - ] |> prop.children - ] |> prop.children - ] - - let protocolInsertElement (model:Model) dispatch = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - b [] [str "Insert tables via ISA-JSON files."] - str " You can use Settings --> Swate.Experts to create these files from existing Swate tables. " - span [Style [Color NFDIColors.Red.Base]] [str "Only missing building blocks will be added."] - ] - ] - - Bulma.field.div [ - fileUploadEle model dispatch - ] - - importToTableEle model dispatch - ] - -module TemplateFromDB = - - let toProtocolSearchElement (model:Model) dispatch = - Bulma.button.span [ - prop.onClick(fun _ -> UpdatePageState (Some Routing.Route.ProtocolSearch) |> dispatch) - Bulma.color.isInfo - Bulma.button.isFullWidth - prop.style [style.margin (length.rem 1, length.px 0)] - prop.text "Browse database" ] - - let addFromDBToTableButton (model:Messages.Model) dispatch = - Bulma.columns [ - Bulma.columns.isMobile - prop.children [ - Bulma.column [ - prop.children [ - Bulma.button.a [ - Bulma.color.isSuccess - if model.ProtocolState.TemplateSelected.IsSome then - Bulma.button.isActive - else - Bulma.color.isDanger - prop.disabled true - Bulma.button.isFullWidth - prop.onClick (fun _ -> - if model.ProtocolState.TemplateSelected.IsNone then - failwith "No template selected!" - // Remove existing columns - let mutable columnsToRemove = [] - // find duplicate columns - let tablecopy = model.ProtocolState.TemplateSelected.Value.Table.Copy() - for header in model.SpreadsheetModel.ActiveTable.Headers do - let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) - if containsAtIndex.IsSome then - columnsToRemove <- containsAtIndex.Value::columnsToRemove - log columnsToRemove - tablecopy.RemoveColumns (Array.ofList columnsToRemove) - tablecopy.IteriColumns(fun i c0 -> - let c1 = {c0 with Cells = [||]} - let c2 = - if c1.Header.isInput then - match model.SpreadsheetModel.ActiveTable.TryGetInputColumn() with - | Some ic -> - {c1 with Cells = ic.Cells} - | _ -> c1 - elif c1.Header.isOutput then - match model.SpreadsheetModel.ActiveTable.TryGetOutputColumn() with - | Some oc -> - {c1 with Cells = oc.Cells} - | _ -> c1 - else - c1 - tablecopy.UpdateColumn(i, c2.Header, c2.Cells) - ) - log(tablecopy.RowCount, tablecopy.ColumnCount) - let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel - let joinConfig = Some ARCtrl.TableJoinOptions.WithValues // If changed to anything else we need different logic to keep input/output values - SpreadsheetInterface.JoinTable (tablecopy, Some index, joinConfig ) |> InterfaceMsg |> dispatch - ) - prop.text "Add template" - ] - ] - ] - if model.ProtocolState.TemplateSelected.IsSome then - Bulma.column [ - Bulma.column.isNarrow - Bulma.button.a [ - prop.onClick (fun e -> RemoveSelectedProtocol |> ProtocolMsg |> dispatch) - Bulma.color.isDanger - Html.i [prop.className "fa-solid fa-times"] |> prop.children - ] |> prop.children - ] - ] - ] - - - - let displaySelectedProtocolEle (model:Model) dispatch = - div [Style [OverflowX OverflowOptions.Auto; MarginBottom "1rem"]] [ - Bulma.table [ - Bulma.table.isFullWidth; - Bulma.table.isBordered - prop.children [ - thead [] [ - Html.tr [ - Html.th "Column" - Html.th "Column TAN" - //Html.th "Unit" - //Html.th "Unit TAN" - ] - ] - tbody [] [ - for column in model.ProtocolState.TemplateSelected.Value.Table.Columns do - //let unitOption = column.TryGetColumnUnits() - yield - Html.tr [ - td [] [str (column.Header.ToString())] - td [] [str (if column.Header.IsTermColumn then column.Header.ToTerm().TermAccessionShort else "-")] - //td [] [str (if unitOption.IsSome then insertBB.UnitTerm.Value.Name else "-")] - //td [] [str (if insertBB.HasUnit then insertBB.UnitTerm.Value.TermAccession else "-")] - ] - ] - ] - ] - ] - - let showDatabaseProtocolTemplate (model:Messages.Model) dispatch = - mainFunctionContainer [ - Bulma.field.div [ - Bulma.help [ - b [] [str "Search the database for templates."] - str " The building blocks from these templates can be inserted into the Swate table. " - span [Style [Color NFDIColors.Red.Base]] [str "Only missing building blocks will be added."] - ] - ] - Bulma.field.div [ - toProtocolSearchElement model dispatch - ] - - Bulma.field.div [ - addFromDBToTableButton model dispatch - ] - if model.ProtocolState.TemplateSelected.IsSome then - Bulma.field.div [ - displaySelectedProtocolEle model dispatch - ] - Bulma.field.div [ - addFromDBToTableButton model dispatch - ] - ] - +type Templates = -let fileUploadViewComponent (model:Messages.Model) dispatch = - div [ - OnSubmit (fun e -> e.preventDefault()) - // https://keycode.info/ - OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) - ] [ + static member Main (model:Messages.Model, dispatch) = + div [ + OnSubmit (fun e -> e.preventDefault()) + // https://keycode.info/ + OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + ] [ - pageHeader "Templates" + pageHeader "Templates" - // Box 1 - Bulma.label "Add template from database." + // Box 1 + Bulma.label "Add template from database." - TemplateFromDB.showDatabaseProtocolTemplate model dispatch + TemplateFromDB.Main(model, dispatch) - // Box 2 - Bulma.label "Add template(s) from file." + // Box 2 + Bulma.label "Add template(s) from file." - TemplateFromJsonFile.protocolInsertElement model dispatch - ] \ No newline at end of file + TemplateFromFile.Main(model, dispatch) + ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs new file mode 100644 index 00000000..9d39bb1c --- /dev/null +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs @@ -0,0 +1,140 @@ +namespace Protocol + +open Feliz +open Feliz.Bulma +open Messages + +type TemplateFromDB = + + static member toProtocolSearchElement (model:Model) dispatch = + Bulma.button.span [ + prop.onClick(fun _ -> UpdatePageState (Some Routing.Route.ProtocolSearch) |> dispatch) + Bulma.color.isInfo + Bulma.button.isFullWidth + prop.style [style.margin (length.rem 1, length.px 0)] + prop.text "Browse database" ] + + static member addFromDBToTableButton (model:Messages.Model) dispatch = + Bulma.columns [ + Bulma.columns.isMobile + prop.children [ + Bulma.column [ + prop.children [ + Bulma.button.a [ + Bulma.color.isSuccess + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.button.isActive + else + Bulma.color.isDanger + prop.disabled true + Bulma.button.isFullWidth + prop.onClick (fun _ -> + if model.ProtocolState.TemplateSelected.IsNone then + failwith "No template selected!" + // Remove existing columns + let mutable columnsToRemove = [] + // find duplicate columns + let tablecopy = model.ProtocolState.TemplateSelected.Value.Table.Copy() + for header in model.SpreadsheetModel.ActiveTable.Headers do + let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) + if containsAtIndex.IsSome then + columnsToRemove <- containsAtIndex.Value::columnsToRemove + log columnsToRemove + tablecopy.RemoveColumns (Array.ofList columnsToRemove) + tablecopy.IteriColumns(fun i c0 -> + let c1 = {c0 with Cells = [||]} + let c2 = + if c1.Header.isInput then + match model.SpreadsheetModel.ActiveTable.TryGetInputColumn() with + | Some ic -> + {c1 with Cells = ic.Cells} + | _ -> c1 + elif c1.Header.isOutput then + match model.SpreadsheetModel.ActiveTable.TryGetOutputColumn() with + | Some oc -> + {c1 with Cells = oc.Cells} + | _ -> c1 + else + c1 + tablecopy.UpdateColumn(i, c2.Header, c2.Cells) + ) + log(tablecopy.RowCount, tablecopy.ColumnCount) + let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel + let joinConfig = Some ARCtrl.TableJoinOptions.WithValues // If changed to anything else we need different logic to keep input/output values + SpreadsheetInterface.JoinTable (tablecopy, Some index, joinConfig ) |> InterfaceMsg |> dispatch + ) + prop.text "Add template" + ] + ] + ] + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.column [ + Bulma.column.isNarrow + Bulma.button.a [ + prop.onClick (fun e -> Protocol.RemoveSelectedProtocol |> ProtocolMsg |> dispatch) + Bulma.color.isDanger + Html.i [prop.className "fa-solid fa-times"] |> prop.children + ] |> prop.children + ] + ] + ] + + static member displaySelectedProtocolEle (model:Model) dispatch = + Html.div [ + prop.style [style.overflowX.auto; style.marginBottom (length.rem 1)] + prop.children [ + Bulma.table [ + Bulma.table.isFullWidth; + Bulma.table.isBordered + prop.children [ + Html.thead [ + Html.tr [ + Html.th "Column" + Html.th "Column TAN" + //Html.th "Unit" + //Html.th "Unit TAN" + ] + ] + Html.tbody [ + for column in model.ProtocolState.TemplateSelected.Value.Table.Columns do + //let unitOption = column.TryGetColumnUnits() + yield + Html.tr [ + Html.td (column.Header.ToString()) + Html.td (if column.Header.IsTermColumn then column.Header.ToTerm().TermAccessionShort else "-") + //td [] [str (if unitOption.IsSome then insertBB.UnitTerm.Value.Name else "-")] + //td [] [str (if insertBB.HasUnit then insertBB.UnitTerm.Value.TermAccession else "-")] + ] + ] + ] + ] + ] + ] + + static member Main(model:Messages.Model, dispatch) = + mainFunctionContainer [ + Bulma.field.div [ + Bulma.help [ + Html.b "Search the database for templates." + Html.text " The building blocks from these templates can be inserted into the Swate table. " + Html.span [ + color.hasTextDanger + prop.text "Only missing building blocks will be added." + ] + ] + ] + Bulma.field.div [ + TemplateFromDB.toProtocolSearchElement model dispatch + ] + + Bulma.field.div [ + TemplateFromDB.addFromDBToTableButton model dispatch + ] + if model.ProtocolState.TemplateSelected.IsSome then + Bulma.field.div [ + TemplateFromDB.displaySelectedProtocolEle model dispatch + ] + Bulma.field.div [ + TemplateFromDB.addFromDBToTableButton model dispatch + ] + ] diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs new file mode 100644 index 00000000..a0528c59 --- /dev/null +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs @@ -0,0 +1,157 @@ +namespace Protocol + +open Fable.Core +open Fable.React +open Fable.React.Props +//open Fable.Core.JS +open Fable.Core.JsInterop + +open Model +open Messages +open Browser.Types +open SpreadsheetInterface +open Messages +open Elmish + +open Feliz +open Feliz.Bulma +open Shared + +type private TemplateFromFileState = { + /// User select type to upload + FileType: ArcFilesDiscriminate + /// User selects json type to upload + JsonFormat: JsonExportFormat + UploadedFile: ArcFiles option + Loading: bool +} with + static member init () = + { + FileType = ArcFilesDiscriminate.Assay + JsonFormat = JsonExportFormat.ROCrate + UploadedFile = None + Loading = false + } + +module private Helper = + + let upload (uploadId: string) (state: TemplateFromFileState) setState dispatch (ev: File list) = + let fileList = ev //: FileList = ev.target?files + + if fileList.Length > 0 then + let file = fileList.Item 0 |> fun f -> f.slice() + + let reader = Browser.Dom.FileReader.Create() + + reader.onload <- fun evt -> + let (r: string) = evt.target?result + async { + setState {state with Loading = true} + let! af = Spreadsheet.IO.Json.readFromJson state.FileType state.JsonFormat r |> Async.AwaitPromise + setState {state with UploadedFile = Some af; Loading = false} + } |> Async.StartImmediate + + reader.onerror <- fun evt -> + curry GenericLog Cmd.none ("Error", evt.Value) |> DevMsg |> dispatch + + reader.readAsText(file) + else + () + let picker = Browser.Dom.document.getElementById(uploadId) + // https://stackoverflow.com/questions/3528359/html-input-type-file-file-selection-event/3528376 + picker?value <- null + +type TemplateFromFile = + + static member private fileUploadButton (state:TemplateFromFileState, setState: TemplateFromFileState -> unit, dispatch) = + let uploadId = "UploadFiles_ElementId" + Bulma.label [ + Bulma.fileInput [ + prop.id uploadId + prop.type' "file"; + prop.style [style.display.none] + prop.onChange (fun (ev: File list) -> + Helper.upload uploadId state setState dispatch ev + ) + ] + Bulma.button.a [ + Bulma.color.isInfo; + Bulma.button.isFullWidth + prop.onClick(fun e -> + e.preventDefault() + let getUploadElement = Browser.Dom.document.getElementById uploadId + getUploadElement.click() + ) + prop.text "Upload protocol" + ] + ] + + static member private SelectorButton<'a when 'a : equality> (targetselector: 'a, selector: 'a, setSelector: 'a -> unit, ?isDisabled) = + Bulma.button.button [ + if isDisabled.IsSome then + prop.disabled isDisabled.Value + prop.style [style.flexGrow 1] + if (targetselector = selector) then + color.isSuccess + button.isSelected + prop.onClick (fun _ -> setSelector targetselector) + prop.text (string targetselector) + ] + + [] + static member Main(model: Messages.Model, dispatch) = + let state, setState = React.useState(TemplateFromFileState.init) + let setJsonFormat = fun x -> setState { state with JsonFormat = x } + let setFileType = fun x -> setState { state with FileType = x } + let fileTypeDisabled (ft: ArcFilesDiscriminate) = + match state.JsonFormat, ft with + // isa and rocrate do not support template + | JsonExportFormat.ROCrate, ArcFilesDiscriminate.Template + | JsonExportFormat.ISA, ArcFilesDiscriminate.Template -> true + | _ -> false + let jsonFormatDisabled (jf: JsonExportFormat) = + match state.FileType ,jf with + // template does not support isa and rocrate + | ArcFilesDiscriminate.Template, JsonExportFormat.ROCrate + | ArcFilesDiscriminate.Template, JsonExportFormat.ISA -> true + | _ -> false + mainFunctionContainer [ + // modal! + match state.UploadedFile with + | Some af -> + Modals.SelectiveImportModal.Main af (fun _ -> TemplateFromFileState.init() |> setState) + | None -> Html.none + Bulma.field.div [ + Bulma.help [ + b [] [str "Import JSON files."] + str " You can use \"Json Export\" to create these files from existing Swate tables. " + ] + ] + Bulma.field.div [ + Bulma.buttons [ + buttons.hasAddons + prop.children [ + JsonExportFormat.ROCrate |> fun jef -> TemplateFromFile.SelectorButton (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + JsonExportFormat.ISA |> fun jef -> TemplateFromFile.SelectorButton (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + JsonExportFormat.ARCtrl |> fun jef -> TemplateFromFile.SelectorButton (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + JsonExportFormat.ARCtrlCompressed |> fun jef -> TemplateFromFile.SelectorButton (jef, state.JsonFormat, setJsonFormat, jsonFormatDisabled jef) + ] + ] + ] + + Bulma.field.div [ + Bulma.buttons [ + buttons.hasAddons + prop.children [ + ArcFilesDiscriminate.Assay |> fun ft -> TemplateFromFile.SelectorButton (ft, state.FileType, setFileType, fileTypeDisabled ft) + ArcFilesDiscriminate.Study |> fun ft -> TemplateFromFile.SelectorButton (ft, state.FileType, setFileType, fileTypeDisabled ft) + ArcFilesDiscriminate.Investigation |> fun ft -> TemplateFromFile.SelectorButton (ft, state.FileType, setFileType, fileTypeDisabled ft) + ArcFilesDiscriminate.Template |> fun ft -> TemplateFromFile.SelectorButton (ft, state.FileType, setFileType, fileTypeDisabled ft) + ] + ] + ] + + Bulma.field.div [ + TemplateFromFile.fileUploadButton(state, setState, dispatch) + ] + ] \ No newline at end of file diff --git a/src/Client/Spreadsheet/IO.fs b/src/Client/Spreadsheet/IO.fs index 9dacb807..85ba5567 100644 --- a/src/Client/Spreadsheet/IO.fs +++ b/src/Client/Spreadsheet/IO.fs @@ -6,22 +6,54 @@ open FsSpreadsheet.Js open Shared open FsSpreadsheet -// TODO: Can this be done better? If we want to allow upload of any isa.xlsx file? -let readFromBytes (bytes: byte []) = - // Try each conversion function and return the first successful result - promise { - let! fswb = FsSpreadsheet.Js.Xlsx.fromXlsxBytes bytes - let ws = fswb.GetWorksheets() - let arcfile = - match ws with - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name ) -> - ArcAssay.fromFsWorkbook fswb |> Assay - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name ) -> - ArcStudy.fromFsWorkbook fswb |> Study - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcInvestigation.metaDataSheetName = ws.Name ) -> - ArcInvestigation.fromFsWorkbook fswb |> Investigation - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.Template.metaDataSheetName = ws.Name ) -> - ARCtrl.Spreadsheet.Template.fromFsWorkbook fswb |> Template - | _ -> failwith "Unable to identify given file. Missing metadata sheet with correct name." - return arcfile - } \ No newline at end of file +module Xlsx = + + let readFromBytes (bytes: byte []) = + // Try each conversion function and return the first successful result + promise { + let! fswb = FsSpreadsheet.Js.Xlsx.fromXlsxBytes bytes + let ws = fswb.GetWorksheets() + let arcfile = + match ws with + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name ) -> + ArcAssay.fromFsWorkbook fswb |> Assay + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name ) -> + ArcStudy.fromFsWorkbook fswb |> Study + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcInvestigation.metaDataSheetName = ws.Name ) -> + ArcInvestigation.fromFsWorkbook fswb |> Investigation + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.Template.metaDataSheetName = ws.Name ) -> + ARCtrl.Spreadsheet.Template.fromFsWorkbook fswb |> Template + | _ -> failwith "Unable to identify given file. Missing metadata sheet with correct name." + return arcfile + } + +module Json = + + open ARCtrlHelper + open ARCtrl + open ARCtrl.Json + + let readFromJson (fileType: ArcFilesDiscriminate) (jsonType: JsonExportFormat) (json: string) = + promise { + let arcfile = + match fileType, jsonType with + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ARCtrl -> ArcInvestigation.fromJsonString json |> ArcFiles.Investigation + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ARCtrlCompressed -> ArcInvestigation.fromCompressedJsonString json |> ArcFiles.Investigation + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ISA -> ArcInvestigation.fromISAJsonString json |> ArcFiles.Investigation + | ArcFilesDiscriminate.Investigation, JsonExportFormat.ROCrate -> ArcInvestigation.fromROCrateJsonString json |> ArcFiles.Investigation + + | ArcFilesDiscriminate.Study, JsonExportFormat.ARCtrl -> ArcStudy.fromJsonString json |> fun x -> ArcFiles.Study(x, []) + | ArcFilesDiscriminate.Study, JsonExportFormat.ARCtrlCompressed -> ArcStudy.fromCompressedJsonString json |> fun x -> ArcFiles.Study(x, []) + | ArcFilesDiscriminate.Study, JsonExportFormat.ISA -> ArcStudy.fromISAJsonString json |> ArcFiles.Study + | ArcFilesDiscriminate.Study, JsonExportFormat.ROCrate -> ArcStudy.fromROCrateJsonString json |> ArcFiles.Study + + | ArcFilesDiscriminate.Assay, JsonExportFormat.ARCtrl -> ArcAssay.fromJsonString json |> ArcFiles.Assay + | ArcFilesDiscriminate.Assay, JsonExportFormat.ARCtrlCompressed -> ArcAssay.fromCompressedJsonString json |> ArcFiles.Assay + | ArcFilesDiscriminate.Assay, JsonExportFormat.ISA -> ArcAssay.fromISAJsonString json |> ArcFiles.Assay + | ArcFilesDiscriminate.Assay, JsonExportFormat.ROCrate -> ArcAssay.fromROCrateJsonString json |> ArcFiles.Assay + + | ArcFilesDiscriminate.Template, JsonExportFormat.ARCtrl -> Template.fromJsonString json |> ArcFiles.Template + | ArcFilesDiscriminate.Template, JsonExportFormat.ARCtrlCompressed -> Template.fromCompressedJsonString json |> ArcFiles.Template + | ArcFilesDiscriminate.Template, anyElse -> failwithf "Error. It is not intended to parse Template from %s format." (string anyElse) + return arcfile + } \ No newline at end of file diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index bac68e74..64a1832c 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -139,7 +139,7 @@ type Msg = // | EditColumn of index: int * newType: SwateCell * b_type: BuildingBlockType option /// This will reset Spreadsheet.Model to Spreadsheet.Model.init() and clear all webstorage. | Reset -| SetArcFileFromBytes of byte [] +| ImportXlsx of byte [] // <--> INTEROP <--> | CreateAnnotationTable of tryUsePrevOutput:bool | AddAnnotationBlock of CompositeColumn @@ -152,7 +152,7 @@ type Msg = | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] /// Starts chain to export active table to isa json -| ExportJson of ArcFiles * JsonExport.JsonExportFormat +| ExportJson of ArcFiles * JsonExportFormat /// Starts chain to export all tables to xlsx swate tables. | ExportXlsx of ArcFiles | ExportXlsxDownload of filename: string * byte [] diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index 3d4d00c6..c285c97b 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -13,13 +13,14 @@ type Msg = | AddAnnotationBlock of CompositeColumn | AddAnnotationBlocks of CompositeColumn [] | JoinTable of ArcTable * index: int option * options: TableJoinOptions option -| ImportFile of ArcFiles +| UpdateArcFile of ArcFiles /// Open modal for selected building block, allows editing on standalone only. | EditBuildingBlock /// Inserts TermMinimal to selected fields of one column | InsertOntologyAnnotation of OntologyAnnotation | InsertFileNames of string list +| ImportXlsx of byte [] /// Starts chain to export active table to isa json -| ExportJson of ArcFiles * JsonExport.JsonExportFormat +| ExportJson of ArcFiles * JsonExportFormat | UpdateTermColumns | UpdateTermColumnsResponse of TermTypes.TermSearchable [] \ No newline at end of file diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index f2a0e1f2..a7b9bbdd 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -80,7 +80,7 @@ module Interface = let cmd = Spreadsheet.JoinTable (table, index, options) |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" - | ImportFile tables -> + | UpdateArcFile tables -> match host with | Some Swatehost.Excel -> //let cmd = OfficeInterop.ImportFile tables |> OfficeInteropMsg |> Cmd.ofMsg @@ -90,6 +90,15 @@ module Interface = let cmd = Spreadsheet.UpdateArcFile tables |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" + | ImportXlsx bytes -> + match host with + | Some Swatehost.Excel -> + Browser.Dom.window.alert "ImportXlsx Not implemented" + model, Cmd.none + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + let cmd = Spreadsheet.ImportXlsx bytes |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd + | _ -> failwith "not implemented" | InsertOntologyAnnotation termMinimal -> match host with //| Swatehost.Excel _ -> diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index 8649fe8b..0e1a1339 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -286,10 +286,10 @@ module Spreadsheet = //| EditColumn (columnIndex, newCellType, b_type) -> // let cmd = createPromiseCmd <| fun _ -> Controller.editColumn (columnIndex, newCellType, b_type) state // state, model, cmd - | SetArcFileFromBytes bytes -> + | ImportXlsx bytes -> let cmd = Cmd.OfPromise.either - Spreadsheet.IO.readFromBytes + Spreadsheet.IO.Xlsx.readFromBytes bytes (UpdateArcFile >> Messages.SpreadsheetMsg) (Messages.curry Messages.GenericError Cmd.none >> Messages.DevMsg) @@ -299,23 +299,23 @@ module Spreadsheet = let n = System.DateTime.Now.ToUniversalTime().ToString("yyyyMMdd_hhmmss") let nameFromId (id: string) = (n + "_" + id + ".json") match arcfile, jef with - | Investigation ai, JsonExport.ARCtrl -> nameFromId ai.Identifier, ArcInvestigation.toJsonString 0 ai - | Investigation ai, JsonExport.ARCtrlCompressed -> nameFromId ai.Identifier, ArcInvestigation.toCompressedJsonString 0 ai - | Investigation ai, JsonExport.ISA -> nameFromId ai.Identifier, ArcInvestigation.toISAJsonString 0 ai - | Investigation ai, JsonExport.ROCrate -> nameFromId ai.Identifier, ArcInvestigation.toROCrateJsonString 0 ai + | Investigation ai, JsonExportFormat.ARCtrl -> nameFromId ai.Identifier, ArcInvestigation.toJsonString 0 ai + | Investigation ai, JsonExportFormat.ARCtrlCompressed -> nameFromId ai.Identifier, ArcInvestigation.toCompressedJsonString 0 ai + | Investigation ai, JsonExportFormat.ISA -> nameFromId ai.Identifier, ArcInvestigation.toISAJsonString 0 ai + | Investigation ai, JsonExportFormat.ROCrate -> nameFromId ai.Identifier, ArcInvestigation.toROCrateJsonString 0 ai - | Study (as',_), JsonExport.ARCtrl -> nameFromId as'.Identifier, ArcStudy.toJsonString 0 (as') - | Study (as',_), JsonExport.ARCtrlCompressed -> nameFromId as'.Identifier, ArcStudy.toCompressedJsonString 0 (as') - | Study (as',aaList), JsonExport.ISA -> nameFromId as'.Identifier, ArcStudy.toISAJsonString (aaList,0) (as') - | Study (as',aaList), JsonExport.ROCrate -> nameFromId as'.Identifier, ArcStudy.toROCrateJsonString (aaList,0) (as') + | Study (as',_), JsonExportFormat.ARCtrl -> nameFromId as'.Identifier, ArcStudy.toJsonString 0 (as') + | Study (as',_), JsonExportFormat.ARCtrlCompressed -> nameFromId as'.Identifier, ArcStudy.toCompressedJsonString 0 (as') + | Study (as',aaList), JsonExportFormat.ISA -> nameFromId as'.Identifier, ArcStudy.toISAJsonString (aaList,0) (as') + | Study (as',aaList), JsonExportFormat.ROCrate -> nameFromId as'.Identifier, ArcStudy.toROCrateJsonString (aaList,0) (as') - | Assay aa, JsonExport.ARCtrl -> nameFromId aa.Identifier, ArcAssay.toJsonString 0 aa - | Assay aa, JsonExport.ARCtrlCompressed -> nameFromId aa.Identifier, ArcAssay.toCompressedJsonString 0 aa - | Assay aa, JsonExport.ISA -> nameFromId aa.Identifier, ArcAssay.toISAJsonString 0 aa - | Assay aa, JsonExport.ROCrate -> nameFromId aa.Identifier, ArcAssay.toROCrateJsonString () aa + | Assay aa, JsonExportFormat.ARCtrl -> nameFromId aa.Identifier, ArcAssay.toJsonString 0 aa + | Assay aa, JsonExportFormat.ARCtrlCompressed -> nameFromId aa.Identifier, ArcAssay.toCompressedJsonString 0 aa + | Assay aa, JsonExportFormat.ISA -> nameFromId aa.Identifier, ArcAssay.toISAJsonString 0 aa + | Assay aa, JsonExportFormat.ROCrate -> nameFromId aa.Identifier, ArcAssay.toROCrateJsonString () aa - | Template t, JsonExport.ARCtrl -> nameFromId t.FileName, Template.toJsonString 0 t - | Template t, JsonExport.ARCtrlCompressed -> nameFromId t.FileName, Template.toCompressedJsonString 0 t + | Template t, JsonExportFormat.ARCtrl -> nameFromId t.FileName, Template.toJsonString 0 t + | Template t, JsonExportFormat.ARCtrlCompressed -> nameFromId t.FileName, Template.toCompressedJsonString 0 t | Template _, anyElse -> failwithf "Error. It is not intended to parse Template to %s format." (string anyElse) Helper.downloadFromString (name , jsonString) diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index ca5143d5..8c37840d 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -157,7 +157,7 @@ type SidebarView = FilePicker.filePickerComponent model dispatch | Routing.Route.Protocol -> - Protocol.Core.fileUploadViewComponent model dispatch + Protocol.Templates.Main (model, dispatch) | Routing.Route.JsonExport -> JsonExporter.Core.FileExporter.Main(model, dispatch) diff --git a/src/Client/style.scss b/src/Client/style.scss index a9dc3f9f..a6cf5e76 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -208,14 +208,6 @@ table { height: fit-content; } -a:not([class]){ - color: $nfdi-blue-light -} - -a:not([class]):hover { - color: $nfdi-blue-lighter-20 -} - .dragover-footertab { position: relative } diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 4ab3a1c6..92193c66 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -8,11 +8,18 @@ open System.Collections.Generic [] module ARCtrlHelper = + [] + type ArcFilesDiscriminate = + | Assay + | Study + | Investigation + | Template + type ArcFiles = - | Template of Template - | Investigation of ArcInvestigation - | Study of ArcStudy * ArcAssay list - | Assay of ArcAssay + | Template of Template + | Investigation of ArcInvestigation + | Study of ArcStudy * ArcAssay list + | Assay of ArcAssay with member this.Tables() : ArcTables = @@ -22,6 +29,21 @@ module ARCtrlHelper = | Study (s,_) -> s | Assay a -> a + [] + type JsonExportFormat = + | ARCtrl + | ARCtrlCompressed + | ISA + | ROCrate + + static member fromString (str: string) = + match str.ToLower() with + | "arctrl" -> ARCtrl + | "arctrlcompressed" -> ARCtrlCompressed + | "isa" -> ISA + | "rocrate" -> ROCrate + | _ -> failwithf "Unknown JSON export format: %s" str + module Helper = diff --git a/src/Shared/JsonExport.fs b/src/Shared/JsonExport.fs deleted file mode 100644 index dc792a1e..00000000 --- a/src/Shared/JsonExport.fs +++ /dev/null @@ -1,15 +0,0 @@ -module Shared.JsonExport - -type JsonExportFormat = - | ARCtrl - | ARCtrlCompressed - | ISA - | ROCrate - - static member fromString (str: string) = - match str.ToLower() with - | "arctrl" -> ARCtrl - | "arctrlcompressed" -> ARCtrlCompressed - | "isa" -> ISA - | "rocrate" -> ROCrate - | _ -> failwithf "Unknown JSON export format: %s" str diff --git a/src/Shared/Shared.fsproj b/src/Shared/Shared.fsproj index 9cdbb1b7..25d70a89 100644 --- a/src/Shared/Shared.fsproj +++ b/src/Shared/Shared.fsproj @@ -4,7 +4,6 @@ net8.0 - From 180332ed8133065d5c6c25694b2b94e75bb0fc78 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Mon, 24 Jun 2024 16:08:14 +0200 Subject: [PATCH 126/135] More work on protocol import :construction: #413 --- src/Client/Modals/SelectiveImportModal.fs | 248 +++++++++++++++++- .../ProtocolTemplates/TemplateFromFile.fs | 23 +- 2 files changed, 268 insertions(+), 3 deletions(-) diff --git a/src/Client/Modals/SelectiveImportModal.fs b/src/Client/Modals/SelectiveImportModal.fs index 79741ab6..6ec66ccb 100644 --- a/src/Client/Modals/SelectiveImportModal.fs +++ b/src/Client/Modals/SelectiveImportModal.fs @@ -8,10 +8,243 @@ open Shared open ARCtrl +[] +type private ImportTable = { + Index: int + /// If FullImport is true, the table will be imported in full, otherwise it will be appended to active table. + FullImport: bool +} + +type private SelectiveImportModalState = { + ImportType: ARCtrl.TableJoinOptions + ImportMetadata: bool + ImportTables: ImportTable list +} with + static member init() = + { + ImportType = ARCtrl.TableJoinOptions.Headers + ImportMetadata = false + ImportTables = [] + } + +module private Helper = + + let submitWithMetadata (uploadedFile: ArcFiles) (state: SelectiveImportModalState) (dispatch: Messages.Msg -> unit) = + if not state.ImportMetadata then failwith "Metadata must be imported" + let createUpdatedTables (arcTables: ResizeArray) = + [ + for it in state.ImportTables do + let sourceTable = arcTables.[it.Index] + let appliedTable = ArcTable.init(sourceTable.Name) + appliedTable.Join(sourceTable, joinOptions=state.ImportType) + appliedTable + ] + |> ResizeArray + let arcFile = + match uploadedFile with + | Assay a as arcFile-> + let tables = createUpdatedTables a.Tables + a.Tables <- tables + arcFile + | Study (s,_) as arcFile -> + let tables = createUpdatedTables s.Tables + s.Tables <- tables + arcFile + | Template t as arcFile -> + let table = createUpdatedTables (ResizeArray[t.Table]) + t.Table <- table.[0] + arcFile + | Investigation _ as arcFile -> + arcFile + SpreadsheetInterface.UpdateArcFile arcFile |> InterfaceMsg |> dispatch + +open Helper + type SelectiveImportModal = + static member private ImportTypeRadio(importType: TableJoinOptions, setImportType: TableJoinOptions -> unit) = + let myradio(target: TableJoinOptions, txt: string) = + let isChecked = importType = target + Html.label [ + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name "importType" + prop.isChecked isChecked + prop.onChange (fun (b:bool) -> if b then setImportType target) + ] + Html.text txt + ] + ] + + Bulma.box [ + Bulma.field.div [ + Bulma.label [ + Html.i [prop.className "fa-solid fa-cog"] + Html.text (" Import Type") + ] + Bulma.control.div [ + prop.className "is-flex is-justify-content-space-between" + prop.children [ + myradio(ARCtrl.TableJoinOptions.Headers, " Only Column Headers") + myradio(ARCtrl.TableJoinOptions.WithValues, " With Values") + myradio(ARCtrl.TableJoinOptions.WithUnit, " With Units") + ] + ] + ] + ] + + static member private MetadataImport(isActive: bool, setActive: bool -> unit) = + Bulma.box [ + if isActive then color.hasBackgroundInfo + prop.children [ + Bulma.field.div [ + Bulma.label [ + Html.i [prop.className "fa-solid fa-lightbulb"] + Html.text (" Metadata") + ] + Bulma.control.div [ + Html.label [ + prop.className "checkbox is-unselectable" + prop.children [ + Html.input [ + prop.type'.checkbox + prop.onChange (fun (b:bool) -> setActive b) + ] + Html.text " Import" + ] + ] + ] + Html.span [ + color.hasTextWarning + prop.text "Importing metadata will overwrite the current file." + ] + ] + ] + ] + + [] + static member private TableImport(index: int, table: ArcTable, state: SelectiveImportModalState, addTableImport: int -> bool -> unit, rmvTableImport: int -> unit) = + let showData, setShowData = React.useState(false) + let name = table.Name + let radioGroup = "radioGroup_" + name + let import = state.ImportTables |> List.tryFind (fun it -> it.Index = index) + let isActive = import.IsSome + let disableAppend = state.ImportMetadata + Bulma.box [ + if isActive then color.hasBackgroundSuccess + prop.children [ + Bulma.field.div [ + Bulma.label [ + Html.i [prop.className "fa-solid fa-table"] + Html.span (" " + name) + Bulma.button.span [ + if showData then button.isActive + button.isSmall + prop.onClick (fun _ -> setShowData (not showData)) + prop.style [style.float'.right; style.cursor.pointer] + prop.children [ + Bulma.icon [ + icon.isSmall + prop.children [ + Html.i [ + prop.style [style.transitionProperty "transform"; style.transitionDuration (System.TimeSpan.FromSeconds 0.35)] + prop.className ["fa-solid"; "fa-angle-down"; if showData then "fa-rotate-180"] + ] + ] + ] + ] + ] + ] + Bulma.control.div [ + Html.label [ + let isInnerActive = isActive && import.Value.FullImport + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name radioGroup + prop.isChecked isInnerActive + prop.onChange (fun (b:bool) -> addTableImport index true) + ] + Html.text " Import" + ] + ] + Html.label [ + let isInnerActive = isActive && not import.Value.FullImport + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name radioGroup + if disableAppend then prop.disabled true + prop.isChecked isInnerActive + prop.onChange (fun (b:bool) -> addTableImport index false) + ] + Html.text " Append to active table" + ] + ] + Html.label [ + let isInnerActive = not isActive + prop.className "radio is-unselectable" + prop.children [ + Html.input [ + prop.type'.radio + prop.name radioGroup + prop.isChecked isInnerActive + prop.onChange (fun (b:bool) -> rmvTableImport index) + ] + Html.text " No Import" + ] + ] + ] + ] + if showData then + Bulma.field.div [ + Bulma.tableContainer [ + Bulma.table [ + Bulma.table.isBordered + prop.children [ + Html.thead [ + Html.tr [ + Html.th "Column" + for ri in 1 .. table.RowCount do + Html.th ri + ] + ] + Html.tbody [ + for c in table.Columns do + Html.tr [ + Html.th (c.Header.ToString()) + for cv in c.Cells do + Html.td (cv.ToString()) + ] + ] + ] + ] + ] + ] + ] + ] + [] static member Main (import: ArcFiles) (rmv: _ -> unit) = + let state, setState = React.useState(SelectiveImportModalState.init) + let tables = + match import with + | Assay a -> a.Tables + | Study (s,_) -> s.Tables + | Template t -> ResizeArray([t.Table]) + | _ -> ResizeArray() + let setMetadataImport = fun b -> + {state with ImportMetadata = b; ImportTables = state.ImportTables |> List.map (fun t -> {t with FullImport = true})} |> setState + let addTableImport = fun (i:int) (fullImport: bool) -> + let newImportTable: ImportTable = {Index = i; FullImport = fullImport} + let newImportTables = newImportTable::state.ImportTables |> List.distinct + {state with ImportTables = newImportTables} |> setState + let rmvTableImport = fun i -> + {state with ImportTables = state.ImportTables |> List.filter (fun it -> it.Index <> i)} |> setState Bulma.modal [ Bulma.modal.isActive prop.children [ @@ -23,8 +256,19 @@ type SelectiveImportModal = Bulma.modalCardTitle "Import" Bulma.delete [ prop.onClick rmv ] ] - Bulma.modalCardBody [ - Html.div "Placeholder1" + Bulma.modalCardBody [ + prop.className "p-5" + prop.children [ + Bulma.button.a [ + prop.onClick (fun _ -> log state) + prop.text "test" + ] + SelectiveImportModal.ImportTypeRadio(state.ImportType, fun it -> {state with ImportType = it} |> setState) + SelectiveImportModal.MetadataImport(state.ImportMetadata, setMetadataImport) + for ti in 0 .. (tables.Count-1) do + let t = tables.[ti] + SelectiveImportModal.TableImport(ti, t, state, addTableImport, rmvTableImport) + ] ] Bulma.modalCardFoot [ Bulma.button.button [ diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs index a0528c59..f26f9c63 100644 --- a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs @@ -16,6 +16,7 @@ open Elmish open Feliz open Feliz.Bulma open Shared +open ARCtrl type private TemplateFromFileState = { /// User select type to upload @@ -101,6 +102,25 @@ type TemplateFromFile = [] static member Main(model: Messages.Model, dispatch) = let state, setState = React.useState(TemplateFromFileState.init) + let af = React.useRef ( + let a = ArcAssay.init("My Assay") + let t1 = a.InitTable("Template Table 1") + t1.AddColumns([| + CompositeColumn.create(CompositeHeader.Input IOType.Source, [| for i in 1 .. 5 do sprintf "Source _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Output IOType.Sample, [| for i in 1 .. 5 do sprintf "Sample _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Component (OntologyAnnotation("instrument model", "MS", "MS:19283")), [| for i in 1 .. 5 do OntologyAnnotation("SCIEX instrument model", "MS", "MS:21387189237") |> CompositeCell.Term |]) + CompositeColumn.create(CompositeHeader.Factor (OntologyAnnotation("temperature", "UO", "UO:21387")), [| for i in 1 .. 5 do CompositeCell.createUnitized("", OntologyAnnotation("degree celcius", "UO", "UO:21387189237")) |]) + |]) + let t2 = a.InitTable("Template Table 2") + t2.AddColumns([| + CompositeColumn.create(CompositeHeader.Input IOType.Source, [| for i in 1 .. 5 do sprintf "Source2 _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Output IOType.Sample, [| for i in 1 .. 5 do sprintf "Sample2 _ %i" i |> CompositeCell.FreeText |]) + CompositeColumn.create(CompositeHeader.Component (OntologyAnnotation("instrument", "MS", "MS:19283")), [| for i in 1 .. 5 do OntologyAnnotation("SCIEX instrument model", "MS", "MS:21387189237") |> CompositeCell.Term |]) + CompositeColumn.create(CompositeHeader.Factor (OntologyAnnotation("temperature", "UO", "UO:21387")), [| for i in 1 .. 5 do CompositeCell.createUnitized("", OntologyAnnotation("degree celcius", "UO", "UO:21387189237")) |]) + |]) + let af = ArcFiles.Assay a + af + ) let setJsonFormat = fun x -> setState { state with JsonFormat = x } let setFileType = fun x -> setState { state with FileType = x } let fileTypeDisabled (ft: ArcFilesDiscriminate) = @@ -118,9 +138,10 @@ type TemplateFromFile = mainFunctionContainer [ // modal! match state.UploadedFile with - | Some af -> + | Some af -> Modals.SelectiveImportModal.Main af (fun _ -> TemplateFromFileState.init() |> setState) | None -> Html.none + Modals.SelectiveImportModal.Main af.current (fun _ -> TemplateFromFileState.init() |> setState) Bulma.field.div [ Bulma.help [ b [] [str "Import JSON files."] From 3903f6792759d62c865611bc755778136228a592 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 25 Jun 2024 15:56:04 +0200 Subject: [PATCH 127/135] Finish protocol import #413 :sparkles: --- src/Client/Client.fable-temp.csproj | 123 ++++++++++++++++++ src/Client/Modals/SelectiveImportModal.fs | 71 ++++++---- .../Pages/ProtocolTemplates/TemplateFromDB.fs | 35 +---- .../ProtocolTemplates/TemplateFromFile.fs | 4 +- src/Client/States/Spreadsheet.fs | 1 + src/Client/States/SpreadsheetInterface.fs | 4 +- src/Client/Update/InterfaceUpdate.fs | 10 ++ src/Client/Update/SpreadsheetUpdate.fs | 3 + src/Shared/ARCtrl.Helper.fs | 58 +++++++++ 9 files changed, 253 insertions(+), 56 deletions(-) create mode 100644 src/Client/Client.fable-temp.csproj diff --git a/src/Client/Client.fable-temp.csproj b/src/Client/Client.fable-temp.csproj new file mode 100644 index 00000000..9f7bb983 --- /dev/null +++ b/src/Client/Client.fable-temp.csproj @@ -0,0 +1,123 @@ + + + + + net8.0 + FABLE_COMPILER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Client/Modals/SelectiveImportModal.fs b/src/Client/Modals/SelectiveImportModal.fs index 6ec66ccb..40a2d01b 100644 --- a/src/Client/Modals/SelectiveImportModal.fs +++ b/src/Client/Modals/SelectiveImportModal.fs @@ -58,6 +58,32 @@ module private Helper = arcFile SpreadsheetInterface.UpdateArcFile arcFile |> InterfaceMsg |> dispatch + let submitTables (tables: ResizeArray) (importState: SelectiveImportModalState) (activeTable: ArcTable) (dispatch: Messages.Msg -> unit) = + if importState.ImportTables.Length = 0 then + () + else + let addMsgs = + importState.ImportTables + |> Seq.filter (fun x -> x.FullImport) + |> Seq.map (fun x -> tables.[x.Index]) + |> Seq.map (fun table -> + let nTable = ArcTable.init(table.Name) + nTable.Join(table, joinOptions=importState.ImportType) + nTable + ) + |> Seq.map (fun table -> SpreadsheetInterface.AddTable table |> InterfaceMsg) + let appendMsg = + let tables = importState.ImportTables |> Seq.filter (fun x -> not x.FullImport) |> Seq.map (fun x -> tables.[x.Index]) + /// Everything will be appended against this table, which in the end will be appended to the main table + let tempTable = ArcTable.init("ThisIsAPlaceholder") + for table in tables do + let preparedTemplate = Table.distinctByHeader tempTable table + tempTable.Join(preparedTemplate, joinOptions=importState.ImportType) + let appendTable = Table.distinctByHeader activeTable tempTable + SpreadsheetInterface.JoinTable (appendTable, None, Some importState.ImportType) |> InterfaceMsg + appendMsg |> dispatch + if Seq.length addMsgs = 0 then () else addMsgs |> Seq.iter dispatch + open Helper type SelectiveImportModal = @@ -87,22 +113,23 @@ type SelectiveImportModal = Bulma.control.div [ prop.className "is-flex is-justify-content-space-between" prop.children [ - myradio(ARCtrl.TableJoinOptions.Headers, " Only Column Headers") - myradio(ARCtrl.TableJoinOptions.WithValues, " With Values") - myradio(ARCtrl.TableJoinOptions.WithUnit, " With Units") + myradio(ARCtrl.TableJoinOptions.Headers, " Column Headers") + myradio(ARCtrl.TableJoinOptions.WithUnit, " ..With Units") + myradio(ARCtrl.TableJoinOptions.WithValues, " ..With Values") ] ] ] ] - static member private MetadataImport(isActive: bool, setActive: bool -> unit) = + static member private MetadataImport(isActive: bool, setActive: bool -> unit, disArcFile: ArcFilesDiscriminate) = + let name = string disArcFile Bulma.box [ if isActive then color.hasBackgroundInfo prop.children [ Bulma.field.div [ Bulma.label [ Html.i [prop.className "fa-solid fa-lightbulb"] - Html.text (" Metadata") + Html.textf " %s Metadata" name ] Bulma.control.div [ Html.label [ @@ -208,17 +235,16 @@ type SelectiveImportModal = prop.children [ Html.thead [ Html.tr [ - Html.th "Column" - for ri in 1 .. table.RowCount do - Html.th ri + for c in table.Headers do + Html.th (c.ToString()) ] ] Html.tbody [ - for c in table.Columns do + for ri in 0 .. (table.RowCount-1) do + let row = table.GetRow(ri, true) Html.tr [ - Html.th (c.Header.ToString()) - for cv in c.Cells do - Html.td (cv.ToString()) + for c in row do + Html.td (c.ToString()) ] ] ] @@ -229,14 +255,14 @@ type SelectiveImportModal = ] [] - static member Main (import: ArcFiles) (rmv: _ -> unit) = + static member Main (import: ArcFiles) (model: Spreadsheet.Model) dispatch (rmv: _ -> unit) = let state, setState = React.useState(SelectiveImportModalState.init) - let tables = + let tables, disArcfile = match import with - | Assay a -> a.Tables - | Study (s,_) -> s.Tables - | Template t -> ResizeArray([t.Table]) - | _ -> ResizeArray() + | Assay a -> a.Tables, ArcFilesDiscriminate.Assay + | Study (s,_) -> s.Tables, ArcFilesDiscriminate.Study + | Template t -> ResizeArray([t.Table]), ArcFilesDiscriminate.Template + | Investigation _ -> ResizeArray(), ArcFilesDiscriminate.Investigation let setMetadataImport = fun b -> {state with ImportMetadata = b; ImportTables = state.ImportTables |> List.map (fun t -> {t with FullImport = true})} |> setState let addTableImport = fun (i:int) (fullImport: bool) -> @@ -259,12 +285,8 @@ type SelectiveImportModal = Bulma.modalCardBody [ prop.className "p-5" prop.children [ - Bulma.button.a [ - prop.onClick (fun _ -> log state) - prop.text "test" - ] SelectiveImportModal.ImportTypeRadio(state.ImportType, fun it -> {state with ImportType = it} |> setState) - SelectiveImportModal.MetadataImport(state.ImportMetadata, setMetadataImport) + SelectiveImportModal.MetadataImport(state.ImportMetadata, setMetadataImport, disArcfile) for ti in 0 .. (tables.Count-1) do let t = tables.[ti] SelectiveImportModal.TableImport(ti, t, state, addTableImport, rmvTableImport) @@ -276,6 +298,9 @@ type SelectiveImportModal = prop.style [style.marginLeft length.auto] prop.text "Submit" prop.onClick(fun e -> + match state.ImportMetadata with + | true -> submitWithMetadata import state dispatch + | false -> submitTables tables state model.ActiveTable dispatch rmv e ) ] diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs index 9d39bb1c..b31c2299 100644 --- a/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs @@ -3,6 +3,7 @@ namespace Protocol open Feliz open Feliz.Bulma open Messages +open Shared type TemplateFromDB = @@ -31,37 +32,11 @@ type TemplateFromDB = prop.onClick (fun _ -> if model.ProtocolState.TemplateSelected.IsNone then failwith "No template selected!" - // Remove existing columns - let mutable columnsToRemove = [] - // find duplicate columns - let tablecopy = model.ProtocolState.TemplateSelected.Value.Table.Copy() - for header in model.SpreadsheetModel.ActiveTable.Headers do - let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) - if containsAtIndex.IsSome then - columnsToRemove <- containsAtIndex.Value::columnsToRemove - log columnsToRemove - tablecopy.RemoveColumns (Array.ofList columnsToRemove) - tablecopy.IteriColumns(fun i c0 -> - let c1 = {c0 with Cells = [||]} - let c2 = - if c1.Header.isInput then - match model.SpreadsheetModel.ActiveTable.TryGetInputColumn() with - | Some ic -> - {c1 with Cells = ic.Cells} - | _ -> c1 - elif c1.Header.isOutput then - match model.SpreadsheetModel.ActiveTable.TryGetOutputColumn() with - | Some oc -> - {c1 with Cells = oc.Cells} - | _ -> c1 - else - c1 - tablecopy.UpdateColumn(i, c2.Header, c2.Cells) - ) - log(tablecopy.RowCount, tablecopy.ColumnCount) + /// Filter out existing building blocks and keep input/output values. + let joinConfig = ARCtrl.TableJoinOptions.WithValues // If changed to anything else we need different logic to keep input/output values + let preparedTemplate = Table.selectiveTablePrepare model.SpreadsheetModel.ActiveTable model.ProtocolState.TemplateSelected.Value.Table let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel - let joinConfig = Some ARCtrl.TableJoinOptions.WithValues // If changed to anything else we need different logic to keep input/output values - SpreadsheetInterface.JoinTable (tablecopy, Some index, joinConfig ) |> InterfaceMsg |> dispatch + SpreadsheetInterface.JoinTable (preparedTemplate, Some index, Some joinConfig) |> InterfaceMsg |> dispatch ) prop.text "Add template" ] diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs index f26f9c63..1a36ea71 100644 --- a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs @@ -139,9 +139,9 @@ type TemplateFromFile = // modal! match state.UploadedFile with | Some af -> - Modals.SelectiveImportModal.Main af (fun _ -> TemplateFromFileState.init() |> setState) + Modals.SelectiveImportModal.Main af model.SpreadsheetModel dispatch (fun _ -> TemplateFromFileState.init() |> setState) | None -> Html.none - Modals.SelectiveImportModal.Main af.current (fun _ -> TemplateFromFileState.init() |> setState) + //Modals.SelectiveImportModal.Main af.current model.SpreadsheetModel dispatch (fun _ -> TemplateFromFileState.init() |> setState) Bulma.field.div [ Bulma.help [ b [] [str "Import JSON files."] diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 64a1832c..bb802a62 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -111,6 +111,7 @@ type Msg = | MoveColumn of current:int * next:int | UpdateActiveCell of (U2 * ColumnType) option | SetActiveCellFromSelected +| AddTable of ArcTable | RemoveTable of index:int | RenameTable of index:int * name:string | UpdateTableOrder of pre_index:int * new_index:int diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index c285c97b..bed6b720 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -10,9 +10,11 @@ type Msg = | Initialize of Swatehost | CreateAnnotationTable of tryUsePrevOutput:bool | RemoveBuildingBlock +| AddTable of ArcTable | AddAnnotationBlock of CompositeColumn | AddAnnotationBlocks of CompositeColumn [] -| JoinTable of ArcTable * index: int option * options: TableJoinOptions option +/// This function will do preprocessing on the table to join +| JoinTable of ArcTable * columnIndex: int option * options: TableJoinOptions option | UpdateArcFile of ArcFiles /// Open modal for selected building block, allows editing on standalone only. | EditBuildingBlock diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index a7b9bbdd..b2673b58 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -56,6 +56,16 @@ module Interface = let cmd = Spreadsheet.CreateAnnotationTable usePrevOutput |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" + | AddTable table -> + match host with + | Some Swatehost.Excel -> + //let cmd = OfficeInterop.AddTable table |> OfficeInteropMsg |> Cmd.ofMsg + failwith "AddTable not implemented for Excel" + model, Cmd.none + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + let cmd = Spreadsheet.AddTable table |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd + | _ -> failwith "not implemented" | AddAnnotationBlock minBuildingBlockInfo -> match host with //| Swatehost.Excel _ -> diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index 0e1a1339..c2a6480a 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -74,6 +74,9 @@ module Spreadsheet = match msg with | UpdateState nextState -> nextState, model, Cmd.none + | AddTable table -> + let nextState = Controller.addTable table state + nextState, model, Cmd.none | CreateAnnotationTable usePrevOutput -> let nextState = Controller.createTable usePrevOutput state nextState, model, Cmd.none diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 92193c66..d557c0ac 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -44,6 +44,64 @@ module ARCtrlHelper = | "rocrate" -> ROCrate | _ -> failwithf "Unknown JSON export format: %s" str +module Table = + + /// + /// This functions returns a **copy** of `toJoinTable` without any column already in `activeTable`. + /// + /// + /// + let distinctByHeader (activeTable: ArcTable) (toJoinTable: ArcTable) : ArcTable = + // Remove existing columns + let mutable columnsToRemove = [] + // find duplicate columns + let tablecopy = toJoinTable.Copy() + for header in activeTable.Headers do + let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) + if containsAtIndex.IsSome then + columnsToRemove <- containsAtIndex.Value::columnsToRemove + tablecopy.RemoveColumns (Array.ofList columnsToRemove) + tablecopy + + /// + /// This function is meant to prepare a table for joining with another table. + /// + /// It removes columns that are already present in the active table. + /// It removes all values from the new table. + /// It also fills new Input/Output columns with the input/output values of the active table. + /// + /// The output of this function can be used with the SpreadsheetInterface.JoinTable Message. + /// + /// The active/current table + /// The new table, which will be added to the existing one. + let selectiveTablePrepare (activeTable: ArcTable) (toJoinTable: ArcTable) : ArcTable = + // Remove existing columns + let mutable columnsToRemove = [] + // find duplicate columns + let tablecopy = toJoinTable.Copy() + for header in activeTable.Headers do + let containsAtIndex = tablecopy.Headers |> Seq.tryFindIndex (fun h -> h = header) + if containsAtIndex.IsSome then + columnsToRemove <- containsAtIndex.Value::columnsToRemove + tablecopy.RemoveColumns (Array.ofList columnsToRemove) + tablecopy.IteriColumns(fun i c0 -> + let c1 = {c0 with Cells = [||]} + let c2 = + if c1.Header.isInput then + match activeTable.TryGetInputColumn() with + | Some ic -> + {c1 with Cells = ic.Cells} + | _ -> c1 + elif c1.Header.isOutput then + match activeTable.TryGetOutputColumn() with + | Some oc -> + {c1 with Cells = oc.Cells} + | _ -> c1 + else + c1 + tablecopy.UpdateColumn(i, c2.Header, c2.Cells) + ) + tablecopy module Helper = From 3b3d4435ad88ad67b529e142a59357adce9d2094 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 25 Jun 2024 16:14:08 +0200 Subject: [PATCH 128/135] Fix unit cell update behavior #440 :bug: --- src/Client/MainComponents/Cells.fs | 31 +++++++++++++++++--------- src/Client/Update/SpreadsheetUpdate.fs | 4 ---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index d6fc589b..28dc6627 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -96,13 +96,7 @@ module private CellAux = |> Option.map header.UpdateWithOA |> Option.iter (fun nextHeader -> Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch) - let oasetter (index, cell: CompositeCell, dispatch) = fun (oa:OntologyAnnotation) -> - let nextCell = - if oa.TermSourceREF.IsNone && oa.TermAccessionNumber.IsNone then // update only mainfield, if mainfield is the only field with value - cell.UpdateMainField oa.NameText - else - cell.UpdateWithOA oa - Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch + let oasetter (index, nextCell: CompositeCell, dispatch) = Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch open CellComponents @@ -354,18 +348,35 @@ type Cell = let setter = fun (s: string) -> let nextCell = cell.UpdateMainField s Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - let oasetter = if cell.isTerm then CellAux.oasetter(index, cell, dispatch) |> Some else None + let oasetter = + if cell.isTerm then + fun (oa:OntologyAnnotation) -> + let nextCell = cell.UpdateWithOA oa + CellAux.oasetter(index, nextCell, dispatch) + |> Some + else + None Cell.BodyBase(Main, cellValue, setter, index, cell, model, dispatch, ?oasetter=oasetter) static member BodyUnit(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let cellValue = cell.GetContent().[1] let setter = fun (s: string) -> - let oa = cell.ToOA() + let oa = cell.ToOA() let newName = if s = "" then None else Some s oa.Name <- newName let nextCell = cell.UpdateWithOA oa Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - let oasetter = if cell.isUnitized then CellAux.oasetter(index, cell, dispatch) |> Some else None + let oasetter = + if cell.isUnitized then + log "IsUnitized" + fun (oa:OntologyAnnotation) -> + log ("oa", oa) + let nextCell = cell.UpdateWithOA oa + log ("nextCell", nextCell) + CellAux.oasetter(index, nextCell, dispatch) + |> Some + else + None Cell.BodyBase(Unit, cellValue, setter, index, cell, model, dispatch, ?oasetter=oasetter) static member BodyTSR(index: (int*int), cell: CompositeCell, model: Model, dispatch) = diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index c2a6480a..614bbd8c 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -104,12 +104,8 @@ module Spreadsheet = let cmd = Cmd.none state, model, cmd | UpdateCell (index, cell) -> - log ("[UPDATECELL]", index, cell) let nextState = state.ActiveTable.UpdateCellAt(fst index,snd index, cell) - log "START" - log state.ActiveTable - log "END" {state with ArcFile = state.ArcFile} nextState, model, Cmd.none | UpdateCells arr -> From 15921a9a907c0792a8088b36e058ebe3b686bd4e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 25 Jun 2024 21:38:55 +0200 Subject: [PATCH 129/135] Fix reference column state not reset when switching table #441 :bug: --- src/Client/MainComponents/SpreadsheetView.fs | 1 + src/Client/Views/SidebarView.fs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index 2ccd674d..1c98ad91 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -107,6 +107,7 @@ let Main (model:Model) (dispatch: Msg -> unit) = //React.useElementListener.on(ref, "keydown", (Spreadsheet.KeyboardShortcuts.onKeydownEvent dispatch)) /// This state is used to track which columns are expanded let state, setState : Set * (Set -> unit) = React.useState(Set.empty) + React.useEffect((fun _ -> setState Set.empty), [|box model.SpreadsheetModel.ActiveView|]) Html.div [ prop.id "SPREADSHEET_MAIN_VIEW" prop.tabIndex 0 diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index 8c37840d..a8822137 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -141,7 +141,7 @@ type SidebarView = str "Swate Release Version " a [Href "https://github.com/nfdi4plants/Swate/releases"; HTMLAttr.Target "_Blank"] [str model.PersistentStorageState.AppVersion] str " Host " - Html.span [prop.style [style.color "#4fb3d9"]; prop.text (sprintf "%O" model.PersistentStorageState.Host)] + Html.a [prop.style [style.cursor.defaultCursor] ;prop.text (sprintf "%O" model.PersistentStorageState.Host)] ] ] From 113c805069203a03fcf4a4f6ce4ed54999b540aa Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 25 Jun 2024 21:46:15 +0200 Subject: [PATCH 130/135] Adjust add building block behavior to user expectations #419 --- src/Client/Pages/BuildingBlock/SearchComponent.fs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index 4abd2a15..7e1a193e 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -134,7 +134,12 @@ let private addBuildingBlockButton (model: Model) dispatch = prop.disabled true Bulma.button.isFullWidth prop.onClick (fun _ -> - let column = CompositeColumn.create(header, [|if body.IsSome then body.Value|]) + let bodyCells = + if body.IsSome then // create as many body cells as there are rows in the active table + Array.init (model.SpreadsheetModel.ActiveTable.RowCount) (fun _ -> body.Value) + else + Array.empty + let column = CompositeColumn.create(header, bodyCells) let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch let id = $"Header_{index}_Main" From dbe34fdb115a655f575dc1844665a0074f9c0335 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Wed, 26 Jun 2024 23:11:38 +0200 Subject: [PATCH 131/135] configure excel add in for online development #442 :construction: --- build/manifest.xml | 12 ++++++------ src/Client/Update/InterfaceUpdate.fs | 1 + src/Client/index.html | 2 +- src/Client/vite.config.mts | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/build/manifest.xml b/build/manifest.xml index 969174f7..98a5568c 100644 --- a/build/manifest.xml +++ b/build/manifest.xml @@ -1,4 +1,4 @@ - + 5d6f5462-3401-48ec-9406-d12882e9ad83 1.0.0 @@ -16,7 +16,7 @@ - + ReadWriteDocument @@ -65,7 +65,7 @@ - + @@ -116,8 +116,8 @@ - - + + diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index b2673b58..bf654603 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -31,6 +31,7 @@ module Interface = let cmd = Cmd.batch [ Cmd.ofMsg (Ontologies.GetOntologies |> OntologyMsg) + log ("HOST",host) match host with | Swatehost.Excel -> Cmd.OfPromise.either diff --git a/src/Client/index.html b/src/Client/index.html index e31ef3ab..d4f2c0bb 100644 --- a/src/Client/index.html +++ b/src/Client/index.html @@ -4,7 +4,7 @@ Swate - + diff --git a/src/Client/vite.config.mts b/src/Client/vite.config.mts index 8afef635..fc6ff3f5 100644 --- a/src/Client/vite.config.mts +++ b/src/Client/vite.config.mts @@ -10,13 +10,13 @@ const proxyTarget = "http://localhost:" + proxyPort; export default defineConfig({ plugins: [ react(), - //basicSsl() + basicSsl() ], build: { outDir: "../../deploy/public", }, server: { - port: 8080, + port: 3000, proxy: { // redirect requests that start with /api/ to the server on port 5000 "/api/": { From 088138b72d8dfb2414b4354f3c889e482ab992a0 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 27 Jun 2024 15:51:07 +0200 Subject: [PATCH 132/135] Improve Form design #401 --- src/Client/MainComponents/Metadata/Forms.fs | 5 +++-- .../MainComponents/Metadata/Investigation.fs | 16 ++++++++-------- src/Client/MainComponents/Metadata/Study.fs | 16 ++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/Client/MainComponents/Metadata/Forms.fs b/src/Client/MainComponents/Metadata/Forms.fs index 82a909e7..10141165 100644 --- a/src/Client/MainComponents/Metadata/Forms.fs +++ b/src/Client/MainComponents/Metadata/Forms.fs @@ -709,7 +709,7 @@ type FormComponents = let element = React.useElementRef() React.useEffect((fun () -> setState input), dependencies=[|box input|]) Bulma.field.div [ - if label.IsSome then Bulma.label label.Value + //if label.IsSome then Bulma.label label.Value Bulma.field.div [ //prop.ref element prop.style [style.position.relative] @@ -721,7 +721,8 @@ type FormComponents = Bulma.field.div [ prop.style [style.flexGrow 1] prop.children [ - if showTextLabels then Bulma.label $"Term Name" + let label = defaultArg label "Term Name" + Bulma.label label let innersetter = fun (oaOpt: OntologyAnnotation option) -> if oaOpt.IsSome then diff --git a/src/Client/MainComponents/Metadata/Investigation.fs b/src/Client/MainComponents/Metadata/Investigation.fs index 4ae50a80..671c8265 100644 --- a/src/Client/MainComponents/Metadata/Investigation.fs +++ b/src/Client/MainComponents/Metadata/Investigation.fs @@ -1,4 +1,4 @@ -module MainComponents.Metadata.Investigation +module MainComponents.Metadata.Investigation open Feliz open Feliz.Bulma @@ -72,13 +72,13 @@ let Main(inv: ArcInvestigation, model: Messages.Model, dispatch: Msg -> unit) = inv.OntologySourceReferences <- ResizeArray oas inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) ) - FormComponents.TextInputs( - Array.ofSeq inv.RegisteredStudyIdentifiers, - "RegisteredStudyIdentifiers", - (fun i -> - inv.RegisteredStudyIdentifiers <- ResizeArray i - inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) - ) + //FormComponents.TextInputs( + // Array.ofSeq inv.RegisteredStudyIdentifiers, + // "RegisteredStudyIdentifiers", + // (fun i -> + // inv.RegisteredStudyIdentifiers <- ResizeArray i + // inv |> Investigation |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch) + //) FormComponents.CommentsInput( Array.ofSeq inv.Comments, "Comments", diff --git a/src/Client/MainComponents/Metadata/Study.fs b/src/Client/MainComponents/Metadata/Study.fs index 9d3956bd..cec36820 100644 --- a/src/Client/MainComponents/Metadata/Study.fs +++ b/src/Client/MainComponents/Metadata/Study.fs @@ -1,4 +1,4 @@ -module MainComponents.Metadata.Study +module MainComponents.Metadata.Study open Feliz open Feliz.Bulma @@ -63,13 +63,13 @@ let Main(study: ArcStudy, assignedAssays: ArcAssay list, model: Messages.Model, study.StudyDesignDescriptors <- ResizeArray(oas) (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) - FormComponents.TextInputs( - Array.ofSeq study.RegisteredAssayIdentifiers, - "Registered Assay Identifiers", - fun rais -> - study.RegisteredAssayIdentifiers <- ResizeArray(rais) - (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch - ) + //FormComponents.TextInputs( + // Array.ofSeq study.RegisteredAssayIdentifiers, + // "Registered Assay Identifiers", + // fun rais -> + // study.RegisteredAssayIdentifiers <- ResizeArray(rais) + // (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch + //) FormComponents.CommentsInput( Array.ofSeq study.Comments, "Comments", From c78b08ef10f9dfe5429624cdaf018b0f10024f89 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 27 Jun 2024 15:51:42 +0200 Subject: [PATCH 133/135] Load office-js cdn only in excel #442 --- src/Client/States/OfficeInteropState.fs | 1 + src/Client/Update/InterfaceUpdate.fs | 45 ++++++++++++++++--- src/Client/Update/OfficeInteropUpdate.fs | 56 ++++++++++++++---------- src/Client/index.html | 1 - 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/Client/States/OfficeInteropState.fs b/src/Client/States/OfficeInteropState.fs index c3c94b49..d828ce2d 100644 --- a/src/Client/States/OfficeInteropState.fs +++ b/src/Client/States/OfficeInteropState.fs @@ -29,6 +29,7 @@ type Msg = // create and update table element functions | CreateAnnotationTable of tryUsePrevOutput:bool | AnnotationtableCreated + | TryFindAnnotationTable | AnnotationTableExists of TryFindAnnoTableResult | InsertOntologyTerm of TermMinimal | AddAnnotationBlock of InsertBuildingBlock diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index bf654603..3caf4601 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -14,10 +14,44 @@ open Shared open Fable.Core.JsInterop open Shared.ARCtrlHelper -module private Helper = +/// This seems like such a hack :( +module private ExcelHelper = + + open Fable.Core open ExcelJS.Fable.GlobalBindings - let initializeAddIn () = Office.onReady() + let initializeAddIn () = Office.onReady().``then``(fun _ -> ()) |> Async.AwaitPromise + + /// Office-js will kill iframe loading in ARCitect, therefore we must load it conditionally + let addOfficeJsScript(callback: unit -> unit) = + let cdn = @"https://appsforoffice.microsoft.com/lib/1/hosted/office.js" + let _type = "text/javascript" + let s = Browser.Dom.document.createElement("script") + s?``type`` <- _type + s?src <- cdn + Browser.Dom.document.head.appendChild s |> ignore + s.onload <- fun _ -> callback() + () + + /// Make a function that loops short sleep sequences until a mutable variable is set to true + /// do mutabel dotnet ref for variable + let myAwaitLoadedThenInit(loaded: ref) = + let rec loop() = + async { + if loaded.Value then + do! initializeAddIn() + else + do! Async.Sleep 100 + do! loop() + } + loop() + + let officeload() = + let loaded = ref false + async { + addOfficeJsScript(fun _ -> loaded.Value <- true) + do! myAwaitLoadedThenInit loaded + } //open Fable.Core.JS @@ -31,13 +65,12 @@ module Interface = let cmd = Cmd.batch [ Cmd.ofMsg (Ontologies.GetOntologies |> OntologyMsg) - log ("HOST",host) match host with | Swatehost.Excel -> - Cmd.OfPromise.either - OfficeInterop.Core.tryFindActiveAnnotationTable + Cmd.OfAsync.either + ExcelHelper.officeload () - (OfficeInterop.AnnotationTableExists >> OfficeInteropMsg) + (fun _ -> TryFindAnnotationTable |> OfficeInteropMsg) (curry GenericError Cmd.none >> DevMsg) | Swatehost.Browser -> Cmd.none diff --git a/src/Client/Update/OfficeInteropUpdate.fs b/src/Client/Update/OfficeInteropUpdate.fs index 10bdcd1f..f6d0f093 100644 --- a/src/Client/Update/OfficeInteropUpdate.fs +++ b/src/Client/Update/OfficeInteropUpdate.fs @@ -9,9 +9,9 @@ open Shared open OfficeInteropTypes module OfficeInterop = - let update (currentModel:Messages.Model) (excelInteropMsg: OfficeInterop.Msg) : Messages.Model * Cmd = + let update (model:Messages.Model) (msg: OfficeInterop.Msg) : Messages.Model * Cmd = - match excelInteropMsg with + match msg with | AutoFitTable hidecols -> let p = fun () -> ExcelJS.Fable.GlobalBindings.Excel.run (OfficeInterop.Core.autoFitTable hidecols) @@ -21,18 +21,26 @@ module OfficeInterop = () (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd + | TryFindAnnotationTable -> + let cmd = + Cmd.OfPromise.either + OfficeInterop.Core.tryFindActiveAnnotationTable + () + (OfficeInterop.AnnotationTableExists >> OfficeInteropMsg) + (curry GenericError Cmd.none >> DevMsg) + model, cmd | AnnotationTableExists annoTableOpt -> let exists = match annoTableOpt with | Success name -> true | _ -> false let nextState = { - currentModel.ExcelState with + model.ExcelState with HasAnnotationTable = exists } - currentModel.updateByExcelState nextState,Cmd.none + model.updateByExcelState nextState,Cmd.none | InsertOntologyTerm (term) -> let cmd = @@ -41,7 +49,7 @@ module OfficeInterop = term (curry GenericLog Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | AddAnnotationBlock (minBuildingBlockInfo) -> let cmd = @@ -50,7 +58,7 @@ module OfficeInterop = (minBuildingBlockInfo) (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | AddAnnotationBlocks minBuildingBlockInfos -> let cmd = @@ -59,7 +67,7 @@ module OfficeInterop = minBuildingBlockInfos (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | ImportFile buildingBlockTables -> let nextCmd = @@ -68,7 +76,7 @@ module OfficeInterop = buildingBlockTables (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, nextCmd + model, nextCmd | RemoveBuildingBlock -> let cmd = @@ -77,7 +85,7 @@ module OfficeInterop = () (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | UpdateUnitForCells (unitTerm) -> let cmd = @@ -86,7 +94,7 @@ module OfficeInterop = unitTerm (curry GenericInteropLogs Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | CreateAnnotationTable(tryUsePrevOutput) -> let cmd = @@ -95,14 +103,14 @@ module OfficeInterop = (false,tryUsePrevOutput) (curry GenericInteropLogs (AnnotationtableCreated |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel,cmd + model,cmd | AnnotationtableCreated -> let nextState = { - currentModel.ExcelState with + model.ExcelState with HasAnnotationTable = true } - currentModel.updateByExcelState nextState, Cmd.none + model.updateByExcelState nextState, Cmd.none | GetParentTerm -> @@ -112,7 +120,7 @@ module OfficeInterop = () (fun tmin -> tmin |> Option.map (fun t -> ARCtrl.OntologyAnnotation.fromTerm t.toTerm) |> TermSearch.UpdateParentTerm |> TermSearchMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd // | FillHiddenColsRequest -> failwith "FillHiddenColsRequest Not implemented yet" @@ -133,11 +141,11 @@ module OfficeInterop = // (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) //let stateCmd = UpdateFillHiddenColsState FillHiddenColsState.ExcelCheckHiddenCols |> OfficeInteropMsg |> Cmd.ofMsg //let cmds = Cmd.batch [cmd; stateCmd] - currentModel, Cmd.none + model, Cmd.none | FillHiddenColumns (termsWithSearchResult) -> let nextState = { - currentModel.ExcelState with + model.ExcelState with FillHiddenColsStateStore = FillHiddenColsState.ExcelWriteFoundTerms } let cmd = @@ -146,15 +154,15 @@ module OfficeInterop = (termsWithSearchResult) (curry GenericInteropLogs (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) (curry GenericError (UpdateFillHiddenColsState FillHiddenColsState.Inactive |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) - currentModel.updateByExcelState nextState, cmd + model.updateByExcelState nextState, cmd | UpdateFillHiddenColsState newState -> let nextState = { - currentModel.ExcelState with + model.ExcelState with FillHiddenColsStateStore = newState } - currentModel.updateByExcelState nextState, Cmd.none + model.updateByExcelState nextState, Cmd.none // | InsertFileNames (fileNameList) -> let cmd = @@ -163,7 +171,7 @@ module OfficeInterop = (fileNameList) (curry GenericLog Cmd.none >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd // | GetSelectedBuildingBlockTerms -> @@ -179,7 +187,7 @@ module OfficeInterop = ] ) (curry GenericError (UpdateCurrentRequestState RequestBuildingBlockInfoStates.Inactive |> BuildingBlockDetails |> Cmd.ofMsg) >> DevMsg) - currentModel, cmd + model, cmd // DEV | TryExcel -> @@ -189,7 +197,7 @@ module OfficeInterop = () ((fun x -> curry GenericLog Cmd.none ("Debug",x)) >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd | TryExcel2 -> let cmd = Cmd.OfPromise.either @@ -197,7 +205,7 @@ module OfficeInterop = () ((fun x -> curry GenericLog Cmd.none ("Debug",x)) >> DevMsg) (curry GenericError Cmd.none >> DevMsg) - currentModel, cmd + model, cmd //| _ -> // printfn "Hit currently non existing message" // currentState, Cmd.none \ No newline at end of file diff --git a/src/Client/index.html b/src/Client/index.html index d4f2c0bb..db90cd02 100644 --- a/src/Client/index.html +++ b/src/Client/index.html @@ -4,7 +4,6 @@ Swate - From 15bc1e8e63da982e6e0ddf61330100c61d9dfa88 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Thu, 27 Jun 2024 16:00:42 +0200 Subject: [PATCH 134/135] Do setup for parent term support in forms #409 --- src/Client/MainComponents/Metadata/Forms.fs | 15 +++++++++------ src/Shared/StaticTermCollection.fs | 11 ++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Client/MainComponents/Metadata/Forms.fs b/src/Client/MainComponents/Metadata/Forms.fs index 10141165..90b2ae5d 100644 --- a/src/Client/MainComponents/Metadata/Forms.fs +++ b/src/Client/MainComponents/Metadata/Forms.fs @@ -85,7 +85,7 @@ module private API = |] let authorString = createAuthorString authors let title = json?title - let publication = Publication.create(pmid, doi, authorString, title, Term.Published) + let publication = Publication.create(pmid, doi, authorString, title, TermCollection.Published) return publication } @@ -122,7 +122,7 @@ module private API = |] let! pubmedId = requestByDOI_FromPubMed doi let authorString = createAuthorString authors - let publication = Publication.create(?pubMedID=pubmedId, doi=doi, authors=authorString, ?title=title, status=Term.Published) + let publication = Publication.create(?pubMedID=pubmedId, doi=doi, authors=authorString, ?title=title, status=TermCollection.Published) return publication } @@ -703,7 +703,7 @@ type FormComponents = ) [] - static member OntologyAnnotationInput (input: OntologyAnnotation, setter: OntologyAnnotation -> unit, ?label: string, ?showTextLabels: bool, ?removebutton: MouseEvent -> unit) = + static member OntologyAnnotationInput (input: OntologyAnnotation, setter: OntologyAnnotation -> unit, ?label: string, ?showTextLabels: bool, ?removebutton: MouseEvent -> unit, ?parent: OntologyAnnotation) = let showTextLabels = defaultArg showTextLabels true let state, setState = React.useState(input) let element = React.useElementRef() @@ -733,6 +733,7 @@ type FormComponents = input=state, fullwidth=true, ?portalTermSelectArea=element.current, + ?parent=parent, debounceSetter=1000 ) ] @@ -776,10 +777,10 @@ type FormComponents = ] [] - static member OntologyAnnotationsInput (oas: OntologyAnnotation [], label: string, setter: OntologyAnnotation [] -> unit, ?showTextLabels: bool) = + static member OntologyAnnotationsInput (oas: OntologyAnnotation [], label: string, setter: OntologyAnnotation [] -> unit, ?showTextLabels: bool, ?parent: OntologyAnnotation) = FormComponents.InputSequence( oas, (OntologyAnnotation.empty()), label, setter, - (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,c,label=b,removebutton=d,?showTextLabels=showTextLabels)) + (fun (a,b,c,d) -> FormComponents.OntologyAnnotationInput(a,c,label=b,removebutton=d,?showTextLabels=showTextLabels, ?parent=parent)) ) [] @@ -879,6 +880,7 @@ type FormComponents = state |> setter ), showTextLabels = false + //parent=Shared.TermCollection.PersonRoleWithinExperiment ) if deletebutton.IsSome then Helper.deleteButton deletebutton.Value @@ -1041,7 +1043,8 @@ type FormComponents = state.Status <- if s = (OntologyAnnotation.empty()) then None else Some s state |> setter ), - "Status" + "Status", + parent=Shared.TermCollection.PublicationStatus ) FormComponents.CommentsInput( Array.ofSeq state.Comments, diff --git a/src/Shared/StaticTermCollection.fs b/src/Shared/StaticTermCollection.fs index e0e97d1b..eed1a05b 100644 --- a/src/Shared/StaticTermCollection.fs +++ b/src/Shared/StaticTermCollection.fs @@ -1,4 +1,4 @@ -module Term +module Shared.TermCollection open ARCtrl @@ -7,3 +7,12 @@ open ARCtrl /// let Published = OntologyAnnotation("published","EFO","EFO:0001796") +/// +/// https://github.com/nfdi4plants/Swate/issues/409#issuecomment-2176134201 +/// +let PublicationStatus = OntologyAnnotation("publication status","EFO","EFO:0001742") + +/// +/// https://github.com/nfdi4plants/Swate/issues/409#issuecomment-2176134201 +/// +let PersonRoleWithinExperiment = OntologyAnnotation("person role within the experiment","AGRO","AGRO:00000378") \ No newline at end of file From 4368e6ddb49380c3d349116770a3531c3f44a012 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 28 Jun 2024 15:37:49 +0200 Subject: [PATCH 135/135] Release v1.0.0-beta.04 :bookmark: --- package-lock.json | 786 +++++++++++++++++++--- paket.dependencies | 6 + src/Server/Properties/launchSettings.json | 43 +- src/Server/Server.fs | 6 +- src/Server/Version.fs | 8 +- tests/Server/JsonExport.Tests.fs | 1 - tests/Server/JsonImport.Tests.fs | 1 - 7 files changed, 697 insertions(+), 154 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce14329b..315784ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "Swate", "dependencies": { "@creativebulma/bulma-tooltip": "^1.2.0", "@nfdi4plants/exceljs": "^0.3.0", @@ -400,10 +399,362 @@ "resolved": "https://registry.npmjs.org/@creativebulma/bulma-tooltip/-/bulma-tooltip-1.2.0.tgz", "integrity": "sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -582,6 +933,201 @@ "node": ">=14" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", @@ -643,9 +1189,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -809,6 +1355,12 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -1061,9 +1613,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001632", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", - "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", + "version": "1.0.30001638", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", + "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", "dev": true, "funding": [ { @@ -1163,9 +1715,9 @@ } }, "node_modules/component-emitter": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz", - "integrity": "sha512-U8EviusIm8Fc5vMbs9opNX8r/hAz8PFYOu003AR1OVkCnDSTaBHB8inMn97yIbkGlI+dcdsItTBjgiZkVVzxYg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==", "dev": true }, "node_modules/compress-commons": { @@ -1259,9 +1811,9 @@ } }, "node_modules/cytoscape": { - "version": "3.29.2", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.29.2.tgz", - "integrity": "sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ==", + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.0.tgz", + "integrity": "sha512-l590mjTHT6/Cbxp13dGPC2Y7VXdgc+rUeF8AnF/JPzhjNevbDJfObnJgaSjlldOgBQZbue+X6IUZ7r5GAgvauQ==", "engines": { "node": ">=0.10" } @@ -1342,9 +1894,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.799", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz", - "integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q==", + "version": "1.4.812", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", + "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==", "dev": true }, "node_modules/emoji-regex": { @@ -1362,9 +1914,9 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -1374,29 +1926,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -1467,9 +2019,9 @@ } }, "node_modules/foreground-child": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.0.tgz", - "integrity": "sha512-CrWQNaEl1/6WeZoarcM9LHupTo3RpZO2Pdk1vktwzPiQTsJnAKJmm3TACKeG5UZbWDfaH2AbvYxzP96y0MT7fA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -1505,6 +2057,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -1671,12 +2237,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2184,6 +2753,12 @@ "wrappy": "1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -2551,15 +3126,15 @@ } }, "node_modules/remotedev": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.7.tgz", - "integrity": "sha512-QomJ4+1A82zZGidaQ4ecRDMAeMT2CxvTvGzzw+OOsP+IfrvF3Pu8SCRezVksjH1WuajmJSzKvOKNRoF1MXFNrA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/remotedev/-/remotedev-0.2.9.tgz", + "integrity": "sha512-W8dHOv9BcFnetFEd08yNb5O9Hd+zkTFFnf9FRjNCkb4u+JgQ/U152Aw4q83AmY3m34d6KZwhK5ip/Qc331+4vA==", "dev": true, "dependencies": { "jsan": "^3.1.3", "querystring": "^0.2.0", "rn-host-detect": "^1.0.1", - "socketcluster-client": "^5.0.0" + "socketcluster-client": "^13.0.0" } }, "node_modules/resolve": { @@ -2685,9 +3260,9 @@ ] }, "node_modules/sass": { - "version": "1.77.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", - "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -2713,28 +3288,18 @@ } }, "node_modules/sc-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.0.6.tgz", - "integrity": "sha512-vXhuJ4GZeOulBjLrKpbVhxyBz4YSqgRdc9m1jaR1byZfwyexarb7xCSe5/A0V42XGjCJ3/FR7wa8UEBtL9xOxg==", - "dev": true, - "dependencies": { - "sc-emitter": "1.x.x" - } - }, - "node_modules/sc-emitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/sc-emitter/-/sc-emitter-1.1.0.tgz", - "integrity": "sha512-f8YiHF/LkRiyZ1iIrzwIkec1VfcNrKBTEJ8w26s/5TEJXcH024Y1V6u1CRl9OeQp8E0zLu+7u56rjWSaH3yePQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz", + "integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==", "dev": true, "dependencies": { - "component-emitter": "1.2.0" + "component-emitter": "1.2.1" } }, "node_modules/sc-errors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.3.3.tgz", - "integrity": "sha512-zJQxMxsQ4N5hnXND4VUwwUOJxANqidCRw7vygFe52+XVrYWERqkVlOhivBS2vt18eWVxUQrgxJXMA0x9Yuzn8A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-1.4.1.tgz", + "integrity": "sha512-dBn92iIonpChTxYLgKkIT/PCApvmYT6EPIbRvbQKTgY6tbEbIy8XVUv4pGyKwEK4nCmvX4TKXcN0iXC6tNW6rQ==", "dev": true }, "node_modules/sc-formatter": { @@ -2812,20 +3377,21 @@ } }, "node_modules/socketcluster-client": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-5.5.2.tgz", - "integrity": "sha512-ivgbsUvTOIvEvba2IrQvhn8xUJoKg0t6OpwIKPXh64zRLpnLxDZ2EZXpjdc8okGHjUArXWs+5MVK6BbQvnNHlw==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-13.0.1.tgz", + "integrity": "sha512-hxiE2xz6mgaBlhXbtBa4POgWVEvIcjCoHzf5LTUVhI9IL8V2ltV3Ze8pQsi9egqTjSz4RHPfyrJ7BiETe5Kthw==", "dev": true, "dependencies": { "base-64": "0.1.0", "clone": "2.1.1", + "component-emitter": "1.2.1", "linked-list": "0.1.0", "querystring": "0.2.0", - "sc-channel": "~1.0.6", - "sc-emitter": "~1.1.0", - "sc-errors": "~1.3.0", - "sc-formatter": "~3.0.0", - "ws": "3.0.0" + "sc-channel": "^1.2.0", + "sc-errors": "^1.4.0", + "sc-formatter": "^3.0.1", + "uuid": "3.2.1", + "ws": "5.1.1" } }, "node_modules/socketcluster-client/node_modules/querystring": { @@ -2838,6 +3404,16 @@ "node": ">=0.4.x" } }, + "node_modules/socketcluster-client/node_modules/uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -2983,15 +3559,16 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { @@ -3005,9 +3582,9 @@ } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -3176,12 +3753,6 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, - "node_modules/ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -3284,12 +3855,12 @@ } }, "node_modules/vite": { - "version": "5.2.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", - "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", + "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", + "esbuild": "^0.21.3", "postcss": "^8.4.38", "rollup": "^4.13.0" }, @@ -3499,21 +4070,14 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.0.0.tgz", - "integrity": "sha512-sjCOvLIEgRVT+inhGpm/f/YeusxCEg5BENrIj31YcOR+GTLcqIJ029uTmLVFNDJBCBvCxhkWFZrR6iMppq/s2A==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.1.1.tgz", + "integrity": "sha512-bOusvpCb09TOBLbpMKszd45WKC2KPtxiyiHanv+H2DE3Az+1db5a/L7sVJZVDPUC1Br8f0SKRr1KjLpD1U/IAw==", "dev": true, "dependencies": { - "safe-buffer": "~5.0.1", - "ultron": "~1.1.0" + "async-limiter": "~1.0.0" } }, - "node_modules/ws/node_modules/safe-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha512-cr7dZWLwOeaFBLTIuZeYdkfO7UzGIKhjYENJFAxUOMKWGaWDm2nJM2rzxNRm5Owu0DH3ApwNo6kx5idXZfb/Iw==", - "dev": true - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/paket.dependencies b/paket.dependencies index 992566a2..5b76225c 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -3,6 +3,12 @@ framework: net8.0 storage: none nuget ARCtrl 2.0.0-alpha.7.swate alpha +nuget ARCtrl.Contract 2.0.0-alpha.7.swate alpha +nuget ARCtrl.Core 2.0.0-alpha.7.swate alpha +nuget ARCtrl.CWL 2.0.0-alpha.7.swate alpha +nuget ARCtrl.FileSystem 2.0.0-alpha.7.swate alpha +nuget ARCtrl.Json 2.0.0-alpha.7.swate alpha +nuget ARCtrl.Spreadsheet 2.0.0-alpha.7.swate alpha nuget Fable.Fetch 2.7.0 nuget Feliz.Bulma.Checkradio nuget Feliz.Bulma.Switch diff --git a/src/Server/Properties/launchSettings.json b/src/Server/Properties/launchSettings.json index bcea70db..c957d04c 100644 --- a/src/Server/Properties/launchSettings.json +++ b/src/Server/Properties/launchSettings.json @@ -1,37 +1,12 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iis": { - "applicationUrl": "https://localhost/Server", - "sslPort": 0 - }, - "iisExpress": { - "applicationUrl": "https://localhost/Swate/", - "sslPort": 0 + "profiles": { + "Server": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + } } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS": { - "commandName": "IIS", - "launchUrl": "https://localhost/Swate", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Server": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "NEO4J_TEST_ENV": "MYOUTPUT" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } - } } \ No newline at end of file diff --git a/src/Server/Server.fs b/src/Server/Server.fs index 10d479b5..d697b1d0 100644 --- a/src/Server/Server.fs +++ b/src/Server/Server.fs @@ -372,10 +372,10 @@ let config (app:IApplicationBuilder) = ) let app = application { - //url "http://localhost:5000" //"http://localhost:5000/" - //app_config config + url "http://*:5000" + app_config config use_router topLevelRouter - //use_cors "CORS_CONFIG" cors_config + use_cors "CORS_CONFIG" cors_config memory_cache use_static "public" use_gzip diff --git a/src/Server/Version.fs b/src/Server/Version.fs index 118754be..4bd2e3df 100644 --- a/src/Server/Version.fs +++ b/src/Server/Version.fs @@ -4,12 +4,12 @@ open System.Reflection [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "Swate" let [] AssemblyVersion = "1.0.0" - let [] AssemblyMetadata_Version = "v1.0.0-beta.03" - let [] AssemblyMetadata_ReleaseDate = "12.03.2024" + let [] AssemblyMetadata_Version = "v1.0.0-beta.04" + let [] AssemblyMetadata_ReleaseDate = "28.06.2024" diff --git a/tests/Server/JsonExport.Tests.fs b/tests/Server/JsonExport.Tests.fs index 704743e3..2795d75f 100644 --- a/tests/Server/JsonExport.Tests.fs +++ b/tests/Server/JsonExport.Tests.fs @@ -4,7 +4,6 @@ open Expecto open Shared open Server -open ISADotNet open JsonImport open System.IO diff --git a/tests/Server/JsonImport.Tests.fs b/tests/Server/JsonImport.Tests.fs index 4deb61bc..c5b057ab 100644 --- a/tests/Server/JsonImport.Tests.fs +++ b/tests/Server/JsonImport.Tests.fs @@ -4,7 +4,6 @@ open Expecto open Shared open Server -open ISADotNet let x = 2 //open JsonImport