From 50ceea1d5f2e7d16354cf662f57a5e1caabb4906 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Mon, 1 Jul 2024 13:30:28 +0200 Subject: [PATCH 01/25] Rmv files :fire: --- src/Server/Server.fsproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Server/Server.fsproj b/src/Server/Server.fsproj index c5564071..3634721b 100644 --- a/src/Server/Server.fsproj +++ b/src/Server/Server.fsproj @@ -21,8 +21,5 @@ - - - \ No newline at end of file From a06318ff8ed5b382786581c2b6de3d93b19a184e Mon Sep 17 00:00:00 2001 From: Kevin F Date: Mon, 1 Jul 2024 15:32:10 +0200 Subject: [PATCH 02/25] fix duplicate type definition --- .../Pages/ProtocolTemplates/ProtocolSearch.fs | 92 ++++++++++--------- src/Client/Views/SidebarView.fs | 2 +- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs index d4d5478b..062fd3b6 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs @@ -1,4 +1,4 @@ -module Protocol.Search +namespace Protocol open Fable.React open Fable.React.Props @@ -7,52 +7,56 @@ open Messages open Feliz open Feliz.Bulma -let private breadcrumbEle (model:Model) dispatch = - Bulma.breadcrumb [ - Bulma.breadcrumb.hasArrowSeparator - prop.children [ - Html.ul [ - Html.li [Html.a [ - prop.onClick (fun _ -> UpdatePageState (Some Routing.Route.Protocol) |> dispatch) - prop.text (Routing.Route.Protocol.toStringRdbl) - ]] - Html.li [ - prop.className "is-active" - prop.children (Html.a [ +module private Helper = + + let breadcrumbEle (model:Model) dispatch = + Bulma.breadcrumb [ + Bulma.breadcrumb.hasArrowSeparator + prop.children [ + Html.ul [ + Html.li [Html.a [ prop.onClick (fun _ -> UpdatePageState (Some Routing.Route.Protocol) |> dispatch) - prop.text (Routing.Route.ProtocolSearch.toStringRdbl) - ]) - ] + prop.text (Routing.Route.Protocol.toStringRdbl) + ]] + Html.li [ + prop.className "is-active" + prop.children (Html.a [ + prop.onClick (fun _ -> UpdatePageState (Some Routing.Route.Protocol) |> dispatch) + prop.text (Routing.Route.ProtocolSearch.toStringRdbl) + ]) + ] + ] ] ] - ] open Fable.Core -[] -let Main (model:Model) dispatch = - let templates, setTemplates = React.useState(model.ProtocolState.Templates) - let config, setConfig = React.useState(TemplateFilterConfig.init) - let filteredTemplates = Protocol.Search.filterTemplates (templates, config) - 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()) - // https://keycode.info/ - OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) - ] [ - breadcrumbEle 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."] - - Bulma.label "Search the database for protocol templates." - - mainFunctionContainer [ - Protocol.Search.InfoField() - Protocol.Search.FileSortElement(model, config, setConfig) - Protocol.Search.Component (filteredTemplates, model, dispatch) - ] - ] \ No newline at end of file +type SearchContainer = + + [] + static member Main (model:Model) dispatch = + let templates, setTemplates = React.useState(model.ProtocolState.Templates) + let config, setConfig = React.useState(TemplateFilterConfig.init) + let filteredTemplates = Protocol.Search.filterTemplates (templates, config) + 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()) + // https://keycode.info/ + OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + ] [ + Helper.breadcrumbEle 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."] + + Bulma.label "Search the database for protocol templates." + + mainFunctionContainer [ + Protocol.Search.InfoField() + Protocol.Search.FileSortElement(model, config, setConfig) + Protocol.Search.Component (filteredTemplates, model, dispatch) + ] + ] \ No newline at end of file diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index a8822137..a6ae70d2 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -163,7 +163,7 @@ type SidebarView = JsonExporter.Core.FileExporter.Main(model, dispatch) | Routing.Route.ProtocolSearch -> - Protocol.Search.Main model dispatch + Protocol.SearchContainer.Main model dispatch | Routing.Route.ActivityLog -> ActivityLog.activityLogComponent model dispatch From b85f6d65cb9dc830aaa290081006a3513701045f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Mon, 1 Jul 2024 17:00:08 +0200 Subject: [PATCH 03/25] Burn old code, paket and old dependencies :fire: --- .editorconfig | 6 +- paket.dependencies | 46 --- paket.lock | 301 ------------------ src/Client/Client.fsproj | 245 +++++++------- src/Client/Init.fs | 1 - src/Client/Messages.fs | 1 - .../Modals/BuildingBlockDetailsModal.fs | 87 ++--- src/Client/Modals/Controller.fs | 6 +- src/Client/Model.fs | 39 ++- src/Client/OfficeInterop/HelperFunctions.fs | 78 +++-- src/Client/OfficeInterop/OfficeInterop.fs | 2 - .../Pages/BuildingBlock/BuildingBlockView.fs | 21 +- src/Client/Pages/FilePicker/FilePickerView.fs | 15 +- src/Client/Pages/Info/InfoView.fs | 197 ++++++------ src/Client/Pages/JsonExporter/JsonExporter.fs | 16 +- .../Pages/ProtocolTemplates/ProtocolSearch.fs | 27 +- .../Pages/ProtocolTemplates/ProtocolView.fs | 25 +- .../ProtocolTemplates/TemplateFromFile.fs | 4 +- src/Client/Pages/Settings/SettingsView.fs | 4 +- src/Client/Pages/TermSearch/TermSearchView.fs | 26 +- src/Client/SharedComponents/AdvancedSearch.fs | 56 ++-- .../AnnotationTableMissingWarning.fs | 4 +- src/Client/SidebarComponents/LayoutHelper.fs | 17 +- src/Client/SidebarComponents/Navbar.fs | 98 +++--- src/Client/SidebarComponents/ResponsiveFA.fs | 69 ++-- src/Client/Update.fs | 2 - src/Client/Views/NotFoundView.fs | 6 +- src/Client/Views/SidebarView.fs | 40 +-- src/Client/Views/SplitWindowView.fs | 2 +- src/Client/paket.references | 23 -- src/Server/Server.fsproj | 48 +-- src/Server/paket.references | 6 - src/Shared/Shared.fsproj | 11 +- src/Shared/paket.references | 2 - tests/Client/Client.Tests.fsproj | 1 - tests/Client/paket.references | 1 - tests/Server/Server.Tests.fsproj | 1 - tests/Server/paket.references | 1 - tests/Shared/Shared.Tests.fsproj | 5 +- tests/Shared/paket.references | 2 - 40 files changed, 589 insertions(+), 953 deletions(-) delete mode 100644 paket.dependencies delete mode 100644 paket.lock delete mode 100644 src/Client/paket.references delete mode 100644 src/Server/paket.references delete mode 100644 src/Shared/paket.references delete mode 100644 tests/Client/paket.references delete mode 100644 tests/Server/paket.references delete mode 100644 tests/Shared/paket.references diff --git a/.editorconfig b/.editorconfig index 30fa4c7b..7e93e1da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,4 +9,8 @@ insert_final_newline = false [*.fs] fsharp_multiline_bracket_style = stroustrup -fsharp_newline_before_multiline_computation_expression = false \ No newline at end of file +fsharp_newline_before_multiline_computation_expression = false + +[*.fsproj] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/paket.dependencies b/paket.dependencies deleted file mode 100644 index 5b76225c..00000000 --- a/paket.dependencies +++ /dev/null @@ -1,46 +0,0 @@ -source https://api.nuget.org/v3/index.json -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 -nuget Fsharp.Core ~> 8 -nuget Fable.Remoting.Giraffe ~> 5 -nuget FsSpreadsheet.Js ~> 6.1.3 -nuget Saturn ~> 0 - -nuget Fable.Core ~> 4 -nuget Fable.Elmish ~> 4 -nuget Fable.Elmish.React ~> 4 -nuget Fable.Elmish.Debugger ~> 4 -nuget Fable.Elmish.HMR ~> 7 -nuget Fable.Remoting.Client ~> 7 -nuget Feliz.CompilerPlugins ~> 2 -nuget Feliz.Bulma -nuget Feliz.UseElmish -nuget Feliz ~> 2 -nuget Fable.Elmish.Browser ~> 4 -nuget Fable.Browser.MediaQueryList -nuget Fable.React ~> 9 - -nuget Feliz.ElmishComponents -nuget ExcelJS.Fable -nuget Neo4j.Driver ~> 5 -nuget Microsoft.Extensions.Configuration.UserSecrets -nuget Microsoft.AspNetCore.Authentication.JwtBearer ~> 6 -nuget System.Text.Encodings.Web - -nuget Fable.Mocha ~> 2 -nuget Expecto ~> 9 -nuget Thoth.Elmish.Debouncer - -nuget Fable.SimpleJson -nuget Fable.SimpleXml \ No newline at end of file diff --git a/paket.lock b/paket.lock deleted file mode 100644 index 1ceec7ed..00000000 --- a/paket.lock +++ /dev/null @@ -1,301 +0,0 @@ -STORAGE: NONE -RESTRICTION: == net8.0 -NUGET - remote: https://api.nuget.org/v3/index.json - 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.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.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.7.swate) - FSharp.Core (>= 6.0.7) - ARCtrl.FileSystem (2.0.0-alpha.7.swate) - Fable.Core (>= 4.2) - FSharp.Core (>= 6.0.7) - 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.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) - Fable.Core (>= 3.2.8) - Fable.React (>= 7.4.1) - FSharp.Core (>= 5.0) - Expecto (9.0.4) - FSharp.Core (>= 4.6) - Mono.Cecil (>= 0.11.3) - Fable.AST (4.5) - Fable.Browser.Blob (1.4) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Browser.Dom (2.16) - Fable.Browser.Blob (>= 1.3) - Fable.Browser.Event (>= 1.5) - Fable.Browser.WebStorage (>= 1.2) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Browser.Event (1.6) - Fable.Browser.Gamepad (>= 1.3) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Browser.Gamepad (1.3) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Browser.MediaQueryList (1.5) - Fable.Browser.Dom (>= 2.16) - Fable.Browser.Event (>= 1.6) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Browser.WebStorage (1.3) - Fable.Browser.Event (>= 1.6) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Browser.XMLHttpRequest (1.4) - Fable.Browser.Blob (>= 1.4) - Fable.Browser.Event (>= 1.6) - Fable.Core (>= 3.2.8) - FSharp.Core (>= 4.7.2) - Fable.Core (4.3) - Fable.Elmish (4.1) - Fable.Core (>= 3.7.1) - FSharp.Core (>= 4.7.2) - Fable.Elmish.Browser (4.0.3) - Fable.Browser.Dom (>= 2.14) - Fable.Elmish (>= 4.1) - Fable.Elmish.UrlParser (>= 1.0.2) - FSharp.Core (>= 4.7.2) - Fable.Elmish.Debugger (4.0) - Fable.Elmish (>= 4.0) - FSharp.Core (>= 6.0.7) - Thoth.Json (>= 6.0) - Fable.Elmish.HMR (7.0) - Fable.Core (>= 3.7.1) - Fable.Elmish.React (>= 4.0) - FSharp.Core (>= 4.7.2) - Fable.Elmish.React (4.0) - Fable.Elmish (>= 4.0) - Fable.ReactDom.Types (>= 18.0) - FSharp.Core (>= 4.7.2) - Fable.Elmish.UrlParser (1.0.2) - FSharp.Core (>= 4.7.2) - Fable.Exceljs (1.6) - Fable.Core (>= 4.0) - FSharp.Core (>= 6.0.7) - Fable.Fetch (2.7) - Fable.Browser.Blob (>= 1.2) - Fable.Browser.Event (>= 1.5) - Fable.Core (>= 3.7.1) - Fable.Promise (>= 2.2.2) - FSharp.Core (>= 4.7.2) - Fable.Mocha (2.17) - Fable.Core (>= 3.0) - FSharp.Core (>= 4.7) - Fable.Parsimmon (4.1) - Fable.Core (>= 3.0) - FSharp.Core (>= 4.6.2) - Fable.Promise (3.2) - Fable.Core (>= 3.7.1) - FSharp.Core (>= 4.7.2) - Fable.React (9.4) - Fable.React.Types (>= 18.3) - Fable.ReactDom.Types (>= 18.2) - FSharp.Core (>= 4.7.2) - Fable.React.Types (18.3) - Fable.Browser.Dom (>= 2.4.4) - Fable.Core (>= 3.2.7) - FSharp.Core (>= 4.7.2) - Fable.ReactDom.Types (18.2) - Fable.React.Types (>= 18.3) - FSharp.Core (>= 4.7.2) - Fable.Remoting.Client (7.32) - Fable.Browser.XMLHttpRequest (>= 1.0) - Fable.Core (>= 3.1.5) - Fable.Remoting.MsgPack (>= 1.24) - Fable.SimpleJson (>= 3.24) - FSharp.Core (>= 4.7.2) - Fable.Remoting.Giraffe (5.19) - Fable.Remoting.Server (>= 5.37) - FSharp.Core (>= 6.0) - Giraffe (>= 5.0) - Microsoft.IO.RecyclableMemoryStream (>= 3.0 < 4.0) - Fable.Remoting.Json (2.23) - FSharp.Core (>= 6.0) - Newtonsoft.Json (>= 12.0.2) - Fable.Remoting.MsgPack (1.24) - FSharp.Core (>= 4.7.2) - Fable.Remoting.Server (5.37) - Fable.Remoting.Json (>= 2.23) - Fable.Remoting.MsgPack (>= 1.24) - FSharp.Core (>= 6.0) - Microsoft.IO.RecyclableMemoryStream (>= 3.0 < 4.0) - Fable.SimpleHttp (3.6) - Fable.Browser.Dom (>= 1.0) - Fable.Browser.XMLHttpRequest (>= 1.1) - Fable.Core (>= 3.0) - FSharp.Core (>= 4.6.2) - Fable.SimpleJson (3.24) - Fable.Core (>= 3.1.5) - Fable.Parsimmon (>= 4.0) - FSharp.Core (>= 4.7) - Fable.SimpleXml (3.4) - Fable.Core (>= 3.0) - Fable.Parsimmon (>= 4.1) - FSharp.Core (>= 4.6.2) - Feliz (2.7) - Fable.ReactDom.Types (>= 18.2) - Feliz.CompilerPlugins (>= 2.2) - FSharp.Core (>= 4.7.2) - Feliz.Bulma (3.0) - Feliz (>= 2.4) - FSharp.Core (>= 4.7.2) - Feliz.Bulma.Checkradio (3.0) - Feliz (>= 2.4) - Feliz.Bulma (>= 3.0) - FSharp.Core (>= 4.7.2) - Feliz.Bulma.Switch (3.0) - Feliz (>= 2.4) - Feliz.Bulma (>= 3.0) - FSharp.Core (>= 4.7.2) - Feliz.CompilerPlugins (2.2) - Fable.AST (>= 4.2.1) - FSharp.Core (>= 4.7.2) - Feliz.ElmishComponents (1.2) - Feliz.UseElmish (>= 1.2.1) - FSharp.Core (>= 4.7) - Feliz.UseElmish (2.5) - Fable.Elmish (>= 4.0) - FSharp.Core (>= 4.7.2) - FSharp.Control.Websockets (0.3) - FSharp.Core (>= 6.0) - Microsoft.IO.RecyclableMemoryStream (>= 3.0) - FSharp.Core (8.0.300) - FsSpreadsheet (6.1.3) - FSharp.Core (>= 6.0.7) - Thoth.Json.Core (>= 0.2.1) - FsSpreadsheet.Js (6.1.3) - Fable.Exceljs (>= 1.6) - Fable.Promise (>= 3.2) - FSharp.Core (>= 6.0.7) - FsSpreadsheet (>= 6.1.3) - Thoth.Json.JavaScript (>= 0.1) - Giraffe (6.4) - FSharp.Core (>= 6.0) - Giraffe.ViewEngine (>= 1.4) - Microsoft.IO.RecyclableMemoryStream (>= 3.0) - Newtonsoft.Json (>= 13.0.3) - System.Text.Json (>= 8.0.3) - Giraffe.ViewEngine (1.4) - FSharp.Core (>= 5.0) - Microsoft.AspNetCore.Authentication.JwtBearer (6.0.31) - Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 6.35) - Microsoft.Bcl.AsyncInterfaces (8.0) - Microsoft.CSharp (4.7) - Microsoft.Extensions.Configuration (8.0) - Microsoft.Extensions.Configuration.Abstractions (>= 8.0) - Microsoft.Extensions.Primitives (>= 8.0) - Microsoft.Extensions.Configuration.Abstractions (8.0) - Microsoft.Extensions.Primitives (>= 8.0) - Microsoft.Extensions.Configuration.FileExtensions (8.0) - Microsoft.Extensions.Configuration (>= 8.0) - Microsoft.Extensions.Configuration.Abstractions (>= 8.0) - Microsoft.Extensions.FileProviders.Abstractions (>= 8.0) - Microsoft.Extensions.FileProviders.Physical (>= 8.0) - Microsoft.Extensions.Primitives (>= 8.0) - Microsoft.Extensions.Configuration.Json (8.0) - Microsoft.Extensions.Configuration (>= 8.0) - Microsoft.Extensions.Configuration.Abstractions (>= 8.0) - Microsoft.Extensions.Configuration.FileExtensions (>= 8.0) - Microsoft.Extensions.FileProviders.Abstractions (>= 8.0) - System.Text.Json (>= 8.0) - Microsoft.Extensions.Configuration.UserSecrets (8.0) - Microsoft.Extensions.Configuration.Abstractions (>= 8.0) - Microsoft.Extensions.Configuration.Json (>= 8.0) - Microsoft.Extensions.FileProviders.Abstractions (>= 8.0) - Microsoft.Extensions.FileProviders.Physical (>= 8.0) - Microsoft.Extensions.FileProviders.Abstractions (8.0) - Microsoft.Extensions.Primitives (>= 8.0) - Microsoft.Extensions.FileProviders.Physical (8.0) - Microsoft.Extensions.FileProviders.Abstractions (>= 8.0) - Microsoft.Extensions.FileSystemGlobbing (>= 8.0) - Microsoft.Extensions.Primitives (>= 8.0) - Microsoft.Extensions.FileSystemGlobbing (8.0) - Microsoft.Extensions.Primitives (8.0) - Microsoft.IdentityModel.Abstractions (7.6) - Microsoft.IdentityModel.JsonWebTokens (7.6) - Microsoft.IdentityModel.Tokens (>= 7.6) - Microsoft.IdentityModel.Logging (7.6) - Microsoft.IdentityModel.Abstractions (>= 7.6) - Microsoft.IdentityModel.Protocols (7.6) - Microsoft.IdentityModel.Tokens (>= 7.6) - Microsoft.IdentityModel.Protocols.OpenIdConnect (7.6) - Microsoft.IdentityModel.Protocols (>= 7.6) - System.IdentityModel.Tokens.Jwt (>= 7.6) - Microsoft.IdentityModel.Tokens (7.6) - Microsoft.IdentityModel.Logging (>= 7.6) - Microsoft.IO.RecyclableMemoryStream (3.0.1) - Mono.Cecil (0.11.5) - Namotion.Reflection (3.1.1) - Microsoft.CSharp (>= 4.3) - Neo4j.Driver (5.21) - Microsoft.Bcl.AsyncInterfaces (>= 5.0) - System.IO.Pipelines (>= 7.0) - System.ValueTuple (>= 4.5) - Newtonsoft.Json (13.0.3) - NJsonSchema (11.0) - Namotion.Reflection (>= 3.1.1) - Newtonsoft.Json (>= 13.0.3) - NJsonSchema.Annotations (>= 11.0) - NJsonSchema.Annotations (11.0) - Saturn (0.17) - FSharp.Control.Websockets (>= 0.2.2) - Giraffe (>= 6.4) - Microsoft.AspNetCore.Authentication.JwtBearer (>= 6.0.3) - System.IdentityModel.Tokens.Jwt (7.6) - Microsoft.IdentityModel.JsonWebTokens (>= 7.6) - Microsoft.IdentityModel.Tokens (>= 7.6) - System.IO.Pipelines (8.0) - System.Text.Encodings.Web (8.0) - System.Text.Json (8.0.3) - System.Text.Encodings.Web (>= 8.0) - System.ValueTuple (4.5) - Thoth.Elmish.Debouncer (2.1) - Fable.Core (>= 4.0) - Fable.Elmish (>= 4.0.1) - Fable.Promise (>= 3.2) - Fable.React (>= 9.3) - FSharp.Core (>= 7.0.200) - Thoth.Json (10.2) - Fable.Core (>= 3.6.2) - FSharp.Core (>= 4.7.2) - Thoth.Json.Core (0.2.1) - Fable.Core (>= 4.1) - FSharp.Core (>= 5.0) - Thoth.Json.JavaScript (0.1) - Fable.Core (>= 4.1) - FSharp.Core (>= 5.0) - Thoth.Json.Core (>= 0.1) - Thoth.Json.Newtonsoft (0.1) - Fable.Core (>= 4.1) - FSharp.Core (>= 5.0) - Newtonsoft.Json (>= 13.0.1) - Thoth.Json.Core (>= 0.1) diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index f874d7cb..53c44494 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -1,119 +1,132 @@ - - net8.0 - FABLE_COMPILER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + net8.0 + FABLE_COMPILER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Client/Init.fs b/src/Client/Init.fs index a55faf06..6b8e1162 100644 --- a/src/Client/Init.fs +++ b/src/Client/Init.fs @@ -6,7 +6,6 @@ open LocalHistory open Model open Messages open Update -open Thoth.Elmish let initializeModel () = let dt = LocalStorage.Darkmode.DataTheme.GET() diff --git a/src/Client/Messages.fs b/src/Client/Messages.fs index 1e43e1bd..f3c4653e 100644 --- a/src/Client/Messages.fs +++ b/src/Client/Messages.fs @@ -2,7 +2,6 @@ module rec Messages open Elmish -open Thoth.Elmish open Shared open Fable.Remoting.Client open Fable.SimpleJson diff --git a/src/Client/Modals/BuildingBlockDetailsModal.fs b/src/Client/Modals/BuildingBlockDetailsModal.fs index 5922074e..bad9a6cf 100644 --- a/src/Client/Modals/BuildingBlockDetailsModal.fs +++ b/src/Client/Modals/BuildingBlockDetailsModal.fs @@ -52,15 +52,16 @@ let private rowIndicesToReadable (rowIndices:int []) = let private infoIcon (txt:string) = if txt = "" then - str "No defintion found" + Html.text "No defintion found" else - span [ - Style [Color NFDIColors.Yellow.Base; (*OverflowY OverflowOptions.Visible*)] - Class ("has-tooltip-right has-tooltip-multiline") - Props.Custom ("data-tooltip", txt) - ] [ - Bulma.icon [ - Html.i [prop.className "fa-solid fa-circle-info"] + Html.span [ + prop.style [style.color NFDIColors.Yellow.Base] + prop.className "has-tooltip-right has-tooltip-multiline" + prop.custom("data-tooltip", txt) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-circle-info"] + ] ] ] @@ -72,31 +73,31 @@ let private searchResultTermToTableHeaderElement (term:TermSearchable option) = match term with | Some isEmpty when isEmpty.Term.Name = "" && isEmpty.Term.TermAccession = "" -> Html.tr [ - th [] [str "-"] - th [] [str "-"] - th [Style [TextAlign TextAlignOptions.Center]] [str "-"] - th [] [str (rowIndicesToReadable isEmpty.RowIndices)] + Html.th "-" + Html.th "-" + Html.th [prop.style [style.textAlign.center]; prop.text "-"] + Html.th (rowIndicesToReadable isEmpty.RowIndices) ] | Some hasResult when hasResult.SearchResultTerm.IsSome -> Html.tr [ - th [] [str hasResult.SearchResultTerm.Value.Name] - th [ Style [TextAlign TextAlignOptions.Center] ] [infoIcon hasResult.SearchResultTerm.Value.Description] - th [] [str hasResult.SearchResultTerm.Value.Accession] - th [] [str (rowIndicesToReadable hasResult.RowIndices)] + Html.th hasResult.SearchResultTerm.Value.Name + Html.th [prop.style [style.textAlign.center]; prop.children [infoIcon hasResult.SearchResultTerm.Value.Description]] + Html.th hasResult.SearchResultTerm.Value.Accession + Html.th (rowIndicesToReadable hasResult.RowIndices) ] | Some hasNoResult when hasNoResult.SearchResultTerm.IsNone -> Html.tr [ - th [ Style [Color NFDIColors.Red.Lighter20] ] [str hasNoResult.Term.Name] - th [ Style [TextAlign TextAlignOptions.Center] ] [infoIcon userSpecificTermMsg] - th [] [str hasNoResult.Term.TermAccession] - th [] [str (rowIndicesToReadable hasNoResult.RowIndices)] + Html.th [prop.style [style.color NFDIColors.Red.Lighter20]; prop.text hasNoResult.Term.Name] + Html.th [prop.style [style.textAlign.center]; prop.children [infoIcon userSpecificTermMsg]] + Html.th hasNoResult.Term.TermAccession + Html.th (rowIndicesToReadable hasNoResult.RowIndices) ] | None -> Html.tr [ - th [] [str "-"] - th [] [str "-"] - th [] [str "-"] - th [] [str "Header"] + Html.th "-" + Html.th "-" + Html.th "-" + Html.th "Header" ] | anythingElse -> failwith $"""Swate encountered an error when trying to parse {anythingElse} to search results.""" @@ -106,24 +107,24 @@ let private searchResultTermToTableElement (term:TermSearchable) = match term with | isEmpty when term.Term.Name = "" && term.Term.TermAccession = "" -> Html.tr [ - td [] [str "-"] - td [ Style [TextAlign TextAlignOptions.Center] ] [str "-"] - td [] [str "-"] - td [] [str (rowIndicesToReadable isEmpty.RowIndices)] + Html.td "-" + Html.td [prop.style [style.textAlign.center]; prop.text "-"] + Html.td "-" + Html.td (rowIndicesToReadable isEmpty.RowIndices) ] | hasResult when term.SearchResultTerm.IsSome -> Html.tr [ - td [] [str hasResult.SearchResultTerm.Value.Name] - td [ Style [TextAlign TextAlignOptions.Center] ] [infoIcon hasResult.SearchResultTerm.Value.Description] - td [] [str hasResult.SearchResultTerm.Value.Accession] - td [] [str (rowIndicesToReadable hasResult.RowIndices)] + Html.td hasResult.SearchResultTerm.Value.Name + Html.td [prop.style [style.textAlign.center]; prop.children [infoIcon hasResult.SearchResultTerm.Value.Description]] + Html.td hasResult.SearchResultTerm.Value.Accession + Html.td (rowIndicesToReadable hasResult.RowIndices) ] | hasNoResult when term.SearchResultTerm.IsNone -> Html.tr [ - td [ Style [Color NFDIColors.Red.Lighter20] ] [str hasNoResult.Term.Name] - td [ Style [TextAlign TextAlignOptions.Center] ] [infoIcon userSpecificTermMsg] - td [] [str hasNoResult.Term.TermAccession] - td [] [str (rowIndicesToReadable hasNoResult.RowIndices)] + Html.td [prop.style [style.color NFDIColors.Red.Lighter20]; prop.text hasNoResult.Term.Name] + Html.td [prop.style [style.textAlign.center]; prop.children [infoIcon userSpecificTermMsg]] + Html.td hasNoResult.Term.TermAccession + Html.td (rowIndicesToReadable hasNoResult.RowIndices) ] | anythingElse -> failwith $"""Swate encountered an error when trying to parse {anythingElse} to search results.""" @@ -135,18 +136,18 @@ let private tableElement (terms:TermSearchable []) = Bulma.table.isFullWidth Bulma.table.isStriped prop.children [ - thead [] [ + Html.thead [ Html.tr [ - th [Class "toExcelColor"] [str "Name"] - th [Class "toExcelColor"; Style [TextAlign TextAlignOptions.Center] ] [str "Desc."] - th [Class "toExcelColor"] [str "TAN"] - th [Class "toExcelColor"] [str "Row"] + Html.th [prop.className "toExcelColor"; prop.text "Name"] + Html.th [prop.className "toExcelColor"; prop.style [ style.textAlign.center]; prop.text "Desc."] + Html.th [prop.className "toExcelColor"; prop.text "TAN"] + Html.th [prop.className "toExcelColor"; prop.text "Row"] ] ] - thead [] [ + Html.thead [ searchResultTermToTableHeaderElement rowHeader ] - tbody [] [ + Html.tbody [ for term in bodyRows do yield searchResultTermToTableElement term diff --git a/src/Client/Modals/Controller.fs b/src/Client/Modals/Controller.fs index bc85dc49..957698c1 100644 --- a/src/Client/Modals/Controller.fs +++ b/src/Client/Modals/Controller.fs @@ -1,8 +1,6 @@ module Modals.Controller -open Fable.React -open Fable.React.Props - +open Feliz [] let private ModalContainerId_inner = "modal_inner_" @@ -17,7 +15,7 @@ let removeModal(name:string) = ///Function to add a modal to the html body of the active document. If an object with the same name exists, it is removed first. ///The name of the modal, this is used for generate an Id for the modal by which it is later identified. ///The modal itself with a open parameter which will be the correct remove function for the modal. -let renderModal(name: string, reactElement: (_ -> unit) -> Fable.React.ReactElement) = +let renderModal(name: string, reactElement: (_ -> unit) -> ReactElement) = let parent = Browser.Dom.document.getElementById("modal-container") let id = createId name /// check if existing and if so remove diff --git a/src/Client/Model.fs b/src/Client/Model.fs index cdb6e93f..bd9c9cfc 100644 --- a/src/Client/Model.fs +++ b/src/Client/Model.fs @@ -4,7 +4,7 @@ open Fable.React open Fable.React.Props open Shared open TermTypes -open Thoth.Elmish +open Feliz open Routing type WindowSize = @@ -52,30 +52,35 @@ type LogItem = | InteropLogging.Error -> Error(System.DateTime.UtcNow,msg.MessageTxt) | InteropLogging.Warning -> Warning(System.DateTime.UtcNow,msg.MessageTxt) + static member private DebugCell = Html.td [prop.style [style.color NFDIColors.LightBlue.Base; style.fontWeight.bold]; prop.text "Debug"] + static member private InfoCell = Html.td [prop.style [style.color NFDIColors.Mint.Base; style.fontWeight.bold]; prop.text "Info"] + static member private ErrorCell = Html.td [prop.style [style.color NFDIColors.Red.Base; style.fontWeight.bold]; prop.text "ERROR"] + static member private WarningCell = Html.td [prop.style [style.color NFDIColors.Yellow.Base; style.fontWeight.bold]; prop.text "Warning"] + static member toTableRow = function | Debug (t,m) -> - tr [] [ - td [] [str (sprintf "[%s]" (t.ToShortTimeString()))] - td [Style [Color NFDIColors.LightBlue.Base; FontWeight "bold"]] [str "Debug"] - td [] [str m] + Html.tr [ + Html.td (sprintf "[%s]" (t.ToShortTimeString())) + LogItem.DebugCell + Html.td m ] | Info (t,m) -> - tr [] [ - td [] [str (sprintf "[%s]" (t.ToShortTimeString()))] - td [Style [Color NFDIColors.Mint.Base; FontWeight "bold"]] [str "Info"] - td [] [str m] + Html.tr [ + Html.td (sprintf "[%s]" (t.ToShortTimeString())) + LogItem.InfoCell + Html.td m ] | Error (t,m) -> - tr [] [ - td [] [str (sprintf "[%s]" (t.ToShortTimeString()))] - td [Style [Color NFDIColors.Red.Base; FontWeight "bold"]] [str "ERROR"] - td [] [str m] + Html.tr [ + Html.td (sprintf "[%s]" (t.ToShortTimeString())) + LogItem.ErrorCell + Html.td m ] | Warning (t,m) -> - tr [] [ - td [] [str (sprintf "[%s]" (t.ToShortTimeString()))] - td [Style [Color NFDIColors.Yellow.Base; FontWeight "bold"]] [str "Warning"] - td [] [str m] + Html.tr [ + Html.td (sprintf "[%s]" (t.ToShortTimeString())) + LogItem.WarningCell + Html.td m ] static member ofStringNow (level:string) (message: string) = diff --git a/src/Client/OfficeInterop/HelperFunctions.fs b/src/Client/OfficeInterop/HelperFunctions.fs index 9efffc7e..90ba4e21 100644 --- a/src/Client/OfficeInterop/HelperFunctions.fs +++ b/src/Client/OfficeInterop/HelperFunctions.fs @@ -320,47 +320,45 @@ let findIndexNextNotHiddenCol (headerVals:obj option []) (startIndex:float) = // ) open System -open Fable.SimpleXml -open Fable.SimpleXml.Generator - -let getCustomXml (customXmlParts:CustomXmlPartCollection) (context:RequestContext) = - promise { - let! getXml = - context.sync().``then``(fun e -> - let items = customXmlParts.items - let xmls = items |> Seq.map (fun x -> x.getXml() ) - - xmls |> Array.ofSeq - ) - - let! xml = - context.sync().``then``(fun e -> - - //let nOfItems = customXmlParts.items.Count - let vals = getXml |> Array.map (fun x -> x.value) - //sprintf "N = %A; items: %A" nOfItems vals - let xml = vals |> String.concat Environment.NewLine - xml - ) - - let xmlParsed = - let isRootElement = xml |> SimpleXml.tryParseElement - if xml = "" then - "" |> SimpleXml.parseElement - elif isRootElement.IsSome then - isRootElement.Value - else - let isManyRootElements = xml |> SimpleXml.tryParseManyElements - if isManyRootElements.IsSome then - isManyRootElements.Value - |> List.tryFind (fun ele -> ele.Name = "customXml") - |> fun customXmlOpt -> if customXmlOpt.IsSome then customXmlOpt.Value else failwith "Swate could not find expected '..' root tag." - else - failwith "Swate could not parse Workbook Custom Xml Parts. Had neither one root nor many root elements. Please contact the developer." - if xmlParsed.Name <> "customXml" then failwith (sprintf "Swate found unexpected root xml element: %s" xmlParsed.Name) - return xmlParsed - } +//let getCustomXml (customXmlParts:CustomXmlPartCollection) (context:RequestContext) = +// promise { +// let! getXml = +// context.sync().``then``(fun e -> +// let items = customXmlParts.items +// let xmls = items |> Seq.map (fun x -> x.getXml() ) + +// xmls |> Array.ofSeq +// ) + +// let! xml = +// context.sync().``then``(fun e -> + +// //let nOfItems = customXmlParts.items.Count +// let vals = getXml |> Array.map (fun x -> x.value) +// //sprintf "N = %A; items: %A" nOfItems vals +// let xml = vals |> String.concat Environment.NewLine +// xml +// ) + +// let xmlParsed = +// let isRootElement = xml |> SimpleXml.tryParseElement +// if xml = "" then +// "" |> SimpleXml.parseElement +// elif isRootElement.IsSome then +// isRootElement.Value +// else +// let isManyRootElements = xml |> SimpleXml.tryParseManyElements +// if isManyRootElements.IsSome then +// isManyRootElements.Value +// |> List.tryFind (fun ele -> ele.Name = "customXml") +// |> fun customXmlOpt -> if customXmlOpt.IsSome then customXmlOpt.Value else failwith "Swate could not find expected '..' root tag." +// else +// failwith "Swate could not parse Workbook Custom Xml Parts. Had neither one root nor many root elements. Please contact the developer." +// if xmlParsed.Name <> "customXml" then failwith (sprintf "Swate found unexpected root xml element: %s" xmlParsed.Name) + +// return xmlParsed +// } //let getAllSwateTableValidation (xmlParsed:XmlElement) = // let protocolGroups = SimpleXml.findElementsByName Xml.ValidationTypes.ValidationXmlRoot xmlParsed diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index 3738f6f6..99a2ad41 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -76,8 +76,6 @@ let exampleExcelFunction2 () = tableNames ) - let! xmlParsed = getCustomXml customXmlParts context - //let tableValidations = getAllSwateTableValidation xmlParsed return (sprintf "%A" allTables) diff --git a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs index 2f47098f..3641e03c 100644 --- a/src/Client/Pages/BuildingBlock/BuildingBlockView.fs +++ b/src/Client/Pages/BuildingBlock/BuildingBlockView.fs @@ -12,7 +12,6 @@ open Shared open TermTypes open CustomComponents -open OfficeInteropTypes open Elmish let update (addBuildingBlockMsg:BuildingBlock.Msg) (state: BuildingBlock.Model) : BuildingBlock.Model * Cmd = @@ -87,17 +86,17 @@ open Feliz.Bulma let addBuildingBlockComponent (model:Model) (dispatch:Messages.Msg -> unit) = - div [ - OnSubmit (fun e -> e.preventDefault()) - // https://keycode.info/ - OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) - ] [ - pageHeader "Building Blocks" + Html.div [ + prop.onSubmit (fun e -> e.preventDefault()) + prop.onKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + prop.children [ + pageHeader "Building Blocks" - // Input forms, etc related to add building block. - Bulma.label "Add annotation building blocks (columns) to the annotation table." - mainFunctionContainer [ - SearchComponent.Main model dispatch + // Input forms, etc related to add building block. + Bulma.label "Add annotation building blocks (columns) to the annotation table." + mainFunctionContainer [ + SearchComponent.Main model dispatch + ] ] //match model.PersistentStorageState.Host with diff --git a/src/Client/Pages/FilePicker/FilePickerView.fs b/src/Client/Pages/FilePicker/FilePickerView.fs index 4906fd69..a49c7d1d 100644 --- a/src/Client/Pages/FilePicker/FilePickerView.fs +++ b/src/Client/Pages/FilePicker/FilePickerView.fs @@ -1,10 +1,5 @@ module FilePicker -open Fable.React -open Fable.React.Props -open Fable.Core.JsInterop -open Thoth.Json -open Thoth.Elmish open ExcelColors open Api open Model @@ -233,13 +228,13 @@ module FileNameTable = Bulma.table.isStriped Bulma.table.isHoverable prop.children [ - tbody [] [ + Html.tbody [ for index,fileName in model.FilePickerState.FileNames do Html.tr [ - td [] [b [] [str $"{index}"]] - td [] [str fileName] - td [] [moveButtonList (index,fileName) model dispatch] - td [Style [TextAlign TextAlignOptions.Right]] [deleteFromTable (index,fileName) model dispatch] + Html.td [Html.b $"{index}"] + Html.td fileName + Html.td [moveButtonList (index,fileName) model dispatch] + Html.td [prop.style [style.textAlign.right]; prop.children [deleteFromTable (index,fileName) model dispatch]] ] ] ] diff --git a/src/Client/Pages/Info/InfoView.fs b/src/Client/Pages/Info/InfoView.fs index d9e93ad6..508389b5 100644 --- a/src/Client/Pages/Info/InfoView.fs +++ b/src/Client/Pages/Info/InfoView.fs @@ -6,9 +6,6 @@ open ExcelColors open Model open Messages open Browser -open Browser.MediaQueryList -open Browser.MediaQueryListExtensions - open CustomComponents open Fable @@ -16,113 +13,117 @@ open Fable.Core open Feliz open Feliz.Bulma -let introductionElement model dispatch = - p [Style [TextAlign TextAlignOptions.Justify]] [ - b [] [str "Swate"] - str " is a " - b [] [str "S"] - str "wate " - b [] [str "W"] - str "orkflow " - b [] [str "A"] - str "nnotation " - b [] [str "T"] - str "ool for " - b [] [str "E"] - str "xcel. This tool provides an easy way to annotate experimental data in an excel application that every wet lab scientist is familiar with. If you are interested check out the full " - a [Href Shared.URLs.SwateWiki; Target "_blank"] [str "documentation"] - str " 📚." - ] +//let introductionElement model dispatch = +// p [Style [TextAlign TextAlignOptions.Justify]] [ +// b [] [str "Swate"] +// str " is a " +// b [] [str "S"] +// str "wate " +// b [] [str "W"] +// str "orkflow " +// b [] [str "A"] +// str "nnotation " +// b [] [str "T"] +// str "ool for " +// b [] [str "E"] +// str "xcel. This tool provides an easy way to annotate experimental data in an excel application that every wet lab scientist is familiar with. If you are interested check out the full " +// a [Href Shared.URLs.SwateWiki; Target "_blank"] [str "documentation"] +// str " 📚." +// ] -let iconContainer (left: ReactElement list) (icon) = - Bulma.field.div [ - prop.className "is-flex" - prop.children [ - Html.div [ - prop.style [style.marginRight(length.rem 2)] - prop.children left - ] - icon - ] - ] +//let iconContainer (left: ReactElement list) (icon) = +// Bulma.field.div [ +// prop.className "is-flex" +// prop.children [ +// Html.div [ +// prop.style [style.marginRight(length.rem 2)] +// prop.children left +// ] +// icon +// ] +// ] -let getInContactElement (model:Model) dispatch = - Bulma.content [ - prop.style [style.textAlign.justify] - prop.children [ - Bulma.label "Get In Contact With Us" +//let getInContactElement (model:Model) dispatch = +// Bulma.content [ +// prop.style [style.textAlign.justify] +// prop.children [ +// Bulma.label "Get In Contact With Us" - p [] [str "Swate is part of the DataPLANT organisation."] - p [] [ - a [Href "https://nfdi4plants.de/"; Target "_Blank"; Title "DataPLANT"; Class "nfdiIcon"; Style [Float FloatOptions.Right; MarginLeft "2em"]] [ - img [Src "https://raw.githubusercontent.com/nfdi4plants/Branding/138420e3b6f9ec9e125c1ca8840874b2be2a1262/logos/DataPLANT_logo_minimal_square_bg_darkblue.svg"; Style [Width "54px"]] - ] - str "Services and infrastructures to support " - a [Href "https://twitter.com/search?q=%23FAIRData&src=hashtag_click"] [ str "#FAIRData" ] - str " science and good data management practices within the plant basic research community. " - a [Href "https://twitter.com/search?q=%23NFDI&src=hashtag_click"] [ str "#NFDI" ] - ] +// Html.p "Swate is part of the DataPLANT organisation." +// Html.p [ +// Html.a [prop.href "https://nfdi4plants.de/"] +// a [Href ; Target "_Blank"; Title "DataPLANT"; Class "nfdiIcon"; Style [Float FloatOptions.Right; MarginLeft "2em"]] [ +// img [Src "https://raw.githubusercontent.com/nfdi4plants/Branding/138420e3b6f9ec9e125c1ca8840874b2be2a1262/logos/DataPLANT_logo_minimal_square_bg_darkblue.svg"; Style [Width "54px"]] +// ] +// str "Services and infrastructures to support " +// a [Href "https://twitter.com/search?q=%23FAIRData&src=hashtag_click"] [ str "#FAIRData" ] +// str " science and good data management practices within the plant basic research community. " +// a [Href "https://twitter.com/search?q=%23NFDI&src=hashtag_click"] [ str "#NFDI" ] +// ] - p [] [ - str "Got a good idea or just want to get in touch? " - a [Href Shared.URLs.Helpdesk.Url;Target "_Blank"] [str "Reach out to us!"] - ] +// Html.p [ +// str "Got a good idea or just want to get in touch? " +// a [Href Shared.URLs.Helpdesk.Url;Target "_Blank"] [str "Reach out to us!"] +// ] - iconContainer - ([ - Html.span "Follow us on Twitter for the more up-to-date information about research data management! " - a [Href Shared.URLs.NFDITwitterUrl; Target "_Blank";] [str "@nfdi4plants"] - ]) - (Bulma.icon [ - prop.href Shared.URLs.NFDITwitterUrl; - prop.target "_Blank"; - prop.title "@nfdi4plants on Twitter" - Bulma.icon.isLarge; - Html.i [prop.classes ["fa-brands fa-twitter"; "myFaBrand myFaTwitter"; "is-size-3"]] - |> prop.children - ]) +// iconContainer +// ([ +// Html.span "Follow us on Twitter for the more up-to-date information about research data management! " +// a [Href Shared.URLs.NFDITwitterUrl; Target "_Blank";] [str "@nfdi4plants"] +// ]) +// (Bulma.icon [ +// prop.href Shared.URLs.NFDITwitterUrl; +// prop.target "_Blank"; +// prop.title "@nfdi4plants on Twitter" +// Bulma.icon.isLarge; +// Html.i [prop.classes ["fa-brands fa-twitter"; "myFaBrand myFaTwitter"; "is-size-3"]] +// |> prop.children +// ]) - iconContainer - ([ - str "You can find the Swate source code " - a [Href Shared.URLs.SwateRepo; Target "_Blank"] [str "here"] - str ". Our developers are always happy to get in contact with you! If you don't have a GitHub account but want to reach out or want to snitch on some nasty bugs 🐛 you can tell us " - a [Href Shared.URLs.Helpdesk.UrlSwateTopic; Target "_Blank"] [str "here"] - str "." - ]) - (Bulma.icon [ - prop.href Shared.URLs.SwateRepo; - prop.target "_Blank"; - prop.title "Swate on GitHub" - Bulma.icon.isLarge; - Html.i [prop.classes ["fa-brands fa-github"; "myFaBrand myFaGithub"; "is-size-3"]] - |> prop.children - ]) - ] - ] +// iconContainer +// ([ +// str "You can find the Swate source code " +// a [Href Shared.URLs.SwateRepo; Target "_Blank"] [str "here"] +// str ". Our developers are always happy to get in contact with you! If you don't have a GitHub account but want to reach out or want to snitch on some nasty bugs 🐛 you can tell us " +// a [Href Shared.URLs.Helpdesk.UrlSwateTopic; Target "_Blank"] [str "here"] +// str "." +// ]) +// (Bulma.icon [ +// prop.href Shared.URLs.SwateRepo; +// prop.target "_Blank"; +// prop.title "Swate on GitHub" +// Bulma.icon.isLarge; +// Html.i [prop.classes ["fa-brands fa-github"; "myFaBrand myFaGithub"; "is-size-3"]] +// |> prop.children +// ]) +// ] +// ] let infoComponent (model : Model) (dispatch : Msg -> unit) = Bulma.content [ pageHeader "Swate" - Bulma.field.div [ - introductionElement model dispatch - ] - Bulma.field.div [ - div [] [ - Bulma.label "Documentation" + //Bulma.field.div [ + // introductionElement model dispatch + //] + //Bulma.field.div [ + // div [] [ + // Bulma.label "Documentation" - ul [] [ - li [] [p [] [ a [Href Shared.URLs.SwateWiki; Target "_blank"] [ str "User documentation"] ] ] - li [] [p [] [ - str "OpenApi docs for " - a [Href (Shared.URLs.Docs.OntologyApi Shared.URLs.Docs.Html); Target "_blank"] [ str "IOntologyAPI"] - str "." ] - ] - ] - ] - ] + // ul [] [ + // li [] [Html.p [ a [Href Shared.URLs.SwateWiki; Target "_blank"] [ str "User documentation"] ] ] + // li [] [Html.p [ + // str "OpenApi docs for " + // a [Href (Shared.URLs.Docs.OntologyApi Shared.URLs.Docs.Html); Target "_blank"] [ str "IOntologyAPI"] + // str "." ] + // ] + // ] + // ] + //] + //Bulma.field.div [ + // getInContactElement model dispatch + //] Bulma.field.div [ - getInContactElement model dispatch + prop.text "WIP 🚧" ] ] \ No newline at end of file diff --git a/src/Client/Pages/JsonExporter/JsonExporter.fs b/src/Client/Pages/JsonExporter/JsonExporter.fs index c64d1e88..d3c07b75 100644 --- a/src/Client/Pages/JsonExporter/JsonExporter.fs +++ b/src/Client/Pages/JsonExporter/JsonExporter.fs @@ -1,7 +1,5 @@ module JsonExporter.Core -open Fable.React -open Fable.React.Props open Fable.Core.JsInterop open Elmish @@ -351,37 +349,37 @@ type FileExporter = Html.ul [ Html.li [ Html.b "ARCtrl" - str ": A simple ARCtrl specific format." + Html.text ": A simple ARCtrl specific format." ] Html.li [ Html.b "ARCtrlCompressed" - str ": A compressed ARCtrl specific format." + Html.text ": A compressed ARCtrl specific format." ] Html.li [ Html.b "ISA" - str ": ISA-JSON format (" + Html.text ": 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.text ")." ] Html.li [ Html.b "ROCrate" - str ": ROCrate format (" + Html.text ": ROCrate format (" Html.a [ prop.target.blank prop.href "https://www.researchobject.org/ro-crate/" prop.text "ROCrate" ] - str ", " + Html.text ", " 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 ")." + Html.text ")." ] ] ] diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs index 062fd3b6..9cd95d52 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolSearch.fs @@ -7,7 +7,7 @@ open Messages open Feliz open Feliz.Bulma -module private Helper = +module private HelperProtocolSearch = let breadcrumbEle (model:Model) dispatch = Bulma.breadcrumb [ @@ -42,21 +42,22 @@ type SearchContainer = 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()) + Html.div [ + prop.onSubmit (fun e -> e.preventDefault()) // https://keycode.info/ - OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) - ] [ - Helper.breadcrumbEle model dispatch + prop.onKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + prop.children [ + HelperProtocolSearch.breadcrumbEle 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 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."] - Bulma.label "Search the database for protocol templates." + Bulma.label "Search the database for protocol templates." - mainFunctionContainer [ - Protocol.Search.InfoField() - Protocol.Search.FileSortElement(model, config, setConfig) - Protocol.Search.Component (filteredTemplates, model, dispatch) + mainFunctionContainer [ + Protocol.Search.InfoField() + Protocol.Search.FileSortElement(model, config, setConfig) + Protocol.Search.Component (filteredTemplates, model, dispatch) + ] ] ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs index 88d0fd82..7667f3c2 100644 --- a/src/Client/Pages/ProtocolTemplates/ProtocolView.fs +++ b/src/Client/Pages/ProtocolTemplates/ProtocolView.fs @@ -23,21 +23,20 @@ open Feliz.Bulma type Templates = static member Main (model:Model, dispatch) = - div [ - OnSubmit (fun e -> e.preventDefault()) - // https://keycode.info/ - OnKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) - ] [ - - pageHeader "Templates" + Html.div [ + prop.onSubmit (fun e -> e.preventDefault()) + prop.onKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + prop.children [ + pageHeader "Templates" - // Box 1 - Bulma.label "Add template from database." + // Box 1 + Bulma.label "Add template from database." - TemplateFromDB.Main(model, dispatch) + TemplateFromDB.Main(model, dispatch) - // Box 2 - Bulma.label "Add template(s) from file." + // Box 2 + Bulma.label "Add template(s) from file." - TemplateFromFile.Main(model, dispatch) + TemplateFromFile.Main(model, dispatch) + ] ] \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs index be8cf0cd..ead28945 100644 --- a/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromFile.fs @@ -144,8 +144,8 @@ type TemplateFromFile = //Modals.SelectiveImportModal.Main af.current model.SpreadsheetModel dispatch (fun _ -> TemplateFromFileState.init() |> setState) Bulma.field.div [ Bulma.help [ - b [] [str "Import JSON files."] - str " You can use \"Json Export\" to create these files from existing Swate tables. " + Html.b "Import JSON files." + Html.text " You can use \"Json Export\" to create these files from existing Swate tables. " ] ] Bulma.field.div [ diff --git a/src/Client/Pages/Settings/SettingsView.fs b/src/Client/Pages/Settings/SettingsView.fs index bea788a8..d040c405 100644 --- a/src/Client/Pages/Settings/SettingsView.fs +++ b/src/Client/Pages/Settings/SettingsView.fs @@ -105,9 +105,7 @@ let swateCore (model:Model) dispatch = let settingsViewComponent (model:Model) dispatch = - div [ - //Style [MaxWidth "500px"] - ] [ + Html.div [ pageHeader "Swate Settings" Bulma.label "Customize Swate" diff --git a/src/Client/Pages/TermSearch/TermSearchView.fs b/src/Client/Pages/TermSearch/TermSearchView.fs index 88364889..2b38c48c 100644 --- a/src/Client/Pages/TermSearch/TermSearchView.fs +++ b/src/Client/Pages/TermSearch/TermSearchView.fs @@ -99,21 +99,22 @@ let private addButton (model: Model, dispatch) = [] let Main (model:Model, dispatch) = let setTerm = fun (term: OntologyAnnotation option) -> TermSearch.UpdateSelectedTerm term |> TermSearchMsg |> dispatch - div [ - OnSubmit (fun e -> e.preventDefault()) - OnKeyDown (fun k -> if (int k.which) = 13 then k.preventDefault()) - ] [ - pageHeader "Ontology term search" + Html.div [ + prop.onSubmit (fun e -> e.preventDefault()) + prop.onKeyDown (fun k -> if k.key = "Enter" then k.preventDefault()) + prop.children [ + pageHeader "Ontology term search" - Bulma.label "Search for an ontology term to fill into the selected field(s)" + Bulma.label "Search for an ontology term to fill into the selected field(s)" - mainFunctionContainer [ - Bulma.field.div [ - Components.TermSearch.Input(setTerm, fullwidth=true, size=Bulma.input.isLarge, ?parent=model.TermSearchState.ParentTerm, advancedSearchDispatch=dispatch) + mainFunctionContainer [ + Bulma.field.div [ + Components.TermSearch.Input(setTerm, fullwidth=true, size=Bulma.input.isLarge, ?parent=model.TermSearchState.ParentTerm, advancedSearchDispatch=dispatch) + ] + addButton(model, dispatch) ] - addButton(model, dispatch) ] - + ] //simpleSearchComponent model dispatch //if model.TermSearchState.SelectedTerm.IsNone then @@ -132,5 +133,4 @@ let Main (model:Model, dispatch) = //if model.TermSearchState.ParentOntology.IsNone then // str "No Parent Ontology selected" //else - // str model.TermSearchState.ParentOntology.Value - ] \ No newline at end of file + // str model.TermSearchState.ParentOntology.Value \ No newline at end of file diff --git a/src/Client/SharedComponents/AdvancedSearch.fs b/src/Client/SharedComponents/AdvancedSearch.fs index 608b7a34..a7b8a43e 100644 --- a/src/Client/SharedComponents/AdvancedSearch.fs +++ b/src/Client/SharedComponents/AdvancedSearch.fs @@ -28,12 +28,11 @@ let private StartAdvancedSearch (state: AdvancedSearch.Model) setState dispatch AdvancedSearch.Msg.GetSearchResults {|config=state.AdvancedSearchOptions; responseSetter = setter|} |> AdvancedSearchMsg |> dispatch let private createLinkOfAccession (accession:string) = - a [ - let link = accession |> URLs.termAccessionUrlOfAccessionStr - Href link - Target "_Blank" - ] [ - str accession + let link = accession |> URLs.termAccessionUrlOfAccessionStr + Html.a [ + prop.href link + prop.target.blank + prop.text accession ] let private isValidAdancedSearchOptions (opt:AdvancedSearchOptions) = @@ -109,24 +108,24 @@ module private ResultsTable = prop.tabIndex 0 prop.className "suggestion" prop.children [ - td [] [ - b [] [ str sugg.Name ] - ] + Html.td [Html.b sugg.Name ] if sugg.IsObsolete then - td [Style [Color "red"]] [str "obsolete"] + Html.td [prop.style [style.color "red"]; prop.text "obsolete"] else - td [] [] - td [ - OnClick ( + Html.td [] + Html.td [ + prop.onClick ( fun e -> e.stopPropagation() ) - Style [FontWeight "light"] - ] [ - small [] [ - createLinkOfAccession sugg.Accession - ] ] - td [] [ + prop.style [style.fontWeight.lighter] + prop.children [ + Html.small [ + createLinkOfAccession sugg.Accession + ] + ] + ] + Html.td [ Bulma.buttons [ Bulma.buttons.isRight prop.children [ @@ -176,10 +175,13 @@ module private ResultsTable = prop.className "suggestion-details" prop.style [if List.contains id state.ActiveDropdowns then style.visibility.visible else style.visibility.collapse] prop.children [ - td [ColSpan 4] [ - Bulma.content [ - b [] [ str "Definition: " ] - str sugg.Description + Html.td [ + prop.colSpan 4 + prop.children [ + Bulma.content [ + Html.b "Definition: " + Html.text sugg.Description + ] ] ] ] @@ -189,7 +191,7 @@ module private ResultsTable = else [| Html.tr [ - td [] [str "No terms found matching your input."] + Html.td "No terms found matching your input." ] |] @@ -217,8 +219,8 @@ module private ResultsTable = Bulma.table [ Bulma.table.isFullWidth prop.children [ - thead [] [] - tbody [] ( + Html.thead [] + Html.tbody ( chunked.[currentPageinationIndex] |> List.ofArray ) ] @@ -261,7 +263,7 @@ let private keepObsoleteCheckradioElement (state:AdvancedSearch.Model) setState let checkradioName = "keepObsolete_checkradio" let id = sprintf "%s"checkradioName Bulma.field.div [ - Bulma.Checkradio.checkbox [ + Bulma.input.radio [ prop.name checkradioName prop.id id prop.isChecked (state.AdvancedSearchOptions.KeepObsolete) diff --git a/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs b/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs index d6b9a730..175028f1 100644 --- a/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs +++ b/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs @@ -19,9 +19,7 @@ let annotationTableMissingWarningComponent (model:Model) (dispatch: Msg-> unit) ) ] Html.h5 "Warning: No annotation table found in worksheet" - Bulma.field.div [ - str "Your worksheet seems to contain no annotation table. You can create one by pressing the button below." - ] + Bulma.field.div "Your worksheet seems to contain no annotation table. You can create one by pressing the button below." Bulma.field.div [ Bulma.button.button [ Bulma.button.isFullWidth diff --git a/src/Client/SidebarComponents/LayoutHelper.fs b/src/Client/SidebarComponents/LayoutHelper.fs index 0deb5b11..85cac180 100644 --- a/src/Client/SidebarComponents/LayoutHelper.fs +++ b/src/Client/SidebarComponents/LayoutHelper.fs @@ -12,8 +12,7 @@ open Messages open Messages.BuildingBlock open Shared open TermTypes - -open OfficeInteropTypes +open Feliz open Elmish let rnd = System.Random() @@ -22,15 +21,17 @@ let mutable order : bool = let v = rnd.Next(0,10) if v > 5 then false else true -let mainFunctionContainer children = - div [ - Class "mainFunctionContainer" - Style [ +let mainFunctionContainer (children: ReactElement list) = + Html.div [ + prop.className "mainFunctionContainer" + prop.style [ let rndVal = rnd.Next(30,70) let colorArr = [|NFDIColors.LightBlue.Lighter10; NFDIColors.Mint.Lighter10;|] - BorderImageSource $"linear-gradient({colorArr.[if order then 0 else 1]} {100-rndVal}%%, {colorArr.[if order then 1 else 0]})" + style.custom("borderImageSource", $"linear-gradient({colorArr.[if order then 0 else 1]} {100-rndVal}%%, {colorArr.[if order then 1 else 0]})") order <- not order - ] ] children + ] + prop.children children + ] open Feliz open Feliz.Bulma diff --git a/src/Client/SidebarComponents/Navbar.fs b/src/Client/SidebarComponents/Navbar.fs index 0bc9471f..10d2d269 100644 --- a/src/Client/SidebarComponents/Navbar.fs +++ b/src/Client/SidebarComponents/Navbar.fs @@ -55,7 +55,7 @@ let private shortCutIconList model dispatch = "Update Ontology Terms", [ Html.i [prop.className "fa-solid fa-spell-check"] - span [] [str model.ExcelState.FillHiddenColsStateStore.toReadableString] + Html.span model.ExcelState.FillHiddenColsStateStore.toReadableString Html.i [prop.className "fa-solid fa-pen"] ], (fun _ -> SpreadsheetInterface.UpdateTermColumns |> InterfaceMsg |> dispatch) @@ -72,7 +72,7 @@ let private shortCutIconList model dispatch = "Get Building Block Information", [ Html.i [prop.className "fa-solid fa-question pr-1"] - span [] [str model.BuildingBlockDetailsState.CurrentRequestState.toStringMsg] + Html.span model.BuildingBlockDetailsState.CurrentRequestState.toStringMsg Html.i [prop.className "fa-solid fa-table-columns"] ], (fun _ -> SpreadsheetInterface.EditBuildingBlock |> InterfaceMsg |> dispatch) @@ -92,57 +92,57 @@ let private quickAccessDropdownElement model dispatch (state: NavbarState) (setS prop.style [ style.padding 0; if isSndNavbar then style.custom("marginLeft", "auto")] prop.title (if state.QuickAccessActive then "Close quick access" else "Open quick access") prop.children [ - div [Style [ - Width "100%" - Height "100%" - Position PositionOptions.Relative - ]] [ - Bulma.button.a [ - prop.style [style.backgroundColor "transparent"; style.height(length.perc 100); if state.QuickAccessActive then style.color NFDIColors.Yellow.Base] - Bulma.color.isWhite - Bulma.button.isInverted - div [Style [ - Display DisplayOptions.InlineFlex - Position PositionOptions.Relative - JustifyContent "center" - ]] [ - Html.i [ - prop.style [ - style.position.absolute - style.display.block - style.custom("transition","opacity 0.25s, transform 0.25s") - style.opacity (if state.QuickAccessActive then 1 else 0) - style.transform (if state.QuickAccessActive then [transform.rotate -180] else [transform.rotate 0]) - ] - prop.className "fa-solid fa-times" - ] - Html.i [ - prop.style [ - style.position.absolute - style.display.block - style.custom("transition","opacity 0.25s, transform 0.25s") - style.opacity (if state.QuickAccessActive then 0 else 1) - ] - prop.className "fa-solid fa-ellipsis" - ] - // Invis placeholder to create correct space (Height, width, margin, padding, etc.) - Html.i [ - prop.style [ - style.display.block - style.opacity 0 - ] - prop.className "fa-solid fa-ellipsis" + Html.div [ + prop.style [style.width(length.perc 100); style.height (length.perc 100); style.position.relative] + prop.children [ + Bulma.button.a [ + prop.style [style.backgroundColor "transparent"; style.height(length.perc 100); if state.QuickAccessActive then style.color NFDIColors.Yellow.Base] + Bulma.color.isWhite + Bulma.button.isInverted + prop.children [ + Html.div [ + prop.style [ style.display.inlineFlex; style.position.relative; style.justifyContent.center] + prop.children [ + Html.i [ + prop.style [ + style.position.absolute + style.display.block + style.custom("transition","opacity 0.25s, transform 0.25s") + style.opacity (if state.QuickAccessActive then 1 else 0) + style.transform (if state.QuickAccessActive then [transform.rotate -180] else [transform.rotate 0]) + ] + prop.className "fa-solid fa-times" + ] + Html.i [ + prop.style [ + style.position.absolute + style.display.block + style.custom("transition","opacity 0.25s, transform 0.25s") + style.opacity (if state.QuickAccessActive then 0 else 1) + ] + prop.className "fa-solid fa-ellipsis" + ] + // Invis placeholder to create correct space (Height, width, margin, padding, etc.) + Html.i [ + prop.style [ + style.display.block + style.opacity 0 + ] + prop.className "fa-solid fa-ellipsis" + ] + ] + ] ] ] - |> prop.children ] ] ] ] let private quickAccessListElement model dispatch = - div [Style [Display DisplayOptions.Flex; FlexDirection "row"]] [ - yield! navbarShortCutIconList model dispatch + Html.div [ + prop.style [style.display.flex; style.flexDirection.row] + prop.children (navbarShortCutIconList model dispatch) ] @@ -192,10 +192,10 @@ let NavbarComponent (model : Model) (dispatch : Msg -> unit) (sidebarsize: Model prop.ariaExpanded false prop.style [style.display.block] prop.children [ - span [AriaHidden true] [ ] - span [AriaHidden true] [ ] - span [AriaHidden true] [ ] - span [AriaHidden true] [ ] + Html.span [prop.ariaHidden true] + Html.span [prop.ariaHidden true] + Html.span [prop.ariaHidden true] + Html.span [prop.ariaHidden true] ] ] ] diff --git a/src/Client/SidebarComponents/ResponsiveFA.fs b/src/Client/SidebarComponents/ResponsiveFA.fs index a14d1faf..5c7af135 100644 --- a/src/Client/SidebarComponents/ResponsiveFA.fs +++ b/src/Client/SidebarComponents/ResponsiveFA.fs @@ -11,40 +11,41 @@ open Feliz open Feliz.Bulma let responsiveFaElement toggle fa faToggled = - div [Style [ - Position PositionOptions.Relative - ]] [ - Bulma.icon [ Html.i [ - prop.style [ - style.position.absolute - style.top 0 - style.left 0 - style.display.block - style.transitionProperty "opacity 0.25s, transform 0.25s" - if toggle then style.opacity 0 else style.opacity 1 - ] - fa - ]] - Bulma.icon [ Html.i [ - prop.style [ - style.position.absolute - style.top 0 - style.left 0 - style.display.block - style.transitionProperty "opacity 0.25s, transform 0.25s" - if toggle then style.opacity 1 else style.opacity 0 - if toggle then style.transform [transform.rotate -180] else style.transform [transform.rotate 0] - ] - faToggled - ]] - // Invis placeholder to create correct space (Height, width, margin, padding, etc.) - Bulma.icon [ Html.i [ - prop.style [ - style.display.block - style.opacity 0 - ] - fa - ]] + Html.div [ + prop.style [style.position.relative] + prop.children [ + Bulma.icon [ Html.i [ + prop.style [ + style.position.absolute + style.top 0 + style.left 0 + style.display.block + style.transitionProperty "opacity 0.25s, transform 0.25s" + if toggle then style.opacity 0 else style.opacity 1 + ] + fa + ]] + Bulma.icon [ Html.i [ + prop.style [ + style.position.absolute + style.top 0 + style.left 0 + style.display.block + style.transitionProperty "opacity 0.25s, transform 0.25s" + if toggle then style.opacity 1 else style.opacity 0 + if toggle then style.transform [transform.rotate -180] else style.transform [transform.rotate 0] + ] + faToggled + ]] + // Invis placeholder to create correct space (Height, width, margin, padding, etc.) + Bulma.icon [ Html.i [ + prop.style [ + style.display.block + style.opacity 0 + ] + fa + ]] + ] ] let private createTriggeredId id = diff --git a/src/Client/Update.fs b/src/Client/Update.fs index bdbb3742..e489ed68 100644 --- a/src/Client/Update.fs +++ b/src/Client/Update.fs @@ -2,11 +2,9 @@ module Update.Update open Elmish -open Thoth.Elmish open Shared open TermTypes -open OfficeInteropTypes open Routing open Messages open Model diff --git a/src/Client/Views/NotFoundView.fs b/src/Client/Views/NotFoundView.fs index ee220b5c..0631e074 100644 --- a/src/Client/Views/NotFoundView.fs +++ b/src/Client/Views/NotFoundView.fs @@ -1,10 +1,8 @@ module NotFoundView -open Fable.React +open Feliz open Messages open Model let notFoundComponent (model:Model) (dispatch:Msg -> unit) = - div [] [ - str "The requested url does not exist in context of this application. Please tell us how you got here so we can fix this together." - ] \ No newline at end of file + Html.div "The requested url does not exist in context of this application. Please tell us how you got here so we can fix this together." \ No newline at end of file diff --git a/src/Client/Views/SidebarView.fs b/src/Client/Views/SidebarView.fs index a6ae70d2..f0e0958f 100644 --- a/src/Client/Views/SidebarView.fs +++ b/src/Client/Views/SidebarView.fs @@ -6,8 +6,6 @@ open ExcelColors open Model open Messages open Browser -open Browser.MediaQueryList -open Browser.MediaQueryListExtensions open CustomComponents open Fable.Core.JsInterop @@ -109,21 +107,22 @@ module private ResizeObserver = let private viewContainer (model: Model) (dispatch: Msg -> unit) (state: SidebarStyle) (setState: SidebarStyle -> unit) (children: ReactElement list) = - div [ - Id Sidebar_Id - OnLoad(fun e -> + Html.div [ + prop.id Sidebar_Id + prop.onLoad(fun e -> let ele = Browser.Dom.document.getElementById(Sidebar_Id) ResizeObserver.observer(state, setState).observe(ele) ) - Style [ - Display DisplayOptions.Flex - FlexGrow "1" - FlexDirection "column" - Position PositionOptions.Relative - MaxWidth "100%" - OverflowY OverflowOptions.Auto + prop.style [ + style.display.flex + style.flexGrow 1 + style.flexDirection.column + style.position.relative + style.maxWidth (length.perc 100) + style.overflowY.auto ] - ] children + prop.children children + ] type SidebarView = @@ -136,12 +135,15 @@ type SidebarView = } |> 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.a [prop.style [style.cursor.defaultCursor] ;prop.text (sprintf "%O" model.PersistentStorageState.Host)] + Html.div [ + prop.style [style.color "grey"; style.position.sticky; style.width.inheritFromParent; style.bottom 0; style.textAlign.center] + prop.children [ + Html.div [ + Html.text "Swate Release Version " + Html.a [prop.href "https://github.com/nfdi4plants/Swate/releases"; prop.target.blank; prop.text model.PersistentStorageState.AppVersion] + Html.text " Host " + Html.a [prop.style [style.cursor.defaultCursor]; prop.text (sprintf "%O" model.PersistentStorageState.Host)] + ] ] ] diff --git a/src/Client/Views/SplitWindowView.fs b/src/Client/Views/SplitWindowView.fs index 9d23e83c..a71668a9 100644 --- a/src/Client/Views/SplitWindowView.fs +++ b/src/Client/Views/SplitWindowView.fs @@ -101,7 +101,7 @@ open Model // 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:Model) (dispatch: Messages.Msg -> unit) = +let Main (left:seq) (right:seq) (mainModel:Model) (dispatch: Messages.Msg -> unit) = let (model, setModel) = React.useState(SplitWindow.init) let isNotMetadataSheet = not (mainModel.SpreadsheetModel.ActiveView = Spreadsheet.ActiveView.Metadata) React.useEffect(model.WriteToLocalStorage, [|box model|]) diff --git a/src/Client/paket.references b/src/Client/paket.references deleted file mode 100644 index 0fe29ff8..00000000 --- a/src/Client/paket.references +++ /dev/null @@ -1,23 +0,0 @@ -FSharp.Core -Fable.Core -Fable.React -Fable.Elmish.React -Fable.Elmish.Debugger -Fable.Elmish.HMR -Fable.Elmish -Fable.Remoting.Client -Thoth.Elmish.Debouncer -Fable.Browser.MediaQueryList -Fable.SimpleJson -Fable.SimpleXml -Feliz.Bulma -Feliz.UseElmish -ExcelJS.Fable -Feliz.ElmishComponents -Fable.Elmish.Browser -FsSpreadsheet -FsSpreadsheet.Js -Feliz.Bulma.Checkradio -Feliz.Bulma.Switch -ARCtrl -Fable.Fetch \ No newline at end of file diff --git a/src/Server/Server.fsproj b/src/Server/Server.fsproj index 3634721b..f67e1641 100644 --- a/src/Server/Server.fsproj +++ b/src/Server/Server.fsproj @@ -1,25 +1,29 @@ - - Exe - net8.0 - 6de80bdf-2a05-4cf7-a1a8-d08581dfa887 - - - - - - - - - - - - - - - - - - + + Exe + net8.0 + 6de80bdf-2a05-4cf7-a1a8-d08581dfa887 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/paket.references b/src/Server/paket.references deleted file mode 100644 index 57b0628c..00000000 --- a/src/Server/paket.references +++ /dev/null @@ -1,6 +0,0 @@ -FSharp.Core -Saturn -Fable.Remoting.Giraffe -Microsoft.Extensions.Configuration.UserSecrets -ARCtrl -Neo4j.Driver \ No newline at end of file diff --git a/src/Shared/Shared.fsproj b/src/Shared/Shared.fsproj index 25d70a89..411daeaa 100644 --- a/src/Shared/Shared.fsproj +++ b/src/Shared/Shared.fsproj @@ -5,7 +5,6 @@ - @@ -14,5 +13,13 @@ - + + + + + + + + + \ No newline at end of file diff --git a/src/Shared/paket.references b/src/Shared/paket.references deleted file mode 100644 index 140dc18d..00000000 --- a/src/Shared/paket.references +++ /dev/null @@ -1,2 +0,0 @@ -FSharp.Core -ARCtrl \ No newline at end of file diff --git a/tests/Client/Client.Tests.fsproj b/tests/Client/Client.Tests.fsproj index 0a52fc4a..15f3b9bd 100644 --- a/tests/Client/Client.Tests.fsproj +++ b/tests/Client/Client.Tests.fsproj @@ -15,5 +15,4 @@ - \ No newline at end of file diff --git a/tests/Client/paket.references b/tests/Client/paket.references deleted file mode 100644 index 47d7d270..00000000 --- a/tests/Client/paket.references +++ /dev/null @@ -1 +0,0 @@ -Fable.Mocha \ No newline at end of file diff --git a/tests/Server/Server.Tests.fsproj b/tests/Server/Server.Tests.fsproj index 9c758c6f..a375f574 100644 --- a/tests/Server/Server.Tests.fsproj +++ b/tests/Server/Server.Tests.fsproj @@ -14,5 +14,4 @@ - \ No newline at end of file diff --git a/tests/Server/paket.references b/tests/Server/paket.references deleted file mode 100644 index f87d8284..00000000 --- a/tests/Server/paket.references +++ /dev/null @@ -1 +0,0 @@ -Expecto \ No newline at end of file diff --git a/tests/Shared/Shared.Tests.fsproj b/tests/Shared/Shared.Tests.fsproj index c3980cb4..854f77c0 100644 --- a/tests/Shared/Shared.Tests.fsproj +++ b/tests/Shared/Shared.Tests.fsproj @@ -8,5 +8,8 @@ - + + + + \ No newline at end of file diff --git a/tests/Shared/paket.references b/tests/Shared/paket.references deleted file mode 100644 index e843d71e..00000000 --- a/tests/Shared/paket.references +++ /dev/null @@ -1,2 +0,0 @@ -Expecto -Fable.Mocha \ No newline at end of file From f4636f7db0a2083225c0ce8d49c60d9e0f4e48a4 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 2 Jul 2024 08:00:32 +0200 Subject: [PATCH 04/25] Fix empty body if no preexisting rows #455 --- src/Client/Pages/BuildingBlock/SearchComponent.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index 60caf140..ab944c60 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -136,7 +136,8 @@ let private addBuildingBlockButton (model: Model) dispatch = prop.onClick (fun _ -> 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) + let rowCount = System.Math.Max(1,model.SpreadsheetModel.ActiveTable.RowCount) + Array.init rowCount (fun _ -> body.Value) else Array.empty let column = CompositeColumn.create(header, bodyCells) From 4d80a42abf7819adcc639dc9935f2563360725ad Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 2 Jul 2024 08:05:21 +0200 Subject: [PATCH 05/25] Burn deprecated code reference :fire: --- src/Client/Pages/BuildingBlock/Dropdown.fs | 10 +--------- src/Client/Pages/BuildingBlock/Helper.fs | 1 - src/Client/Pages/BuildingBlock/SearchComponent.fs | 2 -- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Client/Pages/BuildingBlock/Dropdown.fs b/src/Client/Pages/BuildingBlock/Dropdown.fs index 7810d325..29cb38a2 100644 --- a/src/Client/Pages/BuildingBlock/Dropdown.fs +++ b/src/Client/Pages/BuildingBlock/Dropdown.fs @@ -1,19 +1,11 @@ -module BuildingBlock.Dropdown +module BuildingBlock.Dropdown open Feliz open Feliz.Bulma -open Shared -open TermTypes -open OfficeInteropTypes -open Fable.Core.JsInterop -open Elmish open Model.BuildingBlock -open Model.TermSearch open Model open Messages open ARCtrl -open BuildingBlock.Helper -open Fable.Core [] diff --git a/src/Client/Pages/BuildingBlock/Helper.fs b/src/Client/Pages/BuildingBlock/Helper.fs index b56c058d..97567c07 100644 --- a/src/Client/Pages/BuildingBlock/Helper.fs +++ b/src/Client/Pages/BuildingBlock/Helper.fs @@ -1,7 +1,6 @@ module BuildingBlock.Helper open Shared -open OfficeInteropTypes open Model open Messages open ARCtrl diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index ab944c60..d359d408 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -3,8 +3,6 @@ module BuildingBlock.SearchComponent open Feliz open Feliz.Bulma open Shared -open TermTypes -open OfficeInteropTypes open Fable.Core.JsInterop open Elmish open Model.BuildingBlock From 3d0ae3e919cf03218946630d0c25d8aa8ba704af Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 2 Jul 2024 13:01:54 +0200 Subject: [PATCH 06/25] Update dependencies --- src/Client/Client.fsproj | 2 +- src/Shared/Shared.fsproj | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index 53c44494..a4b88da5 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -127,6 +127,6 @@ - + diff --git a/src/Shared/Shared.fsproj b/src/Shared/Shared.fsproj index 411daeaa..df6fb09b 100644 --- a/src/Shared/Shared.fsproj +++ b/src/Shared/Shared.fsproj @@ -14,12 +14,12 @@ - - - - - - - + + + + + + + \ No newline at end of file From 81e4b9935593259ca05882f0d7714ad087f9f2aa Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 2 Jul 2024 13:03:28 +0200 Subject: [PATCH 07/25] :sparkles: Added helper functions create table for excel #447 --- src/Client/OfficeInterop/OfficeInterop.fs | 144 +++++++++++++----- .../AnnotationTableMissingWarning.fs | 2 +- src/Client/Update/OfficeInteropUpdate.fs | 6 +- 3 files changed, 113 insertions(+), 39 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index 99a2ad41..6d722f9f 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -1,6 +1,5 @@ module OfficeInterop.Core -open System.Collections.Generic open Fable.Core open ExcelJS.Fable open Excel @@ -14,6 +13,91 @@ open OfficeInterop open OfficeInterop.HelperFunctions open BuildingBlockFunctions +open ARCtrl +open ARCtrl.Spreadsheet + +module OfficeInteropExtensions = + + open ARCtrl.Spreadsheet.ArcTable + + type ArcTable with + + /// + /// WIP + /// + /// + /// + /// + static member ofStringSeqs(name:string, headers:#seq, rows:#seq<#seq>) = + + let columns = + Seq.append [headers] rows + |> Seq.transpose + + let columnsList = + columns + |> Seq.toArray + |> Array.map (Seq.toArray) + + + let compositeColumns = ArcTable.composeColumns columnsList + + let arcTable = + ArcTable.init name + |> ArcTable.addColumns(compositeColumns,skipFillMissing = true) + |> Some + + arcTable + + /// + /// Transforms ArcTable to excel compatible "values", row major + /// + member this.ToExcelValues() = + + let table = this + + // Cancel if there are no columns + if table.Columns.Length = 0 then + ResizeArray() + else + let columns = + table.Columns + |> List.ofArray + |> List.sortBy classifyColumnOrder + |> List.collect CompositeColumn.toStringCellColumns + |> Seq.transpose + |> Seq.map (fun x -> + x |> Seq.map (box >> Some) + |> ResizeArray + ) + |> ResizeArray + + columns + + //|> List.iteri (fun colI col -> + // col + // |> List.iteri (fun rowI stringCell -> + // let value = + // if rowI = 0 then + + // match Dictionary.tryGet stringCell stringCount with + // | Some spaces -> + // stringCount.[stringCell] <- spaces + " " + // stringCell + " " + spaces + // | None -> + // stringCount.Add(stringCell,"") + // stringCell + // else stringCell + // let address = FsAddress(rowI+1,colI+1) + // fsTable.Cell(address, ws.CellCollection).SetValueAs value + // ) + //) + //ws + + let x = 0 + +open OfficeInteropExtensions + // Reoccuring Comment Defitinitions // 'annotationTables' -> For a workbook (NOT! worksheet) all tables must have unique names. Therefore not all our tables can be called 'annotationTable'. @@ -165,8 +249,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra "TableStyleMedium7" // The next part loads relevant information from the excel objects and allows us to access them after 'context.sync()' - - let tableRange = range + let tableRange = range.getColumn(0) let _ = tableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount";"address"; "isEntireColumn"; "worksheet"]))) let activeSheet = tableRange.worksheet @@ -186,7 +269,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let! allTableNames = getAllTableNames context // sync with proxy objects after loading values from excel - let! table,newTableLogging = context.sync().``then``( fun _ -> + let! table, newTableLogging = context.sync().``then``( fun _ -> // Filter all names of tables on the active worksheet for names starting with "annotationTable". let annoTables = @@ -209,48 +292,39 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra // Ref. 1 r.enableEvents <- false - // We do not want to create annotation tables of any size. The recommended workflow is to use the addBuildingBlock functionality. - // Therefore we recreate the tableRange but with a columncount of 2. The 2 Basic columns in any annotation table. - // "Source Name" | "Sample Name" - let adaptedRange = - let rowCount = - if useExistingPrevOutput then - (float prevTableOutput.Length + 1.) - elif tableRange.isEntireColumn then - 21. - elif tableRange.rowCount <= 2. then - 2. - else - tableRange.rowCount - activeSheet.getRangeByIndexes(tableRange.rowIndex,tableRange.columnIndex,rowCount,2.) - // Create table in current worksheet - let annotationTable = activeSheet.tables.add(U2.Case1 adaptedRange,true) - - // Update annotationTable column headers - (annotationTable.columns.getItemAt 0.).name <- BuildingBlockType.Source.toString - (annotationTable.columns.getItemAt 1.).name <- BuildingBlockType.Sample.toString - - if useExistingPrevOutput then - let newColValues = prevTableOutput |> Array.map (fun cell -> ResizeArray[|Option.bind (box >> Some) cell.Value|] ) |> ResizeArray - let col1 = (annotationTable.columns.getItemAt 0.) - let body = col1.getDataBodyRange() - body.values <- newColValues - + // Create new annotationTable name let newName = findNewTableName allTableNames + + let inMemoryTable = ArcTable.init(newName) + + let newCells = Array.init (int tableRange.rowCount - 1) (fun _ -> CompositeCell.emptyFreeText) + + inMemoryTable.AddColumn(CompositeHeader.Input IOType.Source, newCells) + + let tableStrings = inMemoryTable.ToExcelValues() + + let annotationTable = activeSheet.tables.add(U2.Case1 tableRange, true) + // Update annotationTable name annotationTable.name <- newName - + + tableRange.values <- tableStrings + // Update annotationTable style annotationTable.style <- style + if useExistingPrevOutput then + let newColValues = prevTableOutput |> Array.map (fun cell -> ResizeArray[|Option.bind (box >> Some) cell.Value|] ) |> ResizeArray + let col1 = (annotationTable.columns.getItemAt 0.) + let body = col1.getDataBodyRange() + body.values <- newColValues + // Fit widths and heights of cols and rows to value size. (In this case the new column headers). activeSheet.getUsedRange().format.autofitColumns() activeSheet.getUsedRange().format.autofitRows() - // let annoTableName = allTableNames |> Array.filter (fun x -> x.StartsWith "annotationTable") - r.enableEvents <- true // Return info message @@ -268,7 +342,7 @@ let createAnnotationTable (isDark:bool, tryUseLastOutput:bool) = Excel.run (fun context -> let selectedRange = context.workbook.getSelectedRange() promise { - let! newTableLogging = createAnnotationTableAtRange (isDark,tryUseLastOutput,selectedRange,context) + let! newTableLogging = createAnnotationTableAtRange (isDark, tryUseLastOutput, selectedRange, context) // Interop logging expects list of logs return [snd newTableLogging] diff --git a/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs b/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs index 175028f1..ae657175 100644 --- a/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs +++ b/src/Client/SidebarComponents/AnnotationTableMissingWarning.fs @@ -24,7 +24,7 @@ let annotationTableMissingWarningComponent (model:Model) (dispatch: Msg-> unit) Bulma.button.button [ Bulma.button.isFullWidth prop.onClick (fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) - prop.text "create annotation table" + prop.text "Create Annotation Table" ] ] ] diff --git a/src/Client/Update/OfficeInteropUpdate.fs b/src/Client/Update/OfficeInteropUpdate.fs index 4eb7337d..1f8e94f2 100644 --- a/src/Client/Update/OfficeInteropUpdate.fs +++ b/src/Client/Update/OfficeInteropUpdate.fs @@ -100,9 +100,9 @@ module OfficeInterop = let cmd = Cmd.OfPromise.either OfficeInterop.Core.createAnnotationTable - (false,tryUsePrevOutput) - (curry GenericInteropLogs (AnnotationtableCreated |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) - (curry GenericError Cmd.none >> DevMsg) + (false, tryUsePrevOutput) + (curry GenericInteropLogs (AnnotationtableCreated |> OfficeInteropMsg |> Cmd.ofMsg) >> DevMsg) //success + (curry GenericError Cmd.none >> DevMsg) //error state, model,cmd | AnnotationtableCreated -> From a337e7ef7489b21fa7f64cfc9d3d7cf3608833c6 Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 2 Jul 2024 15:59:39 +0200 Subject: [PATCH 08/25] Add obsolete to code that shall no longer be used Add helper function for handling arc in excel --- .../Functions/BuildingBlockFunctions.fs | 1 + src/Client/OfficeInterop/OfficeInterop.fs | 85 ++++++++++++++----- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/Client/OfficeInterop/Functions/BuildingBlockFunctions.fs b/src/Client/OfficeInterop/Functions/BuildingBlockFunctions.fs index 4e990fc4..c7f732fe 100644 --- a/src/Client/OfficeInterop/Functions/BuildingBlockFunctions.fs +++ b/src/Client/OfficeInterop/Functions/BuildingBlockFunctions.fs @@ -1,3 +1,4 @@ +[] module OfficeInterop.BuildingBlockFunctions open Fable.Core diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index 6d722f9f..3a4cee4c 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -16,6 +16,17 @@ open BuildingBlockFunctions open ARCtrl open ARCtrl.Spreadsheet +module MyHelper = + + let getTableByName (context:RequestContext) (tableName:string) = + let _ = context.workbook.load(U2.Case1 "tables") + let annotationTable = context.workbook.tables.getItem(tableName) + let annoHeaderRange = annotationTable.getHeaderRowRange() + let _ = annoHeaderRange.load(U2.Case2 (ResizeArray [|"columnIndex"; "values"; "columnCount"|])) |> ignore + let annoBodyRange = annotationTable.getDataBodyRange() + let _ = annoBodyRange.load(U2.Case2 (ResizeArray [|"values"; "numberFormat"|])) |> ignore + annotationTable, annoHeaderRange, annoBodyRange + module OfficeInteropExtensions = open ARCtrl.Spreadsheet.ArcTable @@ -28,7 +39,7 @@ module OfficeInteropExtensions = /// /// /// - static member ofStringSeqs(name:string, headers:#seq, rows:#seq<#seq>) = + static member fromStringSeqs(name:string, headers:#seq, rows:#seq<#seq>) = let columns = Seq.append [headers] rows @@ -74,25 +85,32 @@ module OfficeInteropExtensions = columns - //|> List.iteri (fun colI col -> - // col - // |> List.iteri (fun rowI stringCell -> - // let value = - // if rowI = 0 then - - // match Dictionary.tryGet stringCell stringCount with - // | Some spaces -> - // stringCount.[stringCell] <- spaces + " " - // stringCell + " " + spaces - // | None -> - // stringCount.Add(stringCell,"") - // stringCell - // else stringCell - // let address = FsAddress(rowI+1,colI+1) - // fsTable.Cell(address, ws.CellCollection).SetValueAs value - // ) - //) - //ws + static member arcTableFromExcelTableName (tableName:string, context:RequestContext) = + + let _, headerRange, bodyRowRange = MyHelper.getTableByName context tableName + promise { + let! inMemoryTable = context.sync().``then``(fun _ -> + let headers = + headerRange.values.[0] + |> Seq.map (fun item -> + item + |> Option.map string + |> Option.defaultValue "" + ) + let bodyRows = + bodyRowRange.values + |> Seq.map (fun items -> + items + |> Seq.map (fun item -> + item + |> Option.map string + |> Option.defaultValue "" + ) + ) + ArcTable.fromStringSeqs(tableName, headers, bodyRows) + ) + return inMemoryTable + } let x = 0 @@ -211,6 +229,29 @@ let getPrevTableOutput (context:RequestContext) = let! prevTableName = getPrevAnnotationTable context if prevTableName.IsSome then + + let _, headerRange, bodyRowRange = MyHelper.getTableByName context prevTableName.Value + let! inMemoryTable = context.sync().``then``(fun _ -> + let headers = + headerRange.values.[0] + |> Seq.map (fun item -> + item + |> Option.map string + |> Option.defaultValue "" + ) + let bodyRows = + bodyRowRange.values + |> Seq.map (fun items -> + items + |> Seq.map (fun item -> + item + |> Option.map string + |> Option.defaultValue "" + ) + ) + ArcTable.fromStringSeqs(prevTableName.Value, headers, bodyRows) + ) + // Ref. 2 let! buildingBlocks = BuildingBlockFunctions.getBuildingBlocks context prevTableName.Value @@ -261,7 +302,9 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra promise { // Is user input signals to try and find+reuse the output from the previous annotationTable do this, otherwise just return empty array - let! prevTableOutput = if tryUseLastOutput then getPrevTableOutput context else promise {return Array.empty} + let! prevTableOutput = + if tryUseLastOutput then getPrevTableOutput context + else promise {return Array.empty} // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. let useExistingPrevOutput = tryUseLastOutput && Array.isEmpty >> not <| prevTableOutput From ac701b800328bed1a395664ee6da7e2401b55220 Mon Sep 17 00:00:00 2001 From: patrick blume Date: Thu, 4 Jul 2024 11:19:25 +0200 Subject: [PATCH 09/25] Refactoring: replace let! with do! Feature: implement test version increase in memory table row count --- src/Client/OfficeInterop/OfficeInterop.fs | 178 +++++++++++++--------- 1 file changed, 105 insertions(+), 73 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index 3a4cee4c..6bf8681f 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -27,6 +27,14 @@ module MyHelper = let _ = annoBodyRange.load(U2.Case2 (ResizeArray [|"values"; "numberFormat"|])) |> ignore annotationTable, annoHeaderRange, annoBodyRange + /// Swaps 'Rows with column values' to 'Columns with row values'. + let viewRowsByColumns (rows:ResizeArray>) = + rows + |> Seq.collect (fun x -> Seq.indexed x) + |> Seq.groupBy fst + |> Seq.map (snd >> Seq.map snd >> Seq.toArray) + |> Seq.toArray + module OfficeInteropExtensions = open ARCtrl.Spreadsheet.ArcTable @@ -110,7 +118,7 @@ module OfficeInteropExtensions = ArcTable.fromStringSeqs(tableName, headers, bodyRows) ) return inMemoryTable - } + } let x = 0 @@ -188,7 +196,7 @@ let swateSync (context:RequestContext) = context.sync().``then``(fun _ -> ()) /// Will return Some tableName if any annotationTable exists in a worksheet before the active one. -let getPrevAnnotationTable (context:RequestContext) = +let getPrevAnnotationTableName (context:RequestContext) = promise { let _ = context.workbook.load(propertyNames=U2.Case2 (ResizeArray[|"tables"|])) @@ -196,7 +204,7 @@ let getPrevAnnotationTable (context:RequestContext) = let tables = context.workbook.tables let _ = tables.load(propertyNames=U2.Case2 (ResizeArray[|"items";"worksheet";"name"; "position"; "values"|])) - let! prevTable = context.sync().``then``(fun e -> + let! prevTable = context.sync().``then``(fun _ -> let activeWorksheetPosition = activeWorksheet.position /// Get all names of all tables in the whole workbook. let prevTable = @@ -224,45 +232,45 @@ let getPrevAnnotationTable (context:RequestContext) = // I subtract from the index of the current worksheet the indices of the other found worksheets with annotationTable. // I sort by the resulting lowest number (since the worksheet is then closest to the active one), I find the output column in the particular // annotationTable and use the values it contains for the new annotationTable in the active worksheet. -let getPrevTableOutput (context:RequestContext) = + +let getPrevTable (context:RequestContext) = promise { - let! prevTableName = getPrevAnnotationTable context + let! prevTableName = getPrevAnnotationTableName context if prevTableName.IsSome then + + let! result = ArcTable.arcTableFromExcelTableName(prevTableName.Value, context) + return result - let _, headerRange, bodyRowRange = MyHelper.getTableByName context prevTableName.Value - let! inMemoryTable = context.sync().``then``(fun _ -> - let headers = - headerRange.values.[0] - |> Seq.map (fun item -> - item - |> Option.map string - |> Option.defaultValue "" - ) - let bodyRows = - bodyRowRange.values - |> Seq.map (fun items -> - items - |> Seq.map (fun item -> - item - |> Option.map string - |> Option.defaultValue "" - ) - ) - ArcTable.fromStringSeqs(prevTableName.Value, headers, bodyRows) - ) + else - // Ref. 2 - let! buildingBlocks = BuildingBlockFunctions.getBuildingBlocks context prevTableName.Value + let! result = ArcTable.arcTableFromExcelTableName("", context) + return result + } + +let getPrevTableOutput (context:RequestContext) = + promise { + + let! inMemoryTable = getPrevTable context + + if inMemoryTable.IsSome then + + let outputColumns = inMemoryTable.Value.TryGetOutputColumn() + + if(outputColumns.IsSome) then + + let outputValues = + CompositeColumn.toStringCellColumns outputColumns.Value + |> (fun lists -> lists.Head.Head :: lists.Head.Tail) + |> Array.ofList - let outputCol = buildingBlocks |> Array.tryFind (fun x -> x.MainColumn.Header.isOutputCol) + if outputValues.Length > 0 then return outputValues + else return [||] - let values = - if outputCol.IsSome then - outputCol.Value.MainColumn.Cells - else [||] + else + + return [||] - return values else return [||] } @@ -270,7 +278,7 @@ let getPrevTableOutput (context:RequestContext) = /// This function is used to create a new annotation table. /// 'isDark' refers to the current styling of excel (darkmode, or not). let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, range:Excel.Range, context: RequestContext) = - + // This function is used to create the "next" annotationTable name. // 'allTableNames' is passed from a previous function and contains a list of all annotationTables. let rec findNewTableName allTableNames = @@ -291,7 +299,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra // The next part loads relevant information from the excel objects and allows us to access them after 'context.sync()' let tableRange = range.getColumn(0) - let _ = tableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount";"address"; "isEntireColumn"; "worksheet"]))) + let _ = tableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount"; "address"; "isEntireColumn"; "worksheet"]))) let activeSheet = tableRange.worksheet let _ = activeSheet.load(U2.Case2 (ResizeArray[|"tables"|])) @@ -303,16 +311,16 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra // Is user input signals to try and find+reuse the output from the previous annotationTable do this, otherwise just return empty array let! prevTableOutput = - if tryUseLastOutput then getPrevTableOutput context + if tryUseLastOutput then getPrevTableOutput context else promise {return Array.empty} // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. let useExistingPrevOutput = tryUseLastOutput && Array.isEmpty >> not <| prevTableOutput - + let! allTableNames = getAllTableNames context - + // sync with proxy objects after loading values from excel - let! table, newTableLogging = context.sync().``then``( fun _ -> + let! table = context.sync().``then``( fun _ -> // Filter all names of tables on the active worksheet for names starting with "annotationTable". let annoTables = @@ -343,7 +351,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let inMemoryTable = ArcTable.init(newName) let newCells = Array.init (int tableRange.rowCount - 1) (fun _ -> CompositeCell.emptyFreeText) - + inMemoryTable.AddColumn(CompositeHeader.Input IOType.Source, newCells) let tableStrings = inMemoryTable.ToExcelValues() @@ -357,13 +365,42 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra // Update annotationTable style annotationTable.style <- style - - if useExistingPrevOutput then - let newColValues = prevTableOutput |> Array.map (fun cell -> ResizeArray[|Option.bind (box >> Some) cell.Value|] ) |> ResizeArray - let col1 = (annotationTable.columns.getItemAt 0.) - let body = col1.getDataBodyRange() + annotationTable + ) + + let _ = table.rows.load(propertyNames = U2.Case2 (ResizeArray[|"count"|])) + + let newColValues = + prevTableOutput.[1..] + |> Array.map (fun cell -> + [|cell|] + |> Array.map (box >> Some) + |> ResizeArray + ) |> ResizeArray + + let! table, logging = context.sync().``then``(fun _ -> + //if useExistingPrevOutput then // This is the correct logic + if tryUseLastOutput then // this is only to make testing easier + log "Use prev output" + let rowCount0 = int table.rows.count + let diff = rowCount0 - newColValues.Count + log $"Diff: {diff}" + if diff > 0 then // table larger than values + log "Table larger than values" + // https://learn.microsoft.com/en-us/javascript/api/excel/excel.tablerowcollection?view=excel-js-preview#excel-excel-tablerowcollection-deleterowsat-member(1) + // https://fable.io/docs/javascript/features.html#dynamic-typing-proceed-with-caution + table.rows?deleteRowsAt(newColValues.Count, diff) + elif diff < 0 then // more values than table + let absolute = (-1) * diff + let nextvalues = createMatrixForTables 1 absolute "" + log ("More values than table: ", absolute) + table.rows.add(-1, U4.Case1 nextvalues) |> ignore + () + else + log "Perfect row size for prev values" + let body = (table.columns.getItemAt 0.).getDataBodyRange() body.values <- newColValues - + // Fit widths and heights of cols and rows to value size. (In this case the new column headers). activeSheet.getUsedRange().format.autofitColumns() activeSheet.getUsedRange().format.autofitRows() @@ -371,12 +408,12 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra r.enableEvents <- true // Return info message - let logging = InteropLogging.Msg.create InteropLogging.Info (sprintf "Annotation Table created in [%s] with dimensions 2c x (%.0f + 1h)r." tableRange.address (tableRange.rowCount - 1.)) + let logging = InteropLogging.Msg.create InteropLogging.Info (sprintf "Annotation Table created in [%s] with dimensions 2c x (%.0f + 1h)r." tableRange.address (6. - 1.)) - annotationTable, logging + table, logging ) - return (table,newTableLogging) + return (table, logging) } /// This function is used to create a new annotation table. @@ -521,7 +558,7 @@ let getBuildingBlocksAndSheets() = let tables = context.workbook.tables let _ = tables.load(propertyNames=U2.Case2 (ResizeArray[|"items";"worksheet";"name"; "values"|])) - let! worksheetAnnotationTableNames = context.sync().``then``(fun e -> + let! worksheetAnnotationTableNames = context.sync().``then``(fun _ -> /// Get all names of all tables in the whole workbook. let worksheetTableNames = tables.items @@ -622,7 +659,7 @@ let addAnnotationBlock (newBB:InsertBuildingBlock) = let selectedRange = context.workbook.getSelectedRange() let _ = selectedRange.load(U2.Case1 "columnIndex") - let! nextIndex, headerVals = context.sync().``then``(fun e -> + let! nextIndex, headerVals = context.sync().``then``(fun _ -> // This is necessary to place new columns next to selected col. let rebasedIndex = rebaseIndexToTable selectedRange annoHeaderRange @@ -716,7 +753,7 @@ let replaceOutputColumn (annotationTableName:string) (existingOutputColumn: Buil let _ = existingOutputColCell.load(U2.Case2 (ResizeArray[|"values"|])) let newHeaderValues = ResizeArray[|ResizeArray [|newOutputcolumn.ColumnHeader.toAnnotationTableHeader() |> box |> Some|]|] - let! change = context.sync().``then``(fun e -> + do! context.sync().``then``(fun _ -> existingOutputColCell.values <- newHeaderValues () ) @@ -831,7 +868,7 @@ let addAnnotationBlocksToTable (buildingBlocks:InsertBuildingBlock [], table:Tab let selectedRange = context.workbook.getSelectedRange() let _ = selectedRange.load(U2.Case1 "columnIndex") - let! startIndex, headerVals = context.sync().``then``(fun e -> + let! startIndex, headerVals = context.sync().``then``(fun _ -> // Ref. 3 /// This is necessary to place new columns next to selected col. let rebasedIndex = rebaseIndexToTable selectedRange annoHeaderRange @@ -867,7 +904,7 @@ let addAnnotationBlocksToTable (buildingBlocks:InsertBuildingBlock [], table:Tab let! expandedTable,expandedRowCount = if expandByNRows.IsSome then promise { - let! expandedTable,expandedTableRange = context.sync().``then``(fun e -> + let! expandedTable,expandedTableRange = context.sync().``then``(fun _ -> let newRowsValues = createMatrixForTables startColumnCount expandByNRows.Value "" let newRows = annotationTable.rows.add( @@ -1006,7 +1043,7 @@ let addAnnotationBlocksInNewSheet activateWorksheet (worksheetName:string,buildi let worksheetRange = newWorksheet.getUsedRange() - let! newTable,newTableLogging = createAnnotationTableAtRange (false,false,worksheetRange,context) + let! newTable, newTableLogging = createAnnotationTableAtRange (false, false, worksheetRange, context) let! addNewBuildingBlocksLogging = addAnnotationBlocksToTable(buildingBlocks, newTable, context) @@ -1052,7 +1089,7 @@ let updateUnitForCells (unitTerm:TermMinimal) = let! selectedBuildingBlock = OfficeInterop.BuildingBlockFunctions.findSelectedBuildingBlock context annotationTableName - let! headerVals = context.sync().``then``(fun e -> + let! headerVals = context.sync().``then``(fun _ -> // Get an array of the headers annoHeaderRange.values.[0] |> Array.ofSeq ) @@ -1127,11 +1164,10 @@ let removeAnnotationBlock (tableName:string) (annotationBlock:BuildingBlock) (co yield! refColIndices |] |> Array.sort - let! deleteCols = - context.sync().``then``(fun e -> + do! context.sync().``then``(fun _ -> targetedColIndices |> Array.map (fun targetIndex -> tableCols.items.[targetIndex].delete() - ) + ) |> ignore ) return targetedColIndices @@ -1322,8 +1358,7 @@ let insertOntologyTerm (term:TermMinimal) = let! tryTable = tryFindActiveAnnotationTable() // This function checks multiple scenarios destroying Swate table formatting through the insert ontology term function - let! checkCorrectInsertInSwateTable = - match tryTable with + do! match tryTable with | Success table -> promise { // Input column also affects the next 2 columns so [range.columnIndex; range.columnIndex+1.; range.columnIndex+2.] @@ -1687,12 +1722,11 @@ let deleteAllCustomXml() = promise { - let! getXml = - context.sync().``then``(fun e -> + do! context.sync().``then``(fun _ -> let items = customXmlParts.items let xmls = items |> Seq.map (fun x -> x.delete() ) - xmls |> Array.ofSeq + xmls |> Array.ofSeq |> ignore ) return "Info","Deleted All Custom Xml!" @@ -1709,7 +1743,7 @@ let getSwateCustomXml() = promise { let! getXml = - context.sync().``then``(fun e -> + context.sync().``then``(fun _ -> let items = customXmlParts.items let xmls = items |> Seq.map (fun x -> x.getXml() ) @@ -1717,7 +1751,7 @@ let getSwateCustomXml() = ) let! xml = - context.sync().``then``(fun e -> + context.sync().``then``(fun _ -> //let nOfItems = customXmlParts.items.Count let vals = getXml |> Array.map (fun x -> x.value) @@ -1739,17 +1773,15 @@ let updateSwateCustomXml(newXmlString:String) = promise { - let! deleteXml = - context.sync().``then``(fun e -> + do! context.sync().``then``(fun _ -> let items = customXmlParts.items let xmls = items |> Seq.map (fun x -> x.delete() ) - xmls |> Array.ofSeq + xmls |> Array.ofSeq |> ignore ) - let! addNext = - context.sync().``then``(fun e -> - customXmlParts.add(newXmlString) + do! context.sync().``then``(fun _ -> + customXmlParts.add(newXmlString) |> ignore ) return "Info", "Custom xml update successful" From 257eedb1992b3630128a88bbcbf2876d8ca91bb5 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 5 Jul 2024 14:49:56 +0200 Subject: [PATCH 10/25] Add tailwind support :sparkles: :art: #463 --- package-lock.json | 157 ++++++++++++---------------------- package.json | 13 +-- src/Client/Client.fs | 2 +- src/Client/postcss.config.js | 6 ++ src/Client/style.scss | 3 + src/Client/tailwind.config.js | 12 +++ src/Client/vite.config.mts | 5 +- 7 files changed, 86 insertions(+), 112 deletions(-) create mode 100644 src/Client/postcss.config.js create mode 100644 src/Client/tailwind.config.js diff --git a/package-lock.json b/package-lock.json index 315784ae..6c15efa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,8 +4,8 @@ "requires": true, "packages": { "": { + "name": "Swate", "dependencies": { - "@creativebulma/bulma-tooltip": "^1.2.0", "@nfdi4plants/exceljs": "^0.3.0", "bulma": "^1.0.1", "bulma-checkradio": "^2.1.3", @@ -23,13 +23,11 @@ "@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", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", "remotedev": "^0.2.7", "sass": "^1.69.5", - "selfsigned": "^2.4.1", - "tailwindcss": "^3.3.6", + "tailwindcss": "^3.4.4", "vite": "^5.0.5" }, "engines": { @@ -394,11 +392,6 @@ "node": ">=6.9.0" } }, - "node_modules/@creativebulma/bulma-tooltip": { - "version": "1.2.0", - "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", @@ -1197,15 +1190,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", @@ -1355,12 +1339,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", @@ -1715,9 +1693,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": { @@ -1745,17 +1723,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/core-js": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", - "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", - "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", @@ -2695,15 +2662,6 @@ } } }, - "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.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -2849,9 +2807,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "dev": true, "funding": [ { @@ -2869,7 +2827,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -3126,15 +3084,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": { @@ -3288,18 +3246,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": { + "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.1" + "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": { @@ -3316,19 +3284,6 @@ "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", @@ -3377,21 +3332,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": { @@ -3404,16 +3358,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", @@ -3753,6 +3697,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", @@ -4070,14 +4020,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 a48e3a1b..495d3f81 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,5 @@ { - "browserslist": [ - "ie 11" - ], "private": true, - "type": "module", "engines": { "node": "~18 || ~20", "npm": "~9 || ~10" @@ -13,17 +9,14 @@ "@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", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", "remotedev": "^0.2.7", "sass": "^1.69.5", - "selfsigned": "^2.4.1", - "tailwindcss": "^3.3.6", + "tailwindcss": "^3.4.4", "vite": "^5.0.5" }, "dependencies": { - "@creativebulma/bulma-tooltip": "^1.2.0", "@nfdi4plants/exceljs": "^0.3.0", "bulma": "^1.0.1", "bulma-checkradio": "^2.1.3", diff --git a/src/Client/Client.fs b/src/Client/Client.fs index e46dbb82..7ee323ee 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -7,7 +7,7 @@ open Messages open Model open Update open Fable.Core.JsInterop -let _ = importSideEffects "./style.scss" +importSideEffects "./style.scss" /// This is a basic test case used in Client unit tests let sayHello name = $"Hello {name}" diff --git a/src/Client/postcss.config.js b/src/Client/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/src/Client/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/Client/style.scss b/src/Client/style.scss index a6cf5e76..908a1806 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -16,6 +16,9 @@ $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"; @use "bulma/sass/utilities/css-variables" as cv; +@tailwind base; +@tailwind components; +@tailwind utilities; .my-grey-out { background-color: cv.getVar("scheme-main-ter"); diff --git a/src/Client/tailwind.config.js b/src/Client/tailwind.config.js new file mode 100644 index 00000000..a9c22a79 --- /dev/null +++ b/src/Client/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: "jit", + content: [ + "./index.html", + "./**/*.{fs,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [] +} diff --git a/src/Client/vite.config.mts b/src/Client/vite.config.mts index fc6ff3f5..03bc6731 100644 --- a/src/Client/vite.config.mts +++ b/src/Client/vite.config.mts @@ -28,6 +28,9 @@ export default defineConfig({ // target: proxyTarget, // ws: true, //}, - } + }, + watch: { + ignored: ["**/*.fs"] + }, }, }); \ No newline at end of file From 49c068b85409cdc0f50085fe74d8fcf7d85b4413 Mon Sep 17 00:00:00 2001 From: patrick blume Date: Mon, 8 Jul 2024 08:54:24 +0200 Subject: [PATCH 11/25] Adapt namings and descriptions --- src/Client/OfficeInterop/OfficeInterop.fs | 76 +++++++++++------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index 6bf8681f..c9ac22e3 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -16,33 +16,41 @@ open BuildingBlockFunctions open ARCtrl open ARCtrl.Spreadsheet -module MyHelper = - - let getTableByName (context:RequestContext) (tableName:string) = - let _ = context.workbook.load(U2.Case1 "tables") - let annotationTable = context.workbook.tables.getItem(tableName) - let annoHeaderRange = annotationTable.getHeaderRowRange() - let _ = annoHeaderRange.load(U2.Case2 (ResizeArray [|"columnIndex"; "values"; "columnCount"|])) |> ignore - let annoBodyRange = annotationTable.getDataBodyRange() - let _ = annoBodyRange.load(U2.Case2 (ResizeArray [|"values"; "numberFormat"|])) |> ignore - annotationTable, annoHeaderRange, annoBodyRange - - /// Swaps 'Rows with column values' to 'Columns with row values'. - let viewRowsByColumns (rows:ResizeArray>) = - rows - |> Seq.collect (fun x -> Seq.indexed x) - |> Seq.groupBy fst - |> Seq.map (snd >> Seq.map snd >> Seq.toArray) - |> Seq.toArray - module OfficeInteropExtensions = open ARCtrl.Spreadsheet.ArcTable + type ExcelHelper = + + /// + /// Get the excel table of the given context and name + /// + /// + /// + static member getTableByName (context:RequestContext) (tableName:string) = + let _ = context.workbook.load(U2.Case1 "tables") + let annotationTable = context.workbook.tables.getItem(tableName) + let annoHeaderRange = annotationTable.getHeaderRowRange() + let _ = annoHeaderRange.load(U2.Case2 (ResizeArray [|"columnIndex"; "values"; "columnCount"|])) |> ignore + let annoBodyRange = annotationTable.getDataBodyRange() + let _ = annoBodyRange.load(U2.Case2 (ResizeArray [|"values"; "numberFormat"|])) |> ignore + annotationTable, annoHeaderRange, annoBodyRange + + /// + /// Swaps 'Rows with column values' to 'Columns with row values' + /// + /// + static member viewRowsByColumns (rows:ResizeArray>) = + rows + |> Seq.collect (fun x -> Seq.indexed x) + |> Seq.groupBy fst + |> Seq.map (snd >> Seq.map snd >> Seq.toArray) + |> Seq.toArray + type ArcTable with /// - /// WIP + /// Creates ArcTable based on table name and collections of strings, representing columns and rows /// /// /// @@ -58,7 +66,6 @@ module OfficeInteropExtensions = |> Seq.toArray |> Array.map (Seq.toArray) - let compositeColumns = ArcTable.composeColumns columnsList let arcTable = @@ -95,7 +102,7 @@ module OfficeInteropExtensions = static member arcTableFromExcelTableName (tableName:string, context:RequestContext) = - let _, headerRange, bodyRowRange = MyHelper.getTableByName context tableName + let _, headerRange, bodyRowRange = ExcelHelper.getTableByName context tableName promise { let! inMemoryTable = context.sync().``then``(fun _ -> let headers = @@ -118,9 +125,7 @@ module OfficeInteropExtensions = ArcTable.fromStringSeqs(tableName, headers, bodyRows) ) return inMemoryTable - } - - let x = 0 + } open OfficeInteropExtensions @@ -370,6 +375,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let _ = table.rows.load(propertyNames = U2.Case2 (ResizeArray[|"count"|])) + //Skip header because it is newly generated for inMemory table let newColValues = prevTableOutput.[1..] |> Array.map (fun cell -> @@ -379,25 +385,19 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra ) |> ResizeArray let! table, logging = context.sync().``then``(fun _ -> - //if useExistingPrevOutput then // This is the correct logic - if tryUseLastOutput then // this is only to make testing easier - log "Use prev output" + + //logic to compare size of previous table and current table and adapt size of inMemory table + if useExistingPrevOutput then let rowCount0 = int table.rows.count let diff = rowCount0 - newColValues.Count - log $"Diff: {diff}" - if diff > 0 then // table larger than values - log "Table larger than values" - // https://learn.microsoft.com/en-us/javascript/api/excel/excel.tablerowcollection?view=excel-js-preview#excel-excel-tablerowcollection-deleterowsat-member(1) - // https://fable.io/docs/javascript/features.html#dynamic-typing-proceed-with-caution + + if diff > 0 then // table larger than values -> Delete rows to reduce inMemoryTable size to previous table size table.rows?deleteRowsAt(newColValues.Count, diff) - elif diff < 0 then // more values than table + elif diff < 0 then // more values than table -> Add rows to increase inMemoryTable size to previous table size let absolute = (-1) * diff let nextvalues = createMatrixForTables 1 absolute "" - log ("More values than table: ", absolute) table.rows.add(-1, U4.Case1 nextvalues) |> ignore - () - else - log "Perfect row size for prev values" + let body = (table.columns.getItemAt 0.).getDataBodyRange() body.values <- newColValues From 56828cf4974dbb1537d9acc6daae1fb497a64d7a Mon Sep 17 00:00:00 2001 From: patrick blume Date: Mon, 8 Jul 2024 09:47:26 +0200 Subject: [PATCH 12/25] Applied review requests --- src/Client/OfficeInterop/OfficeInterop.fs | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index c9ac22e3..acf508e7 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -100,7 +100,7 @@ module OfficeInteropExtensions = columns - static member arcTableFromExcelTableName (tableName:string, context:RequestContext) = + static member fromExcelTableName (tableName:string, context:RequestContext) = let _, headerRange, bodyRowRange = ExcelHelper.getTableByName context tableName promise { @@ -238,25 +238,32 @@ let getPrevAnnotationTableName (context:RequestContext) = // I sort by the resulting lowest number (since the worksheet is then closest to the active one), I find the output column in the particular // annotationTable and use the values it contains for the new annotationTable in the active worksheet. -let getPrevTable (context:RequestContext) = +/// +/// Get the previous arc table to the active worksheet +/// +/// +let tryGetPrevTable (context:RequestContext) = promise { let! prevTableName = getPrevAnnotationTableName context if prevTableName.IsSome then - let! result = ArcTable.arcTableFromExcelTableName(prevTableName.Value, context) + let! result = ArcTable.fromExcelTableName (prevTableName.Value, context) return result else - let! result = ArcTable.arcTableFromExcelTableName("", context) - return result + return None } +/// +/// Get output column of arc excel table +/// +/// let getPrevTableOutput (context:RequestContext) = promise { - let! inMemoryTable = getPrevTable context + let! inMemoryTable = tryGetPrevTable context if inMemoryTable.IsSome then @@ -391,9 +398,9 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let rowCount0 = int table.rows.count let diff = rowCount0 - newColValues.Count - if diff > 0 then // table larger than values -> Delete rows to reduce inMemoryTable size to previous table size + if diff > 0 then // table larger than values -> Delete rows to reduce excel table size to previous table size table.rows?deleteRowsAt(newColValues.Count, diff) - elif diff < 0 then // more values than table -> Add rows to increase inMemoryTable size to previous table size + elif diff < 0 then // more values than table -> Add rows to increase excel table size to previous table size let absolute = (-1) * diff let nextvalues = createMatrixForTables 1 absolute "" table.rows.add(-1, U4.Case1 nextvalues) |> ignore @@ -408,7 +415,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra r.enableEvents <- true // Return info message - let logging = InteropLogging.Msg.create InteropLogging.Info (sprintf "Annotation Table created in [%s] with dimensions 2c x (%.0f + 1h)r." tableRange.address (6. - 1.)) + let logging = InteropLogging.Msg.create InteropLogging.Info (sprintf "Annotation Table created in [%s] with dimensions 2c x (%.0f + 1h)r." tableRange.address (tableRange.rowCount - 1.)) table, logging ) From c668f83329080f583386fe925a24083936502b9e Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 9 Jul 2024 09:24:50 +0200 Subject: [PATCH 13/25] Applied early exit Create new worksheet when one is already available --- src/Client/OfficeInterop/OfficeInterop.fs | 75 +++++++++++++++-------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index acf508e7..d6c7da8a 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -313,26 +313,19 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let tableRange = range.getColumn(0) let _ = tableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount"; "address"; "isEntireColumn"; "worksheet"]))) - let activeSheet = tableRange.worksheet + let mutable activeSheet = tableRange.worksheet let _ = activeSheet.load(U2.Case2 (ResizeArray[|"tables"|])) let activeTables = activeSheet.tables.load(propertyNames=U2.Case1 "items") let r = context.runtime.load(U2.Case1 "enableEvents") - - promise { - - // Is user input signals to try and find+reuse the output from the previous annotationTable do this, otherwise just return empty array - let! prevTableOutput = - if tryUseLastOutput then getPrevTableOutput context - else promise {return Array.empty} - - // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. - let useExistingPrevOutput = tryUseLastOutput && Array.isEmpty >> not <| prevTableOutput - let! allTableNames = getAllTableNames context + //Required because a new tablerange is required for the new table range + let mutable hasCreatedNewWorkSheet = false + + promise { // sync with proxy objects after loading values from excel - let! table = context.sync().``then``( fun _ -> + do! context.sync().``then``( fun _ -> // Filter all names of tables on the active worksheet for names starting with "annotationTable". let annoTables = @@ -343,14 +336,44 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra // Fail the function if there are not exactly 0 annotation tables in the active worksheet. // This check is done, to only have one annotationTable per workSheet. - let _ = - match annoTables.Length with - | x when x > 0 -> - failwith "The active worksheet contains more than zero annotationTables. Please move to a new worksheet." - | 0 -> - () - | _ -> - failwith "The active worksheet contains a negative number of annotation tables. Obviously this cannot happen. Please report this as a bug to the developers." + match annoTables.Length with + | 0 -> + () + | x when x = 1 -> + //Create new worksheet and set it active + context.workbook.worksheets.add() |> ignore + let _ = context.workbook.load(propertyNames=U2.Case2 (ResizeArray[|"tables"|])) + let lastWorkSheet = context.workbook.worksheets.getLast() + lastWorkSheet.activate() + activeSheet <- lastWorkSheet + hasCreatedNewWorkSheet <- true + | x when x > 1 -> + failwith "The active worksheet contains more than one annotationTable. This should not happen. Please report this as a bug to the developers." + | _ -> + failwith "The active worksheet contains a negative number of annotation tables. Obviously this cannot happen. Please report this as a bug to the developers." + ) + + // Is user input signals to try and find+reuse the output from the previous annotationTable do this, otherwise just return empty array + let! prevTableOutput = + if (tryUseLastOutput) then getPrevTableOutput context + else promise {return Array.empty} + + // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. + let useExistingPrevOutput = (tryUseLastOutput) && Array.isEmpty >> not <| prevTableOutput + + let! allTableNames = getAllTableNames context + + let _ = activeSheet.load(propertyNames = U2.Case2 (ResizeArray[|"name"|])) + + let newTableRange = + if hasCreatedNewWorkSheet then activeSheet.getCell(tableRange.rowIndex, tableRange.columnIndex) + else tableRange + let _ = + if hasCreatedNewWorkSheet then newTableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount"; "address"; "isEntireColumn"; "worksheet"]))) + else tableRange + + // sync with proxy objects after loading values from excel + let! table = context.sync().``then``( fun _ -> // Ref. 1 r.enableEvents <- false @@ -368,18 +391,18 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let tableStrings = inMemoryTable.ToExcelValues() - let annotationTable = activeSheet.tables.add(U2.Case1 tableRange, true) + let annotationTable = activeSheet.tables.add(U2.Case1 newTableRange, true) // Update annotationTable name annotationTable.name <- newName - tableRange.values <- tableStrings + newTableRange.values <- tableStrings // Update annotationTable style annotationTable.style <- style annotationTable ) - + let _ = table.rows.load(propertyNames = U2.Case2 (ResizeArray[|"count"|])) //Skip header because it is newly generated for inMemory table @@ -413,9 +436,9 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra activeSheet.getUsedRange().format.autofitRows() r.enableEvents <- true - + // Return info message - let logging = InteropLogging.Msg.create InteropLogging.Info (sprintf "Annotation Table created in [%s] with dimensions 2c x (%.0f + 1h)r." tableRange.address (tableRange.rowCount - 1.)) + let logging = InteropLogging.Msg.create InteropLogging.Info (sprintf "Annotation Table created in [%s] with dimensions 2c x (%.0f + 1h)r." newTableRange.address (newTableRange.rowCount - 1.)) table, logging ) From 77601e83e8d856a2dd8232176ffa03bc689ede91 Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 9 Jul 2024 09:51:15 +0200 Subject: [PATCH 14/25] Applied review changes --- src/Client/OfficeInterop/OfficeInterop.fs | 47 ++++++++++++----------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index d6c7da8a..bf9edadd 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -260,7 +260,7 @@ let tryGetPrevTable (context:RequestContext) = /// Get output column of arc excel table /// /// -let getPrevTableOutput (context:RequestContext) = +let tryGetPrevTableOutput (context:RequestContext) = promise { let! inMemoryTable = tryGetPrevTable context @@ -276,15 +276,15 @@ let getPrevTableOutput (context:RequestContext) = |> (fun lists -> lists.Head.Head :: lists.Head.Tail) |> Array.ofList - if outputValues.Length > 0 then return outputValues - else return [||] + if outputValues.Length > 0 then return Some outputValues + else return None else - return [||] + return None else - return [||] + return None } /// This function is used to create a new annotation table. @@ -334,19 +334,20 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra |> Array.map (fun x -> x.name) |> Array.filter (fun x -> x.StartsWith "annotationTable") - // Fail the function if there are not exactly 0 annotation tables in the active worksheet. - // This check is done, to only have one annotationTable per workSheet. match annoTables.Length with + //Create a new annotation table in the active worksheet | 0 -> () + //Create a mew worksheet with a new annotation table when the active worksheet already contains one | x when x = 1 -> //Create new worksheet and set it active context.workbook.worksheets.add() |> ignore - let _ = context.workbook.load(propertyNames=U2.Case2 (ResizeArray[|"tables"|])) let lastWorkSheet = context.workbook.worksheets.getLast() lastWorkSheet.activate() activeSheet <- lastWorkSheet hasCreatedNewWorkSheet <- true + // Fail the function if there are more than 1 annotation table in the active worksheet. + // This check is done, to only have one annotationTable per workSheet. | x when x > 1 -> failwith "The active worksheet contains more than one annotationTable. This should not happen. Please report this as a bug to the developers." | _ -> @@ -355,20 +356,20 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra // Is user input signals to try and find+reuse the output from the previous annotationTable do this, otherwise just return empty array let! prevTableOutput = - if (tryUseLastOutput) then getPrevTableOutput context - else promise {return Array.empty} + if (tryUseLastOutput) then tryGetPrevTableOutput context + else promise {return None} // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. - let useExistingPrevOutput = (tryUseLastOutput) && Array.isEmpty >> not <| prevTableOutput - + let useExistingPrevOutput = (tryUseLastOutput) && prevTableOutput.IsSome + log("useExistingPrevOutput", useExistingPrevOutput) let! allTableNames = getAllTableNames context - let _ = activeSheet.load(propertyNames = U2.Case2 (ResizeArray[|"name"|])) - let newTableRange = if hasCreatedNewWorkSheet then activeSheet.getCell(tableRange.rowIndex, tableRange.columnIndex) else tableRange + let _ = + activeSheet.load(propertyNames = U2.Case2 (ResizeArray[|"name"|])) |> ignore if hasCreatedNewWorkSheet then newTableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount"; "address"; "isEntireColumn"; "worksheet"]))) else tableRange @@ -405,19 +406,19 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let _ = table.rows.load(propertyNames = U2.Case2 (ResizeArray[|"count"|])) - //Skip header because it is newly generated for inMemory table - let newColValues = - prevTableOutput.[1..] - |> Array.map (fun cell -> - [|cell|] - |> Array.map (box >> Some) - |> ResizeArray - ) |> ResizeArray - let! table, logging = context.sync().``then``(fun _ -> //logic to compare size of previous table and current table and adapt size of inMemory table if useExistingPrevOutput then + //Skip header because it is newly generated for inMemory table + let newColValues = + prevTableOutput.Value.[1..] + |> Array.map (fun cell -> + [|cell|] + |> Array.map (box >> Some) + |> ResizeArray + ) |> ResizeArray + let rowCount0 = int table.rows.count let diff = rowCount0 - newColValues.Count From 62b1c673fd832edaaedf497592fe6397041e0338 Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 9 Jul 2024 10:13:13 +0200 Subject: [PATCH 15/25] Applied review changes --- src/Client/OfficeInterop/OfficeInterop.fs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index bf9edadd..26b24584 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -360,17 +360,16 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra else promise {return None} // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. - let useExistingPrevOutput = (tryUseLastOutput) && prevTableOutput.IsSome + let useExistingPrevOutput = prevTableOutput.IsSome log("useExistingPrevOutput", useExistingPrevOutput) let! allTableNames = getAllTableNames context - let newTableRange = - if hasCreatedNewWorkSheet then activeSheet.getCell(tableRange.rowIndex, tableRange.columnIndex) - else tableRange + let _ = activeSheet.load(propertyNames = U2.Case2 (ResizeArray[|"name"|])) |> ignore - let _ = - activeSheet.load(propertyNames = U2.Case2 (ResizeArray[|"name"|])) |> ignore - if hasCreatedNewWorkSheet then newTableRange.load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount"; "address"; "isEntireColumn"; "worksheet"]))) + let newTableRange = + if hasCreatedNewWorkSheet then + activeSheet.getCell(tableRange.rowIndex, tableRange.columnIndex) + .load(U2.Case2 (ResizeArray(["rowIndex"; "columnIndex"; "rowCount"; "address"; "isEntireColumn"; "worksheet"]))) else tableRange // sync with proxy objects after loading values from excel From 2c54f08656fd325dce779791d0c89e59a7db1ea5 Mon Sep 17 00:00:00 2001 From: patrick blume Date: Tue, 9 Jul 2024 10:15:13 +0200 Subject: [PATCH 16/25] applied review changes --- src/Client/OfficeInterop/OfficeInterop.fs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index 26b24584..6a9f57da 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -359,9 +359,6 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra if (tryUseLastOutput) then tryGetPrevTableOutput context else promise {return None} - // If try to use last output check if we found some output in "prevTableOutput" by checking if the array is not empty. - let useExistingPrevOutput = prevTableOutput.IsSome - log("useExistingPrevOutput", useExistingPrevOutput) let! allTableNames = getAllTableNames context let _ = activeSheet.load(propertyNames = U2.Case2 (ResizeArray[|"name"|])) |> ignore @@ -408,7 +405,7 @@ let private createAnnotationTableAtRange (isDark:bool, tryUseLastOutput:bool, ra let! table, logging = context.sync().``then``(fun _ -> //logic to compare size of previous table and current table and adapt size of inMemory table - if useExistingPrevOutput then + if prevTableOutput.IsSome then //Skip header because it is newly generated for inMemory table let newColValues = prevTableOutput.Value.[1..] From febeeb8a3b57a2d5d49f436bacf3b53788bcca6d Mon Sep 17 00:00:00 2001 From: Kevin F Date: Tue, 9 Jul 2024 23:24:42 +0200 Subject: [PATCH 17/25] fix button spacing in reset table modal --- src/Client/Modals/ResetTable.fs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Client/Modals/ResetTable.fs b/src/Client/Modals/ResetTable.fs index 1737e490..d0146ea0 100644 --- a/src/Client/Modals/ResetTable.fs +++ b/src/Client/Modals/ResetTable.fs @@ -27,15 +27,20 @@ let Main (dispatch) (rmv: _ -> unit) = Bulma.field.div [prop.innerHtml "If you only want to delete one sheet, right-click the sheet at the bottom and select `delete`"] ] Bulma.modalCardFoot [ - Bulma.button.a [ - prop.onClick rmv - Bulma.color.isInfo - prop.text "Back" - ] - Bulma.button.a [ - prop.onClick reset - Bulma.color.isDanger - prop.text "Delete" + Bulma.buttons [ + prop.className "grow justify-between" + prop.children [ + Bulma.button.a [ + prop.onClick rmv + Bulma.color.isInfo + prop.text "Back" + ] + Bulma.button.a [ + prop.onClick reset + Bulma.color.isDanger + prop.text "Delete" + ] + ] ] ] ] From 29c421e33d2d617608cc3cb2e5957e34d6053d69 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 16:37:41 +0200 Subject: [PATCH 18/25] Add first implementation of DataMap :sparkles: :construction: #465 --- .db/docker-compose.yml | 5 +- .db/docker.compose.new.yml | 7 +- src/Client/Client.fsproj | 11 +- src/Client/Helper.fs | 55 +++- src/Client/LocalStorage/Darkmode.fs | 1 - src/Client/MainComponents/CellStyles.fs | 88 ++++++ src/Client/MainComponents/Cells.fs | 168 +++++------ src/Client/MainComponents/DataMap/Cells.fs | 31 ++ src/Client/MainComponents/DataMap/DataMap.fs | 183 ++++++++++++ src/Client/MainComponents/FooterTabs.fs | 279 +++++++++++++++--- src/Client/MainComponents/Metadata/Assay.fs | 25 ++ .../MainComponents/Metadata/DatamapConfig.fs | 50 ++++ src/Client/MainComponents/Metadata/Study.fs | 5 + src/Client/MainComponents/NoFileElement.fs | 2 +- src/Client/MainComponents/SpreadsheetView.fs | 35 +-- src/Client/Modals/MoveColumn.fs | 1 - src/Client/OfficeInterop/OfficeInterop.fs | 5 - src/Client/Pages/BuildingBlock/Dropdown.fs | 37 ++- .../Pages/BuildingBlock/SearchComponent.fs | 2 +- src/Client/Pages/Cytoscape/CytoscapeGraph.fs | 2 +- .../Pages/ProtocolTemplates/TemplateFromDB.fs | 2 +- src/Client/SharedComponents/AdvancedSearch.fs | 36 +-- .../SharedComponents/TermSearchInput.fs | 43 ++- .../BuildingBlocks.fs} | 3 +- .../Clipboard.fs} | 5 +- src/Client/Spreadsheet/Controller/DataMap.fs | 43 +++ .../Table.fs} | 10 +- src/Client/States/LocalHistory.fs | 4 +- src/Client/States/Spreadsheet.fs | 18 +- src/Client/States/SpreadsheetInterface.fs | 2 + src/Client/Update/InterfaceUpdate.fs | 19 ++ src/Client/Update/SpreadsheetUpdate.fs | 77 ++--- src/Client/Views/MainWindowView.fs | 4 +- src/Client/Views/SplitWindowView.fs | 3 +- src/Client/Views/XlsxFileView.fs | 4 +- src/Client/style.scss | 8 + src/Shared/ARCtrl.Helper.fs | 55 +++- src/Shared/Shared.fsproj | 14 +- 38 files changed, 1043 insertions(+), 299 deletions(-) create mode 100644 src/Client/MainComponents/CellStyles.fs create mode 100644 src/Client/MainComponents/DataMap/Cells.fs create mode 100644 src/Client/MainComponents/DataMap/DataMap.fs create mode 100644 src/Client/MainComponents/Metadata/DatamapConfig.fs rename src/Client/Spreadsheet/{BuildingBlocks.Controller.fs => Controller/BuildingBlocks.fs} (98%) rename src/Client/Spreadsheet/{Clipboard.Controller.fs => Controller/Clipboard.fs} (98%) create mode 100644 src/Client/Spreadsheet/Controller/DataMap.fs rename src/Client/Spreadsheet/{Table.Controller.fs => Controller/Table.fs} (96%) diff --git a/.db/docker-compose.yml b/.db/docker-compose.yml index 04303408..402788bb 100644 --- a/.db/docker-compose.yml +++ b/.db/docker-compose.yml @@ -19,7 +19,7 @@ services: networks: - swobup_network environment: - - NEO4J_AUTH=neo4j/test + - NEO4J_AUTH=neo4j/testing12345 - NEO4J_server_memory_pagecache_size=7G - NEO4J_server_memory_heap_initial__size=5G - NEO4J_server_memory_heap_max__size=5G @@ -29,7 +29,6 @@ services: - NEO4J_PLUGINS=["graph-data-science", "apoc"] - NEO4J_dbms_security_procedures_unrestricted=algo.*, apoc.* - NEO4J_server_jvm_additional='-XX:+ExitOnOutOfMemoryError' - - NEO4J_dbms_security_auth__minimum__password__length=4 volumes: - ./neo4j/data:/data - ./neo4j/logs:/logs @@ -46,7 +45,7 @@ services: - GITHUB_SECRET=test - DB_URL=bolt://neo4j:7687 - DB_USER=neo4j - - DB_PASSWORD=test + - DB_PASSWORD=testing12345 - ONTOLOGY_REPOSITORY=nfdi4plants/nfdi4plants_ontology - TEMPLATE_REPOSITORY=nfdi4plants/SWATE_templates - SWATE_API=https://swate-alpha.nfdi4plants.org diff --git a/.db/docker.compose.new.yml b/.db/docker.compose.new.yml index 7d598de7..5b7c35b5 100644 --- a/.db/docker.compose.new.yml +++ b/.db/docker.compose.new.yml @@ -24,7 +24,7 @@ services: networks: - swobup_network environment: - - NEO4J_AUTH=neo4j/test + - NEO4J_AUTH=neo4j/testing12345 - NEO4J_server_memory_pagecache_size=7G - NEO4J_server_memory_heap_initial__size=5G - NEO4J_server_memory_heap_max__size=5G @@ -34,7 +34,6 @@ services: - NEO4J_PLUGINS=["graph-data-science", "apoc"] - NEO4J_dbms_security_procedures_unrestricted=algo.*, apoc.* - NEO4J_server_jvm_additional='-XX:+ExitOnOutOfMemoryError' - - NEO4J_dbms_security_auth__minimum__password__length=4 volumes: - ./neo4j/data:/data - ./neo4j/logs:/logs @@ -53,7 +52,7 @@ services: - swobup_network environment: - DB_USER=neo4j - - DB_PASSWORD=test + - DB_PASSWORD=testing12345 - DB_URL=bolt://neo4j:7687 - DB_NAME=neo4j @@ -68,7 +67,7 @@ services: - GITHUB_SECRET=test - DB_URL=bolt://neo4j:7687 - DB_USER=neo4j - - DB_PASSWORD=test + - DB_PASSWORD=testing12345 - ONTOLOGY_REPOSITORY=nfdi4plants/nfdi4plants_ontology - TEMPLATE_REPOSITORY=nfdi4plants/SWATE_templates - SWATE_API=https://swate-alpha.nfdi4plants.org diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index a4b88da5..fbdb1c33 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -53,9 +53,10 @@ - - - + + + + @@ -83,9 +84,11 @@ + + @@ -96,6 +99,8 @@ + + diff --git a/src/Client/Helper.fs b/src/Client/Helper.fs index 5e43dcb5..0abdbcd2 100644 --- a/src/Client/Helper.fs +++ b/src/Client/Helper.fs @@ -1,21 +1,56 @@ -[] +[] module Helper open Fable.Core let log (a) = Browser.Dom.console.log a +let logw (a) = Browser.Dom.console.warn a + let logf a b = let txt : string = sprintf a b log txt open System.Collections.Generic -let debounce<'T> (storage:Dictionary) (key: string) (timeout: int) (fn: 'T -> unit) value = +type DebounceStorage() = + let mutable _storage = Dictionary(HashIdentity.Structural) + let mutable _fnStorage = Dictionary unit>() + + member this.Add(key, timeoutId, ?fn: unit -> unit) = + _storage.[key] <- timeoutId + if fn.IsSome then + _fnStorage.[key] <- fn.Value + + member this.TryGetValue(key) = + match _storage.TryGetValue(key) with + | true, timeoutId -> Some timeoutId + | _ -> None + + member this.Remove(key) = + _storage.Remove(key) |> ignore + _fnStorage.Remove(key) |> ignore + + member this.ClearAndRun() = + for kv in _storage do + Fable.Core.JS.clearTimeout kv.Value + match _fnStorage.TryGetValue kv.Key with + | true, fn -> fn () + | _ -> () + _storage.Clear() + _fnStorage.Clear() + + member this.Clear() = + for kv in _storage do + Fable.Core.JS.clearTimeout kv.Value + _storage.Clear() + _fnStorage.Clear() + +let debounce<'T> (storage:DebounceStorage) (key: string) (timeout: int) (fn: 'T -> unit) value = let key = key // fn.ToString() // Cancel previous debouncer match storage.TryGetValue(key) with - | true, timeoutId -> Fable.Core.JS.clearTimeout timeoutId + | Some timeoutId -> Fable.Core.JS.clearTimeout timeoutId | _ -> () // Create a new timeout and memoize it @@ -26,13 +61,13 @@ let debounce<'T> (storage:Dictionary) (key: string) (timeout: int) fn value ) timeout - storage.[key] <- timeoutId + storage.Add(key, timeoutId, fun () -> fn value) -let debouncel<'T> (storage:Dictionary) (key: string) (timeout: int) (setLoading: bool -> unit) (fn: 'T -> unit) value = +let debouncel<'T> (storage:DebounceStorage) (key: string) (timeout: int) (setLoading: bool -> unit) (fn: 'T -> unit) value = let key = key // fn.ToString() // Cancel previous debouncer match storage.TryGetValue(key) with - | true, timeoutId -> Fable.Core.JS.clearTimeout timeoutId + | Some timeoutId -> Fable.Core.JS.clearTimeout timeoutId | _ -> setLoading true; () // Create a new timeout and memoize it @@ -40,17 +75,17 @@ let debouncel<'T> (storage:Dictionary) (key: string) (timeout: int) Fable.Core.JS.setTimeout (fun () -> match storage.TryGetValue key with - | true, _ -> + | Some _ -> storage.Remove(key) |> ignore setLoading false fn value - | false, _ -> + | None -> setLoading false ) timeout - storage.[key] <- timeoutId + storage.Add(key, timeoutId, fun () -> fn value) -let newDebounceStorage = fun () -> Dictionary(HashIdentity.Structural) +let newDebounceStorage = fun () -> DebounceStorage() type Clipboard = abstract member writeText: string -> JS.Promise diff --git a/src/Client/LocalStorage/Darkmode.fs b/src/Client/LocalStorage/Darkmode.fs index 87fff96d..aa4672c9 100644 --- a/src/Client/LocalStorage/Darkmode.fs +++ b/src/Client/LocalStorage/Darkmode.fs @@ -19,7 +19,6 @@ module private Attribute = module private BrowserSetting = let getDefault() = let m : bool = Browser.Dom.window?matchMedia("(prefers-color-scheme: dark)")?matches - // Browser.Dom.console.log(m) if m then Dark else Light [] diff --git a/src/Client/MainComponents/CellStyles.fs b/src/Client/MainComponents/CellStyles.fs new file mode 100644 index 00000000..39258e6e --- /dev/null +++ b/src/Client/MainComponents/CellStyles.fs @@ -0,0 +1,88 @@ +/// Styling here is shared between datamap and annotationTable view +module MainComponents.CellStyles + +open ARCtrl +open Feliz +open Feliz.Bulma +open Fable.Core + +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.alignItems.center + yield! specificStyle + ] + +let basicValueDisplayCell (v: string) = + Html.span [ + prop.style [ + style.flexGrow 1 + style.padding(length.em 0.5,length.em 0.75) + ] + prop.text v + ] + +let compositeCellDisplay (oa: OntologyAnnotation) (displayValue: string) = + let hasValidOA = oa.TermAccessionShort <> "" + let v = displayValue + 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 + ] + if hasValidOA then + Bulma.icon [Html.i [ + prop.style [style.custom("marginLeft", "auto")] + prop.className ["fa-solid"; "fa-check"] + ]] + ] + ] + +/// +/// rowIndex < 0 equals header +/// +/// +let 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.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 [ + 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}") + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index 22e6d3d7..f58e4fd2 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -2,6 +2,7 @@ module Spreadsheet.Cells open Feliz open Feliz.Bulma +open Fable.Core open Spreadsheet open MainComponents @@ -13,57 +14,6 @@ open Model module private CellComponents = - - 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.alignItems.center - yield! specificStyle - ] - - let basicValueDisplayCell (v: string) = - Html.span [ - prop.style [ - style.flexGrow 1 - style.padding(length.em 0.5,length.em 0.75) - ] - prop.text v - ] - - let compositeCellDisplay (cc: CompositeCell) = - 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"] - 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 - ] - if hasValidOA then - Bulma.icon [Html.i [ - prop.style [style.custom("marginLeft", "auto")] - prop.className ["fa-solid"; "fa-check"] - ]] - ] - ] - let extendHeaderButton (state_extend: Set, columnIndex, setState_extend) = let isExtended = state_extend.Contains(columnIndex) Bulma.icon [ @@ -132,7 +82,7 @@ module private EventPresets = let next = if selectedCells = Set([index]) then Set.empty else Set([index]) next UpdateSelectedCells set |> SpreadsheetMsg |> dispatch - if not set.IsEmpty then + if not set.IsEmpty && model.SpreadsheetModel.TableViewIsActive() then let oa = let columnIndex = set |> Seq.minBy fst |> fst let column = model.SpreadsheetModel.ActiveTable.GetColumn(columnIndex) @@ -144,6 +94,7 @@ module private EventPresets = open Shared open Fable.Core.JsInterop +open CellStyles type Cell = @@ -182,8 +133,10 @@ type Cell = match e.which with | 13. -> //enter if isHeader then setter state + debounceStorage.current.ClearAndRun() makeIdle() | 27. -> //escape + debounceStorage.current.Clear() makeIdle() | _ -> () ) @@ -201,7 +154,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) = + 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 isReadOnly = columnType = Unit let makeIdle() = UpdateActiveCell None |> SpreadsheetMsg |> dispatch @@ -281,13 +234,15 @@ type Cell = ]] [] - static member private BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), cell: CompositeCell, model: Model, dispatch, ?oasetter: OntologyAnnotation -> unit) = + static member BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), model: Model, dispatch, ?oasetter: {|oa: OntologyAnnotation; setter: OntologyAnnotation -> unit|}, ?displayValue, ?readonly: bool) = + let readonly = defaultArg readonly false let columnIndex, rowIndex = index let state = model.SpreadsheetModel let isSelected = state.SelectedCells.Contains index let isIdle = state.CellIsIdle (!^index, columnType) let isActive = not isIdle let ref = React.useElementRef() + let displayValue = defaultArg displayValue cellValue let makeIdle() = UpdateActiveCell None |> SpreadsheetMsg |> dispatch let ele = Browser.Dom.document.getElementById("SPREADSHEET_MAIN_VIEW") @@ -312,33 +267,71 @@ type Cell = prop.children [ Html.div [ cellInnerContainerStyle [] - prop.onDoubleClick(fun e -> - e.preventDefault() - e.stopPropagation() - if isIdle then makeActive() - UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch - ) - if isIdle then prop.onClick <| EventPresets.onClickSelect(index, isIdle, state.SelectedCells, model, dispatch) + if not readonly then + prop.onDoubleClick(fun e -> + e.preventDefault() + e.stopPropagation() + if isIdle then makeActive() + UpdateSelectedCells Set.empty |> SpreadsheetMsg |> dispatch + ) + if isIdle then prop.onClick <| EventPresets.onClickSelect(index, isIdle, state.SelectedCells, model, dispatch) prop.onMouseDown(fun e -> if isIdle && e.shiftKey then e.preventDefault()) prop.children [ if isActive then // Update change to mainState and exit active input. if oasetter.IsSome then - let oa = cell.ToOA() - let onBlur = fun e -> makeIdle() - let onEscape = fun e -> makeIdle() - let onEnter = fun e -> makeIdle() - let headerOA = state.ActiveTable.Headers.[columnIndex].TryOA() - let setter = fun (oa: OntologyAnnotation option) -> - if oa.IsSome then oasetter.Value oa.Value else setter "" + let oa = oasetter.Value.oa + let onBlur = fun e -> makeIdle(); + let onEscape = fun e -> makeIdle(); + let onEnter = fun e -> makeIdle(); + let setter = fun (oa: OntologyAnnotation option) -> + let oa = oa |> Option.defaultValue (OntologyAnnotation()) + oasetter.Value.setter oa + let headerOA = if state.TableViewIsActive() then state.ActiveTable.Headers.[columnIndex].TryOA() else None 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, minWidth=length.px 400) else Cell.CellInputElement(cellValue, false, false, setter, makeIdle) else - if columnType = Main then - compositeCellDisplay cell + if columnType = Main && oasetter.IsSome then + CellStyles.compositeCellDisplay oasetter.Value.oa displayValue else - basicValueDisplayCell cellValue + basicValueDisplayCell displayValue + ] + ] + ] + ] + + [] + static member BodySelect(value: string, setter: string -> unit, values: #seq, index: (int*int), model: Model.Model, dispatch) = + let columnIndex, rowIndex = index + let state = model.SpreadsheetModel + let ref = React.useElementRef() + Html.td [ + prop.key $"Cell_Select_{columnIndex}_{rowIndex}" + cellStyle [] + prop.ref ref + prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + prop.children [ + Html.div [ + cellInnerContainerStyle [] + prop.children [ + Html.div [ + prop.className "select w-full" + prop.children [ + Html.select [ + prop.className "!rounded-none w-full" + prop.value value + prop.onChange(fun (e: string) -> setter e) + prop.children [ + for v in values do + Html.option [ + prop.value v + prop.text v + ] + ] + ] + ] + ] ] ] ] @@ -351,13 +344,18 @@ type Cell = Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch let oasetter = if cell.isTerm then - fun (oa:OntologyAnnotation) -> - let nextCell = cell.UpdateWithOA oa - CellAux.oasetter(index, nextCell, dispatch) + {| + oa = cell.ToOA() + setter = + 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) + let displayValue = cell.ToString() + Cell.BodyBase(Main, cellValue, setter, index, model, dispatch, ?oasetter=oasetter, displayValue=displayValue) static member BodyUnit(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let cellValue = cell.GetContent().[1] @@ -369,16 +367,18 @@ type Cell = Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch 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) + {| + oa = cell.ToOA() + setter = + fun (oa:OntologyAnnotation) -> + let nextCell = cell.UpdateWithOA oa + CellAux.oasetter(index, nextCell, dispatch) + |} |> Some else None - Cell.BodyBase(Unit, cellValue, setter, index, cell, model, dispatch, ?oasetter=oasetter) + let displayValue = cell.ToString() + Cell.BodyBase(Unit, cellValue, setter, index, model, dispatch, ?oasetter=oasetter, displayValue=displayValue) static member BodyTSR(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let contentIndex = if cell.isUnitized then 2 else 1 @@ -389,7 +389,7 @@ type Cell = oa.TermSourceREF <- newTSR let nextCell = cell.UpdateWithOA oa Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - Cell.BodyBase(TSR, cellValue, setter, index, cell, model, dispatch) + Cell.BodyBase(TSR, cellValue, setter, index, model, dispatch) static member BodyTAN(index: (int*int), cell: CompositeCell, model: Model, dispatch) = let contentIndex = if cell.isUnitized then 3 else 2 @@ -400,5 +400,5 @@ type Cell = oa.TermAccessionNumber <- newTAN let nextCell = cell.UpdateWithOA oa Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - Cell.BodyBase(TAN, cellValue, setter, index, cell, model, dispatch) + Cell.BodyBase(TAN, cellValue, setter, index, model, dispatch) \ No newline at end of file diff --git a/src/Client/MainComponents/DataMap/Cells.fs b/src/Client/MainComponents/DataMap/Cells.fs new file mode 100644 index 00000000..b6da6c4c --- /dev/null +++ b/src/Client/MainComponents/DataMap/Cells.fs @@ -0,0 +1,31 @@ +namespace MainComponents.DataMap + +open Feliz +open Feliz.Bulma +open Spreadsheet +open Model +open Messages + +open Fable.Core.JsInterop +open ARCtrl +open MainComponents.CellStyles + +type Cells = + + static member Header(index:int, columnType: ColumnType, header: string) = + let id = $"Datamap_Header_{header}_{index}" + Html.th [ + if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter + prop.key id + prop.id id + cellStyle [] + prop.className "main-contrast-bg" + prop.children [ + Html.div [ + cellInnerContainerStyle [style.custom("backgroundColor","inherit")] + prop.children [ + basicValueDisplayCell header + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/MainComponents/DataMap/DataMap.fs b/src/Client/MainComponents/DataMap/DataMap.fs new file mode 100644 index 00000000..1ad8c24b --- /dev/null +++ b/src/Client/MainComponents/DataMap/DataMap.fs @@ -0,0 +1,183 @@ +namespace MainComponents.DataMap + +open ARCtrl +open Feliz +open Feliz.Bulma +open Model +open SpreadsheetInterface +open Messages +open Shared + +module private Helper = + + let updateFilePath (dtx: DataContext) (index: int) (dispatch: Messages.Msg -> unit) (newVal: string) = + let newVal = if newVal = "" then None else Some newVal + dtx.FilePath <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateSelector (dtx: DataContext) (index: int) (dispatch) (newVal: string) = + let newVal = if newVal = "" then None else Some newVal + dtx.Selector <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateSelectorFormat (dtx: DataContext) (index: int) (dispatch) (newVal: string) = + let newVal = if newVal = "" then None else Some newVal + dtx.SelectorFormat <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let dataFileTrytoString (dtf: DataFile option) = + dtf |> Option.map _.ToStringRdb() |> Option.defaultValue "None" + + let updateDataFile (dtx: DataContext) (index: int) (dispatch) (newVal: string) = + let newVal = DataFile.tryFromString newVal + dtx.DataType <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateFormat (dtx: DataContext) (index: int) (dispatch) (newVal: string) = + let newVal = if newVal = "" then None else Some newVal + dtx.Format <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateDescription (dtx: DataContext) (index: int) (dispatch) (newVal: string) = + let newVal = if newVal = "" then None else Some newVal + dtx.Description <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateGeneratedBy (dtx: DataContext) (index: int) (dispatch) (newVal: string) = + let newVal = if newVal = "" then None else Some newVal + dtx.GeneratedBy <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateExplication (dtx: DataContext) (index: int) (dispatch) (newVal: OntologyAnnotation option) = + dtx.Explication <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateUnit (dtx: DataContext) (index: int) (dispatch) (newVal: OntologyAnnotation option) = + dtx.Unit <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + + let updateObjectType (dtx: DataContext) (index: int) (dispatch) (newVal: OntologyAnnotation option) = + dtx.ObjectType <- newVal + UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch + +module private Components = + /// https://github.com/nfdi4plants/ARC-specification/blob/main/ISA-XLSX.md#examples-2 + let HeaderRow (state:Set) setState (model:Model) (dispatch: Msg -> unit) = + Html.tr [ + prop.children [ + MainComponents.CellStyles.RowLabel -1 + yield! + [ + fun i -> Cells.Header (i, Spreadsheet.Main, "Data Name") + fun i -> Cells.Header (i, Spreadsheet.Main, "Data FilePath") + fun i -> Cells.Header (i, Spreadsheet.Main, "Data Selector") + fun i -> Cells.Header (i, Spreadsheet.Main, "Data Selector Format") + //fun i -> Cells.Header (i, Spreadsheet.Main, "Data File Type") + fun i -> Cells.Header (i, Spreadsheet.Main, "Data Format") + fun i -> Cells.Header (i, Spreadsheet.Main, "Description") + fun i -> Cells.Header (i, Spreadsheet.Main, "GeneratedBy") + fun i -> Cells.Header (i, Spreadsheet.Main, "Explication") + fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") + fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") + fun i -> Cells.Header (i, Spreadsheet.Main, "Unit") + fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") + fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") + fun i -> Cells.Header (i, Spreadsheet.Main, "Object Type") + fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") + fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") + ] + |> List.mapi (fun i f -> f i) + ] + ] + + /// + /// let columnIndex, rowIndex = index + /// + /// + /// + /// let columnIndex, rowIndex = index + /// + /// + let Body (value: string option, setter, index: int * int, model: Model.Model, dispatch: Messages.Msg -> unit, readonly: bool option) = + let value = value |> Option.defaultValue "" + Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.Main, value, setter, index,model, dispatch, ?readonly=readonly) + + let BodyOntologyAnnotation (value: OntologyAnnotation option, setter: OntologyAnnotation option -> unit, index: int * int, model: Model.Model, dispatch: Messages.Msg -> unit) = + let value = defaultArg value (OntologyAnnotation()) + let setter = fun (oa:OntologyAnnotation) -> + if oa.isEmpty() then None else Some oa + |> setter + let oaSetter = {| + oa = value; + setter = fun (oa: OntologyAnnotation) -> oa |> setter + |} + let vMain = value.Name |> Option.defaultValue "" + let setterMain = fun (s:string) -> + value.Name <- if s = "" then None else Some s + setter value + // The same helper functions for TSR + let vTSR = value.TermSourceREF |> Option.defaultValue "" + let setterTSR = fun (s:string) -> + value.TermSourceREF <- if s = "" then None else Some s + setter value + // the same helper for tan + let vTAN = value.TermAccessionNumber |> Option.defaultValue "" + let setterTAN = fun (s:string) -> + value.TermAccessionNumber <- if s = "" then None else Some s + setter value + [ + Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.Main, vMain, setterMain, index, model, dispatch, oasetter=oaSetter) + Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.TSR, vTSR, setterTSR, index, model, dispatch) + Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.TAN, vTAN, setterTAN, index, model, dispatch) + ] + + let BodyRow (dtx: DataContext) (rowIndex: int) (state:Set) (model:Model) (dispatch: Messages.Msg -> unit) = + let mkIndex (col: int) = (col,rowIndex) + let DataMapBaseBody (field, updateFunc) i = Body(field, updateFunc dtx rowIndex dispatch, mkIndex i, model, dispatch, None) + let DataMapBaseBodyOA (field, updateFunc) i = BodyOntologyAnnotation(field, updateFunc dtx rowIndex dispatch, mkIndex i, model, dispatch) + Html.tr [ + MainComponents.CellStyles.RowLabel rowIndex + -1 |> fun i -> Body(dtx.Name, (fun _ -> ()), mkIndex i, model, dispatch, Some true) + 0 |> DataMapBaseBody(dtx.FilePath, Helper.updateFilePath) + 1 |> DataMapBaseBody(dtx.Selector, Helper.updateSelector) + 2 |> DataMapBaseBody(dtx.SelectorFormat, Helper.updateSelectorFormat) + //3 |> fun i -> Spreadsheet.Cells.Cell.BodySelect(Helper.dataFileTrytoString dtx.DataType, (Helper.updateDataFile dtx rowIndex dispatch), ["None"; DataFile.DerivedDataFile.ToStringRdb(); DataFile.ImageFile.ToStringRdb(); DataFile.RawDataFile.ToStringRdb()], mkIndex i, model, dispatch) + 3 |> DataMapBaseBody(dtx.Format, Helper.updateFormat) + 4 |> DataMapBaseBody(dtx.Description, Helper.updateDescription) + 5 |> DataMapBaseBody(dtx.GeneratedBy, Helper.updateGeneratedBy) + yield! 6 |> DataMapBaseBodyOA(dtx.Explication, Helper.updateExplication) + yield! 7 |> DataMapBaseBodyOA(dtx.Unit, Helper.updateUnit) + yield! 8 |> DataMapBaseBodyOA(dtx.ObjectType, Helper.updateObjectType) + ] + + let BodyRows (dtm: DataMap) (state:Set) (model:Model) (dispatch: Msg -> unit) = + Html.tbody [ + for ri in 0 .. (dtm.DataContexts.Count-1) do + yield BodyRow dtm.DataContexts.[ri] ri state model dispatch + ] + +type DataMap = + + [] + static member Main(model: Model, dispatch: Msg -> unit) = + let ref = React.useElementRef() + let state, setState : Set * (Set -> unit) = React.useState(Set.empty) + let dtm = model.SpreadsheetModel.DataMapOrDefault + Html.div [ + prop.id "SPREADSHEET_MAIN_VIEW" + prop.tabIndex 0 + prop.style [style.border(1, borderStyle.solid, "grey"); style.width.minContent; style.marginRight(length.vw 10)] + prop.ref ref + prop.onKeyDown(fun e -> Spreadsheet.KeyboardShortcuts.onKeydownEvent dispatch e) + prop.children [ + Html.table [ + prop.className "fixed_headers" + prop.children [ + Html.thead [ + Components.HeaderRow state setState model dispatch + ] + Components.BodyRows dtm state model dispatch + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/MainComponents/FooterTabs.fs b/src/Client/MainComponents/FooterTabs.fs index 0343371b..e189de8b 100644 --- a/src/Client/MainComponents/FooterTabs.fs +++ b/src/Client/MainComponents/FooterTabs.fs @@ -16,50 +16,187 @@ type private FooterTab = { Name = Option.defaultValue "" name } -let private popup (x: int, y: int) renameMsg deleteMsg (rmv: _ -> unit) = - /// This element will remove the contextmenu when clicking anywhere else - let rmv_element = Html.div [ - prop.onClick rmv - prop.onContextMenu(fun e -> e.preventDefault(); rmv e) - prop.style [ - style.position.fixedRelativeToWindow - style.backgroundColor.transparent - style.left 0 - style.top 0 - style.right 0 - style.bottom 0 - style.display.block +[] +module private TableContextMenu = + + type ContextFunctions = { + Delete : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + Rename : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + } + + let contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (rmv: _ -> unit) = + /// This element will remove the contextmenu when clicking anywhere else + let rmv_element = Html.div [ + prop.onClick rmv + prop.onContextMenu(fun e -> e.preventDefault(); rmv e) + prop.style [ + style.position.fixedRelativeToWindow + style.backgroundColor.transparent + style.left 0 + style.top 0 + style.right 0 + style.bottom 0 + style.display.block + ] ] - ] - let button (name:string, msg, props) = Html.li [ - Bulma.button.button [ - prop.style [style.borderRadius 0] - prop.onClick msg - Bulma.button.isFullWidth - Bulma.button.isSmall - yield! props - prop.text name + let button (name:string, icon: string, msg, props) = Html.li [ + Bulma.button.button [ + prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] + prop.onClick msg + prop.className "py-1" + Bulma.button.isFullWidth + //Bulma.button.isSmall + Bulma.color.isBlack + Bulma.button.isInverted + yield! props + prop.children [ + Bulma.icon [Html.i [prop.className icon]] + Html.span name + ] + ] ] - ] - Html.div [ - prop.style [ - let height = 53 - style.backgroundColor "white" - style.position.absolute - style.left x - style.top (y - height) - style.zIndex 20 - style.width 70 - style.height height + let divider = Html.li [ + Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] ] - prop.children [ - rmv_element - Html.ul [ - button ("Delete", deleteMsg rmv, [Bulma.color.isDanger]) - button ("Rename", renameMsg rmv, []) + let buttonList = [ + button ("Delete", "fa-solid fa-trash", funcs.Delete rmv, []) + button ("Rename", "fa-solid fa-pen-to-square", funcs.Rename rmv, []) + ] + Html.div [ + prop.style [ + style.backgroundColor "white" + style.position.absolute + style.left mousex + style.top (mousey-40) + style.width 150 + style.zIndex 40 // to overlap navbar + style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) + ] + prop.children [ + rmv_element + Html.ul buttonList + ] + ] + + +[] +module private PlusContextMenu = + type ContextFunctions = { + AddTable : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + AddDatamap : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + } + + let contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (rmv: _ -> unit) = + /// This element will remove the contextmenu when clicking anywhere else + let rmv_element = Html.div [ + prop.onClick rmv + prop.onContextMenu(fun e -> e.preventDefault(); rmv e) + prop.style [ + style.position.fixedRelativeToWindow + style.backgroundColor.transparent + style.left 0 + style.top 0 + style.right 0 + style.bottom 0 + style.display.block + ] + ] + let button (name:string, icon: string, msg, props) = Html.li [ + Bulma.button.button [ + prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] + prop.onClick msg + prop.className "py-1" + Bulma.button.isFullWidth + //Bulma.button.isSmall + Bulma.color.isBlack + Bulma.button.isInverted + yield! props + prop.children [ + Bulma.icon [Html.i [prop.className icon]] + Html.span name + ] + ] + ] + let divider = Html.li [ + Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] + ] + let buttonList = [ + button ("Add Table", "fa-solid fa-table", funcs.AddTable rmv, []) + button ("Add Datamap", "fa-solid fa-map", funcs.AddDatamap rmv, []) + ] + Html.div [ + prop.style [ + style.backgroundColor "white" + style.position.absolute + style.left mousex + style.top (mousey-40) + style.width 150 + style.zIndex 40 // to overlap navbar + style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) + ] + prop.children [ + rmv_element + Html.ul buttonList + ] + ] + +module DataMapContextMenu = + type ContextFunctions = { + Delete : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + } + + let contextmenu (mousex: int, mousey: int) (funcs:ContextFunctions) (rmv: _ -> unit) = + /// This element will remove the contextmenu when clicking anywhere else + let rmv_element = Html.div [ + prop.onClick rmv + prop.onContextMenu(fun e -> e.preventDefault(); rmv e) + prop.style [ + style.position.fixedRelativeToWindow + style.backgroundColor.transparent + style.left 0 + style.top 0 + style.right 0 + style.bottom 0 + style.display.block + ] + ] + let button (name:string, icon: string, msg, props) = Html.li [ + Bulma.button.button [ + prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] + prop.onClick msg + prop.className "py-1" + Bulma.button.isFullWidth + //Bulma.button.isSmall + Bulma.color.isBlack + Bulma.button.isInverted + yield! props + prop.children [ + Bulma.icon [Html.i [prop.className icon]] + Html.span name + ] + ] + ] + let divider = Html.li [ + Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] + ] + let buttonList = [ + button ("Delete", "fa-solid fa-trash", funcs.Delete rmv, []) + ] + Html.div [ + prop.style [ + style.backgroundColor "white" + style.position.absolute + style.left mousex + style.top (mousey-40) + style.width 150 + style.zIndex 40 // to overlap navbar + style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) + ] + prop.children [ + rmv_element + Html.ul buttonList ] ] - ] open Spreadsheet.Types @@ -76,7 +213,6 @@ let private drop_handler (eleOrder, state, setState, dispatch) = fun (e: Browser setState {state with IsDraggedOver = false} match getData with | Ok data -> - Browser.Dom.console.log(data) let prev_index = data.OriginOrder let next_index = eleOrder Spreadsheet.UpdateTableOrder(prev_index, next_index) |> Messages.SpreadsheetMsg |> dispatch @@ -127,7 +263,11 @@ let Main (index: int, tables: ArcTables, model: Model, dispatch: Messages.Msg -> let mousePosition = int e.pageX, int e.pageY let deleteMsg rmv = fun e -> rmv e; Spreadsheet.RemoveTable index |> Messages.SpreadsheetMsg |> dispatch let renameMsg rmv = fun e -> rmv e; {state with IsEditable = true} |> setState - let child = popup mousePosition renameMsg deleteMsg + let funcs : TableContextMenu.ContextFunctions = { + Rename = renameMsg + Delete = deleteMsg + } + let child = TableContextMenu.contextmenu mousePosition funcs let name = $"popup_{mousePosition}" Modals.Controller.renderModal(name, child) ) @@ -157,23 +297,60 @@ let Main (index: int, tables: ArcTables, model: Model, dispatch: Messages.Msg -> prop.defaultValue table.Name ] else - Html.a [prop.text table.Name] + Html.a [ + Bulma.icon [Html.i [prop.className "fa-solid fa-table"]] + Html.text table.Name + ] ] ] [] let MainMetadata(model: Model, dispatch: Messages.Msg -> unit) = - let order = 0 let id = "Metadata-Tab" let nav = Spreadsheet.ActiveView.Metadata + let order = nav.TableIndex + Bulma.tab [ + if model.SpreadsheetModel.ActiveView = nav then Bulma.tab.isActive + prop.key id + prop.id id + prop.onClick (fun _ -> Spreadsheet.UpdateActiveView nav |> Messages.SpreadsheetMsg |> dispatch) + prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] + prop.children [ + Html.a [ + Bulma.icon [Html.i [prop.className "fa-solid fa-circle-info"]] + Html.text "Metadata" + ] + ] + ] + +[] +let MainDataMap(model: Model, dispatch: Messages.Msg -> unit) = + let id = "Metadata-Tab" + let nav = Spreadsheet.ActiveView.DataMap + let order = nav.TableIndex Bulma.tab [ if model.SpreadsheetModel.ActiveView = nav then Bulma.tab.isActive prop.key id prop.id id prop.onClick (fun _ -> Spreadsheet.UpdateActiveView nav |> Messages.SpreadsheetMsg |> dispatch) + prop.onContextMenu(fun e -> + e.stopPropagation() + e.preventDefault() + let mousePosition = int e.pageX, int e.pageY + let deleteDatamapMsg rmv = fun e -> rmv e; SpreadsheetInterface.UpdateDatamap None |> Messages.InterfaceMsg |> dispatch + let funcs : DataMapContextMenu.ContextFunctions = { + Delete = deleteDatamapMsg + } + let child = DataMapContextMenu.contextmenu mousePosition funcs + let name = $"popup_{mousePosition}" + Modals.Controller.renderModal(name, child) + ) prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] prop.children [ - Html.a [prop.text "Metadata"] + Html.a [ + Bulma.icon [Html.i [prop.className "fa-solid fa-map"]] + Html.text "Data Map" + ] ] ] @@ -191,6 +368,20 @@ let MainPlus(model: Model, dispatch: Messages.Msg -> unit) = prop.onDragOver drag_preventdefault prop.onDrop <| drop_handler (order, state, setState, dispatch) prop.onClick (fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) + prop.onContextMenu(fun e -> + e.stopPropagation() + e.preventDefault() + let mousePosition = int e.pageX, int e.pageY + let addTableMsg rmv = fun e -> rmv e; SpreadsheetInterface.CreateAnnotationTable false |> Messages.InterfaceMsg |> dispatch + let addDatamapMsg rmv = fun e -> rmv e; SpreadsheetInterface.UpdateDatamap (DataMap.init() |> Some) |> Messages.InterfaceMsg |> dispatch + let funcs : PlusContextMenu.ContextFunctions = { + AddTable = addTableMsg + AddDatamap = addDatamapMsg + } + let child = PlusContextMenu.contextmenu mousePosition funcs + let name = $"popup_{mousePosition}" + Modals.Controller.renderModal(name, child) + ) prop.style [style.custom ("order", order); style.height (length.percent 100); style.cursor.pointer] prop.children [ Html.a [ diff --git a/src/Client/MainComponents/Metadata/Assay.fs b/src/Client/MainComponents/Metadata/Assay.fs index 03e9ab9d..ba03ccab 100644 --- a/src/Client/MainComponents/Metadata/Assay.fs +++ b/src/Client/MainComponents/Metadata/Assay.fs @@ -56,4 +56,29 @@ let Main(assay: ArcAssay, model: Model, dispatch: Msg -> unit) = assay.Comments <- ResizeArray comments assay |> Assay |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) + DatamapConfig.Main( + assay.DataMap, + fun dtm -> + logw "HARDCODED DTM EXTENSION!" + let create_Datacontext (i:int) = + DataContext( + $"id_string_{i}", + "My Name", + DataFile.DerivedDataFile, + "My Format", + "My Selector Format", + OntologyAnnotation("Explication", "MS", "MS:123456"), + OntologyAnnotation("Unit", "MS", "MS:123456"), + OntologyAnnotation("ObjectType", "MS", "MS:123456"), + "My Label", + "My Description", + "KevinF.exe", + (ResizeArray [Comment.create("Hello", "World")]) + ) + dtm |> Option.iter (fun dtm -> + for i in 0 .. 5 do + dtm.DataContexts.Add (create_Datacontext i) + ) + dtm |> SpreadsheetInterface.UpdateDatamap |> InterfaceMsg |> dispatch + ) ] \ No newline at end of file diff --git a/src/Client/MainComponents/Metadata/DatamapConfig.fs b/src/Client/MainComponents/Metadata/DatamapConfig.fs new file mode 100644 index 00000000..54c62a7f --- /dev/null +++ b/src/Client/MainComponents/Metadata/DatamapConfig.fs @@ -0,0 +1,50 @@ +namespace MainComponents.Metadata + +open ARCtrl +open Feliz +open Feliz.Bulma + +type DatamapConfig = + + static member Main(datamap: DataMap option, setDatamap: DataMap option -> unit) = + Bulma.field.div [ + Bulma.box [ + Bulma.block [ + Bulma.content [ + Html.h4 "Datamap" + Html.p "Add datamap sheet. This allows detailed annotation of data files." + ] + ] + Bulma.block [ + Bulma.buttons [ + Bulma.button.button [ + color.isSuccess + if datamap.IsSome then button.isStatic + prop.onClick (fun _ -> + let newDtm = DataMap.init() + setDatamap (Some newDtm) + ) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-map"] + ] + Html.span "Add Datamap" + ] + ] + Bulma.button.button [ + color.isDanger + if datamap.IsNone then button.isStatic + prop.onClick(fun _ -> + setDatamap None + ) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-trash"] + ] + Html.span "Remove Datamap" + ] + ] + ] + ] + ] + ] \ No newline at end of file diff --git a/src/Client/MainComponents/Metadata/Study.fs b/src/Client/MainComponents/Metadata/Study.fs index 6c14b247..75f716e0 100644 --- a/src/Client/MainComponents/Metadata/Study.fs +++ b/src/Client/MainComponents/Metadata/Study.fs @@ -78,4 +78,9 @@ let Main(study: ArcStudy, assignedAssays: ArcAssay list, model: Model, dispatch: study.Comments <- ResizeArray(comments) (study, assignedAssays) |> Study |> Spreadsheet.UpdateArcFile |> SpreadsheetMsg |> dispatch ) + DatamapConfig.Main( + study.DataMap, + fun dtm -> + dtm |> SpreadsheetInterface.UpdateDatamap |> InterfaceMsg |> dispatch + ) ] \ No newline at end of file diff --git a/src/Client/MainComponents/NoFileElement.fs b/src/Client/MainComponents/NoFileElement.fs index d0d34a10..a6a67b6a 100644 --- a/src/Client/MainComponents/NoFileElement.fs +++ b/src/Client/MainComponents/NoFileElement.fs @@ -114,7 +114,7 @@ module private Helper = Bulma.dropdownTrigger [ Bulma.button.span [ Bulma.button.isLarge - Bulma.color.isLink + Bulma.color.isPrimary prop.onClick toggle //prop.onClick(fun e -> SpreadsheetInterface.CreateAnnotationTable e.ctrlKey |> Messages.InterfaceMsg |> dispatch) prop.children [ diff --git a/src/Client/MainComponents/SpreadsheetView.fs b/src/Client/MainComponents/SpreadsheetView.fs index e138fea3..a4c831b5 100644 --- a/src/Client/MainComponents/SpreadsheetView.fs +++ b/src/Client/MainComponents/SpreadsheetView.fs @@ -29,41 +29,10 @@ let private cellPlaceholder (c_opt: CompositeCell option) = ] ] -/// -/// rowIndex < 0 equals header -/// -/// -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.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 [ - 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}") - ] - ] - ] - ] - let private bodyRow (rowIndex: int) (state:Set) (model:Model) (dispatch: Msg -> unit) = let table = model.SpreadsheetModel.ActiveTable Html.tr [ - RowLabel rowIndex + CellStyles.RowLabel rowIndex for columnIndex in 0 .. (table.ColumnCount-1) do let index = columnIndex, rowIndex let cell = model.SpreadsheetModel.ActiveTable.Values.[index] @@ -88,7 +57,7 @@ let private bodyRows (state:Set) (model:Model) (dispatch: Msg -> unit) = let private headerRow (state:Set) setState (model:Model) (dispatch: Msg -> unit) = let table = model.SpreadsheetModel.ActiveTable Html.tr [ - if table.ColumnCount > 0 then RowLabel -1 + if table.ColumnCount > 0 then CellStyles.RowLabel -1 for columnIndex in 0 .. (table.ColumnCount-1) do let header = table.Headers.[columnIndex] Cells.Cell.Header(columnIndex, header, state, setState, model, dispatch) diff --git a/src/Client/Modals/MoveColumn.fs b/src/Client/Modals/MoveColumn.fs index 0e992b67..0f675f20 100644 --- a/src/Client/Modals/MoveColumn.fs +++ b/src/Client/Modals/MoveColumn.fs @@ -5,7 +5,6 @@ open Feliz.Bulma open Model open Messages open Shared -open OfficeInteropTypes open ARCtrl diff --git a/src/Client/OfficeInterop/OfficeInterop.fs b/src/Client/OfficeInterop/OfficeInterop.fs index acf508e7..491a4a45 100644 --- a/src/Client/OfficeInterop/OfficeInterop.fs +++ b/src/Client/OfficeInterop/OfficeInterop.fs @@ -151,11 +151,6 @@ open OfficeInteropExtensions // 'Featured column' -> A featured column can be abstracted as a "term column" and is a pre-implemented usecase. // Such a block will contain TSR and TAN and can be used for directed Term search. - - -[] -let consoleLog (message: string): unit = jsNative - open System open Fable.Core.JsInterop diff --git a/src/Client/Pages/BuildingBlock/Dropdown.fs b/src/Client/Pages/BuildingBlock/Dropdown.fs index 29cb38a2..05d3ad45 100644 --- a/src/Client/Pages/BuildingBlock/Dropdown.fs +++ b/src/Client/Pages/BuildingBlock/Dropdown.fs @@ -11,14 +11,33 @@ open ARCtrl [] let FreeTextInputElement(onSubmit: string -> unit) = let input, setInput = React.useState "" - Html.span [ - Html.input [ - prop.onClick (fun e -> e.stopPropagation()) - prop.onChange (fun (v:string) -> setInput v) - ] - Html.button [ - prop.onClick (fun e -> e.stopPropagation(); onSubmit input) - prop.text "✅" + Bulma.field.div [ + field.isGrouped + prop.className "w-full" + prop.children [ + Bulma.control.div [ + control.isExpanded + prop.children [ + Bulma.input.text [ + prop.className "min-w-48 !rounded-none" + Bulma.input.isSmall + prop.onClick (fun e -> e.stopPropagation()) + prop.onChange (fun (v:string) -> setInput v) + prop.onKeyDown(key.enter, fun e -> e.stopPropagation(); onSubmit input) + ] + ] + ] + Bulma.control.div [ + Bulma.button.span [ + button.isSmall + prop.onClick (fun e -> e.stopPropagation(); onSubmit input) + prop.children [ + Bulma.icon [ + Html.i [prop.className "fa-solid fa-check"] + ] + ] + ] + ] ] ] @@ -91,7 +110,7 @@ module private DropdownElements = let setIO (ioType) = { DropdownPage = DropdownPage.Main; DropdownIsActive = false } |> setUiState (headerType,ioType) |> BuildingBlock.UpdateHeaderWithIO |> BuildingBlockMsg |> dispatch - Bulma.dropdownItem.a [ + Bulma.dropdownItem.button [ match iotype with | IOType.FreeText s -> let onSubmit = fun (v: string) -> diff --git a/src/Client/Pages/BuildingBlock/SearchComponent.fs b/src/Client/Pages/BuildingBlock/SearchComponent.fs index d359d408..ee260195 100644 --- a/src/Client/Pages/BuildingBlock/SearchComponent.fs +++ b/src/Client/Pages/BuildingBlock/SearchComponent.fs @@ -139,7 +139,7 @@ let private addBuildingBlockButton (model: Model) dispatch = else Array.empty let column = CompositeColumn.create(header, bodyCells) - let index = Spreadsheet.BuildingBlocks.Controller.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel + let index = Spreadsheet.Controller.BuildingBlocks.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.AddAnnotationBlock column |> InterfaceMsg |> dispatch let id = $"Header_{index}_Main" scrollIntoViewRetry id diff --git a/src/Client/Pages/Cytoscape/CytoscapeGraph.fs b/src/Client/Pages/Cytoscape/CytoscapeGraph.fs index e762c2ec..ba594648 100644 --- a/src/Client/Pages/Cytoscape/CytoscapeGraph.fs +++ b/src/Client/Pages/Cytoscape/CytoscapeGraph.fs @@ -84,6 +84,6 @@ module Graph = cy <- Some cy_ele //cy.Value.useJS(Cytoscape.JS.cxtMenu) centerOn(model.TargetAccession) - createClickEvent(fun e -> Browser.Dom.console.log( e.target?position() ) ) + createClickEvent(fun e -> ()) updateLayout() \ No newline at end of file diff --git a/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs index fb7d249c..a75bd633 100644 --- a/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs +++ b/src/Client/Pages/ProtocolTemplates/TemplateFromDB.fs @@ -36,7 +36,7 @@ type TemplateFromDB = /// 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 index = Spreadsheet.Controller.BuildingBlocks.SidebarControllerAux.getNextColumnIndex model.SpreadsheetModel SpreadsheetInterface.JoinTable (preparedTemplate, Some index, Some joinConfig) |> InterfaceMsg |> dispatch ) prop.text "Add template" diff --git a/src/Client/SharedComponents/AdvancedSearch.fs b/src/Client/SharedComponents/AdvancedSearch.fs index a7b8a43e..8f70f961 100644 --- a/src/Client/SharedComponents/AdvancedSearch.fs +++ b/src/Client/SharedComponents/AdvancedSearch.fs @@ -260,21 +260,25 @@ module private ResultsTable = let private keepObsoleteCheckradioElement (state:AdvancedSearch.Model) setState = let currentKeepObsolete = state.AdvancedSearchOptions.KeepObsolete - let checkradioName = "keepObsolete_checkradio" - let id = sprintf "%s"checkradioName Bulma.field.div [ - Bulma.input.radio [ - prop.name checkradioName - prop.id id - prop.isChecked (state.AdvancedSearchOptions.KeepObsolete) - prop.onChange (fun (e:bool) -> - {state with AdvancedSearch.Model.AdvancedSearchOptions.KeepObsolete = not currentKeepObsolete} - |> setState - ) - ] - Html.label [ - prop.htmlFor id - prop.text (if currentKeepObsolete then "yes" else "no") + Bulma.control.div [ + Html.label [ + prop.className "checkbox" + prop.children [ + Html.input [ + prop.type'.checkbox + prop.isChecked (state.AdvancedSearchOptions.KeepObsolete) + prop.onChange (fun (e:bool) -> + {state with AdvancedSearch.Model.AdvancedSearchOptions.KeepObsolete = e} + |> setState + ) + ] + Html.span [ + prop.className "is-unselectable" + prop.text (if currentKeepObsolete then " yes" else " no") + ] + ] + ] ] ] @@ -362,9 +366,7 @@ let private inputFormPage (state:AdvancedSearch.Model) (setState:AdvancedSearch. //] Bulma.field.div [ Bulma.label "Keep obsolete terms" - Html.div [ - keepObsoleteCheckradioElement state setState - ] + keepObsoleteCheckradioElement state setState ] ] diff --git a/src/Client/SharedComponents/TermSearchInput.fs b/src/Client/SharedComponents/TermSearchInput.fs index ddaa4b8b..6a63839f 100644 --- a/src/Client/SharedComponents/TermSearchInput.fs +++ b/src/Client/SharedComponents/TermSearchInput.fs @@ -1,4 +1,4 @@ -namespace Components +namespace Components open Feliz open Feliz.Bulma @@ -49,7 +49,7 @@ module TermSearchAux = setSearchTreeState: SearchState -> unit, setLoading: bool -> unit, stopSearch: unit -> unit, - debounceStorage: System.Collections.Generic.Dictionary, + debounceStorage: DebounceStorage, debounceTimer: int ) = let queryDB() = @@ -72,7 +72,7 @@ module TermSearchAux = setSearchTreeState: SearchState -> unit, setLoading: bool -> unit, stopSearch: unit -> unit, - debounceStorage: System.Collections.Generic.Dictionary, + debounceStorage: DebounceStorage, debounceTimer: int ) = let queryDB() = @@ -89,7 +89,7 @@ module TermSearchAux = setSearchNameState <| SearchState.init() debouncel debounceStorage "TermSearch" debounceTimer setLoading queryDB () - let dsetter (inp: OntologyAnnotation option, setter, debounceStorage: System.Collections.Generic.Dictionary, setLoading: bool -> unit, debounceSetter: int option) = + let dsetter (inp: OntologyAnnotation option, setter, debounceStorage: DebounceStorage, setLoading: bool -> unit, debounceSetter: int option) = if debounceSetter.IsSome then debouncel debounceStorage "SetterDebounce" debounceSetter.Value setLoading setter inp else @@ -97,7 +97,7 @@ module TermSearchAux = module Components = - let termSeachNoResults = [ + let termSeachNoResults (advancedTermSearchActiveSetter: (bool -> unit) option) = [ Html.div [ prop.key $"TermSelectItem_NoResults" prop.classes ["term-select-item"] @@ -105,13 +105,18 @@ module TermSearchAux = Html.div "No terms found matching your input." ] ] - Html.div [ - prop.key $"TermSelectItem_Suggestion" - prop.classes ["term-select-item"] - prop.children [ - Html.div "Can't find the term you are looking for? Try Advanced Search!" + if advancedTermSearchActiveSetter.IsSome then + Html.div [ + prop.key $"TermSelectItem_Suggestion" + prop.classes ["term-select-item"] + prop.children [ + Html.span "Can't find the term you are looking for? " + Html.a [ + prop.onClick(fun e -> e.preventDefault(); e.stopPropagation(); advancedTermSearchActiveSetter.Value true) + prop.text "Try Advanced Search!" + ] + ] ] - ] Html.div [ prop.key $"TermSelectItem_Contact" prop.classes ["term-select-item"] @@ -234,6 +239,7 @@ type TermSearch = prop.style [style.marginRight 0] prop.children [ Bulma.button.a [ + prop.className "h-full" prop.style [style.borderWidth 0; style.borderRadius 0] if not searchable then Bulma.color.hasTextGreyLight Bulma.button.isInverted @@ -259,7 +265,7 @@ type TermSearch = ] [] - static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool) = + static member TermSelectArea (id: string, searchNameState: SearchState, searchTreeState: SearchState, setTerm: TermTypes.Term option -> unit, show: bool, setAdvancedTermSearchActive) = let searchesAreComplete = searchNameState.SearchIs = SearchIs.Done && searchTreeState.SearchIs = SearchIs.Done let foundInBoth (term:TermTypes.Term) = (searchTreeState.Results |> Array.contains term) @@ -267,7 +273,7 @@ type TermSearch = let matchSearchState (ss: SearchState) (isDirectedSearch: bool) = match ss with | {SearchIs = SearchIs.Done; Results = [||]} when not isDirectedSearch -> - Components.termSeachNoResults + Components.termSeachNoResults setAdvancedTermSearchActive | {SearchIs = SearchIs.Done; Results = results} -> [ for term in results do let setTerm = fun (e: MouseEvent) -> setTerm (Some term) @@ -281,7 +287,10 @@ type TermSearch = TermSearch.TermSelectItem (term, setTerm, isDirectedSearch) ] | {SearchIs = SearchIs.Running; Results = _ } -> [ - Html.div "loading.." + Html.div [ + prop.className "px-3 py-2" + prop.text "loading.." + ] ] | _ -> [ Html.none @@ -390,9 +399,11 @@ type TermSearch = e.stopPropagation() match e.which with | 27. -> //escape - if onEscape.IsSome then onEscape.Value e stopSearch() + debounceStorage.current.ClearAndRun() + if onEscape.IsSome then onEscape.Value e | 13. -> //enter + debounceStorage.current.ClearAndRun() if onEnter.IsSome then onEnter.Value e | 9. -> //tab if searchableToggle then @@ -402,7 +413,7 @@ type TermSearch = ) ] - let TermSelectArea = TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching) + let TermSelectArea = TermSearch.TermSelectArea (SelectAreaID, searchNameState, searchTreeState, selectTerm, isSearching, (if advancedSearchDispatch.IsSome then Some setAdvancedSearchActive else None)) if portalTermSelectArea.IsSome then ReactDOM.createPortal(TermSelectArea,portalTermSelectArea.Value) elif ref.current.IsSome then diff --git a/src/Client/Spreadsheet/BuildingBlocks.Controller.fs b/src/Client/Spreadsheet/Controller/BuildingBlocks.fs similarity index 98% rename from src/Client/Spreadsheet/BuildingBlocks.Controller.fs rename to src/Client/Spreadsheet/Controller/BuildingBlocks.fs index 393ef38d..42074dc8 100644 --- a/src/Client/Spreadsheet/BuildingBlocks.Controller.fs +++ b/src/Client/Spreadsheet/Controller/BuildingBlocks.fs @@ -1,8 +1,7 @@ -module Spreadsheet.BuildingBlocks.Controller +module Spreadsheet.Controller.BuildingBlocks open System.Collections.Generic open Shared.TermTypes -open Shared.OfficeInteropTypes open Spreadsheet open Types open ARCtrl diff --git a/src/Client/Spreadsheet/Clipboard.Controller.fs b/src/Client/Spreadsheet/Controller/Clipboard.fs similarity index 98% rename from src/Client/Spreadsheet/Clipboard.Controller.fs rename to src/Client/Spreadsheet/Controller/Clipboard.fs index bc1f0de3..4d4f6fe3 100644 --- a/src/Client/Spreadsheet/Clipboard.Controller.fs +++ b/src/Client/Spreadsheet/Controller/Clipboard.fs @@ -1,4 +1,4 @@ -module Spreadsheet.Clipboard.Controller +module Spreadsheet.Controller.Clipboard open Fable.Core open ARCtrl @@ -18,7 +18,6 @@ let copyCellByIndex (index: 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 = @@ -40,7 +39,6 @@ let cutCellByIndex (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Mod state let cutCellsByIndices (indices: (int*int) []) (state: Spreadsheet.Model) : Spreadsheet.Model = - log "HIT" let cells = ResizeArray() for index in indices do let cell = state.ActiveTable.Values.[index] @@ -92,7 +90,6 @@ let pasteCellsIntoSelected (state: Spreadsheet.Model) : JS.Promise Set.toArray |> Array.minBy fst |> fst let selectedSingleColumnCells = state.SelectedCells |> Set.filter (fun index -> fst index = columnIndex) promise { diff --git a/src/Client/Spreadsheet/Controller/DataMap.fs b/src/Client/Spreadsheet/Controller/DataMap.fs new file mode 100644 index 00000000..6335b777 --- /dev/null +++ b/src/Client/Spreadsheet/Controller/DataMap.fs @@ -0,0 +1,43 @@ +module Spreadsheet.Controller.DataMap + +open Shared +open ARCtrl + +let updateDatamap (dataMapOpt: DataMap option) (state: Spreadsheet.Model) : Spreadsheet.Model = + let nextArcFile = + match state.ArcFile with + | Some (Assay a) -> + a.DataMap <- dataMapOpt + Some (Assay a) + | Some (Study (s,_)) -> + s.DataMap <- dataMapOpt + Some (Study (s, [])) + | _ -> + logw "[WARNING] updateDatamap: No Assay or Study found in ArcFile" + state.ArcFile + match dataMapOpt with + | None when state.ActiveView = Spreadsheet.ActiveView.DataMap -> + {state with + ArcFile = nextArcFile + ActiveView = Spreadsheet.ActiveView.Metadata} + | _ -> + {state with + ArcFile = nextArcFile} + +let updateDataMapDataContextAt (dtx) (index) (state: Spreadsheet.Model) : Spreadsheet.Model = + let ensureIndexExists (dtm: DataMap) = + if index >= dtm.DataContexts.Count then failwithf "DataMap does not contain the an item at index: %i. Only %i items exist." index dtm.DataContexts.Count + let nextArcFile = + match state.ArcFile with + | Some (Assay a) when a.DataMap.IsSome -> + ensureIndexExists a.DataMap.Value + a.DataMap.Value.DataContexts.[index] <- dtx + Some (Assay a) + | Some (Study (s,_)) when s.DataMap.IsSome -> + ensureIndexExists s.DataMap.Value + s.DataMap.Value.DataContexts.[index] <- dtx + Some (Study (s, [])) + | _ -> + logw "[WARNING] updateDatamap: No Assay or Study found in ArcFile" + state.ArcFile + {state with ArcFile = nextArcFile} \ No newline at end of file diff --git a/src/Client/Spreadsheet/Table.Controller.fs b/src/Client/Spreadsheet/Controller/Table.fs similarity index 96% rename from src/Client/Spreadsheet/Table.Controller.fs rename to src/Client/Spreadsheet/Controller/Table.fs index 53c33559..8588544e 100644 --- a/src/Client/Spreadsheet/Table.Controller.fs +++ b/src/Client/Spreadsheet/Controller/Table.fs @@ -1,4 +1,4 @@ -module Spreadsheet.Table.Controller +module Spreadsheet.Controller.Table open System.Collections.Generic open Shared.TermTypes @@ -141,7 +141,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 + let cell = cell.Copy() state.ActiveTable.UpdateCellAt(columnIndex, cellRowIndex, cell) ) {state with ArcFile = state.ArcFile} @@ -165,13 +165,13 @@ let clearCells (indexArr: (int*int) []) (state: Spreadsheet.Model) : Spreadsheet open Fable.Core open System -let selectRelativeCell (index: int*int) (move: int*int) (table: ArcTable) = +let selectRelativeCell (index: int*int) (move: int*int) (maxColumnIndex: int) (maxRowIndex: int) = //let index = // match index with // | U2.Case2 index -> index,-1 // | U2.Case1 index -> index - let columnIndex = Math.Min(Math.Max(fst index + fst move, 0), table.ColumnCount-1) - let rowIndex = Math.Min(Math.Max(snd index + snd move, 0), table.RowCount-1) + let columnIndex = Math.Min(Math.Max(fst index + fst move, 0), maxColumnIndex) + let rowIndex = Math.Min(Math.Max(snd index + snd move, 0), maxRowIndex) //if rowIndex = -1 then // U2.Case2 columnIndex //else diff --git a/src/Client/States/LocalHistory.fs b/src/Client/States/LocalHistory.fs index 9e0ca45e..a0177da5 100644 --- a/src/Client/States/LocalHistory.fs +++ b/src/Client/States/LocalHistory.fs @@ -88,7 +88,9 @@ module ConversionTypes = let arcFile = match this.JsonArcFiles with | JsonArcFiles.Investigation -> ArcInvestigation.fromCompressedJsonString this.JsonString |> ArcFiles.Investigation |> Some - | JsonArcFiles.Study -> ArcStudy.fromCompressedJsonString this.JsonString |> fun s -> ArcFiles.Study(s, []) |> Some + | JsonArcFiles.Study -> + let s = ArcStudy.fromCompressedJsonString this.JsonString + ArcFiles.Study(s, []) |> Some | JsonArcFiles.Assay -> ArcAssay.fromCompressedJsonString this.JsonString |> ArcFiles.Assay |> Some | JsonArcFiles.Template -> Template.fromJsonString this.JsonString |> ArcFiles.Template |> Some | JsonArcFiles.None -> None diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index ce226c28..532b642f 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -17,6 +17,7 @@ with [] type ActiveView = | Table of index:int +| DataMap | Metadata with /// @@ -25,7 +26,8 @@ with member this.TableIndex = match this with | Table i -> i - | _ -> 0 + | DataMap -> 0 + | Metadata -> -1 ///If you change this model, it will kill caching for users! if you apply changes to it, make sure to keep a version ///of it and add a try case for it to `tryInitFromLocalStorage` in Spreadsheet/LocalStorage.fs . @@ -68,10 +70,20 @@ type Model = { with get() = match this.ActiveView with | ActiveView.Table i -> this.Tables.GetTableAt(i) - | ActiveView.Metadata -> + | ActiveView.Metadata | ActiveView.DataMap -> let t = ArcTable.init("NULL_TABLE") //return NULL_TABLE-named table for easier handling of return value t.AddColumn(CompositeHeader.FreeText "WARNING", [|CompositeCell.FreeText "If you see this table view, pls contact a developer and report it."|]) t + member this.HasDataMap() = + match this.ArcFile with + | Some (Assay a) -> a.DataMap.IsSome + | Some (Study (s,_)) -> s.DataMap.IsSome + | _ -> false + member this.DataMapOrDefault = + match this.ArcFile with + | Some (Assay a) when a.DataMap.IsSome -> a.DataMap.Value + | Some (Study (s,_)) when s.DataMap.IsSome -> s.DataMap.Value + | _ -> DataMap.init() member this.getSelectedColumnHeader = if this.SelectedCells.IsEmpty then None else let columnIndex = this.SelectedCells |> Set.toList |> List.minBy fst |> fst @@ -110,6 +122,8 @@ type Msg = | MoveColumn of current:int * next:int | UpdateActiveCell of (U2 * ColumnType) option | SetActiveCellFromSelected +| UpdateDatamap of DataMap option +| UpdateDataMapDataContextAt of index: int * DataContext | AddTable of ArcTable | RemoveTable of index:int | RenameTable of index:int * name:string diff --git a/src/Client/States/SpreadsheetInterface.fs b/src/Client/States/SpreadsheetInterface.fs index e39a541e..997f5ff7 100644 --- a/src/Client/States/SpreadsheetInterface.fs +++ b/src/Client/States/SpreadsheetInterface.fs @@ -9,6 +9,8 @@ type Msg = | Initialize of Swatehost | CreateAnnotationTable of tryUsePrevOutput:bool | RemoveBuildingBlock +| UpdateDatamap of DataMap option +| UpdateDataMapDataContextAt of index: int * DataContext | AddTable of ArcTable | AddAnnotationBlock of CompositeColumn | AddAnnotationBlocks of CompositeColumn [] diff --git a/src/Client/Update/InterfaceUpdate.fs b/src/Client/Update/InterfaceUpdate.fs index 4cb5b9dd..38ed6f40 100644 --- a/src/Client/Update/InterfaceUpdate.fs +++ b/src/Client/Update/InterfaceUpdate.fs @@ -90,6 +90,25 @@ module Interface = let cmd = Spreadsheet.CreateAnnotationTable usePrevOutput |> SpreadsheetMsg |> Cmd.ofMsg model, cmd | _ -> failwith "not implemented" + | UpdateDatamap datamapOption -> + match host with + | Some Swatehost.Excel -> + failwith "UpdateDatamap not implemented for Excel" + model, Cmd.none + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + let cmd = Spreadsheet.UpdateDatamap datamapOption |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd + | _ -> failwith "not implemented" + | UpdateDataMapDataContextAt (index,dc) -> + match host with + | Some Swatehost.Excel -> + //let cmd = OfficeInterop.UpdateDataContextAt (dc, index) |> OfficeInteropMsg |> Cmd.ofMsg + failwith "UpdateDataContextAt not implemented for Excel" + model, Cmd.none + | Some Swatehost.Browser | Some Swatehost.ARCitect -> + let cmd = Spreadsheet.UpdateDataMapDataContextAt (index, dc) |> SpreadsheetMsg |> Cmd.ofMsg + model, cmd + | _ -> failwith "not implemented" | AddTable table -> match host with | Some Swatehost.Excel -> diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index c6bcff54..3b9fa3e5 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -4,11 +4,9 @@ open Messages open Elmish open Spreadsheet open LocalHistory +open Spreadsheet open Model open Shared -open Spreadsheet.Table -open Spreadsheet.BuildingBlocks -open Spreadsheet.Clipboard open Fable.Remoting.Client open FsSpreadsheet open FsSpreadsheet.Js @@ -74,20 +72,26 @@ module Spreadsheet = match msg with | UpdateState nextState -> nextState, model, Cmd.none + | UpdateDatamap datamapOption -> + let nextState = Controller.DataMap.updateDatamap datamapOption state + nextState, model, Cmd.none + | UpdateDataMapDataContextAt (index, dtx) -> + let nextState = Controller.DataMap.updateDataMapDataContextAt dtx index state + nextState, model, Cmd.none | AddTable table -> - let nextState = Controller.addTable table state + let nextState = Controller.Table.addTable table state nextState, model, Cmd.none | CreateAnnotationTable usePrevOutput -> - let nextState = Controller.createTable usePrevOutput state + let nextState = Controller.Table.createTable usePrevOutput state nextState, model, Cmd.none | AddAnnotationBlock column -> - let nextState = Controller.addBuildingBlock column state + let nextState = Controller.BuildingBlocks.addBuildingBlock column state nextState, model, Cmd.none | AddAnnotationBlocks columns -> - let nextState = Controller.addBuildingBlocks columns state + let nextState = Controller.BuildingBlocks.addBuildingBlocks columns state nextState, model, Cmd.none | JoinTable (table, index, options) -> - let nextState = Controller.joinTable table index options state + let nextState = Controller.BuildingBlocks.joinTable table index options state nextState, model, Cmd.none | UpdateArcFile arcFile -> let nextState = { state with ArcFile = Some arcFile } @@ -96,7 +100,7 @@ module Spreadsheet = let nextState = Spreadsheet.Model.init(arcFile) nextState, model, Cmd.none | InsertOntologyAnnotation oa -> - let nextState = Controller.insertTerm_IntoSelected oa state + let nextState = Controller.BuildingBlocks.insertTerm_IntoSelected oa state nextState, model, Cmd.none | InsertOntologyAnnotations oas -> failwith "InsertOntologyTerms not implemented in Spreadsheet.Update" @@ -126,13 +130,13 @@ module Spreadsheet = } nextState, model, Cmd.none | RemoveTable removeIndex -> - let nextState = Controller.removeTable removeIndex state + let nextState = Controller.Table.removeTable removeIndex state nextState, model, Cmd.none | RenameTable (index, name) -> - let nextState = Controller.renameTable index name state + let nextState = Controller.Table.renameTable index name state nextState, model, Cmd.none | UpdateTableOrder (prev_index, new_index) -> - let nextState = Controller.updateTableOrder (prev_index, new_index) state + let nextState = Controller.Table.updateTableOrder (prev_index, new_index) state nextState, model, Cmd.none | UpdateHistoryPosition (newPosition) -> let nextState, nextModel = @@ -148,26 +152,26 @@ module Spreadsheet = nextState, nextModel nextState, nextModel, Cmd.none | AddRows (n) -> - let nextState = Controller.addRows n state + let nextState = Controller.Table.addRows n state nextState, model, Cmd.none | Reset -> - let nextState = Controller.resetTableState() + let nextState = Controller.Table.resetTableState() let nextModel = {model with History = LocalHistory.Model.init()} nextState, nextModel, Cmd.none | DeleteRow index -> - let nextState = Controller.deleteRow index state + let nextState = Controller.Table.deleteRow index state nextState, model, Cmd.none | DeleteRows indexArr -> - let nextState = Controller.deleteRows indexArr state + let nextState = Controller.Table.deleteRows indexArr state nextState, model, Cmd.none | DeleteColumn index -> - let nextState = Controller.deleteColumn index state + let nextState = Controller.Table.deleteColumn index state nextState, model, Cmd.none | SetColumn (index, column) -> - let nextState = Controller.setColumn index column state + let nextState = Controller.Table.setColumn index column state nextState, model, Cmd.none | MoveColumn (current, next) -> - let nextState = Controller.moveColumn current next state + let nextState = Controller.Table.moveColumn current next state nextState, model, Cmd.none | UpdateSelectedCells nextSelectedCells -> let nextState = {state with SelectedCells = nextSelectedCells} @@ -183,7 +187,12 @@ module Spreadsheet = | Key.Up -> (0,-1) | Key.Left -> (-1,0) | Key.Right -> (1,0) - let nextIndex = Controller.selectRelativeCell state.SelectedCells.MinimumElement moveBy state.ActiveTable + let maxColIndex, maxRowIndex = + match state.ActiveView with + | ActiveView.Table _ -> (state.ActiveTable.ColumnCount-1), (state.ActiveTable.RowCount-1) + | ActiveView.DataMap -> DataMap.ColumnCount-1 , state.DataMapOrDefault.DataContexts.Count-1 + | _ -> (state.ActiveTable.ColumnCount-1), (state.ActiveTable.RowCount-1) // This does not matter + let nextIndex = Controller.Table.selectRelativeCell state.SelectedCells.MinimumElement moveBy maxColIndex maxRowIndex let s = Set([nextIndex]) UpdateSelectedCells s |> SpreadsheetMsg |> Cmd.ofMsg state, model, cmd @@ -202,48 +211,48 @@ module Spreadsheet = | CopyCell index -> let cmd = Cmd.OfPromise.attempt - (Controller.copyCellByIndex index) + (Controller.Clipboard.copyCellByIndex index) state (curry GenericError Cmd.none >> DevMsg) state, model, cmd | CopyCells indices -> let cmd = Cmd.OfPromise.attempt - (Controller.copyCellsByIndex indices) + (Controller.Clipboard.copyCellsByIndex indices) state (curry GenericError Cmd.none >> DevMsg) state, model, cmd | CopySelectedCell -> let cmd = Cmd.OfPromise.attempt - (Controller.copySelectedCell) + (Controller.Clipboard.copySelectedCell) state (curry GenericError Cmd.none >> DevMsg) state, model, cmd | CopySelectedCells -> let cmd = Cmd.OfPromise.attempt - (Controller.copySelectedCells) + (Controller.Clipboard.copySelectedCells) state (curry GenericError Cmd.none >> DevMsg) state, model, cmd | CutCell index -> - let nextState = Controller.cutCellByIndex index state + let nextState = Controller.Clipboard.cutCellByIndex index state nextState, model, Cmd.none | CutSelectedCell -> let nextState = if state.SelectedCells.IsEmpty then state else - Controller.cutSelectedCell state + Controller.Clipboard.cutSelectedCell state nextState, model, Cmd.none | CutSelectedCells -> let nextState = if state.SelectedCells.IsEmpty then state else - Controller.cutSelectedCells state + Controller.Clipboard.cutSelectedCells state nextState, model, Cmd.none | PasteCell index -> let cmd = Cmd.OfPromise.either - (Clipboard.Controller.pasteCellByIndex index) + (Controller.Clipboard.pasteCellByIndex index) state (UpdateState >> SpreadsheetMsg) (curry GenericError Cmd.none >> DevMsg) @@ -251,7 +260,7 @@ module Spreadsheet = | PasteCellsExtend index -> let cmd = Cmd.OfPromise.either - (Clipboard.Controller.pasteCellsByIndexExtend index) + (Controller.Clipboard.pasteCellsByIndexExtend index) state (UpdateState >> SpreadsheetMsg) (curry GenericError Cmd.none >> DevMsg) @@ -259,7 +268,7 @@ module Spreadsheet = | PasteSelectedCell -> let cmd = Cmd.OfPromise.either - (Clipboard.Controller.pasteCellIntoSelected) + (Controller.Clipboard.pasteCellIntoSelected) state (UpdateState >> SpreadsheetMsg) (curry GenericError Cmd.none >> DevMsg) @@ -267,20 +276,20 @@ module Spreadsheet = | PasteSelectedCells -> let cmd = Cmd.OfPromise.either - (Clipboard.Controller.pasteCellsIntoSelected) + (Controller.Clipboard.pasteCellsIntoSelected) state (UpdateState >> SpreadsheetMsg) (curry GenericError Cmd.none >> DevMsg) state, model, cmd | Clear indices -> - let nextState = Controller.clearCells indices state + let nextState = Controller.Table.clearCells indices state nextState, model, Cmd.none | ClearSelected -> let indices = state.SelectedCells |> Set.toArray - let nextState = Controller.clearCells indices state + let nextState = Controller.Table.clearCells indices state nextState, model, Cmd.none | FillColumnWithTerm index -> - let nextState = Controller.fillColumnWithCell index state + let nextState = Controller.Table.fillColumnWithCell index state nextState, model, Cmd.none //| EditColumn (columnIndex, newCellType, b_type) -> // let cmd = createPromiseCmd <| fun _ -> Controller.editColumn (columnIndex, newCellType, b_type) state diff --git a/src/Client/Views/MainWindowView.fs b/src/Client/Views/MainWindowView.fs index 3513f796..fb2ea977 100644 --- a/src/Client/Views/MainWindowView.fs +++ b/src/Client/Views/MainWindowView.fs @@ -41,9 +41,11 @@ let private SpreadsheetSelectionFooter (model: Model) dispatch = prop.children [ Html.ul [ Bulma.tab [ - prop.style [style.width (length.px 20)] + prop.style [style.width (length.px 20); style.custom ("order", -2)] ] MainComponents.FooterTabs.MainMetadata (model, dispatch) + if model.SpreadsheetModel.HasDataMap() then + MainComponents.FooterTabs.MainDataMap (model, dispatch) for index in 0 .. (model.SpreadsheetModel.Tables.TableCount-1) do MainComponents.FooterTabs.Main (index, model.SpreadsheetModel.Tables, model, dispatch) if model.SpreadsheetModel.CanHaveTables() then diff --git a/src/Client/Views/SplitWindowView.fs b/src/Client/Views/SplitWindowView.fs index a71668a9..8540a028 100644 --- a/src/Client/Views/SplitWindowView.fs +++ b/src/Client/Views/SplitWindowView.fs @@ -103,7 +103,6 @@ open Model [] let Main (left:seq) (right:seq) (mainModel:Model) (dispatch: Messages.Msg -> unit) = let (model, setModel) = React.useState(SplitWindow.init) - 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 [ @@ -112,7 +111,7 @@ let Main (left:seq) (right:seq) (mainModel:Model) (d ] prop.children [ MainComponents.MainViewContainer.Main(minWidth, left) - if isNotMetadataSheet && mainModel.PersistentStorageState.ShowSideBar then + if mainModel.SpreadsheetModel.TableViewIsActive() && mainModel.PersistentStorageState.ShowSideBar then sidebarCombinedElement(sidebarId, model, setModel, dispatch, right) ] ] \ No newline at end of file diff --git a/src/Client/Views/XlsxFileView.fs b/src/Client/Views/XlsxFileView.fs index 888603cf..d0409dcf 100644 --- a/src/Client/Views/XlsxFileView.fs +++ b/src/Client/Views/XlsxFileView.fs @@ -34,4 +34,6 @@ let Main(model: Model, dispatch: Messages.Msg -> unit, openBuildingBlockWidget, Html.none ] ] - ] \ No newline at end of file + ] + | ActiveView.DataMap -> + MainComponents.DataMap.DataMap.Main (model, dispatch) \ No newline at end of file diff --git a/src/Client/style.scss b/src/Client/style.scss index 908a1806..615688f8 100644 --- a/src/Client/style.scss +++ b/src/Client/style.scss @@ -20,6 +20,14 @@ $primarye-invert: white; @tailwind components; @tailwind utilities; +a { + color: var(--bulma-link-text); + cursor: pointer; + text-decoration: none; + transition-duration: var(--bulma-duration); + transition-property: background-color, border-color, color; +} + .my-grey-out { background-color: cv.getVar("scheme-main-ter"); color: cv.getVar("link") diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index d557c0ac..28167332 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -137,6 +137,26 @@ module Extensions = open ARCtrl.Template open ArcTableAux + type DataMap with + static member ColumnCount = 9 + + type DataFile with + member this.ToStringRdb() = + match this with + | DataFile.DerivedDataFile -> "Derived Data File" + | DataFile.ImageFile -> "Image File" + | DataFile.RawDataFile -> "Raw Data File" + static member tryFromString (str: string) = + match str.ToLower() with + | "derived data file" | "deriveddatafile" -> Some DataFile.DerivedDataFile + | "image file" | "imagefile" -> Some DataFile.ImageFile + | "raw data file" | "rawdatafile" -> Some DataFile.RawDataFile + | _ -> None + static member fromString (str:string) = + match DataFile.tryFromString str with + | Some r -> r + | None -> failwithf "Unknown DataFile: %s" str + type OntologyAnnotation with static member empty() = OntologyAnnotation.create() static member fromTerm (term:Term) = OntologyAnnotation(term.Name, term.FK_Ontology, term.Accession) @@ -220,7 +240,14 @@ module Extensions = | anyElse -> sprintf "Unable to convert \"%A\" to CompositeCell." anyElse |> Error type CompositeCell with - + + member this.ToDataCell() = + match this with + | CompositeCell.Unitized (_, unit) -> CompositeCell.createDataFromString unit.NameText + | CompositeCell.FreeText txt -> CompositeCell.createDataFromString txt + | CompositeCell.Term term -> CompositeCell.createDataFromString term.NameText + | CompositeCell.Data _ -> this + static member tryFromContent (content: string []) = match tryFromContent' content with | Ok r -> Some r @@ -248,23 +275,36 @@ module Extensions = 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 + match this with + // term header + | CompositeCell.Term _ when header.IsTermColumn -> this + | CompositeCell.Unitized _ when header.IsTermColumn -> this + | CompositeCell.FreeText _ when header.IsTermColumn -> this.ToTermCell() + | CompositeCell.Data _ when header.IsTermColumn -> this.ToFreeTextCell().ToTermCell() + // data header + | CompositeCell.Term _ when header.IsDataColumn -> this.ToDataCell() + | CompositeCell.Unitized _ when header.IsDataColumn -> this.ToDataCell() + | CompositeCell.FreeText _ when header.IsDataColumn -> this.ToDataCell() + | CompositeCell.Data _ when header.IsDataColumn -> this + // freetext header? + | CompositeCell.Term _ | CompositeCell.Unitized _ -> this.ToFreeTextCell() + | CompositeCell.FreeText _ -> this + | CompositeCell.Data _ -> this.ToFreeTextCell() + member this.UpdateWithOA(oa:OntologyAnnotation) = match this with | CompositeCell.Term _ -> CompositeCell.createTerm oa | CompositeCell.Unitized (v,_) -> CompositeCell.createUnitized (v,oa) | CompositeCell.FreeText _ -> CompositeCell.createFreeText oa.NameText + | CompositeCell.Data _ -> CompositeCell.createDataFromString oa.NameText member this.ToOA() = match this with | CompositeCell.Term oa -> oa | CompositeCell.Unitized (v, oa) -> oa | CompositeCell.FreeText t -> OntologyAnnotation.create t + | CompositeCell.Data d -> OntologyAnnotation.create d.NameText member this.UpdateMainField(s: string) = match this with @@ -273,6 +313,9 @@ module Extensions = CompositeCell.Term oa | CompositeCell.Unitized (_, oa) -> CompositeCell.Unitized (s, oa) | CompositeCell.FreeText _ -> CompositeCell.FreeText s + | CompositeCell.Data d -> + d.Name <- Some s + CompositeCell.Data d /// /// Will return `this` if executed on Freetext cell. diff --git a/src/Shared/Shared.fsproj b/src/Shared/Shared.fsproj index df6fb09b..276a0336 100644 --- a/src/Shared/Shared.fsproj +++ b/src/Shared/Shared.fsproj @@ -14,12 +14,12 @@ - - - - - - - + + + + + + + \ No newline at end of file From 57669ed8d71be0b63fe1a633511a0fc5f5d40043 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 16:40:18 +0200 Subject: [PATCH 19/25] Also check obsolete metadata sheet names during upload #476 --- src/Client/Spreadsheet/IO.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Client/Spreadsheet/IO.fs b/src/Client/Spreadsheet/IO.fs index 85ba5567..44c5d894 100644 --- a/src/Client/Spreadsheet/IO.fs +++ b/src/Client/Spreadsheet/IO.fs @@ -15,11 +15,11 @@ module Xlsx = let ws = fswb.GetWorksheets() let arcfile = match ws with - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name ) -> + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcAssay.metaDataSheetName = ws.Name || ARCtrl.Spreadsheet.ArcAssay.obsoleteMetaDataSheetName = ws.Name) -> ArcAssay.fromFsWorkbook fswb |> Assay - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name ) -> + | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcStudy.metaDataSheetName = ws.Name || ARCtrl.Spreadsheet.ArcStudy.obsoleteMetaDataSheetName = ws.Name) -> ArcStudy.fromFsWorkbook fswb |> Study - | _ when ws.Exists (fun ws -> ARCtrl.Spreadsheet.ArcInvestigation.metaDataSheetName = ws.Name ) -> + | _ 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 From 7967e76fc8690296e344a1e7b265c37c9359a293 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 21:27:36 +0200 Subject: [PATCH 20/25] Improve DataMap implementation --- src/Client/MainComponents/Cells.fs | 12 +- src/Client/MainComponents/ContextMenu.fs | 367 ++++++++++++------- src/Client/MainComponents/DataMap/DataMap.fs | 6 +- src/Client/MainComponents/Metadata/Assay.fs | 40 +- src/Client/Spreadsheet/Controller/DataMap.fs | 20 +- src/Client/Update/SpreadsheetUpdate.fs | 18 +- 6 files changed, 298 insertions(+), 165 deletions(-) diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index f58e4fd2..a2fb3775 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -48,7 +48,8 @@ module private CellAux = |> Option.iter (fun nextHeader -> Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch) let oasetter (index, nextCell: CompositeCell, dispatch) = Msg.UpdateCell (index, nextCell) |> SpreadsheetMsg |> dispatch - + + let contextMenuController index model dispatch = if model.SpreadsheetModel.TableViewIsActive() then ContextMenu.Table.onContextMenu (index, model, dispatch) else ContextMenu.DataMap.onContextMenu (index, model, dispatch) open CellComponents open CellAux @@ -166,6 +167,7 @@ type Cell = prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}-{columnType}" prop.id $"Header_{columnIndex}_{columnType}" cellStyle [] + prop.onContextMenu (CellAux.contextMenuController (columnIndex, -1) model dispatch) prop.className "main-contrast-bg" prop.children [ Html.div [ @@ -234,7 +236,7 @@ type Cell = ]] [] - static member BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), model: Model, dispatch, ?oasetter: {|oa: OntologyAnnotation; setter: OntologyAnnotation -> unit|}, ?displayValue, ?readonly: bool) = + static member BodyBase(columnType: ColumnType, cellValue: string, setter: string -> unit, index: (int*int), model: Model, dispatch, ?oasetter: {|oa: OntologyAnnotation; setter: OntologyAnnotation -> unit|}, ?displayValue, ?readonly: bool, ?tooltip: string) = let readonly = defaultArg readonly false let columnIndex, rowIndex = index let state = model.SpreadsheetModel @@ -258,12 +260,14 @@ type Cell = [|box isSelected|] ) Html.td [ + if tooltip.IsSome then prop.title tooltip.Value prop.key $"Cell_{state.ActiveView.TableIndex}-{columnIndex}-{rowIndex}" cellStyle [ if isSelected then style.backgroundColor(NFDIColors.Mint.Lighter80) ] + prop.readOnly readonly prop.ref ref - prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + prop.onContextMenu (CellAux.contextMenuController index model dispatch) prop.children [ Html.div [ cellInnerContainerStyle [] @@ -310,7 +314,7 @@ type Cell = prop.key $"Cell_Select_{columnIndex}_{rowIndex}" cellStyle [] prop.ref ref - prop.onContextMenu <| ContextMenu.onContextMenu (index, model, dispatch) + prop.onContextMenu (CellAux.contextMenuController index model dispatch) prop.children [ Html.div [ cellInnerContainerStyle [] diff --git a/src/Client/MainComponents/ContextMenu.fs b/src/Client/MainComponents/ContextMenu.fs index f14285e4..a7c860d6 100644 --- a/src/Client/MainComponents/ContextMenu.fs +++ b/src/Client/MainComponents/ContextMenu.fs @@ -6,148 +6,247 @@ open Spreadsheet open ARCtrl open Model -type private ContextFunctions = { - DeleteRow : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit - DeleteColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit - MoveColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit - 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 - UpdateAllCells : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit - //EditColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit - RowIndex : int - ColumnIndex : int -} +module Table = + type private ContextFunctions = { + DeleteRow : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + DeleteColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + MoveColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + 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 + UpdateAllCells : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + //EditColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + RowIndex : int + ColumnIndex : int + } + + let private isUnitOrTermCell (cell: CompositeCell option) = + cell.IsSome && not cell.Value.isFreeText -let private isUnitOrTermCell (cell: CompositeCell option) = - cell.IsSome && not cell.Value.isFreeText + let private isHeader (rowIndex: int) = rowIndex < 0 -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 - prop.onContextMenu(fun e -> e.preventDefault(); rmv e) - prop.style [ - style.position.fixedRelativeToWindow - style.backgroundColor.transparent - style.left 0 - style.top 0 - style.right 0 - style.bottom 0 - style.display.block + 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 isHeader = isHeader funcs.RowIndex + let rmv_element = Html.div [ + prop.onClick rmv + prop.onContextMenu(fun e -> e.preventDefault(); rmv e) + prop.style [ + style.position.fixedRelativeToWindow + style.backgroundColor.transparent + style.left 0 + style.top 0 + style.right 0 + style.bottom 0 + style.display.block + ] ] - ] - let button (name:string, icon: string, msg, props) = Html.li [ - Bulma.button.button [ - prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] - prop.onClick msg - prop.className "py-1" - Bulma.button.isFullWidth - //Bulma.button.isSmall - Bulma.color.isBlack - Bulma.button.isInverted - yield! props - prop.children [ - Bulma.icon [Html.i [prop.className icon]] - Html.span name + let button (name:string, icon: string, msg, props) = Html.li [ + Bulma.button.button [ + prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] + prop.onClick msg + prop.className "py-1" + Bulma.button.isFullWidth + //Bulma.button.isSmall + Bulma.color.isBlack + Bulma.button.isInverted + yield! props + prop.children [ + Bulma.icon [Html.i [prop.className icon]] + Html.span name + ] ] ] - ] - let divider = Html.li [ - Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] - ] - let buttonList = [ - //button ("Edit Column", "fa-solid fa-table-columns", funcs.EditColumn rmv, []) - button ("Fill Column", "fa-solid fa-pen", 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, []) - else - button ("Update Column", "fa-solid fa-ellipsis-vertical", funcs.UpdateAllCells 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, []) - 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, []) - button ("Move Column", "fa-solid fa-arrow-right-arrow-left", funcs.MoveColumn rmv, []) - ] - Html.div [ - prop.style [ - style.backgroundColor "white" - style.position.absolute - style.left mousex - style.top (mousey - 40) - style.width 150 - style.zIndex 40 // to overlap navbar - style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) + let divider = Html.li [ + Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] + ] + let buttonList = [ + //button ("Edit Column", "fa-solid fa-table-columns", funcs.EditColumn rmv, []) + if not isHeader then + button ("Fill Column", "fa-solid fa-pen", 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, []) + else + button ("Update Column", "fa-solid fa-ellipsis-vertical", funcs.UpdateAllCells 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, []) + 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, []) + button ("Move Column", "fa-solid fa-arrow-right-arrow-left", funcs.MoveColumn rmv, []) ] - prop.children [ - rmv_element - Html.ul buttonList + Html.div [ + prop.style [ + style.backgroundColor "white" + style.position.absolute + style.left mousex + style.top (mousey - 40) + style.width 150 + style.zIndex 40 // to overlap navbar + style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) + ] + prop.children [ + rmv_element + Html.ul buttonList + ] ] - ] -open Shared + open Shared -let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Types.MouseEvent) -> - e.stopPropagation() - e.preventDefault() - let mousePosition = int e.pageX, int e.pageY - /// if there are selected cells in the same column as the clicked event, delete all selected rows. - let deleteRowEvent _ = - let s = Set.toArray model.SpreadsheetModel.SelectedCells - if Array.isEmpty s |> not && Array.forall (fun (c,r) -> c = fst index) s && Array.contains index s then - let indexArr = s |> Array.map snd |> Array.distinct - 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 isSelectedCell = model.SpreadsheetModel.SelectedCells.Contains index - //let editColumnEvent _ = Modals.Controller.renderModal("EditColumn_Modal", Modals.EditColumn.Main (fst index) model dispatch) - let triggerMoveColumnModal _ = Modals.Controller.renderModal("MoveColumn_Modal", Modals.MoveColumn.Main(fst index, model, dispatch)) - let triggerUpdateColumnModal _ = - let columnIndex = fst index - let column = model.SpreadsheetModel.ActiveTable.GetColumn columnIndex - Modals.Controller.renderModal("UpdateColumn_Modal", Modals.UpdateColumn.Main(fst index, column, dispatch)) - let funcs = { - DeleteRow = fun rmv e -> rmv e; deleteRowEvent e - DeleteColumn = fun rmv e -> rmv e; Spreadsheet.DeleteColumn (fst index) |> Messages.SpreadsheetMsg |> dispatch - MoveColumn = fun rmv e -> rmv e; triggerMoveColumnModal e - Copy = fun rmv e -> - rmv e; - if isSelectedCell then - Spreadsheet.CopySelectedCells |> Messages.SpreadsheetMsg |> dispatch - else - Spreadsheet.CopyCell index |> Messages.SpreadsheetMsg |> dispatch - Cut = fun rmv e -> rmv e; Spreadsheet.CutCell index |> Messages.SpreadsheetMsg |> dispatch - Paste = fun rmv e -> - rmv e; - if isSelectedCell then - Spreadsheet.PasteSelectedCells |> Messages.SpreadsheetMsg |> dispatch + let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Types.MouseEvent) -> + e.stopPropagation() + e.preventDefault() + let ci, ri = index + let mousePosition = int e.pageX, int e.pageY + /// if there are selected cells in the same column as the clicked event, delete all selected rows. + let deleteRowEvent _ = + let s = Set.toArray model.SpreadsheetModel.SelectedCells + if Array.isEmpty s |> not && Array.forall (fun (c,r) -> c = ci) s && Array.contains index s then + let indexArr = s |> Array.map snd |> Array.distinct + Spreadsheet.DeleteRows indexArr |> Messages.SpreadsheetMsg |> dispatch else - 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() - rmv e; Spreadsheet.UpdateCell (index, nextCell) |> Messages.SpreadsheetMsg |> dispatch - UpdateAllCells = fun rmv e -> rmv e; triggerUpdateColumnModal e - //EditColumn = fun rmv e -> rmv e; editColumnEvent e - RowIndex = snd index - ColumnIndex = fst index + Spreadsheet.DeleteRow (ri) |> Messages.SpreadsheetMsg |> dispatch + let cell = model.SpreadsheetModel.ActiveTable.TryGetCellAt(ci, ri) + let isSelectedCell = model.SpreadsheetModel.SelectedCells.Contains index + //let editColumnEvent _ = Modals.Controller.renderModal("EditColumn_Modal", Modals.EditColumn.Main (fst index) model dispatch) + let triggerMoveColumnModal _ = Modals.Controller.renderModal("MoveColumn_Modal", Modals.MoveColumn.Main(ci, model, dispatch)) + let triggerUpdateColumnModal _ = + let columnIndex = fst index + let column = model.SpreadsheetModel.ActiveTable.GetColumn columnIndex + Modals.Controller.renderModal("UpdateColumn_Modal", Modals.UpdateColumn.Main(ci, column, dispatch)) + let funcs = { + DeleteRow = fun rmv e -> rmv e; deleteRowEvent e + DeleteColumn = fun rmv e -> rmv e; Spreadsheet.DeleteColumn (ci) |> Messages.SpreadsheetMsg |> dispatch + MoveColumn = fun rmv e -> rmv e; triggerMoveColumnModal e + Copy = fun rmv e -> + rmv e; + if isSelectedCell then + Spreadsheet.CopySelectedCells |> Messages.SpreadsheetMsg |> dispatch + else + Spreadsheet.CopyCell index |> Messages.SpreadsheetMsg |> dispatch + Cut = fun rmv e -> rmv e; Spreadsheet.CutCell index |> Messages.SpreadsheetMsg |> dispatch + Paste = fun rmv e -> + rmv e; + if isSelectedCell then + Spreadsheet.PasteSelectedCells |> Messages.SpreadsheetMsg |> dispatch + else + 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() + rmv e; Spreadsheet.UpdateCell (index, nextCell) |> Messages.SpreadsheetMsg |> dispatch + UpdateAllCells = fun rmv e -> rmv e; triggerUpdateColumnModal e + //EditColumn = fun rmv e -> rmv e; editColumnEvent e + RowIndex = snd index + ColumnIndex = fst index + } + let child = contextmenu mousePosition funcs cell + let name = $"context_{mousePosition}" + Modals.Controller.renderModal(name, child) + +module DataMap = + type private ContextFunctions = { + DeleteRow : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + //EditColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + RowIndex : int + ColumnIndex : int } - let child = contextmenu mousePosition funcs cell - let name = $"context_{mousePosition}" - Modals.Controller.renderModal(name, child) \ No newline at end of file + + 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 + prop.onContextMenu(fun e -> e.preventDefault(); rmv e) + prop.style [ + style.position.fixedRelativeToWindow + style.backgroundColor.transparent + style.left 0 + style.top 0 + style.right 0 + style.bottom 0 + style.display.block + ] + ] + let button (name:string, icon: string, msg, props) = Html.li [ + Bulma.button.button [ + prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] + prop.onClick msg + prop.className "py-1" + Bulma.button.isFullWidth + //Bulma.button.isSmall + Bulma.color.isBlack + Bulma.button.isInverted + yield! props + prop.children [ + Bulma.icon [Html.i [prop.className icon]] + Html.span name + ] + ] + ] + let divider = Html.li [ + Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] + ] + let buttonList = [ + button ("Delete Row", "fa-solid fa-delete-left", funcs.DeleteRow rmv, []) + ] + Html.div [ + prop.style [ + style.backgroundColor "white" + style.position.absolute + style.left mousex + style.top (mousey - 40) + style.width 150 + style.zIndex 40 // to overlap navbar + style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) + ] + prop.children [ + rmv_element + Html.ul buttonList + ] + ] + + open Shared + + let onContextMenu (index: int*int, model: Model, dispatch) = fun (e: Browser.Types.MouseEvent) -> + e.stopPropagation() + e.preventDefault() + let mousePosition = int e.pageX, int e.pageY + /// if there are selected cells in the same column as the clicked event, delete all selected rows. + let deleteRowEvent _ = + let s = Set.toArray model.SpreadsheetModel.SelectedCells + if Array.isEmpty s |> not && Array.forall (fun (c,r) -> c = fst index) s && Array.contains index s then + let indexArr = s |> Array.map snd |> Array.distinct + 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 isSelectedCell = model.SpreadsheetModel.SelectedCells.Contains index + //let editColumnEvent _ = Modals.Controller.renderModal("EditColumn_Modal", Modals.EditColumn.Main (fst index) model dispatch) + let triggerMoveColumnModal _ = Modals.Controller.renderModal("MoveColumn_Modal", Modals.MoveColumn.Main(fst index, model, dispatch)) + let triggerUpdateColumnModal _ = + let columnIndex = fst index + let column = model.SpreadsheetModel.ActiveTable.GetColumn columnIndex + Modals.Controller.renderModal("UpdateColumn_Modal", Modals.UpdateColumn.Main(fst index, column, dispatch)) + let funcs = { + DeleteRow = fun rmv e -> rmv e; deleteRowEvent e + //EditColumn = fun rmv e -> rmv e; editColumnEvent e + RowIndex = snd index + ColumnIndex = fst index + } + 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/MainComponents/DataMap/DataMap.fs b/src/Client/MainComponents/DataMap/DataMap.fs index 1ad8c24b..dc015c7f 100644 --- a/src/Client/MainComponents/DataMap/DataMap.fs +++ b/src/Client/MainComponents/DataMap/DataMap.fs @@ -69,13 +69,13 @@ module private Components = yield! [ fun i -> Cells.Header (i, Spreadsheet.Main, "Data Name") - fun i -> Cells.Header (i, Spreadsheet.Main, "Data FilePath") + fun i -> Cells.Header (i, Spreadsheet.Main, "Data File Path") fun i -> Cells.Header (i, Spreadsheet.Main, "Data Selector") fun i -> Cells.Header (i, Spreadsheet.Main, "Data Selector Format") //fun i -> Cells.Header (i, Spreadsheet.Main, "Data File Type") fun i -> Cells.Header (i, Spreadsheet.Main, "Data Format") fun i -> Cells.Header (i, Spreadsheet.Main, "Description") - fun i -> Cells.Header (i, Spreadsheet.Main, "GeneratedBy") + fun i -> Cells.Header (i, Spreadsheet.Main, "Generated By") fun i -> Cells.Header (i, Spreadsheet.Main, "Explication") fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") @@ -100,7 +100,7 @@ module private Components = /// let Body (value: string option, setter, index: int * int, model: Model.Model, dispatch: Messages.Msg -> unit, readonly: bool option) = let value = value |> Option.defaultValue "" - Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.Main, value, setter, index,model, dispatch, ?readonly=readonly) + Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.Main, value, setter, index,model, dispatch, ?readonly=readonly, tooltip="This field is calculated from `Data File Path` and `Data Selector`") let BodyOntologyAnnotation (value: OntologyAnnotation option, setter: OntologyAnnotation option -> unit, index: int * int, model: Model.Model, dispatch: Messages.Msg -> unit) = let value = defaultArg value (OntologyAnnotation()) diff --git a/src/Client/MainComponents/Metadata/Assay.fs b/src/Client/MainComponents/Metadata/Assay.fs index ba03ccab..d280296f 100644 --- a/src/Client/MainComponents/Metadata/Assay.fs +++ b/src/Client/MainComponents/Metadata/Assay.fs @@ -59,26 +59,26 @@ let Main(assay: ArcAssay, model: Model, dispatch: Msg -> unit) = DatamapConfig.Main( assay.DataMap, fun dtm -> - logw "HARDCODED DTM EXTENSION!" - let create_Datacontext (i:int) = - DataContext( - $"id_string_{i}", - "My Name", - DataFile.DerivedDataFile, - "My Format", - "My Selector Format", - OntologyAnnotation("Explication", "MS", "MS:123456"), - OntologyAnnotation("Unit", "MS", "MS:123456"), - OntologyAnnotation("ObjectType", "MS", "MS:123456"), - "My Label", - "My Description", - "KevinF.exe", - (ResizeArray [Comment.create("Hello", "World")]) - ) - dtm |> Option.iter (fun dtm -> - for i in 0 .. 5 do - dtm.DataContexts.Add (create_Datacontext i) - ) + //logw "HARDCODED DTM EXTENSION!" + //let create_Datacontext (i:int) = + // DataContext( + // $"id_string_{i}", + // "My Name", + // DataFile.DerivedDataFile, + // "My Format", + // "My Selector Format", + // OntologyAnnotation("Explication", "MS", "MS:123456"), + // OntologyAnnotation("Unit", "MS", "MS:123456"), + // OntologyAnnotation("ObjectType", "MS", "MS:123456"), + // "My Label", + // "My Description", + // "KevinF.exe", + // (ResizeArray [Comment.create("Hello", "World")]) + // ) + //dtm |> Option.iter (fun dtm -> + // for i in 0 .. 5 do + // dtm.DataContexts.Add (create_Datacontext i) + //) dtm |> SpreadsheetInterface.UpdateDatamap |> InterfaceMsg |> dispatch ) ] \ No newline at end of file diff --git a/src/Client/Spreadsheet/Controller/DataMap.fs b/src/Client/Spreadsheet/Controller/DataMap.fs index 6335b777..407f4d5e 100644 --- a/src/Client/Spreadsheet/Controller/DataMap.fs +++ b/src/Client/Spreadsheet/Controller/DataMap.fs @@ -40,4 +40,22 @@ let updateDataMapDataContextAt (dtx) (index) (state: Spreadsheet.Model) : Spread | _ -> logw "[WARNING] updateDatamap: No Assay or Study found in ArcFile" state.ArcFile - {state with ArcFile = nextArcFile} \ No newline at end of file + {state with ArcFile = nextArcFile} + +let addRows (n:int) (state: Spreadsheet.Model) : Spreadsheet.Model = + let rows = Array.init n (fun _ -> DataContext()) + if state.HasDataMap() then + state.DataMapOrDefault.DataContexts.AddRange(rows) + {state with ArcFile = state.ArcFile} + +let deleteRow (n:int) (state: Spreadsheet.Model) : Spreadsheet.Model = + if state.HasDataMap() then + state.DataMapOrDefault.DataContexts.RemoveAt n + {state with ArcFile = state.ArcFile} + +let deleteRows (rows:int []) (state: Spreadsheet.Model) : Spreadsheet.Model = + if state.HasDataMap() then + rows + |> Array.sortDescending + |> Array.iter (fun n -> state.DataMapOrDefault.DataContexts.RemoveAt n) + {state with ArcFile = state.ArcFile} \ No newline at end of file diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index 3b9fa3e5..66d2385e 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -152,17 +152,29 @@ module Spreadsheet = nextState, nextModel nextState, nextModel, Cmd.none | AddRows (n) -> - let nextState = Controller.Table.addRows n state + let nextState = + if state.TableViewIsActive() then + Controller.Table.addRows n state + else + Controller.DataMap.addRows n state nextState, model, Cmd.none | Reset -> let nextState = Controller.Table.resetTableState() let nextModel = {model with History = LocalHistory.Model.init()} nextState, nextModel, Cmd.none | DeleteRow index -> - let nextState = Controller.Table.deleteRow index state + let nextState = + if state.TableViewIsActive() then + Controller.Table.deleteRow index state + else + Controller.DataMap.deleteRow index state nextState, model, Cmd.none | DeleteRows indexArr -> - let nextState = Controller.Table.deleteRows indexArr state + let nextState = + if state.TableViewIsActive() then + Controller.Table.deleteRows indexArr state + else + Controller.DataMap.deleteRows indexArr state nextState, model, Cmd.none | DeleteColumn index -> let nextState = Controller.Table.deleteColumn index state From 2687f7d582d063a3cb63e20436dbf0b694a33bcb Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 21:29:53 +0200 Subject: [PATCH 21/25] Update README.md :books: --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e5a79108..f9694aaa 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ Swate runs on localhost:8080 (and swobup on localhost:8000). ``` Usage: ./build.cmd -run (--nodb) Start .net backend server, vite frontend (and database, - swobup with docker if not `--nodb`) +run [db] Start .net backend server, vite frontend (and database, + swobup with docker if `db`) -release (pre) Run .net tests tag current branch and force push to +release [pre] Run .net tests tag current branch and force push to release branch (nightly if `pre`), this will trigger Github release with docker image From 005cc4722326459d1853051aec26931f9d9878c9 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 21:42:46 +0200 Subject: [PATCH 22/25] Release v1.0.0-beta.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 4bd2e3df..4ea679b3 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.04" - let [] AssemblyMetadata_ReleaseDate = "28.06.2024" + let [] AssemblyMetadata_Version = "v1.0.0-beta.05" + let [] AssemblyMetadata_ReleaseDate = "12.07.2024" From a47fe02c8943597f545c16b550dc266fd09e994f Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 21:45:58 +0200 Subject: [PATCH 23/25] Fix dockerfile.publish :bug: --- build/Dockerfile.publish | 1 - 1 file changed, 1 deletion(-) diff --git a/build/Dockerfile.publish b/build/Dockerfile.publish index 4aa18723..1e991f90 100644 --- a/build/Dockerfile.publish +++ b/build/Dockerfile.publish @@ -12,7 +12,6 @@ RUN apt-get update && apt-get install nodejs -y WORKDIR /workspace COPY ../. . RUN dotnet tool restore -RUN dotnet paket install RUN dotnet run --project ./build/Build.fsproj bundle From 80c94c9ce765f6d35c802c347987e81e47f8aa27 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Fri, 12 Jul 2024 21:58:15 +0200 Subject: [PATCH 24/25] =?UTF-8?q?fix=20docker=20publish=20=F0=9F=98=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1 - src/Client/Client.fs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c15efa1..ae5d9d64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "Swate", "dependencies": { "@nfdi4plants/exceljs": "^0.3.0", "bulma": "^1.0.1", diff --git a/src/Client/Client.fs b/src/Client/Client.fs index 7ee323ee..7850f0fe 100644 --- a/src/Client/Client.fs +++ b/src/Client/Client.fs @@ -60,6 +60,7 @@ let ARCitect_subscription (initial: Model) : (SubId * Subscribe) l open Elmish.Debug open Elmish.HMR #endif +open Elmish.React //+:cnd:noEmit Program.mkProgram Init.init Update.update View @@ -69,7 +70,7 @@ Program.mkProgram Init.init Update.update View #endif |> Program.withSubscription ARCitect_subscription |> Program.toNavigable (parsePath Routing.Routing.route) Update.urlUpdate -|> Program.withReactSynchronous "elmish-app" +|> Program.withReactBatched "elmish-app" #if DEBUG //|> Program.withDebuggerCoders CustomDebugger.modelEncoder CustomDebugger.modelDecoder |> Program.withDebugger From cedfbd987831d0a216177ae23f414a3807b31903 Mon Sep 17 00:00:00 2001 From: Kevin F Date: Mon, 15 Jul 2024 16:32:46 +0200 Subject: [PATCH 25/25] Refactor datamap to use existing logic paths, by adding a generic(ui) get/setCell logic. --- src/Client/Client.fsproj | 2 +- src/Client/MainComponents/Cells.fs | 22 +- src/Client/MainComponents/ContextMenu.fs | 191 +++++++++--------- src/Client/MainComponents/DataMap/Cells.fs | 31 --- src/Client/MainComponents/DataMap/DataMap.fs | 187 +++++------------ .../Spreadsheet/Controller/BuildingBlocks.fs | 11 +- .../Spreadsheet/Controller/Clipboard.fs | 26 +-- src/Client/Spreadsheet/Controller/Generic.fs | 45 +++++ src/Client/Spreadsheet/Controller/Table.fs | 17 +- src/Client/States/Spreadsheet.fs | 9 + src/Client/Update/SpreadsheetUpdate.fs | 7 +- src/Shared/ARCtrl.Helper.fs | 71 +++++++ 12 files changed, 309 insertions(+), 310 deletions(-) delete mode 100644 src/Client/MainComponents/DataMap/Cells.fs create mode 100644 src/Client/Spreadsheet/Controller/Generic.fs diff --git a/src/Client/Client.fsproj b/src/Client/Client.fsproj index fbdb1c33..46317c4a 100644 --- a/src/Client/Client.fsproj +++ b/src/Client/Client.fsproj @@ -53,6 +53,7 @@ + @@ -99,7 +100,6 @@ - diff --git a/src/Client/MainComponents/Cells.fs b/src/Client/MainComponents/Cells.fs index a2fb3775..c06950d4 100644 --- a/src/Client/MainComponents/Cells.fs +++ b/src/Client/MainComponents/Cells.fs @@ -155,9 +155,10 @@ 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 HeaderBase(columnType: ColumnType, setter: string -> unit, cellValue: string, columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch, ?readonly: bool) = + let readonly = defaultArg readonly false let state = model.SpreadsheetModel - let isReadOnly = columnType = Unit + let isReadOnly = (columnType = Unit || readonly) let makeIdle() = UpdateActiveCell None |> SpreadsheetMsg |> dispatch let makeActive() = UpdateActiveCell (Some (!^columnIndex, columnType)) |> SpreadsheetMsg |> dispatch let isIdle = state.CellIsIdle (!^columnIndex, columnType) @@ -166,6 +167,7 @@ type Cell = if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter prop.key $"Header_{state.ActiveView.TableIndex}-{columnIndex}-{columnType}" prop.id $"Header_{columnIndex}_{columnType}" + prop.readOnly readonly cellStyle [] prop.onContextMenu (CellAux.contextMenuController (columnIndex, -1) model dispatch) prop.className "main-contrast-bg" @@ -194,7 +196,7 @@ type Cell = ] ] - static member Header(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + static member Header(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch, ?readonly: bool) = let cellValue = header.ToString() let setter = fun (s: string) -> @@ -207,22 +209,22 @@ type Cell = | _ -> failwith "this should never happen" nextHeader <- nextHeader.UpdateWithOA updatedOA Msg.UpdateHeader (columnIndex, nextHeader) |> SpreadsheetMsg |> dispatch - Cell.HeaderBase(Main, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + Cell.HeaderBase(Main, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch, ?readonly=readonly) - static member HeaderUnit(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + static member HeaderUnit(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch, ?readonly) = let cellValue = "Unit" let setter = fun (s: string) -> () - Cell.HeaderBase(Unit, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + Cell.HeaderBase(Unit, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch, ?readonly=readonly) - static member HeaderTSR(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + static member HeaderTSR(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch, ?readonly) = let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" let setter = fun (s: string) -> headerTANSetter(columnIndex, s, header, dispatch) - Cell.HeaderBase(TSR, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + Cell.HeaderBase(TSR, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch, ?readonly=readonly) - static member HeaderTAN(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch) = + static member HeaderTAN(columnIndex: int, header: CompositeHeader, state_extend: Set, setState_extend, model: Model, dispatch, ?readonly) = let cellValue = header.TryOA() |> Option.map (fun oa -> oa.TermAccessionShort) |> Option.defaultValue "" let setter = fun (s: string) -> headerTANSetter(columnIndex, s, header, dispatch) - Cell.HeaderBase(TAN, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch) + Cell.HeaderBase(TAN, setter, cellValue, columnIndex, header, state_extend, setState_extend, model, dispatch, ?readonly=readonly) static member Empty() = Html.td [ cellStyle []; prop.readOnly true; prop.children [ diff --git a/src/Client/MainComponents/ContextMenu.fs b/src/Client/MainComponents/ContextMenu.fs index a7c860d6..9d4cb773 100644 --- a/src/Client/MainComponents/ContextMenu.fs +++ b/src/Client/MainComponents/ContextMenu.fs @@ -6,6 +6,46 @@ open Spreadsheet open ARCtrl open Model +module private Shared = + + let isUnitOrTermCell (cell: CompositeCell option) = + cell.IsSome && not cell.Value.isFreeText + + let isHeader (rowIndex: int) = rowIndex < 0 + + let rmv_element rmv= Html.div [ + prop.onClick rmv + prop.onContextMenu(fun e -> e.preventDefault(); rmv e) + prop.style [ + style.position.fixedRelativeToWindow + style.backgroundColor.transparent + style.left 0 + style.top 0 + style.right 0 + style.bottom 0 + style.display.block + ] + ] + let button (name:string, icon: string, msg, props) = Html.li [ + Bulma.button.button [ + prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] + prop.onClick msg + prop.className "py-1" + Bulma.button.isFullWidth + //Bulma.button.isSmall + Bulma.color.isBlack + Bulma.button.isInverted + yield! props + prop.children [ + Bulma.icon [Html.i [prop.className icon]] + Html.span name + ] + ] + ] + let divider = Html.li [ + Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] + ] + module Table = type private ContextFunctions = { DeleteRow : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit @@ -24,65 +64,28 @@ module Table = ColumnIndex : int } - let private isUnitOrTermCell (cell: CompositeCell option) = - cell.IsSome && not cell.Value.isFreeText - - let private isHeader (rowIndex: int) = rowIndex < 0 - 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 isHeader = isHeader funcs.RowIndex - let rmv_element = Html.div [ - prop.onClick rmv - prop.onContextMenu(fun e -> e.preventDefault(); rmv e) - prop.style [ - style.position.fixedRelativeToWindow - style.backgroundColor.transparent - style.left 0 - style.top 0 - style.right 0 - style.bottom 0 - style.display.block - ] - ] - let button (name:string, icon: string, msg, props) = Html.li [ - Bulma.button.button [ - prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] - prop.onClick msg - prop.className "py-1" - Bulma.button.isFullWidth - //Bulma.button.isSmall - Bulma.color.isBlack - Bulma.button.isInverted - yield! props - prop.children [ - Bulma.icon [Html.i [prop.className icon]] - Html.span name - ] - ] - ] - let divider = Html.li [ - Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] - ] + let isHeader = Shared.isHeader funcs.RowIndex let buttonList = [ //button ("Edit Column", "fa-solid fa-table-columns", funcs.EditColumn rmv, []) if not isHeader then - button ("Fill Column", "fa-solid fa-pen", funcs.FillColumn rmv, []) - if isUnitOrTermCell contextCell then + Shared.button ("Fill Column", "fa-solid fa-pen", funcs.FillColumn rmv, []) + if Shared.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, []) + Shared.button (text, "fa-solid fa-arrow-right-arrow-left", funcs.TransformCell rmv, []) else - button ("Update Column", "fa-solid fa-ellipsis-vertical", funcs.UpdateAllCells 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, []) - 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, []) - button ("Move Column", "fa-solid fa-arrow-right-arrow-left", funcs.MoveColumn rmv, []) + Shared.button ("Update Column", "fa-solid fa-ellipsis-vertical", funcs.UpdateAllCells rmv, []) + Shared.button ("Clear", "fa-solid fa-eraser", funcs.Clear rmv, []) + Shared.divider + Shared.button ("Copy", "fa-solid fa-copy", funcs.Copy rmv, []) + Shared.button ("Cut", "fa-solid fa-scissors", funcs.Cut rmv, []) + Shared.button ("Paste", "fa-solid fa-paste", funcs.Paste rmv, []) + Shared.button ("Paste All", "fa-solid fa-paste", funcs.PasteAll rmv, []) + Shared.divider + Shared.button ("Delete Row", "fa-solid fa-delete-left", funcs.DeleteRow rmv, []) + Shared.button ("Delete Column", "fa-solid fa-delete-left fa-rotate-270", funcs.DeleteColumn rmv, []) + Shared.button ("Move Column", "fa-solid fa-arrow-right-arrow-left", funcs.MoveColumn rmv, []) ] Html.div [ prop.style [ @@ -95,7 +98,7 @@ module Table = style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) ] prop.children [ - rmv_element + Shared.rmv_element rmv Html.ul buttonList ] ] @@ -160,48 +163,34 @@ module Table = module DataMap = type private ContextFunctions = { - DeleteRow : (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 //EditColumn : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit + // + 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 + // + DeleteRow : (Browser.Types.MouseEvent -> unit) -> Browser.Types.MouseEvent -> unit RowIndex : int ColumnIndex : int } 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 - prop.onContextMenu(fun e -> e.preventDefault(); rmv e) - prop.style [ - style.position.fixedRelativeToWindow - style.backgroundColor.transparent - style.left 0 - style.top 0 - style.right 0 - style.bottom 0 - style.display.block - ] - ] - let button (name:string, icon: string, msg, props) = Html.li [ - Bulma.button.button [ - prop.style [style.borderRadius 0; style.justifyContent.spaceBetween; style.fontSize (length.rem 0.9)] - prop.onClick msg - prop.className "py-1" - Bulma.button.isFullWidth - //Bulma.button.isSmall - Bulma.color.isBlack - Bulma.button.isInverted - yield! props - prop.children [ - Bulma.icon [Html.i [prop.className icon]] - Html.span name - ] - ] - ] - let divider = Html.li [ - Html.div [ prop.style [style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base); style.margin(2,0); style.width (length.perc 75); style.marginLeft length.auto] ] - ] + let isHeader = Shared.isHeader funcs.RowIndex let buttonList = [ - button ("Delete Row", "fa-solid fa-delete-left", funcs.DeleteRow rmv, []) + if not isHeader then + Shared.button ("Fill Column", "fa-solid fa-pen", funcs.FillColumn rmv, []) + Shared.button ("Clear", "fa-solid fa-eraser", funcs.Clear rmv, []) + Shared.divider + Shared.button ("Copy", "fa-solid fa-copy", funcs.Copy rmv, []) + Shared.button ("Cut", "fa-solid fa-scissors", funcs.Cut rmv, []) + Shared.button ("Paste", "fa-solid fa-paste", funcs.Paste rmv, []) + Shared.button ("Paste All", "fa-solid fa-paste", funcs.PasteAll rmv, []) + Shared.divider + Shared.button ("Delete Row", "fa-solid fa-delete-left", funcs.DeleteRow rmv, []) ] Html.div [ prop.style [ @@ -214,7 +203,7 @@ module DataMap = style.border(1, borderStyle.solid, NFDIColors.DarkBlue.Base) ] prop.children [ - rmv_element + Shared.rmv_element rmv Html.ul buttonList ] ] @@ -242,11 +231,31 @@ module DataMap = let column = model.SpreadsheetModel.ActiveTable.GetColumn columnIndex Modals.Controller.renderModal("UpdateColumn_Modal", Modals.UpdateColumn.Main(fst index, column, dispatch)) let funcs = { + FillColumn = fun rmv e -> rmv e; Spreadsheet.FillColumnWithTerm index |> Messages.SpreadsheetMsg |> dispatch DeleteRow = fun rmv e -> rmv e; deleteRowEvent e + Clear = fun rmv e -> rmv e; if isSelectedCell then Spreadsheet.ClearSelected |> Messages.SpreadsheetMsg |> dispatch else Spreadsheet.Clear [|index|] |> Messages.SpreadsheetMsg |> dispatch + Copy = fun rmv e -> + rmv e; + if isSelectedCell then + Spreadsheet.CopySelectedCells |> Messages.SpreadsheetMsg |> dispatch + else + Spreadsheet.CopyCell index |> Messages.SpreadsheetMsg |> dispatch + Cut = fun rmv e -> rmv e; Spreadsheet.CutCell index |> Messages.SpreadsheetMsg |> dispatch + Paste = fun rmv e -> + rmv e; + if isSelectedCell then + Spreadsheet.PasteSelectedCells |> Messages.SpreadsheetMsg |> dispatch + else + Spreadsheet.PasteCell index |> Messages.SpreadsheetMsg |> dispatch + PasteAll = fun rmv e -> + rmv e; + Spreadsheet.PasteCellsExtend index |> Messages.SpreadsheetMsg |> dispatch //EditColumn = fun rmv e -> rmv e; editColumnEvent e RowIndex = snd index ColumnIndex = fst index } - let child = contextmenu mousePosition funcs cell - let name = $"context_{mousePosition}" - Modals.Controller.renderModal(name, child) \ No newline at end of file + let isHeader = Shared.isHeader funcs.RowIndex + if not isHeader then + 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/MainComponents/DataMap/Cells.fs b/src/Client/MainComponents/DataMap/Cells.fs deleted file mode 100644 index b6da6c4c..00000000 --- a/src/Client/MainComponents/DataMap/Cells.fs +++ /dev/null @@ -1,31 +0,0 @@ -namespace MainComponents.DataMap - -open Feliz -open Feliz.Bulma -open Spreadsheet -open Model -open Messages - -open Fable.Core.JsInterop -open ARCtrl -open MainComponents.CellStyles - -type Cells = - - static member Header(index:int, columnType: ColumnType, header: string) = - let id = $"Datamap_Header_{header}_{index}" - Html.th [ - if columnType.IsRefColumn then Bulma.color.hasBackgroundGreyLighter - prop.key id - prop.id id - cellStyle [] - prop.className "main-contrast-bg" - prop.children [ - Html.div [ - cellInnerContainerStyle [style.custom("backgroundColor","inherit")] - prop.children [ - basicValueDisplayCell header - ] - ] - ] - ] \ No newline at end of file diff --git a/src/Client/MainComponents/DataMap/DataMap.fs b/src/Client/MainComponents/DataMap/DataMap.fs index dc015c7f..8f4d6576 100644 --- a/src/Client/MainComponents/DataMap/DataMap.fs +++ b/src/Client/MainComponents/DataMap/DataMap.fs @@ -8,152 +8,58 @@ open SpreadsheetInterface open Messages open Shared -module private Helper = - - let updateFilePath (dtx: DataContext) (index: int) (dispatch: Messages.Msg -> unit) (newVal: string) = - let newVal = if newVal = "" then None else Some newVal - dtx.FilePath <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateSelector (dtx: DataContext) (index: int) (dispatch) (newVal: string) = - let newVal = if newVal = "" then None else Some newVal - dtx.Selector <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateSelectorFormat (dtx: DataContext) (index: int) (dispatch) (newVal: string) = - let newVal = if newVal = "" then None else Some newVal - dtx.SelectorFormat <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let dataFileTrytoString (dtf: DataFile option) = - dtf |> Option.map _.ToStringRdb() |> Option.defaultValue "None" - - let updateDataFile (dtx: DataContext) (index: int) (dispatch) (newVal: string) = - let newVal = DataFile.tryFromString newVal - dtx.DataType <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateFormat (dtx: DataContext) (index: int) (dispatch) (newVal: string) = - let newVal = if newVal = "" then None else Some newVal - dtx.Format <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateDescription (dtx: DataContext) (index: int) (dispatch) (newVal: string) = - let newVal = if newVal = "" then None else Some newVal - dtx.Description <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateGeneratedBy (dtx: DataContext) (index: int) (dispatch) (newVal: string) = - let newVal = if newVal = "" then None else Some newVal - dtx.GeneratedBy <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateExplication (dtx: DataContext) (index: int) (dispatch) (newVal: OntologyAnnotation option) = - dtx.Explication <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateUnit (dtx: DataContext) (index: int) (dispatch) (newVal: OntologyAnnotation option) = - dtx.Unit <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - - let updateObjectType (dtx: DataContext) (index: int) (dispatch) (newVal: OntologyAnnotation option) = - dtx.ObjectType <- newVal - UpdateDataMapDataContextAt(index, dtx) |> InterfaceMsg |> dispatch - module private Components = - /// https://github.com/nfdi4plants/ARC-specification/blob/main/ISA-XLSX.md#examples-2 - let HeaderRow (state:Set) setState (model:Model) (dispatch: Msg -> unit) = - Html.tr [ - prop.children [ - MainComponents.CellStyles.RowLabel -1 - yield! - [ - fun i -> Cells.Header (i, Spreadsheet.Main, "Data Name") - fun i -> Cells.Header (i, Spreadsheet.Main, "Data File Path") - fun i -> Cells.Header (i, Spreadsheet.Main, "Data Selector") - fun i -> Cells.Header (i, Spreadsheet.Main, "Data Selector Format") - //fun i -> Cells.Header (i, Spreadsheet.Main, "Data File Type") - fun i -> Cells.Header (i, Spreadsheet.Main, "Data Format") - fun i -> Cells.Header (i, Spreadsheet.Main, "Description") - fun i -> Cells.Header (i, Spreadsheet.Main, "Generated By") - fun i -> Cells.Header (i, Spreadsheet.Main, "Explication") - fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") - fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") - fun i -> Cells.Header (i, Spreadsheet.Main, "Unit") - fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") - fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") - fun i -> Cells.Header (i, Spreadsheet.Main, "Object Type") - fun i -> Cells.Header (i, Spreadsheet.TSR, "Term Source REF") - fun i -> Cells.Header (i, Spreadsheet.TAN, "Term Accession Number") - ] - |> List.mapi (fun i f -> f i) - ] - ] - - /// - /// let columnIndex, rowIndex = index - /// - /// - /// - /// let columnIndex, rowIndex = index - /// - /// - let Body (value: string option, setter, index: int * int, model: Model.Model, dispatch: Messages.Msg -> unit, readonly: bool option) = - let value = value |> Option.defaultValue "" - Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.Main, value, setter, index,model, dispatch, ?readonly=readonly, tooltip="This field is calculated from `Data File Path` and `Data Selector`") - - let BodyOntologyAnnotation (value: OntologyAnnotation option, setter: OntologyAnnotation option -> unit, index: int * int, model: Model.Model, dispatch: Messages.Msg -> unit) = - let value = defaultArg value (OntologyAnnotation()) - let setter = fun (oa:OntologyAnnotation) -> - if oa.isEmpty() then None else Some oa - |> setter - let oaSetter = {| - oa = value; - setter = fun (oa: OntologyAnnotation) -> oa |> setter - |} - let vMain = value.Name |> Option.defaultValue "" - let setterMain = fun (s:string) -> - value.Name <- if s = "" then None else Some s - setter value - // The same helper functions for TSR - let vTSR = value.TermSourceREF |> Option.defaultValue "" - let setterTSR = fun (s:string) -> - value.TermSourceREF <- if s = "" then None else Some s - setter value - // the same helper for tan - let vTAN = value.TermAccessionNumber |> Option.defaultValue "" - let setterTAN = fun (s:string) -> - value.TermAccessionNumber <- if s = "" then None else Some s - setter value - [ - Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.Main, vMain, setterMain, index, model, dispatch, oasetter=oaSetter) - Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.TSR, vTSR, setterTSR, index, model, dispatch) - Spreadsheet.Cells.Cell.BodyBase(Spreadsheet.ColumnType.TAN, vTAN, setterTAN, index, model, dispatch) - ] + let NameIndex = -1 - let BodyRow (dtx: DataContext) (rowIndex: int) (state:Set) (model:Model) (dispatch: Messages.Msg -> unit) = - let mkIndex (col: int) = (col,rowIndex) - let DataMapBaseBody (field, updateFunc) i = Body(field, updateFunc dtx rowIndex dispatch, mkIndex i, model, dispatch, None) - let DataMapBaseBodyOA (field, updateFunc) i = BodyOntologyAnnotation(field, updateFunc dtx rowIndex dispatch, mkIndex i, model, dispatch) + let private BodyRow (rowIndex: int) (state:Set) (model:Model) (dispatch: Msg -> unit) = Html.tr [ MainComponents.CellStyles.RowLabel rowIndex - -1 |> fun i -> Body(dtx.Name, (fun _ -> ()), mkIndex i, model, dispatch, Some true) - 0 |> DataMapBaseBody(dtx.FilePath, Helper.updateFilePath) - 1 |> DataMapBaseBody(dtx.Selector, Helper.updateSelector) - 2 |> DataMapBaseBody(dtx.SelectorFormat, Helper.updateSelectorFormat) - //3 |> fun i -> Spreadsheet.Cells.Cell.BodySelect(Helper.dataFileTrytoString dtx.DataType, (Helper.updateDataFile dtx rowIndex dispatch), ["None"; DataFile.DerivedDataFile.ToStringRdb(); DataFile.ImageFile.ToStringRdb(); DataFile.RawDataFile.ToStringRdb()], mkIndex i, model, dispatch) - 3 |> DataMapBaseBody(dtx.Format, Helper.updateFormat) - 4 |> DataMapBaseBody(dtx.Description, Helper.updateDescription) - 5 |> DataMapBaseBody(dtx.GeneratedBy, Helper.updateGeneratedBy) - yield! 6 |> DataMapBaseBodyOA(dtx.Explication, Helper.updateExplication) - yield! 7 |> DataMapBaseBodyOA(dtx.Unit, Helper.updateUnit) - yield! 8 |> DataMapBaseBodyOA(dtx.ObjectType, Helper.updateObjectType) + Spreadsheet.Cells.Cell.BodyBase( + Spreadsheet.ColumnType.Main, + model.SpreadsheetModel.DataMapOrDefault.DataContexts.[rowIndex].Name |> Option.defaultValue "", + (fun _ -> ()), + (NameIndex, rowIndex), + model, + dispatch, + readonly=true, + tooltip="This field is calculated from `Data File Path` and `Data Selector`" + ) + for columnIndex in 0 .. (model.SpreadsheetModel.DataMapOrDefault.ColumnCount-1) do + let index = columnIndex, rowIndex + let cell = model.SpreadsheetModel.DataMapOrDefault.GetCell(columnIndex, rowIndex) + Spreadsheet.Cells.Cell.Body (index, cell, model, dispatch) + let isExtended = state.Contains columnIndex + if isExtended && (cell.isTerm || cell.isUnitized) then + if cell.isUnitized then + Spreadsheet.Cells.Cell.BodyUnit(index, cell, model, dispatch) + else + Spreadsheet.Cells.Cell.Empty() + Spreadsheet.Cells.Cell.BodyTSR(index, cell, model, dispatch) + Spreadsheet.Cells.Cell.BodyTAN(index, cell, model, dispatch) ] - let BodyRows (dtm: DataMap) (state:Set) (model:Model) (dispatch: Msg -> unit) = + let BodyRows (state:Set) (model:Model) (dispatch: Msg -> unit) = Html.tbody [ - for ri in 0 .. (dtm.DataContexts.Count-1) do - yield BodyRow dtm.DataContexts.[ri] ri state model dispatch + for rowInd in 0 .. model.SpreadsheetModel.DataMapOrDefault.RowCount-1 do + yield BodyRow rowInd state model dispatch + ] + + let HeaderRow (state:Set) setState (model:Model) (dispatch: Msg -> unit) = + let headers = [| + for i in 0 .. DataMap.ColumnCount-1 do DataMap.getHeader i + |] + let dtm = model.SpreadsheetModel.DataMapOrDefault + Html.tr [ + MainComponents.CellStyles.RowLabel -1 + Spreadsheet.Cells.Cell.Header(NameIndex, CompositeHeader.FreeText "Data Name", state, setState, model, dispatch, readonly = true) + for columnIndex in 0 .. (dtm.ColumnCount-1) do + let header = headers.[columnIndex] + Spreadsheet.Cells.Cell.Header(columnIndex, header, state, setState, model, dispatch, readonly = true) + let isExtended = state.Contains columnIndex + if isExtended then + Spreadsheet.Cells.Cell.HeaderUnit(columnIndex, header, state, setState, model, dispatch, readonly = true) + Spreadsheet.Cells.Cell.HeaderTSR(columnIndex, header, state, setState, model, dispatch, readonly = true) + Spreadsheet.Cells.Cell.HeaderTAN(columnIndex, header, state, setState, model, dispatch, readonly = true) ] type DataMap = @@ -162,7 +68,6 @@ type DataMap = static member Main(model: Model, dispatch: Msg -> unit) = let ref = React.useElementRef() let state, setState : Set * (Set -> unit) = React.useState(Set.empty) - let dtm = model.SpreadsheetModel.DataMapOrDefault Html.div [ prop.id "SPREADSHEET_MAIN_VIEW" prop.tabIndex 0 @@ -176,7 +81,7 @@ type DataMap = Html.thead [ Components.HeaderRow state setState model dispatch ] - Components.BodyRows dtm state model dispatch + Components.BodyRows state model dispatch ] ] ] diff --git a/src/Client/Spreadsheet/Controller/BuildingBlocks.fs b/src/Client/Spreadsheet/Controller/BuildingBlocks.fs index 42074dc8..cff53e05 100644 --- a/src/Client/Spreadsheet/Controller/BuildingBlocks.fs +++ b/src/Client/Spreadsheet/Controller/BuildingBlocks.fs @@ -69,17 +69,12 @@ let joinTable(tableToAdd: ArcTable) (index: int option) (options: TableJoinOptio {state with ArcFile = state.ArcFile} let insertTerm_IntoSelected (term:OntologyAnnotation) (state: Spreadsheet.Model) : Spreadsheet.Model = - let table = state.ActiveTable let selected = state.SelectedCells |> Set.toArray SanityChecks.verifyOnlyOneColumnSelected selected - let column = table.GetColumn(fst selected[0]) //can use [0] as we verify we only have one column selected. for (colIndex, rowIndex) in selected do - let c = table.TryGetCellAt(colIndex,rowIndex) - let newCell = - match c with - | Some cc -> cc.UpdateWithOA term - | None -> column.GetDefaultEmptyCell().UpdateWithOA term - table.UpdateCellAt(colIndex,rowIndex, newCell) + let c = Generic.getCell (colIndex,rowIndex) state + let newCell = c.UpdateWithOA term + Controller.Generic.setCell (colIndex,rowIndex) newCell state {state with ArcFile = state.ArcFile} //let insertCells_IntoSelected (term:CompositeCell []) (state: Spreadsheet.Model) : Spreadsheet.Model = diff --git a/src/Client/Spreadsheet/Controller/Clipboard.fs b/src/Client/Spreadsheet/Controller/Clipboard.fs index 4d4f6fe3..5f25b34d 100644 --- a/src/Client/Spreadsheet/Controller/Clipboard.fs +++ b/src/Client/Spreadsheet/Controller/Clipboard.fs @@ -13,11 +13,11 @@ let copyCells (cells: CompositeCell []) : JS.Promise = navigator.clipboard.writeText(tab) let copyCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise = - let cell = state.ActiveTable.Values.[index] + let cell = Generic.getCell index state copyCell cell let copyCellsByIndex (indices: (int*int) []) (state: Spreadsheet.Model) : JS.Promise = - let cells = [|for index in indices do yield state.ActiveTable.Values.[index] |] + let cells = [| for index in indices do yield Generic.getCell index state |] copyCells cells let copySelectedCell (state: Spreadsheet.Model) : JS.Promise = @@ -31,20 +31,20 @@ let copySelectedCells (state: Spreadsheet.Model) : JS.Promise = copyCellsByIndex indices state let cutCellByIndex (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - let cell = state.ActiveTable.Values.[index] + let cell = Generic.getCell index state // Remove selected cell value let emptyCell = cell.GetEmptyCell() - state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) + Generic.setCell index emptyCell state copyCell cell |> Promise.start state let cutCellsByIndices (indices: (int*int) []) (state: Spreadsheet.Model) : Spreadsheet.Model = let cells = ResizeArray() for index in indices do - let cell = state.ActiveTable.Values.[index] + let cell = Generic.getCell index state // Remove selected cell value let emptyCell = cell.GetEmptyCell() - state.ActiveTable.UpdateCellAt(fst index,snd index, emptyCell) + Generic.setCell index emptyCell state cells.Add(cell) copyCells (Array.ofSeq cells) |> Promise.start state @@ -62,20 +62,20 @@ let cutSelectedCells (state: Spreadsheet.Model) : Spreadsheet.Model = let pasteCellByIndex (index: int*int) (state: Spreadsheet.Model) : JS.Promise = promise { let! tab = navigator.clipboard.readText() - let header = state.ActiveTable.Headers.[fst index] + let header = Generic.getHeader (fst index) state let cell = CompositeCell.fromTabTxt tab |> Array.head |> _.ConvertToValidCell(header) - state.ActiveTable.SetCellAt(fst index, snd index, cell) + Generic.setCell index cell state return state } let pasteCellsByIndexExtend (index: int*int) (state: Spreadsheet.Model) : JS.Promise = promise { let! tab = navigator.clipboard.readText() - let header = state.ActiveTable.Headers.[fst index] + let header = Generic.getHeader (fst index) state 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 + Generic.setCells indexedCells state return state } @@ -94,18 +94,18 @@ let pasteCellsIntoSelected (state: Spreadsheet.Model) : JS.Promise Set.filter (fun index -> fst index = columnIndex) promise { let! tab = navigator.clipboard.readText() - let header = state.ActiveTable.Headers.[columnIndex] + let header = Generic.getHeader columnIndex state let cells = CompositeCell.fromTabTxt tab |> Array.map _.ConvertToValidCell(header) if cells.Length = 1 then let cell = cells.[0] let newCells = selectedSingleColumnCells |> Array.ofSeq |> Array.map (fun index -> index, cell) - state.ActiveTable.SetCellsAt newCells + Generic.setCells newCells state return state else 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 + Generic.setCells indexedCellsTrimmed state return state } \ No newline at end of file diff --git a/src/Client/Spreadsheet/Controller/Generic.fs b/src/Client/Spreadsheet/Controller/Generic.fs new file mode 100644 index 00000000..3a646042 --- /dev/null +++ b/src/Client/Spreadsheet/Controller/Generic.fs @@ -0,0 +1,45 @@ +module Spreadsheet.Controller.Generic + +open Spreadsheet +open ARCtrl +open Shared + +let getCell ((ci,ri): int*int) (state: Spreadsheet.Model) : CompositeCell = + match state.ActiveView with + | IsTable -> state.ActiveTable.Values[ci,ri] + | IsDataMap -> state.DataMapOrDefault.GetCell(ci,ri) + | IsMetadata -> failwith "Cannot get cell in metadata view" + +let setCell ((ci,ri): int*int) (cell: CompositeCell) (state: Spreadsheet.Model) : unit = + match state.ActiveView with + | IsTable -> state.ActiveTable.UpdateCellAt(ci,ri,cell) + | IsDataMap -> state.DataMapOrDefault.SetCell(ci,ri,cell) + | IsMetadata -> failwith "Cannot set cell in metadata view" + +let setCells (cells: ((int*int)*CompositeCell) []) (state: Spreadsheet.Model) : unit = + match state.ActiveView with + | IsTable -> + state.ActiveTable.SetCellsAt cells + | IsDataMap -> + for ((ci,ri),cell) in cells do + state.DataMapOrDefault.SetCell(ci,ri,cell) + | IsMetadata -> + failwith "Unable to UpdateCell on Metadata sheet" + +let getHeader (index: int) (state: Spreadsheet.Model) : CompositeHeader = + match state.ActiveView with + | IsTable -> state.ActiveTable.Headers.[index] + | IsDataMap -> state.DataMapOrDefault.GetHeader(index) + | IsMetadata -> failwith "Cannot get header in metadata view" + +let getRowCount (state: Spreadsheet.Model) : int = + match state.ActiveView with + | IsTable -> state.ActiveTable.RowCount + | IsDataMap -> state.DataMapOrDefault.RowCount + | IsMetadata -> failwith "Cannot get row count in metadata view" + +let getColCount (state: Spreadsheet.Model) : int = + match state.ActiveView with + | IsTable -> state.ActiveTable.ColumnCount + | IsDataMap -> state.DataMapOrDefault.ColumnCount + | IsMetadata -> failwith "Cannot get column count in metadata view" \ No newline at end of file diff --git a/src/Client/Spreadsheet/Controller/Table.fs b/src/Client/Spreadsheet/Controller/Table.fs index 8588544e..e9afd2b7 100644 --- a/src/Client/Spreadsheet/Controller/Table.fs +++ b/src/Client/Spreadsheet/Controller/Table.fs @@ -135,15 +135,11 @@ let moveColumn (current: int) (next: int) (state: Spreadsheet.Model) : Spreadshe SelectedCells = Set.empty } let fillColumnWithCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet.Model = - let cell = state.ActiveTable.TryGetCellAt index + let cell = Generic.getCell index state let columnIndex = fst index - state.ActiveTable.IteriColumns(fun i column -> - let cell = cell|> Option.defaultValue (column.GetDefaultEmptyCell()) - if i = columnIndex then - for cellRowIndex in 0 .. column.Cells.Length-1 do - let cell = cell.Copy() - state.ActiveTable.UpdateCellAt(columnIndex, cellRowIndex, cell) - ) + for ri in 0 .. Generic.getRowCount state - 1 do + let copy = cell.Copy() + Generic.setCell (columnIndex, ri) cell state {state with ArcFile = state.ArcFile} /// @@ -152,14 +148,13 @@ let fillColumnWithCell (index: int*int) (state: Spreadsheet.Model) : Spreadsheet /// /// 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 cell = Generic.getCell index state let emptyCell = cell.GetEmptyCell() index, emptyCell |] - table.SetCellsAt newCells + Generic.setCells newCells state state open Fable.Core diff --git a/src/Client/States/Spreadsheet.fs b/src/Client/States/Spreadsheet.fs index 532b642f..aa63ff75 100644 --- a/src/Client/States/Spreadsheet.fs +++ b/src/Client/States/Spreadsheet.fs @@ -29,6 +29,15 @@ with | DataMap -> 0 | Metadata -> -1 +[] +module ActivePattern = + + let (|IsTable|IsDataMap|IsMetadata|) (input:ActiveView) = + match input with + | ActiveView.Table _ -> IsTable + | ActiveView.DataMap -> IsDataMap + | ActiveView.Metadata -> IsMetadata + ///If you change this model, it will kill caching for users! if you apply changes to it, make sure to keep a version ///of it and add a try case for it to `tryInitFromLocalStorage` in Spreadsheet/LocalStorage.fs . type Model = { diff --git a/src/Client/Update/SpreadsheetUpdate.fs b/src/Client/Update/SpreadsheetUpdate.fs index 66d2385e..3780fd3d 100644 --- a/src/Client/Update/SpreadsheetUpdate.fs +++ b/src/Client/Update/SpreadsheetUpdate.fs @@ -108,13 +108,12 @@ module Spreadsheet = let cmd = Cmd.none state, model, cmd | UpdateCell (index, cell) -> - let nextState = - state.ActiveTable.UpdateCellAt(fst index,snd index, cell) - {state with ArcFile = state.ArcFile} + Controller.Generic.setCell index cell state + let nextState = {state with ArcFile = state.ArcFile} nextState, model, Cmd.none | UpdateCells arr -> + Controller.Generic.setCells arr state let nextState = - state.ActiveTable.SetCellsAt arr {state with ArcFile = state.ArcFile} nextState, model, Cmd.none | UpdateHeader (index, header) -> diff --git a/src/Shared/ARCtrl.Helper.fs b/src/Shared/ARCtrl.Helper.fs index 28167332..06217c75 100644 --- a/src/Shared/ARCtrl.Helper.fs +++ b/src/Shared/ARCtrl.Helper.fs @@ -105,6 +105,8 @@ module Table = module Helper = + let doptstr (o: string option) = Option.defaultValue "" o + let arrayMoveColumn (currentColumnIndex: int) (newColumnIndex: int) (arr: ResizeArray<'A>) = let ele = arr.[currentColumnIndex] arr.RemoveAt(currentColumnIndex) @@ -134,11 +136,80 @@ module Helper = [] module Extensions = + open Helper open ARCtrl.Template open ArcTableAux + [] + module DataMapIndices = + + [] + let FilePath = 0 + [] + let Selector= 1 + [] + let SelectorFormat = 2 + [] + let Format = 3 + [] + let Description = 4 + [] + let GeneratedBy = 5 + [] + let Explication = 6 + [] + let Unit = 7 + [] + let ObjectType = 8 + type DataMap with + + member this.GetCell(columnIndex: int, rowIndex: int) = + let r = this.DataContexts.[rowIndex] + match columnIndex with + | DataMapIndices.FilePath -> doptstr r.FilePath |> CompositeCell.FreeText + | DataMapIndices.Selector -> doptstr r.Selector |> CompositeCell.FreeText + | DataMapIndices.SelectorFormat -> doptstr r.SelectorFormat |> CompositeCell.FreeText + | DataMapIndices.Format -> doptstr r.Format |> CompositeCell.FreeText + | DataMapIndices.Description -> doptstr r.Description |> CompositeCell.FreeText + | DataMapIndices.GeneratedBy -> doptstr r.GeneratedBy |> CompositeCell.FreeText + | DataMapIndices.Explication -> r.Explication |> Option.defaultValue (OntologyAnnotation()) |> CompositeCell.Term + | DataMapIndices.Unit -> r.Unit |> Option.defaultValue (OntologyAnnotation()) |> CompositeCell.Term + | DataMapIndices.ObjectType -> r.ObjectType |> Option.defaultValue (OntologyAnnotation()) |> CompositeCell.Term + | i -> failwithf "Invalid column index for DataMap: %i" i + member this.SetCell(columnIndex: int, rowIndex: int, cell: CompositeCell) = + let r = this.DataContexts.[rowIndex] + match columnIndex with + | DataMapIndices.FilePath -> r.FilePath <- Some cell.AsFreeText + | DataMapIndices.Selector -> r.Selector <- Some cell.AsFreeText + | DataMapIndices.SelectorFormat -> r.SelectorFormat <- Some cell.AsFreeText + | DataMapIndices.Format -> r.Format <- Some cell.AsFreeText + | DataMapIndices.Description -> r.Description <- Some cell.AsFreeText + | DataMapIndices.GeneratedBy -> r.GeneratedBy <- Some cell.AsFreeText + | DataMapIndices.Explication -> r.Explication <- Some cell.AsTerm + | DataMapIndices.Unit -> r.Unit <- Some cell.AsTerm + | DataMapIndices.ObjectType -> r.ObjectType <- Some cell.AsTerm + | i -> failwithf "Invalid column index for DataMap: %i" i + + static member getHeader(columnIndex: int) = + match columnIndex with + | DataMapIndices.FilePath -> CompositeHeader.FreeText "File Path" + | DataMapIndices.Selector -> CompositeHeader.FreeText "Selector" + | DataMapIndices.SelectorFormat -> CompositeHeader.FreeText "Selector Format" + | DataMapIndices.Format -> CompositeHeader.FreeText "Format" + | DataMapIndices.Description -> CompositeHeader.FreeText "Description" + | DataMapIndices.GeneratedBy -> CompositeHeader.FreeText "Generated By" + | DataMapIndices.Explication -> CompositeHeader.Parameter (OntologyAnnotation "Explication") + | DataMapIndices.Unit -> CompositeHeader.Parameter (OntologyAnnotation "Unit") + | DataMapIndices.ObjectType -> CompositeHeader.Parameter (OntologyAnnotation "Object Type") + | i -> failwithf "Invalid column index for DataMap: %i" i + + member this.GetHeader(columnIndex: int) = + DataMap.getHeader(columnIndex) + static member ColumnCount = 9 + member this.ColumnCount = DataMap.ColumnCount + member this.RowCount = this.DataContexts.Count type DataFile with member this.ToStringRdb() =